diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index fe60b8c..b302ee7 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -4933,6 +4933,28 @@ function doCast(prev: GameState, cmd: Extract): Comma } return { ok: true, state, events: events2 }; } + // CHAOS aims at no one: cast bare, it sweeps every other living + // wizard into the pile, each offered their FULL SHIELD sit-out in + // turn order. (Full shields only exclude; nothing in the set cancels + // a zero-point storm, so the queue is the whole defense.) The older + // player-targeted form is still accepted below, so stored games + // replay unchanged. + if (inHand.cardId === "chaos" && (!cmd.target || cmd.target.kind !== "player")) { + consumeCast(state, caster, inHand, mods, false); + if (state.turn.attackUsed) state.turn.secondAttackUsed = true; + state.turn.attackUsed = true; + 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, + }]; + const queue = turnOrderFrom(state, caster.id).filter((id) => + id !== caster.id && state.players.find((p) => p.id === id)!.alive); + state.chaosPending = { casterId: caster.id, excluded: [], queue }; + finishChaosIfReady(state, events); + return { ok: true, state, events }; + } if (!cmd.target || cmd.target.kind !== "player") return err("attack spells target a player"); if (cmd.target.playerId === caster.id) return err("you cannot attack yourself"); const target = state.players.find((p) => p.id === (cmd.target as { playerId: PlayerId }).playerId); diff --git a/packages/engine/test/casting.test.ts b/packages/engine/test/casting.test.ts index 6edee4b..ecc9f38 100644 --- a/packages/engine/test/casting.test.ts +++ b/packages/engine/test/casting.test.ts @@ -402,6 +402,35 @@ describe("the ward window and chaos shields", () => { expect(state.players.find((p) => p.id === owner.id)!.hand.some((c) => c.cardId === "ward")).toBe(false); }); + it("chaos casts bare — no target, every other wizard queued for a shield", () => { + let { state } = threeGame(); + state = toRound2(state); + state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 }); + const caster = activePlayer(state); + const others = state.players.filter((p) => p.id !== caster.id).map((p) => p.id); + const chaos = giveCard(state, caster.id, "chaos", "C", 0); + const shielder = others[0]!; + giveCard(state, shielder, "full-shield", "S", 0); + const shielderHand = () => state.players.find((p) => p.id === shielder)!.hand.map((c) => c.instanceId).sort(); + const kept = shielderHand().filter((id) => id !== "full-shield#S").sort(); + + state = must(state, caster.id, { type: "cast", instanceId: chaos.instanceId }); + // No stack: the queue opens at once, in turn order from the caster. + expect(state.stack).toBeNull(); + expect(state.chaosPending?.queue.length).toBe(2); + const first = state.chaosPending!.queue[0]!; + state = first === shielder + ? must(state, first, { type: "counteract", instanceId: "full-shield#S" }) + : must(state, first, { type: "pass" }); + const second = state.chaosPending!.queue[0]!; + state = second === shielder + ? must(state, second, { type: "counteract", instanceId: "full-shield#S" }) + : must(state, second, { type: "pass" }); + expect(state.chaosPending).toBeNull(); + // The shielded hand rode out the storm untouched. + expect(shielderHand()).toEqual(kept); + }); + it("chaos: bystanders may shield out, reflections are refused", () => { let { state } = threeGame(); state = toRound2(state); diff --git a/packages/web/src/net.svelte.ts b/packages/web/src/net.svelte.ts index a214ccf..04f4d07 100644 --- a/packages/web/src/net.svelte.ts +++ b/packages/web/src/net.svelte.ts @@ -226,6 +226,7 @@ const NOTABLE_EVENTS = new Set([ "firewallBurned", "objectThrown", "gameWon", "positionsSwapped", "teleported", "stonesDestroyed", "thumbOfGod", "stoneTurnedToWater", "waterwallCrashes", "sectorRotated", "sectorRelocated", "homeBasesSwapped", + "handsScrambled", ]); const SEAT_KEY = "wizwar-seat";