diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 6d04b76..cf42574 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -660,7 +660,7 @@ export type Command = | { type: "cancelAmbush"; ambushId: string } | { type: "counteract"; instanceId: string; params?: { cell?: Cell }; numberInstanceIds?: string[] } | { type: "pass" } - | { type: "pickUpTreasure" } + | { type: "pickUpTreasure"; treasureId?: string } | { type: "pickUpObject"; instanceId: string } | { type: "dropObject"; instanceId: string } | { type: "dropTreasure" } @@ -3380,7 +3380,7 @@ function applyCommandInner(state: GameState, playerId: PlayerId, command: Comman case "cancelAmbush": return doCancelAmbush(state, command.ambushId); case "counteract": return err("nothing to counteract"); case "pass": return err("nothing to pass on"); - case "pickUpTreasure": return doPickUpTreasure(state); + case "pickUpTreasure": return doPickUpTreasure(state, command.treasureId); case "pickUpObject": return doPickUpObject(state, command.instanceId); case "dropObject": return doDropObject(state, command.instanceId); case "dropTreasure": return doDropTreasure(state); @@ -5248,7 +5248,7 @@ function homeOwnerAt(state: GameState, cell: Cell): PlayerId | null { // --- Treasures --------------------------------------------------------------- -function doPickUpTreasure(prev: GameState): CommandResult { +function doPickUpTreasure(prev: GameState, treasureId?: string): CommandResult { const blocked = requireActionsAvailable(prev); if (blocked) return err(blocked); @@ -5262,10 +5262,12 @@ function doPickUpTreasure(prev: GameState): CommandResult { if (safe && state.squareContents[here]!.createdBy !== p.id && !state.openSafes.includes(here)) { return err("it is locked inside a safe"); } + // Two treasures can share a square; an id names the one wanted. const t = state.treasures.find( - (t) => t.position && cellKey(t.position) === cellKey(p.position) && !t.carriedBy, + (t) => t.position && cellKey(t.position) === cellKey(p.position) && !t.carriedBy && + (treasureId == null || t.id === treasureId), ); - if (!t) return err("no treasure here"); + if (!t) return err(treasureId ? "that treasure is not here for the taking" : "no treasure here"); t.carriedBy = p.id; t.position = null; diff --git a/packages/engine/test/terrain-objects-control.test.ts b/packages/engine/test/terrain-objects-control.test.ts index a56efd4..aecca54 100644 --- a/packages/engine/test/terrain-objects-control.test.ts +++ b/packages/engine/test/terrain-objects-control.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { applyCommand, activePlayer, boardView, gameLos, sustainedOn } from "../src/game"; +import { applyCommand, activePlayer, boardView, createGame, gameLos, sustainedOn } from "../src/game"; import { cellKey, edgeKey, neighbor, SIDES } from "../src/board"; import type { CardInstance } from "../src/cards"; import { newGame, must, giveCard, toRound2, faceOff, castAt, emptyNeighborCell } from "./helpers"; @@ -277,3 +277,17 @@ describe("control effects", () => { expect(state.players.find((p) => p.id === me.id)!.hand.some((c) => c.cardId === "create-wall")).toBe(true); }); }); + +describe("picking one of two treasures", () => { + it("a named treasure is the one taken", () => { + let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"], deckRev: 16 }); + state = toRound2(state); + const p = activePlayer(state); + const [t1, t2] = state.treasures.filter((t) => t.owner !== p.id); + t1!.position = { ...p.position }; t1!.carriedBy = null; + t2!.position = { ...p.position }; t2!.carriedBy = null; + const r = applyCommand(state, p.id, { type: "pickUpTreasure", treasureId: t2!.id }); + if (!r.ok) throw new Error(r.error); + expect(r.state.players.find((q) => q.id === p.id)!.carriedTreasureId).toBe(t2!.id); + }); +}); diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 7c18943..97a21d1 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -313,6 +313,7 @@ selectedCard = null; megaBoost = "life"; holdDoor = false; + pickChoice = false; pendingTeleport = null; attachedNumber = null; attachedMods = []; @@ -821,11 +822,26 @@ dispatch({ type: "endTurn", draw: drawCount }); } - function pickUp() { dispatch({ type: "pickUpTreasure" }); } + let pickChoice = $state(false); + function pickUp() { + if (treasuresHere.length > 1) { pickChoice = true; return; } + dispatch({ type: "pickUpTreasure" }); + } + function pickUpNamed(treasureId: string) { + pickChoice = false; + dispatch({ type: "pickUpTreasure", treasureId }); + } function drop() { dispatch({ type: "dropTreasure" }); } function pass() { dispatch({ type: "pass" }); } const me = $derived(view?.players.find((p) => p.id === view?.you) ?? null); + /** Two treasures can share a square: one dispatches, more ask whose. */ + const treasuresHere = $derived( + view && me + ? view.treasures.filter((t) => t.position && t.position.x === me.position.x && + t.position.y === me.position.y && !t.carriedBy) + : [], + ); const carryingTreasure = $derived(me?.carriedTreasureId != null); const treasureHere = $derived( view != null && me != null && @@ -1659,6 +1675,15 @@ {#if isYourTurn} + {#if pickChoice} + + — whose? + {#each treasuresHere as t (t.id)} + + {/each} + + + {/if} {#each objectsHere as obj (obj.instanceId)}