Files
Mana-Loop/src/lib/game/stores.test.ts
T
Unknown 52413777cd Task 1: Implement 5-tier talent tree structure
- Rewrite skill definitions with 5-tier Continuous Talent Tree
- Add perk choices at Level 5 and Level 10 for each tier
- Add Elite Perks at T3 L10 and T5 L10
- Remove scrollCrafting (violates NO INSTANT FINISHING pillar)
- Set max:1 for skills that don't need evolution paths
- All 512 tests passing
2026-04-23 13:22:44 +02:00

495 lines
17 KiB
TypeScript
Executable File

/**
* Tests for the split store architecture
*
* Tests each store in isolation and integration between stores
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
useManaStore,
useSkillStore,
usePrestigeStore,
useCombatStore,
useUIStore,
fmt,
fmtDec,
computeMaxMana,
computeElementMax,
computeRegen,
computeClickMana,
calcDamage,
calcInsight,
getMeditationBonus,
getIncursionStrength,
canAffordSpellCost,
} from './stores';
import {
ELEMENTS,
GUARDIANS,
SPELLS_DEF,
SKILLS_DEF,
PRESTIGE_DEF,
getStudySpeedMultiplier,
getStudyCostMultiplier,
HOURS_PER_TICK,
} from './constants';
import type { GameState } from './types';
// ─── Test Fixtures ───────────────────────────────────────────────────────────
// Reset all stores before each test
beforeEach(() => {
useManaStore.setState({
rawMana: 10,
meditateTicks: 0,
totalManaGathered: 0,
elements: (() => {
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
Object.keys(ELEMENTS).forEach((k) => {
elements[k] = { current: 0, max: 10, unlocked: ['fire', 'water', 'air', 'earth'].includes(k) };
});
return elements;
})(),
});
useSkillStore.setState({
skills: {},
skillProgress: {},
skillUpgrades: {},
skillTiers: {},
paidStudySkills: {},
currentStudyTarget: null,
parallelStudyTarget: null,
});
usePrestigeStore.setState({
loopCount: 0,
insight: 0,
totalInsight: 0,
loopInsight: 0,
prestigeUpgrades: {},
memorySlots: 3,
pactSlots: 1,
memories: [],
defeatedGuardians: [],
signedPacts: [],
pactRitualFloor: null,
pactRitualProgress: 0,
});
useCombatStore.setState({
currentFloor: 1,
floorHP: 151,
floorMaxHP: 151,
maxFloorReached: 1,
activeSpell: 'manaBolt',
currentAction: 'meditate',
castProgress: 0,
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
});
useUIStore.setState({
logs: [],
paused: false,
gameOver: false,
victory: false,
});
});
// ─── Mana Store Tests ─────────────────────────────────────────────────────────
describe('ManaStore', () => {
describe('initial state', () => {
it('should have correct initial values', () => {
const state = useManaStore.getState();
expect(state.rawMana).toBe(10);
expect(state.meditateTicks).toBe(0);
expect(state.totalManaGathered).toBe(0);
});
it('should have base elements unlocked', () => {
const state = useManaStore.getState();
expect(state.elements.fire.unlocked).toBe(true);
expect(state.elements.water.unlocked).toBe(true);
expect(state.elements.air.unlocked).toBe(true);
expect(state.elements.earth.unlocked).toBe(true);
expect(state.elements.light.unlocked).toBe(false);
});
});
describe('raw mana operations', () => {
it('should add raw mana', () => {
useManaStore.getState().addRawMana(50, 100);
expect(useManaStore.getState().rawMana).toBe(60);
});
it('should cap at max mana', () => {
useManaStore.getState().addRawMana(200, 100);
expect(useManaStore.getState().rawMana).toBe(100);
});
it('should spend raw mana', () => {
const result = useManaStore.getState().spendRawMana(5);
expect(result).toBe(true);
expect(useManaStore.getState().rawMana).toBe(5);
});
it('should fail to spend more than available', () => {
const result = useManaStore.getState().spendRawMana(50);
expect(result).toBe(false);
expect(useManaStore.getState().rawMana).toBe(10);
});
});
describe('element operations', () => {
it('should convert raw mana to element', () => {
useManaStore.getState().addRawMana(90, 1000); // Have 100 mana
const result = useManaStore.getState().convertMana('fire', 1);
expect(result).toBe(true);
expect(useManaStore.getState().elements.fire.current).toBe(1);
});
it('should unlock new element', () => {
useManaStore.getState().addRawMana(490, 1000); // Have 500 mana
const result = useManaStore.getState().unlockElement('light', 500);
expect(result).toBe(true);
expect(useManaStore.getState().elements.light.unlocked).toBe(true);
});
});
});
// ─── Skill Store Tests ───────────────────────────────────────────────────────
describe('SkillStore', () => {
describe('study skill', () => {
it('should start studying a skill', () => {
useManaStore.getState().addRawMana(90, 1000); // Have 100 mana
const result = useSkillStore.getState().startStudyingSkill('manaWell', 100);
expect(result.started).toBe(true);
expect(result.cost).toBe(100);
expect(useSkillStore.getState().currentStudyTarget).not.toBeNull();
expect(useSkillStore.getState().currentStudyTarget?.type).toBe('skill');
expect(useSkillStore.getState().currentStudyTarget?.id).toBe('manaWell');
});
it('should not start studying without enough mana', () => {
const result = useSkillStore.getState().startStudyingSkill('manaWell', 50);
expect(result.started).toBe(false);
expect(result.cost).toBe(100);
expect(useSkillStore.getState().currentStudyTarget).toBeNull();
});
it('should track paid study skills', () => {
useManaStore.getState().addRawMana(90, 1000);
useSkillStore.getState().startStudyingSkill('manaWell', 100);
expect(useSkillStore.getState().paidStudySkills['manaWell']).toBe(0);
});
it('should resume studying for free after payment', () => {
useManaStore.getState().addRawMana(90, 1000);
// First study attempt
const result1 = useSkillStore.getState().startStudyingSkill('manaWell', 100);
expect(result1.cost).toBe(100);
// Cancel study (simulated)
useSkillStore.getState().cancelStudy(0);
// Resume should be free
const result2 = useSkillStore.getState().startStudyingSkill('manaWell', 0);
expect(result2.started).toBe(true);
expect(result2.cost).toBe(0);
});
});
describe('update study progress', () => {
it('should update study progress', () => {
useManaStore.getState().addRawMana(90, 1000);
useSkillStore.getState().startStudyingSkill('manaWell', 100);
const result = useSkillStore.getState().updateStudyProgress(1);
expect(result.completed).toBe(false);
expect(useSkillStore.getState().currentStudyTarget?.progress).toBe(1);
});
it('should complete study when progress reaches required', () => {
useManaStore.getState().addRawMana(90, 1000);
useSkillStore.getState().startStudyingSkill('manaWell', 100);
// manaWell requires 4 hours
const result = useSkillStore.getState().updateStudyProgress(4);
expect(result.completed).toBe(true);
expect(result.target?.id).toBe('manaWell');
expect(useSkillStore.getState().currentStudyTarget).toBeNull();
});
it('should apply study speed multiplier', () => {
useManaStore.getState().addRawMana(90, 1000);
useSkillStore.getState().setSkillLevel('quickLearner', 5); // 50% faster
useSkillStore.getState().startStudyingSkill('manaWell', 100);
// The caller should calculate progress with speed multiplier
const speedMult = getStudySpeedMultiplier(useSkillStore.getState().skills);
const result = useSkillStore.getState().updateStudyProgress(3 * speedMult); // 3 * 1.5 = 4.5
expect(result.completed).toBe(true);
});
});
describe('skill level operations', () => {
it('should set skill level', () => {
useSkillStore.getState().setSkillLevel('manaWell', 5);
expect(useSkillStore.getState().skills['manaWell']).toBe(5);
});
it('should increment skill level', () => {
useSkillStore.getState().setSkillLevel('manaWell', 5);
useSkillStore.getState().incrementSkillLevel('manaWell');
expect(useSkillStore.getState().skills['manaWell']).toBe(6);
});
});
describe('prerequisites', () => {
it('should not start studying without prerequisites', () => {
useManaStore.getState().addRawMana(990, 1000);
// manaOverflow requires manaWell 3
const result = useSkillStore.getState().startStudyingSkill('manaOverflow', 1000);
expect(result.started).toBe(false);
});
it('should start studying with prerequisites met', () => {
useManaStore.getState().addRawMana(990, 1000);
useSkillStore.getState().setSkillLevel('manaWell', 3);
const result = useSkillStore.getState().startStudyingSkill('manaOverflow', 1000);
expect(result.started).toBe(true);
});
});
});
// ─── Prestige Store Tests ────────────────────────────────────────────────────
describe('PrestigeStore', () => {
describe('prestige upgrades', () => {
it('should purchase prestige upgrade', () => {
usePrestigeStore.getState().startNewLoop(1000); // Add 1000 insight
const result = usePrestigeStore.getState().doPrestige('manaWell');
expect(result).toBe(true);
expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1);
expect(usePrestigeStore.getState().insight).toBe(500); // 1000 - 500 cost
});
it('should not purchase without enough insight', () => {
usePrestigeStore.getState().startNewLoop(100);
const result = usePrestigeStore.getState().doPrestige('manaWell');
expect(result).toBe(false);
});
});
describe('loop management', () => {
it('should increment loop count', () => {
usePrestigeStore.getState().incrementLoopCount();
expect(usePrestigeStore.getState().loopCount).toBe(1);
usePrestigeStore.getState().incrementLoopCount();
expect(usePrestigeStore.getState().loopCount).toBe(2);
});
it('should reset for new loop', () => {
usePrestigeStore.getState().startNewLoop(1000);
usePrestigeStore.getState().doPrestige('manaWell');
usePrestigeStore.getState().resetPrestigeForNewLoop(
500, // total insight
{ manaWell: 1 }, // prestige upgrades
[], // memories
3 // memory slots
);
expect(usePrestigeStore.getState().insight).toBe(500);
expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1);
expect(usePrestigeStore.getState().defeatedGuardians).toEqual([]);
expect(usePrestigeStore.getState().signedPacts).toEqual([]);
});
});
describe('guardian pacts', () => {
it('should add signed pact', () => {
usePrestigeStore.getState().addSignedPact(10);
expect(usePrestigeStore.getState().signedPacts).toContain(10);
});
it('should add defeated guardian', () => {
usePrestigeStore.getState().addDefeatedGuardian(10);
expect(usePrestigeStore.getState().defeatedGuardians).toContain(10);
});
it('should not add same guardian twice', () => {
usePrestigeStore.getState().addDefeatedGuardian(10);
usePrestigeStore.getState().addDefeatedGuardian(10);
expect(usePrestigeStore.getState().defeatedGuardians.length).toBe(1);
});
});
});
// ─── Combat Store Tests ───────────────────────────────────────────────────────
describe('CombatStore', () => {
describe('floor operations', () => {
it('should advance floor', () => {
useCombatStore.getState().advanceFloor();
expect(useCombatStore.getState().currentFloor).toBe(2);
expect(useCombatStore.getState().maxFloorReached).toBe(2);
});
it('should cap at floor 100', () => {
useCombatStore.setState({ currentFloor: 100 });
useCombatStore.getState().advanceFloor();
expect(useCombatStore.getState().currentFloor).toBe(100);
});
});
describe('action management', () => {
it('should set action', () => {
useCombatStore.getState().setAction('climb');
expect(useCombatStore.getState().currentAction).toBe('climb');
useCombatStore.getState().setAction('study');
expect(useCombatStore.getState().currentAction).toBe('study');
});
});
describe('spell management', () => {
it('should set active spell', () => {
useCombatStore.getState().learnSpell('fireball');
useCombatStore.getState().setSpell('fireball');
expect(useCombatStore.getState().activeSpell).toBe('fireball');
});
it('should learn spell', () => {
useCombatStore.getState().learnSpell('fireball');
expect(useCombatStore.getState().spells['fireball']?.learned).toBe(true);
});
});
});
// ─── UI Store Tests ───────────────────────────────────────────────────────────
describe('UIStore', () => {
describe('log management', () => {
it('should add log message', () => {
useUIStore.getState().addLog('Test message');
expect(useUIStore.getState().logs).toContain('Test message');
});
it('should clear logs', () => {
useUIStore.getState().addLog('Test message');
useUIStore.getState().clearLogs();
expect(useUIStore.getState().logs.length).toBe(0);
});
});
describe('pause management', () => {
it('should toggle pause', () => {
expect(useUIStore.getState().paused).toBe(false);
useUIStore.getState().togglePause();
expect(useUIStore.getState().paused).toBe(true);
useUIStore.getState().togglePause();
expect(useUIStore.getState().paused).toBe(false);
});
it('should set pause state', () => {
useUIStore.getState().setPaused(true);
expect(useUIStore.getState().paused).toBe(true);
});
});
describe('game over state', () => {
it('should set game over', () => {
useUIStore.getState().setGameOver(true);
expect(useUIStore.getState().gameOver).toBe(true);
expect(useUIStore.getState().victory).toBe(false);
});
it('should set victory', () => {
useUIStore.getState().setGameOver(true, true);
expect(useUIStore.getState().gameOver).toBe(true);
expect(useUIStore.getState().victory).toBe(true);
});
});
});
// ─── Integration Tests ─────────────────────────────────────────────────────────
describe('Store Integration', () => {
describe('skill study flow', () => {
it('should complete full study flow', () => {
// Setup: give enough mana
useManaStore.getState().addRawMana(90, 1000);
// Start studying
const startResult = useSkillStore.getState().startStudyingSkill('manaWell', 100);
expect(startResult.started).toBe(true);
expect(startResult.cost).toBe(100);
// Deduct mana (simulating UI behavior)
if (startResult.cost > 0) {
useManaStore.getState().spendRawMana(startResult.cost);
}
// Set action to study
useCombatStore.getState().setAction('study');
expect(useCombatStore.getState().currentAction).toBe('study');
// Update progress until complete
const result = useSkillStore.getState().updateStudyProgress(4);
expect(result.completed).toBe(true);
// Level up skill
useSkillStore.getState().setSkillLevel('manaWell', 1);
expect(useSkillStore.getState().skills['manaWell']).toBe(1);
});
});
describe('mana and prestige interaction', () => {
it('should apply prestige mana bonus', () => {
// Get prestige upgrade
usePrestigeStore.getState().startNewLoop(1000);
usePrestigeStore.getState().doPrestige('manaWell');
// Check that prestige upgrade is recorded
expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1);
// Mana well prestige gives +500 max mana per level
const state = {
skills: {},
prestigeUpgrades: usePrestigeStore.getState().prestigeUpgrades,
skillUpgrades: {},
skillTiers: {},
};
const maxMana = computeMaxMana(state);
expect(maxMana).toBe(100 + 500); // Base 100 + 500 from prestige
});
});
});
console.log('✅ All store tests defined. Run with: bun test src/lib/game/stores.test.ts');