From 5f4d29d96e98d9e405484a05925c653349116ead Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Mon, 15 Jun 2026 12:28:48 +0200 Subject: [PATCH] fix: createDefaultCombatState now uses discipline-aware channel stats - Import computeChannelStats in combat-reset.ts - Replace hardcoded channelSpeedMultiplier/channelDrainRate with computed values - Fixes bug where resetCombat() lost discipline-modified channel stats - Add regression tests in combat-reset-channel-stats.test.ts --- docs/circular-deps.txt | 17 +-- docs/dependency-graph.json | 15 +-- docs/project-structure.txt | 1 + .../combat-reset-channel-stats.test.ts | 111 ++++++++++++++++++ src/lib/game/stores/combat-reset.ts | 7 +- 5 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 src/lib/game/__tests__/combat-reset-channel-stats.test.ts diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 597847a..c4221b6 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,14 +1,15 @@ # Circular Dependencies -Generated: 2026-06-15T08:59:04.077Z -Found: 7 circular chain(s) — these MUST be fixed before modifying involved files. +Generated: 2026-06-15T10:13:15.747Z +Found: 8 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts -2. 2) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts -3. 3) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts > stores/crafting-equipment-tick.ts -4. 4) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts > stores/pipelines/equipment-crafting.ts -5. 5) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts -6. 6) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts -7. 7) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts > stores/non-combat-room-actions.ts +2. 2) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts +3. 3) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts +4. 4) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts > stores/crafting-equipment-tick.ts +5. 5) stores/combatStore.ts > stores/combat-actions.ts > stores/combat-room-enchantments.ts > stores/craftingStore.ts > stores/pipelines/equipment-crafting.ts +6. 6) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts +7. 7) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts +8. 8) stores/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.ts > stores/non-combat-room-actions.ts ## How to fix 1. Identify which import in the chain can be extracted to a shared types/utils file. diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 0e1b766..12f2754 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-15T08:59:01.387Z", + "generated": "2026-06-15T10:13:13.518Z", "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." }, @@ -150,10 +150,6 @@ "stores/craftingStore.types.ts", "types.ts" ], - "crafting-actions/disenchant-actions.ts": [ - "stores/craftingStore.types.ts", - "stores/manaStore.ts" - ], "crafting-actions/equipment-actions.ts": [ "crafting-utils.ts", "stores/craftingStore.types.ts", @@ -164,7 +160,6 @@ "crafting-actions/computed-getters.ts", "crafting-actions/crafting-equipment-actions.ts", "crafting-actions/design-actions.ts", - "crafting-actions/disenchant-actions.ts", "crafting-actions/equipment-actions.ts", "crafting-actions/preparation-actions.ts" ], @@ -669,20 +664,16 @@ "stores/craftingStore.ts": [ "crafting-actions/application-actions.ts", "crafting-actions/crafting-material-actions.ts", + "crafting-actions/design-actions.ts", "crafting-actions/equipment-actions.ts", "crafting-actions/preparation-actions.ts", - "crafting-design.ts", - "crafting-utils.ts", - "effects/discipline-effects.ts", - "effects/special-effects.ts", - "effects/upgrade-effects.ts", + "stores/attunementStore.ts", "stores/combatStore.ts", "stores/crafting-equipment-tick.ts", "stores/crafting-initial-state.ts", "stores/craftingStore.types.ts", "stores/manaStore.ts", "stores/pipelines/equipment-crafting.ts", - "stores/uiStore.ts", "types/equipmentSlot.ts", "utils/result.ts", "utils/safe-persist.ts" diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 61febab..3329547 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -209,6 +209,7 @@ Mana-Loop/ │ │ │ │ ├── bug-377-mana-auto-unlock.test.ts │ │ │ │ ├── bug-fixes.test.ts │ │ │ │ ├── combat-actions.test.ts +│ │ │ │ ├── combat-reset-channel-stats.test.ts │ │ │ │ ├── combat-utils.test.ts │ │ │ │ ├── computed-stats.test.ts │ │ │ │ ├── conversion-pause-bug-regression.test.ts diff --git a/src/lib/game/__tests__/combat-reset-channel-stats.test.ts b/src/lib/game/__tests__/combat-reset-channel-stats.test.ts new file mode 100644 index 0000000..264834f --- /dev/null +++ b/src/lib/game/__tests__/combat-reset-channel-stats.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { useCombatStore } from '../stores/combatStore'; +import { useDisciplineStore } from '../stores/discipline-slice'; +import { createDefaultCombatState } from '../stores/combat-reset'; +import { computeChannelStats } from '../stores/combat-channel'; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function resetCombatStore() { + useCombatStore.setState({ + currentFloor: 1, + floorHP: 100, + floorMaxHP: 100, + maxFloorReached: 1, + activeSpell: 'manaBolt', + currentAction: 'meditate', + castProgress: 0, + spireMode: false, + currentRoom: { roomType: 'combat', enemies: [] }, + clearedFloors: {}, + climbDirection: null, + isDescending: false, + golemancy: { activeGolems: [], lastSummonFloor: 0, golemDesigns: {}, golemLoadout: [] }, + equipmentSpellStates: [], + comboHitCount: 0, + floorHitCount: 0, + spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } }, + activityLog: [], + achievements: { unlocked: [], progress: {} }, + totalSpellsCast: 0, + totalDamageDealt: 0, + totalCraftsCompleted: 0, + }); +} + +function resetDisciplineStore() { + useDisciplineStore.setState({ + activeDisciplines: [], + disciplineXP: {}, + disciplineLevels: {}, + concurrentLimit: 1, + }); +} + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +describe('createDefaultCombatState channel stats', () => { + beforeEach(() => { + resetCombatStore(); + resetDisciplineStore(); + }); + + it('should use base channel stats when no disciplines are active', () => { + const state = createDefaultCombatState(1); + // Base values from combat-channel.ts + expect(state.channelSpeedMultiplier).toBe(1.5); + expect(state.channelDrainRate).toBe(0.08); + }); + + it('should match computeChannelStats output', () => { + const state = createDefaultCombatState(1); + const expected = computeChannelStats(); + expect(state.channelSpeedMultiplier).toBe(expected.speedMultiplier); + expect(state.channelDrainRate).toBe(expected.drainRate); + }); + + it('should reset isChanneling to false', () => { + useCombatStore.setState({ isChanneling: true }); + const state = createDefaultCombatState(1); + expect(state.isChanneling).toBe(false); + }); + + it('should produce channel stats consistent with combat tick computation', () => { + // The key invariant: after resetCombat, the channel stats in the store + // should match what computeChannelStats() returns, so the first tick + // doesn't see a mismatch. + const defaults = createDefaultCombatState(1); + const expected = computeChannelStats(); + expect(defaults.channelSpeedMultiplier).toBeCloseTo(expected.speedMultiplier, 10); + expect(defaults.channelDrainRate).toBeCloseTo(expected.drainRate, 10); + }); +}); + +describe('resetCombat preserves channel stat consistency', () => { + beforeEach(() => { + resetCombatStore(); + resetDisciplineStore(); + }); + + it('should set channel stats from computeChannelStats on reset', () => { + // Simulate combat tick having written modified channel stats + useCombatStore.setState({ + channelSpeedMultiplier: 2.0, + channelDrainRate: 0.12, + }); + + // Reset combat + useCombatStore.getState().resetCombat(1); + + // After reset, stats should be the discipline-computed defaults + const expected = computeChannelStats(); + expect(useCombatStore.getState().channelSpeedMultiplier).toBe(expected.speedMultiplier); + expect(useCombatStore.getState().channelDrainRate).toBe(expected.drainRate); + }); + + it('should not leave isChanneling true after reset', () => { + useCombatStore.setState({ isChanneling: true }); + useCombatStore.getState().resetCombat(1); + expect(useCombatStore.getState().isChanneling).toBe(false); + }); +}); diff --git a/src/lib/game/stores/combat-reset.ts b/src/lib/game/stores/combat-reset.ts index c558859..185402c 100644 --- a/src/lib/game/stores/combat-reset.ts +++ b/src/lib/game/stores/combat-reset.ts @@ -7,6 +7,7 @@ import { getFloorMaxHP } from '../utils'; import { generateSpireFloorState, getRoomsForFloor } from '../utils/spire-utils'; import { makeInitialSpells } from './combat-actions'; import type { CombatState } from './combat-state.types'; +import { computeChannelStats } from './combat-channel'; export function createDefaultCombatState( startFloor: number, @@ -64,10 +65,10 @@ export function createDefaultCombatState( totalDamageDealt: 0, totalCraftsCompleted: 0, - // Transference Channel defaults + // Transference Channel defaults (discipline-aware) isChanneling: false, - channelSpeedMultiplier: 1.5, - channelDrainRate: 0.08, + channelSpeedMultiplier: computeChannelStats().speedMultiplier, + channelDrainRate: computeChannelStats().drainRate, // Room Enchantments defaults lastRoomCoverage: 0,