import type { DamageAppliedEvent, GameEvents } from "../game/events.js"; import type { StatusApplicationOutcome, StatusInstance } from "./statusApplication.js"; import { readDamageTable, type DamageTableDef, type DamageTableRow } from "./damageTable.js"; import { type HitZoneResolution, type HitZoneTable } from "./hitZones.js"; import { type StatusDef, type StatusRuntimeState, type StatusTickEvent } from "./statusRuntime.js"; /** What a target presents to the table: one resistance class and its armor points. */ export interface TargetProfile { /** Resistance class (`"flesh"`, `"shield"`, `"armor"`) the target presents. */ resistance: string; /** Armor points before any carried armor-shred statuses. Default `0`. */ armor?: number; } /** One status a hit tries to apply, scaled against that status's own base chance and values. */ export interface DamageStatusRequest { id: string; chanceScale?: number; durationScale?: number; magnitudeScale?: number; } /** One incoming hit: what struck, how hard, where it landed, and what statuses it carries. */ export interface DamageRuntimeHitInput { target: string; type: string; /** Damage before the zone multiplier, the table, and armor. */ base: number; /** Hit zone id; omitted uses the table's `defaultZone`. */ zone?: string; source?: string; statuses?: readonly (string | DamageStatusRequest)[]; } /** Every stage of one resolved hit, so a UI or a test can read why the final number is what it is. */ export interface DamageRuntimeResolution { target: string; type: string; base: number; zone: HitZoneResolution; /** Type × resistance multiplier before armor. */ multiplier: number; /** Armor after carried shred statuses, never below zero. */ armor: number; /** Fraction of the hit armor removes at `armor`. */ mitigation: number; /** `base × zone.multiplier × multiplier × (1 − mitigation)`. */ damage: number; statuses: StatusApplicationOutcome[]; } /** A quoted time to kill for one fixed hit against one health pool. */ export interface TimeToKill { damagePerShot: number; /** Hits needed, or `Infinity` when the hit deals nothing. */ shots: number; /** Seconds from the first hit to the last: `(shots - 1) × intervalSeconds`. */ seconds: number; intervalSeconds: number; } /** Serialized model state: registered targets, carried statuses, and the probe counters. */ export interface DamageRuntimeState { targets: Array<{ id: string; } & TargetProfile>; statuses: StatusRuntimeState; hits: number; crits: number; dealt: number; last: { type: string; base: number; damage: number; zone: string; crit: boolean; } | null; } /** Everything the model reads: the table, the zones, the status definitions, and the RNG. */ export interface DamageRuntimeConfig { table: DamageTableDef; zones: HitZoneTable; statuses?: readonly StatusDef[]; /** Stat id carried armor-shred statuses reduce. Default `"armor"`. */ armorStat?: string; /** Injected `[0,1)` stream for crit rolls. Omitting it warns once and is not replayable. */ rng?: () => number; /** Bus every applied damage is announced on as `combat.damage`. Omitted, events reach only the probe. */ events?: GameEvents; /** * A target's live health, read once per application before the caller applies it. The event's * `remaining` is that value minus the damage dealt, and `lethal` is `remaining <= 0`; omit the * reader and every event reports `remaining: null` and `lethal: false`. */ healthOf?: (targetId: string) => number | null; /** Register a one-line devtools probe under this name. */ devtoolsName?: string; } /** In-play dials, all retunable without discarding registered targets or carried statuses. */ export interface DamageRuntimeRetune { table?: DamageTableDef; zones?: HitZoneTable; statuses?: readonly StatusDef[]; armorStat?: string; } /** * A damage model: registered targets, the table/zones/statuses they are hit with, and carried * status state. `hit` resolves a blow, `tick` runs status effects, `timeToKill` quotes a fight, and * `table` reads the whole chart. State is plain data, so a save, a replay, or a worker can move it. */ export interface DamageRuntime { /** Register or replace a target's resistance class and armor. */ registerTarget(id: string, profile: TargetProfile): void; removeTarget(id: string): void; target(id: string): TargetProfile | null; /** Resolve one hit and apply any statuses it carries. */ hit(input: DamageRuntimeHitInput): DamageRuntimeResolution; /** Apply one status directly (an ability, a hazard) without a damage hit. */ applyStatus(target: string, statusId: string, options?: DamageStatusRequest & { source?: string; }): StatusApplicationOutcome; /** Advance carried statuses; damage ticks are resolved through this model's table and armor. */ tick(dtSeconds: number): StatusTickEvent[]; /** Quote a fixed hit against a health pool; pass `crit` explicitly so the number never depends on a roll. */ timeToKill(input: { target: string; type: string; base: number; hp: number; intervalSeconds: number; zone?: string; crit?: boolean; }): TimeToKill; /** Read the whole table with armor mitigation, optionally at given armor values and base damage. */ table(read?: Parameters[1]): DamageTableRow[]; retune(patch: DamageRuntimeRetune): void; snapshot(): DamageRuntimeState; restore(next: DamageRuntimeState): void; /** Carried statuses, for a HUD or a test. */ statusesOf(targetId: string): readonly StatusInstance[]; /** The last `limit` damage applications this model announced, oldest first. Default all it keeps. */ recentDamage(limit?: number): readonly DamageAppliedEvent[]; stats(): { targets: number; hits: number; crits: number; statusTicks: number; dealt: number; }; /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */ dispose(): void; } /** Raised when a hit names a target that was never registered — a wiring error, not a 0-damage hit. * @capability damage-target-unknown reject a hit on a target with no registered resistance or armor profile */ export declare class UnknownTargetProfileError extends Error { readonly target: string; constructor(target: string); } /** * Build a damage model from data. The number a hit lands is * `base × zone multiplier × type multiplier × (1 − armor mitigation)`; statuses tick through the * same table and armor, so a burn on an armored target is mitigated like a bullet. Every application * — a hit or a status tick — is announced as `combat.damage` carrying the target, source, type, * amount, absorbed, remaining health and a lethal flag, so a HUD names the blow instead of guessing. * * @capability damage-runtime resolve hits through a damage table, hit zones, and armor, and tick data-defined statuses with declared stacking */ export declare function createDamageRuntime(config: DamageRuntimeConfig): DamageRuntime;