From ff3a2683585c959625952ddfbec839b16ab5663c Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Mon, 18 May 2026 17:51:06 +0200 Subject: [PATCH] fix: resolve all Priority 5 CRASH/BLOCKER issues (#51-#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #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 --- docs/circular-deps.txt | 2 +- docs/dependency-graph.json | 2 +- src/components/game/index.ts | 1 - src/components/game/tabs/DisciplinesTab.tsx | 358 +++++++++----------- src/lib/game/stores/discipline-slice.ts | 3 +- src/lib/game/utils/discipline-math.ts | 16 +- 6 files changed, 169 insertions(+), 213 deletions(-) diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 53c570f..815ccf1 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # 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. 1. Processed 123 files (1.2s) (29 warnings) diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index e3f970e..6f31360 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_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.", "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." }, diff --git a/src/components/game/index.ts b/src/components/game/index.ts index 5f5bed2..d9a58a6 100755 --- a/src/components/game/index.ts +++ b/src/components/game/index.ts @@ -2,7 +2,6 @@ // Re-exports all game tab components for cleaner imports // Tab components -export { CraftingTab } from './crafting'; export { SpellsTab } from './SpellsTab'; export { StatsTab } from './StatsTab'; diff --git a/src/components/game/tabs/DisciplinesTab.tsx b/src/components/game/tabs/DisciplinesTab.tsx index 3b634c2..006e6d4 100644 --- a/src/components/game/tabs/DisciplinesTab.tsx +++ b/src/components/game/tabs/DisciplinesTab.tsx @@ -1,181 +1,149 @@ import React, { useEffect, useState } from 'react'; 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 { enchanterDisciplines } from '@/lib/game/data/disciplines/enchanter'; import { fabricatorDisciplines } from '@/lib/game/data/disciplines/fabricator'; import { invokerDisciplines } from '@/lib/game/data/disciplines/invoker'; import { calculateStatBonus, calculateManaDrain } from '@/lib/game/utils/discipline-math'; -import { useRef } from 'react'; 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 = ({ + 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((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 ( +
+

{name}

+

{description}

+ +
+ {Math.round(progressPercent)}% +
+
0 ? 'bg-green-500' : 'bg-red-500'}`} + style={{ width: `${Math.round(progressPercent)}%` }} + /> +
+
+ +
+ Drain: {estimatedDrain.toFixed(1)} ✦{' '} + XP: {displayXp} +
+ +
+ Stat Bonus: {activeStatBonus.toFixed(2)} on {statBonus} +
+ +
+ Perks: +
    + {unlockedPerks && unlockedPerks.length > 0 ? ( + unlockedPerks.map((p) => ( +
  • {p.replace(/-([0-9]+)$/, ' $1')}
  • + )) + ) : ( +
  • —locked—
  • + )} +
+
+ +
+ +
+
+ ); +}; + export const DisciplinesTab: React.FC = () => { - const store = useDisciplineStore(); - const { disciplines, activeIds, concurrentLimit } = store; + const { activeIds, concurrentLimit } = useDisciplineStore(); const [mounted, setMounted] = useState(false); + const [activeAttunement, setActiveAttunement] = useState('base'); useEffect(() => { 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((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 ( -
-

{name}

-

{description}

- -
- {Math.round(progressPercent)}% -
-
0 ? 'bg-green-500' : 'bg-red-500' - }`} - style={{ width: `${progressPercent}%` }} - /> -
-
- -
- Drain: {estimatedDrain.toFixed(1)} ✦{' '} - XP: {displayXp} -
- - {/* Bonus display */} -
- Stat Bonus: {activeStatBonus.toFixed(2)} on {statBonus} -
- - {/* Perks */} -
- Perks: -
    - {unlockedPerks && unlockedPerks.length > 0 ? ( - unlockedPerks.map((p) => ( -
  • {p.replace(/-([0-9]+)$/, ' $1')}
  • - )) - ) : ( -
  • —locked—
  • - )} -
-
- - {/* Action button */} -
- -
-
- ); - }; - if (!mounted) { return (
@@ -184,25 +152,21 @@ export const DisciplinesTab: React.FC = () => { ); } + const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement); + return (
{/* Tab bar */}
- {attunementTabs.map((tab) => { - const isActiveTab = store - .getState() - .activeAttunement === tab.label.toLowerCase(); + {ATTUNEMENT_TABS.map((tab) => { + const isActiveTab = activeAttunement === tab.key; return (
- {/* Discipline cards */} + {/* Discipline cards — only render active tab */}
- {attunementTabs - .map((tab) => - tab.items.map((disc) => { - const state = useDisciplineStore().getState(); - const discState = state.disciplines[disc.id] ?? { xp: 0, paused: true }; - const isActive = activeIds.includes(disc.id); - const isVisible = attunementTabs.find((t) => t.label === tab.label)?.items === tab.items; - return isVisible && ( - 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() - } + {activeTab?.items.map((disc) => ( + p.threshold)} + perkValues={disc.perks?.map((p) => p.value)} + perkTypes={disc.perks?.map((p) => p.type)} + statBonus={disc.statBonus.stat} + baseValue={disc.statBonus.baseValue} + drainBase={disc.drainBase} + difficultyFactor={disc.difficultyFactor} + scalingFactor={disc.scalingFactor} + /> + ))}
{/* Summary info */} @@ -250,4 +202,4 @@ export const DisciplinesTab: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/lib/game/stores/discipline-slice.ts b/src/lib/game/stores/discipline-slice.ts index 1d84f10..6cd1477 100644 --- a/src/lib/game/stores/discipline-slice.ts +++ b/src/lib/game/stores/discipline-slice.ts @@ -58,7 +58,8 @@ export const useDisciplineStore = create()( return d && !d.paused; }).length; 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 }; return { diff --git a/src/lib/game/utils/discipline-math.ts b/src/lib/game/utils/discipline-math.ts index 5c7f961..ad1e2c0 100644 --- a/src/lib/game/utils/discipline-math.ts +++ b/src/lib/game/utils/discipline-math.ts @@ -1,7 +1,7 @@ // ─── Discipline Math Utilities ──────────────────────────────────────────────── // 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 @@ -64,21 +64,25 @@ export function canActivateDiscipline( */ export function canProceedDiscipline( discipline: DisciplineDefinition, - disciplineState: DisciplineState, - gameState: { elements?: Record; rawMana?: number } + disciplineState: DisciplineState | undefined, + gameState?: { elements?: Record; rawMana?: number } ): boolean { + if (!disciplineState) return true; if (disciplineState.paused) return false; - + + // If no game state provided, allow activation (optimistic) + if (!gameState) return true; + const drain = calculateManaDrain( discipline.drainBase, disciplineState.xp, discipline.difficultyFactor ); - + if (discipline.manaType === 'raw') { return (gameState.rawMana || 0) >= drain; } - + const element = gameState.elements?.[discipline.manaType]; return element && element.current >= drain; }