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:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user