/** * 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 { const elements: Record = {}; 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(); 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 }); }); });