forked from phenom/healer-man
132 lines
5.4 KiB
TypeScript
132 lines
5.4 KiB
TypeScript
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()}
|
||
/>
|
||
</>
|
||
);
|
||
}
|