Jobs cookbook
Four recipes covering the most common job shapes. Each one is drawn directly from shipped game data so you can compare against the real files under resources/jobs/.
For the complete schema (every attribute, every element), see reference/jobs-reference.md.
Recipe 1: Pure-effect job (no gold, no performance)
Section titled “Recipe 1: Pure-effect job (no gold, no performance)”When to use this shape: rest jobs, household chores, training sessions. The girl does something useful each shift but earns no gold and has no performance score. The shift summary shows a single flat message.
Exemplar: resources/jobs/houserest/
This job has only three files:
houserest/ job.xml effects.xml messages/work.xml text/en.xmljob.xml
Section titled “job.xml”<?xml version="1.0" encoding="UTF-8"?><Job id="houserest" schema="1"> <Title>House Rest</Title> <Description>She relaxes at home, recovering energy and health.</Description> <DefaultImage>profile</DefaultImage></Job>No <Eligibility> block means any girl can be assigned. No performance.xml or wage.xml means no gold is paid.
effects.xml
Section titled “effects.xml”<?xml version="1.0" encoding="UTF-8"?><Effects> <SetStat target="self" stat="Tiredness" delta="-25"/> <SetStat target="self" stat="Happiness" delta="10"/> <SetStat target="self" stat="Health" delta="15"/> <SetStat target="self" stat="Mana" delta="10"/></Effects>Applied unconditionally every shift. Negative delta decreases the stat; positive increases it.
messages/work.xml
Section titled “messages/work.xml”<?xml version="1.0" encoding="UTF-8"?><Bank id="work"> <Text id="houserest.work.default" weight="1"/></Bank>One entry, no <When>, always fires. The content is in text/en.xml.
text/en.xml
Section titled “text/en.xml”<?xml version="1.0" encoding="UTF-8"?><Locale lang="en"> <Text id="houserest.work.default">She relaxed at home and recovered her energy.</Text></Locale>That is the entire job. No performance curve, no wage, no gains file needed.
Recipe 2: Performance + wage job
Section titled “Recipe 2: Performance + wage job”When to use this shape: any job where the girl earns gold and better stats produce a better result. The engine computes a performance score from her stats and skills, maps it to a gold amount via a curve, then picks a shift message that matches her tier.
Exemplar: resources/jobs/barmaid/
This job uses all six files:
barmaid/ job.xml performance.xml wage.xml gains.xml effects.xml (not present for barmaid, but common) messages/work.xml text/en.xmlThe pipeline
Section titled “The pipeline”performance.xmlcomputes a score from the girl’s stats and skills.wage.xmlmaps that score to gold.gains.xmldistributes skill XP.messages/work.xmlpicks a message based on the same score.
performance.xml
Section titled “performance.xml”<?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="Optimist" delta="5"/> <TraitMod trait="Cum Addict" delta="-5"/></Performance>Each <Factor> multiplies the girl’s value by its weight. The engine sums all factors and divides by the total weight to produce a normalised score. <TraitMod> entries add or subtract flat points if the girl has that trait.
wage.xml
Section titled “wage.xml”<?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>Points must be listed in descending performance order. The engine finds the two breakpoints the girl’s score falls between and interpolates linearly. Negative wages cost the player money (the girl caused more damage than she earned).
gains.xml
Section titled “gains.xml”<?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>15 XP is distributed across the listed entries proportional to their weights. baseSkill="3" adds a flat 3 points on top for each entry.
messages/work.xml (excerpt)
Section titled “messages/work.xml (excerpt)”<?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.perfect.charming" weight="2"> <When> <Performance ge="245"/> <Trait id="Charming"/> </When> </Text> <Text id="barmaid.work.great.1" weight="2"> <When><Performance ge="185" le="244"/></When> </Text> <Text id="barmaid.work.good.1" weight="3"> <When><Performance ge="145" le="184"/></When> </Text> <Text id="barmaid.work.worst.1" weight="1"> <When><Performance le="69"/></When> </Text></Bank>Notice barmaid.work.perfect.charming: it has higher weight (2 vs 1) so it fires more often for charming girls at the top tier. Multiple entries can match the same shift; the engine picks one proportionally by weight.
Recipe 3: Parameterized multi-slot job
Section titled “Recipe 3: Parameterized multi-slot job”When to use this shape: several job slots that differ only in one dimension, so you want to avoid duplicating most of the XML. One data directory registers under multiple job IDs, each with a different parameter value. <JobParam> conditions in effects.xml and messages/work.xml gate which blocks fire for which slot.
Exemplar: resources/jobs/houseso/
Three job slots (Straight Training, Bisexual Training, Lesbian Training) all share one houseso/ directory. They are registered in engine code with orientation=straight, orientation=bi, and orientation=lesbian respectively.
job.xml
Section titled “job.xml”<?xml version="1.0" encoding="UTF-8"?><Job id="houseso" schema="1"> <Title>Sexual Orientation Training</Title> <Description>She undergoes orientation training. The exact effect depends on the orientation parameter set when the job is registered.</Description> <DefaultImage>profile</DefaultImage></Job>effects.xml (abridged)
Section titled “effects.xml (abridged)”<?xml version="1.0" encoding="UTF-8"?><Effects> <Group> <When> <JobParam name="orientation" value="straight"/> </When> <SetSkill target="self" skill="NormalSex" delta_min="2" delta_max="4"/> <SetSkill target="self" skill="Anal" delta_min="1" delta_max="2"/> <SetSkill target="self" skill="Lesbian" delta_min="-1" delta_max="-3"/> <SetStat target="self" stat="Tiredness" delta="5"/> <SetStat target="self" stat="Exp" delta="2"/> </Group> <Group> <When> <JobParam name="orientation" value="bi"/> </When> <SetSkill target="self" skill="NormalSex" delta_min="1" delta_max="2"/> <SetSkill target="self" skill="Lesbian" delta_min="1" delta_max="2"/> <SetSkill target="self" skill="Group" delta_min="2" delta_max="4"/> <SetStat target="self" stat="Tiredness" delta="5"/> <SetStat target="self" stat="Exp" delta="2"/> </Group> <Group> <When> <JobParam name="orientation" value="lesbian"/> </When> <SetSkill target="self" skill="Lesbian" delta_min="2" delta_max="4"/> <SetSkill target="self" skill="NormalSex" delta_min="-3" delta_max="-1"/> <SetSkill target="self" skill="Anal" delta_min="-1" delta_max="0"/> <SetStat target="self" stat="Tiredness" delta="5"/> <SetStat target="self" stat="Exp" delta="2"/> </Group></Effects>Only the group whose <JobParam> matches the slot’s registered parameter will fire. The other two groups are skipped entirely.
messages/work.xml
Section titled “messages/work.xml”<?xml version="1.0" encoding="UTF-8"?><Bank id="work"> <Text id="houseso.work.straight" weight="1"> <When> <JobParam name="orientation" value="straight"/> </When> </Text> <Text id="houseso.work.bi" weight="1"> <When> <JobParam name="orientation" value="bi"/> </When> </Text> <Text id="houseso.work.lesbian" weight="1"> <When> <JobParam name="orientation" value="lesbian"/> </When> </Text></Bank>Each slot sees exactly one eligible message because only one <JobParam> condition matches per run.
text/en.xml
Section titled “text/en.xml”<?xml version="1.0" encoding="UTF-8"?><Locale lang="en"> <Text id="houseso.work.straight">She underwent straight orientation training.</Text> <Text id="houseso.work.bi">She underwent bisexual orientation training.</Text> <Text id="houseso.work.lesbian">She underwent lesbian orientation training.</Text></Locale>The parameter pattern works for any dimension where two or three slots share most of their logic. If the slots differ in more than one dimension, or have very different effect lists, separate directories are cleaner.
Recipe 4: Composed messages (a dynamic slot inside authored prose)
Section titled “Recipe 4: Composed messages (a dynamic slot inside authored prose)”When to use this shape: a shift line has one genuinely independent, flavor-only phrase that varies, such as what she sang, the venue she was taken to or the drink she mixed. Writing every combination by hand would create dozens of near-duplicate strings. Composition lets you author the sentence once and fill the variable phrase from a named bank. Use fragment slots only for independently variable, prose-only phrases; the surrounding authored message and the job’s normal systems may still carry mechanics.
Exemplars: resources/jobs/barsinger/ (full: two banks recombined), resources/jobs/escort/ (mixed: one flavor slot in otherwise authored lines; the job’s mechanics stay in its normal systems, outside the fragments).
Composition is additive: fixed and composed lines live in the same work.xml bag. You are adding one composed line, not rewriting the others.
messages/banks.xml
Section titled “messages/banks.xml”Declare the named fragment pools. BarSinger composes “what she sang” against “how well she sang it”:
<?xml version="1.0" encoding="UTF-8"?><Banks> <Bank id="song-genre"> <!-- generic tier: ungated, priority 0 --> <Text id="barsinger.genre.rock"/> <Text id="barsinger.genre.classical"/> <Text id="barsinger.genre.country"/> <!-- 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> <Text id="barsinger.quality.painfully"><When><Performance le="69"/></When></Text> <!-- ...one fragment per performance tier, covering the whole range (success through failure) with no gap... --> </Bank></Banks>messages/work.xml
Section titled “messages/work.xml”The composed line is an ordinary work.xml entry; it just happens to use ${pick:...} slots in its body. It sits alongside the fixed lines:
<Bank id="work"> <!-- existing fixed lines stay exactly as they are --> <Text id="barsinger.work.great.1" weight="2"><When><Performance ge="185" le="244"/></When></Text> <!-- the new composed line, ungated so it joins the pool across ALL performance: it narrates success and failure alike, because song-quality has a fragment for every tier. --> <Text id="barsinger.work.song" weight="3"/></Bank>You could instead gate the composed line to a performance band (for example, only strong shifts); it is an ordinary <Text>. BarSinger leaves it ungated so composition varies the bad narration as well as the good.
text/en.xml
Section titled “text/en.xml”The fragment bodies, plus the composed sentence with its slots:
<Locale lang="en"> <Text id="barsinger.genre.rock">rock songs</Text> <Text id="barsinger.genre.deathmetal">death metal songs</Text> <Text id="barsinger.quality.perfectly">perfectly, holding the room silent until the final note</Text> <Text id="barsinger.quality.well">well, keeping the bar's attention through the set</Text> <Text id="barsinger.quality.badly">painfully off-key, until even the nearest tables stopped pretending not to notice</Text>
<Text id="barsinger.work.song">${name} filled her set with ${pick:song-genre} and sang them ${pick:song-quality}.</Text></Locale>Because the line is ungated and the quality bank spans every tier, one composed sentence now covers success, mediocrity and failure. The existing fixed lines still appear too:
- success: “Anna chose a lively ragtime number and played it flawlessly, leaving the room silent until the last note faded.”
- mediocrity: “Anna chose a gentle waltz and played it haltingly, with pauses that broke the piece apart.”
- failure: “Anna chose a classical etude and played it badly out of time, until nearby patrons began to wince.”
Those three use the BarPiano pattern. BarSinger follows the same range, from holding the room silent to driving nearby tables to stop pretending not to notice. Give low-performance fragments as much character as high-performance ones; a flat “badly” wastes the slot.
Keep fragments prose-only. A fragment may not carry Updates=, Image=, or effects; the loader rejects it. The mechanics (and any Updates=) live on the top-level work.xml line and apply exactly once, so which fragment is chosen never changes the shift’s result. The Escort exemplar leans on this: its outing venue is pure flavor, while the Escort score comes from performance.xml / wage.xml / effects.xml.
See reference/jobs-reference.md (messages/banks.xml) for the full attribute list, the priority-then-weight selection rule, the seeded-RNG note, and the validation failures.
See also
Section titled “See also”reference/jobs-reference.md: full attribute and element referencesnippets/effects.xml: copy-paste starter for common effect patternsreference/when-conditions.md: full<When>condition language