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:
@@ -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