forked from phenom/healer-man
112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
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 (
|
|
<span className={`timed-effect-strip ${className}`.trim()} aria-label="Periodic effects">
|
|
{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 (
|
|
<span
|
|
key={effect.id}
|
|
className={`timed-effect timed-effect--${effect.kind} ${onInspect ? "timed-effect--inspectable" : ""}`.trim()}
|
|
tabIndex={onInspect ? 0 : undefined}
|
|
aria-label={`${label}, ${effect.kind === "hot" ? "healing" : "damage"} over time${stacks > 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
|
|
? <img src={ability.icon} alt="" draggable={false} />
|
|
: <span className="timed-effect__fallback" aria-hidden="true">{effect.kind === "hot" ? "+" : "!"}</span>}
|
|
{stacks > 1 ? <b className="timed-effect__stacks" aria-hidden="true">{stacks}</b> : null}
|
|
<b className="timed-effect__timer" aria-hidden="true">{timer}</b>
|
|
</span>
|
|
);
|
|
})}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 (
|
|
<LiveTimedEffectStrip
|
|
effects={effects}
|
|
className={className}
|
|
onInspect={onInspect}
|
|
tooltipId={tooltipId}
|
|
/>
|
|
);
|
|
}
|