import { useShallow } from "zustand/react/shallow"; import { usePresentationClock } from "./usePresentationClock"; import { abilityById } from "../game/abilityCatalog"; import { useCombatStore, type TimedCombatEffect } from "../game/combatStore"; import { formatTimedEffectTimer, groupTimedEffectsForDisplay, } 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 = usePresentationClock(); const visibleEffects = groupTimedEffectsForDisplay(effects, now); if (!visibleEffects.length) return null; return ( {visibleEffects.map(({ effect, stacks, remainingMs }) => { const ability = abilityById(effect.sourceAbilityId); 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, }; return ( 1 ? `, ${stacks} stacks` : ""}, ${timer} remaining`} aria-describedby={onInspect ? tooltipId : undefined} title={onInspect ? undefined : `${label}${stacks > 1 ? ` · ${stacks} stacks` : ""} · ${timer} remaining`} onPointerEnter={onInspect ? () => 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 ? : } {stacks > 1 ? : null} ); })} ); } /** * 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 requestedTargetIds = targetIds ?? [targetId]; const effects = useCombatStore(useShallow((state) => state.effects.filter((effect) => ( requestedTargetIds.includes(effect.targetId) && kinds.includes(effect.kind) )))); if (!effects.length) return null; return ( ); }