Files
healer-man/src/ui/AuraStrip.tsx
T
2026-09-09 11:21:33 -04:00

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>
);
}