The card's second power was never built. New tearTreasure command: with STRENGTH on you, in the same square as a carrier, the tear spends your attack; the victim keeps the treasure only on a D4 roll of 1. Per the FAQ it is not a pickup (your turn's actions continue), and full or weakened arms tear it loose onto the floor instead. A torn treasure's warded owner gets their window, blind grabs flail like blind punches, and the automatons wrestle too. A new command breaks no stored ledger, so live games gain the grip at once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
102 lines
3.9 KiB
TypeScript
102 lines
3.9 KiB
TypeScript
// The council of three: each automaton temperament reads YOUR view — the
|
|
// same cards and board you see, nothing more — and says what it would do
|
|
// in your robes. Purely advisory; nothing is dispatched.
|
|
|
|
import { automatonCommand, cardDef, type Command, type GameView } from "@wizwar/engine";
|
|
import type { AutomatonStyle } from "@wizwar/engine";
|
|
|
|
const DIRECTION: Record<string, string> = { N: "north", S: "south", E: "east", W: "west" };
|
|
|
|
const VOICES: Record<AutomatonStyle, { name: string; glyph: string; mood: string }> = {
|
|
hunter: { name: "The Hunter", glyph: "🏹", mood: "eyes on the gold" },
|
|
berserker: { name: "The Berserker", glyph: "🔥", mood: "eyes on the blood" },
|
|
worrier: { name: "The Worrier", glyph: "🛡", mood: "eyes on the exits" },
|
|
};
|
|
|
|
function nameOf(view: GameView, instanceId: string): string {
|
|
const c = view.yourHand.find((c) => c.instanceId === instanceId);
|
|
return c ? cardDef(c.cardId).name : "a card";
|
|
}
|
|
|
|
function describe(view: GameView, cmd: Command): string {
|
|
switch (cmd.type) {
|
|
case "move":
|
|
return `step ${DIRECTION[cmd.direction]}`;
|
|
case "playNumberForMovement":
|
|
return `play the ${nameOf(view, cmd.instanceId)} for extra movement`;
|
|
case "cast": {
|
|
const card = nameOf(view, cmd.instanceId);
|
|
const at =
|
|
cmd.target?.kind === "player" ? ` at ${cmd.target.playerId}`
|
|
: cmd.target?.kind === "cell" ? ` at the square (${cmd.target.cell.x}, ${cmd.target.cell.y})`
|
|
: cmd.target?.kind === "edge" ? ` on the wall line beside (${cmd.target.cell.x}, ${cmd.target.cell.y})`
|
|
: cmd.target?.kind === "creature" ? " on the monster"
|
|
: "";
|
|
const num = cmd.numberInstanceIds?.length
|
|
? ` with ${cmd.numberInstanceIds.map((id) => nameOf(view, id)).join(" and ")}`
|
|
: "";
|
|
return `cast ${card}${at}${num}`;
|
|
}
|
|
case "counteract":
|
|
return `answer with ${nameOf(view, cmd.instanceId)}`;
|
|
case "pass":
|
|
return "let it resolve — nothing in hand is worth spending on this";
|
|
case "punch":
|
|
return `punch ${cmd.targetId}`;
|
|
case "punchWall":
|
|
return "punch the wall";
|
|
case "pickUpTreasure":
|
|
return "pick up the treasure here (that ends the turn's actions)";
|
|
case "pickUpObject":
|
|
return "pick up the object here (that ends the turn's actions)";
|
|
case "dropTreasure":
|
|
return "drop the treasure here";
|
|
case "dropObject":
|
|
return `drop ${nameOf(view, cmd.instanceId)} here`;
|
|
case "warpStep":
|
|
return "step through the warp underfoot";
|
|
case "testIllusion":
|
|
return "test that shimmering wall — doubting it is free";
|
|
case "moveCreature":
|
|
return `march the monster ${DIRECTION[cmd.direction]}`;
|
|
case "creatureAttack":
|
|
return `set the monster on ${cmd.targetId}`;
|
|
case "tearTreasure":
|
|
return `wrest the treasure from ${cmd.targetId}'s arms`;
|
|
case "setAmbush":
|
|
return `lay an ambush with ${nameOf(view, cmd.instanceId)}`;
|
|
case "discard":
|
|
return `shed ${cmd.instanceIds.map((id) => nameOf(view, id)).join(", ")} to make room`;
|
|
case "endTurn":
|
|
return cmd.draw > 0 ? `end the turn and draw ${cmd.draw}` : "end the turn";
|
|
default:
|
|
return "bide";
|
|
}
|
|
}
|
|
|
|
export interface TableHint {
|
|
name: string;
|
|
glyph: string;
|
|
mood: string;
|
|
advice: string;
|
|
}
|
|
|
|
/** What each temperament would do from this seat, phrased for a novice. */
|
|
export function tableHints(view: GameView): TableHint[] {
|
|
const styles: AutomatonStyle[] = ["hunter", "berserker", "worrier"];
|
|
const out: TableHint[] = [];
|
|
for (const s of styles) {
|
|
const v = VOICES[s];
|
|
let advice: string;
|
|
try {
|
|
const cmd = automatonCommand(view, s, "archmage");
|
|
advice = cmd ? `I would ${describe(view, cmd)}.` : "Nothing calls for us right now — wait for your moment.";
|
|
} catch (e) {
|
|
console.warn("hint council choked:", e);
|
|
advice = "…I have no counsel here.";
|
|
}
|
|
out.push({ name: v.name, glyph: v.glyph, mood: v.mood, advice });
|
|
}
|
|
return out;
|
|
}
|