export type Vec3 = { x: number; y: number; z: number; }; /** A rotation quaternion. Physics (dynamic-body) kinds only — kinematic kinds carry no orientation on the wire. */ export type Quat = { x: number; y: number; z: number; w: number; }; /** Pinned units. Linear in meters / meters-per-second; every angle is degrees (see the *Deg suffixes). */ export declare const UNITS: { readonly position: "meters"; readonly velocity: "meters/second"; readonly angles: "degrees"; }; /** Stable per-player identity (set at join from the room credential; not part of the per-tick churn). */ export interface PlayerIdentity { /** Opaque per-connection room playerKey (= the state.players map key). Gameplay state is keyed by THIS, never by `userId`. */ id: string; /** Sanitized display name for nameplates (sanitized before it enters room state — see plan H4). */ displayName: string; /** * The player's PLATFORM user id (the credential `sub`), so a roster UI can link a seat to a real * profile (batch /api/v1/users lookup, profile drill-in). v18 deliberately supersedes the original §4 * stance that `sub` never replicates: user ids are already public directory data on the platform, so * room-mates learn nothing new. Copied once at join from the verified credential, never client-supplied. * '' when the room predates v18. Gameplay state stays keyed by the opaque per-connection `id`. */ userId: string; /** * CDN URL of this player's equipped universal avatar (a converted single-file GLB on the * helix-humanoid rig), or '' when they have none — clients render the default body on ''. * Resolved and skeleton-gated SERVER-SIDE at join (backend → signed credential → room), never * client-supplied — and it is item-keyed public catalog data, NOT the userId (which stays * server-side, §4). Fixed for the seat's lifetime: equip changes apply on the next join. (v17) */ avatarUrl: string; } /** * One replicated prop attachment (v20). Exactly what a remote replica needs to render the prop: the * asset URL (opaque world data — every client fetches the same CDN asset), the socket name, and the * sender-RESOLVED socket-local offsets — the preset is applied at the sender, so replicas place the * prop correctly across humanoid-character version skew. `preset` still rides the wire as a forward * hook (Phase 3 pairs presets with grip finger poses); placement never depends on it. */ export interface AttachmentEntry { /** Client-generated id, unique within the seat (ATTACHMENT_LIMITS.idPattern); replicas diff + evict by it. */ id: string; /** Prop GLB URL (https, room-sanitized, length-bounded). A failed/unknown asset no-ops on replicas. */ asset: string; /** Socket name on the helix-humanoid rig (e.g. 'hand_r.grip'); a socket the replica's rig lacks no-ops. */ socket: string; /** Socket-local offset, METERS. Vec3 like every other wire position (the room mirrors it as Vec3Schema). */ position: Vec3; /** Socket-local orientation, DEGREES (XYZ euler order), as a Vec3 {x,y,z}. */ rotationEulerDeg: Vec3; /** HELD_ITEM_PRESETS key or '' — the offsets above are already final. */ preset: string; } /** * One emote the player CARRIES (v28) — an owned item, resolved onto the seat by the ROOM from the platform, * never named by the client (the `avatarUrl` trust shape, not the `attachments` one). `slot` is the wheel * position it plays from, `clip` the animation URL every client fetches for itself, `playback` how it runs. */ export interface EmoteEntry { /** Wheel position: 'r0'..'r7' are the eight radial slots, 'sit'/'lie' the two posture emotes. */ slot: 'r0' | 'r1' | 'r2' | 'r3' | 'r4' | 'r5' | 'r6' | 'r7' | 'sit' | 'lie'; /** The universal-item id this emote came from; replicas diff + evict by (slot, itemId). */ itemId: string; /** Clip URL (servable, room-sanitized, EMOTE_LIMITS-bounded). A failed/unknown clip no-ops on replicas. */ clip: string; /** How the clip runs once triggered: once through, or until it is stopped. */ playback: 'oneshot' | 'loop'; /** * The emote's SOUND url, every client fetches it for itself exactly like `clip` and under the same servable * rule. Absent when the emote is silent — audio is garnish, so a clip whose sound fails sanitization still * lands, without it. */ audio?: string; /** The item's display title, for the wheel's petals. Nameplate-sanitized + capped at EMOTE_LIMITS.maxLabelLength; absent when the platform named it nothing. */ label?: string; /** * Optional ENTER transition: plays once and chains into `clip`. Same servable-url rule and the same * EMOTE_LIMITS.maxClipUrlLength bound as `clip`. Never present with `roles`. */ intro?: string; /** * Optional EXIT transition: plays on a deliberate end and on a oneshot's natural completion. Same servable-url * rule and bound as `clip`. Never present with `roles`. It replicates through the v19 `gesture` relay under the * suffixed name `emote.:outro` — the relay is name-opaque, so no new message channel exists for it. */ outro?: string; /** * PAIRED emote (v28): the second role this emote offers, so another player can join it. Absent = a solo emote, * and only an 'r0'..'r7' slot may carry it (the room strips it elsewhere) — the posture slots are stances, not * invitations. The INITIATOR is whoever plays the emote; a joiner runs `joinerClip` at the offset below, * which is expressed in the INITIATOR-LOCAL frame (+Z = the initiator's facing, +X their right) so the pair * stays composed wherever the initiator stands. The offer itself is a HELD POSE (`offerAtS`), and a pair is * strictly 1:1 — the room refuses a second joiner on an initiator that already has one. */ roles?: { /** The joiner's clip URL — same servable-url rule and same EMOTE_LIMITS.maxClipUrlLength bound as `clip`. */ joinerClip: string; /** * The OFFER freeze, SECONDS into the INITIATOR's own `clip`: the engine plays 0→offerAtS once as a wind-up, then * HOLDS that pose until a joiner arrives or the initiator cancels. ≥ 0 (0 = the first frame), bounded by * EMOTE_LIMITS.maxOfferAtS and independent of `playback`, which describes only the JOINED performance. */ offerAtS: number; /** Joiner offset from the initiator, METERS, initiator-local; each axis clamped to ±EMOTE_LIMITS.maxPairOffsetM. */ offsetX: number; offsetZ: number; /** The joiner's facing RELATIVE to the initiator's, DEGREES, normalized to [−180, 180] (180 = facing them). */ offsetYawDeg: number; }; } /** * The per-tick replicated kinematic + animation parameters for one player. The room holds the * authoritative copy (a @colyseus/schema mirror of these fields); the NetworkDriver maps each field * onto the named character blackboard param. FIRST DRAFT — the exact injected set is confirmed by * spike S2 (engine headless-replica write-path); changing the field set bumps CONTRACT_VERSION. */ export interface PlayerReplicaState { /** World feet position, METERS. Interpolated by the remote replica. */ position: Vec3; /** Body facing, DEGREES. The adapter converts to radians for body.setFacingYaw. */ facingYawDeg: number; /** Horizontal planar speed, M/S. → blackboard 'speed' (blend-space input). */ speed: number; /** Signed movement direction relative to facing, DEGREES (−180..180). → blackboard 'direction'. */ moveDirectionDeg: number; /** Vertical velocity, M/S (+up). → blackboard 'verticalVelocity' (jump/fall states). */ verticalVelocity: number; /** Ground contact. → blackboard 'isGrounded' (grounded/air transitions). */ grounded: boolean; /** Crouch stance. → routes the locomotion anim graph to its crouched states. */ crouched: boolean; /** * Physics owns this player's body (v25) — the death ragdoll, or a world's manual `character.ragdoll.enter()`. * Client-authoritative cosmetic state like `crouched`: replicas mirror it into their OWN ragdoll driver, so a * late joiner sees a limp player through STATE, not a missed event. */ ragdolled: boolean; /** * The shove this ragdoll STARTED with (v26): unit horizontal direction × speed (m/s); all 0 when the body just * crumpled. Held stable while `ragdolled` is true so a replica that observes the false→true EDGE re-creates the * entry impulse on its own corpse. A late joiner reads state (not the edge) and must NOT re-apply it — the * sender's followed position already encodes where the flight ended. */ ragdollLaunchX: number; ragdollLaunchZ: number; ragdollSpeedMps: number; /** Aim yaw, DEGREES (world). → blackboard 'cameraYaw' (aim-additive source). */ aimYawDeg: number; /** Aim pitch, DEGREES. → blackboard 'cameraPitch' (aim-additive source). */ aimPitchDeg: number; /** Ability ids currently active (e.g. ['locomotion','fly']). Drives remote ability activation. */ activeAbilities: string[]; /** * The world-registered LOOPING gesture clip this player currently plays, '' when none (v19). Loops replicate * as STATE — not events — so a late joiner starts them at replica spawn; oneshot gestures ride the transient * `gesture` broadcast instead. The name is opaque world data: every client registered the same clips from the * same bundle (registerAnimation), so only the name crosses the wire; an unknown name renders as no gesture. */ loopGesture: string; /** * The props currently attached to this player (v20). Attachments are persistent, so they sync as STATE — * a late joiner converges at replica spawn — never as events. Written whole by the `attachments` full-set * replace message; bounded by ATTACHMENT_LIMITS.maxAttachments; [] when none (or the room predates v20). */ attachments: AttachmentEntry[]; /** * Live IK reach state (v21) — the WORLD-SPACE point each hand's procedural arm IK is holding (ik.reach: * door handles, buttons, levers) and the interaction gaze point (ik.lookAt). Continuous per-tick data * like position/aim, ridden in on the `state` upload (omitted field = inactive). Replicas feed the points * into their OWN local solvers — no bone transforms ever cross the wire. When an `*Active` flag is false * the paired vector just holds its last value (unwritten fields delta-encode to ~0 bytes) — always check * the flag first. False/zero when the room predates v21. */ reachLeftActive: boolean; /** Left-hand reach target, world METERS. Meaningful only while reachLeftActive. */ reachLeft: Vec3; reachRightActive: boolean; /** Right-hand reach target, world METERS. Meaningful only while reachRightActive. */ reachRight: Vec3; lookTargetActive: boolean; /** Interaction gaze point, world METERS (beats replicated aim-follow gaze). Meaningful only while lookTargetActive. */ lookTarget: Vec3; /** * Gun state (v24) — weapon SIGNALS, not motion: replicas re-run the same local easing/FX off these fields. * Absent when the sender carries no gun-control ability (or the room predates v24). * * `shotSeq` is a monotonic per-session shot counter, +1 per real shot. It is coalesced latest-wins at * 10/20 Hz, so consumers react to the DELTA (never assume +1); it restarts at 0 on reconnect, and a * non-positive delta means re-anchor (never replay). */ shotSeq?: number; /** Weapon drawn (vs holstered). */ gunDrawn?: boolean; /** ADS intent, not an aim fraction — replicas ease locally with the same time constant. */ aiming?: boolean; /** Reload in progress (wire-timed on replicas). */ reloading?: boolean; /** * The emote set this player CARRIES (v28) — resolved SERVER-SIDE from their equipped items at join and on the * `emoteRefresh` hint, exactly like `avatarUrl` (v17): a client can never name the clip URLs that land on its * own seat, so this is NOT the client-written `attachments` lane despite the matching array shape. Emotes are * persistent → they sync as STATE, so a late joiner converges at replica spawn. Bounded by * EMOTE_LIMITS.maxEntries; absent/[] when the player carries none, the world disabled emotes, or the room * predates v28. */ emotes?: EmoteEntry[]; /** * The locomotion clip set this player MOVES on (v34), as ONE servable pack-manifest base URL — resolved * SERVER-SIDE from their equipped locomotion pack at join and on the `avatarRefresh` hint, exactly like * `avatarUrl` (v17): a client can never name the pack that lands on its own seat. A pack is persistent → it * syncs as STATE, so a late joiner sees the right walk at replica spawn instead of a corrected one. '' or * absent = the platform locomotion set, which is also what a room predating v34 replicates. */ locomotionPack?: string; /** * PAIRED emotes (v28) — the pair this player JOINED, and never the one they offer: the initiator's own seat * carries nothing (their offering is derivable — their `loopGesture` is `emote.` and that slot's entry * has `roles`). ROOM-WRITTEN ONLY, like `avatarUrl`/`emotes` and unlike every other field here: the `emoteJoin` * message is a request the room validates (liveness, range, the initiator actually offering and not already * paired — a pair is strictly 1:1) and answers by writing — or by writing nothing — so a client can never seat * itself in someone else's emote. The room also owns every clear (the initiator * stops/changes/teleports/leaves/dies, the joiner dies or drifts away). * * '' = not paired; otherwise the INITIATOR's player id (the `players` map key, the id-space `activeAbilities` * and every other ref use). */ pairInitiator: string; /** The initiator's slot being joined ('r0'..'r7'), so replicas read the roles off the right entry. '' = none. */ pairSlot: string; /** Room clock (epoch ms, the `{op:now}` source) at pair creation, so replicas can phase-align. 0 = none. */ pairStartMs: number; }