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

This commit is contained in:
2026-05-28 18:14:19 +02:00
parent 13c185a216
commit bc184cefb0
8 changed files with 49 additions and 9 deletions
+21 -4
View File
@@ -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
}
+3 -2
View File
@@ -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) => {
+11
View File
@@ -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;
+2
View File
@@ -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();
+3
View File
@@ -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;
}