test: add store action and cross-store tick integration tests; fix pact ritual double-counting bug
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m16s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m16s
- Add test-setup.ts: shared test environment setup helper for tick integration tests - Add combat-store.test.ts (24 tests): setCurrentFloor, advanceFloor, setFloorHP, setMaxFloorReached, setAction, setSpell, setCastProgress, learnSpell, setSpellState, debugSetFloor, resetFloorHP, resetCombat, climbDownFloor, exitSpireMode - Add mana-store.test.ts (36 tests): setRawMana, addRawMana, spendRawMana, gatherMana, convertMana, unlockElement, addElementMana, spendElementMana, craftComposite, processConvertAction, resetMana, meditation ticks, setElementMax - Add tick-integration.test.ts (19 tests): time progression, mana regeneration, incursion penalty, meditation, loop end, paused/game over states - Add tick-integration-pact.test.ts (9 tests): victory condition, pact ritual progress, multiple ticks accumulation - Add tick-debug.test.ts (3 debug tests): regen trace, pact ritual trace, persist leak check - Fix bug in gameStore.ts: updatePactRitualProgress was called with absolute newProgress instead of incremental HOURS_PER_TICK, causing exponential progress accumulation - All 422 tests pass (18 test files), all files under 400-line limit
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-05-20T10:36:04.388Z
|
||||
Generated: 2026-05-20T11:01:19.491Z
|
||||
Found: 1 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
|
||||
1. Processed 125 files (1.3s) (4 warnings)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-05-20T10:36:02.953Z",
|
||||
"generated": "2026-05-20T11:01:18.069Z",
|
||||
"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."
|
||||
},
|
||||
|
||||
@@ -190,15 +190,23 @@ Mana-Loop/
|
||||
│ │ │ ├── store-method-tests/
|
||||
│ │ │ ├── achievements.test.ts
|
||||
│ │ │ ├── bug-fixes.test.ts
|
||||
│ │ │ ├── combat-store.test.ts
|
||||
│ │ │ ├── combat-utils.test.ts
|
||||
│ │ │ ├── computed-stats.test.ts
|
||||
│ │ │ ├── discipline-math.test.ts
|
||||
│ │ │ ├── discipline-store.test.ts
|
||||
│ │ │ ├── enemy-generator.test.ts
|
||||
│ │ │ ├── floor-utils.test.ts
|
||||
│ │ │ ├── formatting.test.ts
|
||||
│ │ │ ├── mana-store.test.ts
|
||||
│ │ │ ├── mana-utils.test.ts
|
||||
│ │ │ ├── prestige-store.test.ts
|
||||
│ │ │ ├── regression-fixes.test.ts
|
||||
│ │ │ └── spire-utils.test.ts
|
||||
│ │ │ ├── spire-utils.test.ts
|
||||
│ │ │ ├── test-setup.ts
|
||||
│ │ │ ├── tick-debug.test.ts
|
||||
│ │ │ ├── tick-integration-pact.test.ts
|
||||
│ │ │ └── tick-integration.test.ts
|
||||
│ │ ├── constants/
|
||||
│ │ │ ├── spells-modules/
|
||||
│ │ │ │ ├── advanced-spells.ts
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { getFloorMaxHP } from '../utils';
|
||||
|
||||
beforeEach(() => {
|
||||
useCombatStore.setState({
|
||||
currentFloor: 1,
|
||||
floorHP: getFloorMaxHP(1),
|
||||
floorMaxHP: getFloorMaxHP(1),
|
||||
maxFloorReached: 1,
|
||||
activeSpell: 'manaBolt',
|
||||
currentAction: 'meditate',
|
||||
castProgress: 0,
|
||||
spireMode: false,
|
||||
clearedFloors: {},
|
||||
climbDirection: null,
|
||||
isDescending: false,
|
||||
spells: {
|
||||
manaBolt: { learned: true, level: 1, studyProgress: 0 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('CombatStore', () => {
|
||||
describe('setCurrentFloor', () => {
|
||||
it('should set current floor and reset HP', () => {
|
||||
useCombatStore.getState().setCurrentFloor(5);
|
||||
const state = useCombatStore.getState();
|
||||
expect(state.currentFloor).toBe(5);
|
||||
expect(state.floorHP).toBe(state.floorMaxHP);
|
||||
});
|
||||
});
|
||||
|
||||
describe('advanceFloor', () => {
|
||||
it('should advance to next floor', () => {
|
||||
useCombatStore.getState().advanceFloor();
|
||||
expect(useCombatStore.getState().currentFloor).toBe(2);
|
||||
});
|
||||
it('should reset cast progress on advance', () => {
|
||||
useCombatStore.setState({ castProgress: 0.5 });
|
||||
useCombatStore.getState().advanceFloor();
|
||||
expect(useCombatStore.getState().castProgress).toBe(0);
|
||||
});
|
||||
it('should update max floor reached', () => {
|
||||
useCombatStore.setState({ currentFloor: 3, maxFloorReached: 3 });
|
||||
useCombatStore.getState().advanceFloor();
|
||||
expect(useCombatStore.getState().maxFloorReached).toBe(4);
|
||||
});
|
||||
it('should cap at floor 100', () => {
|
||||
useCombatStore.setState({ currentFloor: 100 });
|
||||
useCombatStore.getState().advanceFloor();
|
||||
expect(useCombatStore.getState().currentFloor).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setFloorHP', () => {
|
||||
it('should set floor HP', () => {
|
||||
useCombatStore.getState().setFloorHP(50);
|
||||
expect(useCombatStore.getState().floorHP).toBe(50);
|
||||
});
|
||||
it('should clamp negative HP to zero', () => {
|
||||
useCombatStore.getState().setFloorHP(-10);
|
||||
expect(useCombatStore.getState().floorHP).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMaxFloorReached', () => {
|
||||
it('should set max floor reached', () => {
|
||||
useCombatStore.getState().setMaxFloorReached(10);
|
||||
expect(useCombatStore.getState().maxFloorReached).toBe(10);
|
||||
});
|
||||
it('should not decrease max floor reached', () => {
|
||||
useCombatStore.setState({ maxFloorReached: 10 });
|
||||
useCombatStore.getState().setMaxFloorReached(5);
|
||||
expect(useCombatStore.getState().maxFloorReached).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAction', () => {
|
||||
it('should set current action', () => {
|
||||
useCombatStore.getState().setAction('climb');
|
||||
expect(useCombatStore.getState().currentAction).toBe('climb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSpell', () => {
|
||||
it('should set active spell when learned', () => {
|
||||
useCombatStore.getState().setSpell('manaBolt');
|
||||
expect(useCombatStore.getState().activeSpell).toBe('manaBolt');
|
||||
});
|
||||
it('should not set spell when not learned', () => {
|
||||
useCombatStore.getState().setSpell('fireball');
|
||||
expect(useCombatStore.getState().activeSpell).toBe('manaBolt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCastProgress', () => {
|
||||
it('should set cast progress', () => {
|
||||
useCombatStore.getState().setCastProgress(0.75);
|
||||
expect(useCombatStore.getState().castProgress).toBe(0.75);
|
||||
});
|
||||
});
|
||||
|
||||
describe('learnSpell', () => {
|
||||
it('should learn a new spell', () => {
|
||||
useCombatStore.getState().learnSpell('fireball');
|
||||
const spell = useCombatStore.getState().spells['fireball'];
|
||||
expect(spell.learned).toBe(true);
|
||||
expect(spell.level).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSpellState', () => {
|
||||
it('should update spell state', () => {
|
||||
useCombatStore.getState().setSpellState('manaBolt', { level: 5 });
|
||||
expect(useCombatStore.getState().spells.manaBolt.level).toBe(5);
|
||||
});
|
||||
it('should create spell if not exists', () => {
|
||||
useCombatStore.getState().setSpellState('fireball', { learned: true, level: 3 });
|
||||
const spell = useCombatStore.getState().spells['fireball'];
|
||||
expect(spell.learned).toBe(true);
|
||||
expect(spell.level).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('debugSetFloor', () => {
|
||||
it('should set floor and reset HP', () => {
|
||||
useCombatStore.getState().debugSetFloor(10);
|
||||
const state = useCombatStore.getState();
|
||||
expect(state.currentFloor).toBe(10);
|
||||
expect(state.floorHP).toBe(state.floorMaxHP);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetFloorHP', () => {
|
||||
it('should reset floor HP to max', () => {
|
||||
useCombatStore.setState({ floorHP: 1 });
|
||||
useCombatStore.getState().resetFloorHP();
|
||||
expect(useCombatStore.getState().floorHP).toBe(useCombatStore.getState().floorMaxHP);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetCombat', () => {
|
||||
it('should reset combat to starting state', () => {
|
||||
useCombatStore.setState({ currentFloor: 50, castProgress: 0.9 });
|
||||
useCombatStore.getState().resetCombat(1);
|
||||
const state = useCombatStore.getState();
|
||||
expect(state.currentFloor).toBe(1);
|
||||
expect(state.castProgress).toBe(0);
|
||||
expect(state.activeSpell).toBe('manaBolt');
|
||||
});
|
||||
it('should keep specified spells', () => {
|
||||
useCombatStore.getState().resetCombat(1, ['fireball']);
|
||||
expect(useCombatStore.getState().spells['fireball'].learned).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('climbDownFloor', () => {
|
||||
it('should descend one floor', () => {
|
||||
useCombatStore.setState({ currentFloor: 5 });
|
||||
useCombatStore.getState().climbDownFloor();
|
||||
expect(useCombatStore.getState().currentFloor).toBe(4);
|
||||
});
|
||||
it('should not go below floor 1', () => {
|
||||
useCombatStore.setState({ currentFloor: 1 });
|
||||
useCombatStore.getState().climbDownFloor();
|
||||
expect(useCombatStore.getState().currentFloor).toBe(1);
|
||||
});
|
||||
it('should reset HP and cast progress on descent', () => {
|
||||
useCombatStore.setState({ currentFloor: 5, castProgress: 0.5, floorHP: 1 });
|
||||
useCombatStore.getState().climbDownFloor();
|
||||
const state = useCombatStore.getState();
|
||||
expect(state.castProgress).toBe(0);
|
||||
expect(state.floorHP).toBe(state.floorMaxHP);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exitSpireMode', () => {
|
||||
it('should exit spire mode and reset state', () => {
|
||||
useCombatStore.setState({ spireMode: true, climbDirection: 'up', isDescending: false });
|
||||
useCombatStore.getState().exitSpireMode();
|
||||
const state = useCombatStore.getState();
|
||||
expect(state.spireMode).toBe(false);
|
||||
expect(state.climbDirection).toBeNull();
|
||||
expect(state.isDescending).toBe(false);
|
||||
expect(state.currentAction).toBe('meditate');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||
|
||||
beforeEach(() => {
|
||||
useDisciplineStore.setState({
|
||||
disciplines: {}, activeIds: [], concurrentLimit: 1, totalXP: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('DisciplineStore', () => {
|
||||
describe('activate', () => {
|
||||
it('should activate a discipline', () => {
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
const state = useDisciplineStore.getState();
|
||||
expect(state.activeIds).toContain('raw-mastery');
|
||||
expect(state.disciplines['raw-mastery']).toBeDefined();
|
||||
expect(state.disciplines['raw-mastery'].paused).toBe(false);
|
||||
});
|
||||
it('should not activate unknown discipline', () => {
|
||||
useDisciplineStore.getState().activate('nonexistent');
|
||||
expect(useDisciplineStore.getState().activeIds).toHaveLength(0);
|
||||
});
|
||||
it('should not activate same discipline twice', () => {
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
expect(useDisciplineStore.getState().activeIds).toHaveLength(1);
|
||||
});
|
||||
it('should respect concurrent limit', () => {
|
||||
useDisciplineStore.setState({ concurrentLimit: 1 });
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
useDisciplineStore.getState().activate('elemental-attunement');
|
||||
expect(useDisciplineStore.getState().activeIds).toHaveLength(1);
|
||||
});
|
||||
it('should allow activation when under limit', () => {
|
||||
useDisciplineStore.setState({ concurrentLimit: 2 });
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
useDisciplineStore.getState().activate('elemental-attunement');
|
||||
expect(useDisciplineStore.getState().activeIds).toHaveLength(2);
|
||||
});
|
||||
it('should activate element discipline on first call (no prior state)', () => {
|
||||
useDisciplineStore.getState().activate('elemental-attunement', { elements: {} });
|
||||
expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement');
|
||||
});
|
||||
it('should activate element discipline with matching unlocked element', () => {
|
||||
useDisciplineStore.getState().activate('elemental-attunement', {
|
||||
elements: { fire: { unlocked: true } },
|
||||
});
|
||||
expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement');
|
||||
});
|
||||
it('should not re-activate paused discipline', () => {
|
||||
useDisciplineStore.getState().activate('elemental-attunement', {
|
||||
elements: { fire: { unlocked: true, current: 100 } },
|
||||
});
|
||||
useDisciplineStore.setState({
|
||||
disciplines: {
|
||||
'elemental-attunement': { id: 'elemental-attunement', xp: 200, paused: true },
|
||||
},
|
||||
});
|
||||
useDisciplineStore.getState().activate('elemental-attunement', {
|
||||
elements: { fire: { unlocked: true, current: 100 } },
|
||||
});
|
||||
expect(useDisciplineStore.getState().disciplines['elemental-attunement'].paused).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deactivate', () => {
|
||||
it('should deactivate a discipline', () => {
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
useDisciplineStore.getState().deactivate('raw-mastery');
|
||||
expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery');
|
||||
});
|
||||
it('should not error on non-active discipline', () => {
|
||||
useDisciplineStore.getState().deactivate('raw-mastery');
|
||||
expect(useDisciplineStore.getState().activeIds).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processTick', () => {
|
||||
it('should accrue XP for active discipline', () => {
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
useDisciplineStore.getState().processTick({ rawMana: 100, elements: {} });
|
||||
const state = useDisciplineStore.getState();
|
||||
expect(state.disciplines['raw-mastery'].xp).toBe(1);
|
||||
expect(state.totalXP).toBe(1);
|
||||
});
|
||||
it('should drain raw mana', () => {
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
const result = useDisciplineStore.getState().processTick({ rawMana: 100, elements: {} });
|
||||
expect(result.rawMana).toBeLessThan(100);
|
||||
});
|
||||
it('should drain element mana for element discipline', () => {
|
||||
useDisciplineStore.setState({ concurrentLimit: 2 });
|
||||
useDisciplineStore.getState().activate('elemental-attunement', {
|
||||
elements: { fire: { unlocked: true } },
|
||||
});
|
||||
const result = useDisciplineStore.getState().processTick({
|
||||
rawMana: 0, elements: { fire: { current: 100 } },
|
||||
});
|
||||
expect(result.elements.fire.current).toBeLessThan(100);
|
||||
});
|
||||
it('should pause discipline when insufficient mana', () => {
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
useDisciplineStore.getState().processTick({ rawMana: 0, elements: {} });
|
||||
expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(true);
|
||||
});
|
||||
it('should not accrue XP when paused', () => {
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
useDisciplineStore.setState({
|
||||
disciplines: { 'raw-mastery': { id: 'raw-mastery', xp: 5, paused: true } },
|
||||
});
|
||||
useDisciplineStore.getState().processTick({ rawMana: 100, elements: {} });
|
||||
expect(useDisciplineStore.getState().disciplines['raw-mastery'].xp).toBe(5);
|
||||
});
|
||||
it('should process multiple active disciplines', () => {
|
||||
useDisciplineStore.setState({ concurrentLimit: 2 });
|
||||
useDisciplineStore.getState().activate('raw-mastery');
|
||||
useDisciplineStore.getState().activate('elemental-attunement', {
|
||||
elements: { fire: { unlocked: true } },
|
||||
});
|
||||
useDisciplineStore.getState().processTick({
|
||||
rawMana: 100, elements: { fire: { current: 100 } },
|
||||
});
|
||||
expect(useDisciplineStore.getState().totalXP).toBe(2);
|
||||
});
|
||||
it('should increase concurrent limit at XP thresholds', () => {
|
||||
useDisciplineStore.setState({
|
||||
concurrentLimit: 1, totalXP: 499, activeIds: ['raw-mastery'],
|
||||
disciplines: { 'raw-mastery': { id: 'raw-mastery', xp: 499, paused: false } },
|
||||
});
|
||||
useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} });
|
||||
expect(useDisciplineStore.getState().totalXP).toBe(500);
|
||||
expect(useDisciplineStore.getState().concurrentLimit).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useManaStore } from '../stores/manaStore';
|
||||
import { MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants';
|
||||
import type { ElementState } from '../types';
|
||||
|
||||
function makeElements(): Record<string, ElementState> {
|
||||
const keys = [
|
||||
'fire','water','air','earth','light','dark','death',
|
||||
'transference','metal','sand','lightning','crystal','stellar','void',
|
||||
];
|
||||
const elements: Record<string, ElementState> = {};
|
||||
for (const k of keys) {
|
||||
elements[k] = { current: 0, max: 10, unlocked: BASE_UNLOCKED_ELEMENTS.includes(k) };
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
useManaStore.setState({
|
||||
rawMana: 10, meditateTicks: 0, totalManaGathered: 0, elements: makeElements(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('ManaStore', () => {
|
||||
describe('setRawMana', () => {
|
||||
it('should set raw mana to the given amount', () => {
|
||||
useManaStore.getState().setRawMana(50);
|
||||
expect(useManaStore.getState().rawMana).toBe(50);
|
||||
});
|
||||
it('should clamp negative values to zero', () => {
|
||||
useManaStore.getState().setRawMana(-10);
|
||||
expect(useManaStore.getState().rawMana).toBe(0);
|
||||
});
|
||||
it('should allow setting to zero', () => {
|
||||
useManaStore.getState().setRawMana(0);
|
||||
expect(useManaStore.getState().rawMana).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRawMana', () => {
|
||||
it('should add raw mana up to max', () => {
|
||||
useManaStore.getState().addRawMana(40, 100);
|
||||
expect(useManaStore.getState().rawMana).toBe(50);
|
||||
});
|
||||
it('should cap at max mana', () => {
|
||||
useManaStore.getState().addRawMana(200, 100);
|
||||
expect(useManaStore.getState().rawMana).toBe(100);
|
||||
});
|
||||
it('should track total mana gathered', () => {
|
||||
useManaStore.getState().addRawMana(30, 100);
|
||||
useManaStore.getState().addRawMana(20, 100);
|
||||
expect(useManaStore.getState().totalManaGathered).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('spendRawMana', () => {
|
||||
it('should return true and deduct when sufficient mana', () => {
|
||||
useManaStore.getState().setRawMana(50);
|
||||
expect(useManaStore.getState().spendRawMana(20)).toBe(true);
|
||||
expect(useManaStore.getState().rawMana).toBe(30);
|
||||
});
|
||||
it('should return false when insufficient mana', () => {
|
||||
useManaStore.getState().setRawMana(5);
|
||||
expect(useManaStore.getState().spendRawMana(20)).toBe(false);
|
||||
expect(useManaStore.getState().rawMana).toBe(5);
|
||||
});
|
||||
it('should allow spending exact amount', () => {
|
||||
useManaStore.getState().setRawMana(10);
|
||||
expect(useManaStore.getState().spendRawMana(10)).toBe(true);
|
||||
expect(useManaStore.getState().rawMana).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gatherMana', () => {
|
||||
it('should add mana and track total gathered', () => {
|
||||
useManaStore.getState().gatherMana(5, 100);
|
||||
expect(useManaStore.getState().rawMana).toBe(15);
|
||||
expect(useManaStore.getState().totalManaGathered).toBe(5);
|
||||
});
|
||||
it('should cap at max mana', () => {
|
||||
useManaStore.getState().gatherMana(200, 50);
|
||||
expect(useManaStore.getState().rawMana).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertMana', () => {
|
||||
beforeEach(() => {
|
||||
useManaStore.setState({
|
||||
rawMana: 1000,
|
||||
elements: { ...makeElements(), fire: { current: 0, max: 10, unlocked: true } },
|
||||
});
|
||||
});
|
||||
it('should convert raw mana to element mana', () => {
|
||||
const result = useManaStore.getState().convertMana('fire', 5);
|
||||
expect(result).toBe(true);
|
||||
expect(useManaStore.getState().elements.fire.current).toBe(5);
|
||||
expect(useManaStore.getState().rawMana).toBe(1000 - 5 * MANA_PER_ELEMENT);
|
||||
});
|
||||
it('should return false for locked element', () => {
|
||||
expect(useManaStore.getState().convertMana('water', 1)).toBe(false);
|
||||
});
|
||||
it('should return false when insufficient raw mana for full amount', () => {
|
||||
useManaStore.setState({ rawMana: 50 });
|
||||
expect(useManaStore.getState().convertMana('fire', 1)).toBe(false);
|
||||
});
|
||||
it('should return false when element is at max', () => {
|
||||
useManaStore.setState({
|
||||
elements: { ...makeElements(), fire: { current: 10, max: 10, unlocked: true } },
|
||||
});
|
||||
expect(useManaStore.getState().convertMana('fire', 1)).toBe(false);
|
||||
});
|
||||
it('should return false when raw mana < cost for requested amount', () => {
|
||||
useManaStore.setState({ rawMana: 250 });
|
||||
expect(useManaStore.getState().convertMana('fire', 5)).toBe(false);
|
||||
});
|
||||
it('should succeed when raw mana covers full requested amount', () => {
|
||||
useManaStore.setState({ rawMana: 500 });
|
||||
const result = useManaStore.getState().convertMana('fire', 5);
|
||||
expect(result).toBe(true);
|
||||
expect(useManaStore.getState().elements.fire.current).toBe(5);
|
||||
expect(useManaStore.getState().rawMana).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unlockElement', () => {
|
||||
it('should unlock element and deduct cost', () => {
|
||||
useManaStore.setState({ rawMana: 100 });
|
||||
expect(useManaStore.getState().unlockElement('fire', 50)).toBe(true);
|
||||
expect(useManaStore.getState().elements.fire.unlocked).toBe(true);
|
||||
expect(useManaStore.getState().rawMana).toBe(50);
|
||||
});
|
||||
it('should return false when already unlocked', () => {
|
||||
useManaStore.setState({ rawMana: 100 });
|
||||
useManaStore.getState().unlockElement('transference', 10);
|
||||
expect(useManaStore.getState().unlockElement('transference', 10)).toBe(false);
|
||||
});
|
||||
it('should return false when insufficient mana', () => {
|
||||
useManaStore.setState({ rawMana: 10 });
|
||||
expect(useManaStore.getState().unlockElement('fire', 50)).toBe(false);
|
||||
expect(useManaStore.getState().elements.fire.unlocked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addElementMana', () => {
|
||||
it('should add element mana up to max', () => {
|
||||
useManaStore.setState({
|
||||
elements: { ...makeElements(), fire: { current: 0, max: 10, unlocked: true } },
|
||||
});
|
||||
useManaStore.getState().addElementMana('fire', 5, 10);
|
||||
expect(useManaStore.getState().elements.fire.current).toBe(5);
|
||||
});
|
||||
it('should cap at max', () => {
|
||||
useManaStore.setState({
|
||||
elements: { ...makeElements(), fire: { current: 8, max: 10, unlocked: true } },
|
||||
});
|
||||
useManaStore.getState().addElementMana('fire', 5, 10);
|
||||
expect(useManaStore.getState().elements.fire.current).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('spendElementMana', () => {
|
||||
it('should spend element mana when sufficient', () => {
|
||||
useManaStore.setState({
|
||||
elements: { ...makeElements(), fire: { current: 10, max: 10, unlocked: true } },
|
||||
});
|
||||
expect(useManaStore.getState().spendElementMana('fire', 5)).toBe(true);
|
||||
expect(useManaStore.getState().elements.fire.current).toBe(5);
|
||||
});
|
||||
it('should return false when insufficient', () => {
|
||||
useManaStore.setState({
|
||||
elements: { ...makeElements(), fire: { current: 3, max: 10, unlocked: true } },
|
||||
});
|
||||
expect(useManaStore.getState().spendElementMana('fire', 5)).toBe(false);
|
||||
expect(useManaStore.getState().elements.fire.current).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('craftComposite', () => {
|
||||
beforeEach(() => {
|
||||
useManaStore.setState({
|
||||
elements: {
|
||||
...makeElements(),
|
||||
fire: { current: 5, max: 10, unlocked: true },
|
||||
earth: { current: 5, max: 10, unlocked: true },
|
||||
metal: { current: 0, max: 10, unlocked: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should craft composite from recipe', () => {
|
||||
const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']);
|
||||
expect(result).toBe(true);
|
||||
expect(useManaStore.getState().elements.metal.current).toBe(1);
|
||||
expect(useManaStore.getState().elements.metal.unlocked).toBe(true);
|
||||
expect(useManaStore.getState().elements.fire.current).toBe(4);
|
||||
expect(useManaStore.getState().elements.earth.current).toBe(4);
|
||||
});
|
||||
it('should return false when missing ingredients', () => {
|
||||
useManaStore.setState({
|
||||
elements: {
|
||||
...makeElements(),
|
||||
fire: { current: 0, max: 10, unlocked: true },
|
||||
earth: { current: 5, max: 10, unlocked: true },
|
||||
metal: { current: 0, max: 10, unlocked: false },
|
||||
},
|
||||
});
|
||||
expect(useManaStore.getState().craftComposite('metal', ['fire', 'earth'])).toBe(false);
|
||||
});
|
||||
it('should handle multiple different ingredients in recipe', () => {
|
||||
useManaStore.setState({
|
||||
elements: {
|
||||
...makeElements(),
|
||||
sand: { current: 0, max: 10, unlocked: false },
|
||||
earth: { current: 5, max: 10, unlocked: true },
|
||||
water: { current: 5, max: 10, unlocked: true },
|
||||
},
|
||||
});
|
||||
const result = useManaStore.getState().craftComposite('sand', ['earth', 'water']);
|
||||
expect(result).toBe(true);
|
||||
expect(useManaStore.getState().elements.sand.current).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processConvertAction', () => {
|
||||
it('should return null when no unlocked elements with room', () => {
|
||||
expect(useManaStore.getState().processConvertAction(50)).toBeNull();
|
||||
});
|
||||
it('should return null when raw mana < 100', () => {
|
||||
useManaStore.setState({
|
||||
elements: { ...makeElements(), fire: { current: 0, max: 10, unlocked: true } },
|
||||
});
|
||||
expect(useManaStore.getState().processConvertAction(50)).toBeNull();
|
||||
});
|
||||
it('should auto-convert raw to element mana', () => {
|
||||
useManaStore.setState({
|
||||
elements: { ...makeElements(), fire: { current: 0, max: 10, unlocked: true } },
|
||||
});
|
||||
const result = useManaStore.getState().processConvertAction(500);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.elements.fire.current).toBe(5);
|
||||
expect(result!.rawMana).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetMana', () => {
|
||||
it('should reset to initial state with default upgrades', () => {
|
||||
useManaStore.setState({ rawMana: 500, meditateTicks: 100, totalManaGathered: 999 });
|
||||
useManaStore.getState().resetMana({});
|
||||
expect(useManaStore.getState().rawMana).toBe(10);
|
||||
expect(useManaStore.getState().meditateTicks).toBe(0);
|
||||
expect(useManaStore.getState().totalManaGathered).toBe(0);
|
||||
});
|
||||
it('should apply prestige upgrades', () => {
|
||||
useManaStore.getState().resetMana({ manaStart: 2, elemMax: 1, elemStart: 1 });
|
||||
expect(useManaStore.getState().rawMana).toBe(30);
|
||||
for (const k of BASE_UNLOCKED_ELEMENTS) {
|
||||
expect(useManaStore.getState().elements[k].current).toBe(5);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('meditation ticks', () => {
|
||||
it('should set meditate ticks', () => {
|
||||
useManaStore.getState().setMeditateTicks(42);
|
||||
expect(useManaStore.getState().meditateTicks).toBe(42);
|
||||
});
|
||||
it('should increment meditate ticks', () => {
|
||||
useManaStore.getState().incrementMeditateTicks();
|
||||
useManaStore.getState().incrementMeditateTicks();
|
||||
expect(useManaStore.getState().meditateTicks).toBe(2);
|
||||
});
|
||||
it('should reset meditate ticks', () => {
|
||||
useManaStore.getState().setMeditateTicks(100);
|
||||
useManaStore.getState().resetMeditateTicks();
|
||||
expect(useManaStore.getState().meditateTicks).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setElementMax', () => {
|
||||
it('should set max for all elements', () => {
|
||||
useManaStore.getState().setElementMax(50);
|
||||
for (const key of Object.keys(useManaStore.getState().elements)) {
|
||||
expect(useManaStore.getState().elements[key].max).toBe(50);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||
|
||||
beforeEach(() => {
|
||||
usePrestigeStore.setState({
|
||||
loopCount: 0, insight: 0, totalInsight: 0, loopInsight: 0,
|
||||
prestigeUpgrades: {}, memorySlots: 3, pactSlots: 1,
|
||||
memories: [], defeatedGuardians: [], signedPacts: [],
|
||||
signedPactDetails: {}, pactRitualFloor: null, pactRitualProgress: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('PrestigeStore', () => {
|
||||
describe('doPrestige', () => {
|
||||
it('should purchase upgrade when sufficient insight', () => {
|
||||
usePrestigeStore.setState({ insight: 500 });
|
||||
expect(usePrestigeStore.getState().doPrestige('manaWell')).toBe(true);
|
||||
expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1);
|
||||
expect(usePrestigeStore.getState().insight).toBe(0);
|
||||
});
|
||||
it('should return false when insufficient insight', () => {
|
||||
usePrestigeStore.setState({ insight: 100 });
|
||||
expect(usePrestigeStore.getState().doPrestige('manaWell')).toBe(false);
|
||||
});
|
||||
it('should return false for invalid upgrade id', () => {
|
||||
usePrestigeStore.setState({ insight: 9999 });
|
||||
expect(usePrestigeStore.getState().doPrestige('nonexistent')).toBe(false);
|
||||
});
|
||||
it('should return false when at max level', () => {
|
||||
usePrestigeStore.setState({ insight: 5000, prestigeUpgrades: { manaWell: 5 } });
|
||||
expect(usePrestigeStore.getState().doPrestige('manaWell')).toBe(false);
|
||||
});
|
||||
it('should increase memorySlots for deepMemory', () => {
|
||||
usePrestigeStore.setState({ insight: 1000 });
|
||||
usePrestigeStore.getState().doPrestige('deepMemory');
|
||||
expect(usePrestigeStore.getState().memorySlots).toBe(4);
|
||||
});
|
||||
it('should allow purchasing same upgrade multiple times', () => {
|
||||
usePrestigeStore.setState({ insight: 1500 });
|
||||
usePrestigeStore.getState().doPrestige('manaWell');
|
||||
usePrestigeStore.getState().doPrestige('manaWell');
|
||||
expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(2);
|
||||
expect(usePrestigeStore.getState().insight).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMemory', () => {
|
||||
it('should add a memory when slots available', () => {
|
||||
usePrestigeStore.getState().addMemory({ skillId: 'fire', level: 5 });
|
||||
expect(usePrestigeStore.getState().memories).toHaveLength(1);
|
||||
});
|
||||
it('should not add duplicate skillId', () => {
|
||||
usePrestigeStore.getState().addMemory({ skillId: 'fire', level: 5 });
|
||||
usePrestigeStore.getState().addMemory({ skillId: 'fire', level: 3 });
|
||||
expect(usePrestigeStore.getState().memories).toHaveLength(1);
|
||||
});
|
||||
it('should not exceed memory slots', () => {
|
||||
usePrestigeStore.setState({ memorySlots: 1 });
|
||||
usePrestigeStore.getState().addMemory({ skillId: 'fire', level: 1 });
|
||||
usePrestigeStore.getState().addMemory({ skillId: 'water', level: 1 });
|
||||
expect(usePrestigeStore.getState().memories).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMemory', () => {
|
||||
it('should remove memory by skillId', () => {
|
||||
usePrestigeStore.setState({
|
||||
memories: [{ skillId: 'fire', level: 5 }, { skillId: 'water', level: 3 }],
|
||||
});
|
||||
usePrestigeStore.getState().removeMemory('fire');
|
||||
expect(usePrestigeStore.getState().memories).toHaveLength(1);
|
||||
expect(usePrestigeStore.getState().memories[0].skillId).toBe('water');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearMemories', () => {
|
||||
it('should clear all memories', () => {
|
||||
usePrestigeStore.setState({
|
||||
memories: [{ skillId: 'fire', level: 5 }, { skillId: 'water', level: 3 }],
|
||||
});
|
||||
usePrestigeStore.getState().clearMemories();
|
||||
expect(usePrestigeStore.getState().memories).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startPactRitual', () => {
|
||||
it('should start ritual when conditions met', () => {
|
||||
usePrestigeStore.setState({ defeatedGuardians: [10] });
|
||||
expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(true);
|
||||
expect(usePrestigeStore.getState().pactRitualFloor).toBe(10);
|
||||
});
|
||||
it('should return false when guardian not defeated', () => {
|
||||
expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(false);
|
||||
});
|
||||
it('should return false when pact already signed', () => {
|
||||
usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [10] });
|
||||
expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(false);
|
||||
});
|
||||
it('should return false when insufficient raw mana', () => {
|
||||
usePrestigeStore.setState({ defeatedGuardians: [10] });
|
||||
expect(usePrestigeStore.getState().startPactRitual(10, 10)).toBe(false);
|
||||
});
|
||||
it('should return false when pact slots full', () => {
|
||||
usePrestigeStore.setState({ defeatedGuardians: [10, 20], signedPacts: [20], pactSlots: 1 });
|
||||
expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(false);
|
||||
});
|
||||
it('should return false when ritual already in progress', () => {
|
||||
usePrestigeStore.setState({ defeatedGuardians: [10], pactRitualFloor: 20 });
|
||||
expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelPactRitual', () => {
|
||||
it('should cancel ritual and reset progress', () => {
|
||||
usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 5 });
|
||||
usePrestigeStore.getState().cancelPactRitual();
|
||||
expect(usePrestigeStore.getState().pactRitualFloor).toBeNull();
|
||||
expect(usePrestigeStore.getState().pactRitualProgress).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completePactRitual', () => {
|
||||
it('should sign pact and remove from defeated', () => {
|
||||
usePrestigeStore.setState({ pactRitualFloor: 10, defeatedGuardians: [10], signedPacts: [] });
|
||||
const logs: string[] = [];
|
||||
usePrestigeStore.getState().completePactRitual((msg) => logs.push(msg));
|
||||
expect(usePrestigeStore.getState().signedPacts).toContain(10);
|
||||
expect(usePrestigeStore.getState().defeatedGuardians).not.toContain(10);
|
||||
expect(usePrestigeStore.getState().pactRitualFloor).toBeNull();
|
||||
expect(logs).toHaveLength(1);
|
||||
});
|
||||
it('should do nothing when no ritual in progress', () => {
|
||||
usePrestigeStore.setState({ pactRitualFloor: null });
|
||||
const logs: string[] = [];
|
||||
usePrestigeStore.getState().completePactRitual((msg) => logs.push(msg));
|
||||
expect(logs).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defeatGuardian', () => {
|
||||
it('should add guardian to defeated list', () => {
|
||||
usePrestigeStore.getState().defeatGuardian(10);
|
||||
expect(usePrestigeStore.getState().defeatedGuardians).toContain(10);
|
||||
});
|
||||
it('should not add if already defeated', () => {
|
||||
usePrestigeStore.setState({ defeatedGuardians: [10] });
|
||||
usePrestigeStore.getState().defeatGuardian(10);
|
||||
expect(usePrestigeStore.getState().defeatedGuardians).toHaveLength(1);
|
||||
});
|
||||
it('should not add if pact already signed', () => {
|
||||
usePrestigeStore.setState({ signedPacts: [10] });
|
||||
usePrestigeStore.getState().defeatGuardian(10);
|
||||
expect(usePrestigeStore.getState().defeatedGuardians).not.toContain(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSignedPact', () => {
|
||||
it('should add a signed pact', () => {
|
||||
usePrestigeStore.getState().addSignedPact(10);
|
||||
expect(usePrestigeStore.getState().signedPacts).toContain(10);
|
||||
});
|
||||
it('should not duplicate', () => {
|
||||
usePrestigeStore.setState({ signedPacts: [10] });
|
||||
usePrestigeStore.getState().addSignedPact(10);
|
||||
expect(usePrestigeStore.getState().signedPacts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePact', () => {
|
||||
it('should remove a signed pact', () => {
|
||||
usePrestigeStore.setState({ signedPacts: [10, 20] });
|
||||
usePrestigeStore.getState().removePact(10);
|
||||
expect(usePrestigeStore.getState().signedPacts).not.toContain(10);
|
||||
expect(usePrestigeStore.getState().signedPacts).toContain(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startNewLoop', () => {
|
||||
it('should increment loop count and add insight', () => {
|
||||
usePrestigeStore.setState({ insight: 100, totalInsight: 500 });
|
||||
usePrestigeStore.getState().startNewLoop(50);
|
||||
expect(usePrestigeStore.getState().loopCount).toBe(1);
|
||||
expect(usePrestigeStore.getState().insight).toBe(150);
|
||||
expect(usePrestigeStore.getState().totalInsight).toBe(550);
|
||||
});
|
||||
it('should reset loop-specific state', () => {
|
||||
usePrestigeStore.setState({
|
||||
defeatedGuardians: [10], signedPacts: [20],
|
||||
pactRitualFloor: 10, pactRitualProgress: 5,
|
||||
});
|
||||
usePrestigeStore.getState().startNewLoop(0);
|
||||
expect(usePrestigeStore.getState().defeatedGuardians).toHaveLength(0);
|
||||
expect(usePrestigeStore.getState().signedPacts).toHaveLength(0);
|
||||
expect(usePrestigeStore.getState().pactRitualFloor).toBeNull();
|
||||
expect(usePrestigeStore.getState().pactRitualProgress).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLoopInsight', () => {
|
||||
it('should set loop insight', () => {
|
||||
usePrestigeStore.getState().setLoopInsight(42);
|
||||
expect(usePrestigeStore.getState().loopInsight).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('incrementLoopCount', () => {
|
||||
it('should increment loop count', () => {
|
||||
usePrestigeStore.getState().incrementLoopCount();
|
||||
expect(usePrestigeStore.getState().loopCount).toBe(1);
|
||||
usePrestigeStore.getState().incrementLoopCount();
|
||||
expect(usePrestigeStore.getState().loopCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPrestigeForNewLoop', () => {
|
||||
it('should set insight, upgrades, memories, and slots', () => {
|
||||
usePrestigeStore.getState().resetPrestigeForNewLoop(
|
||||
500, { manaWell: 2 }, [{ skillId: 'fire', level: 3 }], 4,
|
||||
);
|
||||
const state = usePrestigeStore.getState();
|
||||
expect(state.insight).toBe(500);
|
||||
expect(state.prestigeUpgrades).toEqual({ manaWell: 2 });
|
||||
expect(state.memories).toHaveLength(1);
|
||||
expect(state.memorySlots).toBe(4);
|
||||
expect(state.defeatedGuardians).toHaveLength(0);
|
||||
expect(state.signedPacts).toHaveLength(0);
|
||||
expect(state.loopInsight).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { beforeEach } from 'vitest';
|
||||
import { useGameStore } from '../stores/gameStore';
|
||||
import { useManaStore, makeInitialElements } from '../stores/manaStore';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||
import { useUIStore } from '../stores/uiStore';
|
||||
import { useAttunementStore } from '../stores/attunementStore';
|
||||
import { useCraftingStore } from '../stores/craftingStore';
|
||||
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||
|
||||
// Clear all zustand persist localStorage keys to prevent cross-test contamination
|
||||
const _persistKeys = [
|
||||
'mana-loop-ui-storage',
|
||||
'mana-loop-game-storage',
|
||||
'mana-loop-mana',
|
||||
'mana-loop-combat',
|
||||
'mana-loop-prestige',
|
||||
'mana-loop-attunements',
|
||||
'mana-loop-crafting',
|
||||
'mana-loop-discipline-store',
|
||||
];
|
||||
|
||||
export function setupTickTestEnvironment(): void {
|
||||
for (const key of _persistKeys) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
useUIStore.setState({ paused: false, gameOver: false, victory: false, logs: [] });
|
||||
useGameStore.setState({ day: 1, hour: 0, incursionStrength: 0, containmentWards: 0, initialized: true });
|
||||
const elements = makeInitialElements(10, {});
|
||||
useManaStore.setState({ rawMana: 50, meditateTicks: 0, totalManaGathered: 0, elements });
|
||||
useCombatStore.setState({
|
||||
currentAction: 'meditate', currentFloor: 1, floorHP: 100, floorMaxHP: 100,
|
||||
maxFloorReached: 1, castProgress: 0, spireMode: false, climbDirection: null,
|
||||
isDescending: false, comboHitCount: 0, floorHitCount: 0,
|
||||
});
|
||||
usePrestigeStore.setState({
|
||||
prestigeUpgrades: {}, signedPacts: [], loopInsight: 0, defeatedGuardians: [],
|
||||
pactRitualFloor: null, pactRitualProgress: 0,
|
||||
});
|
||||
useAttunementStore.setState({
|
||||
attunements: { enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 } },
|
||||
});
|
||||
useDisciplineStore.setState({ disciplines: {}, activeIds: [], concurrentLimit: 2, totalXP: 0 });
|
||||
useCraftingStore.setState({
|
||||
designProgress: null, designProgress2: null, preparationProgress: null,
|
||||
applicationProgress: null, equipmentCraftingProgress: null,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useGameStore } from '../stores/gameStore';
|
||||
import { useManaStore, makeInitialElements } from '../stores/manaStore';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||
import { useUIStore } from '../stores/uiStore';
|
||||
import { useAttunementStore } from '../stores/attunementStore';
|
||||
import { useCraftingStore } from '../stores/craftingStore';
|
||||
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||
import { computeRegen } from '../utils/mana-utils';
|
||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||
import { getAttunementConversionRate, ATTUNEMENTS_DEF } from '../data/attunements';
|
||||
import { GUARDIANS, HOURS_PER_TICK } from '../constants';
|
||||
|
||||
beforeEach(() => {
|
||||
useUIStore.setState({ paused: false, gameOver: false, victory: false, logs: [] });
|
||||
useGameStore.setState({ day: 1, hour: 0, incursionStrength: 0, containmentWards: 0, initialized: true });
|
||||
const elements = makeInitialElements(10, {});
|
||||
useManaStore.setState({ rawMana: 50, meditateTicks: 0, totalManaGathered: 0, elements });
|
||||
useCombatStore.setState({
|
||||
currentAction: 'meditate', currentFloor: 1, floorHP: 100, floorMaxHP: 100,
|
||||
maxFloorReached: 1, castProgress: 0, spireMode: false, climbDirection: null,
|
||||
isDescending: false, comboHitCount: 0, floorHitCount: 0,
|
||||
});
|
||||
usePrestigeStore.setState({
|
||||
prestigeUpgrades: {}, signedPacts: [], loopInsight: 0, defeatedGuardians: [],
|
||||
pactRitualFloor: null, pactRitualProgress: 0,
|
||||
});
|
||||
useAttunementStore.setState({
|
||||
attunements: { enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 } },
|
||||
});
|
||||
useDisciplineStore.setState({ disciplines: {}, activeIds: [], concurrentLimit: 2, totalXP: 0 });
|
||||
useCraftingStore.setState({
|
||||
designProgress: null, designProgress2: null, preparationProgress: null,
|
||||
applicationProgress: null, equipmentCraftingProgress: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe('debug', () => {
|
||||
it('trace regen values', () => {
|
||||
// Measure actual regen empirically
|
||||
const before = useManaStore.getState().rawMana;
|
||||
useGameStore.getState().tick();
|
||||
const after = useManaStore.getState().rawMana;
|
||||
const transferenceBefore = useManaStore.getState().elements.transference?.current || 0;
|
||||
|
||||
// Do another tick to measure transference conversion
|
||||
useGameStore.getState().tick();
|
||||
const transferenceAfter = useManaStore.getState().elements.transference?.current || 0;
|
||||
|
||||
console.log('rawMana gain per tick:', after - before);
|
||||
console.log('transference gain per tick:', transferenceAfter - transferenceBefore);
|
||||
console.log('total regen (raw + conversion):', (after - before) + (transferenceAfter - transferenceBefore));
|
||||
|
||||
// Now compute what the tick does internally
|
||||
const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any);
|
||||
console.log('disciplineEffects bonuses:', disciplineEffects.bonuses);
|
||||
|
||||
const baseRegen = computeRegen(
|
||||
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {}, attunements: {} },
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
console.log('baseRegen (as computed by tick):', baseRegen);
|
||||
|
||||
let totalConversionPerHour = 0;
|
||||
const attState = useAttunementStore.getState();
|
||||
Object.entries(attState.attunements).forEach(([id, state]) => {
|
||||
if (!state.active) return;
|
||||
const def = ATTUNEMENTS_DEF[id];
|
||||
if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
|
||||
const scaledRate = getAttunementConversionRate(id, state.level || 1);
|
||||
totalConversionPerHour += scaledRate;
|
||||
});
|
||||
console.log('totalConversionPerHour:', totalConversionPerHour);
|
||||
|
||||
const effectiveRegen = baseRegen - totalConversionPerHour;
|
||||
console.log('effectiveRegen per hour:', effectiveRegen);
|
||||
console.log('expected raw gain per tick:', effectiveRegen * HOURS_PER_TICK);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('check pact ritual with floor 30 (pactTime=6)', () => {
|
||||
const g30 = GUARDIANS[30];
|
||||
console.log('Guardian 30 pactTime:', g30?.pactTime);
|
||||
|
||||
usePrestigeStore.setState({ pactRitualFloor: 30, pactRitualProgress: 0 });
|
||||
|
||||
// Do 50 ticks (2 hours)
|
||||
for (let i = 0; i < 50; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
console.log('After 50 ticks (2 hours):');
|
||||
console.log(' pactRitualProgress:', usePrestigeStore.getState().pactRitualProgress);
|
||||
console.log(' pactRitualFloor:', usePrestigeStore.getState().pactRitualFloor);
|
||||
console.log(' signedPacts:', usePrestigeStore.getState().signedPacts);
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('check if persist is leaking between tests', () => {
|
||||
// Check prestige store state before any modifications
|
||||
const prestigeState = usePrestigeStore.getState();
|
||||
console.log('prestigeState.pactRitualProgress:', prestigeState.pactRitualProgress);
|
||||
console.log('prestigeState.pactRitualFloor:', prestigeState.pactRitualFloor);
|
||||
console.log('prestigeState.signedPacts:', prestigeState.signedPacts);
|
||||
|
||||
// Check discipline store
|
||||
const disciplineState = useDisciplineStore.getState();
|
||||
console.log('disciplineState.disciplines:', JSON.stringify(disciplineState.disciplines));
|
||||
console.log('disciplineState.activeIds:', JSON.stringify(disciplineState.activeIds));
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useGameStore } from '../stores/gameStore';
|
||||
import { useManaStore } from '../stores/manaStore';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||
import { useUIStore } from '../stores/uiStore';
|
||||
import { HOURS_PER_TICK } from '../constants';
|
||||
import { setupTickTestEnvironment } from './test-setup';
|
||||
|
||||
beforeEach(setupTickTestEnvironment);
|
||||
|
||||
// ─── 8. Victory Condition ────────────────────────────────────────────────────
|
||||
|
||||
describe('victory condition', () => {
|
||||
it('should trigger victory when maxFloorReached >= 100 and signedPacts includes 100', () => {
|
||||
useCombatStore.setState({ maxFloorReached: 100 });
|
||||
usePrestigeStore.setState({ signedPacts: [100] });
|
||||
|
||||
useGameStore.getState().tick();
|
||||
|
||||
expect(useUIStore.getState().gameOver).toBe(true);
|
||||
expect(useUIStore.getState().victory).toBe(true);
|
||||
});
|
||||
|
||||
it('should set loopInsight on victory', () => {
|
||||
useCombatStore.setState({ maxFloorReached: 100 });
|
||||
usePrestigeStore.setState({ signedPacts: [100] });
|
||||
|
||||
useGameStore.getState().tick();
|
||||
|
||||
expect(usePrestigeStore.getState().loopInsight).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should not trigger victory with floor 100 but no pact', () => {
|
||||
useCombatStore.setState({ maxFloorReached: 100 });
|
||||
usePrestigeStore.setState({ signedPacts: [] });
|
||||
|
||||
useGameStore.getState().tick();
|
||||
|
||||
expect(useUIStore.getState().victory).toBe(false);
|
||||
expect(useUIStore.getState().gameOver).toBe(false);
|
||||
});
|
||||
|
||||
it('should not trigger victory with pact 100 but floor < 100', () => {
|
||||
useCombatStore.setState({ maxFloorReached: 99 });
|
||||
usePrestigeStore.setState({ signedPacts: [100] });
|
||||
|
||||
useGameStore.getState().tick();
|
||||
|
||||
expect(useUIStore.getState().victory).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 9. Pact Ritual Progress ─────────────────────────────────────────────────
|
||||
|
||||
describe('pact ritual progress', () => {
|
||||
it('should increase pactRitualProgress by HOURS_PER_TICK per tick', () => {
|
||||
usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 0 });
|
||||
useGameStore.getState().tick();
|
||||
expect(usePrestigeStore.getState().pactRitualProgress).toBeCloseTo(HOURS_PER_TICK, 10);
|
||||
});
|
||||
|
||||
it('should accumulate pact ritual progress over multiple ticks', () => {
|
||||
// Use floor 90 guardian (pactTime=20 hours) so 50 ticks (2 hours) doesn't complete it
|
||||
usePrestigeStore.setState({ pactRitualFloor: 90, pactRitualProgress: 0 });
|
||||
const numTicks = 50;
|
||||
for (let i = 0; i < numTicks; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
expect(usePrestigeStore.getState().pactRitualProgress).toBeCloseTo(
|
||||
numTicks * HOURS_PER_TICK,
|
||||
5,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not progress pact ritual when pactRitualFloor is null', () => {
|
||||
usePrestigeStore.setState({ pactRitualFloor: null, pactRitualProgress: 0 });
|
||||
useGameStore.getState().tick();
|
||||
expect(usePrestigeStore.getState().pactRitualProgress).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 10. Multiple Ticks Accumulation ─────────────────────────────────────────
|
||||
|
||||
describe('multiple ticks accumulation', () => {
|
||||
it('should correctly accumulate state over 100 ticks', () => {
|
||||
const numTicks = 100;
|
||||
const manaBefore = useManaStore.getState().rawMana;
|
||||
|
||||
// Measure first tick regen (baseline, no meditation bonus yet)
|
||||
useGameStore.getState().tick();
|
||||
const firstTickRegen = useManaStore.getState().rawMana - manaBefore;
|
||||
|
||||
// Do 99 more ticks
|
||||
for (let i = 0; i < 99; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
|
||||
const game = useGameStore.getState();
|
||||
const mana = useManaStore.getState();
|
||||
|
||||
// Time should have advanced by 100 * HOURS_PER_TICK = 4 hours
|
||||
const expectedTotalHours = numTicks * HOURS_PER_TICK;
|
||||
const expectedDayIncrement = Math.floor(expectedTotalHours / 24);
|
||||
const expectedHour = expectedTotalHours % 24;
|
||||
|
||||
expect(game.day).toBe(1 + expectedDayIncrement);
|
||||
expect(game.hour).toBeCloseTo(expectedHour, 5);
|
||||
|
||||
// Mana should have accumulated (with meditation bonus increasing over time)
|
||||
const totalGain = mana.rawMana - manaBefore;
|
||||
const minExpected = firstTickRegen * numTicks;
|
||||
const maxExpected = firstTickRegen * 1.5 * numTicks;
|
||||
expect(totalGain).toBeGreaterThan(minExpected - 0.01);
|
||||
expect(totalGain).toBeLessThan(maxExpected + 0.01);
|
||||
|
||||
// Meditate ticks should match
|
||||
expect(mana.meditateTicks).toBe(numTicks);
|
||||
});
|
||||
|
||||
it('should correctly accumulate over enough ticks for a full day', () => {
|
||||
const manaBefore = useManaStore.getState().rawMana;
|
||||
|
||||
// Do 601 ticks (enough for a full day)
|
||||
const numTicks = 601;
|
||||
for (let i = 0; i < numTicks; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
|
||||
expect(useGameStore.getState().day).toBe(2);
|
||||
// Mana should have increased (capped at maxMana=100)
|
||||
expect(useManaStore.getState().rawMana).toBeGreaterThan(manaBefore);
|
||||
expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,224 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useGameStore } from '../stores/gameStore';
|
||||
import { useManaStore } from '../stores/manaStore';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||
import { useUIStore } from '../stores/uiStore';
|
||||
import { HOURS_PER_TICK, MAX_DAY, INCURSION_START_DAY } from '../constants';
|
||||
import { getMeditationBonus } from '../utils/mana-utils';
|
||||
import { getIncursionStrength } from '../utils/combat-utils';
|
||||
import { setupTickTestEnvironment } from './test-setup';
|
||||
|
||||
beforeEach(setupTickTestEnvironment);
|
||||
|
||||
// ─── 1. Time Progression ─────────────────────────────────────────────────────
|
||||
|
||||
describe('time progression', () => {
|
||||
it('should increase hour by HOURS_PER_TICK after one tick', () => {
|
||||
useGameStore.getState().tick();
|
||||
expect(useGameStore.getState().hour).toBeCloseTo(HOURS_PER_TICK, 10);
|
||||
expect(useGameStore.getState().day).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment day after enough ticks for 24 hours', () => {
|
||||
const ticks = 601;
|
||||
for (let i = 0; i < ticks; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
const { day, hour } = useGameStore.getState();
|
||||
expect(day).toBe(2);
|
||||
expect(hour).toBeGreaterThanOrEqual(0);
|
||||
expect(hour).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it('should advance multiple days correctly', () => {
|
||||
const totalTicks = 601 * 3;
|
||||
for (let i = 0; i < totalTicks; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
expect(useGameStore.getState().day).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 2. Mana Regeneration ────────────────────────────────────────────────────
|
||||
|
||||
describe('mana regeneration', () => {
|
||||
it('should increase rawMana after one tick', () => {
|
||||
const before = useManaStore.getState().rawMana;
|
||||
useGameStore.getState().tick();
|
||||
const after = useManaStore.getState().rawMana;
|
||||
expect(after).toBeGreaterThan(before);
|
||||
});
|
||||
|
||||
it('should accumulate mana over multiple ticks', () => {
|
||||
const mana0 = useManaStore.getState().rawMana;
|
||||
useGameStore.getState().tick();
|
||||
const mana1 = useManaStore.getState().rawMana;
|
||||
const firstTickRegen = mana1 - mana0;
|
||||
|
||||
for (let i = 0; i < 99; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
const mana100 = useManaStore.getState().rawMana;
|
||||
|
||||
const minExpected = mana0 + firstTickRegen * 100;
|
||||
expect(mana100).toBeGreaterThan(minExpected - 0.01);
|
||||
|
||||
const maxExpected = mana0 + firstTickRegen * 2 * 100;
|
||||
expect(mana100).toBeLessThan(maxExpected + 0.01);
|
||||
});
|
||||
|
||||
it('should not exceed maxMana', () => {
|
||||
useManaStore.setState({ rawMana: 99.999 });
|
||||
for (let i = 0; i < 100; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 3. Incursion Penalty ────────────────────────────────────────────────────
|
||||
|
||||
describe('incursion penalty', () => {
|
||||
it('should have zero incursion before INCURSION_START_DAY', () => {
|
||||
useGameStore.setState({ day: INCURSION_START_DAY - 1, hour: 23.9 });
|
||||
useGameStore.getState().tick();
|
||||
const strength = getIncursionStrength(useGameStore.getState().day, useGameStore.getState().hour);
|
||||
expect(strength).toBeCloseTo(0, 5);
|
||||
});
|
||||
|
||||
it('should reduce mana regen after INCURSION_START_DAY', () => {
|
||||
const mana0 = useManaStore.getState().rawMana;
|
||||
useGameStore.getState().tick();
|
||||
const baseRegen = useManaStore.getState().rawMana - mana0;
|
||||
|
||||
useGameStore.setState({ day: 25, hour: 0 });
|
||||
const mana25 = useManaStore.getState().rawMana;
|
||||
useGameStore.getState().tick();
|
||||
const incursionRegen = useManaStore.getState().rawMana - mana25;
|
||||
|
||||
expect(incursionRegen).toBeLessThan(baseRegen);
|
||||
const incursion = getIncursionStrength(25, HOURS_PER_TICK);
|
||||
expect(incursion).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have stronger incursion on later days', () => {
|
||||
const s1 = getIncursionStrength(21, 0);
|
||||
const s2 = getIncursionStrength(28, 0);
|
||||
expect(s2).toBeGreaterThan(s1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 4. Meditation ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('meditation', () => {
|
||||
it('should increment meditateTicks when currentAction is meditate', () => {
|
||||
useCombatStore.setState({ currentAction: 'meditate' });
|
||||
useGameStore.getState().tick();
|
||||
expect(useManaStore.getState().meditateTicks).toBe(1);
|
||||
});
|
||||
|
||||
it('should increase meditateTicks over multiple ticks', () => {
|
||||
useCombatStore.setState({ currentAction: 'meditate' });
|
||||
for (let i = 0; i < 10; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
expect(useManaStore.getState().meditateTicks).toBe(10);
|
||||
});
|
||||
|
||||
it('should reset meditateTicks when action changes from meditate', () => {
|
||||
useCombatStore.setState({ currentAction: 'meditate' });
|
||||
for (let i = 0; i < 5; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
expect(useManaStore.getState().meditateTicks).toBe(5);
|
||||
|
||||
useCombatStore.setState({ currentAction: 'climb' });
|
||||
useGameStore.getState().tick();
|
||||
expect(useManaStore.getState().meditateTicks).toBe(0);
|
||||
});
|
||||
|
||||
it('should apply meditation multiplier to regen', () => {
|
||||
useCombatStore.setState({ currentAction: 'meditate' });
|
||||
const ticksFor4Hours = Math.round(4 / HOURS_PER_TICK);
|
||||
for (let i = 0; i < ticksFor4Hours; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
|
||||
const meditateTicks = useManaStore.getState().meditateTicks;
|
||||
const medMult = getMeditationBonus(meditateTicks, {}, 1);
|
||||
expect(medMult).toBeGreaterThan(1);
|
||||
|
||||
const manaBefore = useManaStore.getState().rawMana;
|
||||
useGameStore.getState().tick();
|
||||
const meditatedRegen = useManaStore.getState().rawMana - manaBefore;
|
||||
|
||||
useManaStore.setState({ rawMana: 50 });
|
||||
useCombatStore.setState({ currentAction: 'climb' });
|
||||
useGameStore.getState().tick();
|
||||
const unMeditatedRegen = useManaStore.getState().rawMana - 50;
|
||||
|
||||
expect(meditatedRegen).toBeGreaterThan(unMeditatedRegen);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 5. Loop End ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('loop end', () => {
|
||||
it('should set gameOver when day exceeds MAX_DAY', () => {
|
||||
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
|
||||
useGameStore.getState().tick();
|
||||
expect(useUIStore.getState().gameOver).toBe(true);
|
||||
});
|
||||
|
||||
it('should set loopInsight in prestigeStore when loop ends', () => {
|
||||
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
|
||||
useGameStore.getState().tick();
|
||||
expect(usePrestigeStore.getState().loopInsight).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should not set victory on normal loop end', () => {
|
||||
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
|
||||
useGameStore.getState().tick();
|
||||
expect(useUIStore.getState().victory).toBe(false);
|
||||
});
|
||||
|
||||
it('should log the loop end message', () => {
|
||||
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
|
||||
useGameStore.getState().tick();
|
||||
const logs = useUIStore.getState().logs;
|
||||
expect(logs.some(l => l.includes('loop ends'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 6. Paused Game ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('paused game', () => {
|
||||
it('should be a no-op when paused is true', () => {
|
||||
useUIStore.setState({ paused: true });
|
||||
const gameBefore = { ...useGameStore.getState() };
|
||||
const manaBefore = useManaStore.getState().rawMana;
|
||||
|
||||
useGameStore.getState().tick();
|
||||
|
||||
expect(useGameStore.getState().hour).toBe(gameBefore.hour);
|
||||
expect(useGameStore.getState().day).toBe(gameBefore.day);
|
||||
expect(useManaStore.getState().rawMana).toBe(manaBefore);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 7. Game Over ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('game over', () => {
|
||||
it('should be a no-op when gameOver is true', () => {
|
||||
useUIStore.setState({ gameOver: true });
|
||||
const gameBefore = { ...useGameStore.getState() };
|
||||
const manaBefore = useManaStore.getState().rawMana;
|
||||
|
||||
useGameStore.getState().tick();
|
||||
|
||||
expect(useGameStore.getState().hour).toBe(gameBefore.hour);
|
||||
expect(useGameStore.getState().day).toBe(gameBefore.day);
|
||||
expect(useManaStore.getState().rawMana).toBe(manaBefore);
|
||||
});
|
||||
});
|
||||
@@ -222,7 +222,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
usePrestigeStore.getState().removeDefeatedGuardian(prestigeState.pactRitualFloor);
|
||||
usePrestigeStore.getState().setPactRitualFloor(null);
|
||||
} else {
|
||||
usePrestigeStore.getState().updatePactRitualProgress(newProgress);
|
||||
usePrestigeStore.getState().updatePactRitualProgress(HOURS_PER_TICK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user