Files
healer-man/src/ui/GameplayPanels.tsx
T
2026-09-23 16:14:19 -04:00

137 lines
6.0 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, useMemo } from "react";
import { classById } from "../app/characterCatalog";
import { useShellStore } from "../app/shellStore";
import { useCombatStore } from "../game/combatStore";
import {
abilityAtLevel,
abilityRankForLevel,
abilityTalentNodeId,
learnedAbilityRank,
} 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";
import { getLoadedTalentGameplayPanel, loadTalentGameplayPanel } from "./talentGameplayResources";
import {
getLoadedBossGuidePanel,
getLoadedPartyManagementPanel,
loadBossGuidePanel,
loadPartyManagementPanel,
} from "./gameplayResources";
const PartyManagementPanel = lazy(() => loadPartyManagementPanel().then((module) => ({ default: module.PartyManagementPanel })));
const BossGuidePanel = lazy(() => loadBossGuidePanel().then((module) => ({ default: module.BossGuidePanel })));
const TalentGameplayPanel = lazy(loadTalentGameplayPanel);
const BossCardCollectionScreen = lazy(() => import("./BossCardCollection"));
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);
const PreloadedTalentGameplayPanel = getLoadedTalentGameplayPanel();
const PreloadedPartyManagementPanel = getLoadedPartyManagementPanel();
const PreloadedBossGuidePanel = getLoadedBossGuidePanel();
const spellbookAbilities = useMemo(() => [...abilities]
.sort((left, right) => left.unlockLevel - right.unlockLevel)
.map((catalogAbility) => {
const learnedTalentRank = learnedAbilityRank(catalogAbility, talentRanks);
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: !abilityTalentNodeId(catalogAbility) || Boolean(learnedTalentRank),
talentLocked: Boolean(abilityTalentNodeId(catalogAbility)),
passive: ability.passive,
rank: ability.source === "runewaker" ? undefined : activeRank?.rank,
rankCount: ability.source === "runewaker" ? undefined : catalogAbility.ranks?.length,
ranks: ability.source === "runewaker" ? undefined : 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 === "boss-cards" ? <Suspense fallback={null}><BossCardCollectionScreen /></Suspense> : null}
{overlay === "party"
? PreloadedPartyManagementPanel
? <PreloadedPartyManagementPanel />
: <Suspense fallback={null}><PartyManagementPanel /></Suspense>
: null}
{overlay === "boss-guide"
? PreloadedBossGuidePanel
? <PreloadedBossGuidePanel />
: <Suspense fallback={null}><BossGuidePanel /></Suspense>
: null}
<EquipmentPanel />
<AbilityBindingsPanel />
<SpellbookPanel abilities={spellbookAbilities} playerLevel={level} onActivate={castAbility} />
{overlay === "talents"
? classId.startsWith("rom-")
? <RomSkillsPanel />
: PreloadedTalentGameplayPanel
? <PreloadedTalentGameplayPanel />
: <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()}
/>
</>
);
}