import { useEffect, useMemo, useState, type CSSProperties } from "react"; import { useGameStore } from "../game/store"; import { useMenuController, type MenuAction } from "../input/useMenuController"; import { ControllerButton } from "./ControllerButton"; export interface SpellbookAbilityView { id: string; name: string; description: string; level: number; color: string; sigil: string; iconUrl?: string; school?: string; resourceCost?: number; resourceLabel?: string; cooldownSeconds?: number; castSeconds?: number; range?: number; known: boolean; talentLocked?: boolean; passive?: boolean; rank?: number; rankCount?: number; ranks?: readonly { rank: number; level: number; spellId: number; description: string; }[]; } export interface SpellbookPanelProps { abilities: readonly SpellbookAbilityView[]; playerLevel: number; onActivate: (abilityId: string) => void; open?: boolean; onClose?: () => void; } function isUnlocked(ability: SpellbookAbilityView, playerLevel: number): boolean { return ability.known && ability.level <= playerLevel; } export function spellbookAbilitiesForDisplay( abilities: readonly SpellbookAbilityView[], playerLevel: number, unlockedOnly: boolean, ): SpellbookAbilityView[] { return abilities .filter((ability) => !unlockedOnly || isUnlocked(ability, playerLevel)) .sort((left, right) => Number(Boolean(left.passive)) - Number(Boolean(right.passive)) || left.level - right.level || left.name.localeCompare(right.name)); } function AbilityIcon({ ability }: { ability: SpellbookAbilityView }) { const [failed, setFailed] = useState(false); useEffect(() => setFailed(false), [ability.iconUrl]); return ( ); } interface SpellbookDialogProps extends Required> { unlockedOnly: boolean; onUnlockedOnlyChange: (unlockedOnly: boolean) => void; } function SpellbookDialog({ abilities, playerLevel, onActivate, onClose, unlockedOnly, onUnlockedOnlyChange, }: SpellbookDialogProps) { const orderedAbilities = useMemo( () => spellbookAbilitiesForDisplay(abilities, playerLevel, false), [abilities, playerLevel], ); const visibleAbilities = useMemo( () => unlockedOnly ? orderedAbilities.filter((ability) => isUnlocked(ability, playerLevel)) : orderedAbilities, [orderedAbilities, playerLevel, unlockedOnly], ); const available = useMemo( () => orderedAbilities.filter((ability) => isUnlocked(ability, playerLevel)), [orderedAbilities, playerLevel], ); const future = abilities.filter((ability) => !ability.known || ability.level > playerLevel); const [inspectedId, setInspectedId] = useState(available[0]?.id ?? orderedAbilities[0]?.id ?? ""); const actions = useMemo(() => visibleAbilities.map((ability) => ({ id: `spell-${ability.id}`, run: () => { setInspectedId(ability.id); if (!ability.passive && ability.known && ability.level <= playerLevel) onActivate(ability.id); }, })), [visibleAbilities, onActivate, playerLevel]); const controller = useMenuController(actions, { columns: 2, onBack: onClose }); const controllerAbility = controller.selectedId.startsWith("spell-") ? controller.selectedId.slice("spell-".length) : ""; const inspected = visibleAbilities.find((ability) => ability.id === (controllerAbility || inspectedId)) ?? visibleAbilities[0] ?? null; const inspectedLocked = Boolean(inspected && (!inspected.known || inspected.level > playerLevel)); return (

Class abilities · Level {playerLevel}

Spellbook

{available.length} Unlocked