diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index e644173..00e6a33 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,4 +1,4 @@ # Circular Dependencies -Generated: 2026-05-28T11:45:27.810Z +Generated: 2026-05-28T13:28:24.658Z No circular dependencies found. ✅ diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 2d14e0f..e6c7ec2 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-28T11:45:26.075Z", + "generated": "2026-05-28T13:28:22.544Z", "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/tabs/SpireCombatPage/SpireCombatPage.tsx b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx index 24d6f7f..6efb128 100644 --- a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx +++ b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx @@ -18,7 +18,13 @@ import { DebugName } from '@/components/game/debug/debug-context'; // ─── Derived Stats Hook ────────────────────────────────────────────────────── function useSpireStats(prestigeUpgrades: Record, equippedInstances: Record, equipmentInstances: Record) { - const disciplineEffects = computeDisciplineEffects(); + let disciplineEffects: ReturnType | null = null; + try { + disciplineEffects = computeDisciplineEffects(); + } catch { + // If discipline state is corrupted, proceed without discipline effects + disciplineEffects = { bonuses: {}, multipliers: {}, specials: new Set(), meditationCapBonus: 0, conversions: {} }; + } const upgradeEffects = getUnifiedEffects({ skillUpgrades: {}, diff --git a/src/lib/game/stores/combat-actions.ts b/src/lib/game/stores/combat-actions.ts index 56db94c..39c9d7d 100644 --- a/src/lib/game/stores/combat-actions.ts +++ b/src/lib/game/stores/combat-actions.ts @@ -87,8 +87,10 @@ export function processCombatTick( let currentFloor = state.currentFloor; let floorMaxHP = state.floorMaxHP; - // Process complete casts for active spell - while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) { + // Process complete casts for active spell (safety counter prevents infinite loop) + let safetyCounter = 0; + const MAX_CASTS_PER_TICK = 100; + while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements) && safetyCounter < MAX_CASTS_PER_TICK) { // Deduct spell cost const afterCost = deductSpellCost(spellDef.cost, rawMana, elements); rawMana = afterCost.rawMana; @@ -108,9 +110,16 @@ export function processCombatTick( elements = result.elements; const finalDamage = result.modifiedDamage || damage; + // Guard against NaN damage — if damage is not finite, stop processing + if (!Number.isFinite(finalDamage)) { + logMessages.push('⚠️ Combat stopped: invalid damage value'); + break; + } + // Apply damage floorHP = Math.max(0, floorHP - finalDamage); castProgress -= 1; + safetyCounter++; // Check if floor is cleared if (floorHP <= 0) { @@ -142,8 +151,10 @@ export function processCombatTick( const eProgressPerTick = HOURS_PER_TICK * eSpellCastSpeed * totalAttackSpeed; let eCastProgress = (eSpell.castProgress || 0) + eProgressPerTick; - // Process complete casts for equipment spells - while (eCastProgress >= 1 && canAffordSpellCost(eSpellDef.cost, rawMana, elements)) { + // Process complete casts for equipment spells (safety counter prevents infinite loop) + let eSafetyCounter = 0; + const MAX_E_CASTS_PER_TICK = 100; + while (eCastProgress >= 1 && canAffordSpellCost(eSpellDef.cost, rawMana, elements) && eSafetyCounter < MAX_E_CASTS_PER_TICK) { // Deduct cost const eAfterCost = deductSpellCost(eSpellDef.cost, rawMana, elements); rawMana = eAfterCost.rawMana; @@ -162,8 +173,14 @@ export function processCombatTick( elements = eResult.elements; const eFinalDamage = eResult.modifiedDamage || eDamage; + // Guard against NaN damage + if (!Number.isFinite(eFinalDamage)) { + break; + } + floorHP = Math.max(0, floorHP - eFinalDamage); eCastProgress -= 1; + eSafetyCounter++; if (floorHP <= 0) break; // Floor cleared, stop processing } diff --git a/src/lib/game/stores/combatStore.ts b/src/lib/game/stores/combatStore.ts index 9f5f580..623da09 100644 --- a/src/lib/game/stores/combatStore.ts +++ b/src/lib/game/stores/combatStore.ts @@ -200,7 +200,7 @@ export const useCombatStore = create()( enterSpireMode: () => { const freshRoom = generateSpireFloorState(1, 0, 1); - set({ + set((s) => ({ spireMode: true, currentAction: 'climb', currentFloor: 1, @@ -211,7 +211,8 @@ export const useCombatStore = create()( climbDirection: null, isDescending: false, clearedFloors: {}, - }); + maxFloorReached: Math.max(s.maxFloorReached, s.currentFloor), + })); }, learnSpell: (spellId: string) => { diff --git a/src/lib/game/stores/discipline-slice.ts b/src/lib/game/stores/discipline-slice.ts index 2d5d220..3cf4e7f 100644 --- a/src/lib/game/stores/discipline-slice.ts +++ b/src/lib/game/stores/discipline-slice.ts @@ -56,6 +56,7 @@ export interface DisciplineStoreActions { unlockedEffects: string[]; }; setPracticingCallbacks(callbacks: { onStartPracticing: () => void; onStopPracticing: () => void }): void; + resetDisciplines: () => void; } export type DisciplineStore = DisciplineStoreState & DisciplineStoreActions; @@ -146,6 +147,16 @@ export const useDisciplineStore = create()( set({ practicingCallbacks: callbacks }); }, + resetDisciplines() { + set({ + disciplines: {}, + activeIds: [], + concurrentLimit: MAX_CONCURRENT_DISCIPLINES, + totalXP: 0, + processedPerks: [], + }); + }, + processTick(mana) { const s = get(); let rawMana = mana.rawMana; diff --git a/src/lib/game/stores/gameLoopActions.ts b/src/lib/game/stores/gameLoopActions.ts index 5f0f444..eded73c 100644 --- a/src/lib/game/stores/gameLoopActions.ts +++ b/src/lib/game/stores/gameLoopActions.ts @@ -6,6 +6,7 @@ import { useUIStore } from './uiStore'; import { usePrestigeStore } from './prestigeStore'; import { useManaStore } from './manaStore'; import { useCombatStore } from './combatStore'; +import { useDisciplineStore } from './discipline-slice'; import { computeDisciplineEffects } from '../effects/discipline-effects'; export const createStartNewLoop = (set: (state: Partial) => void) => () => { @@ -41,6 +42,7 @@ export const createStartNewLoop = (set: (state: Partial) = usePrestigeStore.getState().incrementLoopCount(); useManaStore.getState().resetMana(pu); + useDisciplineStore.getState().resetDisciplines(); // Reset combat with starting floor and any spells from prestige upgrades const startSpells = makeInitialSpells(); diff --git a/src/lib/game/utils/combat-utils.ts b/src/lib/game/utils/combat-utils.ts index d6eedd9..f4bd0bb 100644 --- a/src/lib/game/utils/combat-utils.ts +++ b/src/lib/game/utils/combat-utils.ts @@ -186,6 +186,9 @@ export function calcDamage( damage *= critDamageMult; } + // Final NaN guard — if anything produced NaN, return a safe fallback + if (!Number.isFinite(damage)) return 5; + return damage; }