Files
healer-man/src/ui/ArenaPanels.tsx
T
2026-09-17 09:13:15 -04:00

58 lines
4.6 KiB
TypeScript

import { lazy, Suspense, useMemo } from "react";
import type { CharacterProfile } from "../app/types";
import { classById } from "../app/characterCatalog";
import { useShellStore } from "../app/shellStore";
import { useGameStore } from "../game/store";
import { useCombatStore } from "../game/combatStore";
import { useArenaStore } from "../game/arenaStore";
import { abilitiesForCharacter, abilityAtLevel, learnedAbilityRank } from "../game/abilityCatalog";
import type { ArenaActorSnapshot, ArenaAuthoritativeSnapshot, ArenaStageManifest } from "../game/arenaTypes";
import { SpellbookPanel } from "./SpellbookPanel";
import { EquipmentPanel } from "./EquipmentPanel";
import { AbilityBindingsPanel } from "./AbilityBindingsPanel";
import { KeybindsPanel } from "./KeybindsPanel";
import { OptionsPanel } from "./OptionsPanel";
import { UiLayoutEditor } from "./UiLayoutEditor";
const Talents = lazy(() => import("./TalentGameplayPanel"));
export function ArenaPanels({ character, actor, snapshot, stage, cast }: {
character: CharacterProfile; actor: ArenaActorSnapshot | null; snapshot: ArenaAuthoritativeSnapshot;
stage: ArenaStageManifest; cast: (id: string) => void;
}) {
const overlay = useGameStore((state) => state.overlay);
const settings = useCombatStore((state) => state.settings);
const presentation = useShellStore((state) => state.combatPresentationSettings);
const close = () => useGameStore.getState().closeOverlay();
const color = classById(character.classId).color;
const abilities = useMemo(() => abilitiesForCharacter(character.classId, character.secondaryClassId).map((definition) => {
const rank = learnedAbilityRank(definition, actor?.combat?.talentRanks ?? character.talentRanks);
const ability = abilityAtLevel(definition, actor?.level ?? character.level, rank);
return { id: ability.id, name: ability.name, description: ability.description, level: ability.unlockLevel,
color, sigil: ability.sigil, iconUrl: ability.icon, known: ability.passive || Boolean(actor?.abilityIds?.includes(ability.id)),
passive: ability.passive, resourceCost: ability.cost.amount, resourceLabel: ability.cost.resource,
cooldownSeconds: ability.cooldownMs / 1000, castSeconds: ability.castTimeMs / 1000, range: ability.range.max };
}), [actor?.level, actor?.abilityIds, character, color]);
return <>
<SpellbookPanel abilities={abilities} playerLevel={actor?.level ?? character.level} open={overlay === "spellbook"} onClose={close}
onActivate={(id) => { close(); cast(id); }} />
<EquipmentPanel readOnly />
{overlay === "talents" && <Suspense fallback={<div className="arena-result-panel">Loading talents…</div>}><Talents readOnly /></Suspense>}
<AbilityBindingsPanel /><KeybindsPanel /><UiLayoutEditor />
<OptionsPanel settings={settings} open={overlay === "options"} onClose={close}
onChange={(patch) => useCombatStore.getState().updateSettings(patch)} onReset={() => useCombatStore.getState().resetSettings()}
presentationSettings={presentation} onPresentationChange={(patch) => useShellStore.getState().updateCombatPresentationSettings(patch)}
onPresentationReset={() => useShellStore.getState().resetCombatPresentationSettings()} />
{(overlay === "map" || overlay === "party" || overlay === "boss-guide") && <section className="arena-result-panel arena-reference-panel" role="dialog" aria-label="Arena overview">
<button type="button" onClick={close}>Close</button><h2>{stage.name}</h2>
<svg viewBox={`${stage.bounds.min[0]} ${stage.bounds.min[2]} ${stage.bounds.max[0] - stage.bounds.min[0]} ${stage.bounds.max[2] - stage.bounds.min[2]}`} aria-label="Arena map">
{stage.lineOfSight.map((object) => <rect key={object.id} x={object.position[0] - object.size[0] / 2} y={object.position[2] - object.size[2] / 2} width={object.size[0]} height={object.size[2]} fill="#717c76" />)}
{snapshot.actors.filter((entry) => entry.alive).map((entry) => <circle key={entry.id} cx={entry.position[0]} cy={entry.position[2]} r={entry.id === actor?.id ? 2 : 1.4} fill={entry.team === actor?.team ? "#71c9ff" : "#ff736e"}><title>{entry.name}</title></circle>)}
</svg>
<p>Win by defeating the opposing team. Healing dampening: {Math.round(snapshot.dampening * 100)}%.</p>
<p>Talents and equipment use the loadout you entered with. Change them between matches.</p>
<div className="arena-pet-orders">{(["attack", "defend", "stop", "recall"] as const).map((command) => <button key={command} type="button" onClick={() => useArenaStore.getState().intent({ type: "command", command })}>{command}</button>)}</div>
</section>}
</>;
}