Daggers, rocks, blades, and wands can be set down too
The rulebook's movable-object list is wider than the stones: "magic stones, treasure chests, the dagger, the large rock, and the wizardblade" — and the expansion expects wands to change hands, since "its remaining charges go with it." A new isMovableObject helper in the engine names that full set, doDropObject honors it, and the hint bar's "Drop it here" button follows suit. Charges are keyed by card instance, so a dropped wand carries them to whoever picks it up — covered by new tests, along with the refusal to drop spell cards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c04ed1e0ee
commit
789d927122
@@ -4578,12 +4578,28 @@ function doPickUpObject(prev: GameState, instanceId: string): CommandResult {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Physical, droppable objects beyond the object-type stones. Rulebook:
|
||||
* "Movable objects include magic stones, treasure chests, the dagger, the
|
||||
* large rock, and the wizardblade" — plus the expansion wands, which the
|
||||
* rules expect to change hands ("its remaining charges go with it";
|
||||
* charges are keyed by instance, so they travel automatically).
|
||||
*/
|
||||
const MOVABLE_OBJECT_CARD_IDS = new Set([
|
||||
"dagger", "large-rock", "wizardblade",
|
||||
"blaster-wand", "shift-wand", "sticky-wand", "warp-wand",
|
||||
]);
|
||||
|
||||
export function isMovableObject(cardId: string): boolean {
|
||||
return cardDef(cardId).cardType === "object" || MOVABLE_OBJECT_CARD_IDS.has(cardId);
|
||||
}
|
||||
|
||||
function doDropObject(prev: GameState, instanceId: string): CommandResult {
|
||||
const state = clone(prev);
|
||||
const p = activePlayer(state);
|
||||
const card = p.hand.find((c) => c.instanceId === instanceId);
|
||||
if (!card) return err("card not in hand");
|
||||
if (cardDef(card.cardId).cardType !== "object") return err("only objects can be dropped");
|
||||
if (!isMovableObject(card.cardId)) return err("only objects can be dropped");
|
||||
takeFromHand(p, instanceId);
|
||||
const key = cellKey(p.position);
|
||||
state.groundObjects[key] = [...(state.groundObjects[key] ?? []), card];
|
||||
|
||||
@@ -198,3 +198,52 @@ describe("magic wands", () => {
|
||||
expect(state.players.find((p) => p.id === me.id)!.hand.some((c) => c.cardId === "fireball")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dropping objects", () => {
|
||||
it("a charged wand can be dropped and picked up, charges intact", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const wand = giveCard(state, attacker, "blaster-wand");
|
||||
giveCard(state, attacker, "number-3", "N", 1);
|
||||
|
||||
// Charge it with a 3 (one charge spent on the shot, two remain).
|
||||
state = must(state, attacker, {
|
||||
type: "cast", instanceId: wand.instanceId, numberInstanceIds: ["number-3#N"],
|
||||
target: { kind: "player", playerId: defender },
|
||||
});
|
||||
state = must(state, defender, { type: "pass" });
|
||||
expect(state.wandCharges[wand.instanceId]).toBe(2);
|
||||
|
||||
// Set it down: leaves the hand and the displayed list, lands on the floor.
|
||||
state = must(state, attacker, { type: "dropObject", instanceId: wand.instanceId });
|
||||
const owner = state.players.find((p) => p.id === attacker)!;
|
||||
expect(owner.hand.some((c) => c.instanceId === wand.instanceId)).toBe(false);
|
||||
expect(owner.displayed.includes(wand.instanceId)).toBe(false);
|
||||
const key = cellKey(owner.position);
|
||||
expect((state.groundObjects[key] ?? []).some((c) => c.instanceId === wand.instanceId)).toBe(true);
|
||||
|
||||
// The wizard standing there picks it up on their turn — charges travel.
|
||||
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
||||
state = must(state, defender, { type: "pickUpObject", instanceId: wand.instanceId });
|
||||
const thief = state.players.find((p) => p.id === defender)!;
|
||||
expect(thief.hand.some((c) => c.instanceId === wand.instanceId)).toBe(true);
|
||||
expect(state.wandCharges[wand.instanceId]).toBe(2);
|
||||
expect(state.players.find((p) => p.id === defender)!.id).toBe(activePlayer(state).id);
|
||||
});
|
||||
|
||||
it("the dagger and wizardblade can be set down; spell cards cannot", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const who = activePlayer(state).id;
|
||||
const dagger = giveCard(state, who, "dagger");
|
||||
const fireball = giveCard(state, who, "fireball", "F", 1);
|
||||
|
||||
state = must(state, who, { type: "dropObject", instanceId: dagger.instanceId });
|
||||
expect((state.groundObjects[cellKey(activePlayer(state).position)] ?? [])
|
||||
.some((c) => c.cardId === "dagger")).toBe(true);
|
||||
|
||||
const refused = applyCommand(state, who, { type: "dropObject", instanceId: fireball.instanceId });
|
||||
expect(refused.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import Help from "./Help.svelte";
|
||||
import Replay from "./Replay.svelte";
|
||||
import { local } from "./local.svelte";
|
||||
import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey } from "@wizwar/engine";
|
||||
import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey, isMovableObject } from "@wizwar/engine";
|
||||
import type { CardInstance, Side } from "@wizwar/engine";
|
||||
|
||||
net.connect();
|
||||
@@ -912,7 +912,7 @@
|
||||
Cast{attachedNumber ? ` with the ${numberTotal}` : ""}
|
||||
</button>
|
||||
{/if}
|
||||
{#if selectedCard && selectedDef.cardType === "object" && isYourTurn}
|
||||
{#if selectedCard && isMovableObject(selectedCard.cardId) && isYourTurn}
|
||||
<button class="stamp tiny" onclick={() => {
|
||||
if (selectedCard) dispatch({ type: "dropObject", instanceId: selectedCard.instanceId });
|
||||
clearSelection();
|
||||
|
||||
Reference in New Issue
Block a user