import { useEffect, useState } from "react"; import { abilityById } from "../game/abilityCatalog"; import { useCombatStore, type TimedCombatEffect } from "../game/combatStore"; import { formatTimedEffectTimer, timedEffectRemainingMs, } from "../game/timedEffectDisplay"; import { timedEffectDescription, type StatusEffectInspectHandler, type StatusEffectInspection, } from "./statusEffectInspection"; export type TimedEffectKind = TimedCombatEffect["kind"]; interface TimedEffectStripProps { readonly targetId: string | null; readonly targetIds?: readonly (string | null)[]; readonly kinds?: readonly TimedEffectKind[]; readonly className?: string; readonly onInspect?: StatusEffectInspectHandler; readonly tooltipId?: string; } function LiveTimedEffectStrip({ effects, className = "", onInspect, tooltipId, }: { readonly effects: readonly TimedCombatEffect[]; readonly className?: string; readonly onInspect?: StatusEffectInspectHandler; readonly tooltipId?: string; }) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { setNow(Date.now()); const timer = window.setInterval(() => setNow(Date.now()), 100); return () => window.clearInterval(timer); }, []); const visibleEffects = effects .map((effect) => ({ effect, ability: abilityById(effect.sourceAbilityId), remainingMs: timedEffectRemainingMs(effect, now), })) .filter(({ remainingMs }) => remainingMs > 0) .sort((left, right) => left.remainingMs - right.remainingMs); if (!visibleEffects.length) return null; return ( {visibleEffects.map(({ effect, ability, remainingMs }) => { const label = ability?.name ?? effect.sourceAbilityId; const timer = formatTimedEffectTimer(remainingMs); const disposition = effect.kind === "hot" ? "buff" : "debuff"; const inspection: StatusEffectInspection = { id: effect.id, name: label, disposition, description: timedEffectDescription(effect.kind, ability?.description), remainingLabel: `${timer} remaining`, stacks: 1, }; 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} > {ability?.icon ? : } ); })} ); } /** * Subscribes only to effects on this unit. Unaffected world plates therefore * stay dormant while the combat simulation advances. */ export function TimedEffectStrip({ targetId, targetIds, kinds = ["dot", "hot"], className, onInspect, tooltipId, }: TimedEffectStripProps) { const allEffects = useCombatStore((state) => state.effects); const requestedTargetIds = targetIds ?? [targetId]; const effects = allEffects.filter((effect) => ( requestedTargetIds.includes(effect.targetId) && kinds.includes(effect.kind) )); if (!effects.length) return null; return ( ); }