fix: createDefaultCombatState now uses discipline-aware channel stats
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m25s

- 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
This commit is contained in:
2026-06-15 12:28:48 +02:00
parent e76528b449
commit 5f4d29d96e
5 changed files with 128 additions and 23 deletions
+9 -8
View File
@@ -1,14 +1,15 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-06-15T08:59:04.077Z Generated: 2026-06-15T10:13:15.747Z
Found: 7 circular chain(s) — these MUST be fixed before modifying involved files. Found: 8 circular chain(s) — these MUST be fixed before modifying involved files.
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts 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 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 > stores/crafting-equipment-tick.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/pipelines/equipment-crafting.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/golem-combat-actions.ts > stores/golem-combat-helpers.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/attunementStore.ts > stores/combatStore.ts > stores/combat-descent-actions.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 > stores/non-combat-room-actions.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 ## How to fix
1. Identify which import in the chain can be extracted to a shared types/utils file. 1. Identify which import in the chain can be extracted to a shared types/utils file.
+3 -12
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_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.", "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." "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", "stores/craftingStore.types.ts",
"types.ts" "types.ts"
], ],
"crafting-actions/disenchant-actions.ts": [
"stores/craftingStore.types.ts",
"stores/manaStore.ts"
],
"crafting-actions/equipment-actions.ts": [ "crafting-actions/equipment-actions.ts": [
"crafting-utils.ts", "crafting-utils.ts",
"stores/craftingStore.types.ts", "stores/craftingStore.types.ts",
@@ -164,7 +160,6 @@
"crafting-actions/computed-getters.ts", "crafting-actions/computed-getters.ts",
"crafting-actions/crafting-equipment-actions.ts", "crafting-actions/crafting-equipment-actions.ts",
"crafting-actions/design-actions.ts", "crafting-actions/design-actions.ts",
"crafting-actions/disenchant-actions.ts",
"crafting-actions/equipment-actions.ts", "crafting-actions/equipment-actions.ts",
"crafting-actions/preparation-actions.ts" "crafting-actions/preparation-actions.ts"
], ],
@@ -669,20 +664,16 @@
"stores/craftingStore.ts": [ "stores/craftingStore.ts": [
"crafting-actions/application-actions.ts", "crafting-actions/application-actions.ts",
"crafting-actions/crafting-material-actions.ts", "crafting-actions/crafting-material-actions.ts",
"crafting-actions/design-actions.ts",
"crafting-actions/equipment-actions.ts", "crafting-actions/equipment-actions.ts",
"crafting-actions/preparation-actions.ts", "crafting-actions/preparation-actions.ts",
"crafting-design.ts", "stores/attunementStore.ts",
"crafting-utils.ts",
"effects/discipline-effects.ts",
"effects/special-effects.ts",
"effects/upgrade-effects.ts",
"stores/combatStore.ts", "stores/combatStore.ts",
"stores/crafting-equipment-tick.ts", "stores/crafting-equipment-tick.ts",
"stores/crafting-initial-state.ts", "stores/crafting-initial-state.ts",
"stores/craftingStore.types.ts", "stores/craftingStore.types.ts",
"stores/manaStore.ts", "stores/manaStore.ts",
"stores/pipelines/equipment-crafting.ts", "stores/pipelines/equipment-crafting.ts",
"stores/uiStore.ts",
"types/equipmentSlot.ts", "types/equipmentSlot.ts",
"utils/result.ts", "utils/result.ts",
"utils/safe-persist.ts" "utils/safe-persist.ts"
+1
View File
@@ -209,6 +209,7 @@ Mana-Loop/
│ │ │ │ ├── bug-377-mana-auto-unlock.test.ts │ │ │ │ ├── bug-377-mana-auto-unlock.test.ts
│ │ │ │ ├── bug-fixes.test.ts │ │ │ │ ├── bug-fixes.test.ts
│ │ │ │ ├── combat-actions.test.ts │ │ │ │ ├── combat-actions.test.ts
│ │ │ │ ├── combat-reset-channel-stats.test.ts
│ │ │ │ ├── combat-utils.test.ts │ │ │ │ ├── combat-utils.test.ts
│ │ │ │ ├── computed-stats.test.ts │ │ │ │ ├── computed-stats.test.ts
│ │ │ │ ├── conversion-pause-bug-regression.test.ts │ │ │ │ ├── conversion-pause-bug-regression.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);
});
});
+4 -3
View File
@@ -7,6 +7,7 @@ import { getFloorMaxHP } from '../utils';
import { generateSpireFloorState, getRoomsForFloor } from '../utils/spire-utils'; import { generateSpireFloorState, getRoomsForFloor } from '../utils/spire-utils';
import { makeInitialSpells } from './combat-actions'; import { makeInitialSpells } from './combat-actions';
import type { CombatState } from './combat-state.types'; import type { CombatState } from './combat-state.types';
import { computeChannelStats } from './combat-channel';
export function createDefaultCombatState( export function createDefaultCombatState(
startFloor: number, startFloor: number,
@@ -64,10 +65,10 @@ export function createDefaultCombatState(
totalDamageDealt: 0, totalDamageDealt: 0,
totalCraftsCompleted: 0, totalCraftsCompleted: 0,
// Transference Channel defaults // Transference Channel defaults (discipline-aware)
isChanneling: false, isChanneling: false,
channelSpeedMultiplier: 1.5, channelSpeedMultiplier: computeChannelStats().speedMultiplier,
channelDrainRate: 0.08, channelDrainRate: computeChannelStats().drainRate,
// Room Enchantments defaults // Room Enchantments defaults
lastRoomCoverage: 0, lastRoomCoverage: 0,