Card wave 3: terrain, thrown objects, drag, and control spells
Terrain layer: FILL SQUARE WITH STONE (impassable, blocks LOS via new cell-blocking sight checks), THORNBUSH (enter = 1 damage + turn ends + next turn lost; no attacking in or into a bush), WALL OF FIRE (new firewall edge state — passable for 4 magical damage, blocks LOS, expires with its duration), WATERWALL (instant wave: players within two spaces washed back two, 1 damage per blocked space), and DISPEL CREATION with provenance tracking (only conjured walls/fire/stone/ bushes dispel — printed maze is safe). Objects: DAGGER (3) and LARGE ROCK (2) are physical throws Full Shield cannot stop; they land on the floor and anyone may pick them up (ending their turn's actions, hand limit enforced); DROP OBJECT forces a named object or carried treasure to the ground; DRAG pulls floor objects, treasures, or players straight toward the caster. Control: LOCK IN PLACE (no moving or being moved — teleports, swaps, knockbacks and drags all respect it), BUDDY (a pact the caster breaks by attacking), MIST-BODY (through walls and doors, cannot attack or be attacked, still burns in firewalls), REUSE SPELL (retrieve your last spell). Client renders terrain, firewalls, and ground objects, with cell/edge/two-stage targeting and card-name inputs. 40 cards implemented; 63 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
67743dd17e
commit
2d88b4ab40
@@ -8,7 +8,7 @@ import boardsData from "../data/boards.json";
|
||||
|
||||
export type Cell = { readonly x: number; readonly y: number };
|
||||
export type Side = "N" | "S" | "E" | "W";
|
||||
export type EdgeState = "open" | "wall" | "door";
|
||||
export type EdgeState = "open" | "wall" | "door" | "firewall";
|
||||
export type Rotation = 0 | 90 | 180 | 270;
|
||||
|
||||
export interface SectorPlacement {
|
||||
@@ -252,17 +252,40 @@ export function stepTarget(
|
||||
|
||||
/**
|
||||
* Line of sight from the center of `from` to the center of `to`, blocked by
|
||||
* wall/door edges the segment crosses. Grazing a wall endpoint (passing
|
||||
* exactly through a corner adjacent to a wall) counts as blocked — strict
|
||||
* reading; revisit against FAQ rulings if needed. LOS through wraparound
|
||||
* openings is not yet modeled (TODO).
|
||||
* wall/door/firewall edges the segment crosses and by any `blockedCells`
|
||||
* (solid stone, thornbushes) it passes through. Grazing a wall endpoint
|
||||
* (passing exactly through a corner adjacent to a wall) counts as blocked —
|
||||
* strict reading; revisit against FAQ rulings if needed. LOS through
|
||||
* wraparound openings is not yet modeled (TODO).
|
||||
*/
|
||||
export function hasLineOfSight(board: AssembledBoard, from: Cell, to: Cell): boolean {
|
||||
export function hasLineOfSight(
|
||||
board: AssembledBoard,
|
||||
from: Cell,
|
||||
to: Cell,
|
||||
blockedCells?: Record<string, true>,
|
||||
): boolean {
|
||||
if (cellKey(from) === cellKey(to)) return true;
|
||||
// Centers of cells: (x + 0.5, y + 0.5).
|
||||
const x0 = from.x + 0.5, y0 = from.y + 0.5;
|
||||
const x1 = to.x + 0.5, y1 = to.y + 0.5;
|
||||
|
||||
if (blockedCells) {
|
||||
for (const key of Object.keys(blockedCells)) {
|
||||
const [bx, by] = key.split(",").map(Number) as [number, number];
|
||||
if ((bx === from.x && by === from.y) || (bx === to.x && by === to.y)) continue;
|
||||
// The sight line is blocked if it crosses any side of the solid cell.
|
||||
const sides: [number, number, number, number][] = [
|
||||
[bx, by, bx + 1, by],
|
||||
[bx, by + 1, bx + 1, by + 1],
|
||||
[bx, by, bx, by + 1],
|
||||
[bx + 1, by, bx + 1, by + 1],
|
||||
];
|
||||
if (sides.some(([ax, ay, cx, cy]) => segmentsIntersect(x0, y0, x1, y1, ax, ay, cx, cy))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, state] of Object.entries(board.edges)) {
|
||||
if (state === "open") continue;
|
||||
// Reconstruct the wall segment for this edge.
|
||||
|
||||
Reference in New Issue
Block a user