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>
91 lines
2.7 KiB
TypeScript
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],
|
|
};
|
|
}
|