import { useEffect, useState } from "react"; import type { ActiveAura, AuraDefinition, AuraDisposition } from "../game/combatAuras"; import { useCombatStore } from "../game/combatStore"; import { formatTimedEffectTimer } from "../game/timedEffectDisplay"; import type { StatusEffectInspectHandler, StatusEffectInspection } from "./statusEffectInspection"; export const LONG_TERM_AURA_DURATION_MS = 60_000; interface AuraStripProps { readonly targetId: string; readonly targetIds?: readonly string[]; readonly dispositions?: readonly AuraDisposition[]; readonly className?: string; readonly hideLongTerm?: boolean; readonly showOnlyLongTerm?: boolean; readonly targetLabels?: Readonly>; readonly onInspect?: StatusEffectInspectHandler; readonly tooltipId?: string; } export function auraDefinitionDisplaysInStrip(definition: AuraDefinition): boolean { return definition.hideFromAuraStrip !== true; } export function auraDefinitionIcon(definition: AuraDefinition): string | null { return definition.icon?.trim() || null; } export function auraDisplayName(definition: AuraDefinition): string { return definition.name.replace(/\s+\((?:DBC\s+\d+,\s*)?Aura\s+\d+\)$/i, "").trim(); } /** Permanent and one-minute-or-longer auras are persistent party-frame clutter. */ export function auraDefinitionIsLongTerm(definition: AuraDefinition): boolean { return definition.durationMs === null || definition.durationMs >= LONG_TERM_AURA_DURATION_MS; } export function auraDescription(aura: ActiveAura): string { const authored = aura.definition.description?.trim(); if (authored) return authored; const absorb = aura.absorbs.reduce((total, pool) => total + pool.remaining, 0); if (absorb > 0) return `Absorbs up to ${Math.ceil(absorb)} incoming damage.`; if (aura.definition.controlImmunities?.length) { return `Grants immunity to ${aura.definition.controlImmunities.join(", ")}.`; } if (aura.definition.modifiers?.length) { return aura.definition.disposition === "buff" ? "Improves combat attributes while active." : "Reduces combat effectiveness while active."; } if (aura.definition.procs?.length) return "Can trigger an additional effect during combat."; return aura.definition.disposition === "buff" ? "A beneficial effect is active." : "A harmful effect is active."; } export function AuraStrip({ targetId, targetIds, dispositions = ["buff", "debuff"], className = "", hideLongTerm = false, showOnlyLongTerm = false, targetLabels, onInspect, tooltipId, }: AuraStripProps) { const allAuras = useCombatStore((state) => state.auras); const [now, setNow] = useState(() => Date.now()); const requestedTargetIds = targetIds ?? [targetId]; const auras = allAuras.filter((aura) => ( requestedTargetIds.includes(aura.targetId) && dispositions.includes(aura.definition.disposition) && auraDefinitionDisplaysInStrip(aura.definition) && (!hideLongTerm || !auraDefinitionIsLongTerm(aura.definition)) && (!showOnlyLongTerm || auraDefinitionIsLongTerm(aura.definition)) && (aura.expiresAt === null || aura.expiresAt > now) )); useEffect(() => { if (!auras.some((aura) => aura.expiresAt !== null)) return undefined; const timer = window.setInterval(() => setNow(Date.now()), 100); return () => window.clearInterval(timer); }, [auras]); if (!auras.length) return null; return ( {auras.map((aura) => { const remaining = aura.expiresAt === null ? null : Math.max(0, aura.expiresAt - now); const timer = remaining === null ? "" : formatTimedEffectTimer(remaining); const displayName = auraDisplayName(aura.definition); const targetLabel = targetLabels?.[aura.targetId]; const initial = displayName.charAt(0).toUpperCase() || "*"; const icon = auraDefinitionIcon(aura.definition); const inspection: StatusEffectInspection = { id: aura.instanceId, name: displayName, disposition: aura.definition.disposition, description: auraDescription(aura), remainingLabel: timer ? `${timer} remaining` : "Permanent", stacks: aura.stacks, ...(targetLabel ? { targetLabel } : {}), ...(aura.definition.dispelCategory ? { dispelCategory: aura.definition.dispelCategory } : {}), }; return ( onInspect(inspection) : undefined} onPointerLeave={onInspect ? (event) => { if (event.currentTarget !== document.activeElement) onInspect(null); } : undefined} onFocus={onInspect ? () => onInspect(inspection) : undefined} onBlur={onInspect ? () => onInspect(null) : undefined} > {icon ? : } {aura.stacks > 1 ? {aura.stacks} : null} {timer ? : null} ); })} ); }