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 in six places. Five in job XML, plus one on item effects:

SurfaceFileWhat it gates
<Text>messages/work.xmlWhether this text variant fires this shift
<Factor>performance.xmlWhether this stat/skill weight contributes this shift
<GainTrait> / <LoseTrait>gains.xmlWhether this trait grant/removal is eligible this shift
<Eligibility>job.xmlWhether a girl can be assigned this job at all
<EffectGroup>effects.xmlWhether this group of stat/skill/brothel effects fires this shift
<Effect>Items.itemsxWhether 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"/>

<Pleasure .../>: pleasure threshold (1.14 toehold, not yet evaluated) New

Section titled “<Pleasure .../>: pleasure threshold (1.14 toehold, not yet evaluated) New”

Reserved for the upcoming refusal model. Accepts the same comparison ops as <Performance> (eq, ne, ge, le, gt, lt). In 1.14 the leaf is parsed and silently treated as always true, so writing it does not gate the surrounding rule yet.

<Pleasure le="30"/>

This exists so packs adopting the refusal-model schema today will load cleanly on 1.14 engines. Once the pleasure stat ships in a future release, the same XML starts gating correctly without re-authoring. If you are writing for 1.14 only and want a real gate today, do not use <Pleasure>; the leaf is inert until the stat is wired.

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

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 New

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

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 <EffectGroup>. Only valid inside an <EffectGroup> 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:

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

The <Bind> declaration must appear in the same <EffectGroup> 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.

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

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

KeyCounts girls assigned to
singer_presentSinger
piano_presentPiano
sleazybarmaid_presentSleazy Barmaid
barstripper_presentStripper
bar_staff_presentBar 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 sourceNotes
beastsBrothelManager::m_BeastsCaptured beasts in the player’s pool, fed by Beast Capture.
anti_preg_potionsBrothelManager::m_AntiPregPotionsAnti-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=SourceNotes
advertising_budgetBrothel::m_AdvertisingBudgetThe 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_girlsBrothel::m_NumGirlsTotal 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.

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

OpMeaning
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>YesText is picked after performance is computed
<GainTrait>/<LoseTrait> <When>YesGain rolls happen after performance is computed
<Factor> <When>NoFactors compute performance; circular reference
<Eligibility> in job.xmlNoEligibility 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 idWired?Notes
PregnantYes
SlaveYes
FreeYesOpposite of Slave
PoisonedYes
InseminationYesActive insemination cooldown
TormentedReservedAlways evaluates false in v1; not yet populated
ControlledOrgasmReservedAlways evaluates false in v1; not yet populated
CatatonicReservedAlways 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 nameCondition
barThe brothel has a bar upgrade (m_Bar is true)
casinoThe 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.

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

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 and effect types
  • 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