import type { Vec3 } from "../world/geometry.js"; /** * A box of authored world geometry a body can hide behind. Structurally a `WorldSolid`, so * `ctx.world.solids.all()` drops in directly; a game with its own collision can pass any boxes. */ export interface CoverSolid { readonly center: readonly [number, number, number]; readonly halfExtents: readonly [number, number, number]; /** Yaw about `center`, radians, in the engine's `rotationY` convention. Default `0`. */ readonly rotationY?: number; } /** One stand position beside authored geometry, with the face direction it hides against. */ export interface CoverPoint { /** Stable id from the rounded position and face, so a held point survives a rebuild. */ readonly id: string; readonly position: Vec3; /** Unit XZ direction pointing away from the solid — the way a body leans out to shoot. */ readonly normal: readonly [number, number]; } /** * Bounded source of cover near a position: generated from authored geometry, or the game's own * spatial query. Called for one member at a time, never for the whole squad. */ export interface CoverSource { near(position: Vec3, radius: number, limit?: number): readonly CoverPoint[]; } /** How cover points sit relative to the geometry that generates them. */ export interface CoverPointOptions { /** How far a stand point sits off the face, world units. Default `0.7`. */ standOff?: number; /** Spacing of stand points along a face. Default `2`. */ spacing?: number; /** Faces shorter than this get no stand points. Default `1.2`. */ minFaceLength?: number; /** Most stand points per face. Default `3`. */ maxPerFace?: number; /** Ground height at a stand point; omit to use the bottom of the solid. */ yOf?: (x: number, z: number) => number; /** Rejects stand points that are not walkable — wire `nav.findPolygon(p) !== null`. */ walkable?: (point: Vec3) => boolean; } /** * Stand points on every vertical face of the supplied boxes: one row per face, spaced along it, * `standOff` out from the surface. Geometry in, cover out — no authored coordinate list, so moving * a wall in the editor moves its cover. * * @capability cover-points-from-solids derive stand-able cover positions from authored solid boxes */ export declare function coverPointsFromSolids(solids: readonly CoverSolid[], options?: CoverPointOptions): CoverPoint[]; /** * The `limit` nearest cover points within `radius`, nearest first, ties broken by id. A linear scan * of the points the field holds; the field caps how many points exist. * * @capability cover-points-near bounded nearest-cover query */ export declare function coverPointsNear(points: readonly CoverPoint[], position: Vec3, radius: number, limit?: number): CoverPoint[]; /** Serializable cover field state: the boxes and the dials, not the derived points. */ export interface CoverFieldSnapshot { solids: CoverSolid[]; standOff: number; spacing: number; minFaceLength: number; maxPerFace: number; } /** Options for {@link createCoverField}: the authored boxes, the dials, and a probe name. */ export interface CoverFieldOptions extends CoverPointOptions { solids: readonly CoverSolid[]; /** Register a devtools probe under this name so the overlay shows the derived point count. */ devtoolsName?: string; } /** Cover derived from authored geometry, rebuilt when the world is edited. */ export interface CoverField extends CoverSource { points(): readonly CoverPoint[]; count(): number; /** Re-derive from a new set of boxes after an editor edit or a procedural world change. */ rebuild(solids: readonly CoverSolid[]): void; retune(patch: CoverPointOptions): void; snapshot(): CoverFieldSnapshot; restore(next: CoverFieldSnapshot): void; /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */ dispose(): void; } /** * Cover over authored boxes: derive the stand points once, re-derive them after an edit, and answer * bounded nearest-cover queries. `walkable` is injected, so the seam checks the game's own ground * test (`nav.findPolygon(point) !== null`) instead of assuming a flat floor. * * @capability cover-field authored-geometry cover with a bounded nearest query and rebuild on edit */ export declare function createCoverField(options: CoverFieldOptions): CoverField; /** A remembered threat: where the member last saw it and how strongly it still believes it. */ export interface CoverThreat { readonly id: string; readonly position: Vec3; /** `0..1` memory confidence; a fading memory steers less. */ readonly confidence: number; } /** What each term is worth when choosing cover. A role owns these numbers. */ export interface CoverWeights { /** Hides the stand point from remembered threats. Default `3`. */ hidden?: number; /** Stands at the role's preferred fighting distance from the nearest threat. Default `1`. */ range?: number; /** Stands at the role's flank angle around the threat. Default `1`. */ flank?: number; /** Stays close to where the member is now. Default `0.6`. */ near?: number; /** Avoids a point a squadmate already holds. Default `2`. */ crowding?: number; /** Adds distance from the nearest threat — a fallback. Default `0`. */ retreat?: number; /** Penalizes a point the member cannot walk to in a straight line (a detour around the obstacle). Default `1.2`. */ path?: number; } /** Everything one cover decision reads: where the member is, what it believes, and its role's policy. */ export interface CoverQuery { position: Vec3; /** Radius `near()` was called with, so `near` and `retreat` can normalize. */ radius: number; threats: readonly CoverThreat[]; /** Distance from the nearest threat this role would rather fight at. */ preferredRange: number; /** Angle around the threat to attack from, radians off the squad's line. Default `0`. */ flankAngle?: number; /** Point the squad's line runs through, so a flank angle means the same for every member. Default `position`. */ lineThrough?: Vec3; /** Ids squadmates already hold. */ taken?: ReadonlySet; /** True when nothing blocks the segment — the game's collision layer. */ blocked: (from: Vec3, to: Vec3) => boolean; /** Ignore memories weaker than this. Default `0.05`. */ minConfidence?: number; /** Only consider points hidden from at least one remembered threat. Default `false`. */ hiddenOnly?: boolean; /** * Radius of the body a point must hide, world units: a point counts as hidden only when the body * standing there is out of sight, not just its centre. `0` tests the point alone. Default `0.4`. */ bodyRadius?: number; weights?: CoverWeights; } /** A scored cover point, with every term visible so a probe or test can see why it won. */ export interface CoverChoice { readonly point: CoverPoint; readonly score: number; readonly hidden: number; readonly range: number; readonly flank: number; readonly near: number; readonly retreat: number; /** `1` when the straight walk to the point is blocked, so reaching it means a detour. */ readonly path: number; } /** * Score one stand point against the threats the member remembers. `hidden` is the confidence-weighted * share of remembered threats that cannot see the point, so a point that hides from the shooter that * matters scores over one that hides from a fading one. Returns `null` when nothing it can hide from * is worth considering. * * @capability cover-score threat-driven cover point scoring */ export declare function scoreCoverPoint(point: CoverPoint, query: CoverQuery): CoverChoice | null; /** * The best of `points` under one {@link CoverQuery}, ties broken by point id. Pass the candidate set * the injected query already bounded; this scores what it is given. * * @capability cover-select select cover by threat, role range and flank angle */ export declare function selectCover(points: readonly CoverPoint[], query: CoverQuery): CoverChoice | null;