forked from phenom/healer-man
123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
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 (
|
|
<span className={`timed-effect-strip ${className}`.trim()} aria-label="Periodic effects">
|
|
{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 (
|
|
<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, ${timer} remaining`}
|
|
aria-describedby={onInspect ? tooltipId : undefined}
|
|
title={onInspect ? undefined : `${label} · ${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>}
|
|
<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 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 (
|
|
<LiveTimedEffectStrip
|
|
effects={effects}
|
|
className={className}
|
|
onInspect={onInspect}
|
|
tooltipId={tooltipId}
|
|
/>
|
|
);
|
|
}
|