Files
healer-man/src/ui/GameplayPanels.tsx
T
2026-08-18 12:06:04 -04:00

132 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { lazy, Suspense, useEffect, useMemo } from "react";
import { classById } from "../app/characterCatalog";
import { useShellStore } from "../app/shellStore";
import { useCombatStore } from "../game/combatStore";
import {
abilityAtLevel,
abilityRankForLevel,
} from "../game/abilityCatalog";
import { useGameStore } from "../game/store";
import { OptionsPanel } from "./OptionsPanel";
import { AbilityBindingsPanel } from "./AbilityBindingsPanel";
import { SpellbookPanel } from "./SpellbookPanel";
import { EquipmentPanel } from "./EquipmentPanel";
import { RomSkillsPanel } from "./RomSkillsPanel";
const PartyManagementPanel = lazy(() => import("./PartyManagementPanel").then((module) => ({ default: module.PartyManagementPanel })));
let talentGameplayPanelModule: Promise<typeof import("./TalentGameplayPanel")> | null = null;
function loadTalentGameplayPanel() {
if (!talentGameplayPanelModule) {
talentGameplayPanelModule = import("./TalentGameplayPanel").catch((error: unknown) => {
talentGameplayPanelModule = null;
throw error;
});
}
return talentGameplayPanelModule;
}
function preloadTalentGameplayPanel() {
void loadTalentGameplayPanel().catch(() => undefined);
}
const TalentGameplayPanel = lazy(loadTalentGameplayPanel);
function TalentsPanelLoading() {
const openOverlay = useGameStore((state) => state.openOverlay);
return (
<div className="modal-backdrop game-panel-backdrop" role="presentation">
<section className="game-panel talents-panel talents-panel--loading" role="dialog" aria-modal="true" aria-labelledby="talents-loading-title">
<header className="game-panel__header">
<div>
<p className="eyebrow">Specialization</p>
<h2 id="talents-loading-title">Talents</h2>
</div>
<button type="button" className="icon-button" onClick={() => openOverlay("pause")} aria-label="Return to pause menu">×</button>
</header>
<p className="game-panel__empty" role="status">Preparing talent trees…</p>
</section>
</div>
);
}
/** Maps the data-driven combat catalogs into the full-screen game panels. */
export function GameplayPanels() {
const classId = useCombatStore((state) => state.classId);
const level = useCombatStore((state) => state.level);
const abilities = useCombatStore((state) => state.abilities);
const talentRanks = useCombatStore((state) => state.talentRanks);
const settings = useCombatStore((state) => state.settings);
const presentationSettings = useShellStore((state) => state.combatPresentationSettings);
const overlay = useGameStore((state) => state.overlay);
const classDefinition = classById(classId);
useEffect(() => {
if (overlay === "pause" && !classId.startsWith("rom-")) preloadTalentGameplayPanel();
}, [classId, overlay]);
const spellbookAbilities = useMemo(() => [...abilities]
.sort((left, right) => left.unlockLevel - right.unlockLevel)
.map((catalogAbility) => {
const learnedTalentRank = catalogAbility.talentEntryId === undefined
? undefined
: talentRanks[`coa-entry-${catalogAbility.talentEntryId}`] ?? 0;
const ability = abilityAtLevel(catalogAbility, level, learnedTalentRank);
const activeRank = abilityRankForLevel(catalogAbility, level, learnedTalentRank);
return {
id: ability.id,
name: ability.name,
description: ability.description,
level: ability.unlockLevel,
color: classDefinition.color,
sigil: ability.sigil,
iconUrl: ability.icon,
school: classDefinition.name,
resourceCost: ability.cost.amount,
resourceLabel: useCombatStore.getState().resourceName,
cooldownSeconds: ability.cooldownMs / 1_000,
castSeconds: ability.castTimeMs / 1_000,
range: ability.range.max,
known: catalogAbility.talentEntryId === undefined || Boolean(learnedTalentRank),
talentLocked: catalogAbility.talentEntryId !== undefined,
passive: ability.passive,
rank: activeRank?.rank,
rankCount: catalogAbility.ranks?.length,
ranks: catalogAbility.ranks?.map((rank) => ({
rank: rank.rank,
level: rank.level,
spellId: rank.spellId,
description: rank.description,
})),
};
}), [abilities, classDefinition.color, classDefinition.name, level, talentRanks]);
const castAbility = (abilityId: string) => {
const game = useGameStore.getState();
game.closeOverlay();
useCombatStore.getState().castAbility(abilityId, game.playerPosition, Date.now());
};
return (
<>
{overlay === "party" ? <Suspense fallback={null}><PartyManagementPanel /></Suspense> : null}
<EquipmentPanel />
<AbilityBindingsPanel />
<SpellbookPanel abilities={spellbookAbilities} playerLevel={level} onActivate={castAbility} />
{overlay === "talents"
? classId.startsWith("rom-")
? <RomSkillsPanel />
: <Suspense fallback={<TalentsPanelLoading />}><TalentGameplayPanel /></Suspense>
: null}
<OptionsPanel
settings={settings}
onChange={(patch) => useCombatStore.getState().updateSettings(patch)}
onReset={() => useCombatStore.getState().resetSettings()}
presentationSettings={presentationSettings}
onPresentationChange={(patch) => useShellStore.getState().updateCombatPresentationSettings(patch)}
onPresentationReset={() => useShellStore.getState().resetCombatPresentationSettings()}
/>
</>
);
}