import type { StructuredCombatEvent } from "../game/combatEvents"; export const PARTY_HEAL_POPUP_LIFETIME_MS = 1_500; export const MAX_PARTY_HEAL_POPUPS = 20; export interface PartyHealPopup { readonly key: string; readonly targetActorId: string; readonly amount: number; readonly critical: boolean; readonly horizontalOffset: number; } function eventKey(event: StructuredCombatEvent): string { return `${event.occurredAt}:${event.id}`; } /** Convert only newly committed, effective in-combat heals into party-frame UI events. */ export function addedPartyHealPopups( previousEvents: readonly StructuredCombatEvent[], nextEvents: readonly StructuredCombatEvent[], inCombat: boolean, ): PartyHealPopup[] { if (!inCombat || previousEvents === nextEvents) return []; const previousKeys = new Set(previousEvents.map(eventKey)); return nextEvents .filter((event) => ( event.kind === "healing" && event.effectiveAmount > 0 && !previousKeys.has(eventKey(event)) )) .map((event) => ({ key: eventKey(event), targetActorId: event.targetActorId, amount: Math.max(1, Math.round(event.effectiveAmount)), critical: event.critical, horizontalOffset: ((event.id % 3) - 1) * 14, })); } export function appendPartyHealPopups( current: readonly PartyHealPopup[], added: readonly PartyHealPopup[], ): PartyHealPopup[] { if (added.length === 0) return [...current]; const addedKeys = new Set(added.map((popup) => popup.key)); return [...current.filter((popup) => !addedKeys.has(popup.key)), ...added] .slice(-MAX_PARTY_HEAL_POPUPS); }