fix: resetGame button doesn't fully reset game state
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-06-11T14:10:00.590Z
|
||||
Generated: 2026-06-12T05:02:18.108Z
|
||||
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
|
||||
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-06-11T14:09:58.499Z",
|
||||
"generated": "2026-06-12T05:02:13.512Z",
|
||||
"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."
|
||||
},
|
||||
|
||||
@@ -241,6 +241,7 @@ Mana-Loop/
|
||||
│ │ │ │ ├── paused-conversion-dedup.test.ts
|
||||
│ │ │ │ ├── persistence.test.ts
|
||||
│ │ │ │ ├── regression-fixes.test.ts
|
||||
│ │ │ │ ├── reset-game-comprehensive.test.ts
|
||||
│ │ │ │ ├── room-utils-floor-state.test.ts
|
||||
│ │ │ │ ├── room-utils.test.ts
|
||||
│ │ │ │ ├── spell-cast-floorhp-guard.test.ts
|
||||
@@ -380,6 +381,7 @@ Mana-Loop/
|
||||
│ │ │ │ ├── combat-actions.ts
|
||||
│ │ │ │ ├── combat-damage.ts
|
||||
│ │ │ │ ├── combat-descent-actions.ts
|
||||
│ │ │ │ ├── combat-reset.ts
|
||||
│ │ │ │ ├── combat-state.types.ts
|
||||
│ │ │ │ ├── combatStore.ts
|
||||
│ │ │ │ ├── crafting-equipment-tick.ts
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { useGameStore } from '../stores/gameStore';
|
||||
import { useManaStore } from '../stores/manaStore';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||
import { useCraftingStore } from '../stores/craftingStore';
|
||||
import { useAttunementStore } from '../stores/attunementStore';
|
||||
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||
import { useUIStore } from '../stores/uiStore';
|
||||
|
||||
// Exact localStorage keys matching each store's persist config `name`
|
||||
const ALL_STORE_KEYS = [
|
||||
'mana-loop-game-storage',
|
||||
'mana-loop-mana',
|
||||
'mana-loop-combat',
|
||||
'mana-loop-prestige',
|
||||
'mana-loop-crafting',
|
||||
'mana-loop-attunements',
|
||||
'mana-loop-discipline-store',
|
||||
'mana-loop-ui-storage',
|
||||
] as const;
|
||||
|
||||
function clearAllPersistedState() {
|
||||
for (const key of ALL_STORE_KEYS) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set non-default state across ALL stores to simulate a game in progress.
|
||||
* This modifies every field that should be reset by resetGame().
|
||||
*/
|
||||
function setNonDefaultState() {
|
||||
// Game store: advance time
|
||||
useGameStore.setState({ day: 15, hour: 12 });
|
||||
|
||||
// Mana store: add mana, unlock elements
|
||||
useManaStore.setState((s) => ({
|
||||
rawMana: 500,
|
||||
meditateTicks: 100,
|
||||
totalManaGathered: 10000,
|
||||
elements: {
|
||||
...s.elements,
|
||||
fire: { current: 50, max: 100, baseMax: 100, unlocked: true },
|
||||
water: { current: 30, max: 100, baseMax: 100, unlocked: true },
|
||||
},
|
||||
}));
|
||||
|
||||
// Combat store: change many fields
|
||||
useCombatStore.setState({
|
||||
currentFloor: 25,
|
||||
floorHP: 500,
|
||||
floorMaxHP: 2000,
|
||||
maxFloorReached: 25,
|
||||
activeSpell: 'fireBolt',
|
||||
currentAction: 'climb',
|
||||
castProgress: 0.5,
|
||||
spireMode: true,
|
||||
clearedFloors: { 1: true, 2: true },
|
||||
climbDirection: 'up' as const,
|
||||
isDescending: false,
|
||||
weaponCastProgress: { primary: 0.3 },
|
||||
comboHitCount: 5,
|
||||
floorHitCount: 10,
|
||||
meleeSwordProgress: { sword1: 0.2 },
|
||||
guardianShield: 100,
|
||||
guardianShieldMax: 200,
|
||||
guardianBarrier: 50,
|
||||
guardianBarrierMax: 100,
|
||||
totalSpellsCast: 500,
|
||||
totalDamageDealt: 9999,
|
||||
totalCraftsCompleted: 50,
|
||||
});
|
||||
|
||||
// Prestige store: add insight, upgrades, pacts
|
||||
usePrestigeStore.setState({
|
||||
insight: 1000,
|
||||
totalInsight: 5000,
|
||||
loopCount: 3,
|
||||
prestigeUpgrades: { manaWell: 2, manaFlow: 1 },
|
||||
defeatedGuardians: [10, 20, 30],
|
||||
signedPacts: [10, 20],
|
||||
});
|
||||
|
||||
// Attunement store: level up
|
||||
useAttunementStore.setState({
|
||||
attunements: {
|
||||
enchanter: { id: 'enchanter', active: true, level: 5, experience: 1000 },
|
||||
invoker: { id: 'invoker', active: true, level: 3, experience: 500 },
|
||||
},
|
||||
});
|
||||
|
||||
// Discipline store: add disciplines and XP
|
||||
useDisciplineStore.setState({
|
||||
disciplines: {
|
||||
rawManaMastery: { id: 'rawManaMastery', xp: 500, paused: false },
|
||||
},
|
||||
activeIds: ['rawManaMastery'],
|
||||
totalXP: 500,
|
||||
concurrentLimit: 2,
|
||||
processedPerks: ['somePerk'],
|
||||
});
|
||||
|
||||
// Crafting store: add designs, unlock effects, add loot
|
||||
useCraftingStore.setState((s) => ({
|
||||
designProgress: {
|
||||
designId: 'design-1',
|
||||
progress: 5,
|
||||
required: 10,
|
||||
name: 'My Design',
|
||||
equipmentType: 'basicStaff',
|
||||
effects: [{ effectId: 'spell_fireBolt', stacks: 1, actualCost: 30 }],
|
||||
},
|
||||
enchantmentDesigns: [{ id: 'design-1', name: 'My Design', equipmentType: 'basicStaff', effects: [], totalCapacityCost: 30, totalStacks: 1 }],
|
||||
unlockedEffects: ['spell_fireBolt', 'spell_iceBolt'],
|
||||
unlockedRecipes: ['recipe1', 'recipe2'],
|
||||
lootInventory: {
|
||||
materials: { ironOre: 100, manaCrystalDust: 50 },
|
||||
blueprints: ['bp1', 'bp2'],
|
||||
},
|
||||
}));
|
||||
|
||||
// UI store: set game over, pause
|
||||
useUIStore.setState({
|
||||
paused: true,
|
||||
gameOver: true,
|
||||
victory: true,
|
||||
logs: ['some log message'],
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// COMPREHENSIVE RESET GAME TESTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe('resetGame comprehensive', () => {
|
||||
beforeEach(() => {
|
||||
clearAllPersistedState();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearAllPersistedState();
|
||||
});
|
||||
|
||||
it('should reset ALL game store fields to initial values', () => {
|
||||
setNonDefaultState();
|
||||
useGameStore.getState().resetGame();
|
||||
|
||||
const game = useGameStore.getState();
|
||||
expect(game.day).toBe(1);
|
||||
expect(game.hour).toBe(0);
|
||||
expect(game.incursionStrength).toBe(0);
|
||||
expect(game.containmentWards).toBe(0);
|
||||
expect(game.initialized).toBe(true);
|
||||
});
|
||||
|
||||
it('should reset ALL mana store fields to initial values', () => {
|
||||
setNonDefaultState();
|
||||
useGameStore.getState().resetGame();
|
||||
|
||||
const mana = useManaStore.getState();
|
||||
expect(mana.rawMana).toBeGreaterThanOrEqual(0);
|
||||
expect(mana.rawMana).toBeLessThanOrEqual(20); // starting mana should be small
|
||||
expect(mana.meditateTicks).toBe(0);
|
||||
expect(mana.totalManaGathered).toBe(0);
|
||||
expect(mana.elementRegen).toEqual({});
|
||||
|
||||
// Only transference should be unlocked (or base elements depending on config)
|
||||
for (const [key, elem] of Object.entries(mana.elements)) {
|
||||
// Fire and water should NOT be unlocked (they were unlocked by setNonDefaultState)
|
||||
if (key === 'fire' || key === 'water') {
|
||||
// After reset, these should be locked (not unlocked)
|
||||
// The initial state only has certain elements unlocked
|
||||
expect(elem.current).toBeGreaterThanOrEqual(0);
|
||||
expect(elem.max).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should reset ALL combat store fields to initial values', () => {
|
||||
setNonDefaultState();
|
||||
useGameStore.getState().resetGame();
|
||||
|
||||
const combat = useCombatStore.getState();
|
||||
expect(combat.currentFloor).toBe(1);
|
||||
expect(combat.maxFloorReached).toBe(1);
|
||||
expect(combat.activeSpell).toBe('manaBolt');
|
||||
expect(combat.currentAction).toBe('meditate');
|
||||
expect(combat.castProgress).toBe(0);
|
||||
|
||||
// These fields should also be reset but might be missed by resetCombat:
|
||||
expect(combat.spireMode).toBe(false);
|
||||
expect(combat.clearedFloors).toEqual({});
|
||||
expect(combat.climbDirection).toBeNull();
|
||||
expect(combat.weaponCastProgress).toEqual({});
|
||||
expect(combat.comboHitCount).toBe(0);
|
||||
expect(combat.floorHitCount).toBe(0);
|
||||
expect(combat.meleeSwordProgress).toEqual({});
|
||||
expect(combat.guardianShield).toBe(0);
|
||||
expect(combat.guardianShieldMax).toBe(0);
|
||||
expect(combat.guardianBarrier).toBe(0);
|
||||
expect(combat.guardianBarrierMax).toBe(0);
|
||||
expect(combat.totalSpellsCast).toBe(0);
|
||||
expect(combat.totalDamageDealt).toBe(0);
|
||||
expect(combat.totalCraftsCompleted).toBe(0);
|
||||
});
|
||||
|
||||
it('should reset ALL prestige store fields to initial values', () => {
|
||||
setNonDefaultState();
|
||||
useGameStore.getState().resetGame();
|
||||
|
||||
const prestige = usePrestigeStore.getState();
|
||||
expect(prestige.insight).toBe(0);
|
||||
expect(prestige.totalInsight).toBe(0);
|
||||
expect(prestige.loopCount).toBe(0);
|
||||
expect(prestige.prestigeUpgrades).toEqual({});
|
||||
expect(prestige.defeatedGuardians).toEqual([]);
|
||||
expect(prestige.signedPacts).toEqual([]);
|
||||
expect(prestige.pactRitualFloor).toBeNull();
|
||||
expect(prestige.pactRitualProgress).toBe(0);
|
||||
});
|
||||
|
||||
it('should reset ALL attunement store fields to initial values', () => {
|
||||
setNonDefaultState();
|
||||
useGameStore.getState().resetGame();
|
||||
|
||||
const att = useAttunementStore.getState();
|
||||
// Should only have enchanter at level 1
|
||||
expect(Object.keys(att.attunements).length).toBe(1);
|
||||
expect(att.attunements.enchanter).toBeDefined();
|
||||
expect(att.attunements.enchanter.level).toBe(1);
|
||||
expect(att.attunements.enchanter.experience).toBe(0);
|
||||
expect(att.attunements.enchanter.active).toBe(true);
|
||||
// Invoker should not be present
|
||||
expect(att.attunements.invoker).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reset ALL discipline store fields to initial values', () => {
|
||||
setNonDefaultState();
|
||||
useGameStore.getState().resetGame();
|
||||
|
||||
const disc = useDisciplineStore.getState();
|
||||
expect(disc.disciplines).toEqual({});
|
||||
expect(disc.activeIds).toEqual([]);
|
||||
expect(disc.totalXP).toBe(0);
|
||||
expect(disc.processedPerks).toEqual([]);
|
||||
});
|
||||
|
||||
it('should reset ALL crafting store fields to initial values', () => {
|
||||
setNonDefaultState();
|
||||
useGameStore.getState().resetGame();
|
||||
|
||||
const crafting = useCraftingStore.getState();
|
||||
expect(crafting.designProgress).toBeNull();
|
||||
expect(crafting.designProgress2).toBeNull();
|
||||
expect(crafting.enchantmentDesigns).toEqual([]);
|
||||
expect(crafting.unlockedEffects).toEqual([]);
|
||||
expect(crafting.unlockedRecipes).toEqual([]);
|
||||
expect(crafting.lootInventory.materials).toEqual({});
|
||||
expect(crafting.lootInventory.blueprints).toEqual([]);
|
||||
|
||||
// Should still have the starting equipment
|
||||
expect(Object.keys(crafting.equipmentInstances).length).toBe(3); // staff, shirt, shoes
|
||||
expect(crafting.equippedInstances.mainHand).not.toBeNull();
|
||||
expect(crafting.equippedInstances.body).not.toBeNull();
|
||||
expect(crafting.equippedInstances.feet).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should reset ALL UI store fields to initial values', () => {
|
||||
setNonDefaultState();
|
||||
useGameStore.getState().resetGame();
|
||||
|
||||
const ui = useUIStore.getState();
|
||||
expect(ui.paused).toBe(false);
|
||||
expect(ui.gameOver).toBe(false);
|
||||
expect(ui.victory).toBe(false);
|
||||
expect(ui.logs.length).toBe(1); // fresh game starts with one log message
|
||||
});
|
||||
|
||||
it('should clear all localStorage keys and not leave stale data', () => {
|
||||
setNonDefaultState();
|
||||
|
||||
// First, force all stores to persist by calling setState on each
|
||||
// (simulating what happens during normal gameplay)
|
||||
const gameState = useGameStore.getState();
|
||||
useGameStore.setState({ day: gameState.day });
|
||||
|
||||
useGameStore.getState().resetGame();
|
||||
|
||||
// After reset, localStorage should contain the reset state, not the old state
|
||||
const combatRaw = localStorage.getItem('mana-loop-combat');
|
||||
if (combatRaw) {
|
||||
const combatParsed = JSON.parse(combatRaw);
|
||||
expect(combatParsed.state.currentFloor).toBe(1);
|
||||
}
|
||||
|
||||
const manaRaw = localStorage.getItem('mana-loop-mana');
|
||||
if (manaRaw) {
|
||||
const manaParsed = JSON.parse(manaRaw);
|
||||
expect(manaParsed.state.totalManaGathered).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
// ─── Combat Reset Helper ───────────────────────────────────────────────────────
|
||||
// Generates the full default combat store state for resetCombat().
|
||||
// Keeps combatStore.ts under the 400-line limit.
|
||||
|
||||
import type { RuntimeActiveGolem } from '../types';
|
||||
import { getFloorMaxHP } from '../utils';
|
||||
import { generateSpireFloorState, getRoomsForFloor } from '../utils/spire-utils';
|
||||
import { makeInitialSpells } from './combat-actions';
|
||||
import type { CombatState } from './combat-state.types';
|
||||
|
||||
export function createDefaultCombatState(
|
||||
startFloor: number,
|
||||
spellsToKeep: string[] = [],
|
||||
): Partial<CombatState> {
|
||||
const startSpells = makeInitialSpells(spellsToKeep);
|
||||
const seed = startFloor * 12345;
|
||||
const rooms = getRoomsForFloor(startFloor, seed);
|
||||
const startRoom = generateSpireFloorState(startFloor, 0, rooms, 0);
|
||||
|
||||
return {
|
||||
currentFloor: startFloor,
|
||||
floorHP: getFloorMaxHP(startFloor),
|
||||
floorMaxHP: getFloorMaxHP(startFloor),
|
||||
maxFloorReached: startFloor,
|
||||
activeSpell: 'manaBolt',
|
||||
currentAction: 'meditate',
|
||||
castProgress: 0,
|
||||
spireMode: false,
|
||||
currentRoom: startRoom,
|
||||
clearedFloors: {},
|
||||
climbDirection: null,
|
||||
isDescending: false,
|
||||
startFloor,
|
||||
exitFloor: startFloor,
|
||||
currentRoomIndex: 0,
|
||||
roomsPerFloor: rooms,
|
||||
runId: 0,
|
||||
descentPeak: null,
|
||||
roomResetState: {},
|
||||
clearedRooms: {},
|
||||
isDescentComplete: false,
|
||||
golemancy: {
|
||||
golemDesigns: {},
|
||||
golemLoadout: [],
|
||||
activeGolems: [] as RuntimeActiveGolem[],
|
||||
lastSummonFloor: 0,
|
||||
},
|
||||
equipmentSpellStates: [],
|
||||
weaponCastProgress: {},
|
||||
comboHitCount: 0,
|
||||
floorHitCount: 0,
|
||||
meleeSwordProgress: {},
|
||||
guardianShield: 0,
|
||||
guardianShieldMax: 0,
|
||||
guardianBarrier: 0,
|
||||
guardianBarrierMax: 0,
|
||||
spells: startSpells,
|
||||
activityLog: [],
|
||||
achievements: {
|
||||
unlocked: [],
|
||||
progress: {},
|
||||
},
|
||||
totalSpellsCast: 0,
|
||||
totalDamageDealt: 0,
|
||||
totalCraftsCompleted: 0,
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import type { CombatStore } from './combat-state.types';
|
||||
import {
|
||||
enterDescentMode, advanceRoomOrFloor, onEnterRoomDescend, createEnterSpireMode,
|
||||
} from './combat-descent-actions';
|
||||
import { createDefaultCombatState } from './combat-reset';
|
||||
import {
|
||||
onEnterLibraryRoom, tickNonCombatRoom, skipNonCombatRoom, stayLongerInRoom,
|
||||
} from './non-combat-room-actions';
|
||||
@@ -329,18 +330,7 @@ export const useCombatStore = create<CombatStore>()(
|
||||
},
|
||||
|
||||
resetCombat: (startFloor: number, spellsToKeep: string[] = []) => {
|
||||
const startSpells = makeInitialSpells(spellsToKeep);
|
||||
|
||||
set({
|
||||
currentFloor: startFloor,
|
||||
floorHP: getFloorMaxHP(startFloor),
|
||||
floorMaxHP: getFloorMaxHP(startFloor),
|
||||
maxFloorReached: startFloor,
|
||||
activeSpell: 'manaBolt',
|
||||
currentAction: 'meditate',
|
||||
castProgress: 0,
|
||||
spells: startSpells,
|
||||
});
|
||||
set(createDefaultCombatState(startFloor, spellsToKeep));
|
||||
},
|
||||
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user