import { type BehaviorDescriptor } from "./behaviors.js"; export type EntityRole = "player" | "npc" | "prop"; export interface EntityMovement { walkSpeed?: number; frozen?: boolean; } export type EntityPosition = readonly [number, number, number]; export type SpawnPositionInput = EntityPosition | { x: number; y: number; z: number; }; export interface SceneEntity { id: string; name: string; position: EntityPosition; rotationY: number; rotationX: number; rotationZ: number; /** World units per second, derived by `setPose` with `dt` or explicitly replaced by `setVelocity`. Zero until set. */ velocity: EntityPosition; role: EntityRole; movement: EntityMovement; behaviors: readonly BehaviorDescriptor[]; /** Render-only visibility: `true` skips the entity's visual entirely (seated vehicle riders, cutscene actors, stealth cloaks) while the entity keeps simulating — position, stats, AI targeting, and queries are untouched. Absent/`false` renders normally. */ hidden?: boolean; /** Game-defined per-instance data set at `spawn`/`update` and carried to `renderEntity`/queries — narrow with {@link entityMetaOf} (or a type guard) on read (#286.1); never encode state in the id/name string. */ meta: TMeta; } /** * Narrow `entity.meta` with a type guard — prefer this over `entity.meta as T` so failed shapes return `null` * instead of lying to the type checker. * * @capability entity-meta cast-free narrow of SceneEntity.meta via a type guard */ export declare function entityMetaOf(entity: SceneEntity, isMeta: (value: unknown) => value is T): T | null; /** Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. */ export declare function groundSpeed(entity: SceneEntity): number; export declare function movedWhileFrozen(entity: SceneEntity, threshold?: number): boolean; export interface SpawnOptions { id?: string; position?: SpawnPositionInput; rotationY?: number; rotationX?: number; rotationZ?: number; role?: EntityRole; movement?: EntityMovement; behaviors?: readonly BehaviorDescriptor[]; hidden?: boolean; meta?: TMeta; /** When `id` is already spawned: `"throw"` (default), `"replace"` (fresh spawn over it — remount-safe world setup, #284.10), or `"keep"` (leave it untouched and return the id). */ onExisting?: "throw" | "replace" | "keep"; } export interface EntityPose { position: SpawnPositionInput; rotationY?: number; rotationX?: number; rotationZ?: number; /** Seconds since the previous pose; when > 0 the store derives `velocity` from the position delta. Omit for teleports (velocity unchanged). */ dt?: number; } export interface SpawnPose { position: EntityPosition; rotationY: number; } export interface PoseConstraintFrame { entityId: string; current: EntityPosition; next: EntityPosition; dt?: number; } /** Return a replacement position to constrain the step, or nothing to accept it. */ export type PoseConstraint = (frame: PoseConstraintFrame) => readonly [number, number, number] | undefined | void; export type EntityUpdatePatch = Partial, "name" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "hidden" | "meta">> & { /** Accepts the same friendly shapes as `spawn`/`setPose` (#286.13). Raw patch semantics — velocity is not derived; use `setPose` with `dt` for that. */ position?: SpawnPositionInput; }; /** * Per-entity scratch and cooldown timers, auto-cleared on despawn (#533.8) — the home for AI state * (next-shot-at, alert level) that would otherwise be smuggled through the serializable `meta` or a * hand-pruned module-level map. Keys share one namespace per entity: `arm`/`ready`/`remaining` store a * deadline under a key, `get`/`set` store arbitrary scratch. */ export interface EntityBlackboard { /** Read a scratch value; `undefined` if unset or the entity has despawned. */ get(id: string, key: string): T | undefined; /** Write a scratch value; does not notify subscribers, and clears when the entity despawns. */ set(id: string, key: string, value: unknown): void; has(id: string, key: string): boolean; delete(id: string, key: string): void; /** Drop all scratch and timers for one entity. */ clear(id: string): void; /** Arm a cooldown for `key` that stays busy until absolute time `untilMs`. */ arm(id: string, key: string, untilMs: number): void; /** True when no cooldown is armed for `key`, or its deadline has passed. */ ready(id: string, key: string, nowMs: number): boolean; /** Milliseconds until the armed cooldown elapses; 0 when ready. */ remaining(id: string, key: string, nowMs: number): number; } /** Entity state, pose writes, keyed behavior notifications and snapshot/hydration under stable instance IDs. */ export interface EntityStore { spawn(name: string, options?: SpawnOptions): string; despawn(id: string): boolean; update(id: string, patch: EntityUpdatePatch): boolean; setPose(id: string, pose: EntityPose): boolean; /** Replace velocity without moving the entity. Copies a finite three-number tuple; invalid input or a missing id returns false unchanged. */ setVelocity(id: string, velocity: EntityPosition): boolean; /** Register a constraint applied inside every `setPose` for this entity — the self-driven sibling of the shell's `beforeCommit` (#282.9): nav clamps, corridor walls, arena bounds without wrapping every call site. `null` clears; despawn clears automatically. */ setPoseConstraint(id: string, constraint: PoseConstraint | null): void; get(id: string): SceneEntity | null; list(): readonly SceneEntity[]; /** Stable-identity list of live entity ids; identity changes only on spawn/despawn/hydrate, not on pose writes. Pair with {@link subscribeMembership} for membership-only React subscriptions that skip per-frame pose churn (#625). */ ids(): readonly string[]; clear(): void; subscribe(listener: () => void): () => void; /** Notified only when the entity set changes (spawn/despawn/hydrate), never on a pose or field update — the render tree re-reconciles on membership, individual markers read live poses imperatively. */ subscribeMembership(listener: () => void): () => void; /** Changed behavior data or removal, keyed by id and notified before ordinary subscribers. Equal-data hydration and pose writes do not fire. */ subscribeBehaviors(listener: (id: string) => void): () => void; snapshot(): readonly SceneEntity[]; /** * Apply a `snapshot()` (e.g. from an authoritative host) to this store — the counterpart of `snapshot()` * for host→client world mirroring: spawns entities present in the snapshot but not here, overwrites * existing ones field-for-field (including velocity), and despawns any local entity absent from it. */ hydrate(entities: readonly SceneEntity[]): void; spawnPoseOf(id: string): SpawnPose | null; resetToSpawn(id: string): boolean; blackboard: EntityBlackboard; } /** Commit hook for a derived index; runs after the write and before existing subscribers. * `undefined` denotes a whole-store replacement. Value overwrites keep membership false. */ export interface EntityStoreOptions { onChange?: (instanceId: string | undefined, membershipChanged: boolean) => void; } export declare function createEntityStore(options?: EntityStoreOptions): EntityStore;