158 lines
6.6 KiB
TypeScript
158 lines
6.6 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
|
import type { DisciplineDefinition, DisciplineState } from '@/lib/game/types/disciplines';
|
|
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
|
|
import { elementalAttunementDisciplines } from '@/lib/game/data/disciplines/elemental';
|
|
import { elementalRegenDisciplines } from '@/lib/game/data/disciplines/elemental-regen';
|
|
import { elementalRegenAdvancedDisciplines } from '@/lib/game/data/disciplines/elemental-regen-advanced';
|
|
import { enchanterDisciplines } from '@/lib/game/data/disciplines/enchanter';
|
|
import { enchanterUtilityDisciplines } from '@/lib/game/data/disciplines/enchanter-utility';
|
|
import { enchanterSpellDisciplines } from '@/lib/game/data/disciplines/enchanter-spells';
|
|
import { enchanterSpecialDisciplines } from '@/lib/game/data/disciplines/enchanter-special';
|
|
import { fabricatorDisciplines } from '@/lib/game/data/disciplines/fabricator';
|
|
import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker';
|
|
import { checkDisciplinePrerequisites } from '@/lib/game/utils/discipline-math';
|
|
import { ALL_DISCIPLINES } from '@/lib/game/data/disciplines';
|
|
import { useManaStore } from '@/lib/game/stores/manaStore';
|
|
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
|
|
import clsx from 'clsx';
|
|
import { DebugName } from '@/components/game/debug/debug-context';
|
|
import { DisciplineCard } from './DisciplineCard';
|
|
import { ElementalSubtab } from './ElementalSubtab';
|
|
|
|
// ─── Attunement Tabs ─────────────────────────────────────────────────────────
|
|
|
|
interface AttunementTab {
|
|
key: string;
|
|
label: string;
|
|
items: DisciplineDefinition[];
|
|
}
|
|
|
|
const ATTUNEMENT_TABS: AttunementTab[] = [
|
|
{ key: 'base', label: 'Base', items: baseDisciplines },
|
|
{ key: 'elemental', label: 'Elemental', items: [...elementalAttunementDisciplines, ...elementalRegenDisciplines, ...elementalRegenAdvancedDisciplines] },
|
|
{ key: 'enchanter', label: 'Enchanter', items: [...enchanterDisciplines, ...enchanterUtilityDisciplines, ...enchanterSpellDisciplines, ...enchanterSpecialDisciplines] },
|
|
{ key: 'fabricator', label: 'Fabricator', items: fabricatorDisciplines },
|
|
{ key: 'invoker', label: 'Invoker', items: invokerDisciplines },
|
|
];
|
|
|
|
// ─── Discipline Card Wrapper (for flat grid) ─────────────────────────────────
|
|
|
|
interface CardWrapperProps {
|
|
disc: DisciplineDefinition;
|
|
disciplines: Record<string, DisciplineState>;
|
|
activeIds: string[];
|
|
concurrentLimit: number;
|
|
elements: ReturnType<typeof useManaStore.getState>['elements'];
|
|
signedPacts: ReturnType<typeof usePrestigeStore.getState>['signedPacts'];
|
|
onToggle: (id: string, paused: boolean) => void;
|
|
}
|
|
|
|
const CardWrapper: React.FC<CardWrapperProps> = ({
|
|
disc, disciplines, activeIds, concurrentLimit, elements, signedPacts, onToggle,
|
|
}) => {
|
|
const discState = disciplines[disc.id] ?? { xp: 0, paused: true };
|
|
const prereqCheck = checkDisciplinePrerequisites(disc, disciplines, ALL_DISCIPLINES, elements, signedPacts);
|
|
return (
|
|
<DisciplineCard
|
|
definition={disc}
|
|
xp={discState.xp}
|
|
paused={discState.paused}
|
|
autoPaused={discState.autoPaused}
|
|
activeIds={activeIds}
|
|
concurrentLimit={concurrentLimit}
|
|
isLocked={!prereqCheck.canProceed}
|
|
missingPrereqs={prereqCheck.missingPrereqs}
|
|
missingSourceMana={disc.sourceManaTypes
|
|
? disc.sourceManaTypes
|
|
.filter((src) => src !== 'raw' && (!elements[src] || !elements[src].unlocked))
|
|
.map((src) => `${src} mana`)
|
|
: []}
|
|
onToggle={onToggle}
|
|
/>
|
|
);
|
|
};
|
|
|
|
// ─── Disciplines Tab ─────────────────────────────────────────────────────────
|
|
|
|
export const DisciplinesTab: React.FC = () => {
|
|
const activeIds = useDisciplineStore((s) => s.activeIds);
|
|
const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit);
|
|
const disciplines = useDisciplineStore((s) => s.disciplines);
|
|
const activate = useDisciplineStore((s) => s.activate);
|
|
const deactivate = useDisciplineStore((s) => s.deactivate);
|
|
|
|
const [activeAttunement, setActiveAttunement] = useState<string>('base');
|
|
|
|
const elements = useManaStore((s) => s.elements);
|
|
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
|
|
|
// NOTE: activate() now reads rawMana/elements/signedPacts directly from the
|
|
// mana and prestige stores, so the UI only needs to pass the discipline id.
|
|
// This prevents the recurring bug where a missing field in a manually
|
|
// constructed gameState bag silently prevented reactivation.
|
|
const handleToggle = useCallback((id: string, paused: boolean) => {
|
|
if (paused) {
|
|
activate(id);
|
|
} else {
|
|
deactivate(id);
|
|
}
|
|
}, [activate, deactivate]);
|
|
|
|
const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement);
|
|
|
|
return (
|
|
<DebugName name="DisciplinesTab">
|
|
<div className="mt-6">
|
|
{/* Tab bar */}
|
|
<div className="flex gap-2 mb-4">
|
|
{ATTUNEMENT_TABS.map((tab) => (
|
|
<button
|
|
key={tab.key}
|
|
onClick={() => setActiveAttunement(tab.key)}
|
|
className={clsx('rounded px-3 py-1', {
|
|
'bg-blue-600 text-white': activeAttunement === tab.key,
|
|
'text-gray-600': activeAttunement !== tab.key,
|
|
})}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Grouped layout for elemental tab, grid for others */}
|
|
{activeAttunement === 'elemental' ? (
|
|
<ElementalSubtab
|
|
disciplines={disciplines}
|
|
activeIds={activeIds}
|
|
concurrentLimit={concurrentLimit}
|
|
elements={elements}
|
|
onToggle={handleToggle}
|
|
/>
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{activeTab?.items.map((disc) => (
|
|
<CardWrapper
|
|
key={disc.id}
|
|
disc={disc}
|
|
disciplines={disciplines}
|
|
activeIds={activeIds}
|
|
concurrentLimit={concurrentLimit}
|
|
elements={elements}
|
|
signedPacts={signedPacts}
|
|
onToggle={handleToggle}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Summary */}
|
|
<div className="mt-4 flex flex-col sm:flex-row gap-2 text-sm text-gray-500">
|
|
<div>Active Disciplines: {activeIds.length} / {concurrentLimit}</div>
|
|
<div>Concurrent Limit: {concurrentLimit}</div>
|
|
</div>
|
|
</div>
|
|
</DebugName>
|
|
);
|
|
};
|