forked from phenom/healer-man
68 lines
2.9 KiB
TypeScript
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 })) };
|
|
}
|