Disease makes its caster the carrier (rules rev 7)

The card says it plainly — "You're the carrier! Disease caused by
spell does not affect you, only others" — but the engine cast it AT an
adjacent victim, making them the plague rat. At rev 7 it is a
self-cast on fear's pattern: no target, no counteraction window
("REFLECTIONs have no effect against this"), duration equals the
number, and sharing a square bites in both directions — the carrier
walking in, or anyone walking onto the carrier. Older ledgers hold
targeted disease casts with full stack exchanges, so the legacy attack
path survives for rev ≤6 games and all 33 ledgers replay clean. The
client offers the plain Cast button for it in rev-7 games.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-28 12:35:35 -04:00
co-authored by Claude Fable 5
parent db3b14fd51
commit b99c22f14a
3 changed files with 97 additions and 3 deletions
+35 -2
View File
@@ -234,8 +234,10 @@ export interface CastParams {
/** The revision new games are dealt under; GameConfig.deckRev pins it per game.
* Rev 6: STRENGTH's treasure-tear opens a counteraction window ("this would
* be an attack") instead of resolving instantly. */
export const CURRENT_RULES_REV = 6;
* be an attack") instead of resolving instantly.
* Rev 7: DISEASE is a self-cast plague ("You're the carrier!") the caster
* carries it, sharing a square bites both directions, no counteraction. */
export const CURRENT_RULES_REV = 7;
export interface GameConfig {
playerIds: PlayerId[];
@@ -2180,6 +2182,9 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
},
},
disease: {
// Legacy (rev ≤6): cast AT an adjacent victim through the attack
// stack; ledgers from those games replay this path. Rev 7 games
// never reach it — doCast intercepts disease as a self-cast plague.
kind: "attack",
baseDamage: () => 0,
sameSquare: false,
@@ -4223,6 +4228,18 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
}
checkVictory(state, events);
}
// Rev 7: sharing a plague square cuts both ways — walking INTO a
// carrier's square catches the disease's bite just the same.
if ((state.config.deckRev ?? 1) >= 7 && p.alive) {
for (const other of state.players) {
if (!other.alive || other.id === p.id) continue;
if (cellKey(other.position) !== cellKey(p.position)) continue;
if (sustainedOn(state, other.id, "disease").length === 0) continue;
applyDamage(state, events, p, 3, "disease", null, "physical");
checkVictory(state, events);
break;
}
}
if (crossedFirewall) {
events.push({ type: "firewallBurned", player: p.id });
@@ -4991,6 +5008,22 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
return null;
};
// DISEASE (rev 7): "You're the carrier!" — a self-cast plague on fear's
// pattern, no target and no counteraction window ("REFLECTIONs have no
// effect against this"). Older games cast it at a victim through the
// stack; their ledgers replay the legacy attack path below.
if (inHand.cardId === "disease" && (state.config.deckRev ?? 1) >= 7) {
consumeCast(state, caster, inHand, mods, false);
state.lastSpellUsed[caster.id] = inHand.cardId;
const events: GameEvent[] = [{
type: "spellCast", caster: caster.id, card: inHand, cardId: inHand.cardId,
numberCards: mods.numbers, numberValue: mods.magnitude.numberValue,
from: caster.position, target: null, targetCell: null,
}];
attachSustained(state, events, "disease", caster.id, caster.id, mods.magnitude.duration);
return { ok: true, state, events };
}
if (effect.kind === "attack") {
const pre = attackPreconditions(state);
if (pre) return err(pre);