Visible synergies
Some jobs work better when other jobs are also being worked. A bar with a Cook in the kitchen produces better tips for the Barmaid than a bar without one. A Singer with a Pianist backing her sounds different than a Singer alone. A Security guard in a bustling brothel has a busier shift than one in an empty room.
This is synergy. Crossgate models it in two layers:
- The numeric layer: a
<Mod name="work.foo"/>hook ineffects.xmlthat adjusts the girl’s shift output based on what else is happening in the brothel. The engine reads this through internal channels. - The narrative layer: visible message variants in
messages/work.xmlthat describe the synergy state in the prose, so the player learns from the narration what would otherwise only be visible in the gold totals.
This page covers the narrative layer in depth, with the numeric layer as scaffolding. If you’re authoring a custom job that synergizes with another job, this is the discipline to follow.
Why visible synergies?
Section titled “Why visible synergies?”Many older mods treat synergies as silent multipliers. A Cook bumps tip income by some hidden scalar; the player sees more gold at end of shift but never reads anything in the prose explaining why. The synergy is a wiki/datamining fact, not a story fact.
Crossgate’s rule: every synergy has at least one variant per perf bucket that names the partner role in the prose, gated on the actual synergy state. The player learns the synergy from reading shift summaries. No wiki required.
The four partner-direction shapes
Section titled “The four partner-direction shapes”There are four different shapes of “what plays the role of partner” in a synergy. When you author a synergy-bearing job, your first decision is which shape applies.
1. partner-presence flag (binary 0/1)
Section titled “1. partner-presence flag (binary 0/1)”Another girl assigned to a specific job in the same brothel right now.
| Job | Reads | Publisher |
|---|---|---|
| Barsinger | piano_present |
Piano (the Pianist’s job effects.xml) |
| Barstripper | sleazybarmaid_present |
Sleazy Barmaid |
| Sleazy Barmaid | barstripper_present |
Barstripper (mutual cross-consumer) |
| Sleazy Waitress | barstripper_present |
Barstripper |
Authoring shape:
<!-- in messages/work.xml: 4 perf buckets x {with_partner, solo} = 8 variants --><Text id="barsinger.work.perfect.with_piano" weight="3"> <When> <Performance ge="245"/> <SynergyGE key="piano_present" value="1"/> </When></Text><Text id="barsinger.work.perfect.solo" weight="2"> <When> <Performance ge="245"/> <SynergyLT key="piano_present" value="1"/> </When></Text>The <SynergyGE> / <SynergyLT> leaves gate each variant on whether the partner is actually on shift, so the with_piano line only fires when a Pianist is working and the solo line only when none is. See when-conditions.md for the leaf grammar and the full list of synergy keys.
2. partner = brothel-state scalar
Section titled “2. partner = brothel-state scalar”A numeric attribute on the brothel itself, often player-set.
| Job | Reads | Publisher |
|---|---|---|
| Advertising | advertising_budget (player slider, gold/week) |
(player) |
Authoring shape:
<!-- in messages/work.xml --><Text id="advertising.work.with_budget" weight="3"> <When> <Performance ge="100"/> <BrothelStat key="advertising_budget" ge="500"/> </When></Text><BrothelStat> reads the per-brothel scalar (here the player’s advertising-budget slider) and compares it with the usual ops (ge / lt / …). Budget-specific prose (“gold flowing into posters”) fires only when the budget actually clears the threshold, so it never lands on a zero-budget shift. Pair it with an lt-gated “lean budget” variant for the other side.
3. partner = headcount channel
Section titled “3. partner = headcount channel”A live count of girls in some role(s) on shift.
| Job | Reads | Publisher |
|---|---|---|
| Security | working_girls (total girls assigned to the brothel) |
engine |
Authoring shape:
<Text id="security.work.busy_brothel" weight="3"> <When> <Performance ge="100"/> <BrothelStat key="working_girls" ge="3"/> </When></Text><Text id="security.work.quiet_brothel" weight="2"> <When> <Performance ge="100"/> <BrothelStat key="working_girls" lt="2"/> </When></Text>Headcount is just another <BrothelStat> key (working_girls = total girls assigned to the brothel), so “stretched thin in a busy brothel” fires only when the count is high and “quiet shift” only when it is low.
4. partner = per-girl-item-state
Section titled “4. partner = per-girl-item-state”A property of the working girl herself, typically the equipment/inventory she carries into the shift. Different from the first three: there is no other girl, no brothel-wide scalar, no crowd. The “partner” is gear.
| Job | Reads | Publisher |
|---|---|---|
| Escort | per-girl Inventory item-tag (e.g. quality_outfit) |
(player-equipped) |
Authoring shape:
<Text id="escort.work.with_quality_outfit" weight="3"> <When> <Performance ge="200"/> <HasItemTag tag="quality_outfit"/> </When></Text><HasItemTag> fires when the girl carries any item declaring a matching <Tag> in its .itemsx definition, so prose that names “her silk gown caught the lamplight” only fires when she is actually carrying tagged gear. The base-game Escort job doesn’t ship these variants yet, but the leaf is live for your own packs.
Choosing weights
Section titled “Choosing weights”All four synergy leaves (<SynergyGE> / <SynergyLT>, <BrothelStat>, <HasItemTag>) are live, so you gate each variant on the real state and pick weights for how often the prose reads:
| Synergy direction | Gate leaf | Typical weighting |
|---|---|---|
| partner-presence flag | <SynergyGE> / <SynergyLT> |
3:2 with-partner : solo |
| partner = brothel-state scalar | <BrothelStat key="..." ge/lt="N"/> |
a ge-gated and an lt-gated variant |
| partner = headcount channel | <BrothelStat key="working_girls" ge/lt="N"/> |
busy vs quiet pair |
| partner = per-girl-item-state | <HasItemTag tag="..."/> |
with-gear variant plus an ungated fallback |
The rule: gate every state-specific claim on the leaf that proves it, and always keep an ungated (or lt / solo) variant so a bucket has a line to fall back to.
Discipline for every synergy-bearing job
Section titled “Discipline for every synergy-bearing job”When you author a job that registers OR consumes a synergy hook:
- At least 3 perf buckets carry synergy variants. Typically perfect / great / good / ok. The bad / worst buckets often skip synergy variants because at those Performance levels the failure dominates the prose.
- Variants reference the partner by role, not by stat or by mechanic. Write “with the cook keeping food coming” not “with food_quality > 50.”
- Trait-conditioned overlays stack on top of synergy variants, not in place of them. A Charismatic singer with no Piano backing still gets her Charismatic-line; she just gets it from the
solobucket instead of thewith_pianobucket. - Gate every state-specific variant on its
<When>leaf (<SynergyGE/LT>,<BrothelStat>,<HasItemTag>), and keep an ungated or opposite-side variant in the same bucket so a line always fires. - Shift-summary attribution line (optional but recommended): when the synergy fires, surface the magnitude in a one-line attribution: “Food bonus from Cook: +12 tips.” The player sees both the narrative (prose variant) and the magnitude (attribution line).
Worked example: a custom Bouncer job
Section titled “Worked example: a custom Bouncer job”Suppose you’re authoring a Bouncer (different from Security; a Bouncer’s shift gets busier the more girls are working the floor):
effects.xml:
<Effects> <Mod name="work.bouncer"/> <SetStat target="self" stat="Tiredness" delta="6"/></Effects>The hook work.bouncer doesn’t exist in the engine today. That’s fine; unknown <Mod> names parse and no-op silently (forward-compat). When the engine later registers work.bouncer (your packaged C++ extension, or a future Crossgate update), the hook fires.
messages/work.xml (using direction 3, partner = headcount):
<Bank id="work"> <!-- Fallback pair: one always-eligible line per tone --> <Text id="bouncer.work.calm.1" weight="1"/> <Text id="bouncer.work.rowdy.1" weight="1"/>
<!-- Busy-floor variant: fires when the brothel is crowded. --> <Text id="bouncer.work.busy_floor" weight="3"> <When> <Performance ge="100"/> <BrothelStat key="working_girls" ge="3"/> </When> </Text>
<!-- Empty-floor variant: fires when it is quiet. --> <Text id="bouncer.work.empty_floor" weight="2"> <When> <Performance ge="100"/> <BrothelStat key="working_girls" lt="2"/> </When> </Text></Bank>text/en.xml:
<Locale lang="en"> <Text id="bouncer.work.calm.1">${name} kept the floor calm tonight, watching for trouble that never came.</Text> <Text id="bouncer.work.rowdy.1">${name} broke up two fights and threw out three drunks before the night was through.</Text> <Text id="bouncer.work.busy_floor">With the strippers and singers drawing crowds all night, ${name} barely had a moment off her feet; the floor needed eyes everywhere.</Text> <Text id="bouncer.work.empty_floor">A quiet brothel meant a quiet shift; ${name} spent most of it leaning against the doorframe, watching the rare customer trickle in.</Text></Locale>The busy/empty variants gate on working_girls and fire as soon as the brothel’s headcount crosses the threshold; the always-eligible fallback pair guarantees every shift still has a line.
Reference
Section titled “Reference”effects-reference.md: the<Mod>element + the registered hook listjobs-reference.md:messages/work.xmlandtext/en.xmlschemawhen-conditions.md:<When>clause grammar