Files
healer-man/src/game/combatEvents.ts
T

82 lines
2.9 KiB
TypeScript

import type { DamageSchool } from "./combatAuras";
export type StructuredCombatEventKind =
| "damage"
| "healing"
| "absorb"
| "resource"
| "aura"
| "cast"
| "interrupt"
| "death"
| "resurrection";
export interface StructuredCombatEvent {
readonly id: number;
readonly occurredAt: number;
readonly kind: StructuredCombatEventKind;
readonly sourceActorId: string;
readonly targetActorId: string;
readonly abilityId?: string;
/** Source ability that caused an interruption; abilityId remains the canceled cast. */
readonly causeAbilityId?: string;
readonly school?: DamageSchool;
readonly rawAmount: number;
readonly effectiveAmount: number;
readonly overheal: number;
readonly absorbed: number;
readonly critical: boolean;
readonly outcome?: string;
/** Independent mitigation is preserved even when a blocked spell also crits. */
readonly blocked?: boolean;
readonly blockedAmount?: number;
/** Source critical-block aura succeeded after the ordinary block roll. */
readonly criticalBlocked?: boolean;
/** Explicit weapon channel; unknown spell/native channels remain unlabeled. */
readonly attackHand?: "mainhand" | "offhand" | "ranged" | "extra-mainhand";
/** Original effect that queued an extra swing, independently of its unique attack ID. */
readonly extraAttackSourceSpellId?: number;
}
export type CombatEventInput = Omit<StructuredCombatEvent, "id">;
export const MAX_COMBAT_EVENT_HISTORY = 200;
export type CombatEventListener = (event: StructuredCombatEvent) => void;
const combatEventListeners = new Set<CombatEventListener>();
/** Lossless event stream for recorders; the in-store history remains a small HUD ring. */
export function subscribeCombatEvents(listener: CombatEventListener): () => void {
combatEventListeners.add(listener);
return () => combatEventListeners.delete(listener);
}
function publishCombatEvent(event: StructuredCombatEvent): void {
for (const listener of combatEventListeners) {
try {
listener(event);
} catch {
// A metrics consumer must never interrupt authoritative combat.
}
}
}
export function appendCombatEvent(
events: readonly StructuredCombatEvent[],
event: CombatEventInput,
id: number,
maximum = MAX_COMBAT_EVENT_HISTORY,
): readonly StructuredCombatEvent[] {
const next = Object.freeze({
...event,
id,
occurredAt: Math.max(0, Number.isFinite(event.occurredAt) ? event.occurredAt : 0),
rawAmount: Math.max(0, Number.isFinite(event.rawAmount) ? event.rawAmount : 0),
effectiveAmount: Math.max(0, Number.isFinite(event.effectiveAmount) ? event.effectiveAmount : 0),
overheal: Math.max(0, Number.isFinite(event.overheal) ? event.overheal : 0),
absorbed: Math.max(0, Number.isFinite(event.absorbed) ? event.absorbed : 0),
});
publishCombatEvent(next);
return Object.freeze([...events, next].slice(-Math.max(1, Math.trunc(maximum))));
}