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
?
: {effect.kind === "hot" ? "+" : "!"}}
{stacks > 1 ? {stacks} : null}
{timer}
);
})}
);
}
/**
* 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 (
);
}