forked from phenom/healer-man
58 lines
4.6 KiB
TypeScript
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>}
|
|
</>;
|
|
}
|