Job data reference
This page is the full schema reference for job data directories. If you are new to job authoring, start with examples/jobs-cookbook.md for step-by-step recipes, then come back here when you need the exact attribute list.
Directory layout
Section titled “Directory layout”Each job lives in its own directory under resources/jobs/:
resources/jobs/<id>/ job.xml required -- identity, title, description, eligibility effects.xml optional -- stat/skill/brothel changes applied each shift performance.xml optional -- how the performance score is calculated wage.xml optional -- gold earned as a function of performance gains.xml optional -- skill XP and trait grants each shift messages/ work.xml optional -- shift-end message variants refuses.xml optional -- refusal-arm message variants banks.xml optional -- named fragment banks for composed messages text/ en.xml optional -- localizable strings for the messages aboveOnly job.xml is required. A job with no other files is valid: the girl shows up in the job list, can be assigned, but nothing happens each shift.
The directory name (<id>) must be lowercase, no spaces, and must match the id attribute in job.xml. It also becomes the prefix for all text IDs in messages/work.xml and text/en.xml.
job.xml
Section titled “job.xml”Declares the job’s identity and optional eligibility gate.
<?xml version="1.0" encoding="UTF-8"?><Job id="barmaid" schema="1"> <Title>Barmaid</Title> <Description>She will staff the bar and serve drinks.</Description> <DefaultImage>profile</DefaultImage></Job>| Element / attribute | Required | Notes |
|---|---|---|
id (attribute on <Job>) |
Yes | Must match the directory name. Lowercase, no spaces. |
schema="1" (attribute on <Job>) |
Yes | Always 1 for now. |
<Title> |
Yes | Displayed in the job picker UI. |
<Description> |
Yes | Short tooltip shown in job details. |
<DefaultImage> |
No | Image catalog type used for the shift-event portrait. Defaults to profile if omitted. Common values: profile, sex, strip, oral, wait, cook, massage, escort, dom. Individual outcomes can override this per-<Text> via the Image= attribute (see messages/work.xml below). |
<Eligibility> |
No | A block of <When> leaves. Leaves are direct children of <Eligibility> (do not wrap them in <When> here; the loader iterates the children directly, unlike the message-bank <When> surfaces where the wrapper is required). If present, a girl can only be assigned this job while every child leaf is satisfied; the same gate is re-checked at the top of every shift, so a girl whose state flipped after assignment (stock drained, status changed) produces a “could not work” event instead of running the shift. See reference/when-conditions.md for the full leaf catalogue. |
<Filter> |
No | Restricts the job to buildings that have the named feature. Text content; current values: Bar. Omit (or leave empty) for brothel-core jobs that any building hosts. |
<Phase> |
No | Selects the turn-loop pass this job runs in. Text content; one of Prepare, Produce, Main (default), Late. Use Prepare for jobs that must run before customer traffic (Advertising), Produce for jobs that publish a value other jobs read this turn (BarCook publishing food quality before BarMaid serves it), Main for everything else, Late for clean-up passes that run after the rest of the turn. |
Example with eligibility:
<Job id="basictraining" schema="1"> <Title>Basic Training</Title> <Description>She trains her combat skills.</Description> <DefaultImage>profile</DefaultImage> <Eligibility> <Stat name="Level" le="10"/> </Eligibility></Job>Example with <Filter> and <Phase>:
<Job id="barcook" schema="1"> <Title>Bar Cook</Title> <Description>She will cook food for the bar.</Description> <DefaultImage>profile</DefaultImage> <Filter>Bar</Filter> <Phase>Produce</Phase></Job>This job is offered only in buildings that have a Bar, and runs in the Produce pass so that any Main-pass consumer (BarMaid, BarWaitress) sees the value it publishes for the rest of the turn.
effects.xml
Section titled “effects.xml”Applied once per shift for every girl working this job. Can change the girl’s stats and skills, fan out changes to all girls in the building, or change building properties.
<?xml version="1.0" encoding="UTF-8"?><Effects> <SetStat target="self" stat="Tiredness" delta="-25"/> <SetStat target="self" stat="Happiness" delta="10"/></Effects><SetStat>
Section titled “<SetStat>”Changes a girl’s stat by a fixed or random amount.
| Attribute | Required | Notes |
|---|---|---|
target |
Yes | Who is affected. See targets below. |
stat |
Yes | Stat name. Same set as <When><Stat>. |
delta |
Yes (or delta_min+delta_max) | Fixed integer change. Negative values decrease the stat. |
delta_min / delta_max |
Yes (or delta) | Random range, inclusive. The engine picks uniformly. |
clamp_max |
No | Cap the resulting value at this ceiling. Useful for Fame, Health, etc. |
<SetSkill>
Section titled “<SetSkill>”Changes a girl’s skill by a fixed or random amount.
<SetSkill target="self" skill="NormalSex" delta_min="2" delta_max="4"/>Same attributes as <SetStat> with skill instead of stat. Skill names: Anal, Magic, BDSM, NormalSex, Beastiality, Group, Lesbian, Service, Strip, Combat, Performance.
<SetBrothel>
Section titled “<SetBrothel>”Changes a property on the building.
<SetBrothel target="brothel" key="Filthiness" delta="-5"/><SetBrothel target="player.brothels" key="Fame" delta="$delta" clamp_max="100"/>| Attribute | Notes |
|---|---|
target |
brothel for the current building; player.brothels to fan out to every building the player owns. |
key |
Building property to change. Current set: Filthiness, Fame. |
delta |
Fixed integer, or $bindname to reference a <Bind> value (see below). |
clamp_max |
Optional ceiling. |
<EffectGroup> (also written <Group>)
Section titled “<EffectGroup> (also written <Group>)”Groups a set of effects behind a <When> condition and optional <Bind> declarations. If the <When> is not satisfied, the entire group is skipped.
<EffectGroup> <Bind name="delta" expr="(Charisma + Intelligence + Service) / 60"/> <When> <Bind name="delta" ge="1"/> </When> <SetBrothel target="player.brothels" key="Fame" delta="$delta" clamp_max="100"/> <SetStat target="self" stat="Happiness" delta="1"/></EffectGroup>Order inside a group: <Bind> elements are evaluated first, then <When> is checked using those binds, then the remaining entries are applied if the check passed.
The short alias <Group> is accepted by the engine in addition to <EffectGroup>.
<Bind>
Section titled “<Bind>”Declares a named integer computed from a formula. Only valid inside an <EffectGroup>. The result can be referenced in that group’s <When> as <Bind name="..." op="..." value="N"/> and in delta="$name" on effect entries.
<Bind name="bonus" expr="clamp((Intelligence + Service) / 2 / 33, 0, 3)"/>Supported identifiers in expr: stat names, skill names, and the clamp(value, min, max) function. You cannot reference a bind from another group; repeat the formula if you need it in two places.
<RandomChoice>
Section titled “<RandomChoice>”Picks exactly one child <Option> per shift, weighted.
<RandomChoice> <Option weight="6"><SetStat target="self" stat="Exp" delta="0"/></Option> <Option weight="2"><SetSkill target="self" skill="Service" delta="1"/></Option> <Option weight="2"><SetSkill target="self" skill="Strip" delta="1"/></Option></RandomChoice>Weights are relative. In the example above: 60% no gain, 20% Service +1, 20% Strip +1.
Targets
Section titled “Targets”| Target string | Meaning |
|---|---|
self |
The girl working the job this shift. |
brothel.girls |
Every girl currently assigned to the same building (including self). |
player.brothels |
All buildings the player currently owns (for <SetBrothel> only). |
brothel |
The current building (for <SetBrothel> only). |
performance.xml
Section titled “performance.xml”Defines how the girl’s performance score is calculated for this shift. The score is a weighted sum of her stats and skills, modified by traits.
<?xml version="1.0" encoding="UTF-8"?><Performance> <Factor skill="Service" weight="3"/> <Factor stat="Intelligence" weight="3"/> <Factor stat="Charisma" weight="2"/> <Factor skill="Performance" weight="2"/>
<TraitMod trait="Psychic" delta="10"/> <TraitMod trait="Fleet of Foot" delta="10"/> <TraitMod trait="Cum Addict" delta="-5"/></Performance><Factor>
Section titled “<Factor>”Contributes one stat or skill to the performance total.
| Attribute | Required | Notes |
|---|---|---|
stat or skill |
Yes (one) | The stat or skill to pull from. |
weight |
Yes | Multiplier. Higher = more influence. No maximum. |
<When> child |
No | If present, this factor only applies when the condition is satisfied. |
The engine multiplies the girl’s value by the weight for each factor, sums all factors, and divides by the total weight. The result is a number in roughly 0-1000+ range.
<TraitMod>
Section titled “<TraitMod>”Flat bonus or penalty added to the score when the girl has the named trait.
| Attribute | Notes |
|---|---|
trait |
Trait name, case-sensitive. |
delta |
Integer. Positive = bonus, negative = penalty. |
If the girl does not have the trait, the entry has no effect.
Jobs without a performance.xml are pure-effect jobs: the performance score is always 0, so there is no performance tier (and a wage.xml would earn nothing). Use this shape only when the shift outcome does not vary by quality, such as pure rest.
Performance-banded narration requires this file. If your messages/work.xml gates any <Text> on a <Performance> band (<Performance ge="245"/>, <Performance ge="100" le="144"/>, and so on), you must ship a performance.xml. Without one the score is always 0, so every shift matches only the lowest band (<Performance le="..."/>) and the higher bands never fire. This catches even simple household jobs: a cook or cleaner whose narration should read better when she does well still needs a performance.xml to compute that score. “Pure-effect” and “performance-banded” are mutually exclusive.
wage.xml
Section titled “wage.xml”Converts the performance score into gold earned this shift. Uses a piecewise linear curve: the engine finds which two breakpoints the score falls between and interpolates linearly. For the full picture of how wages compose with girl Ask Prices, tips, and trait modifiers, see pricing.md.
<?xml version="1.0" encoding="UTF-8"?><Wage currency="gold"> <Curve type="piecewise"> <Point perf="245" wage="155"/> <Point perf="185" wage="95"/> <Point perf="145" wage="55"/> <Point perf="100" wage="15"/> <Point perf="70" wage="-5"/> <Point perf="0" wage="-15"/> </Curve></Wage>| Attribute | Notes |
|---|---|
currency="gold" |
Only gold is supported in v1. Required. |
type="piecewise" |
Only piecewise is supported in v1. Required. |
perf on <Point> |
Performance score at this breakpoint. Points must be listed in descending order. |
wage on <Point> |
Gold earned at this performance. Negative values deduct from the brothel (costs the player money). |
A job without wage.xml earns no gold. This is correct for training and household-service jobs.
gains.xml
Section titled “gains.xml”Controls skill and stat XP distributed at the end of each shift. XP is shared across the declared entries proportional to their weights.
<?xml version="1.0" encoding="UTF-8"?><Gains xp="15" baseSkill="3"> <Skill name="Service" weight="3"/> <Skill name="Performance" weight="2"/> <Stat name="Charisma" weight="1"/></Gains>| Attribute / element | Notes |
|---|---|
xp on <Gains> |
Total XP distributed this shift, before weights. |
baseSkill on <Gains> |
Flat skill-point floor. Added to each entry regardless of XP. |
<Skill name="..." weight="..." Max="..."> |
A skill that receives XP. Max caps the resulting skill value; further XP into this entry is discarded once the cap is reached. |
<Stat name="..." weight="..."> |
A stat that receives XP. |
<GainTrait> / <LoseTrait> |
Optional. Grant or remove a trait via a progress accumulator. Key attributes: trait= (required), threshold= (default 1000), amount= (default 100). The trait fires after enough qualifying shifts. Can include a <When> gate so only matching shifts count. Progress is visible to players in Girl Details. See gains.md for the full reference and examples. |
A job without gains.xml grants no skill XP. Jobs that are purely for rest or housework (cook, rest) typically omit this file.
messages/work.xml
Section titled “messages/work.xml”Shift-end message variants. The engine picks one eligible entry at random (weighted) and shows it in the turn summary.
<?xml version="1.0" encoding="UTF-8"?><Bank id="work"> <Text id="barmaid.work.perfect.1" weight="1"> <When><Performance ge="245"/></When> </Text> <Text id="barmaid.work.great.1" weight="2"> <When><Performance ge="185" le="244"/></When> </Text> <Text id="barmaid.work.ok.1" weight="2"> <When><Performance ge="100" le="144"/></When> </Text></Bank>| Attribute | Notes |
|---|---|
id on <Bank> |
Always work for shift messages. |
id on <Text> |
Must match an entry in text/en.xml. Convention: <jobid>.work.<tier>.<n>. |
weight |
Relative probability. Higher = more likely to be selected. |
Updates="..." |
Optional shorthand for in-line stat / earnings adjustments fired together with the message. Semicolon-separated list of <Name><op><Value> entries, e.g. Updates="Tiredness+=5;Tips+=10". Recognised aliases on the earnings side: Tips= and Wages=. |
Image="..." |
Optional. Overrides the job’s <DefaultImage> for this single outcome (1.15.5+). Same vocabulary as <DefaultImage>: any image-catalog tag name (sex, oral, anal, massage, wait, ecchi, etc.). Empty or omitted: the job’s <DefaultImage> is used. Unknown tag names degrade to profile the same way <DefaultImage> does, so a typo can’t brick a shift. |
<When> child |
If present, this variant only fires when the condition is satisfied. Multiple eligible variants are drawn from using their weights. |
Every job should have at least one entry with no <When> (or a <When> that always matches), so the turn summary always has something to show.
Per-outcome image override
Section titled “Per-outcome image override”A single message bag can paint different portraits for different outcomes. The Masseuse bag is the canonical example: a clean shift shows the massage portrait, a happy-ending variant shows sex or oral, a refusal arm shows refuse.
<Bank id="work"> <!-- Default outcome: uses the job's <DefaultImage> (massage). --> <Text id="masseuse.work.great.1" weight="3"> <When><Performance ge="185"/></When> </Text> <!-- Horny variant: paints "oral" instead of "massage". --> <Text id="masseuse.work.horny.oral.1" weight="1" Image="oral"> <When> <Performance ge="185"/> <Stat name="Libido" ge="80"/> </When> </Text></Bank>Image= is per-<Text>, never per-<Bank>. Leave it off and you fall back to <DefaultImage>. Set it to any image-catalog tag and that outcome paints the matching portrait instead. Unknown tag names degrade to profile, just like an unknown <DefaultImage>.
messages/refuses.xml
Section titled “messages/refuses.xml”The refusal bag. When a girl refuses (a customer on a customer-facing job, or the whole shift), the engine picks one eligible line from here instead of a work.xml line. Same authoring shape as work.xml – a <Bank> of weighted <Text> variants, each with an optional <When> gate – so everything you know from shift messages carries straight over.
<?xml version="1.0" encoding="UTF-8"?><Bank> <!-- Baseline pool: ungated lines so every refusal has something to show. --> <Text weight="3"> ${name} crossed her arms and refused to follow the customer to a room. </Text>
<!-- Personality overlays: gate on traits, refusal axes, or the reason. --> <Text weight="2"> <When><Trait id="Noble"/></When> ${name} regarded the customer the way one regards mud on a clean floor, and informed him he had mistaken her for something purchasable. </Text> <Text weight="2"> <When><Dignity ge="70"/></When> ${name} drew herself up, looked the customer dead in the eye, and refused. He thought better of pressing the point. </Text> <Text weight="2"> <When><Obedience le="20"/></When> ${name} spat at the customer's feet and stormed off. There was no convincing her. </Text> <Text weight="2"> <When><RefusalReason name="fear"/></When> ${name} cowered in the corner until the customer cursed and left. </Text></Bank>| Attribute | Notes |
|---|---|
id on <Bank> |
Optional; the engine identifies the bag by filename (refuses.xml). |
<Text> body |
Author the prose inline (above), or use id= plus a matching text/en.xml entry exactly like work.xml if you want it localized. |
weight |
Relative probability among eligible lines. Keep the ungated baseline lines at higher weights so they stay dominant when no overlay fires. |
Image="..." |
Optional per-line portrait override (e.g. profile, bdsm); defaults to the refuse image type for the bag. |
<When> child |
Gates the line. A refusal bag has the full <When> grammar plus <RefusalReason> and the <Dignity> / <Fear> / <Obedience> shorthands (see when-conditions.md). |
What a refusal line can key on:
- Personality traits –
<Trait id="Iron Will"/>,<Trait id="Shy"/>,<Trait id="Aggressive"/>,<Trait id="Noble"/>, so a proud girl refuses differently than a timid one. - Character-state axes –
<Dignity ge="70"/>(proud),<Obedience le="20"/>(defiant),<Fear ge="60"/>(afraid of you),<Stat name="Happiness" le="20"/>(despairing),<Stat name="Tiredness" ge="80"/>(exhausted),<Stat name="PCHate" ge="70"/>(resentful of you). - The inferred reason –
<RefusalReason name="dignity|rebellion|fear|hate|daunted|disease|orientation|beast|generic"/>, so the prose matches why she balked. - A specific girl –
<Girl name="..."/>for per-character refusal lines (a character pack can ship her own).
<Performance> gates do not belong here: the shift’s performance score is not computed before a refusal, so a <Performance> gate in a refusal bag silently never matches.
The whore jobs ship a refusal bag today; other customer-facing jobs gain their own refuses.xml as the refusal model extends to them. Until a job has its own bag (or while a <When> matches nothing), the engine falls back to a generic refusal line, so a partial bag is always safe.
messages/banks.xml
Section titled “messages/banks.xml”Optional fragment banks for composing a message from interchangeable phrases instead of writing every combination out by hand. A bank is a named pool of short prose fragments; a work.xml (or refuses.xml) line drops a ${pick:<bank-id>} slot into its body, and the engine fills each slot independently when the message fires.
Composition is optional and additive – it does not replace full authored messages. It lets one part of an otherwise hand-written sentence vary. Fixed lines and composed lines coexist freely in the same work.xml bag, and most jobs use no banks at all. A composed message is still authored prose; the bank just supplies the variable phrase.
The 80 / 20 pattern
Section titled “The 80 / 20 pattern”The intended shape is mostly authored prose with one small dynamic slot – roughly 80% fixed sentence, 20% picked fragment. The sentence, tone, punctuation, and any mechanics stay in the top-level message; only the genuinely variable phrase comes from a bank.
<!-- messages/work.xml: the authored line, gated to a performance tier --><Text id="escort.work.outing.good" weight="2"> <When><Performance ge="145" le="184"/></When></Text><!-- text/en.xml: 80% authored, 20% dynamic --><Text id="escort.work.outing.good">${name} accompanied her client to ${pick:outing}, kept him easy company all evening, and saw him home satisfied.</Text>banks.xml structure
Section titled “banks.xml structure”A root <Banks> holds one or more named <Bank id="..."> elements; each is a pool of <Text> fragments. Fragments use the same vocabulary as work.xml lines – an id-ref into text/en.xml, a weight, and a <When> gate – plus a priority tier.
<?xml version="1.0" encoding="UTF-8"?><Banks> <Bank id="song-genre"> <!-- generic tier (priority 0, ungated): the default pool --> <Text id="barsinger.genre.rock"/> <Text id="barsinger.genre.classical"/> <Text id="barsinger.genre.country"/> <!-- ...seven in total... -->
<!-- trait override (priority 10): wins ~60% of the time when Aggressive --> <Text id="barsinger.genre.deathmetal" priority="10"> <When><Trait id="Aggressive"/><RandomChance pct="60"/></When> </Text> </Bank>
<Bank id="song-quality"> <Text id="barsinger.quality.perfectly"><When><Performance ge="245"/></When></Text> <Text id="barsinger.quality.well"><When><Performance ge="145" le="184"/></When></Text> <!-- ...one per performance tier, covering the whole range with no gap... --> </Bank></Banks>| Attribute | Notes |
|---|---|
id on <Bank> |
The bank name referenced by ${pick:<id>}. |
id on <Text> |
The fragment’s text/en.xml key, which holds its prose body. |
weight |
Relative probability within the selected priority tier. Default 1. |
priority |
Selection tier. Default 0. See Selection below. |
<When> child |
Gates the fragment, evaluated in the same shift context as the parent message, with the full when-conditions.md grammar. |
${pick:<bank-id>} in a body
Section titled “${pick:<bank-id>} in a body”Inside a work.xml / refuses.xml body (in text/en.xml), ${pick:<bank-id>} is replaced by one fragment chosen from that bank. The pick: prefix is required: it keeps a bank reference distinct from a <Bind> value that happens to share the name. Several slots, and repeats of the same bank, each roll independently:
<Text id="barmaid.work.mix">${name} mixed ${pick:cocktail} and ${pick:cocktail} without spilling a drop.</Text>${pick:...} is resolved first, before ${name} / ${shift} / bind tokens, so a fragment may itself contain ${name} (interpolated afterwards). A fragment may not contain another ${pick:...} – recursion is rejected at load.
Selection: priority first, then weight
Section titled “Selection: priority first, then weight”For each slot the engine:
- evaluates every fragment’s
<When>once and keeps the eligible ones; - finds the highest
priorityamong those eligible; - picks one fragment from that top tier only, by
weight.
A higher-priority fragment therefore wins outright when it is eligible, instead of being averaged into the generic pool. This is what makes “60% Death Metal if Aggressive, otherwise a normal genre” work: the override sits at priority="10", gated on <Trait id="Aggressive"/><RandomChance pct="60"/>. When the trait is present and the 60% roll passes it is the only top-tier fragment and wins; otherwise it is ineligible and selection falls back to the priority="0" generics. If nothing is eligible the slot resolves to empty – so always give a bank an ungated generic tier.
The gate syntax is exactly the runtime grammar (see when-conditions.md): chance is <RandomChance pct="60"/> (the attribute is pct=), performance is <Performance ge="245"/> / <Performance ge="185" le="244"/>, traits are <Trait id="..."/>.
Randomness
Section titled “Randomness”Fragment conditions and selection use the RNG supplied to job resolution – the same stream the rest of the shift draws from – not a separate global path. Seeded tests are therefore reproducible; in normal play the stream is the shared game RNG, so composition is as random as any other shift roll.
Mechanics stay on the top-level message
Section titled “Mechanics stay on the top-level message”Fragments are prose only. A fragment may not carry Updates=, Image=, or any effect; the loader rejects a bank that tries. The mechanically meaningful outcome – and its Updates=, applied exactly once – is the top-level work.xml line; the fragment only varies wording. This guarantees that re-wording, re-weighting, or localizing a fragment can never change a shift’s gameplay result. If a scored sub-event needs to vary, model it as a separate top-level outcome, not a fragment.
Validation failures
Section titled “Validation failures”banks.xml is checked when the job loads; a failure stops the job from registering and logs a [DataJobs] error rather than shipping a broken line. A bank is rejected when:
- a
${pick:<id>}in a message references a bank that does not exist; - a fragment body contains another
${pick:...}(recursion); - a fragment carries
Updates=,Image=, or an effect (not prose-only); - a declared
<Bank>has no fragments.
Worked examples
Section titled “Worked examples”- BarSinger (
resources/jobs/barsinger/) – the complete example: two banks (song-genrexsong-quality) composing${name} sang ${pick:song-genre} ${pick:song-quality}., with trait overrides and performance-gated quality, sitting alongside the job’s existing fixed lines. - Escort (
resources/jobs/escort/) – the mixed example: a single pure-flavoroutingbank fills one slot in otherwise fully-authored lines. Escort’s normal job mechanics (performance, wage, per-shift effects) live inperformance.xml/wage.xml/effects.xml– entirely outside the fragments, and untouched by which venue is named.
examples/jobs-cookbook.md has a step-by-step composition recipe.
text/en.xml
Section titled “text/en.xml”Localizable text strings. Each <Text id="..."> entry provides the display string for the matching id in messages/work.xml.
<?xml version="1.0" encoding="UTF-8"?><Locale lang="en"> <Text id="barmaid.work.perfect.1">${name} was sliding drinks all over the bar without spilling a drop.</Text> <Text id="barmaid.work.ok.1">${name} made a few mistakes but none of them were lethal.</Text></Locale>Token interpolation in <Text>
Section titled “Token interpolation in <Text>”The <Text> body recognizes a small placeholder grammar evaluated at message-emission time:
| Token | Substitution |
|---|---|
${name} |
The girl’s display name. |
${shift} |
The current shift name (e.g. day, night). |
${<bind_name>} |
The integer value of any <Bind> declared earlier in the same <Group> as the message; useful for reporting a derived performance score, a tip total, etc. |
${pick:<bank-id>} |
One randomly selected fragment from the named bank in messages/banks.xml. Resolved before the tokens above, so a fragment may itself contain ${name}. See messages/banks.xml. |
$$ |
A literal $ character. |
Unknown tokens are left in place verbatim, so a typo like ${nmae} shows up in the turn summary rather than corrupting the message; this also keeps packs authored against a newer engine readable on an older one.
<Group> <Bind name="tip" expr="(Charisma + Service) / 20"/> <Text id="barmaid.work.great.1">${name} pulled in ${tip} extra coins on the ${shift} shift.</Text></Group>This file only holds strings. The logic (weights, conditions) lives entirely in messages/work.xml. Translators only need to touch text/en.xml (or add a text/fr.xml sibling).
See also
Section titled “See also”examples/jobs-cookbook.md: worked recipes (pure-effect, performance+wage, parameterized multi-slot, composed messages)snippets/effects.xml: copy-paste starter for common effect patternsreference/when-conditions.md: full<When>condition language reference