diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 1ce1aff..f82c8ee 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -167,6 +167,9 @@ export interface TurnState { /** SLOW: "his attacks [reduce] to every other turn". */ attackForbidden: boolean; actionsEnded: boolean; + /** A reflected LIGHTNING BLAST stuns mid-turn: no end-of-turn draw either + * (FAQ: "you lose the rest of your turn and can't draw cards"). */ + drawForbidden?: boolean; } export interface CastStack { @@ -845,6 +848,15 @@ const CARD_EFFECTS: Record if (ctx.damageDealt <= 0 || !ctx.defender.alive) return; ctx.defender.lostTurns++; ctx.events.push({ type: "stunned", player: ctx.defender.id, turnsLost: 1 }); + // Stunned mid-own-turn (the bolt came back): FAQ — "If this gets FULLY + // REFLECTED, you lose the rest of your turn and can't draw cards (in + // addition to damage)." Rules rev 28; older games played on and replay so. + const st = ctx.state; + if ((st.config.deckRev ?? 1) >= 28 && + st.players[st.turn.activeIndex]!.id === ctx.defender.id) { + st.turn.actionsEnded = true; + st.turn.drawForbidden = true; + } }, }, powerthrust: { kind: "attack", requiresLos: true, baseDamage: (n) => 2 + (n ?? 0) }, @@ -5955,7 +5967,7 @@ function doEndTurn(prev: GameState, draw: number): CommandResult { const events: GameEvent[] = []; const room = handLimit(p) - p.hand.length; - const count = Math.min(draw, room); + const count = state.turn.drawForbidden ? 0 : Math.min(draw, room); if (count > 0) { const drawn: CardInstance[] = []; let toDraw = count; diff --git a/packages/engine/test/casting.test.ts b/packages/engine/test/casting.test.ts index 3d2c42f..bf4ebbf 100644 --- a/packages/engine/test/casting.test.ts +++ b/packages/engine/test/casting.test.ts @@ -1023,3 +1023,45 @@ describe("the reflected-attack window (rules rev 25)", () => { expect(state.players.find((p) => p.id === attacker)!.life).toBe(5); }); }); + +describe("a reflected lightning blast ends the caster's turn (rules rev 28)", () => { + function boltRig(deckRev: number) { + let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"], deckRev }); + state = toRound2(state); + const { attacker, defender } = faceOff(state); + const lb = giveCard(state, attacker, "lightning-blast"); + giveCard(state, attacker, "number-3", "N", 1); + giveCard(state, defender, "full-reflection", "FR", 0); + state = must(state, attacker, { + type: "cast", instanceId: lb.instanceId, numberInstanceIds: ["number-3#N"], + target: { kind: "player", playerId: defender }, + }); + state = must(state, defender, { type: "counteract", instanceId: "full-reflection#FR" }); + state = must(state, attacker, { type: "pass" }); + // rev 25+: the returned bolt opens its window; the caster declines to answer. + if (state.stack) state = must(state, attacker, { type: "pass" }); + return { state, attacker, defender }; + } + + it("FAQ: 'you lose the rest of your turn and can't draw cards'", () => { + let { state, attacker } = boltRig(28); + expect(state.players.find((p) => p.id === attacker)!.life).toBe(12); + expect(state.turn.actionsEnded).toBe(true); + // No more marching, and the end-of-turn draw comes up empty. + expect(applyCommand(state, attacker, { type: "move", direction: "N" }).ok).toBe(false); + const before = state.players.find((p) => p.id === attacker)!.hand.length; + state = must(state, attacker, { type: "endTurn", draw: 2 }); + expect(state.players.find((p) => p.id === attacker)!.hand.length).toBe(before); + // And the stun still costs the next turn. + expect(state.players.find((p) => p.id === attacker)!.lostTurns + + (activePlayer(state).id === attacker ? 1 : 0)).toBeGreaterThanOrEqual(1); + }); + + it("rev 27 keeps the old behavior: the caster plays on and draws", () => { + let { state, attacker } = boltRig(27); + expect(state.turn.actionsEnded).toBe(false); + const before = state.players.find((p) => p.id === attacker)!.hand.length; + state = must(state, attacker, { type: "endTurn", draw: 2 }); + expect(state.players.find((p) => p.id === attacker)!.hand.length).toBeGreaterThan(before); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 9bc8f95..79d5fed 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 = 27; +const RULES_REV = 28; const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 85dc6bd..47c92c8 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1587,7 +1587,11 @@ {/if} {#if actionsSpent} -
Your spellwork is spent for this turn — all that remains is to draw and end it.
+ {#if view.turn.drawForbidden} +
Your own bolt has left you reeling — the rest of this turn is lost, and no cards come with it.
+ {:else} +
Your spellwork is spent for this turn — all that remains is to draw and end it.
+ {/if} {/if} {#if view.yourAmbushes.length > 0} diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index 699b8d4..ba97206 100644 --- a/packages/web/src/local.svelte.ts +++ b/packages/web/src/local.svelte.ts @@ -136,7 +136,7 @@ class LocalGame { seed, sets: expansion ? ["basic", "expansion1"] : ["basic"], ...(colors ? { colors } : {}), - deckRev: 27, + deckRev: 28, }; const { state, events } = createGame(config); for (const e of events) {