Gameplay content

NPC life

How the town lives without the player - which actions NPCs take, which scenes happen to them, and how to declare your mod's scenes for NPCs.

Every adult the game knows lives on their own, whether the player is around or not. A scheduler picks what each one does next from the actions they are allowed to take, weighted by their personality and needs; some of those actions play a scene. When it happens in the room the player is standing in, it is shown: the animation plays, the lines appear as speech bubbles over whoever says them. Elsewhere it resolves offscreen with exactly the same result. The player only watches.

Letting NPCs take an action

An action is player-only until it says otherwise. Add an NPC block to the .ulaction:

luareturn {
	NPC = {
		ENABLED = true,
		BASE = 0.12,                                   -- how attractive it is to anyone
		TRAITS = { extraversion = 0.3, openness = 0.2 }, -- who likes it more (or less, when negative)
		NEEDS = { { STAT = "energy", TARGET = 55, SCALE = 55, WEIGHT = 0.6 } },
		MINIMUM_STATS = { energy = 10 },               -- never picked below these
		SCENE = "book_discussion",                     -- optional: always plays this scene
		SCENES = { "music_discussion", "sing_along" }, -- optional: plays one of these
		REQUIRES_NEARBY = true,                        -- optional: needs someone else there
	},
	ACTION_UNIQUEID = "read_a_book",
	...
}

WHERE, WHEN, CONDITIONS and EFFECTS apply to NPCs exactly as to the player: an NPC only reads at the library if they are at the library, and a meal costs them its price.

Traits are the twelve personality dimensions of ul_Base/SocialSimulation/foundation.ulsocial: openness, conscientiousness, extraversion, agreeableness, emotional_reactivity, empathy, honesty, impulsivity, risk_tolerance, dominance, attachment, vindictiveness.

NPC = false keeps an action player-only on purpose.

NPC field Meaning
ENABLED true to let NPCs take it.
BASE, TRAITS, NEEDS, MINIMUM_STATS As above.
BEHAVIORS Like TRAITS, with the behavior ids of ul_Base/Personality/base.ulpersonality (gossip, spending, helping, risk_taking…).
RISK, PRESSURE_WEIGHT Weights of the world mode's risk bias and of the NPC's money pressure (debt, no home).
SCENE, SCENES One scene, or up to 16 to pick from. Without either, the action's SCENE_ALWAYS plays.
REQUIRES_NEARBY Only when someone else is at the place.
HISTORY, HISTORY_MAJOR A history event type recorded each time an NPC does it, and whether it is major.

Some actions can never be NPC actions, whatever the block says: interactions (WHERE = { "interaction" }), and any action with WHO, WHO_LUA, AVAILABLE, END_PARTY, OUTDOOR or WEATHER, a BUFF effect, an Interacting:, rapportwithplayer or age effect, or a SCENE_ALWAYS (or NPC.SCENE) that no .ulsocial declares. A FURNITURE action is taken only at its FURNITURE_SURE places, and one that costs money only by an NPC who has it.

Declaring a scene for NPCs

A scene never runs for NPCs unless a .ulsocial file under your mod's SocialSimulation/ folder declares it. The scene file itself is unchanged: the same RUN runs with the NPC as Player and the other person as Actor.

json{
  "version": 1,
  "scenes": [
    {
      "scene": "ul_talk_joke",
      "target": "nearby",
      "where": ["cafe", "bar", "pub"],
      "fromHour": 8,
      "toHour": 23,
      "ambient": 1,
      "libraries": ["ul_Core/ul_core.lua", "ul_Core/ul_event_director.lua"],
      "choices": [
        { "point": 0, "index": 0, "base": 0.05, "traits": { "agreeableness": 0.4 } },
        { "point": 0, "index": 1, "base": 0, "traits": { "dominance": 0.6, "agreeableness": -0.4 } }
      ]
    }
  ]
}
Field Meaning
scene The .ulscene file name, without extension.
target "self" (the NPC alone) or "nearby" (someone at the same place becomes Actor).
where, fromHour, toHour Where and when it may happen. "home", "work", "all" and "-tag" work as in WHERE.
actions The NPC actions it hangs on: it may play when an NPC does one of them.
ambient A weight. Instead of hanging on an action, the scene may happen during whatever the NPC is doing, where and when it allows. Use it for the scenes the player meets anywhere (WHAT = { "all" }).
libraries "mod/file.lua" from a mod's Lua/ folder, loaded into the scene's private state first (at most 16). optionalLibraries are skipped when their mod is off.
choices How an NPC answers each Game:Choice. point is the 1-based GetChoice it applies to, or 0 for every one; index is the option. The option with the best base + trait/behavior/need score wins. A choice point with no rule for an available option makes the scene refuse.
maximumParticipants How many times the scene may call Game:generatePerson() (1-4, default 1). In an NPC scene that call takes no arguments and creates nobody: the first call returns Actor, each next one someone else already at the same place. 2 is Actor and one more. When nobody fits, the scene is refused.
targetRole, targetGender, targetWorker, targetJobTitles, targetMinimumStats Who qualifies as Actor. Minors never do. Do not test age in targetMinimumStats: a person the player never met reads 0.
targetFamilyPresent 0-8: how many of the initiator's family must be at the place.
initiatorMinimumVars { "var": minimum } (up to 8): only NPCs whose variables reach these may draw the scene. Use it for scenes that only make sense for a few people, so they are not drawn and refused all day.

How often ambient scenes happen overall is autonomy.ambientSceneChance in ul_Base/SocialSimulation/daily_life.ulsocial (0.1: one action in ten carries one).

Ambient scenes also play in front of the player: while the player stands in a room, every autonomy.watchedSceneMinSeconds–watchedSceneMaxSeconds real seconds (25–50 by default, 0 turns it off) two people standing there play one, with its lines as bubbles over them. A scene whose where names the room's place ("bar") is drawn about forty times more often than an "all" one, so a venue's own scenes are what the player sees. The strangers who play one become people of the town. The people in a scene walk up to each other before the first line.

A scene can declare "targetMayBeBusy": true: then, once per scene playing in the room, someone free may run it on one of the two people talking (autonomy.watchedBusyTargetChance, 0.35). The mark stays in their conversation, the initiator walks up to them, and the scene's own lines over the mark are skipped. Use it for what a distracted person invites (a pickpocket, a spiked drink), and read Actor:isDistracted() to make it more likely to work.

What an NPC scene can do

NPC scenes run in an isolated state and commit as a whole: if anything fails, nothing happens. Stats, variables, relationships, contacts, timeouts and money go through; anything that only makes sense for the player (quests, the HUD, the camera, saving) makes the scene refuse. Game:isNpcSimulation() is true there, for the rare branch that has to differ.

  • Actor:dress(), strip(), undress() and animate("…"), Game:AnimatePair(a, b, tag), Game:Say and Game:Narrate are presentation: shown on the bodies when the player is in the room, nothing otherwise. A stripped NPC dresses again from their own wardrobe.
  • Game:Sex(a, b) between NPCs is one outcome, not a session: ul_Base's npcSex rules (condom, conception, STD, history) decide what follows, and one generic clip plays if the player is there. Game:Filter(...) picks that clip.
  • Actor:isInterestedIn(other) reads the orientation each person was generated with.
  • Game:LoseDating(person): the initiator and that person split up (dating and marriage become an ex), published with the rest of the scene.
  • Witness works as in a player scene, recorded when the scene succeeds; knownBy rows add who and role (Witnesses).
  • Actor:getVar(k) is nil when unset, as in a player scene (getActorVar and Game:getGlobal stay 0). Library code that registers listeners with Events:on loads fine: the registration is ignored, an NPC scene never fires events.
  • getSpecific / getRelatedPerson answer an invalid person for the player-only lists an NPC has nothing in (Coursemate, Prostitute, Doctor, Poly...), so the scene declines as it would for anyone without one. ExDating is the NPC's exes, Relative any adult relative.

Shared world state

In an NPC scene, Game:getGlobal and Game:setGlobal belong to the NPC: each person keeps their own copy. A mod whose state the whole town shares (an election, a city's grievances) declares its prefixes once in any of its .ulsocial files:

json{ "version": 1, "worldGlobals": ["pol_", "ch_"], "scenes": [ ... ] }

Globals starting with those prefixes are then read from the game's own state in NPC scenes, and their writes are published with the rest of the scene when it succeeds (a refused scene changes nothing). If another scene changed a value this scene read before it finished, the scene is refused and tried again later.

Life choices

Once a day each NPC also decides how to get through it: ask someone for help, steal, extort, pick a fight — or one of the life choices mods declare in a .ulsocial lifeChoices array (ul_Base/SocialSimulation/dark_life.ulsocial ships selling sex, using and dealing drugs, and forcing someone). Every option is scored and the highest wins, so a choice only happens to the people whose circumstances and personality push them there.

json{
  "lifeChoices": [
    {
      "id": "use_drugs",
      "history": "npc_used_drugs",
      "cashMin": -25,
      "base": -1.3, "pressure": 0.9, "risk": 0.3,
      "dependence": "drug_dependence", "dependenceGain": 8, "dependenceWeight": 1.8,
      "behaviors": { "substance_use": 0.9, "impulsivity": 0.3 },
      "effects": { "mood": 12, "energy": -6 }
    }
  ]
}
Field Meaning
id Reusing an id replaces that choice.
history The history event recorded (default npc_<id>; "" records none).
base, pressure, risk, needBonus Score: base + pressure × their debt/homelessness pressure + risk × the world mode's risk + needBonus when short of money.
behaviors Personality behavior biases and their weights.
requiresNeed Only when they are short of money.
gender, targetGender "male" / "female" for who may choose it and with whom.
cashMin, cashMax Money gained (positive, a random amount in the range) or spent (negative; never chosen without it).
targetPays Someone at the same place pays it (they must have it). Any of targetPays, sex or targetEffects makes the choice need that person.
sex "consensual" or "forced": goes through the npcSex rules (condom, conception, STD, history).
dependence, dependenceGain, dependenceWeight A stat this choice feeds, and how much that stat pulls them back to it: addiction is a loop.
drive, driveMinimum, driveWeight A stat that has to be high enough, and pushes harder the higher it is.
effects, targetEffects { "stat": delta } on them and on the other person.

People who come and go

A room is not a photograph. While the player stands in a venue, the room keeps time (ABirdviewPawn::ReconcileRoomPeople, every few real seconds):

  • Arrivals. Anyone whose day now brings them here (their routine, their schedule, their home or work in this building) walks in by the street door and joins the room's life. The player's partner comes home in the evening; the children come back from school; a regular turns up at the bar.
  • Departures. Anyone whose day now takes them elsewhere walks to the door and leaves. Someone talking with the player, in a scene, or serving at the counter waits until they are free.
  • Strangers turn over on their own: every turnover_minutes of game time (Animations/ambient.json, arrivals) one leaves and, below the room's cap, a new one arrives. Homes have no strangers (home_tags).
  • Capacity. Map/venue_capacity.json says how many people a venue of each tag holds (default 10). A routine that sends someone to a full venue is refused (AI:goTo fails that script and the next one runs: they do something else), and a loaded room past its cap sends an arrival away the same way. Today a tag is every venue of that kind in the city, so the cap is per kind of venue, not per door.

The other rooms

People use the venue's other rooms (GENERATE.ROOMS of the recipe): the room move of room life sends someone to the bathroom, the bedroom, the toilets, the kitchen for a few minutes of game time, by the rooms block of Animations/ambient.json:

json"rooms": {
  "bathroom": { "weight": 3, "minutes": [8, 25], "purposes": ["bathe", "toilet", "change"], "private": true, "adult_lock_chance": 0.35 },
  "kitchen":  { "weight": 3, "minutes": [5, 20], "purposes": ["cook", "snack"], "private": false }
}

A purpose that names an activity (bathe, toilet) is played there; any other word is only what the event below reports. A private room has a door state: shut, or locked. An adult locks it adult_lock_chance of the time. A minor in a private room always locks it: this is an engine rule (ALifeRoom::IsDoorLocked), a mod cannot change it, and nothing about what is behind that door is ever said. A click on a locked door raises doorLocked and goes nowhere.

An unlocked door opened on someone doing something private raises walkedIn: the engine plays the occupant honestly (the shower keeps running); what the two make of it is mod content (ul_OpenDoors ships the first scenes).

luaEvents:on(M, "walkedIn", function(who, onWhom, room, purpose, venue)
	-- who opened the door of `room` ("bathroom") where onWhom was `purpose` ("bathe", "toilet",
	-- "change", "rest"...). Ids as strings; the player's id when the player is one of them.
end)
Events:on(M, "doorLocked", function(room, occupant, purpose)
	-- the player tried a locked door; occupant may be "" when nobody known holds it
end)

Footsteps

Everyone who walks makes a step every stride_cm (footsteps in ambient.json), played from where they are with distance attenuation: someone crossing behind you is loud, someone at the far end of the bar is faint. files are globs under any mod's Audio/ folder (Steps/hard_*.wav); soft_files play in the rooms and venues named by soft_rooms (carpet). Ship your own set in your mod's Audio/Steps/ and name it in your ambient.json.

City levers

The simulation reads a few multipliers from globals any mod can set with Game:setGlobal (1 when unset, clamped to 0-10). They act from the moment they change. ul_CityHall's policies write them; another mod (a disaster, a festival, a strike) can too.

Global What it multiplies
city_rent the rent everyone pays, and what a home costs when someone looks for one
city_wage income from work
city_jobs the chance an application gets a job
city_healthcare_cost the price of treatment for an illness
city_disease the chance of catching an illness
city_shelter divides deaths from exposure and starvation on the street
city_police lowers (above 1) or raises (below 1) the risk bias of every risky choice

Checking your scenes

From the project, python Tools/SocialSimulation/audit_scene_compatibility.py lists every scene, what blocks it for NPCs and which libraries it needs. -NpcSceneRun=a,b runs chosen scenes twenty times between two NPCs, with -NpcSceneGlobals=key=value;key=value setting story globals first ({A} and {B} stand for the two NPCs' ids), for scenes that need a state of the world to run. The -NpcSceneSweep harness then runs every candidate for real, and -NpcSoak -SoakDays=150 on the world map lets a hundred people live a season and reports what they did.