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)}