fix: defensive hardening — NaN guards, cast loop safety, discipline reset on new loop, spire mode maxFloorReached fix
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-05-28T11:45:27.810Z
|
||||
Generated: 2026-05-28T13:28:24.658Z
|
||||
|
||||
No circular dependencies found. ✅
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -18,7 +18,13 @@ import { DebugName } from '@/components/game/debug/debug-context';
|
||||
// ─── Derived Stats Hook ──────────────────────────────────────────────────────
|
||||
|
||||
function useSpireStats(prestigeUpgrades: Record<string, number>, equippedInstances: Record<string, string | null>, equipmentInstances: Record<string, import('@/lib/game/types').EquipmentInstance>) {
|
||||
const disciplineEffects = computeDisciplineEffects();
|
||||
let disciplineEffects: ReturnType<typeof computeDisciplineEffects> | 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: {},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ export const useCombatStore = create<CombatStore>()(
|
||||
|
||||
enterSpireMode: () => {
|
||||
const freshRoom = generateSpireFloorState(1, 0, 1);
|
||||
set({
|
||||
set((s) => ({
|
||||
spireMode: true,
|
||||
currentAction: 'climb',
|
||||
currentFloor: 1,
|
||||
@@ -211,7 +211,8 @@ export const useCombatStore = create<CombatStore>()(
|
||||
climbDirection: null,
|
||||
isDescending: false,
|
||||
clearedFloors: {},
|
||||
});
|
||||
maxFloorReached: Math.max(s.maxFloorReached, s.currentFloor),
|
||||
}));
|
||||
},
|
||||
|
||||
learnSpell: (spellId: string) => {
|
||||
|
||||
@@ -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<DisciplineStore>()(
|
||||
set({ practicingCallbacks: callbacks });
|
||||
},
|
||||
|
||||
resetDisciplines() {
|
||||
set({
|
||||
disciplines: {},
|
||||
activeIds: [],
|
||||
concurrentLimit: MAX_CONCURRENT_DISCIPLINES,
|
||||
totalXP: 0,
|
||||
processedPerks: [],
|
||||
});
|
||||
},
|
||||
|
||||
processTick(mana) {
|
||||
const s = get();
|
||||
let rawMana = mana.rawMana;
|
||||
|
||||
@@ -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<GameCoordinatorState>) => void) => () => {
|
||||
@@ -41,6 +42,7 @@ export const createStartNewLoop = (set: (state: Partial<GameCoordinatorState>) =
|
||||
usePrestigeStore.getState().incrementLoopCount();
|
||||
|
||||
useManaStore.getState().resetMana(pu);
|
||||
useDisciplineStore.getState().resetDisciplines();
|
||||
|
||||
// Reset combat with starting floor and any spells from prestige upgrades
|
||||
const startSpells = makeInitialSpells();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user