fix: deactivate all disciplines when entering/exiting the Spire
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s

- Add deactivateAll() action to discipline-slice.ts
- Call deactivateAll() in createEnterSpireMode() (combat-descent-actions.ts)
- Add spireMode guard in gameStore.ts tick() to skip discipline processTick
- Call deactivateAll() in exitSpireMode() (combatStore.ts) as safety measure
- Extract buildConversionParams to utils/conversion-params.ts to keep gameStore.ts under 400 lines
- Add regression tests (5 tests, all 1136 passing)

Fixes #347
This commit is contained in:
2026-06-10 11:41:25 +02:00
parent 076282caf3
commit 48eee17d43
10 changed files with 295 additions and 30 deletions
@@ -10,6 +10,7 @@ import { useManaStore } from './manaStore';
import { summonGolemsOnRoomEntry } from './golem-combat-actions';
import { computeDisciplineEffects } from '../effects/discipline-effects';
import { useAttunementStore } from './attunementStore';
import { useDisciplineStore } from './discipline-slice';
import {
onEnterLibraryRoom,
onEnterRecoveryRoom,
@@ -278,6 +279,9 @@ export function createEnterSpireMode(get: GetFn, set: SetFn) {
golemancy: { activeGolems: [], lastSummonFloor: 0, golemDesigns: get().golemancy?.golemDesigns ?? {}, golemLoadout: [] },
});
// Deactivate all active disciplines when entering the Spire
useDisciplineStore.getState().deactivateAll();
get().addActivityLog('floor_transition',
`Entered the Spire at Floor ${startFloor}`);
};
+3
View File
@@ -17,6 +17,7 @@ import {
import {
onEnterLibraryRoom, tickNonCombatRoom, skipNonCombatRoom, stayLongerInRoom,
} from './non-combat-room-actions';
import { useDisciplineStore } from './discipline-slice';
import {
addGolemDesign, removeGolemDesign, toggleGolemLoadoutEntry,
} from './golemancy-actions';
@@ -210,6 +211,8 @@ export const useCombatStore = create<CombatStore>()(
maxFloorReached: Math.max(s.maxFloorReached, 1),
golemancy: { ...s.golemancy, activeGolems: [] as RuntimeActiveGolem[] },
});
// Deactivate all disciplines on spire exit for safety
useDisciplineStore.getState().deactivateAll();
get().addActivityLog('floor_transition', 'Exited the Spire');
},
+17
View File
@@ -52,6 +52,7 @@ export interface DisciplineStoreState {
export interface DisciplineStoreActions {
activate: (id: string, gameStateOverrides?: { elements?: Record<string, ElementState>; rawMana?: number; signedPacts?: number[] }) => void;
deactivate: (id: string) => void;
deactivateAll: () => void;
processTick: (mana: { rawMana: number; elements: Record<string, ElementState> }) => {
rawMana: number;
elements: Record<string, ElementState>;
@@ -161,6 +162,22 @@ export const useDisciplineStore = create<DisciplineStore>()(
});
},
deactivateAll() {
set((s) => {
const newDisciplines = { ...s.disciplines };
for (const id of s.activeIds) {
if (newDisciplines[id]) {
newDisciplines[id] = { ...newDisciplines[id], paused: true };
}
}
get().practicingCallbacks?.onStopPracticing?.();
return {
activeIds: [],
disciplines: newDisciplines,
};
});
},
setPracticingCallbacks(callbacks) {
set({ practicingCallbacks: callbacks });
},
+6 -26
View File
@@ -19,7 +19,6 @@ import { useCombatStore } from './combatStore';
import { useAttunementStore } from './attunementStore';
import { useCraftingStore } from './craftingStore';
import { useDisciplineStore } from './discipline-slice';
import { ATTUNEMENTS_DEF } from '../data/attunements';
import { createResetGame, createGatherMana } from './gameActions';
import { createSafeStorage } from '../utils/safe-persist';
import { createStartNewLoop } from './gameLoopActions';
@@ -27,6 +26,7 @@ import { buildTickContext, applyTickWrites } from './tick-pipeline';
import { processEnchantingTicks } from './pipelines/enchanting-tick';
import { buildGolemCombatPipeline } from './pipelines/golem-combat';
import { getGuardianForFloor } from '../data/guardian-encounters';
import { buildConversionParams } from '../utils/conversion-params';
import type { TickContext, TickWrites } from './tick-pipeline';
import type { GameCoordinatorState } from './gameStore.types';
@@ -228,7 +228,10 @@ export const useGameStore = create<GameCoordinatorStore>()(
usePrestigeStore.getState().completePactRitual(addLog);
}
const dr = useDisciplineStore.getState().processTick({ rawMana, elements });
// Skip discipline processing during spire runs (defense-in-depth)
const dr = ctx.combat.spireMode
? { rawMana, elements, unlockedEffects: [], unlockedRecipes: [], autoPausedNames: [] }
: useDisciplineStore.getState().processTick({ rawMana, elements });
rawMana = dr.rawMana; elements = dr.elements;
rawMana = Math.min(rawMana, computeMaxMana({ prestigeUpgrades: ctx.prestige.prestigeUpgrades }, undefined, computeDisciplineEffects()));
@@ -373,27 +376,4 @@ export const useGameStore = create<GameCoordinatorStore>()(
)
);
/** Build pact element map and gross regen for the unified conversion system */
function buildConversionParams(
signedPacts: number[],
attunements: Record<string, { active: boolean; level: number }>,
): { pactElementMap: Record<number, string>; grossRegen: Record<string, number> } {
const pactElementMap: Record<number, string> = {};
for (const floor of signedPacts) {
const guardian = getGuardianForFloor(floor);
if (guardian?.element?.length) {
pactElementMap[floor] = guardian.element[0];
}
}
const grossRegen: Record<string, number> = {};
for (const [id, state] of Object.entries(attunements)) {
if (!state.active) continue;
const def = ATTUNEMENTS_DEF[id];
if (def?.primaryManaType && def.rawManaRegen) {
const levelMult = Math.pow(1.5, (state.level || 1) - 1);
grossRegen[def.primaryManaType] = (grossRegen[def.primaryManaType] || 0)
+ def.rawManaRegen * levelMult;
}
}
return { pactElementMap, grossRegen };
}