Gameplay content

Actions

Things the player can do at a place or with a person, their cost in time, their effects, and the scenes they lead to.

An action is anything in the player's action list: Drink coffee, Work out, Flirt, Take photos. Each is one .ulaction file, anywhere in your mod. The folder it sits in is its category in the list: Actions/fitness/jog.ulaction is listed under fitness.

luareturn {
	ACTION_UNIQUEID = "ynl_order_shot",
	ACTION_NAME = "Order a shot",
	WHERE = { "bar", "pub", "nightclub" },
	WHEN = { 18, 4 },
	MINUTES = { 5, 15 },
	TIMEOUT_MINUTES = 20,
	ALSO_TIMEOUT = { "ynl_order_beer" },
	EFFECTS = {
		{ STAT = "money", AMOUNT = -4, TRIGGER = "START" },
		{ STAT = "ynl_drunk", AMOUNT = 15, TRIGGER = "END" },
		{ STAT = "mood", AMOUNT = 2, TRIGGER = "END" },
	},
	CONDITIONS = {
		{ STAT = "money", COMPARATOR = ">=", VALUE = 4 },
		{ STAT = "age", COMPARATOR = ">=", VALUE = 18 },
	},
	SCENE_ALWAYS = "",
}

Fields

Field Meaning
ACTION_UNIQUEID The id. Scenes list it in WHAT; a later mod with the same id replaces the action.
ACTION_NAME Text in the action list.
WHERE Venue tags where it is offered (see Map and venues). "all" with "-tag" exclusions, "interaction" for interactions, "quest" for quest objectives.
WHEN { from, to }: hours it is offered. { 18, 4 } wraps past midnight; { 0, 24 } is always. Both ends excluded.
MINUTES { min, max }: how long it takes; the clock advances by a random amount in that range when it completes.
TIMEOUT_MINUTES After completing, the action is not offered again for this long. 0 = no cooldown.
ALSO_TIMEOUT Other action ids (or scene names) that get the same cooldown.
EFFECTS Stat changes, see below.
CONDITIONS Stat requirements on the player, see below.
WHO_LUA For actions aimed at a person: which people it is offered for, see interactions.
SCENE_ALWAYS A scene name to run every time the action completes, instead of the random roll. Several names separated by commas ("photoshoot, photoshoot_nude") are tried in random order until one does not return false.
GROUP Optional. The heading it is listed under: one of the action groups ("needs", "work", "errands", "social", "romance", "leisure", "self", "underworld", "stories") or one your mod adds. Without it, the group whose FOLDERS names the action's folder; failing that, "leisure" (and a line in the log).
CAREER Optional. The career the action belongs to ("sexwork", "crime", "modeling" or one your mod adds). A career action is listed under the Careers entry wherever the player stands, never in a venue's own list. Without it, the career whose FOLDERS names the action's folder, if any.
PRIORITY Optional number, default 0. Order inside its group: higher first, then by name.
AVAILABLE Optional. The name of a function in one of your libraries that says whether the action can be taken now, and why not. See below.
HIDE_IF_LOCKED Optional, true: while the action is not possible it is not listed at all, instead of greyed out with its reason. For secret or stat-gated content.
END_PARTY Optional, true: when the action completes, everyone with the player (the party) goes their own way. ul_Base's sleep sets it.
TARGET_OUT Optional, true: the action is offered on someone unconscious or dead. Every other person action is not listed for them (you cannot chat up a body on the floor). ul_Unconscious puts its actions on an outcold intent.
OUTDOOR Optional, true: refused while the weather is one with outdoor = false (rain, storm). See Weather and seasons.
WEATHER Optional. { "clear", "heatwave" }: only in those weathers; { "-fog" }: never in those.
FURNITURE Optional. { "washer", "laundry_machine" }: offered only where the room has a piece whose name holds one of these words (a washing machine for do_laundry).
FURNITURE_SURE Optional, with FURNITURE. { "laundry" }: places that always have that piece, where it is not looked for. NPCs, who never load a room, do the action only there.
INTENT Optional, for interactions: the petal of the person ring it is listed under ("talk", "interaction"…), see the person ring.
CONVERSATION Optional, for interactions. { MIN = 3 }, { MIN = 2, MAX = 4 }: how many people the conversation must hold, the player and the clicked person included. See conversations.
PRIVATE Optional, true: in someone else's home (residential) the action is offered only when the player is welcome there: a resident present is in the party, is their partner, spouse, family or housemate, or invited them (Actor:inviteHome). Otherwise it is not listed. ul_Base's bathe, sleep, nap, eat and drink at home set it.

Action groups

Every action list (a venue's, a person's menu, the phone) is shown the same way: up to three recommended actions, then what can be done now under its group headings, then a "Not now" section with the reason for each, the ones that only need waiting (time of day, weather, cooldown) first. A HIDE_IF_LOCKED action is not listed while locked.

The headings come from .ulgroups files, anywhere in a mod (ul_Base/Actions/ul_base.ulgroups has the base ones). A file returns a list:

luareturn {
	{ ID = "gang", NAME = "The gang", ICON = "gang", ORDER = 85,
	  FOLDERS = { "gang" } },
}

ORDER places the heading among the others (base groups use 10 to 90). FOLDERS lists folder names whose actions default to this group when they have no GROUP. An entry with an existing ID changes that group instead of adding one: only the fields it sets change. Keep new groups rare; a short list of headings is what keeps the menus readable.

A group can also list its actions by id with ACTIONS = { "ul_bar_drink", ... }. That list wins over the action's own GROUP and over its folder, so a mod can move another mod's action into a group without editing it. ACTIONS lists add up across mods. On the map, a venue's groups are the petals of the ring around the building (base ids: food, shopping, social, romance, leisure, sport, needs, self, home, errands, work, underworld, stories).

Phone apps

The phone shows apps from .ulapps files (ul_Base/Actions/ul_base.ulapps has the base ones):

luareturn {
	{ ID = "bank", NAME = "Bank", ICON = "Locations/bank", COLOR = "#1F8F8A", ORDER = 50 },
	{ ID = "bank.bills", APP = "bank", NAME = "Bills & loans", ORDER = 20,
	  ACTIONS = { "ul_bill_pay", "ul_finance_borrow" } },
}

An entry without APP is an app: ICON is <Map/Icons group>/<name>, and it has ACTIONS, a SCREEN (an engine page shown first: messages, alerts, contacts, agenda, money, careers), or both. An entry with APP = "<app id>" is a tab of that app, shown as a pill over its list. ACTIONS add up across mods; for any other field the last mod wins. Keep the phone to eight apps: it is one screen. Add a tab or actions to an existing app before adding an app.

An action that can be done from the phone (WHERE = { "phone" } or { "all" }) and that no app lists appears under the apps, filtered by where the player is. Entries with APP = "menu" are not on the phone: their actions are the pause menu's My life page.

Careers

A career is a line of work that is not about one venue. The rail's Careers entry lists each career that has anything to offer, with its actions organized like any other list. Careers come from .ulcareers files, same format as .ulgroups:

luareturn {
	{ ID = "smuggling", NAME = "Smuggling", ICON = "smuggling", ORDER = 40, FOLDERS = { "smuggling" } },
}

A mod adds a career this way; it cannot add its own entry to the rail's top section. A whole new system with its own screens belongs in a HUD script menu instead, which the rail lists under its Mods entry.

Effects

luaEFFECTS = {
	{ STAT = "energy", AMOUNT = -10, TRIGGER = "START" },
	{ STAT = "fitness", AMOUNT = 0.5, TRIGGER = "END" },
	{ STAT = "Interacting:rapportwithplayer", AMOUNT = 2, TRIGGER = "END" },
},
  • STAT: any stat id. Prefix with Interacting: to change the stat of the person the action is aimed at instead of the player's.
  • AMOUNT: added to the stat (negative to remove). Scaled by difficulty for stats marked HIGHER_BETTER.
  • TRIGGER:
    • START: when the action begins.
    • END: when it completes.
    • DURATION: spread over the action as its bar fills, so an action stopped halfway gives half.
    • INTERRUPTED: when the player stops it before the end.
    • BUFF with BUFF_MINUTES = n: added when it completes and taken away n game minutes later. Doing the action again stacks. A buff on the player is kept in saves with the time it has left; one with no BUFF_MINUTES lasts until the game is loaded.

An entry with KNOWLEDGE instead of STAT teaches a node of the knowledge tree: AMOUNT is hours of study or practice (the node's study hours take its score from 0 to 75), per hour of the action with DURATION, once with START or END. A node whose requirements are unmet is skipped silently.

lua{ KNOWLEDGE = "first_aid", AMOUNT = 1, TRIGGER = "DURATION" },
note

MOVE_FIRST = true makes the player walk to the person the action is aimed at when it starts. ANIMATION is read but not used yet.

Conditions

luaCONDITIONS = {
	{ STAT = "fitness", COMPARATOR = ">=", VALUE = 40 },
},

Every condition must hold for the action to be offered. Comparators: >=, <=, >, <, ==, !=. They test the player's stats. For anything more complex, offer the action and decide in a SCENE_ALWAYS scene.

Add HIDES = true to a condition that says whether the action is for the player at all, rather than whether it can be done right now: hired for this job, a member of the club, owns the van. When such a condition fails, the action is not listed, instead of being shown under "Not now":

lua{ STAT = "nurse_hired", COMPARATOR = ">", VALUE = 0.5, HIDES = true },

A condition with TYPE = "KNOWLEDGE" asks the knowledge tree instead of a stat: the node, and the score out of 100 it must have reached (SCORE, default 25; LEVEL is accepted as the same number). It refuses with "Needs Anatomy and physiology 50 (Competent)."; HIDES works the same way.

lua{ TYPE = "KNOWLEDGE", KNOWLEDGE = "anatomy", SCORE = 50, HIDES = true },

When an action is offered

At a place, the list shows every action whose WHERE matches the venue, whose WHEN contains the current hour, which is not on cooldown, and whose conditions pass.

In the Actions panel, an action that cannot be taken is listed under "Not now", below the ones that can, with the reason: "Only between 18:00 and 02:00.", "Again in 3 h.", "Needs Money of at least 25.", "Choose someone first.", or your own.

Refusing with a reason

Conditions only compare stats. When the rule is your mod's own (a diary that is full, a reply still pending, a contact who is no longer available), name a function with AVAILABLE instead of starting the action and refusing inside its scene, after its minutes were spent:

lua-- Actions/phone/ynl_book.ulaction
	AVAILABLE = "Nightlife.canBook",

-- Lua/ynl_nightlife.lua
Nightlife = Nightlife or {}
function Nightlife.canBook(Game, Player, Target, actionId)
	if Nightlife.bookings() >= 3 then
		return false, "You already have three tables booked."
	end
	return true
end
  • It receives Game, Player, the person the player is aimed at (nil when none) and the action id, and returns true, or false and the reason shown to the player.
  • It is checked after CONDITIONS, WHO_LUA, WHEN and the cooldown, so it only has to say what those cannot.
  • It is called whenever the list is drawn, again right before the action starts, and once more right before it completes: an action whose reason appears while it runs ends as interrupted, with the reason on screen, and its scene does not run. Do not change anything in it: it must only read.
  • A name that is not a loaded function, or a function that raises an error, refuses the action ("Not possible right now.") and logs why.

What happens when it completes

  1. END effects are applied and the clock advances by MINUTES.
  2. The cooldowns start (TIMEOUT_MINUTES, ALSO_TIMEOUT).
  3. Then exactly one of:
    • a scene queued by Game:followUp in the previous scene runs; otherwise
    • the SCENE_ALWAYS scene runs; otherwise
    • the random roll picks a scene whose WHAT lists this action.

SCENE_ALWAYS is how an action is a scene: "Throw a house party", "Ask for a raise". Give the scene an empty WHAT so it never fires on its own.

Interactions

When the player clicks a person, the menu lists interactions: actions with WHERE = { "interaction" }.

luareturn {
	ACTION_UNIQUEID = "ynl_buy_a_drink",
	ACTION_NAME = "Buy them a drink",
	WHERE = { "interaction" },
	WHEN = { 17, 4 },
	MINUTES = { 10, 20 },
	TIMEOUT_MINUTES = 120,
	EFFECTS = {
		{ STAT = "money", AMOUNT = -6, TRIGGER = "START" },
		{ STAT = "Interacting:rapportwithplayer", AMOUNT = 3, TRIGGER = "END" },
	},
	CONDITIONS = {},
	WHO_LUA = [=[return not Actor:isTemporary() and Game:GetCurrentLocation() ~= "home"]=],
	SCENE_ALWAYS = "ynl_buy_a_drink",
}
  • WHO_LUA is the body of a function that receives Game, Player, CurrentCompanion and Actor (the clicked person) and returns whether the interaction is offered for them. Leave it out to offer it for everyone.
  • The [=[ … ]=] brackets are a Lua long string, so the code can contain quotes.
  • In the SCENE_ALWAYS scene, Actor (and Game:getTarget()) is the clicked person.
  • Interacting: effects change that person's stats.

An ordinary (non-interaction) action can also have a WHO_LUA: it is then only offered while the player is aimed at someone who passes it.

The person ring

Clicking a person opens a ring of petals around them, one per intent that has something to offer; a petal opens its actions in the panel beside the ring. The intents come from .ulintents files, same format as .ulgroups, plus TRAIT_GROUP (the personality group whose glyph the petal shows) and COLOR (its neon, sRGB hex). ul_Base/Actions/ul_base.ulintents has talk, give, together, romance, desire, money, boss, hostile and tools; ul_Interactions adds interaction (gestures played on the bodies with Actor:Interaction, from a hand on the arm to a hand round the throat).

lua-- ul_Interactions/Actions/ul_interactions.ulintents
return {
	{ ID = "interaction", NAME = "Interaction", DESCRIPTION = "Touch, hold, kiss, grab, hit", ORDER = 35,
	  TRAIT_GROUP = "bonds", COLOR = "#22D3C5", ACTIONS = {} },
}

An action lands in a petal by, in this order: its own INTENT, an intent whose ACTIONS lists it, its GROUP in an intent's GROUPS, its folder in an intent's FOLDERS, and failing all of that the first intent (talk, with a line in the log). Any interaction you add later with INTENT = "interaction" shows up in that petal with nothing else to change. An entry with an existing ID appends its ACTIONS to that intent's.

Conversations

The game knows who is talking with whom in the room on screen: two people chatting in room life (a third who walks over joins them), the people of a scene the room is playing, and the player once they step in. CONVERSATION counts the conversation the player would be in by speaking to the clicked person: that person, whoever they are talking with, and the player. Speaking to one of two people chatting at the bar is three.

lua-- ul_MultiTalk/Actions/ul_mt_toast.ulaction
	CONVERSATION = { MIN = 4 },

Out of bounds, the action is not listed; with HIDES = false it is listed under "Not now" with "Needs at least 4 people in the conversation.". While the player has someone in a chat clicked, the others in that chat wait for them instead of wandering off.

In the scene, Game:getConversation() is everyone in it but the player and Game:conversationSize() the count CONVERSATION used. Game:joinConversation() makes the player one of them: they stay where they are, turn to the player, and a scene the room was playing with them ends. The player leaves with Game:leaveConversation(), or by walking a few metres away. Actor:getConversation() / Actor:conversationSize() are the same for any person (an empty list and 1: talking with nobody), Actor:joinConversation(Other) and Actor:leaveConversation() move NPCs between conversations.

luaRUN = function(Game, Player, CurrentCompanion)
	Game:SceneStart()
	Game:joinConversation()
	for _, p in ipairs(Game:getConversation()) do
		p:ModifyStat("rapportwithplayer", 1)
	end
	Game:Narrate("Every glass comes up.")
	Game:SceneEnd()
end,

Organising actions

  • The category comes from the folder name, so keep related actions together: Actions/nightlife/.
  • The base game's categories include artistic, basicneeds, beauty, crime, entertainment, fitness, housework, intellectual, outdoors, phone, produce, sexwork, shopping, social, work. Reuse one when your action fits it.
  • Before inventing an action, check whether an existing one only lacks your venue in its WHERE: extending a list (by overriding the action) keeps effects consistent.