Files
Mana-Loop/src/lib/game/store.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

1043 lines
38 KiB
TypeScript
Executable File

/**
* Unit Tests for Mana Loop Game Logic
*
* This file contains comprehensive tests for the game's core mechanics.
* Updated for the new skill system with tiers and upgrade trees.
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
fmt,
fmtDec,
getFloorMaxHP,
getFloorElement,
computeMaxMana,
computeElementMax,
computeRegen,
computeClickMana,
calcDamage,
calcInsight,
getMeditationBonus,
getIncursionStrength,
canAffordSpellCost,
} from './store';
import {
ELEMENTS,
GUARDIANS,
SPELLS_DEF,
SKILLS_DEF,
PRESTIGE_DEF,
FLOOR_ELEM_CYCLE,
MANA_PER_ELEMENT,
MAX_DAY,
INCURSION_START_DAY,
getStudySpeedMultiplier,
getStudyCostMultiplier,
rawCost,
elemCost,
} from './constants';
import {
SKILL_EVOLUTION_PATHS,
getUpgradesForSkillAtMilestone,
getNextTierSkill,
getTierMultiplier,
generateTierSkillDef,
} from './skill-evolution';
import type { GameState, SpellCost } from './types';
// ─── Test Fixtures ───────────────────────────────────────────────────────────
function createMockState(overrides: Partial<GameState> = {}): GameState {
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
Object.keys(ELEMENTS).forEach((k) => {
elements[k] = { current: 0, max: 10, unlocked: false };
});
return {
day: 1,
hour: 0,
loopCount: 0,
gameOver: false,
victory: false,
paused: false,
rawMana: 100,
meditateTicks: 0,
totalManaGathered: 0,
elements,
currentFloor: 1,
floorHP: 100,
floorMaxHP: 100,
maxFloorReached: 1,
signedPacts: [],
activeSpell: 'manaBolt',
currentAction: 'meditate',
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
skills: {},
skillProgress: {},
skillUpgrades: {},
skillTiers: {},
equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null },
inventory: [],
blueprints: {},
schedule: [],
autoSchedule: false,
studyQueue: [],
craftQueue: [],
currentStudyTarget: null,
insight: 0,
totalInsight: 0,
prestigeUpgrades: {},
memorySlots: 3,
memories: [],
incursionStrength: 0,
containmentWards: 0,
log: [],
loopInsight: 0,
attunements: {
enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 },
invoker: { id: 'invoker', active: false, level: 1, experience: 0 },
fabricator: { id: 'fabricator', active: false, level: 1, experience: 0 },
},
golemancy: {
enabledGolems: [],
summonedGolems: [],
lastSummonFloor: 0,
},
...overrides,
} as GameState;
}
// ─── Formatting Tests ─────────────────────────────────────────────────────────
describe('Formatting Functions', () => {
describe('fmt (format number)', () => {
it('should format numbers less than 1000 as integers', () => {
expect(fmt(0)).toBe('0');
expect(fmt(1)).toBe('1');
expect(fmt(999)).toBe('999');
});
it('should format thousands with K suffix', () => {
expect(fmt(1000)).toBe('1.0K');
expect(fmt(1500)).toBe('1.5K');
expect(fmt(999999)).toBe('1000.0K');
});
it('should format millions with M suffix', () => {
expect(fmt(1000000)).toBe('1.00M');
expect(fmt(1500000)).toBe('1.50M');
});
it('should format billions with B suffix', () => {
expect(fmt(1000000000)).toBe('1.00B');
});
it('should handle non-finite numbers', () => {
expect(fmt(Infinity)).toBe('0');
expect(fmt(NaN)).toBe('0');
expect(fmt(-Infinity)).toBe('0');
});
});
describe('fmtDec (format decimal)', () => {
it('should format numbers with specified decimal places', () => {
expect(fmtDec(1.234, 2)).toBe('1.23');
expect(fmtDec(1.5, 0)).toBe('2'); // toFixed rounds
expect(fmtDec(1.567, 1)).toBe('1.6');
});
it('should handle non-finite numbers', () => {
expect(fmtDec(Infinity, 2)).toBe('0');
expect(fmtDec(NaN, 2)).toBe('0');
});
});
});
// ─── Floor Tests ──────────────────────────────────────────────────────────────
describe('Floor Functions', () => {
describe('getFloorMaxHP', () => {
it('should return guardian HP for guardian floors', () => {
expect(getFloorMaxHP(10)).toBe(GUARDIANS[10].hp);
expect(getFloorMaxHP(100)).toBe(GUARDIANS[100].hp);
});
it('should scale HP for non-guardian floors', () => {
expect(getFloorMaxHP(1)).toBeGreaterThan(0);
expect(getFloorMaxHP(5)).toBeGreaterThan(getFloorMaxHP(1));
expect(getFloorMaxHP(50)).toBeGreaterThan(getFloorMaxHP(25));
});
it('should have increasing scaling', () => {
const hp1 = getFloorMaxHP(1);
const hp5 = getFloorMaxHP(5);
const hp10 = getFloorMaxHP(10); // Guardian floor
const hp50 = getFloorMaxHP(50); // Guardian floor
// HP should increase
expect(hp5).toBeGreaterThan(hp1);
expect(hp10).toBeGreaterThan(hp5);
expect(hp50).toBeGreaterThan(hp10);
// Guardian floors have much more HP
expect(hp10).toBeGreaterThan(1000);
expect(hp50).toBeGreaterThan(10000);
});
});
describe('getFloorElement', () => {
it('should cycle through elements in order (7 elements)', () => {
// FLOOR_ELEM_CYCLE = ["fire", "water", "air", "earth", "light", "dark", "death"]
expect(getFloorElement(1)).toBe('fire');
expect(getFloorElement(2)).toBe('water');
expect(getFloorElement(3)).toBe('air');
expect(getFloorElement(4)).toBe('earth');
expect(getFloorElement(5)).toBe('light');
expect(getFloorElement(6)).toBe('dark');
expect(getFloorElement(7)).toBe('death');
});
it('should wrap around after 7 floors', () => {
expect(getFloorElement(8)).toBe('fire');
expect(getFloorElement(9)).toBe('water');
expect(getFloorElement(15)).toBe('fire'); // (15-1) % 7 = 0 -> fire
expect(getFloorElement(16)).toBe('water'); // (16-1) % 7 = 1 -> water
});
});
});
// ─── Mana Calculation Tests ───────────────────────────────────────────────────
describe('Mana Calculation Functions', () => {
describe('computeMaxMana', () => {
it('should return base mana with no upgrades', () => {
const state = createMockState();
expect(computeMaxMana(state)).toBe(100);
});
it('should add mana from manaWell skill', () => {
const state = createMockState({ skills: { manaWell: 5 } });
expect(computeMaxMana(state)).toBe(100 + 5 * 100);
});
it('should add mana from prestige upgrades', () => {
const state = createMockState({ prestigeUpgrades: { manaWell: 3 } });
expect(computeMaxMana(state)).toBe(100 + 3 * 500);
});
});
describe('computeElementMax', () => {
it('should return base element cap with no upgrades', () => {
const state = createMockState();
expect(computeElementMax(state)).toBe(10);
});
it('should add cap from elemAttune skill', () => {
const state = createMockState({ skills: { elemAttune: 5 } });
expect(computeElementMax(state)).toBe(10 + 5 * 50);
});
it('should add cap from prestige upgrades', () => {
const state = createMockState({ prestigeUpgrades: { elementalAttune: 3 } });
expect(computeElementMax(state)).toBe(10 + 3 * 25);
});
});
describe('computeRegen', () => {
it('should return base regen with no upgrades', () => {
// Base regen is 2, but Enchanter attunement adds 0.5
const state = createMockState();
expect(computeRegen(state)).toBe(2.5); // 2 + 0.5 from enchanter
});
it('should add regen from manaFlow skill', () => {
// Base 2 + enchanter 0.5 + manaFlow 5
const state = createMockState({ skills: { manaFlow: 5 } });
expect(computeRegen(state)).toBe(2 + 0.5 + 5 * 1);
});
it('should add regen from manaSpring skill', () => {
// Base 2 + enchanter 0.5 + manaSpring 2
const state = createMockState({ skills: { manaSpring: 1 } });
expect(computeRegen(state)).toBe(2 + 0.5 + 2);
});
it('should multiply by temporal echo prestige', () => {
const state = createMockState({ prestigeUpgrades: { temporalEcho: 2 } });
// Base 2 * 1.2 (temporal) = 2.4, + enchanter 0.5 = 2.9
// Note: temporal bonus applies to base, not attunement
expect(computeRegen(state)).toBe(2 * 1.2 + 0.5);
});
});
describe('computeClickMana', () => {
it('should return base click mana with no upgrades', () => {
const state = createMockState();
expect(computeClickMana(state)).toBe(1);
});
it('should add mana from manaTap skill', () => {
const state = createMockState({ skills: { manaTap: 1 } });
expect(computeClickMana(state)).toBe(1 + 1);
});
it('should add mana from manaSurge skill', () => {
const state = createMockState({ skills: { manaSurge: 1 } });
expect(computeClickMana(state)).toBe(1 + 3);
});
it('should stack manaTap and manaSurge', () => {
const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } });
expect(computeClickMana(state)).toBe(1 + 1 + 3);
});
});
});
// ─── Damage Calculation Tests ─────────────────────────────────────────────────
describe('Damage Calculation', () => {
describe('calcDamage', () => {
it('should return spell base damage with no bonuses', () => {
const state = createMockState();
const dmg = calcDamage(state, 'manaBolt');
expect(dmg).toBeGreaterThanOrEqual(5); // Base damage
expect(dmg).toBeLessThanOrEqual(5 * 1.5); // No crit bonus, just base
});
it('should multiply by signed pacts', () => {
const state = createMockState({ signedPacts: [10] });
// Pact multiplier is 1.5 for floor 10
const dmg = calcDamage(state, 'manaBolt');
const minDmg = 5 * 1.5;
expect(dmg).toBeGreaterThanOrEqual(minDmg);
});
it('should stack multiple pacts', () => {
const state = createMockState({ signedPacts: [10, 20] });
const pactMult = GUARDIANS[10].pact * GUARDIANS[20].pact;
const dmg = calcDamage(state, 'manaBolt');
const minDmg = 5 * pactMult;
expect(dmg).toBeGreaterThanOrEqual(minDmg);
});
describe('Elemental bonuses', () => {
it('should give +25% for same element', () => {
const state = createMockState({ spells: { fireball: { learned: true, level: 1 } } });
// Floor 1 is fire element
const dmg = calcDamage(state, 'fireball', 'fire');
// Without crit: 15 * 1.25
expect(dmg).toBeGreaterThanOrEqual(15 * 1.25);
});
it('should give +50% for opposing element (super effective)', () => {
const state = createMockState({ spells: { waterJet: { learned: true, level: 1 } } });
// Water vs fire - water is the opposite of fire, so water is super effective
const dmg = calcDamage(state, 'waterJet', 'fire');
// Base 12 * 1.5 = 18 (without crit)
expect(dmg).toBeGreaterThanOrEqual(18 * 0.5); // Can crit, so min is lower
});
it('should give +50% when attacking opposite element (fire vs water)', () => {
const state = createMockState({ spells: { fireball: { learned: true, level: 1 } } });
// Fire vs water - fire is the opposite of water, so fire is super effective
const dmg = calcDamage(state, 'fireball', 'water');
// Base 15 * 1.5 = 22.5 (without crit)
expect(dmg).toBeGreaterThanOrEqual(22.5 * 0.5); // Can crit
});
it('should be neutral for non-opposing elements', () => {
const state = createMockState({ spells: { fireball: { learned: true, level: 1 } } });
// Fire vs air (neutral - neither same nor opposite)
const dmg = calcDamage(state, 'fireball', 'air');
expect(dmg).toBeGreaterThanOrEqual(15 * 0.5); // No bonus, but could crit
expect(dmg).toBeLessThanOrEqual(15 * 1.5);
});
});
});
});
// ─── Insight Calculation Tests ─────────────────────────────────────────────────
describe('Insight Calculation', () => {
describe('calcInsight', () => {
it('should calculate insight from floor progress', () => {
const state = createMockState({ maxFloorReached: 10 });
const insight = calcInsight(state);
expect(insight).toBe(10 * 15);
});
it('should calculate insight from mana gathered', () => {
const state = createMockState({ totalManaGathered: 5000 });
const insight = calcInsight(state);
// Formula: floor*15 + mana/500 + pacts*150
// With default maxFloorReached=1: 1*15 + 5000/500 + 0 = 15 + 10 = 25
expect(insight).toBe(25);
});
it('should calculate insight from signed pacts', () => {
const state = createMockState({ signedPacts: [10, 20] });
const insight = calcInsight(state);
// Formula: floor*15 + mana/500 + pacts*150
// With default maxFloorReached=1: 1*15 + 0 + 2*150 = 15 + 300 = 315
expect(insight).toBe(315);
});
it('should multiply by insightAmp prestige', () => {
const state = createMockState({
maxFloorReached: 10,
prestigeUpgrades: { insightAmp: 2 },
});
const insight = calcInsight(state);
expect(insight).toBe(Math.floor(10 * 15 * 1.5));
});
it('should multiply by insightHarvest skill', () => {
const state = createMockState({
maxFloorReached: 10,
skills: { insightHarvest: 3 },
});
const insight = calcInsight(state);
expect(insight).toBe(Math.floor(10 * 15 * 1.3));
});
});
});
// ─── Meditation Tests ─────────────────────────────────────────────────────────
describe('Meditation Bonus', () => {
describe('getMeditationBonus', () => {
it('should start at 1x with no meditation', () => {
expect(getMeditationBonus(0, {})).toBe(1);
});
it('should ramp up over time', () => {
const bonus1hr = getMeditationBonus(25, {}); // 1 hour of ticks
expect(bonus1hr).toBeGreaterThan(1);
const bonus4hr = getMeditationBonus(100, {}); // 4 hours
expect(bonus4hr).toBeGreaterThan(bonus1hr);
});
it('should cap at 1.5x without meditation skill', () => {
const bonus = getMeditationBonus(200, {}); // 8 hours
expect(bonus).toBe(1.5);
});
it('should give 2.5x with meditation skill after 4 hours', () => {
const bonus = getMeditationBonus(100, { meditation: 1 });
expect(bonus).toBe(2.5);
});
it('should give 3.0x with deepTrance skill after 6 hours', () => {
const bonus = getMeditationBonus(150, { meditation: 1, deepTrance: 1 });
expect(bonus).toBe(3.0);
});
it('should give 5.0x with voidMeditation skill after 8 hours', () => {
const bonus = getMeditationBonus(200, { meditation: 1, deepTrance: 1, voidMeditation: 1 });
expect(bonus).toBe(5.0);
});
});
});
// ─── Incursion Tests ──────────────────────────────────────────────────────────
describe('Incursion Strength', () => {
describe('getIncursionStrength', () => {
it('should be 0 before incursion start day', () => {
expect(getIncursionStrength(19, 0)).toBe(0);
expect(getIncursionStrength(19, 23)).toBe(0);
});
it('should start at incursion start day', () => {
// Incursion starts at day 20, hour 0
// Formula: totalHours / maxHours * 0.95
// At day 20, hour 0: totalHours = 0, so strength = 0
// Need hour > 0 to see incursion
expect(getIncursionStrength(INCURSION_START_DAY, 1)).toBeGreaterThan(0);
});
it('should increase over time', () => {
const early = getIncursionStrength(INCURSION_START_DAY, 12);
const late = getIncursionStrength(25, 12);
expect(late).toBeGreaterThan(early);
});
it('should cap at 95%', () => {
const strength = getIncursionStrength(MAX_DAY, 23);
expect(strength).toBeLessThanOrEqual(0.95);
});
});
});
// ─── Spell Cost Tests ─────────────────────────────────────────────────────────
describe('Spell Cost System', () => {
describe('rawCost', () => {
it('should create a raw mana cost', () => {
const cost = rawCost(10);
expect(cost.type).toBe('raw');
expect(cost.amount).toBe(10);
expect(cost.element).toBeUndefined();
});
});
describe('elemCost', () => {
it('should create an elemental mana cost', () => {
const cost = elemCost('fire', 5);
expect(cost.type).toBe('element');
expect(cost.element).toBe('fire');
expect(cost.amount).toBe(5);
});
});
describe('canAffordSpellCost', () => {
it('should allow raw mana costs when enough raw mana', () => {
const cost = rawCost(10);
const elements = { fire: { current: 0, max: 10, unlocked: true } };
expect(canAffordSpellCost(cost, 100, elements)).toBe(true);
});
it('should deny raw mana costs when not enough raw mana', () => {
const cost = rawCost(100);
const elements = { fire: { current: 0, max: 10, unlocked: true } };
expect(canAffordSpellCost(cost, 50, elements)).toBe(false);
});
it('should allow elemental costs when enough element mana', () => {
const cost = elemCost('fire', 5);
const elements = { fire: { current: 10, max: 10, unlocked: true } };
expect(canAffordSpellCost(cost, 0, elements)).toBe(true);
});
it('should deny elemental costs when element not unlocked', () => {
const cost = elemCost('fire', 5);
const elements = { fire: { current: 10, max: 10, unlocked: false } };
expect(canAffordSpellCost(cost, 100, elements)).toBe(false);
});
it('should deny elemental costs when not enough element mana', () => {
const cost = elemCost('fire', 10);
const elements = { fire: { current: 5, max: 10, unlocked: true } };
expect(canAffordSpellCost(cost, 100, elements)).toBe(false);
});
});
});
// ─── Study Speed Tests ────────────────────────────────────────────────────────
describe('Study Speed Functions', () => {
describe('getStudySpeedMultiplier', () => {
it('should return 1 with no quickLearner skill', () => {
expect(getStudySpeedMultiplier({})).toBe(1);
});
it('should increase by 10% per level', () => {
expect(getStudySpeedMultiplier({ quickLearner: 1 })).toBe(1.1);
expect(getStudySpeedMultiplier({ quickLearner: 5 })).toBe(1.5);
});
});
describe('getStudyCostMultiplier', () => {
it('should return 1 with no focusedMind skill', () => {
expect(getStudyCostMultiplier({})).toBe(1);
});
it('should decrease by 5% per level', () => {
expect(getStudyCostMultiplier({ focusedMind: 1 })).toBe(0.95);
expect(getStudyCostMultiplier({ focusedMind: 5 })).toBe(0.75);
});
});
});
// ─── Constants Validation Tests ───────────────────────────────────────────────
describe('Game Constants', () => {
describe('ELEMENTS', () => {
it('should have all base elements', () => {
// Life, blood, wood were removed - we have 7 base elements now
expect(ELEMENTS.fire).toBeDefined();
expect(ELEMENTS.water).toBeDefined();
expect(ELEMENTS.air).toBeDefined();
expect(ELEMENTS.earth).toBeDefined();
expect(ELEMENTS.light).toBeDefined();
expect(ELEMENTS.dark).toBeDefined();
expect(ELEMENTS.death).toBeDefined();
});
it('should have composite elements with recipes', () => {
// blood and wood were removed
expect(ELEMENTS.metal.recipe).toEqual(['fire', 'earth']);
expect(ELEMENTS.sand.recipe).toEqual(['earth', 'water']);
expect(ELEMENTS.lightning.recipe).toEqual(['fire', 'air']);
});
it('should have exotic elements with 3-ingredient recipes', () => {
expect(ELEMENTS.crystal.recipe).toHaveLength(3);
expect(ELEMENTS.stellar.recipe).toHaveLength(3);
expect(ELEMENTS.void.recipe).toHaveLength(3);
});
it('should have utility element transference', () => {
expect(ELEMENTS.transference).toBeDefined();
expect(ELEMENTS.transference.cat).toBe('utility');
});
});
describe('GUARDIANS', () => {
it('should have guardians on expected floors', () => {
// Note: Floor 70 was removed
[10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => {
expect(GUARDIANS[floor]).toBeDefined();
});
});
it('should have increasing HP', () => {
let prevHP = 0;
[10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => {
expect(GUARDIANS[floor].hp).toBeGreaterThan(prevHP);
prevHP = GUARDIANS[floor].hp;
});
});
it('should have increasing pact multipliers', () => {
let prevPact = 1;
[10, 20, 30, 40, 50, 60, 80, 90, 100].forEach(floor => {
expect(GUARDIANS[floor].pact).toBeGreaterThan(prevPact);
prevPact = GUARDIANS[floor].pact;
});
});
});
describe('SPELLS_DEF', () => {
it('should have manaBolt as a basic spell', () => {
expect(SPELLS_DEF.manaBolt).toBeDefined();
expect(SPELLS_DEF.manaBolt.tier).toBe(0);
expect(SPELLS_DEF.manaBolt.cost.type).toBe('raw');
});
it('should have spells for existing base elements', () => {
// Life was removed, death is present
const elements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'];
elements.forEach(elem => {
const hasSpell = Object.values(SPELLS_DEF).some(s => s.elem === elem);
expect(hasSpell).toBe(true);
});
});
it('should have increasing damage for higher tiers', () => {
const tier0Avg = Object.values(SPELLS_DEF).filter(s => s.tier === 0).reduce((a, s) => a + s.dmg, 0);
const tier1Avg = Object.values(SPELLS_DEF).filter(s => s.tier === 1).reduce((a, s) => a + s.dmg, 0);
const tier2Avg = Object.values(SPELLS_DEF).filter(s => s.tier === 2).reduce((a, s) => a + s.dmg, 0);
expect(tier1Avg).toBeGreaterThan(tier0Avg);
expect(tier2Avg).toBeGreaterThan(tier1Avg);
});
});
describe('SKILLS_DEF', () => {
it('should have skills with valid categories', () => {
const validCategories = ['mana', 'study', 'research', 'ascension', 'enchant', 'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'craft'];
Object.values(SKILLS_DEF).forEach(skill => {
expect(validCategories).toContain(skill.cat);
});
});
it('should have reasonable study times', () => {
Object.values(SKILLS_DEF).forEach(skill => {
expect(skill.studyTime).toBeGreaterThan(0);
expect(skill.studyTime).toBeLessThanOrEqual(72);
});
});
});
describe('PRESTIGE_DEF', () => {
it('should have prestige upgrades with valid costs', () => {
Object.values(PRESTIGE_DEF).forEach(def => {
expect(def.cost).toBeGreaterThan(0);
expect(def.max).toBeGreaterThan(0);
});
});
});
});
// ─── Element Recipe Tests ─────────────────────────────────────────────────────
describe('Element Crafting Recipes', () => {
it('should have valid ingredient references', () => {
Object.entries(ELEMENTS).forEach(([id, def]) => {
if (def.recipe) {
def.recipe.forEach(ingredient => {
expect(ELEMENTS[ingredient]).toBeDefined();
});
}
});
});
it('should not have circular recipes', () => {
const visited = new Set<string>();
const checkCircular = (id: string, path: string[]): boolean => {
if (path.includes(id)) return true;
const def = ELEMENTS[id];
if (!def.recipe) return false;
return def.recipe.some(ing => checkCircular(ing, [...path, id]));
};
Object.keys(ELEMENTS).forEach(id => {
expect(checkCircular(id, [])).toBe(false);
});
});
});
// ─── Integration Tests ────────────────────────────────────────────────────────
describe('Integration Tests', () => {
it('should have consistent element references across all definitions', () => {
// All spell elements should exist
Object.values(SPELLS_DEF).forEach(spell => {
if (spell.elem !== 'raw') {
expect(ELEMENTS[spell.elem]).toBeDefined();
}
});
// All guardian elements should exist
Object.values(GUARDIANS).forEach(guardian => {
expect(ELEMENTS[guardian.element]).toBeDefined();
});
});
it('should have balanced spell costs relative to damage', () => {
Object.values(SPELLS_DEF).forEach(spell => {
const dmgPerCost = spell.dmg / spell.cost.amount;
// Damage per mana should be reasonable (between 0.5 and 50)
expect(dmgPerCost).toBeGreaterThan(0.5);
expect(dmgPerCost).toBeLessThan(50);
});
});
it('should have balanced skill requirements', () => {
Object.entries(SKILLS_DEF).forEach(([id, skill]) => {
if (skill.req) {
Object.entries(skill.req).forEach(([reqId, level]) => {
expect(SKILLS_DEF[reqId]).toBeDefined();
expect(level).toBeGreaterThan(0);
expect(level).toBeLessThanOrEqual(SKILLS_DEF[reqId].max);
});
}
});
});
});
// ─── Skill Evolution Tests ─────────────────────────────────────────────────────
describe('Skill Evolution System', () => {
describe('SKILL_EVOLUTION_PATHS', () => {
it('should have evolution paths for major skills', () => {
expect(SKILL_EVOLUTION_PATHS.manaWell).toBeDefined();
expect(SKILL_EVOLUTION_PATHS.manaFlow).toBeDefined();
expect(SKILL_EVOLUTION_PATHS.quickLearner).toBeDefined();
expect(SKILL_EVOLUTION_PATHS.focusedMind).toBeDefined();
});
it('should have multiple tiers for evolution', () => {
expect(SKILL_EVOLUTION_PATHS.manaWell.tiers.length).toBeGreaterThanOrEqual(3);
});
});
describe('getTierMultiplier', () => {
it('should return correct multipliers for tiered skills', () => {
expect(getTierMultiplier('manaWell')).toBe(1);
expect(getTierMultiplier('manaWell_t2')).toBe(10);
expect(getTierMultiplier('manaWell_t3')).toBe(100);
});
});
describe('getNextTierSkill', () => {
it('should return next tier skill ID', () => {
expect(getNextTierSkill('manaWell')).toBe('manaWell_t2');
expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3');
});
});
});
// ─── Individual Skill Tests (Current System) ───────────────────────────────────
describe('Individual Skill Tests', () => {
// ─── Mana Skills ────────────────────────────────────────────────────────────
describe('manaWell', () => {
it('should add +100 max mana per level', () => {
const state1 = createMockState({ skills: { manaWell: 1 } });
expect(computeMaxMana(state1)).toBe(100 + 100);
const state5 = createMockState({ skills: { manaWell: 5 } });
expect(computeMaxMana(state5)).toBe(100 + 500);
});
it('should stack with prestige manaWell', () => {
const state = createMockState({
skills: { manaWell: 3 },
prestigeUpgrades: { manaWell: 2 }
});
expect(computeMaxMana(state)).toBe(100 + 300 + 1000);
});
it('should have evolution path to Deep Reservoir at tier 2', () => {
const tier2 = SKILL_EVOLUTION_PATHS.manaWell.tiers.find(t => t.tier === 2);
expect(tier2).toBeDefined();
expect(tier2?.name).toBe('Deep Reservoir');
});
});
describe('manaFlow', () => {
it('should add +1 regen/hr per level', () => {
// Base regen is 2 + enchanter 0.5 = 2.5
const state0 = createMockState();
expect(computeRegen(state0)).toBe(2.5);
const state3 = createMockState({ skills: { manaFlow: 3 } });
expect(computeRegen(state3)).toBe(2 + 0.5 + 3);
const state10 = createMockState({ skills: { manaFlow: 10 } });
expect(computeRegen(state10)).toBe(2 + 0.5 + 10);
});
});
describe('elemAttune', () => {
it('should add +50 elem mana cap per level', () => {
const state0 = createMockState();
expect(computeElementMax(state0)).toBe(10);
const state3 = createMockState({ skills: { elemAttune: 3 } });
expect(computeElementMax(state3)).toBe(10 + 150);
});
});
describe('manaOverflow', () => {
it('should be defined with correct properties', () => {
expect(SKILLS_DEF.manaOverflow).toBeDefined();
expect(SKILLS_DEF.manaOverflow.max).toBe(5);
expect(SKILLS_DEF.manaOverflow.desc).toContain('click');
});
it('should require Mana Well 3', () => {
expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 });
});
});
// ─── Study Skills ───────────────────────────────────────────────────────────
describe('quickLearner', () => {
it('should add +10% study speed per level', () => {
expect(getStudySpeedMultiplier({ quickLearner: 0 })).toBe(1);
expect(getStudySpeedMultiplier({ quickLearner: 1 })).toBe(1.1);
expect(getStudySpeedMultiplier({ quickLearner: 5 })).toBe(1.5);
});
});
describe('focusedMind', () => {
it('should reduce study mana cost by 5% per level', () => {
expect(getStudyCostMultiplier({ focusedMind: 0 })).toBe(1);
expect(getStudyCostMultiplier({ focusedMind: 1 })).toBe(0.95);
expect(getStudyCostMultiplier({ focusedMind: 5 })).toBe(0.75);
});
it('should reduce skill study cost', () => {
const baseCost = SKILLS_DEF.manaWell.base;
const costMult5 = getStudyCostMultiplier({ focusedMind: 5 });
const reducedCost = Math.floor(baseCost * costMult5);
expect(reducedCost).toBe(75);
});
it('should reduce spell study cost', () => {
const baseCost = SPELLS_DEF.fireball.unlock;
const costMult5 = getStudyCostMultiplier({ focusedMind: 5 });
const reducedCost = Math.floor(baseCost * costMult5);
expect(reducedCost).toBe(75);
});
});
describe('meditation', () => {
it('should give 2.5x regen after 4 hours meditating', () => {
const bonus = getMeditationBonus(100, { meditation: 1 }); // 100 ticks = 4 hours
expect(bonus).toBe(2.5);
});
it('should not give bonus without enough time', () => {
const bonus = getMeditationBonus(50, { meditation: 1 }); // 2 hours
expect(bonus).toBeLessThan(2.5);
});
});
describe('knowledgeRetention', () => {
it('should save +20% study progress on cancel per level', () => {
expect(SKILLS_DEF.knowledgeRetention).toBeDefined();
expect(SKILLS_DEF.knowledgeRetention.max).toBe(3);
expect(SKILLS_DEF.knowledgeRetention.desc).toContain('20% study progress saved');
});
});
// ─── Research Skills ────────────────────────────────────────────────────────
describe('manaTap', () => {
it('should add +1 mana per click', () => {
const state0 = createMockState();
expect(computeClickMana(state0)).toBe(1);
const state1 = createMockState({ skills: { manaTap: 1 } });
expect(computeClickMana(state1)).toBe(2);
});
});
describe('manaSurge', () => {
it('should add +3 mana per click', () => {
const state = createMockState({ skills: { manaSurge: 1 } });
expect(computeClickMana(state)).toBe(1 + 3);
});
it('should stack with manaTap', () => {
const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } });
expect(computeClickMana(state)).toBe(1 + 1 + 3);
});
it('should require manaTap', () => {
expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 });
});
});
describe('manaSpring', () => {
it('should add +2 mana regen', () => {
// Base 2 + enchanter 0.5 + manaSpring 2
const state = createMockState({ skills: { manaSpring: 1 } });
expect(computeRegen(state)).toBe(2 + 0.5 + 2);
});
});
describe('deepTrance', () => {
it('should extend meditation bonus to 6hrs for 3x', () => {
const bonus = getMeditationBonus(150, { meditation: 1, deepTrance: 1 }); // 6 hours
expect(bonus).toBe(3.0);
});
it('should require meditation', () => {
expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 });
});
});
describe('voidMeditation', () => {
it('should extend meditation bonus to 8hrs for 5x', () => {
const bonus = getMeditationBonus(200, { meditation: 1, deepTrance: 1, voidMeditation: 1 }); // 8 hours
expect(bonus).toBe(5.0);
});
it('should require deepTrance', () => {
expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 });
});
});
// ─── Ascension Skills ───────────────────────────────────────────────────────
describe('insightHarvest', () => {
it('should add +10% insight gain per level', () => {
const state0 = createMockState({ maxFloorReached: 10 });
const insight0 = calcInsight(state0);
const state3 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 3 } });
const insight3 = calcInsight(state3);
// Level 3 = 1.3x insight
expect(insight3).toBe(Math.floor(insight0 * 1.3));
});
});
describe('guardianBane', () => {
it('should add +20% damage vs guardians per level', () => {
expect(SKILLS_DEF.guardianBane).toBeDefined();
expect(SKILLS_DEF.guardianBane.max).toBe(3);
expect(SKILLS_DEF.guardianBane.desc).toContain('20% dmg vs guardians');
});
});
// ─── Enchanter Skills ───────────────────────────────────────────────────────
describe('enchanting', () => {
it('should require enchanter attunement', () => {
expect(SKILLS_DEF.enchanting).toBeDefined();
expect(SKILLS_DEF.enchanting.attunement).toBe('enchanter');
});
});
describe('efficientEnchant', () => {
it('should require enchanting 3', () => {
expect(SKILLS_DEF.efficientEnchant).toBeDefined();
expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 });
});
});
// ─── Fabricator/Golemancy Skills ────────────────────────────────────────────
describe('golemMastery', () => {
it('should be defined with correct properties', () => {
expect(SKILLS_DEF.golemMastery).toBeDefined();
expect(SKILLS_DEF.golemMastery.max).toBe(1);
});
});
describe('golemEfficiency', () => {
it('should be defined with correct properties', () => {
expect(SKILLS_DEF.golemEfficiency).toBeDefined();
expect(SKILLS_DEF.golemEfficiency.max).toBe(1);
});
});
// ─── Crafting Skills ────────────────────────────────────────────────────────
describe('effCrafting', () => {
it('should reduce craft time by 10% per level', () => {
expect(SKILLS_DEF.effCrafting).toBeDefined();
expect(SKILLS_DEF.effCrafting.max).toBe(1);
expect(SKILLS_DEF.effCrafting.desc).toContain('10% craft time');
});
});
describe('fieldRepair', () => {
it('should be defined with correct properties', () => {
expect(SKILLS_DEF.fieldRepair).toBeDefined();
expect(SKILLS_DEF.fieldRepair.max).toBe(1);
});
});
describe('elemCrafting', () => {
it('should add +25% craft output per level', () => {
expect(SKILLS_DEF.elemCrafting).toBeDefined();
expect(SKILLS_DEF.elemCrafting.max).toBe(1);
expect(SKILLS_DEF.elemCrafting.desc).toContain('25% craft output');
});
});
});
// ─── Skill Requirement Tests ─────────────────────────────────────────────────
describe('Skill Requirements', () => {
it('manaOverflow should require manaWell 3', () => {
expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 });
});
it('manaSurge should require manaTap 1', () => {
expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 });
});
it('deepTrance should require meditation 1', () => {
expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 });
});
it('voidMeditation should require deepTrance 1', () => {
expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 });
});
it('efficientEnchant should require enchanting 3', () => {
expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 });
});
});