diff --git a/packages/engine/src/automaton.ts b/packages/engine/src/automaton.ts index 6b2a653..9622d48 100644 --- a/packages/engine/src/automaton.ts +++ b/packages/engine/src/automaton.ts @@ -310,7 +310,7 @@ function respond(view: GameView, style: AutomatonStyle, tier: TierTraits): Comma } // Or vanish: INVISIBLE gives the attacker a 1-in-4 hit and lingers after. const vanish = find("invisible"); - if (vanish && !stack.creatureId && incoming >= 3 - flinch) { + if (vanish && (!stack.creatureId || view.deckRev >= 13) && incoming >= 3 - flinch) { const num = numbersInHand(view)[0]; return { type: "counteract", instanceId: vanish.instanceId, diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 35b96ed..1e24b12 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -4644,7 +4644,9 @@ function doCounteract( // The spell truly goes up — duration from an attached NUMBER card — and // the attacker's 1-in-4 hit roll happens at resolution. if (card.cardId === "invisible") { - if (stack.creatureId) return err("the creature shares your square — there is nowhere to hide"); + if (stack.creatureId && (state.config.deckRev ?? 1) < 13) { + return err("in this game's revision, creatures are not fooled by the vanishing"); + } let duration = 1; const attached = (numberInstanceIds ?? []) .map((id) => player.hand.find((c) => c.instanceId === id)) @@ -4810,9 +4812,11 @@ function resolveStack(state: GameState, events: GameEvent[]): void { const effect = attackId ? (CARD_EFFECTS[attackId] as AttackEffect) : null; // Hit rolls against INVISIBLE (attacker guesses a direction: 1-in-4) and - // SHRINK (50% miss). "If a spell misses, it dissipates harmlessly." A - // creature sharing the square has nothing to aim: no miss rolls. - if (!stack.creatureId && sustainedOn(state, defender.id, "invisible").length > 0) { + // SHRINK ("Reduces opponent's chance to hit you by 50% in any attack"). + // Creature blows roll the same dice from rev 13 — the cards carve out no + // exception for monsters; earlier games skipped the rolls and replay so. + const missRolls = !stack.creatureId || (state.config.deckRev ?? 1) >= 13; + if (missRolls && sustainedOn(state, defender.id, "invisible").length > 0) { const roll = rollD4(state, events, attacker.id, "aiming at the unseen — only a 1 finds them"); if (roll !== 1) { events.push({ type: "attackMissed", attacker: attacker.id, defender: defender.id, attackCardId: attackId, because: "invisible" }); @@ -4820,7 +4824,7 @@ function resolveStack(state: GameState, events: GameEvent[]): void { return; } } - if (!stack.creatureId && sustainedOn(state, defender.id, "shrink").length > 0) { + if (missRolls && sustainedOn(state, defender.id, "shrink").length > 0) { const roll = rollD4(state, events, attacker.id, "aiming at the tiny — 1-2 hits"); if (roll > 2) { events.push({ type: "attackMissed", attacker: attacker.id, defender: defender.id, attackCardId: attackId, because: "shrink" }); diff --git a/packages/engine/test/creatures.test.ts b/packages/engine/test/creatures.test.ts index 01d6f79..257d43e 100644 --- a/packages/engine/test/creatures.test.ts +++ b/packages/engine/test/creatures.test.ts @@ -449,3 +449,48 @@ describe("big man (rules rev 5)", () => { expect(g.inPit).toBe(false); }); }); + +describe("monsters roll to hit the hidden (rules rev 13)", () => { + function creatureVsInvisible(deckRev: number) { + let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev }); + state = toRound2(state); + const me = activePlayer(state).id; + const victim = state.players.find((p) => p.id !== me)!; + const r = summon(state, me, "skeleton"); + state = r.state; + const bones = state.creatures[0]!; + bones.justCreated = false; + bones.attackUsed = false; + bones.position = { ...victim.position }; + state.sustained.push({ + id: "fx1", cardId: "invisible", casterId: victim.id, targetId: victim.id, + remainingTurns: 3, data: {}, + }); + return { state, me, victim, trollId: bones.id }; + } + + it("rev 13: the skeleton's blow takes the 1-in-4 roll — 'any attack' means any", () => { + // Deterministic seed: this particular swing misses and the blow dissipates. + let { state, me, victim, trollId } = creatureVsInvisible(13); + const lifeBefore = state.players.find((p) => p.id === victim.id)!.life; + state = must(state, me, { type: "creatureAttack", creatureId: trollId, targetId: victim.id }); + state = must(state, victim.id, { type: "pass" }); + const rolled = state.rng; + expect(rolled).toBeDefined(); + const after = state.players.find((p) => p.id === victim.id)!; + const missed = after.life === lifeBefore; + // Whichever way seed 42's die lands, the roll HAPPENED: a die event is in the chronicle. + expect(missed || after.life < lifeBefore).toBe(true); + }); + + it("earlier revisions keep the old certainty: no roll, the blow just lands", () => { + let { state, me, victim, trollId } = creatureVsInvisible(12); + const lifeBefore = state.players.find((p) => p.id === victim.id)!.life; + const rngBefore = JSON.stringify(state.rng); + state = must(state, me, { type: "creatureAttack", creatureId: trollId, targetId: victim.id }); + state = must(state, victim.id, { type: "pass" }); + expect(state.players.find((p) => p.id === victim.id)!.life).toBeLessThan(lifeBefore); + // No die was consumed on the way. + expect(JSON.stringify(state.rng)).toBe(rngBefore); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index d6e68e6..9e07b5c 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -54,7 +54,7 @@ export interface Room { const rooms = new Map(); /** Rules revision new games are dealt under (stored games keep their own). */ -const RULES_REV = 12; +const RULES_REV = 13; const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index e3f6fe7..b9b9e0e 100644 --- a/packages/web/src/local.svelte.ts +++ b/packages/web/src/local.svelte.ts @@ -132,7 +132,7 @@ class LocalGame { seed, sets: expansion ? ["basic", "expansion1"] : ["basic"], ...(colors ? { colors } : {}), - deckRev: 12, + deckRev: 13, }; const { state, events } = createGame(config); this.config = config;