Files
healer-man/src/game/dpsMeter.ts
T
2026-09-12 21:14:41 -04:00

68 lines
2.9 KiB
TypeScript

import type { StructuredCombatEvent } from "./combatEvents";
export interface DpsMeterActor {
readonly id: string;
readonly name: string;
readonly color: string;
}
export interface DpsMeterState {
readonly startedAt: number | null;
readonly endedAt: number | null;
readonly updatedAt: number | null;
readonly elapsedMs: number;
readonly damage: Readonly<Record<string, DpsMeterActor & { total: number }>>;
}
export interface DpsMeterRow extends DpsMeterActor {
readonly total: number;
readonly dps: number;
readonly percent: number;
}
export interface DpsMeterSnapshot {
readonly active: boolean;
readonly durationSeconds: number;
readonly totalDamage: number;
readonly totalDps: number;
readonly rows: readonly DpsMeterRow[];
}
export function emptyDpsMeter(): DpsMeterState {
return { startedAt: null, endedAt: null, updatedAt: null, elapsedMs: 0, damage: {} };
}
/** Keep the last pull visible until the next starts; menus do not add combat time. */
export function advanceDpsMeter(state: DpsMeterState, now: number, inCombat: boolean, suspended = false): DpsMeterState {
if (!Number.isFinite(now)) return state;
if (state.startedAt === null || state.endedAt !== null) {
return inCombat ? { ...emptyDpsMeter(), startedAt: now, updatedAt: now } : state;
}
const updatedAt = Math.max(state.updatedAt ?? now, now);
return { ...state, updatedAt,
elapsedMs: state.elapsedMs + (suspended ? 0 : updatedAt - (state.updatedAt ?? updatedAt)),
endedAt: inCombat ? null : updatedAt };
}
/** Consume the lossless stream, never the truncated combat-log history. */
export function recordDpsDamage(state: DpsMeterState, event: StructuredCombatEvent, actor: DpsMeterActor): DpsMeterState {
if (event.kind !== "damage" || !Number.isFinite(event.effectiveAmount) || event.effectiveAmount <= 0) return state;
const next = advanceDpsMeter(state, event.occurredAt, true);
return { ...next, damage: { ...next.damage,
[actor.id]: { ...actor, total: (next.damage[actor.id]?.total ?? 0) + event.effectiveAmount } } };
}
export function dpsMeterSnapshot(state: DpsMeterState, actors: readonly DpsMeterActor[] = []): DpsMeterSnapshot {
const durationSeconds = state.elapsedMs / 1_000;
const seconds = Math.max(1, durationSeconds);
const damage = state.startedAt === null ? {} : {
...Object.fromEntries(actors.map((actor) => [actor.id, { ...actor, total: 0 }])), ...state.damage,
};
const ordered = Object.values(damage).sort((a, b) => b.total - a.total || a.name.localeCompare(b.name));
const maximum = Math.max(1, ordered[0]?.total ?? 0);
const totalDamage = ordered.reduce((sum, actor) => sum + actor.total, 0);
return { active: state.startedAt !== null && state.endedAt === null, durationSeconds, totalDamage,
totalDps: totalDamage / seconds,
rows: ordered.map((actor) => ({ ...actor, dps: actor.total / seconds, percent: actor.total / maximum * 100 })) };
}