52413777cd
- 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
495 lines
17 KiB
TypeScript
Executable File
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');
|