import type { Vec3, Quat } from './state.js'; import type { PlayerIdentity, PlayerReplicaState } from './state.js'; /** * The runtime value vocabulary for declared custom state. A `ref`-typed var's value is the **id** of a live * member (a player's `playerKey` or an entity id) as a string, or `null` when unset/dangling — hence `null` * is included. The DECLARATION vocabulary (`VarType`: number/string/boolean/vec3/ref + bounds) lives in * `@hypersoniclabs/helix-manifest` (authored config, validated at publish); this is the wire value side. */ export type RoomVarValue = number | string | boolean | Vec3 | null | RoomVarCollection; /** * Per-player/room/entity COLLECTION values (Tier 2 Phase 4.5.13 + P3 collections, spec §5). A `list` var reads as * an ordered array — of a scalar element (`number`/`string`/`boolean`), a **ref** element (an id `string`), or a * flat **record** (`{field: scalar}`, P3) — realized server-side as a colyseus ArraySchema; a `counterMap` var * reads as a string→number record over its declared keys (a MapSchema). P3 also lets entities hold * collections and broadcast payloads carry a collection snapshot. Mutated only via the dedicated effects * (append/clear/addCount/removeAt/setField) + read via listLength/listAt/count — never a normal scalar expr/arg. */ export type RoomVarRecord = Record; export type RoomVarCollection = number[] | string[] | boolean[] | RoomVarRecord[] | Record; /** A declared bag of custom state — names + types are declared per-world; used for both room-level and per-player vars. */ export type RoomVars = Record; /** Full authoritative per-player state: the FIXED character contract + identity + DECLARED game vars. */ export interface PlayerState extends PlayerIdentity, PlayerReplicaState { /** Declared per-player game state (e.g. team, health, score). Same vocabulary as roomVars; declared per-world. */ vars: RoomVars; /** * Presence (Tier 2 Phase 3, spec §8): `false` while this seat sits in the reconnection grace window after an * unexpected drop — the seat (and its refs) persists, but the server excludes it from reductions. The seat is * removed (an `onRemove('players')`) only at grace expiry; until then the engine should render the replica idle. * This layers on Colyseus's own reconnection — it annotates the seat, it does not drive the reconnect handshake. */ connected: boolean; /** * Participation (Tier 2 P3-B2 elimination): `false` when this still-connected player has been removed from the * ACTIVE set mid-match (eliminated / benched / a spectator). An inactive seat is excluded from the same gameplay * reductions + spatial detection a disconnected seat is (forEachPlayer / playerCount / aggregate players / * nearestPlayer / zone + contact), but it stays `connected` and keeps rendering, and `broadcast` still reaches it * (so it can spectate). The room sets it via the `eliminate`/`revive` effects; defaults `true`. Clients may read it * to render spectators (e.g. greyed out). (Distinct from `connected`, which is presence/reconnection-grace.) */ active: boolean; /** * Source-time stamp (ms) of this seat's current `position` — the SANITIZED sender send-time (`StateMessage.tMs`), or * the server's apply-clock when the upload carried none. Clients SPACE interpolation keyframes by `posT` deltas * instead of local arrival time, so jitter in DELIVERY (network + 20 Hz-tick aliasing) stops becoming jitter in * rendered MOTION. Monotonic per seat; only deltas are meaningful (origin = the sender's arbitrary clock base). */ posT: number; /** * Server-owned monotonic avatar refresh revision (v33). The room increments this after each successful * authoritative equipped-avatar read, including when the base GLB URL is unchanged but its composition changed. * Older rooms omit the field; clients treat an absent value as 0. */ avatarRevision: number; } /** * RESERVED (Tier 2): a server-spawned object (pickup, projectile, NPC). Empty in v1 — the shape is * reserved so adding server-spawned entities later is additive. Tier 2 gives these author-defined * behavior; v1 only freezes the slot in RoomState. */ export interface EntityState { id: string; /** Author-defined kind (e.g. 'flag', 'pickup'). */ kind: string; /** * Server-owned logical seat occupancy for the opt-in dynamic Universal * Vehicle ABI. Older/static Worlds omit this map and retain authored vars. */ vehicleSeats?: Record; position?: Vec3; /** * Physics state — present ONLY for a `physics` (dynamic-body) kind (networked-physics, plan Phase 0). A kinematic * entity carries position only (these stay undefined; @colyseus/schema delta-encodes an unwritten field to ~0 bytes). * Synced so a remote client extrapolates with the RIGHT velocities (research §2: without synced velocity, remote * extrapolation pops) and reproduces orientation. Units: m/s (linear), rad/s (angular), unit quaternion. */ velocity?: Vec3; angularVelocity?: Vec3; orientation?: Quat; /** * Who simulates this entity (Tier 2 Phase 4.6c, spec §9). '' = server-authoritative (deterministic kinematics). * A player id (a state.players key) = an owner-authoritative entity that client simulates + uploads (the others * render it); a client reads this to know which entities it should be uploading via uploadEntity. */ controller: string; /** * Monotonic authority epoch (Tier 2 Phase 4.5.12, spec §9), bumped on every controller handoff — host * migration AND voluntary ownership transfer. A controller client mirrors this into each EntityStateMessage; * the room rejects an upload whose epoch ≠ the current one, fencing a stale authority era (a still-connected * prior owner's in-flight frames after ownership moved). 0 at spawn; clients only read it. */ authorityEpoch: number; /** * Declared custom vars (same vocabulary as roomVars). A root vehicle with a * signed detach protocol also carries `vDetachBits`: a server-owned uint16 * whose permanent bits identify parts that have detached. Clients only read * this value; detached-child expiry never clears it. */ vars: RoomVars & { readonly vDetachBits?: number; }; /** * Source-time stamp (ms) of this entity's current `position` — the server's apply-clock (owner-entity sender-time is * a follow-up; server-authoritative entities self-stamp each tick). Consumers space interpolation by `posT` deltas, * not arrival time. Monotonic; deltas only. 0 before first move. */ posT: number; } /** One synchronized module-instance chunk group. Local ids are meaningful only together with its module id. */ export interface ModuleState { roomVars: RoomVars; players: Record; entities: Record; phase: string; phaseStartTick: number; timerDeadlines: Record; } /** * Fixed public wire for a module installed after a room's reflection handshake. * The authoritative room keeps the full typed program/state privately and * projects only creator-declared public scalar room vars into these maps. */ export interface DynamicModuleState { phase: string; externalRevision: number; booleans: Record; numbers: Record; strings: Record; } /** The authoritative shared state. Always carries all three collections (entities empty until Tier 2). */ export interface RoomState { /** Per-player authoritative state, keyed by player id. Realized as a colyseus MapSchema. */ players: Record; /** Room-level declared game state (scores, timers, objectives). Names/types declared per-world. */ roomVars: RoomVars; /** RESERVED (Tier 2): server-spawned entities. Empty in v1. */ entities: Record; /** Authoritative server clock: a monotonic counter advanced once per fixed-rate sim tick. */ serverTick: number; /** * Authoritative server wall-clock (epoch ms) sampled at the last tick. Clients align to it (offset via the * ping/pong echo, see messages.ts) and interpolate between patches, so timer/phase deadlines expressed * against server time agree across clients without a per-tick countdown var. */ serverTimeMs: number; /** * The current phase of the world's room-scoped state machine (Tier 2 Phase 3, spec §8), or `''` when the * world declares no `states`. Authored phase names; changed only by the server's `transitionTo`. A * late-joiner reads it off synced state to know whether to spawn in or spectate (the join policy). */ phase: string; /** * The `serverTick` at which the room entered its current `phase`. With `serverTick`/`serverTimeMs` a client * computes time-in-phase locally (e.g. a round countdown) without a per-tick countdown var. */ phaseStartTick: number; /** * Active timers (Tier 2 Phase 3, spec §8) → absolute deadline as `serverTimeMs` (epoch ms). The map key is * the timer name for a room-scoped timer, or `"|"` for a per-player **keyed** timer (e.g. * read your own cooldown at `"cooldown|" + room.sessionId`). A client computes the seconds remaining as * `(deadline − estimatedServerTimeMs) / 1000` using its ping/pong offset, so a countdown interpolates locally * without a per-tick countdown var. An entry is removed when the timer fires or is cancelled. */ timerDeadlines: Record; /** * S2 module-v1 instance groups, addressed by the transport key `m:`. Absent on every legacy world; * use `room.module(id)` rather than indexing this transport container directly. */ modules?: Record; /** * Trusted runtime modules added after room creation. Present only for a * dynamic-module ABI build; legacy Worlds retain their exact reflected shape. */ dynamicModules?: Record; }