diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index fd930b3..2c94189 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -133,6 +133,8 @@ export interface TurnState { secondAttackUsed: boolean; /** Wand instances already used this turn ("maximum of once per turn"). */ wandsUsed: string[]; + /** ADD spent to allow a second movement number card this turn. */ + movementAddUsed: boolean; /** SLOW: "his attacks [reduce] to every other turn". */ attackForbidden: boolean; actionsEnded: boolean; @@ -525,7 +527,7 @@ export type CastTarget = export type Command = | { type: "move"; direction: Side } - | { type: "playNumberForMovement"; instanceId: string } + | { type: "playNumberForMovement"; instanceId: string; addInstanceId?: string } | { type: "punch"; targetId: PlayerId } | { type: "warpStep" } | { type: "moveCreature"; creatureId: string; direction: Side } @@ -2948,6 +2950,7 @@ export function createGame(config: GameConfig): { state: GameState; events: Game attackUsed: false, secondAttackUsed: false, wandsUsed: [], + movementAddUsed: false, attackForbidden: false, actionsEnded: false, }, @@ -3062,7 +3065,7 @@ export function applyCommand(state: GameState, playerId: PlayerId, command: Comm switch (command.type) { case "move": return doMove(state, command.direction); - case "playNumberForMovement": return doPlayNumberForMovement(state, command.instanceId); + case "playNumberForMovement": return doPlayNumberForMovement(state, command.instanceId, command.addInstanceId); case "punch": return doPunch(state, command.targetId); case "warpStep": return doWarpStep(state); case "moveCreature": return doMoveCreature(state, command.creatureId, command.direction); @@ -3378,17 +3381,29 @@ function doWarpStep(prev: GameState): CommandResult { return { ok: true, state, events: [{ type: "warpStepped", player: p.id, from, to: p.position }] }; } -function doPlayNumberForMovement(prev: GameState, instanceId: string): CommandResult { +function doPlayNumberForMovement(prev: GameState, instanceId: string, addInstanceId?: string): CommandResult { const blocked = requireActionsAvailable(prev); if (blocked) return err(blocked); - if (prev.turn.numberPlayedForMovement) return err("only one number card may boost movement per turn"); const mover = activePlayer(prev); if (sustainedOn(prev, mover.id, "slow").length > 0) { return err("you are slowed — no number cards for movement"); } + // "You may add two NUMBER cards together for any single action" — an ADD + // permits one extra movement number this turn. + if (prev.turn.numberPlayedForMovement) { + if (!addInstanceId) return err("only one number card may boost movement per turn (ADD permits a second)"); + if (prev.turn.movementAddUsed) return err("ADD already joined two numbers to your movement"); + } const state = clone(prev); const p = activePlayer(state); + if (state.turn.numberPlayedForMovement && addInstanceId) { + const addCard = p.hand.find((c) => c.instanceId === addInstanceId); + if (!addCard || addCard.cardId !== "add") return err("ADD card not in hand"); + takeFromHand(p, addInstanceId); + state.discard.push(addCard); + state.turn.movementAddUsed = true; + } const card = takeFromHand(p, instanceId); if (!card) return err("card not in hand"); if (!isNumberCard(card.cardId)) return err("not a number card"); @@ -4604,6 +4619,7 @@ function beginTurnFor(state: GameState, events: GameEvent[], index: number): voi attackUsed: false, secondAttackUsed: false, wandsUsed: [], + movementAddUsed: false, attackForbidden, actionsEnded: false, }; diff --git a/packages/engine/test/expansion-combat.test.ts b/packages/engine/test/expansion-combat.test.ts index 60a0031..3cb9e35 100644 --- a/packages/engine/test/expansion-combat.test.ts +++ b/packages/engine/test/expansion-combat.test.ts @@ -246,3 +246,23 @@ describe("thumb of god (divine meteor)", () => { expect(allObjects.some((c) => c.cardId === "dagger")).toBe(true); }); }); + +describe("add for movement", () => { + it("an ADD permits a second number card on movement, once", () => { + let { state } = newGame(); + const me = activePlayer(state).id; + giveCard(state, me, "number-3", "N1", 0); + giveCard(state, me, "number-2", "N2", 1); + giveCard(state, me, "add", "A", 2); + giveCard(state, me, "number-4", "N3", 3); + state = must(state, me, { type: "playNumberForMovement", instanceId: "number-3#N1" }); + expect(state.turn.movementAllowance).toBe(6); + // Second without ADD: refused. + expect(applyCommand(state, me, { type: "playNumberForMovement", instanceId: "number-2#N2" }).ok).toBe(false); + // With ADD: allowed. + state = must(state, me, { type: "playNumberForMovement", instanceId: "number-2#N2", addInstanceId: "add#A" }); + expect(state.turn.movementAllowance).toBe(8); + // A third is refused even with another add wished for. + expect(applyCommand(state, me, { type: "playNumberForMovement", instanceId: "number-4#N3" }).ok).toBe(false); + }); +}); diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 29d88e8..1ab23e2 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -149,8 +149,17 @@ ]); function playNumberForMovement() { - if (!selectedCard) return; - net.command({ type: "playNumberForMovement", instanceId: selectedCard.instanceId }); + if (!selectedCard || !view) return; + const cmd: Parameters[0] = { + type: "playNumberForMovement", + instanceId: selectedCard.instanceId, + }; + // A second movement number needs an ADD — spend one from hand automatically. + if (view.turn.numberPlayedForMovement) { + const add = view.yourHand.find((c) => c.cardId === "add"); + if (add) cmd.addInstanceId = add.instanceId; + } + net.command(cmd); clearSelection(); } @@ -592,7 +601,7 @@ {/if} {#if selectedCard && isNumberCard(selectedCard.cardId)} {/if}