/** * Continuous area-effect membership: moving sources emit an area that follows them each tick, * receivers that move in and out get enter / refresh / leave edges, and membership is cleaned up * when a source is removed, disabled, or the field is cleared. A pure lifecycle machine — it never * applies damage, heals, or stat changes itself; the caller routes each edge through its own * effect pipeline (see `combat/effects`, `combat/dotField`, `stats/statModifiers`). This is the * dynamic, source-following counterpart to `scene/authoredTriggers` (static document volumes). * * Bounded by design: the caller supplies a broad-phase `candidates(shape)` query (e.g. * `visibility/spatialIndex`'s `querySphere`) plus a `positionOf` lookup, so a step visits only * receivers near each source — never the whole world. Serializable: `serialize()` captures the * membership set and each member's refresh phase so cadence and enter/leave edges survive save/load. */ import type { EntityPosition } from "../scene/entityStore.js"; /** * The area a source occupies this tick. `sphere` is first-class; `custom` leaves a seam for authored * volumes and other shapes while still exposing a bounding sphere (`center`/`radius`) for the caller's * broad-phase candidate query. */ export type AreaShape = { readonly kind: "sphere"; readonly center: EntityPosition; readonly radius: number; } | { readonly kind: "custom"; /** Bounding-sphere center the caller's broad-phase `candidates` query is built from. */ readonly center: EntityPosition; /** Bounding-sphere radius the caller's broad-phase `candidates` query is built from. */ readonly radius: number; /** Authoritative containment test applied to each broad-phase candidate. */ readonly contains: (point: EntityPosition) => boolean; }; /** Descriptor for one continuous area source; re-supplied each tick so its shape follows the emitter. */ export interface AreaSourceSpec
{ /** Stable source id (the emitter). Re-using an id updates the descriptor and preserves membership. */ readonly id: string; /** The source's area this tick — pass the emitter's live position so membership follows it. */ readonly shape: AreaShape; /** Caller data describing what this source does (damage amount, heal, buff key, capture rate…). */ readonly payload: P; /** Milliseconds between `refresh` edges emitted for each member; omit for enter/leave only. */ readonly refreshMs?: number; /** Aggregation group key for stacking policies; defaults to the source id. */ readonly stackKey?: string; /** When false the source keeps its descriptor but holds no members (current members `leave`). Default true. */ readonly enabled?: boolean; } /** Why a `leave` edge fired. */ export type AreaLeaveReason = "exit" | "source-removed" | "disabled" | "cleared"; /** The three membership edges a step can emit. */ export type AreaEventKind = "enter" | "refresh" | "leave"; /** One membership edge emitted by `step`, `removeSource`, or `clear`. */ export interface AreaEffectEvent
{ readonly kind: AreaEventKind; readonly sourceId: string; readonly receiverId: string; readonly stackKey: string; readonly payload: P; /** Number of refresh cadences crossed this step (>= 1 on `refresh`; a large dt can cross several). */ readonly ticks?: number; /** Present only on `leave`. */ readonly reason?: AreaLeaveReason; } /** One active (source, receiver) membership — feed a receiver's list to a stacking policy. */ export interface AreaMembership
{
readonly sourceId: string;
readonly receiverId: string;
readonly stackKey: string;
readonly payload: P;
}
/** Per-tick inputs the field needs to reconcile membership without scanning the world. */
export interface AreaStepInput {
/** Milliseconds since the previous step; negative values are clamped to 0. */
readonly dtMs: number;
/** Bounded broad-phase: receiver ids possibly inside `shape` (e.g. `spatialIndex.querySphere`). */
readonly candidates: (shape: AreaShape) => Iterable {
readonly sources: {
readonly id: string;
readonly payload: P;
readonly refreshMs?: number;
readonly stackKey: string;
readonly enabled: boolean;
}[];
readonly memberships: {
readonly sourceId: string;
readonly receiverId: string;
readonly sinceRefreshMs: number;
}[];
}
/** Runtime handle tracking continuous area membership across ticks. */
export interface AreaEffectField {
/** Register or update a source (upsert by id); an update preserves existing membership and refresh phase. */
setSource(spec: AreaSourceSpec ): void;
/** Remove a source and return a `leave` (reason `source-removed`) for each of its members. */
removeSource(id: string): AreaEffectEvent [];
hasSource(id: string): boolean;
sourceIds(): string[];
/** Reconcile every source against the receivers its `candidates` query returns; emit enter/refresh/leave. */
step(input: AreaStepInput): AreaEffectEvent [];
/** Receiver ids currently inside `sourceId`. */
members(sourceId: string): string[];
isMember(sourceId: string, receiverId: string): boolean;
/** Every active membership across all sources. */
memberships(): AreaMembership [];
/** Active memberships affecting a single receiver — the input to a stacking policy. */
membershipsOf(receiverId: string): AreaMembership [];
/** Drop all sources and members; returns a `leave` (reason `cleared`) for each member. */
clear(): AreaEffectEvent [];
/** Snapshot the persistent membership lifecycle for save/load; shapes are transient and re-supplied via `setSource`. */
serialize(): AreaFieldState ;
}
/**
* Build a continuous area-effect field. Drive it with `setSource` (once per live source per tick, so
* shapes follow their emitters) and `step` (to reconcile membership and drain enter/refresh/leave edges).
* Optionally restore prior membership by passing a `serialize()` snapshot; re-`setSource` live shapes
* before the first `step` after restore, since shapes are transient.
*
* @capability area-effect-field source-following area membership with enter/refresh/leave edges, stacking, and cleanup
*/
export declare function createAreaEffectField (state?: AreaFieldState ): AreaEffectField ;