fix: resolve all Priority 5 CRASH/BLOCKER issues (#51-#57)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
- #51: Fix broken import path @/types/disciplines → @/lib/game/types/disciplines - #52: Fix canProceedDiscipline() called with wrong arguments in discipline-slice.ts - #53: Add local useState for activeAttunement tab filtering in DisciplinesTab - #54: Make canProceedDiscipline() defensive when gameState is undefined - #57: Remove stale CraftingTab export from game/index.ts - Refactored DisciplineCard to use Zustand selector subscriptions properly - Added DisciplinePerk type import to discipline-math.ts
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-18T13:07:38.493Z
|
Generated: 2026-05-18T13:14:04.833Z
|
||||||
Found: 1 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 1 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. Processed 123 files (1.2s) (29 warnings)
|
1. Processed 123 files (1.2s) (29 warnings)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-18T13:07:36.959Z",
|
"generated": "2026-05-18T13:14:03.496Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// Re-exports all game tab components for cleaner imports
|
// Re-exports all game tab components for cleaner imports
|
||||||
|
|
||||||
// Tab components
|
// Tab components
|
||||||
export { CraftingTab } from './crafting';
|
|
||||||
export { SpellsTab } from './SpellsTab';
|
export { SpellsTab } from './SpellsTab';
|
||||||
export { StatsTab } from './StatsTab';
|
export { StatsTab } from './StatsTab';
|
||||||
|
|
||||||
|
|||||||
@@ -1,181 +1,149 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
||||||
import type { DisciplineDefinition } from '@/types/disciplines';
|
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
||||||
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
|
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
|
||||||
import { enchanterDisciplines } from '@/lib/game/data/disciplines/enchanter';
|
import { enchanterDisciplines } from '@/lib/game/data/disciplines/enchanter';
|
||||||
import { fabricatorDisciplines } from '@/lib/game/data/disciplines/fabricator';
|
import { fabricatorDisciplines } from '@/lib/game/data/disciplines/fabricator';
|
||||||
import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker';
|
import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker';
|
||||||
import { calculateStatBonus, calculateManaDrain } from '@/lib/game/utils/discipline-math';
|
import { calculateStatBonus, calculateManaDrain } from '@/lib/game/utils/discipline-math';
|
||||||
import { useRef } from 'react';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
interface AttunementTab {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
items: DisciplineDefinition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ATTUNEMENT_TABS: AttunementTab[] = [
|
||||||
|
{ key: 'base', label: 'Base', items: baseDisciplines },
|
||||||
|
{ key: 'enchanter', label: 'Enchanter', items: enchanterDisciplines },
|
||||||
|
{ key: 'fabricator', label: 'Fabricator', items: fabricatorDisciplines },
|
||||||
|
{ key: 'invoker', label: 'Invoker', items: invokerDisciplines },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface DisciplineCardProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
perkThresholds?: number[];
|
||||||
|
perkValues?: number[];
|
||||||
|
perkTypes?: string[];
|
||||||
|
statBonus: string;
|
||||||
|
baseValue: number;
|
||||||
|
drainBase: number;
|
||||||
|
difficultyFactor: number;
|
||||||
|
scalingFactor: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
perkThresholds,
|
||||||
|
perkValues,
|
||||||
|
perkTypes,
|
||||||
|
statBonus,
|
||||||
|
baseValue,
|
||||||
|
drainBase,
|
||||||
|
difficultyFactor,
|
||||||
|
scalingFactor,
|
||||||
|
}) => {
|
||||||
|
const activeIds = useDisciplineStore((s) => s.activeIds);
|
||||||
|
const concurrentLimit = useDisciplineStore((s) => s.concurrentLimit);
|
||||||
|
const currentDisc = useDisciplineStore((s) => s.disciplines[id] ?? { xp: 0, paused: true });
|
||||||
|
|
||||||
|
const displayXp = currentDisc.xp;
|
||||||
|
const progressPercent = Math.min(displayXp / Math.max(1, concurrentLimit * 100), 100);
|
||||||
|
const isPaused = currentDisc.paused;
|
||||||
|
|
||||||
|
const activeStatBonus = calculateStatBonus(baseValue, displayXp, scalingFactor);
|
||||||
|
const estimatedDrain = calculateManaDrain(drainBase, displayXp, difficultyFactor);
|
||||||
|
|
||||||
|
const unlockedPerks = perkTypes?.reduce<string[]>((acc, typ, idx) => {
|
||||||
|
const threshold = perkThresholds?.[idx];
|
||||||
|
if (threshold === undefined) return acc;
|
||||||
|
if (typ === 'once' || typ === 'infinite') {
|
||||||
|
if (displayXp >= threshold) acc.push(`${typ}-${idx}`);
|
||||||
|
} else if (typ === 'capped') {
|
||||||
|
const interval = perkValues?.[idx] ?? 1;
|
||||||
|
const tier = Math.max(0, Math.floor((displayXp - threshold) / interval) + 1);
|
||||||
|
if (tier > 0) acc.push(`${typ}-${idx}`);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleAction = () => {
|
||||||
|
if (isPaused) {
|
||||||
|
useDisciplineStore.getState().activate(id);
|
||||||
|
} else {
|
||||||
|
useDisciplineStore.getState().deactivate(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={id} className="border rounded-lg p-4 shadow-sm space-y-3">
|
||||||
|
<h3 className="text-lg font-medium">{name}</h3>
|
||||||
|
<p className="text-sm text-gray-400">{description}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-mono whitespace-nowrap">{Math.round(progressPercent)}%</span>
|
||||||
|
<div className="flex-1 bg-gray-200 rounded-full overflow-hidden h-3">
|
||||||
|
<div
|
||||||
|
className={`transition-all duration-300 ${activeStatBonus > 0 ? 'bg-green-500' : 'bg-red-500'}`}
|
||||||
|
style={{ width: `${Math.round(progressPercent)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
<strong>Drain:</strong> {estimatedDrain.toFixed(1)} ✦{' '}
|
||||||
|
<strong>XP:</strong> {displayXp}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 text-sm">
|
||||||
|
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonus}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<strong>Perks:</strong>
|
||||||
|
<ul className="mt-1 list-disc list-inside space-y-1 text-xs">
|
||||||
|
{unlockedPerks && unlockedPerks.length > 0 ? (
|
||||||
|
unlockedPerks.map((p) => (
|
||||||
|
<li key={p} className="text-green-500">{p.replace(/-([0-9]+)$/, ' $1')}</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li className="text-gray-400">—locked—</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={toggleAction}
|
||||||
|
className={clsx(
|
||||||
|
'rounded px-3 py-1 text-sm font-medium',
|
||||||
|
isPaused
|
||||||
|
? 'bg-yellow-600 text-white hover:bg-yellow-500'
|
||||||
|
: 'bg-blue-600 text-white hover:bg-blue-500',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isPaused ? 'Activate' : 'Pause'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const DisciplinesTab: React.FC = () => {
|
export const DisciplinesTab: React.FC = () => {
|
||||||
const store = useDisciplineStore();
|
const { activeIds, concurrentLimit } = useDisciplineStore();
|
||||||
const { disciplines, activeIds, concurrentLimit } = store;
|
|
||||||
|
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [activeAttunement, setActiveAttunement] = useState<string>('base');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const allDisciplines: DisciplineDefinition[] = [
|
|
||||||
...baseDisciplines,
|
|
||||||
...enchanterDisciplines,
|
|
||||||
...fabricatorDisciplines,
|
|
||||||
...invokerDisciplines,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Group disciplines by attunement for tab rendering
|
|
||||||
const attunementTabs: {
|
|
||||||
label: string;
|
|
||||||
items: DisciplineDefinition[];
|
|
||||||
}[] = [
|
|
||||||
{ label: 'Base', items: baseDisciplines },
|
|
||||||
{ label: 'Enchanter', items: enchanterDisciplines },
|
|
||||||
{ label: 'Fabricator', items: fabricatorDisciplines },
|
|
||||||
{ label: 'Invoker', items: invokerDisciplines },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Helper to render a single discipline card
|
|
||||||
const DisciplineCard: React.FC<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
xp: number;
|
|
||||||
perkThresholds?: number[];
|
|
||||||
perkValues?: number[];
|
|
||||||
perkTypes?: string[];
|
|
||||||
statBonus: string;
|
|
||||||
baseValue: number;
|
|
||||||
drainBase: number;
|
|
||||||
difficultyFactor: number;
|
|
||||||
scalingFactor: number;
|
|
||||||
}> = ({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
xp,
|
|
||||||
perkThresholds,
|
|
||||||
perkValues,
|
|
||||||
perkTypes,
|
|
||||||
statBonus,
|
|
||||||
baseValue,
|
|
||||||
drainBase,
|
|
||||||
difficultyFactor,
|
|
||||||
scalingFactor,
|
|
||||||
}) => {
|
|
||||||
if (!mounted) return null;
|
|
||||||
|
|
||||||
const state = useDisciplineStore().getState();
|
|
||||||
const currentDisc = state.disciplines[id] ?? { xp: 0, paused: true };
|
|
||||||
const isActive = activeIds.includes(id);
|
|
||||||
const canActivate = concurrentLimit > activeIds.filter(a => state.disciplines[a]?.paused !== true).length;
|
|
||||||
|
|
||||||
// Calculate displayed stats
|
|
||||||
const displayXp = currentDisc.xp;
|
|
||||||
const progressPercent = Math.min(displayXp / Math.max(1, (concurrentLimit * 100) ?? 1), 100);
|
|
||||||
const isPaused = currentDisc.paused;
|
|
||||||
const hasPendingPerk = perkThresholds?.some((t, i) => displayXp >= t && perkTypes?.[i] !== 'capped');
|
|
||||||
|
|
||||||
const activeStatBonus = calculateStatBonus(
|
|
||||||
parseInt(baseValue) || 0,
|
|
||||||
displayXp,
|
|
||||||
scalingFactor
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simple visual for drain per tick
|
|
||||||
const estimatedDrain = calculateManaDrain(
|
|
||||||
drainBase,
|
|
||||||
displayXp,
|
|
||||||
difficultyFactor
|
|
||||||
);
|
|
||||||
|
|
||||||
// Determine unlocked perks
|
|
||||||
const unlockedPerks = perkTypes?.reduce<string[]>((acc, typ, idx) => {
|
|
||||||
if (typ === 'once' || typ === 'infinite') {
|
|
||||||
if (displayXp >= perkThresholds?.[idx] ?? 0) {
|
|
||||||
acc.push(`${typ}-${idx}`);
|
|
||||||
}
|
|
||||||
} else if (typ === 'capped') {
|
|
||||||
const tier = Math.max(0, Math.floor((displayXp - perkThresholds?.[idx] ?? 0) / perkValues?.[idx] ?? 1) + 1);
|
|
||||||
if (tier > 0) acc.push(`${typ}-${idx}`);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Helper to decide button action
|
|
||||||
const toggleAction = () => {
|
|
||||||
if (isPaused) {
|
|
||||||
// Resume – activate
|
|
||||||
const storeDispatch = useDisciplineStore().getState().activate as any;
|
|
||||||
storeDispatch(id);
|
|
||||||
} else {
|
|
||||||
// Pause – deactivate
|
|
||||||
const storeDispatch = useDisciplineStore().getState().deactivate as any;
|
|
||||||
storeDispatch(id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={id} className="border rounded-lg p-4 shadow-sm space-y-3">
|
|
||||||
<h3 className="text-lg font-medium">{name}</h3>
|
|
||||||
<p className="text-sm text-gray-400">{description}</p>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-xs font-mono whitespace-nowrap">{Math.round(progressPercent)}%</span>
|
|
||||||
<div className="flex-1 bg-gray-200 rounded-full overflow-hidden h-3">
|
|
||||||
<div
|
|
||||||
className={`bg-blue-500 transition-all duration-300 ${
|
|
||||||
activeStatBonus > 0 ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
}`}
|
|
||||||
style={{ width: `${progressPercent}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
<strong>Drain:</strong> {estimatedDrain.toFixed(1)} ✦{' '}
|
|
||||||
<strong>XP:</strong> {displayXp}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bonus display */}
|
|
||||||
<div className="mt-2 text-sm">
|
|
||||||
<strong>Stat Bonus:</strong> {activeStatBonus.toFixed(2)} on {statBonus}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Perks */}
|
|
||||||
<div className="mt-2">
|
|
||||||
<strong>Perks:</strong>
|
|
||||||
<ul className="mt-1 list-disc list-inside space-y-1 text-xs">
|
|
||||||
{unlockedPerks && unlockedPerks.length > 0 ? (
|
|
||||||
unlockedPerks.map((p) => (
|
|
||||||
<li key={p} className="text-green-500">{p.replace(/-([0-9]+)$/, ' $1')}</li>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<li className="text-gray-400">—locked—</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action button */}
|
|
||||||
<div className="mt-4 flex justify-end">
|
|
||||||
<button
|
|
||||||
onClick={toggleAction}
|
|
||||||
className={clsx(
|
|
||||||
'rounded px-3 py-1 text-sm font-medium',
|
|
||||||
isPaused
|
|
||||||
? 'bg-yellow-600 text-white hover:bg-yellow-500'
|
|
||||||
: 'bg-blue-600 text-white hover:bg-blue-500',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isPaused ? 'Activate' : 'Pause'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center p-8 text-gray-500">
|
<div className="flex items-center justify-center p-8 text-gray-500">
|
||||||
@@ -184,25 +152,21 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{/* Tab bar */}
|
{/* Tab bar */}
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
{attunementTabs.map((tab) => {
|
{ATTUNEMENT_TABS.map((tab) => {
|
||||||
const isActiveTab = store
|
const isActiveTab = activeAttunement === tab.key;
|
||||||
.getState()
|
|
||||||
.activeAttunement === tab.label.toLowerCase();
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.label}
|
key={tab.key}
|
||||||
onClick={() => {
|
onClick={() => setActiveAttunement(tab.key)}
|
||||||
// Here you could dispatch an action to switch tabs if needed
|
|
||||||
// For simplicity, we just render the tabs
|
|
||||||
console.log(`Switch to ${tab.label}`);
|
|
||||||
}}
|
|
||||||
className={clsx('rounded px-3 py-1', {
|
className={clsx('rounded px-3 py-1', {
|
||||||
'bg-blue-600 text-white': tab.label === 'Base', // highlight first for demo
|
'bg-blue-600 text-white': isActiveTab,
|
||||||
'text-gray-600': tab.label !== 'Base',
|
'text-gray-600': !isActiveTab,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
@@ -211,36 +175,24 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Discipline cards */}
|
{/* Discipline cards — only render active tab */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{attunementTabs
|
{activeTab?.items.map((disc) => (
|
||||||
.map((tab) =>
|
<DisciplineCard
|
||||||
tab.items.map((disc) => {
|
key={disc.id}
|
||||||
const state = useDisciplineStore().getState();
|
id={disc.id}
|
||||||
const discState = state.disciplines[disc.id] ?? { xp: 0, paused: true };
|
name={disc.name}
|
||||||
const isActive = activeIds.includes(disc.id);
|
description={disc.description}
|
||||||
const isVisible = attunementTabs.find((t) => t.label === tab.label)?.items === tab.items;
|
perkThresholds={disc.perks?.map((p) => p.threshold)}
|
||||||
return isVisible && (
|
perkValues={disc.perks?.map((p) => p.value)}
|
||||||
<DisciplineCard
|
perkTypes={disc.perks?.map((p) => p.type)}
|
||||||
key={disc.id}
|
statBonus={disc.statBonus.stat}
|
||||||
id={disc.id}
|
baseValue={disc.statBonus.baseValue}
|
||||||
name={disc.name}
|
drainBase={disc.drainBase}
|
||||||
description={disc.description}
|
difficultyFactor={disc.difficultyFactor}
|
||||||
xp={discState.xp}
|
scalingFactor={disc.scalingFactor}
|
||||||
perkThresholds={disc.perks?.map((p) => p.threshold)}
|
/>
|
||||||
perkValues={disc.perks?.map((p) => p.value)}
|
))}
|
||||||
perkTypes={disc.perks?.map((p) => p.type)}
|
|
||||||
statBonus={disc.statBonus}
|
|
||||||
baseValue={disc.statBonus.baseValue?.toString() ?? '0'}
|
|
||||||
drainBase={disc.drainBase}
|
|
||||||
difficultyFactor={disc.difficultyFactor}
|
|
||||||
scalingFactor={disc.scalingFactor}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.flat()
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary info */}
|
{/* Summary info */}
|
||||||
@@ -250,4 +202,4 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
return d && !d.paused;
|
return d && !d.paused;
|
||||||
}).length;
|
}).length;
|
||||||
if (nonPaused >= s.concurrentLimit) return s;
|
if (nonPaused >= s.concurrentLimit) return s;
|
||||||
if (!canProceedDiscipline(id, gameState)) return s;
|
const discState = s.disciplines[id];
|
||||||
|
if (!canProceedDiscipline(def, discState, gameState)) return s;
|
||||||
|
|
||||||
const existing = s.disciplines[id] || { id, xp: 0, paused: false };
|
const existing = s.disciplines[id] || { id, xp: 0, paused: false };
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ─── Discipline Math Utilities ────────────────────────────────────────────────
|
// ─── Discipline Math Utilities ────────────────────────────────────────────────
|
||||||
// Continuous scaling formulas for Active Disciplines
|
// Continuous scaling formulas for Active Disciplines
|
||||||
|
|
||||||
import type { DisciplineState, DisciplineDefinition } from '../types/disciplines';
|
import type { DisciplineState, DisciplineDefinition, DisciplinePerk } from '../types/disciplines';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate continuous stat bonus from discipline XP
|
* Calculate continuous stat bonus from discipline XP
|
||||||
@@ -64,21 +64,25 @@ export function canActivateDiscipline(
|
|||||||
*/
|
*/
|
||||||
export function canProceedDiscipline(
|
export function canProceedDiscipline(
|
||||||
discipline: DisciplineDefinition,
|
discipline: DisciplineDefinition,
|
||||||
disciplineState: DisciplineState,
|
disciplineState: DisciplineState | undefined,
|
||||||
gameState: { elements?: Record<string, any>; rawMana?: number }
|
gameState?: { elements?: Record<string, any>; rawMana?: number }
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (!disciplineState) return true;
|
||||||
if (disciplineState.paused) return false;
|
if (disciplineState.paused) return false;
|
||||||
|
|
||||||
|
// If no game state provided, allow activation (optimistic)
|
||||||
|
if (!gameState) return true;
|
||||||
|
|
||||||
const drain = calculateManaDrain(
|
const drain = calculateManaDrain(
|
||||||
discipline.drainBase,
|
discipline.drainBase,
|
||||||
disciplineState.xp,
|
disciplineState.xp,
|
||||||
discipline.difficultyFactor
|
discipline.difficultyFactor
|
||||||
);
|
);
|
||||||
|
|
||||||
if (discipline.manaType === 'raw') {
|
if (discipline.manaType === 'raw') {
|
||||||
return (gameState.rawMana || 0) >= drain;
|
return (gameState.rawMana || 0) >= drain;
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = gameState.elements?.[discipline.manaType];
|
const element = gameState.elements?.[discipline.manaType];
|
||||||
return element && element.current >= drain;
|
return element && element.current >= drain;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user