Skip to content

`<When>` conditions

<When> is the eligibility language for pack-defined job data. It lets you write boolean conditions in XML (no Lua, no scripting) that the engine evaluates at runtime to decide whether a text line fires, whether a stat weight applies, whether a trait is granted, or whether a girl can hold a job at all.

A <When> block is pure boolean: it is either satisfied or not. For probability and weighting, use the weight= attribute on the parent element or chance= on outcome nodes; those are separate concerns (see “When NOT to use <When>” at the bottom).


<When> appears across job XML and item effects:

Surface File What it gates
<Text> messages/work.xml Whether this shift-narration variant fires this shift
<Text> messages/refuses.xml Whether this refusal line fires when she refuses
<Factor> performance.xml Whether this stat/skill weight contributes this shift
<GainTrait> / <LoseTrait> gains.xml Whether this trait grant/removal is eligible this shift
<Eligibility> job.xml Whether a girl can be assigned this job at all
<Group> effects.xml Whether this group of stat/skill/brothel effects fires this shift
<Effect> Items.itemsx Whether this single item effect applies to the current wearer (1.15.5+). See reference/items-reference.md.

Example: a text line that only fires for pregnant girls:

<Text weight="1">
<When>
<Status id="Pregnant"/>
</When>
She moves carefully around the customers.
</Text>

A bare <When> with multiple children requires all of them. No keyword needed.

<When>
<Trait id="Charming"/>
<Stat name="Beauty" ge="80"/>
</When>

Reads: “she has the Charming trait AND her Beauty is 80 or above.”


Use combinators to compose sub-expressions.

Explicit AND. Useful when you want to wrap a group inside an <Any>.

<All>
<Trait id="Charming"/>
<Stat name="Beauty" ge="80"/>
</All>
<Any>
<Trait id="Charming"/>
<Trait id="Seductive"/>
</Any>

Reads: “she has Charming or Seductive (or both).”

Sugar for “not any of these.” Equivalent to <Not><Any>...</Any></Not>.

<None>
<Status id="Pregnant"/>
<Status id="Poisoned"/>
</None>

Reads: “she is neither pregnant nor poisoned.”

Takes exactly one child. Use <None> when you want to negate a list.

<Not>
<Trait id="Shy"/>
</Not>

Reads: “she does not have the Shy trait.”


Compares a girl’s current stat (post-trait modifiers, same value the game shows in the UI).

<Stat name="Beauty" ge="70"/>

Recognised stat names: Charisma, Happiness, Libido, Constitution, Intelligence, Confidence, Mana, Agility, Fame, Level, AskPrice, House, Exp, Age, Obedience, Spirit, Beauty, Tiredness, Health, PCFear, PCLove, PCHate.

<Skill name="Service" ge="50"/>

Recognised skill names: Anal, Magic, BDSM, NormalSex, Beastiality, Group, Lesbian, Service, Strip, Combat, Performance.

<Performance .../>: shift performance score

Section titled “<Performance .../>: shift performance score”

Compares the girl’s accumulated performance score for the current shift. Only valid inside <Text> and <GainTrait>/<LoseTrait> conditions; see “Performance scope” below.

<Performance ge="245"/>

<Difficulty .../>: per-act difficulty score

Section titled “<Difficulty .../>: per-act difficulty score”

Compares the difficulty scalar (0..10) of the act in scope, using the same comparison ops as <Stat>. Off-surface (no act difficulty in scope) it reads as 0.

<Difficulty ge="6"/>

Alias for the Libido stat, using the same comparison ops as <Stat> (eq, ne, ge, le, gt, lt). <Pleasure ge="80"/> is exactly <Stat name="Libido" ge="80"/>; use whichever reads more clearly in context.

<Pleasure le="30"/>

Requires at least one comparison op (same as <Performance> / <Stat>); a bare <Pleasure/> is a load error.

<RefusalReason name="..."/>: why she refused New

Section titled “<RefusalReason name="..."/>: why she refused New”

Meaningful inside a refusal bag (messages/refuses.xml). When a girl refuses, the engine infers a single dominant reason from the pressure that drove the refusal and threads it into the line pick. <RefusalReason> lets a refusal <Text> fire only for a given reason, so the prose matches why she actually balked.

<Text weight="2">
<When><RefusalReason name="dignity"/></When>
${name} drew herself up, looked the customer dead in the eye, and refused. He thought better of pressing the point.
</Text>

Recognised reason names:

Name Driven by
dignity Pride and self-possession (high Spirit)
rebellion Defiance at being told what to do (low Obedience)
fear Fright at the act or the customer
hate Resentment aimed at you, the owner, as much as the customer
daunted Being overwhelmed by the difficulty of the work
disease She believed the customer was diseased
orientation The customer’s sex is one she will not serve
beast Refusing a beast on grounds of dignity or condition
generic No single axis dominated

Outside a refusal bag there is no refusal in scope, so the leaf evaluates to false (it is inert in work.xml, performance.xml, and the rest).

<CustomerGender male/> / <CustomerGender female/>: customer sex

Section titled “<CustomerGender male/> / <CustomerGender female/>: customer sex”

True when the customer in scope is of the named sex. Exactly one of the empty attributes male / female is required. Off-surface (no customer in scope) it evaluates false. Useful for gating refusal or shift prose on who she is serving.

<CustomerGender male/>

<Dignity .../>, <Fear .../>, <Obedience .../>: refusal-axis shorthands New

Section titled “<Dignity .../>, <Fear .../>, <Obedience .../>: refusal-axis shorthands New”

Numeric leaves (same comparison ops as <Stat>) that read the girl’s refusal-relevant axes under their refusal-model names. They are plain aliases for existing stats, handy when writing refusal prose:

Leaf Reads High when
<Dignity .../> Spirit She is proud and self-possessed
<Fear .../> PCFear She is afraid of you
<Obedience .../> Obedience She does as she is told
<Dignity ge="70"/>
<Obedience le="20"/>

<Dignity ge="70"/> is exactly <Stat name="Spirit" ge="70"/>; use whichever reads more clearly. They work on any surface but earn their keep in messages/refuses.xml.

True if the girl currently has the named trait. Trait names are case-sensitive and must match the Name attribute in the .traitsx definition exactly.

<Trait id="Night Owl"/>

Unknown trait names are a load error (see Error catalogue).

<TraitCategory id="..."/>: any trait in a category

Section titled “<TraitCategory id="..."/>: any trait in a category”

True if the girl currently has any trait belonging to the named category. Categories are case-insensitive.

<TraitCategory id="Magic"/>

Categories come from two sources:

  1. Type= / <Categories> in .traitsx: the core catalogue (split by category under resources/data/traits/) and any pack trait file declare category via the Type= attribute, optionally extended by a <Categories> block.
  2. Filename inference: a trait loaded from the legacy .traits text format takes its category from the filename (Physical.traits -> Physical). This only applies to legacy .traits files a pack might still ship; the core catalogue moved to .traitsx with explicit Type= in 1.15.

<TraitCategory> is the right shape when a rule wants to say “any combat-style trait”, “any addiction”, “any disease”. It replaces the otherwise-long <Any><Trait id="..."/><Trait id="..."/>...</Any> pattern.

<Girl name="..."/>: match the girl’s display name

Section titled “<Girl name="..."/>: match the girl’s display name”

True if the girl’s display Name matches the given string exactly. Comparison is case-sensitive and trimmed of surrounding whitespace.

<Girl name="Alice"/>

Use this to key effects, dialogue, or eligibility to one named heroine: signature gear that only buffs her, a cursed artifact that wrecks one character and ignores everyone else, a unique-rival job-gate.

The name attribute is required. A bare <Girl/> is a load error.

Available on every <When> surface, including item effects (<Effect> in Items.itemsx).

True if the girl currently has the named status. See “Status flags” below for the full v1 list and notes on which ones are wired.

<Status id="Slave"/>

<DayNight value="day|night"/>: shift timing

Section titled “<DayNight value="day|night"/>: shift timing”
<DayNight value="night"/>

True if the current shift matches the value. Accepted values: day, night.

<BuildingFlag name="bar|casino"/>: building upgrade present

Section titled “<BuildingFlag name="bar|casino"/>: building upgrade present”

True if the current building has the named upgrade installed. See “Building flags” below.

<BuildingFlag name="bar"/>

<Bind name="..." op="..." value="N"/>: derived integer comparison

Section titled “<Bind name="..." op="..." value="N"/>: derived integer comparison”

Compares a named integer value that was computed by a <Bind> expression declared in the same <Group>. Only valid inside an <Group> that declares the named bind. Supported ops are the same as <Stat> (eq/ne/ge/le/gt/lt).

Use this when you want to gate effects on a derived formula rather than a raw stat. For example, to fire a bonus only when a girl’s average of three stats reaches a threshold:

<Group>
<Bind name="perf" expr="(Charisma + Intelligence + Service) / 3"/>
<When>
<Bind name="perf" ge="20"/>
</When>
<SetStat target="self" stat="Tiredness" delta="5"/>
</Group>

The <Bind> declaration must appear in the same <Group> as the <When> that references it. Referencing a bind from an outer or sibling group is not supported and will cause a load error.

<JobParam name="..." value="..."/>: job slot parameter

Section titled “<JobParam name="..." value="..."/>: job slot parameter”

True if the named string parameter registered with this job slot equals the given value. Used to parameterize a single data directory across multiple job slots. For example, three orientation-training slots can share one houseso/ directory and use <JobParam> to gate which effects fire for each slot.

<Group>
<When>
<JobParam name="orientation" value="straight"/>
</When>
<SetSkill target="self" skill="NormalSex" delta_min="2" delta_max="4"/>
</Group>

Parameters are registered in engine code (or a future job-registry.xml), not in the job XML itself. If no parameter with the given name is registered for the current slot, the condition evaluates to false.

Stochastic gate. Fires N percent of the time when evaluated. AND-composes with sibling predicates inside <When>.

  • pct (required, integer, 1..99). 0 warns + makes the rule never fire. >= 100 warns + makes the rule always fire (just remove the leaf instead).
  • Available wherever <When> is parsed: trait rotations, <Group>, message variants, <Filter>. On surfaces where the engine doesn’t have an RNG in scope (eligibility, factor evaluation) the leaf evaluates to false.
  • Multiple <RandomChance> leaves in one <When> are independent rolls; two pct="50" leaves give a 25% net chance.

Example: 20% chance per shift to grow the Charismatic trait once Performance is high enough.

<GainTrait trait="Charismatic">
<When>
<Not><Trait id="Charismatic"/></Not>
<Performance ge="120"/>
<RandomChance pct="20"/>
</When>
</GainTrait>

Example: dual-band trait gain, high chance under condition A, low chance otherwise. Use two sibling <GainTrait> blocks, each with its own predicate band and chance.

<GainTrait trait="Aggressive">
<When>
<Not><Trait id="Aggressive"/></Not>
<Stat name="Spirit" ge="50"/>
<RandomChance pct="25"/>
</When>
</GainTrait>
<GainTrait trait="Aggressive">
<When>
<Not><Trait id="Aggressive"/></Not>
<RandomChance pct="10"/>
</When>
</GainTrait>

<SynergyGE key="..." value="N"/> / <SynergyLT key="..." value="N"/>: bar staff headcount

Section titled “<SynergyGE key="..." value="N"/> / <SynergyLT key="..." value="N"/>: bar staff headcount”

Asks how many girls in the same brothel are assigned to a related job in the same shift. GE fires when the count is greater than or equal to value; LT fires when it is strictly less than value.

Example: text variant that fires only when a Pianist is on shift.

<Text id="barsinger.work.perfect.with_piano" weight="3">
<When>
<Performance ge="245"/>
<SynergyGE key="piano_present" value="1"/>
</When>
With ${pianist_name} keeping steady time on the keys, ${name}'s voice carried all the way to the back tables.
</Text>

Example: solo-singer fallback when no Pianist is on shift.

<Text id="barsinger.work.perfect.solo" weight="2">
<When>
<Performance ge="245"/>
<SynergyLT key="piano_present" value="1"/>
</When>
${name} held the room on her own tonight, no backing and no safety net.
</Text>

Available keys:

Key Counts girls assigned to
singer_present Singer
piano_present Piano
sleazybarmaid_present Sleazy Barmaid
barstripper_present Stripper
bar_staff_present Bar Maid, Waitress, Stripper, Singer, Bar Cook, Piano, Sleazy Barmaid, Sleazy Waitress

Day/night: counts are tracked per shift. The leaf reads the shift the evaluating girl is currently working, so a night-shift Barmaid counts only girls who are also working the night shift.

Self-counting: the girl whose rule is being evaluated is included in the count if she also matches the key. A Singer asking singer_present ge 1 will always see at least 1 — herself.

value="0" behaviour: SynergyGE with value="0" always fires (trivially true); SynergyLT with value="0" never fires (count can’t be negative). Both produce a warning at parse time. Use value="1" as the minimum meaningful threshold.

Unknown keys are a parse error. Only the five keys listed above are valid in v1.

<Stock resource="..." ...cmp.../>: player-stock pool count New

Section titled “<Stock resource="..." ...cmp.../>: player-stock pool count New”

Asks how many of a named global player-stock resource the player currently has. Uses the same comparison-op shape as <Stat> / <Skill> (eq/ne/ge/le/gt/lt). The pool is global (not per-brothel), so the leaf reports the same value regardless of which brothel a girl is in.

The primary use is gating arena and consumable-driven jobs that would otherwise produce a phantom shift when the input pool is empty. The first shipped use closes the FightBeasts “fight nothing” bug:

<Eligibility>
<Stock resource="beasts" ge="1"/>
</Eligibility>

With this gate the player cannot assign FightBeasts when the beast pool is empty, and an already-assigned girl whose pool drained between turns produces a “could not work” event instead of fighting (and being paid against) an empty cage. The gate composes with sibling predicates the usual way; you can write an <All> of a stock check plus a stat check plus a trait check.

Available resources:

resource= Pool source Notes
beasts BrothelManager::m_Beasts Captured beasts in the player’s pool, fed by Beast Capture.
anti_preg_potions BrothelManager::m_AntiPregPotions Anti-pregnancy potions in stock, fed by the auto-restock flag or by Alchemy.

handmade_goods and alchemy_ingredients are real BrothelManager pools but are not yet in the registry; no shipped job needs them yet. They can be added without an engine release when a real use case appears.

Unknown resource names are a parse error and the error message lists the valid set, so a typo doesn’t silently disable the gate.

Re-evaluated at shift time: unlike most <When> surfaces which fire once per shift, <Eligibility> is re-checked both when the player assigns the job and again at the top of every shift. If the stock drains between assignment and the shift firing, the shift is preempted with a “could not work” message and the assignment is preserved for the next turn; pack authors do not need to model the recovery themselves.

<BrothelStat key="..." ...cmp.../>: per-brothel scalar New

Section titled “<BrothelStat key="..." ...cmp.../>: per-brothel scalar New”

Compares a per-brothel scalar value against a CmpList. Distinct from <Stock> (which reads global player-stock pools) and from <SynergyGE>/<SynergyLT> (which counts job assignments inside the brothel). Reads values that live on the Brothel itself — the player-set advertising budget, total assigned headcount, future security level, and so on.

Same comparison-op shape as <Stat> / <Skill> / <Stock>.

<Text id="advertising.work.with_budget" weight="3">
<When>
<Performance ge="100"/>
<BrothelStat key="advertising_budget" ge="500"/>
</When>
</Text>

Available keys (1.15.6):

key= Source Notes
advertising_budget Brothel::m_AdvertisingBudget The weekly gold/week slider the player sets in Brothel Management. Used by the Advertising bag to gate “with budget” / “lean budget” / “no budget” prose.
working_girls Brothel::m_NumGirls Total girls assigned to this brothel, including Free Time and Rest. Used by the Security bag to gate “busy” / “stretched thin” / “quiet” prose. A refinement to “girls on a paying job” can layer later by extending the registry.

Unknown keys are a parse error and the error message lists the valid set, so a typo doesn’t silently disable the gate.

<HasItemTag tag="..."/>: per-girl inventory tag check New

Section titled “<HasItemTag tag="..."/>: per-girl inventory tag check New”

Fires when the girl carries any item (equipped or stashed in her 40-slot inventory) that declares a matching <Tag> in its .itemsx definition. Open registry — any non-empty string is accepted both at the item-write side and here at the <When> read side, so you can introduce new categorizations in your own pack without an engine release.

On the item side (in your Items.itemsx):

<Item Name="Crystal Earrings">
<Type>Necklace</Type>
<Tag>quality</Tag>
<Tag>luxury</Tag>
<effects>...</effects>
</Item>

Multiple <Tag> children stack and de-duplicate. Either element text (<Tag>name</Tag>) or name= attribute (<Tag name="name"/>) works. Whitespace is trimmed; empty entries are silently skipped.

On the consumer side (any <When> block — text variant, gain rule, eligibility, effect):

<Text id="escort.shift.luxury" weight="2">
<When>
<HasItemTag tag="luxury"/>
<Performance ge="100"/>
</When>
...
</Text>

The leaf evaluates true if the girl currently carries at least one item whose <Tag> set contains the requested string. Composes AND-wise with other <When> predicates the usual way. A girl with no tag-bearing items always evaluates false; an empty tag="" attribute is a parse error (would always be false anyway).

Tag naming. Convention: lowercase snake_case, broad enough to cluster meaningfully (quality over nice_thing), narrow enough to be useful (weapon over equipment). The engine doesn’t enforce anything beyond non-empty — resources/data/Items/ ships zero tags today, so the field is a clean canvas for pack authors. Build your tag vocabulary as part of your pack’s authoring conventions and document it in the pack’s README.md so other modders can layer on top.

<HasStateFlag name="..." min_turns="..." inverted="..."/>: timed state label check New

Section titled “<HasStateFlag name="..." min_turns="..." inverted="..."/>: timed state label check New”

Fires when the girl currently has the named StateFlag with at least min_turns weeks remaining on its counter. Permanent flags (no counter) always satisfy any min_turns threshold.

<Text Image="sex">
<When><HasStateFlag name="InHeat"/></When>
She moves with an unusual urgency tonight, driven by something deeper than desire.
</Text>
Attribute Required Default Notes
name yes string – must match a registered StateFlag
min_turns no 1 integer >= 1; flag must have at least this many turns remaining
inverted no false boolean (true/false); when true, the condition fires when the flag is absent or below min_turns

The engine validates name at parse time. An unknown name is a parse error and the containing job is rejected. See state-flags.md for the full registry.

<BrothelScratch key="..." ...cmp.../>: per-brothel scratch slot read New

Section titled “<BrothelScratch key="..." ...cmp.../>: per-brothel scratch slot read New”

Reads a per-brothel scratch slot — an integer keyed by a free-form string, written by <SetBrothelScratch> elsewhere in your pack. This is the consumer half of the “Job A writes a signal, Job B reads it next shift” pattern. Distinct from:

  • <BrothelStat>, which reads built-in scalars (advertising_budget, working_girls) on a closed registry that needs an engine release to extend.
  • <Stock>, which reads global player pools (beasts, anti_preg_potions) shared across all brothels.
  • <SynergyGE> / <SynergyLT>, which count job-assignment headcounts inside the brothel.

The scratch slot registry is OPEN — any string both writes and reads. The engine doesn’t whitelist. Pack authors design their own vocabulary.

Missing key reads as 0. Writing <BrothelScratch key="food_quality" eq="0"/> matches both the “never written” case and the “explicitly zero” case. Authors don’t need to seed a key before reading; this matches the natural “fresh save / new brothel / drained week” semantics.

Worked example — BarMaid reacts to BarCook output:

<!-- in resources/jobs/barcook/effects.xml -->
<Effects>
<SetBrothelScratch key="food_quality" delta="1" persist="week"/>
</Effects>
<!-- in resources/jobs/barmaid/messages/work.xml -->
<Text id="barmaid.work.great.good_kitchen" weight="3">
<When>
<Performance ge="185" le="244"/>
<BrothelScratch key="food_quality" ge="3"/>
</When>
The kitchen kept up with the floor tonight.
</Text>

The BarCook writes food_quality+=1 per shift; over a week of multiple BarCook shifts, the value climbs into the threshold range. The BarMaid bag’s overlay fires only when the cook has been working enough. persist="week" drains the slot at end-of-week so each week’s quality reflects only that week’s cooking, not the cumulative lifetime.

See also: <SetBrothelScratch> (the writer side, documented in jobs-reference.md).

<JobState scope="customer" key="..." ...cmp.../>: per-customer scratch slot read New

Section titled “<JobState scope="customer" key="..." ...cmp.../>: per-customer scratch slot read New”

Reads a per-customer scratch slot — same shape as <BrothelScratch> but the scratch lives for the duration of a single customer interaction rather than across shifts. The writer side is <SetJobState scope="customer" key="X" delta="N"/> (see jobs-reference.md).

<Text id="masseuse.shift.happy_ending" weight="3">
<When>
<Performance ge="100"/>
<JobState scope="customer" key="happiness" ge="50"/>
</When>
...
</Text>

scope. Only customer is accepted in 1.15.6. Future “shift” or “girl” scopes can layer in without a schema change once their scratch surfaces ship.

Reads as 0 when there’s no customer in scope. The scratch lives on a per-customer effect-application loop established by the consumer (the masseuse customer-satisfaction loop is the canonical use case). On per-shift Apply calls that don’t establish a customer scope, <JobState> reads as 0 — exactly like <BrothelScratch> on a missing key. This lets authors write defensively gated overlays without crashing off-surface.

Note — consumer-side wiring is forward-looking in 1.15.6. The grammar surface ships now so authors can pre-write their per-customer overlays, but the WMR data-driven framework runs at per-shift granularity today. No shipped job carves out a per-customer loop yet; the surface is in place for whichever consumer (masseuse satisfaction loop, customer happiness modelling, etc.) lands first. Pre-written overlays will fire as soon as the consumer loop ships, with no XML changes needed.


Numeric leaves (<Stat>, <Skill>, <Performance>) require at least one comparison op. Providing none is a load error.

Op Meaning
eq="N" equals N
ne="N" not equal to N
ge="N" greater than or equal to N
le="N" less than or equal to N
gt="N" strictly greater than N
lt="N" strictly less than N

Multiple ops on the same leaf are AND’d. This lets you write a range in one element:

<Stat name="Beauty" ge="80" le="95"/>

Reads: “Beauty is between 80 and 95 (inclusive).”


<Performance> may only appear where the shift score is already known:

Surface <Performance> allowed? Reason
<Text> <When> Yes Text is picked after performance is computed
<GainTrait>/<LoseTrait> <When> Yes Gain rolls happen after performance is computed
<Factor> <When> No Factors compute performance; circular reference
<Eligibility> in job.xml No Eligibility is checked at job-assignment time and re-checked at the top of every shift; the shift score is not yet known either time

Using <Performance> in a forbidden surface is a load error.


v1 status set (8 values):

Status id Wired? Notes
Pregnant Yes
Slave Yes
Free Yes Opposite of Slave
Poisoned Yes
Insemination Yes Active insemination cooldown
Tormented Reserved Always evaluates false in v1; not yet populated
ControlledOrgasm Reserved Always evaluates false in v1; not yet populated
Catatonic Reserved Always evaluates false in v1; not yet populated

Caveats: The three reserved statuses (Tormented, ControlledOrgasm, Catatonic) are part of the grammar so they don’t become load errors, but they will never fire in the current engine. Avoid building game logic that depends on them until they are wired up.


v1 building flag set (2 values):

Flag name Condition
bar The brothel has a bar upgrade (m_Bar is true)
casino The brothel has a gambling hall upgrade (m_GamblingHall is true)

More flags (movie_studio, etc.) will be added as the corresponding buildings become full standalone entities.


All errors are hard-fail at pack load. The job is refused if any <When> in it is invalid. Multiple errors are collected and written to gamelog.txt before the job is skipped, so you can fix everything in one pass.

Format is always path/to/file.xml:LINE -- message.

Category Example error
Unknown leaf tag barmaid/messages/work.xml:42 -- unknown element <Foo> in <When>
Unknown combinator barmaid/job.xml:8 -- <When> child <Maybe> is not a combinator or leaf
Empty combinator barmaid/job.xml:11 -- <Any> requires at least one child
<Not> arity barmaid/job.xml:14 -- <Not> takes exactly one child, got 2
Unknown stat/skill barmaid/performance.xml:5 -- <Stat name="Booty"> -- no such stat
Unknown trait barmaid/messages/work.xml:51 -- <Trait id="Charmin"> -- no such trait
Unknown status barmaid/job.xml:9 -- <Status id="Sleepy"> -- no such status (v1: Pregnant, Slave, Free, Poisoned, Insemination, Tormented, ControlledOrgasm, Catatonic)
Performance out of scope barmaid/performance.xml:7 -- <Performance> not allowed inside <Factor> (perf not in scope)
No CmpOp on numeric leaf barmaid/messages/work.xml:42 -- <Stat name="Beauty"> requires at least one of eq/ne/ge/le/gt/lt
Unknown DayNight value barmaid/job.xml:5 -- <DayNight value="dusk"> -- must be "day" or "night"
Unknown BuildingFlag barmaid/job.xml:6 -- <BuildingFlag name="dungeon"> -- v1 set: bar, casino
Missing <Girl name> MyPack/Items.itemsx:12 -- <Girl> requires a name attribute
Unknown StateFlag name barmaid/messages/work.xml:67 -- <HasStateFlag name="OnFire"> -- no such StateFlag

When you see a load error popup at startup saying “N pack jobs failed to load”, open gamelog.txt in the game folder and search for your pack name. All errors for every failed job are listed there.


For probability: <When> is boolean; it either passes or it doesn’t. To weight one outcome over another, use weight=N on the parent <Text> element, or chance=N on outcome nodes. These are separate mechanisms.

For Lua scripting: <When> has no Lua integration and never will. It is deliberately a closed vocabulary so the engine can validate it completely at load time. If you need logic that reads custom state, Lua hooks are the right tool (planned for a future step).


  • snippets/when-block.xml: copy-paste template
  • examples/when-walkthrough.xml: non-trivial worked example
  • reference/effects-reference.md: full schema for effects.xml (SetStat, SetSkill, SetBrothel, RandomChoice, Group, Bind, RunHelper) including common patterns
  • reference/traits-reference.md: trait definitions, effect types, and <OnGenerate> / <Periodic> lifecycle inflict rules
  • reference/state-flags.md: StateFlag registry, decay semantics, and StateFlag vs Temporary trait guidance
  • reference/items-reference.md: consumable items that can grant traits
  • reference/jobs-reference.md: full schema reference for job data directories, including effects.xml, performance.xml, wage.xml, gains.xml, and messages/work.xml