// Core domain types for the Sabotage Town game engine. // // The engine is a pure, deterministic reducer over a stream of actions (§3.7 of PLAN.md). // Nothing here reads the wall clock or generates randomness — time arrives via each action's // `serverTime`, and any random outcome is recorded in the action itself. export type Did = string export type ResourceId = string export type Role = 'worker' | 'saboteur' export type PlayerStatus = 'alive' | 'disconnected' | 'kicked' export type Facing = 'south' | 'west' | 'east' | 'north' export type StationState = 'working' | 'broken' | 'sabotaged' export type GamePhase = 'lobby' | 'round' | 'ended' export type Outcome = 'workers' | 'saboteur' | 'no-one' | 'aborted' /** What a player is actively doing at a station (consumes a timer). */ export type ActivityKind = // worker | 'collect' | 'repair' | 'inspect' // saboteur | 'pretendCollect' | 'breakStation' | 'fakeRepair' | 'sabotage' // special stations (all stateful: each rolls the failure dice on a real interaction when sabotaged) | 'deposit' | 'fakeDeposit' | 'scoreboard' | 'pretendScoreboard' | 'callVote' export interface Vec { x: number y: number } /** A rectangle in world pixels (top-left origin, y-down). */ export interface Rect { x: number y: number w: number h: number } /** A walkability grid for pathfinding (built from the level's collision rects — see nav.ts). */ export interface NavGrid { cellSize: number cols: number rows: number /** world-px position of the top-left corner of cell (0,0) */ originX: number originY: number /** row-major flags, blocked[row*cols + col] === 1 means impassable */ blocked: number[] } export interface GameConfig { /** Ordered resource ids, e.g. ['wood','stone','wheat','ore']. */ resources: ResourceId[] /** Collected amount required per resource for a worker win. */ targets: Record roundDurationMs: number /** the 1-cost actions: collect / pretendCollect / breakStation / deposit / fakeDeposit / * scoreboard / pretendScoreboard / callVote */ timer1Ms: number /** repair / fakeRepair */ timer2Ms: number /** inspect / sabotage */ timer3Ms: number /** per-slot cooldown after a resolved action */ slotCooldownMs: number debrisTtlMs: number voteDurationMs: number /** walk speed along a path, world px per second */ moveSpeedPxPerSec: number /** grace window after the saboteur disconnects mid-round before the round aborts (§3.6) */ saboteurGraceMs: number } export interface Slot { /** did of the player currently mid-action in this slot, else null */ occupantDid: Did | null /** a slot regenerates after a Collect: you can't Collect from it again until now >= this. Only * Collect sets it and only Collect is blocked by it; other activities ignore it entirely. * Secret — never exposed in projection. */ collectCooldownUntil: number } export interface ActiveAction { kind: ActivityKind stationId: string slot: number startedAt: number endsAt: number /** which resource this action concerns — set for collect / pretendCollect / deposit, else null */ resourceId: ResourceId | null } /** A carried resource. `authentic: false` is a saboteur's fake. */ export interface Carry { authentic: boolean resourceId: ResourceId } export interface Player { did: Did handle: string spriteRef: string | null /** null until the round starts; hidden from others by projection until game end */ role: Role | null status: PlayerStatus pos: Vec facing: Facing carrying: Carry | null active: ActiveAction | null /** current pathfinding destination (world px), or null when idle/arrived */ dest: Vec | null /** remaining waypoints (world px) from the next one to `dest`; player heads toward path[0] */ path: Vec[] } export interface Station { id: string pos: Vec /** the station's footprint; a player must be inside it to occupy a slot (enforced at the API) */ area: Rect slots: Slot[] /** Capability set (a station may combine several — e.g. a restaurant table that is both a collect * and a deposit point). Resources are the in-play set: the map may author a superset, narrowed to * the scenario's resources record at room creation. */ /** in-play resources collectible here (also the saboteur's pretendCollect set); [] if not a collect station */ collect: ResourceId[] /** in-play resources this station accepts for deposit; [] if not a deposit station */ deposit: ResourceId[] /** in-play resource keys whose totals can be read here (Timer-3); [] if not a scoreboard station */ scoreboard: ResourceId[] /** can call a vote here */ meeting: boolean /** station-wide state (applies to all slots). Broken blocks collect AND deposit (repair only); * sabotaged secretly affects collect only. Meaningful when collect or deposit is non-empty. */ state: StationState /** how many collects have happened since this station was sabotaged; ramps the failure dice. * May be negative right after a fakeRepair (extra grace). */ collectionsSinceSabotage: number /** when the station entered the sabotaged state (ms), or null — feeds the time-based failure * chance alongside collectionsSinceSabotage */ sabotagedAt: number | null /** per-viewer knowledge: dids that have learned this station is sabotaged (inspectors + the * saboteur who set it) */ revealedSabotageTo: Did[] } export interface Debris { id: string pos: Vec authentic: boolean resourceId: ResourceId expiresAt: number /** per-viewer knowledge: dids that have inspected this debris (and thus know authenticity) */ inspectedBy: Did[] } export interface Vote { startedAt: number endsAt: number /** voter did -> target did */ ballots: Record } export interface GameState { code: string phase: GamePhase /** authoritative current time (ms epoch); advanced by every action's serverTime */ now: number config: GameConfig startedAt: number | null endsAt: number | null players: Record stations: Record /** collected totals per resource */ resources: Record debris: Record vote: Vote | null outcome: Outcome | null /** when the round will abort because the saboteur disconnected and didn't return in time (§3.6), * or null when the saboteur is present. SERVER-ONLY — never projected: it would identify the * saboteur. Set on the saboteur's playerDisconnected, cleared on their playerReconnected. */ saboteurGraceEndsAt: number | null /** the level's navigation grid for pathfinding; null until provided at gameCreated */ nav: NavGrid | null /** count of actions applied — serves as the subscription cursor */ seq: number } /** Seed used by `gameCreated` to lay out a station. Capabilities mirror Station; `slotCount` is * filled from the player-count balance at room creation (never authored on the map). */ export interface StationSeed { id: string pos: Vec area: Rect slotCount: number collect: ResourceId[] deposit: ResourceId[] scoreboard: ResourceId[] meeting: boolean /** optional human label authored on the map (`town.deception.stationName`, e.g. "Chicken coop"), * used to make deduction prompts read naturally. A plain display string, not a record ref. */ name?: string }