Skip to content

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:

  1. The numeric layer: a <Mod name="work.foo"/> hook in effects.xml that adjusts the girl’s shift output based on what else is happening in the brothel. The engine reads this through internal channels.
  2. The narrative layer: visible message variants in messages/work.xml that 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.

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.

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.

Another girl assigned to a specific job in the same brothel right now.

JobReadsPublisher
Barsingerpiano_presentPiano (the Pianist’s job effects.xml)
Barstrippersleazybarmaid_presentSleazy Barmaid
Sleazy Barmaidbarstripper_presentBarstripper (mutual cross-consumer)
Sleazy Waitressbarstripper_presentBarstripper

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"/>
<!-- Future: <SynergyGE key="piano_present" value="1"/> -->
</When>
</Text>
<Text id="barsinger.work.perfect.solo" weight="2">
<When>
<Performance ge="245"/>
<!-- Future: <SynergyLT key="piano_present" value="1"/> -->
</When>
</Text>

Today the <When> clauses ride a Performance-only fallback gate because the brothel-state leaves haven’t shipped in <When> grammar yet. Both variants will fire at high Performance regardless of whether the partner is actually present. This is acceptable for partner-presence prose because “with the pianist’s backing” is plausibly true at high Performance; the audience can’t always tell whether it was the singer or the piano carrying the night, so the prose stays defensible.

A numeric attribute on the brothel itself, often player-set.

JobReadsPublisher
Advertisingadvertising_budget (player slider, gold/week)(player)

Authoring shape:

<!-- in messages/work.xml -->
<Text id="advertising.work.with_budget" weight="0">
<When>
<Performance ge="100"/>
<!-- Future: <BrothelStatGE key="advertising_budget" value="500"/> AND flip weight 0->3. -->
</When>
</Text>

weight="0" for now. Budget-specific prose (“gold flowing into posters”) would fire on a high-Performance shift with zero budget under a Performance-only gate, producing wrong-context narration the player notices. Different from partner-presence: a singer carrying alone at high Performance still works as prose; an Advertiser on a zero-budget shift narrating “gold flowing into posters” doesn’t. So you ship the variant inert and flip the weight when the gate becomes real in a future engine version.

A live count of girls in some role(s) on shift.

JobReadsPublisher
Securityworking_girls (live Stripper + Whore count)engine

Authoring shape:

<Text id="security.work.busy_brothel" weight="0">
<When>
<Performance ge="100"/>
<!-- Future: <HeadcountGE key="working_girls" value="3"/> AND flip weight 0->3. -->
</When>
</Text>
<Text id="security.work.quiet_brothel" weight="0">
<When>
<Performance ge="100"/>
<!-- Future: <HeadcountLT key="working_girls" value="2"/> AND flip weight 0->2. -->
</When>
</Text>

Same weight="0" discipline as brothel-state scalar: “stretched thin in a busy brothel” vs “quiet shift” prose is too population-specific to fire on a Performance-only fallback today.

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.

JobReadsPublisher
Escortper-girl Inventory item-tag (e.g. quality_outfit)(player-equipped)

Authoring shape:

<Text id="escort.work.with_quality_outfit" weight="0">
<When>
<Performance ge="200"/>
<!-- Future: <HasItemTag tag="quality_outfit"/> AND flip weight 0->3. -->
</When>
</Text>

weight="0" for the same reason as directions 2 and 3: prose that names “her silk gown caught the lamplight” lands wrong when she went out in rags. The per-girl-item check has no <When> leaf yet (future inventory grammar), so synergy variants ride inert and the legacy fallback pair carries the prose.

Synergy directionVariants todayWhy
partner-presence flagweight > 0 (typically 3:2 with-partner:solo)Prose stays plausible at high Performance even when partner absent
partner = brothel-state scalarweight=0Prose names a specific state (budget, equipment, etc.) that a Performance-only gate can’t approximate
partner = headcount channelweight=0Prose names crowd density that a Performance-only gate can’t approximate
partner = per-girl-item-stateweight=0Prose names equipment the girl is carrying that a Performance-only gate can’t infer

The rule: if your prose claim would land wrong in the absence of the actual synergy state, ship the variant inert until the matching <When> leaf lands.

When you author a job that registers OR consumes a synergy hook:

  1. 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.
  2. Variants reference the partner by role, not by stat or by mechanic. Write “with the cook keeping food coming” not “with food_quality > 50.”
  3. 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 solo bucket instead of the with_piano bucket.
  4. Each weight="0" variant has a Future: comment naming the expected scalar key and the weight to flip to. This makes the eventual follow-up mechanical: search-replace, no re-derivation.
  5. 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).

Suppose you’re authoring a Bouncer (different from Security; Bouncer specifically reads Stripper + Singer headcount because those jobs draw rowdy crowds):

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">
<!-- Legacy fallback pair -->
<Text id="bouncer.work.calm.1" weight="3"/>
<Text id="bouncer.work.rowdy.1" weight="1"/>
<!-- High-rowdy variant. weight=0 until headcount leaves ship. -->
<!-- Future: <HeadcountGE key="performers" value="2"/> AND flip weight 0->3. -->
<Text id="bouncer.work.busy_floor" weight="0">
<When><Performance ge="100"/></When>
</Text>
<!-- Empty-floor variant. weight=0 until headcount leaves ship. -->
<!-- Future: <HeadcountLT key="performers" value="1"/> AND flip weight 0->2. -->
<Text id="bouncer.work.empty_floor" weight="0">
<When><Performance ge="100"/></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>

When the headcount leaves ship, you do four search-replaces (add <HeadcountGE> / <HeadcountLT>, flip weight="0" to weight="3" and weight="2") and the variants light up automatically. Until then your job ships safely with the legacy fallback pair.