111 lines
4.8 KiB
TypeScript
111 lines
4.8 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import type { AuraDisposition } from "../game/combatAuras";
|
|
import { useCombatStore } from "../game/combatStore";
|
|
import { formatTimedEffectTimer } from "../game/timedEffectDisplay";
|
|
import {
|
|
auraDefinitionDisplaysInStrip,
|
|
auraDefinitionIcon,
|
|
auraDefinitionIsDispellableDebuff,
|
|
auraDefinitionIsLongTerm,
|
|
auraDescription,
|
|
auraDisplayName,
|
|
groupAurasForDisplay,
|
|
} from "./auraDisplay";
|
|
import type { StatusEffectInspectHandler, StatusEffectInspection } from "./statusEffectInspection";
|
|
|
|
interface AuraStripProps {
|
|
readonly targetId: string;
|
|
readonly targetIds?: readonly string[];
|
|
readonly dispositions?: readonly AuraDisposition[];
|
|
readonly className?: string;
|
|
readonly hideLongTerm?: boolean;
|
|
readonly showOnlyLongTerm?: boolean;
|
|
readonly highlightDispellableDebuffs?: boolean;
|
|
readonly targetLabels?: Readonly<Record<string, string>>;
|
|
readonly onInspect?: StatusEffectInspectHandler;
|
|
readonly tooltipId?: string;
|
|
}
|
|
|
|
export function AuraStrip({
|
|
targetId,
|
|
targetIds,
|
|
dispositions = ["buff", "debuff"],
|
|
className = "",
|
|
hideLongTerm = false,
|
|
showOnlyLongTerm = false,
|
|
highlightDispellableDebuffs = false,
|
|
targetLabels,
|
|
onInspect,
|
|
tooltipId,
|
|
}: AuraStripProps) {
|
|
const allAuras = useCombatStore((state) => state.auras);
|
|
const [now, setNow] = useState(() => Date.now());
|
|
const requestedTargetIds = targetIds ?? [targetId];
|
|
const auras = groupAurasForDisplay(allAuras.filter((aura) => (
|
|
requestedTargetIds.includes(aura.targetId)
|
|
&& dispositions.includes(aura.definition.disposition)
|
|
&& auraDefinitionDisplaysInStrip(aura.definition)
|
|
&& (!hideLongTerm || !auraDefinitionIsLongTerm(aura.definition))
|
|
&& (!showOnlyLongTerm || auraDefinitionIsLongTerm(aura.definition))
|
|
&& (aura.expiresAt === null || aura.expiresAt > now)
|
|
)));
|
|
|
|
useEffect(() => {
|
|
if (!auras.some((aura) => aura.expiresAt !== null)) return undefined;
|
|
const timer = window.setInterval(() => setNow(Date.now()), 100);
|
|
return () => window.clearInterval(timer);
|
|
}, [auras]);
|
|
|
|
if (!auras.length) return null;
|
|
|
|
return (
|
|
<span className={`aura-strip ${className}`.trim()} aria-label="Active buffs and debuffs">
|
|
{auras.map((aura) => {
|
|
const remaining = aura.expiresAt === null ? null : Math.max(0, aura.expiresAt - now);
|
|
const timer = remaining === null ? "" : formatTimedEffectTimer(remaining);
|
|
const displayName = auraDisplayName(aura.definition);
|
|
const targetLabel = targetLabels?.[aura.targetId];
|
|
const initial = displayName.charAt(0).toUpperCase() || "*";
|
|
const icon = auraDefinitionIcon(aura.definition);
|
|
const dispellable = highlightDispellableDebuffs
|
|
&& auraDefinitionIsDispellableDebuff(aura.definition);
|
|
const dispelLabel = dispellable ? `, dispellable ${aura.definition.dispelCategory}` : "";
|
|
const inspection: StatusEffectInspection = {
|
|
id: aura.instanceId,
|
|
name: displayName,
|
|
disposition: aura.definition.disposition,
|
|
description: auraDescription(aura),
|
|
remainingLabel: timer ? `${timer} remaining` : "Permanent",
|
|
stacks: aura.stacks,
|
|
...(targetLabel ? { targetLabel } : {}),
|
|
...(aura.definition.dispelCategory
|
|
? { dispelCategory: aura.definition.dispelCategory }
|
|
: {}),
|
|
};
|
|
return (
|
|
<span
|
|
key={aura.instanceId}
|
|
className={`aura aura--${aura.definition.disposition} ${dispellable ? "aura--dispellable" : ""} ${onInspect ? "aura--inspectable" : ""}`.trim()}
|
|
tabIndex={onInspect ? 0 : undefined}
|
|
aria-label={`${displayName}${targetLabel ? ` on ${targetLabel}` : ""}, ${aura.definition.disposition}${dispelLabel}${timer ? `, ${timer} remaining` : ""}`}
|
|
aria-describedby={onInspect ? tooltipId : undefined}
|
|
title={onInspect ? undefined : `${displayName}${dispellable ? ` · Dispellable ${aura.definition.dispelCategory}` : ""}${timer ? ` · ${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}
|
|
>
|
|
{icon
|
|
? <img className="aura__icon" src={icon} alt="" draggable={false} />
|
|
: <span className="aura__initial" aria-hidden="true">{initial}</span>}
|
|
{aura.stacks > 1 ? <b className="aura__stacks">{aura.stacks}</b> : null}
|
|
{timer ? <b className="aura__timer" aria-hidden="true">{timer}</b> : null}
|
|
</span>
|
|
);
|
|
})}
|
|
</span>
|
|
);
|
|
}
|