import type { EntityPosition } from "./entityStore.js"; export type Aim = { origin: EntityPosition; direction: EntityPosition; } | { yaw: number; pitch: number; spread?: number; }; export interface QueryArcOptions { from: string; aim: Aim; radius: number; halfAngleDeg?: number; } export interface MoveTowardOptions { speed: number; stopDistance?: number; dt: number; /** * Slide the step against the solid geometry that blocks the player instead of lerping through it * (default `true` wherever the runtime wires a resolver). Set `false` for a mover that is meant to * ignore the world — a flier, a camera rig, a decorative drift. */ avoidSolids?: boolean; } export interface SpatialGridOptions { cellSize: number; } export interface SpatialApiOptions { resolvePosition: (instanceId: string) => EntityPosition | undefined; candidates: () => readonly string[]; /** * When omitted, `hasLineOfSight` always returns true for known entities (open-field bypass). * Wire a wall/collision test here for combat LoS; `true` means the segment is blocked. */ occluder?: (from: EntityPosition, to: EntityPosition) => boolean; /** * Broadphase for `inRadius`/`queryArc`. Defaults to `{ cellSize: 8 }`. Pass `false` to force a * linear scan. When enabled, the index rebuilds lazily on first use after `invalidate()` or when * `getVersion()` advances — call `updateEntity(id)` after each committed move, or `invalidate()` * after an unreported batch, unless a version source is wired. */ grid?: SpatialGridOptions | false; /** When this number changes between queries, the grid rebuilds without an explicit `invalidate()`. */ getVersion?: () => number; /** Opt in only when every candidate add/remove/position change calls `updateEntity(id)` or * `invalidate()`. Avoids scanning all candidate IDs on warm queries. Initially unresolved IDs * still cost one position lookup per query until updated or invalidated. Default false retains * discovery of new candidates without notification. If positions also resolve outside the * candidate set, pass its explicit presence flag to `updateEntity`. Rebuilds are deferred to * the first query. */ incremental?: boolean; /** * Slide a horizontal step against the scene's solid geometry, returning the step actually allowed. * Wired by the runtime to the same blocking-collider query the player resolver reads, so a chaser * calling `moveToward` stops at the wall the player stops at. Omitted (tests, bare indexes) leaves * `moveToward` a pure lerp. */ resolveStep?: (position: EntityPosition, stepX: number, stepZ: number) => { stepX: number; stepZ: number; }; } export interface SpatialApi { distance(aInstanceId: string, bInstanceId: string): number | null; inRadius(center: EntityPosition | string, radius: number, filter?: (instanceId: string) => boolean): string[]; hasLineOfSight(fromInstanceId: string, toInstanceId: string): boolean; queryArc(options: QueryArcOptions): string[]; moveToward(instanceId: string, target: EntityPosition | string, options: MoveTowardOptions): EntityPosition | null; invalidate(): void; /** Notify after committing a candidate's position or membership. Pass `false` to remove an ID * even if its position still resolves, or `true` for a present candidate with a late position. * Omitted presence asserts membership when the position resolves, otherwise removes the entry; * subset candidate sets with a broader position resolver must pass explicit presence. * Preserves candidate order; does not build an invalidated/cold index or enumerate candidates. */ updateEntity(instanceId: string, candidatePresent?: boolean): void; } export declare function distanceBetween(a: EntityPosition, b: EntityPosition): number; /** Spatial queries with an optional notified incremental broadphase. * @capability spatial-incremental Update one entity's spatial cell after a committed write without scanning the population */ export declare function createSpatialApi(options: SpatialApiOptions): SpatialApi;