Files
wizwar6e/packages/engine/src/view.ts
T
Eric WagonerandClaude Fable 5 2d88b4ab40 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>
2026-08-15 20:13:14 -04:00

91 lines
2.7 KiB
TypeScript

// Per-player projection of GameState: everything public, plus YOUR hand.
// The server sends this after every state change; clients never see the
// deck order or other players' hands.
import { type AssembledBoard } from "./board";
import { type CardInstance } from "./cards";
import {
boardView,
type CastStack,
type GameState,
type PlayerId,
type SquareContent,
type SustainedEffect,
type TreasureState,
type TurnState,
} from "./game";
export interface PlayerPublicView {
id: PlayerId;
position: { x: number; y: number };
home: { x: number; y: number };
life: number;
alive: boolean;
handCount: number;
carriedTreasureId: string | null;
lostTurns: number;
extraTurns: number;
displayed: CardInstance[];
}
export interface GameView {
you: PlayerId;
phase: GameState["phase"];
winner: PlayerId | null;
turn: TurnState;
activePlayerId: PlayerId;
/** Board with dynamic wall changes already merged in. */
board: AssembledBoard;
players: PlayerPublicView[];
yourHand: CardInstance[];
treasures: TreasureState[];
deckCount: number;
discardCount: number;
/** Cards on the stack are face-up: the whole exchange is public. */
stack: CastStack | null;
pendingDiscard: PlayerId | null;
/** Duration spells in play (public knowledge). */
sustained: SustainedEffect[];
squareContents: Record<string, SquareContent>;
groundObjects: Record<string, CardInstance[]>;
doorStates: Record<string, "jammed" | "removed">;
openDoorEdges: string[];
}
export function viewFor(state: GameState, playerId: PlayerId): GameView {
const you = state.players.find((p) => p.id === playerId);
return {
you: playerId,
phase: state.phase,
winner: state.winner,
turn: state.turn,
activePlayerId: state.players[state.turn.activeIndex]!.id,
board: boardView(state),
players: state.players.map((p) => ({
id: p.id,
position: p.position,
home: p.home,
life: p.life,
alive: p.alive,
handCount: p.hand.length,
carriedTreasureId: p.carriedTreasureId,
lostTurns: p.lostTurns,
extraTurns: p.extraTurns,
displayed: p.hand.filter((c) => p.displayed.includes(c.instanceId)),
})),
yourHand: you ? [...you.hand] : [],
treasures: state.treasures.map((t) => ({ ...t })),
deckCount: state.deck.length,
discardCount: state.discard.length,
stack: state.stack,
pendingDiscard: state.pendingDiscard,
sustained: state.sustained.map((s) => ({ ...s })),
squareContents: { ...state.squareContents },
groundObjects: Object.fromEntries(
Object.entries(state.groundObjects).map(([k, v]) => [k, [...v]]),
),
doorStates: { ...state.doorStates },
openDoorEdges: [...state.openDoorEdges],
};
}