The whole punch list: rev 8, general FAQ, and four promoted UX items
Rules rev 8 closes the last two fidelity threads. A SPEED bonus turn burns a turn of duration spells on the hastened wizard — recipient- counted, per the FAQ's most obscure ruling — with self-cast durations already burning through the normal turn-start sweep (expiry extracted into expireEffect so both paths share the cleanup). And nothing can be created on a DIMENSIONAL WARP token, as the card face always said. The general-topic FAQ sections (Combat, Line of Sight, Monsters, Treasures, and five more) join the rules tab verbatim under a double- ruled divider, completing the FAQ's absorption: card rulings on the cards, general rulings in the rules. The four promoted UX items: cell-target spells now dim ineligible squares (creations mirror emptySquareTarget from the viewer's knowledge, summons want clear sighted squares, teleport BFSes its four spaces exactly as the engine does, stone-to-water lights only stone, dispel lights only creations); hotseat games get the full replay reel, each step seen from its actor's own seat; a "stop announcing attacks on this device" link in the fanfare modal (undo lives beside the notification toggle in the lobby); the corner inspector now serves only the selected casting card, with every examine-a-card peek unified on the centered treatment; and the dev socket URL keys on vite's port instead of hijacking every localhost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f657144127
commit
d6d34887e3
@@ -182,3 +182,85 @@ export function sightedCellsFor(view: GameView): Set<string> {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const CREATION_CARD_IDS = new Set([
|
||||
"fill-square-with-stone", "thornbush", "killer-ooze", "rosebush", "dust-cloud",
|
||||
"fill-square-with-slime", "create-pit", "handful-of-tacks", "glue", "safe", "boobytrap",
|
||||
]);
|
||||
const SUMMON_CARD_IDS = new Set([
|
||||
"troll", "skeleton", "wraith", "fire-imp", "democratic-monster", "shadow",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Squares a cell-target card can legally aim at, for the client's dimming
|
||||
* aid — mirroring the engine's own validation from the viewer's knowledge.
|
||||
* Null = this card's eligibility is not modeled; light everything.
|
||||
*/
|
||||
export function eligibleCellsFor(view: GameView, cardId: string): Set<string> | null {
|
||||
const me = view.players.find((p) => p.id === view.you);
|
||||
if (!me) return null;
|
||||
const cells = Object.keys(view.board.cells);
|
||||
const key = (x: number, y: number) => `${x},${y}`;
|
||||
const sighted = sightedCellsFor(view);
|
||||
|
||||
if (CREATION_CARD_IDS.has(cardId)) {
|
||||
// emptySquareTarget: on the board, unoccupied by content, home, wizard,
|
||||
// treasure, object, or a warp token — and in the caster's sight.
|
||||
const out = new Set<string>();
|
||||
for (const k of cells) {
|
||||
if (view.squareContents[k]) continue;
|
||||
const [x, y] = k.split(",").map(Number) as [number, number];
|
||||
if (view.board.homes.some((h) => key(h.x, h.y) === k)) continue;
|
||||
if (view.players.some((p) => p.alive && key(p.position.x, p.position.y) === k)) continue;
|
||||
if (view.treasures.some((t) => t.position && key(t.position.x, t.position.y) === k)) continue;
|
||||
if ((view.groundObjects[k] ?? []).length > 0) continue;
|
||||
if (view.dimWarps.some((w) => key(w.a.x, w.a.y) === k || key(w.b.x, w.b.y) === k)) continue;
|
||||
if (!sighted.has(k)) continue;
|
||||
out.add(k);
|
||||
void x; void y;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (SUMMON_CARD_IDS.has(cardId)) {
|
||||
// Summon: any sighted square free of content and creatures.
|
||||
const out = new Set<string>();
|
||||
for (const k of cells) {
|
||||
if (view.squareContents[k]) continue;
|
||||
if (view.creatures.some((c) => key(c.position.x, c.position.y) === k)) continue;
|
||||
if (!sighted.has(k)) continue;
|
||||
out.add(k);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (cardId === "teleport") {
|
||||
// Up to four spaces, walls and objects ignored; not into solid stone.
|
||||
// BFS over existing cells, matching the engine's wallIgnoringDistance.
|
||||
const out = new Set<string>();
|
||||
const dist = new Map<string, number>([[key(me.position.x, me.position.y), 0]]);
|
||||
const queue = [me.position];
|
||||
while (queue.length > 0) {
|
||||
const cur = queue.shift()!;
|
||||
const d = dist.get(key(cur.x, cur.y))!;
|
||||
if (d >= 4) continue;
|
||||
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]] as const) {
|
||||
const n = { x: cur.x + dx, y: cur.y + dy };
|
||||
const nk = key(n.x, n.y);
|
||||
if (!view.board.cells[nk] || dist.has(nk)) continue;
|
||||
dist.set(nk, d + 1);
|
||||
queue.push(n);
|
||||
if (view.squareContents[nk]?.kind !== "stone") out.add(nk);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (cardId === "stone-to-water") {
|
||||
return new Set(cells.filter((k) => view.squareContents[k]?.kind === "stone"));
|
||||
}
|
||||
if (cardId === "dispel-creation") {
|
||||
return new Set(cells.filter((k) => view.squareContents[k] != null));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user