State and memory
Remembering things between scenes: globals, person variables, aliases, cooldowns, buildings and time.
A scene forgets its local variables when it ends. Anything that must last goes into one of the stores below, all of which are saved with the game.
| Store | Holds | Use it for |
|---|---|---|
Person variables Actor:setVar |
numbers, text, true/false, per person | Anything about someone: "met the bouncer", a nickname, a counter |
Story globals Game:setGlobal |
numbers or text | Facts about the world: a storyline's stage, a setting |
Actor aliases Game:setActorAlias |
a person | "The bouncer" of your storyline, reachable from any scene |
Actor tags Actor:setActorVar |
a mark on a person | The same, found with Game:getPerson(tag) |
Building aliases Game:generateBuildingAlias |
a building | "The hotel" a quest happens in |
Cooldowns Game:timeout |
a timer per id | Keeping scenes and actions from repeating |
Globals
luaGame:setGlobal("ynl_story_stage", 2)
local stage = Game:getGlobal("ynl_story_stage") -- 0 if never set
Game:clearGlobal("ynl_story_stage")
A global holds a number or a text (Game:setGlobal("ynl_bar_name", "The Anchor")); true/false are stored as 1/0 and nil clears it. An unset global reads as 0, so design keys whose 0 means "not started" or "default". Numbers are single precision: keep counters and ids below 16 million.
Person variables
See People → Memory. Prefix every key with your mod id.
Aliases for people
lua-- scene 1
local bouncer = Game:generatePerson(true, "athletic")
bouncer:makePermanent()
Game:setActorAlias("ynl_bouncer", bouncer)
-- any later scene
local bouncer = Game:getActorAlias("ynl_bouncer")
if bouncer and bouncer:isValid() then
bouncer:show()
end
getActorAlias returns nil if the alias was never set or the person is not in the world right now.
Buildings
A building alias names one building of a venue type and becomes a global your scenes can use by name:
luaGame:generateBuildingAlias("YnlClub", "nightclub") -- picks a nightclub in this city
YnlClub:addObjective("ynl_meet_dealer") -- offer this action there
| Method | Effect |
|---|---|
alias:addObjective(actionId) |
Make a quest action (an action with WHERE = { "quest" }) available at this building |
alias:removeObjective(actionId) |
Take it away again |
alias:hasObjective(actionId) |
Is it offered there |
alias:getLocation() |
The venue tag it points to |
alias:isValid() |
Was the alias generated |
alias:isSameBuilding(other) |
Do two aliases point to the same building |
alias:getBuildingId() |
The one building it stands for ("London:x:y"), "" until the map has picked one |
alias:isHere() |
Is the player in that building |
alias:pinHere() |
Bind the alias to the building the player is in ("the bar we met in") |
alias:pinTo(buildingId) |
Bind it to any building of the city by id, such as the cemetery a dead person lies in (ref:grave().building) |
alias:getName() |
The building's real name (an OSM pub name) or its venue type's |
An alias is one building, not every building of its type: the first time the city map is loaded it picks one of them, keeps it and saves it. Objectives are then offered only there, and the quest panel names it and opens it.
currentBuilding is always available: the building the player is in. Aliases survive the save and are re-published as globals at the start of every scene. See Quests.
Game:findNearbyBuilding("bar", "pub") returns one of those venue types near the player.
Cooldowns
luaGame:timeout(48, "ynl_dealer_call") -- start a 48-hour cooldown
if not Game:isTimingOut("ynl_dealer_call") then
-- the cooldown is over (or was never started)
end
A scene's own name as the id blocks it from the random roll (Scenes); an action's id does the same for the action. Any other id is yours to test. Durations are scaled by the player's scene-frequency setting.
Time
| Call | Returns / does |
|---|---|
Game:passTime(min, max) |
Advance the clock by a random number of hours in that range (one argument: exactly that much) |
Game:GetHoursPassed() |
Game hours since day 0, for measuring durations yourself |
Game:GetDate(hours) |
{ day, month, year, weekday, hour, minute, text } for a moment (nil = now; weekday 0 = Sunday) |
Game:FormatDate(hours, short) |
"10 August 2026", or "10 Aug 2026" when short is true |
Game:GetBuiltin("TimePassed") |
Hours since this scene started |
Measuring something over game time:
lua-- when it starts
Player:setVar("ynl_hangover_since", Game:GetHoursPassed())
-- later
local hours = Game:GetHoursPassed() - (Player:getVar("ynl_hangover_since") or 0)
if hours > 12 then
-- it has worn off
end
Story memory (Memory)
A long story is scenes that throw earlier choices back at the player: "you told them you were single", "the night you didn't ask". Memory (a ul_Base library, so always loaded) is a notebook for that: a key, a short value and when it was noted, so a later scene can say "three weeks ago" without doing the arithmetic.
luaMemory.note("ynl_lie", "single") -- remember a value (text, number or true)
Memory.has("ynl_lie") -- was it noted?
Memory.text("ynl_lie") -- "single", or nil
Memory.is("ynl_lie", "single")
Memory.ago("ynl_lie") -- "last night", "three days ago", "a month ago"
Memory.when("ynl_lie") -- the game hour it was noted
Memory.forget("ynl_lie")
Memory.count("ynl_") -- how many of your keys are set
-- one line of prose, chosen by what was remembered
Game:Narrate(Memory.pick("ynl_lie", {
single = "'Single, tonight at least.' I remember.",
taken = "You said you were spoken for.",
}, "You never did say."))
Per-person memories go on the person and come back with them: Memory.about(p, "ynl_kissed", true) writes, Memory.about(p, "ynl_kissed") reads, Memory.aboutWhen(p, key) gives the hour. Prefix keys with your mod id; Memory never opens a scene or changes a stat.
ul_Undertow is the worked example: nine long scenes where the bar remembers the lie, the dinner remembers the rooms you looked in, and the weekend reads all of it.
Money
Move the player's money through the Ledger, not the money stat: Ledger:pay(mod, key, amount, memo) and Ledger:receive(mod, key, amount, memo) show in the money history with your memo, and a payment the cash cannot cover is refused instead of silently clamping (Money and loans). A key works once: a second pay with the same key moves nothing and returns the first result, which is what makes a retried scene safe. Number your keys ("rent_" .. week, or a counter you save).
In a scene that just needs "pay this, earn that", UL.moneyDelta(mod, amount, memo) from ul_Core does it for you: positive receives, negative pays, keys numbered, and a charge bigger than the cash takes what there is. Every official scene uses it.
Amounts in content are in dollars; Ledger:format(amount) (or Game:convertToLocalCurrency(amount)) writes one in the current city's currency, symbol included ("£15").
Settings for your mod
Settings belong to the player, not to a save: Game:setSetting(key, value) keeps them across every game (in Saved/ModSettings.json), and Game:getSetting(key, default) reads one back as the number, boolean or text it was. Prefix keys with your mod's name.
lua-- Lua/nightlife.lua
Nightlife = Nightlife or {}
function Nightlife.bouncerStrictness()
return ModGame:getSetting("ynl.strictness", 50)
end
Game:setSetting(key, nil) forgets one.
The ul_Health module does exactly this, with a settings page in the Mods panel (Mod UI).