import type { EntityPosition } from "../scene/entityStore.js"; import { type StatPool, type StatPoolAccess } from "../stats/statPool.js"; export interface ReceiveRule { order: string[]; modifiers?: Record; } export type ReceiveMap = Record; export interface EffectVia { item?: string; amount?: number; } export interface SingleTargetEffectInput { from: string; to: string; effect: string; via?: EffectVia; } export interface AreaEffectInput { from: string; effect: string; via?: EffectVia; at: EntityPosition; radius: number; falloff?: "linear" | "none"; los?: boolean; } export type EffectInput = SingleTargetEffectInput | AreaEffectInput; export interface AppliedPoolDelta { statId: string; delta: number; } /** * Identity of an entity slain by a lethal effect, captured before the death system despawns it. * Serializable and genre-agnostic: `catalogId` is the slain entity's spawn kind/name (the same * value the `entity.died` event carries), `name` an optional human-readable label for kill feeds, * and `userId` the owning player when the slain unit was player-controlled. The slain entity's * instance id is already {@link EffectResult.instanceId}. */ export interface SlainIdentity { catalogId: string; name?: string; userId?: string; } export interface EffectResult { instanceId: string; effect: string; applied: AppliedPoolDelta[]; lethal: boolean; /** * Present only on a lethal hit: the slain entity's identity, captured before despawn so * kill-credit / XP can read `catalogId`/`name` without a game-side spawn-time registry. * A non-lethal hit omits this field. */ slain?: SlainIdentity; } export interface LethalContext { from: string; via?: EffectVia; effect: string; } export interface CombatSpatialDeps { inRadius(center: EntityPosition, radius: number): string[]; hasLineOfSight(from: EntityPosition | string, to: string): boolean; positionOf(instanceId: string): EntityPosition | undefined; } export interface AreaTargetInput { at: EntityPosition; radius: number; falloff?: "linear" | "none"; los?: boolean; } export interface AreaTarget { instanceId: string; scale: number; } export declare function resolveAreaTargets(spatial: CombatSpatialDeps, input: AreaTargetInput, accept?: (instanceId: string) => boolean): AreaTarget[]; /** * One effect application that drained a target's pools, as an announcer needs it. The effect path * has no damage table, so the caller names the channel (`effect`) and reads health its own way. */ export interface EffectDamageApplication { /** The entity that took the drain. */ target: string; /** Who dealt it: the effect's `from` instance id (attacker, hazard, projectile owner). */ source: string; /** The effect id that landed (`"damage"`, `"bullet"`, `"fire"`). */ effect: string; /** Magnitude before receive modifiers and area falloff. */ incoming: number; /** Magnitude drained after modifiers, falloff and available pool headroom. */ dealt: number; /** The last pool's current after the drain — the pool whose exhaustion is `lethal`. */ remaining: number | null; /** True when the last pool reached its minimum. */ lethal: boolean; } export interface EffectSystemDeps { resolveReceive(instanceId: string): ReceiveMap | null | undefined; /** Preferred portable bridge to caller-owned resource state. */ statPools?: StatPoolAccess; /** * Legacy native bridge retained for source compatibility. New integrations * should provide {@link statPools} and keep their own state model. */ resolveStats?(instanceId: string): Record | undefined; getStat(itemId: string, stat: string): number | null; spatial: CombatSpatialDeps; drainStatByEffect?: Record; /** * Resolve the target's identity, called on a lethal hit *before* {@link onLethal} despawns it. * The returned {@link SlainIdentity} is attached to the lethal {@link EffectResult.slain} so kill * credit works without a parallel registry. Return `null`/`undefined` to omit identity. */ resolveSlainIdentity?(instanceId: string): SlainIdentity | null | undefined; onLethal?(instanceId: string, ctx: LethalContext): void; /** * Called once per target a draining effect lands on, after its pools changed and before * {@link onLethal} despawns it. Restorative applications (`via.amount` below zero) stay silent. */ onDamage?(application: EffectDamageApplication): void; } export interface EffectSystem { canReceive(instanceId: string, effect: string, magnitude?: number): string | null; preview(input: SingleTargetEffectInput): number; applyEffect(input: EffectInput): EffectResult[]; } /** * Resolve direct or area resource effects through caller-owned stat pools, * including ordered absorption, restoration, modifiers, and lethal context. * * @capability status-effects commit direct or area effects through caller-owned stat pools with ordered spillover and lethal results */ export declare function createEffectSystem(deps: EffectSystemDeps): EffectSystem;