import { useEffect, useState } from "react"; import { abilityById } from "../game/abilityCatalog"; import { PLAYER_AGGRO_ID } from "../game/aggro"; import { useCombatStore } from "../game/combatStore"; import { formatTimedEffectTimer, timedEffectRemainingMs } from "../game/timedEffectDisplay"; import { AuraStrip } from "./AuraStrip"; import { auraDisplaysInPlayerStatusPanel, auraDescription, auraDisplayName, groupAurasForDisplay, } from "./auraDisplay"; import { TimedEffectStrip } from "./TimedEffectStrip"; import { timedEffectDescription, type StatusEffectInspection, } from "./statusEffectInspection"; const PLAYER_EFFECT_TARGETS: readonly (string | null)[] = [null, PLAYER_AGGRO_ID]; const TOOLTIP_ID = "player-status-effect-tooltip"; function statusLabel(disposition: StatusEffectInspection["disposition"]): string { return disposition === "buff" ? "Buff" : "Debuff"; } export function PlayerStatusPanel({ now }: { readonly now: number }) { const auras = useCombatStore((state) => state.auras); const effects = useCombatStore((state) => state.effects); const shield = useCombatStore((state) => state.shield); const shieldExpiresAt = useCombatStore((state) => state.shieldExpiresAt); const [inspected, setInspected] = useState(null); const activeAuras = groupAurasForDisplay(auras.filter((aura) => ( auraDisplaysInPlayerStatusPanel(aura, now) ))); const activeEffects = effects.filter((effect) => ( PLAYER_EFFECT_TARGETS.includes(effect.targetId) && timedEffectRemainingMs(effect, now) > 0 )); const shieldActive = shield > 0 && (shieldExpiresAt <= 0 || shieldExpiresAt > now); const buffCount = activeAuras.filter((aura) => aura.definition.disposition === "buff").length + activeEffects.filter((effect) => effect.kind === "hot").length + (shieldActive ? 1 : 0); const debuffCount = activeAuras.filter((aura) => aura.definition.disposition === "debuff").length + activeEffects.filter((effect) => effect.kind === "dot").length; const activeInspectionIds = new Set([ ...activeAuras.map((aura) => aura.instanceId), ...activeEffects.map((effect) => effect.id), ...(shieldActive ? ["player-absorb-shield"] : []), ]); const inspectedIsActive = inspected ? activeInspectionIds.has(inspected.id) : true; const shieldInspection: StatusEffectInspection = { id: "player-absorb-shield", name: "Absorb Shield", disposition: "buff", description: `Absorbs up to ${Math.ceil(shield)} incoming damage before health is lost.`, remainingLabel: shieldExpiresAt > now ? `${formatTimedEffectTimer(shieldExpiresAt - now)} remaining` : "Until depleted", stacks: 1, }; const liveInspection = inspected && inspectedIsActive ? (() => { if (inspected.id === shieldInspection.id) return shieldInspection; const aura = activeAuras.find((candidate) => candidate.instanceId === inspected.id); if (aura) { const remaining = aura.expiresAt === null ? "Permanent" : `${formatTimedEffectTimer(Math.max(0, aura.expiresAt - now))} remaining`; return { ...inspected, name: auraDisplayName(aura.definition), disposition: aura.definition.disposition, description: auraDescription(aura), remainingLabel: remaining, stacks: aura.stacks, ...(aura.definition.dispelCategory ? { dispelCategory: aura.definition.dispelCategory } : {}), } satisfies StatusEffectInspection; } const effect = activeEffects.find((candidate) => candidate.id === inspected.id); if (effect) { const ability = abilityById(effect.sourceAbilityId); return { ...inspected, name: ability?.name ?? effect.sourceAbilityId, disposition: effect.kind === "hot" ? "buff" : "debuff", description: timedEffectDescription(effect.kind, ability?.description), remainingLabel: `${formatTimedEffectTimer(timedEffectRemainingMs(effect, now))} remaining`, } satisfies StatusEffectInspection; } return inspected; })() : null; useEffect(() => { if (inspected && !inspectedIsActive) setInspected(null); }, [inspected, inspectedIsActive]); return (
Buffs{buffCount}
{shieldActive ? ( setInspected(shieldInspection)} onPointerLeave={(event) => { if (event.currentTarget !== document.activeElement) setInspected(null); }} onFocus={() => setInspected(shieldInspection)} onBlur={() => setInspected(null)} > ) : null} {!buffCount ? None active : null}
Debuffs{debuffCount}
{!debuffCount ? None active : null}
{liveInspection ? ( ) : null}
); }