`<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 surfaces
Section titled “When surfaces”<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 items-reference. |
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>Implicit AND
Section titled “Implicit AND”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.”
Combinators
Section titled “Combinators”Use combinators to compose sub-expressions.
<All>: every child must be true
Section titled “<All>: every child must be true”Explicit AND. Useful when you want to wrap a group inside an <Any>.
<All> <Trait id="Charming"/> <Stat name="Beauty" ge="80"/></All><Any>: at least one child must be true
Section titled “<Any>: at least one child must be true”<Any> <Trait id="Charming"/> <Trait id="Seductive"/></Any>Reads: “she has Charming or Seductive (or both).”
<None>: no child may be true
Section titled “<None>: no child may be true”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.”
<Not>: inverts exactly one child
Section titled “<Not>: inverts exactly one child”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.”
Leaf reference
Section titled “Leaf reference”<Stat name="..." .../>: stat threshold
Section titled “<Stat name="..." .../>: stat threshold”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="..." .../>: skill threshold
Section titled “<Skill name="..." .../>: skill threshold”<Skill name="Service" ge="50"/>Recognised skill names: Anal, Magic, BDSM, NormalSex, Beastiality, Group, Lesbian, Service, Strip, Combat, Performance, Farming, AnimalHandling, Crafting, Herbalism, Brewing, Cooking, Medicine, OralSex, TittySex, Handjob, Footjob. Eleven of these arrive with game 1.21, which is not released yet, and they are not all in the same state. OralSex, TittySex, Handjob and Footjob are already weighed by the Escort job, so setting them does change a shift. The seven occupational skills (Farming, AnimalHandling, Crafting, Herbalism, Brewing, Cooking, Medicine) are accepted everywhere but no job reads them yet, so setting one changes nothing in a shift until the Farm jobs are wired to them.
<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"/><Pleasure .../>: libido alias
Section titled “<Pleasure .../>: libido alias”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.
<Outcome name="..."/>: the fight’s real result
Section titled “<Outcome name="..."/>: the fight’s real result”Meaningful inside the work bags of jobs that resolve a real contest (the arena fight jobs). The line pick that headlines the shift runs with the fight’s actual result in scope, so a <Text> gated on <Outcome> fires only when that result happened. Recognised names, and the only ones accepted at load: victory, defeat, draw.
<Text weight="2"> <When><Outcome name="victory"/><Trait id="Sadistic"/></When> ${name} could have ended the duel cleanly halfway through. She did not.</Text>Rules worth knowing:
- When any outcome-gated line is eligible, it wins over outcome-blind lines, so a bank that authors a victory arm never headlines a victory with generic prose.
- Off-surface (a job with no contest result) the leaf is simply false, and an outcome the engine does not recognise behaves the same way, so gated arms never fire without a real matching result.
- A work bank that uses
<Outcome>becomes prose-only:Updates="..."inline effects anywhere in that bank are a load error. Mechanics belong ineffects.xml.
<RefusalReason name="..."/>: why she refused
Section titled “<RefusalReason name="..."/>: why she refused”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 male / female is required, and what the game looks for is the attribute being there, not what it says. 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
Section titled “<Dignity .../>, <Fear .../>, <Obedience .../>: refusal-axis shorthands”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.
<Trait id="..."/>: trait presence
Section titled “<Trait id="..."/>: trait presence”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:
Type=/<Categories>in.traitsx: the core catalogue (split by category underresources/data/traits/) and any pack trait file declare category via theType=attribute, optionally extended by a<Categories>block.- Filename inference: a trait loaded from the legacy
.traitstext format takes its category from the filename (Physical.traits->Physical). This only applies to legacy.traitsfiles a pack might still ship; the core catalogue moved to.traitsxwith explicitType=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).
<Status id="..."/>: status flag
Section titled “<Status id="..."/>: status flag”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="score" expr="(Charisma + Intelligence + Service) / 3"/> <When> <Bind name="score" 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.
Besides stats and skills, a bind expression in a job’s effects.xml can read perf, stock(<resource>), demand(<resource>), setting(<id>) and, from game 1.21, counter(<name>); the effects reference lists what each one reads. A <Bind ge="1"> on a value built from counter() is how a job says “only while the shift’s shared limit is not used up”, which is what the Farm’s Milker does.
<BuildingSetting id="..."/>: a policy the player set on this building (game 1.21, unreleased)
Section titled “<BuildingSetting id="..."/>: a policy the player set on this building (game 1.21, unreleased)”True when the named switch is on for the building the shift is worked in. The switch is declared by the building’s definition (<Settings><Setting id="..."/>, see the buildings reference) and answered by the player on that building’s Building Setup, one answer per building. A setting the building does not declare reads false, so a job naming a policy nobody offered never acts on it; a job naming a setting no installed building declares loads with a warning (the usual typo). A number setting is not a switch: it reads false here and is read through setting(<id>) in a <Bind> expression instead. The pack validator reports both mistakes as errors before the game ever sees them, an id nothing declares and an id of the wrong kind, and warns when the buildings that staff the job do not offer the setting (see the tools page).
<Group needs="materials" observe="magic"> <Bind name="spare" expr="Mana - 15 - setting(mana_reserve)"/> <When> <BuildingSetting id="magic_assist"/> <Skill name="Magic" ge="25" reason="${name} has too little skill in magic to try a charm."/> <Stat name="Mana" ge="15" reason="${name} has no mana to spare for a charm this shift."/> <Bind name="spare" ge="0" reason="${name} kept her mana above this building's reserve and tried no charm."/> </When> ...</Group>reason="..." on a condition (game 1.21, unreleased)
Section titled “reason="..." on a condition (game 1.21, unreleased)”Any condition written directly under a <When> (or directly under an <All>) may carry reason: one sentence, ${name} allowed. It changes nothing about whether the condition passes. It is read only for a <Group observe="..."> in effects.xml: when the group’s gate stays closed, the first failing condition’s sentence goes into the worker’s report and into the measurement record, so the player learns why the policy did not act. Conditions without a reason close the gate silently. See <Group observe="..."> in the effects reference.
<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.
<RandomChance pct="N"/>
Section titled “<RandomChance pct="N"/>”Stochastic gate. Fires N percent of the time when evaluated. AND-composes with sibling predicates inside <When>.
pct(integer, 1..99).0warns + makes the rule never fire.>= 100warns + makes the rule always fire (just remove the leaf instead).bind(game 1.21, unreleased) takes the percentage from a<Bind>declared in the same<Group>instead:<Bind name="p" expr="Herbalism / 2"/>then<RandomChance bind="p"/>. Writepctorbind, not both, or the file is refused. A bind at 0 or below never fires and one at 100 or above always fires, and neither case draws a random number. The Farm’s Farmer uses two of them in one<When>so a find needs both the skill roll and the performance roll.- 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; twopct="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.../>: resource stock count
Section titled “<Stock resource="..." ...cmp.../>: resource stock count”Asks how many of a named resource the player currently has. Uses the same comparison-op shape as <Stat> / <Skill> (eq/ne/ge/le/gt/lt). Which stock it reads follows the resource’s own StorageScope: an estate-scoped resource is one pool for the whole game, so the leaf answers the same everywhere, while a building-scoped one is read from the venue the job actually runs in (game 1.20.3 and later, and only buildings give a resource a shelf of its own).
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="core/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.
Which resources you can name: any resource any pack declares, written as
its package-qualified id (MyPack/soap), and the game’s own two, core/beasts
and core/anti_preg_potions. Declaring your own is the resources
reference.
The game’s resources, for gating against content that already exists:
resource= |
Pool source | Notes |
|---|---|---|
core/beasts |
the player’s beast pool | Captured beasts, fed by Beast Capture. |
core/anti_preg_potions |
the player’s potion stock | Anti-pregnancy potions, fed by the auto-restock setting or by Alchemy. |
The bare names beasts and anti_preg_potions still work: they are the
pre-1.17 spelling and the game maps them onto the two ids above. Write the
qualified form in new content, since it is the only one that can name a pack’s
resource.
A bare name that is not one of those two is a parse error, and the message says to use a qualified id, so a typo does not silently disable the gate. A qualified id is accepted even when nothing has declared it, because packs load in an order nothing controls; if it is still undeclared when the condition runs, the leaf is simply false.
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
Section titled “<BrothelStat key="..." ...cmp.../>: per-brothel scalar”Compares a per-brothel scalar value against a CmpList. Distinct from <Stock> (which reads resource stock) 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 |
the building’s advertising slider | 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 |
the building’s headcount | 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. |
filthiness |
the building’s filth when her shift starts | Game 1.21, unreleased. Any building, not only a brothel. The Farm’s cleaner finishes early when there is little to do, its Researcher’s study is described by it, and its crafting posts mention mending tools from ge="10". |
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
Section titled “<HasItemTag tag="..."/>: per-girl inventory tag check”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" Desc="Cut glass that catches every lamp." Type="Necklace" Cost="900" Rarity="Shop25" Infinite="false"> <Tag>quality</Tag> <Tag>luxury</Tag> <Effect What="Stat" Name="Charisma" Amount="5" /></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
Section titled “<HasStateFlag name="..." min_turns="..." inverted="..."/>: timed state label check”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
Section titled “<BrothelScratch key="..." ...cmp.../>: per-brothel scratch slot read”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,filthiness) on a closed registry that needs an engine release to extend.<Stock>, which reads resource stock: one pool shared across the whole game for an estate-scoped resource, or this venue’s own shelf for a building-scoped one.<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](/docs/pack-authoring/reference/jobs-reference/)).
<JobState scope="customer" key="..." ...cmp.../>: per-customer scratch slot read
Section titled “<JobState scope="customer" key="..." ...cmp.../>: per-customer scratch slot read”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](/docs/pack-authoring/reference/jobs-reference/)).
<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.
Comparison ops
Section titled “Comparison ops”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 scope
Section titled “Performance scope”<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.
Status flags
Section titled “Status flags”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.
Building flags
Section titled “Building flags”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.
Error catalogue
Section titled “Error catalogue”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.
When NOT to use <When>
Section titled “When NOT to use <When>”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).
See also
Section titled “See also”snippets/when-block.xml: copy-paste templatetools/pack-authoring-kit/snippets/when-walkthrough.xml(ships with the game): non-trivial worked example- effects-reference: full schema for
effects.xml(SetStat, SetSkill, SetBrothel, RandomChoice, Group, Bind, RunHelper) including common patterns - traits-reference: trait definitions, effect types, and
<OnGenerate>/<Periodic>lifecycle inflict rules - state-flags: StateFlag registry, decay semantics, and StateFlag vs Temporary trait guidance
- items-reference: consumable items that can grant traits
- jobs-reference: full schema reference for job data directories, including
effects.xml,performance.xml,wage.xml,gains.xml, andmessages/work.xml