Unique scenes cookbook
Four recipes for authoring scenes that reflect a girl’s personality, her current situation, and (where one specific girl is involved) her name. All examples use real, shipped syntax — compare against resources/jobs/ and resources/packages/Sample_Full/.
For the complete predicate list, see reference/when-conditions.md. For full job structure see reference/jobs-reference.md.
Scope. This cookbook covers what’s pack-authorable today — XML + the
<When>engine, plus optional per-girl Lua overrides. Long arcs (“after 30 in-game days…”) aren’t pack-authorable yet; see “Known gap” at the bottom.
Recipe 1: Personality-gated shift text
Section titled “Recipe 1: Personality-gated shift text”When to use this shape: you want a job to read differently depending on the girl’s traits or stats. A Shy girl bartending should sound different from a Charming one. The base text bag still fires for everyone else.
Exemplar: resources/jobs/barmaid/messages/work.xml
Inside any text bag, attach a <When> block. Variants with a matching <When> are eligible alongside the unconditional ones; the engine picks among the eligible bag entries by weight=.
<Text id="barmaid.work.perfect.psychic" weight="3"> <When> <Performance ge="245"/> <Trait id="Psychic"/> </When></Text>
<Text id="barmaid.work.great.beauty" weight="3"> <When> <Performance ge="185" le="244"/> <Stat name="Beauty" ge="85"/> </When></Text>A bare <When> with multiple children means all must match (implicit AND). The first variant fires only when the girl scored a “perfect” shift and has the Psychic trait. The second wants a “great” shift and Beauty ≥ 85.
Authoring rule: keep at least one unconditional variant in each performance bucket, so a girl who matches no gate still has something to say.
Recipe 2: Multi-trait personality blend
Section titled “Recipe 2: Multi-trait personality blend”When to use this shape: the line should fire when she has any of a related cluster of traits, or when several conditions all need to hold.
Use the combinators from reference/when-conditions.md:
<!-- Either Charming or Seductive triggers this line --><Text id="streetwalker.work.flirt" weight="2"> <When> <Any> <Trait id="Charming"/> <Trait id="Seductive"/> </Any> </When></Text>
<!-- Shy girl, working at night, sober: a specific personality moment --><Text id="barmaid.work.shy_night" weight="2"> <When> <Trait id="Shy"/> <DayNight value="night"/> <None> <Status id="Pregnant"/> <Status id="Drugged"/> </None> </When></Text><Any> = OR. Implicit AND when children sit directly under <When>. <None> = “none of these” (sugar for not-any).
Recipe 3: A scene for one specific named girl
Section titled “Recipe 3: A scene for one specific named girl”When to use this shape: you’ve written a unique character and want lines that only she can roll. This is the closest thing to “Sarah’s bespoke scene” — a 1.15 addition.
Exemplar — defining the girl: resources/packages/Sample_Full/Girls.girlsx
<Girls> <Girl Name='Sample Girl' Desc='A cheerful young woman with a quick wit and a quicker smile.' Charisma='40' Beauty='50' Confidence='50' Spirit='60' Age='22' Status='Slave'> <Trait Name='Quick Learner'/> <Trait Name='Optimistic'/> </Girl></Girls>The bespoke text in a job’s messages/work.xml:
<Text id="barmaid.work.signature.sample_girl" weight="5"> <When> <Girl name="Sample Girl"/> <Performance ge="185"/> </When></Text>Then in text/en.xml:
<Text id="barmaid.work.signature.sample_girl"> Sample Girl winked at every regular by name, and the tips landed before the drinks did.</Text><Girl name="..."/> matches the girl’s display name exactly. Combine with <Trait>, <Stat>, <Performance> to scope further — “this scene plays only for Sarah, only when she’s Pregnant, only on a great shift” is one nested <When>.
Pair this with the UniqueRegistry (1.15+) which guarantees at most one live Sarah at a time, so the bespoke content never feels duplicated.
Recipe 4: Reacting to a life change (status flags)
Section titled “Recipe 4: Reacting to a life change (status flags)”When to use this shape: her situation has changed — pregnancy, slave status, drugged, poisoned — and the scene should acknowledge it.
<!-- Pregnant girl tending bar --><Text id="barmaid.work.pregnant" weight="2"> <When> <Status id="Pregnant"/> </When></Text>
<!-- Slave with low Spirit cracking a real smile --><Text id="barmaid.work.spirit_breakthrough" weight="3"> <When> <Status id="Slave"/> <Stat name="Spirit" le="30"/> <Performance ge="185"/> </When></Text><Status> covers run-time conditions the engine tracks per girl. <Stat> reads any of her current stats with ge/le/gt/lt/eq. Compose freely.
Recipe 5: Per-girl interaction script (Lua override)
Section titled “Recipe 5: Per-girl interaction script (Lua override)”When to use this shape: the <When>-gated text bag isn’t expressive enough — you want a branching conversation tree, custom buttons, or stat changes triggered by player choices.
Exemplar: resources/packages/Sample_Full/Characters/MeetGirl.lua and docs/modding-and-packages/interactions.md.
In the girl’s XML, point TRIGGER_TALK at your custom script. From inside that Lua file you can:
- Branch on
wm.girl.traits,wm.girl.stats,wm.girl.lifetime.*(acts, pregnancies, wear tier, broken-threshold flag) - Show choices, mutate stats, grant items, push other screens
Use this surface when the XML <When> engine runs out — typically: conversation trees, mini-quests, anything that needs player choice mid-scene. For passive shift narration, prefer Recipes 1–4.
Recipe 6: A meet scene whose choices change her stats
Section titled “Recipe 6: A meet scene whose choices change her stats”When to use this shape: you’ve written a unique girl and you want the way the player recruits her to leave a mark — a girl pressured into the work arrives demoralised; one treated with respect arrives emboldened. This is the per-girl Town meet (Type="Meet" Where="Town"), and it’s a step up from Recipe 5: the player’s choice doesn’t just branch the text, it writes back to her stats.
Exemplar: resources/packages/Tifa_MeetExample/ — a complete, self-contained example pack (scripts only, no art).
The one rule that makes it work
Section titled “The one rule that makes it work”Assigning to a stat field does nothing on its own:
wm.girl.confidence = wm.girl.confidence - 10 -- edits a throwaway copyYou must call wm.girl:update() afterward — colon, not dot (the colon passes the girl as self; wm.girl.update() with a dot errors). update() diffs your Lua values against the engine’s and applies the differences:
wm.girl.confidence = wm.girl.confidence - 10wm.girl.spirit = wm.girl.spirit - 5wm.girl:update() -- now it's realForgetting update() is the single most common reason a stat edit “doesn’t work.”
A meet has three possible consequence layers
Section titled “A meet has three possible consequence layers”A meet scene can pull any of these; most scenes use one or two:
| Layer | What it does | How |
|---|---|---|
| Recruit chance | the choice changes the odds she joins | add to the chance number in your acceptance roll |
| Her stats | the encounter changes who she becomes | wm.girl.<stat> = … then wm.girl:update() |
| Her memory | how she was recruited colours later behaviour | not pack-authorable yet — see “Known gap” |
Naming trap. The shipped
MeetTownDefault.luanames its recruit-chance knobsspirit_mod/confidence_mod. Despite the names, those add to the chance, not to the girl’s spirit or confidence. They are plain fields the script reads — not an engine feature. In your own script, name a chance modifier something honest likechance_bonusso you don’t conflate the two layers.
The pattern
Section titled “The pattern”A helper that applies a set of deltas and syncs once:
local function clamp(v) if v < 0 then return 0 end if v > 100 then return 100 end return vend
-- deltas e.g. { confidence = -5, spirit = -10, pc_fear = 15 }local function apply_stat_changes(deltas) for field, delta in pairs(deltas) do wm.girl[field] = clamp((wm.girl[field] or 50) + delta) end wm.girl:update() -- the line that makes the changes realendEach approach carries both its chance modifier and the stat marks it leaves:
local approaches = { { caption = "Treat her as a fighter -- offer respect", honest = true, chance_bonus = 10, on_accept = { confidence = 5, pc_love = 5 } }, -- arrives emboldened
{ caption = "Lean on her -- she's out of options", honest = true, chance_bonus = -10, on_accept = { spirit = -10, confidence = -5, pc_fear = 15 } }, -- cowed
{ caption = "Promise easy money, stay vague", honest = false, chance_bonus = 5, on_accept = { pc_hate = 15 } }, -- she saw through it}The acceptance roll reads her stats (a proud, high-Spirit girl is a hard sell) and adds the chosen approach’s bonus — note it never writes a stat; that’s the next step’s job:
local function accepts(approach) local chance = 60 chance = chance - (wm.girl.spirit - 50) / 2 chance = chance - (wm.girl.confidence - 50) / 3 chance = chance + (wm.girl.obedience - 50) / 3 chance = chance + approach.chance_bonus if not approach.honest and wm.girl.intelligence > 60 then chance = chance - 20 -- a clever girl sees through a vague pitch end chance = math.max(25, math.min(90, chance)) return math.random(100) <= chanceendAnd the resolution wires the two together — roll, then on success apply the marks before adding her to the brothel:
if accepts(approach) then wm.message(approach.accept_text, 0) apply_stat_changes(approach.on_accept) -- Rung 2: the encounter leaves a mark wm.add_girl_to_brothel(wm.girl)else wm.message(approach.refuse_text, 0)endSee the exemplar pack for the full init() / run() stage scaffolding (identical in shape to Recipe 5 and the snippet skeleton). The stat field names are the lowercase stat names — spirit, confidence, obedience, happiness, pc_love, pc_fear, pc_hate, … — listed in reference/lua-scripting.md.
Keep the deltas small. This is colour on arrival — a few points that make the recruitment memorable — not a stat-min-max lever.
Known gap: long-arc history
Section titled “Known gap: long-arc history”<When> can read her current traits, stats, status, and the current shift’s performance. It cannot natively express “30 in-game days after she started this job” or “after her third miscarriage”. Those need Lua reading wm.girl.lifetime.* counters.
Roadmap signal: the 2.x track aims to expose declarative time-and-history triggers to packs so long-arc storytelling lands without writing Lua.
Until then, the practical pattern is:
- Use XML for everything that depends on who she is right now.
- Drop into Lua only for arc progression and player-choice scenes.