fix: createDefaultCombatState now uses discipline-aware channel stats
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m25s
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:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user