Skip to content

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.


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.


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).

Assigning to a stat field does nothing on its own:

wm.girl.confidence = wm.girl.confidence - 10 -- edits a throwaway copy

You 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 - 10
wm.girl.spirit = wm.girl.spirit - 5
wm.girl:update() -- now it's real

Forgetting 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.lua names its recruit-chance knobs spirit_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 like chance_bonus so you don’t conflate the two layers.

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 v
end
-- 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 real
end

Each 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) <= chance
end

And 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)
end

See 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.


<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:

  1. Use XML for everything that depends on who she is right now.
  2. Drop into Lua only for arc progression and player-choice scenes.