import type { EditorMarker } from "../editor/types.js"; import type { LaneGraph } from "../nav/laneGraph.js"; import { type HeatPoint, type HeatState } from "./heatSystem.js"; /** How police answer the current heat tier — watching the streets up to cordoning them. */ export type PoliceResponseKind = "patrol" | "investigate" | "pursue" | "roadblock"; /** One response tier: the heat it begins at, what it puts on the street, and how wide it searches. */ export interface PoliceTier { response: PoliceResponseKind; /** Cumulative heat at which this tier begins. Ascending across the table. */ threshold: number; /** Pursuers this tier wants active on the street. */ pursuers: number; /** Radius, metres, of the search area while the trail is fresh. */ searchRadius: number; /** Roadblock sites this tier holds, chosen from the authored road data. */ roadblocks: number; } /** The starting tier table: patrol until heat arrives, then investigate, pursue and finally cordon. */ export declare const DEFAULT_POLICE_TIERS: readonly PoliceTier[]; /** * One remembered sighting of the suspect, as the perception service reports it — * `PerceptionMemory` from `sensor/perception` satisfies this shape. */ export interface PoliceSighting { lastKnownPos: readonly [number, number, number]; lastSeenAt: number; confidence: number; /** Whose senses produced the fact, and the observe pass it arrived on. */ sourceId: string; sourceTick: number; } /** * The witness seam: perception memory, queried by observer id. Pass `createPerception` * (`sensor/perception`) straight in — a crime is witnessed when an observer still holds the suspect * in view, so there is no second sight path to keep in step. */ export interface PoliceWitness { memoryOf(observerId: string, targetId: string): PoliceSighting | null; } /** A lane near a world point: its id, the arc of the nearest point on it, and how far away that is. */ export interface PoliceLaneHit { lane: string; arc: number; distance: number; } /** A roadblock site on a lane, resolved from authored road data. */ export interface PoliceRoadblockSite { id: string; lane: string; /** Arc along `lane` where the block sits. */ arc: number; /** World XZ of the block, on the lane centerline. */ at: HeatPoint; /** Lane heading there, so the blocking vehicle faces along the road. */ heading: number; } /** Road data the response reads: nearby lanes for the search area, authored sites for roadblocks. */ export interface PoliceRoads { /** Lanes within `radius` metres of a point, nearest first, at most `limit` of them. */ lanesNear(x: number, z: number, radius: number, limit: number): readonly PoliceLaneHit[]; /** Every authored roadblock site, sorted by id. */ roadblockSites(): readonly PoliceRoadblockSite[]; } /** The serializable policy of a response: the tier table, the heat curve and the search shape. */ export interface PoliceTuning { /** Sorted ascending by `threshold`; level 0 (patrol) is implicit below the first. */ tiers: readonly PoliceTier[]; /** Heat bled per second once the suspect has broken sight. */ decayPerSecond: number; /** Extra seconds after the last witnessed gain before decay starts. Default 0. */ decayDelaySeconds: number; /** Seconds at zero heat with pursuers alive before the response stands down. */ standDownSeconds: number; /** Seconds out of every witness's sight before the suspect has escaped. */ escapeSeconds: number; /** How stale a sighting may be and still count as "in view", seconds. */ inViewGraceSeconds: number; /** Metres per second the search area grows while the trail is cold. */ searchGrowthPerSecond: number; /** Cap on the grown search radius, metres. */ searchRadiusMax: number; /** Most lanes reported in one search area. */ searchLaneLimit: number; /** Most roadblocks held at once, whatever the tier asks for. */ roadblockLimit: number; /** Heat ceiling; omit for unbounded. */ maxHeat?: number; /** `[min, max]` radius for spawn points; omit to skip ring generation. */ spawnRingRadius?: readonly [number, number]; /** Seed for the injected generator that rolls spawn points. Same seed, same ring. */ seed?: number; } /** The starting policy: linear decay, a six-second escape and a search area that grows at 2 m/s. */ export declare const DEFAULT_POLICE_TUNING: PoliceTuning; /** Everything {@link createPoliceResponse} needs: the witness seam, the roster and the caller's policy. */ export interface PoliceResponseConfig extends Partial { witness: PoliceWitness; /** Bounded roster of observer ids to ask each tick. The caller owns and bounds it. */ observers: () => readonly string[]; /** The id the witness roster remembers the suspect by. */ suspectId: string; /** Lane data for the search area and roadblocks; omit for a radius-only search and no roadblocks. */ roads?: PoliceRoads; /** Register a devtools probe under this name so the overlay shows heat, tier and search work. */ devtoolsName?: string; } /** Where the search is, as the last sighting places it. */ export interface PoliceSearchArea { /** Centre: the suspect's last known world XZ. */ center: HeatPoint; /** Radius, metres — the tier's own radius, grown while the trail goes cold. */ radius: number; /** Lane ids inside the area, nearest first. Empty without `roads`. */ lanes: readonly string[]; /** Perception clock of the sighting that anchored the area. */ lastSeenAt: number; sourceId: string; sourceTick: number; } /** Per-tick world facts {@link PoliceResponse.tick} needs but cannot derive itself. */ export interface PoliceTickContext { /** The clock the witness memories are stamped on, milliseconds — the same one `observe` got. */ nowMs: number; /** Pursuers the caller currently has on the street. */ activePursuers: number; /** Origin for spawn points; usually the suspect. */ around: HeatPoint; } /** One tick's answer: the heat, the tier, the search area, the cordon and the escape. */ export interface PoliceStep { heat: number; /** `null` below the first threshold — the implicit patrol response. */ tier: PoliceTier | null; response: PoliceResponseKind; tierChanged: boolean; pursuerBudget: number; /** `max(0, pursuerBudget - activePursuers)` — how many more pursuers to send this tick. */ wantPursuers: number; /** `wantPursuers` points on the configured ring around `ctx.around`; empty without `spawnRingRadius`. */ spawnPoints: readonly HeatPoint[]; searchArea: PoliceSearchArea | null; /** Sites the cordon holds this tick, nearest to the search centre first. */ roadblocks: readonly PoliceRoadblockSite[]; /** Seconds since any witness last had the suspect in view. */ outOfSightSeconds: number; /** True once out of sight for `escapeSeconds`; decay runs while it holds. */ escaped: boolean; /** True exactly the tick pursuit should fully clear — despawn every remaining pursuer. */ standDown: boolean; } /** Counters a devtools probe, a HUD or a test reads. */ export interface PoliceStats { heat: number; response: PoliceResponseKind; ticks: number; /** Crimes reported through `reportCrime`. */ crimes: number; /** Of those, the ones a witness held the suspect in view for. */ witnessed: number; /** Sightings that moved the search area's anchor. */ sightings: number; /** Times the suspect broke sight for `escapeSeconds`. */ escapes: number; /** Ticks a roadblock tier held a cordon. */ roadblockTicks: number; /** Witness queries made in the last tick — one per observer. */ sightingsChecked: number; /** Lanes the last search area covered. */ lanesInArea: number; /** Authored sites inside the last search area. */ roadblockSites: number; /** Slowest single `tick` seen so far, milliseconds. */ slowestTickMs: number; } /** Serializable response state — the heat curve, the sight timer, the anchor and the counters. */ export interface PoliceResponseState { tunables: PoliceTuning; heat: HeatState; outOfSightSeconds: number; lastSighting: { position: HeatPoint; at: number; sourceId: string; sourceTick: number; } | null; /** Crimes reported since the last tick, judged for a witness on the next one. */ pending: readonly number[]; counters: { ticks: number; crimes: number; witnessed: number; sightings: number; escapes: number; roadblockTicks: number; }; } /** Stateful wanted/pursuit API with retuning and save/restore support. */ export interface PoliceResponse { /** Report a crime. Whether it raises heat is decided on the next tick, from witness memory. */ reportCrime(amount: number): void; tick(dt: number, ctx: PoliceTickContext): PoliceStep; /** Heat right now and the tier it resolves to. */ wanted(): { heat: number; tier: PoliceTier | null; response: PoliceResponseKind; }; /** The search area from the last tick, or `null`. */ searchArea(): PoliceSearchArea | null; /** The cordon from the last tick. */ roadblocks(): readonly PoliceRoadblockSite[]; stats(): PoliceStats; retune(patch: Partial): void; state(): PoliceResponseState; restore(next: PoliceResponseState): void; /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */ dispose(): void; } /** * The wanted level a crime sandbox needs, over the shared heat curve: a crime raises heat only while * a witness holds the suspect in view, the heat bleeds on a stated linear curve once the suspect has * been out of every witness's sight for `escapeSeconds`, the tier table decides what police do * (patrol → investigate → pursue → roadblock), the search area sits where the suspect was last seen * and grows while the trail is cold, and a roadblock tier cordons it with authored sites off the lane * graph. Heat, tiers and curves are caller policy; witness facts come from `sensor/perception` memory * (`sourceId`/`sourceTick` kept), so there is no second sight path. * * Deterministic: observers are asked in roster order, ties break on id, and the injected generator is * the only source of variety. Bounded per tick: one memory query per observer, one indexed lane query, * one pass over the authored roadblock sites, no path search. * * @capability police-response witness-gated heat, tiered police response, last-sighting search areas and lane-graph roadblocks */ export declare function createPoliceResponse(config: PoliceResponseConfig): PoliceResponse; /** How the document reader recognizes authored road content. */ export interface PoliceRoadsOptions { /** Marker kinds authored as one roadblock site each. Default `["traffic_roadblock"]`. */ roadblockKinds?: readonly string[]; /** Metres a marker may sit from a lane and still become a site. Default 8. */ joinRadius?: number; /** Grid cell, metres, the lane index is bucketed by. Default 8. */ cellSize?: number; } /** * Read the police road data out of an authored lane graph: `traffic_roadblock` markers become * cordon sites projected onto the lane they block, and lanes are indexed so `lanesNear` answers from * the local cells rather than a scan of every lane. With no roadblock markers authored, the graph's * intersections are the sites, so a cordon still sits where streets meet. Built once; the index is * rebuildable from the document, so it is never serialized. * * @capability police-roads lane-graph roadblock sites and an indexed nearby-lane query for search areas */ export declare function policeRoadsFromLaneGraph(graph: LaneGraph, markers?: readonly EditorMarker[], options?: PoliceRoadsOptions): PoliceRoads;