/** * Comprehensive Skill Tests * * Tests each skill to verify they work exactly as their descriptions say. * Updated for the new skill system with tiers and upgrade trees. */ import { describe, it, expect } from 'vitest'; import { computeMaxMana, computeElementMax, computeRegen, computeClickMana, calcInsight, getMeditationBonus, } from '../computed-stats'; import { SKILLS_DEF, PRESTIGE_DEF, GUARDIANS, getStudySpeedMultiplier, getStudyCostMultiplier, ELEMENTS, } from '../constants'; import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier, generateTierSkillDef, canTierUp, } from '../skill-evolution'; import type { GameState } from '../types'; // ─── Test Helpers ─────────────────────────────────────────────────────────── function createMockState(overrides: Partial = {}): GameState { const elements: Record = {}; const baseElements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death', 'transference', 'metal', 'sand', 'crystal', 'stellar', 'void', 'lightning']; baseElements.forEach((k) => { elements[k] = { current: 0, max: 10, unlocked: baseElements.slice(0, 4).includes(k) }; }); 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, castProgress: 0, currentRoom: { roomType: 'combat', enemies: [{ id: 'enemy', hp: 100, maxHP: 100, armor: 0, dodgeChance: 0, element: 'fire' }], }, maxFloorReached: 1, signedPacts: [], activeSpell: 'manaBolt', currentAction: 'meditate', spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } }, skills: {}, skillProgress: {}, skillUpgrades: {}, skillTiers: {}, parallelStudyTarget: null, equippedInstances: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory1: null, accessory2: null }, equipmentInstances: {}, enchantmentDesigns: [], designProgress: null, preparationProgress: null, applicationProgress: null, equipmentCraftingProgress: null, unlockedEffects: [], equipmentSpellStates: [], equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null }, inventory: [], blueprints: {}, lootInventory: { materials: {}, blueprints: [] }, schedule: [], autoSchedule: false, studyQueue: [], craftQueue: [], currentStudyTarget: null, achievements: { unlocked: [], progress: {} }, totalSpellsCast: 0, totalDamageDealt: 0, totalCraftsCompleted: 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, }, insight: 0, totalInsight: 0, prestigeUpgrades: {}, memorySlots: 3, memories: [], incursionStrength: 0, containmentWards: 0, log: [], loopInsight: 0, ...overrides, } as GameState; } // ─── Mana Skills Tests ───────────────────────────────────────────────────────── describe('Mana Skills', () => { describe('Mana Well (+100 max mana)', () => { it('should add 100 max mana per level', () => { const state0 = createMockState({ skills: { manaWell: 0 } }); const state1 = createMockState({ skills: { manaWell: 1 } }); const state5 = createMockState({ skills: { manaWell: 5 } }); const state10 = createMockState({ skills: { manaWell: 10 } }); expect(computeMaxMana(state0, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100); expect(computeMaxMana(state1, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 100); expect(computeMaxMana(state5, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 500); expect(computeMaxMana(state10, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 1000); }); it('skill definition should match description', () => { expect(SKILLS_DEF.manaWell.desc).toBe("+100 max mana"); expect(SKILLS_DEF.manaWell.max).toBe(10); }); it('should have upgrade tree', () => { expect(SKILL_EVOLUTION_PATHS.manaWell).toBeDefined(); expect(SKILL_EVOLUTION_PATHS.manaWell.tiers.length).toBe(5); }); }); describe('Mana Flow (+1 regen/hr)', () => { it('should add 1 regen per hour per level', () => { const state0 = createMockState({ skills: { manaFlow: 0 } }); const state1 = createMockState({ skills: { manaFlow: 1 } }); const state5 = createMockState({ skills: { manaFlow: 5 } }); const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 }; expect(computeRegen(state0, effects)).toBe(2); expect(computeRegen(state1, effects)).toBe(2 + 1); expect(computeRegen(state5, effects)).toBe(2 + 5); }); it('skill definition should match description', () => { expect(SKILLS_DEF.manaFlow.desc).toBe("+1 regen/hr"); expect(SKILLS_DEF.manaFlow.max).toBe(10); }); }); describe('Mana Spring (+2 mana regen)', () => { it('should add 2 mana regen', () => { const state0 = createMockState({ skills: { manaSpring: 0 } }); const state1 = createMockState({ skills: { manaSpring: 1 } }); const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 }; expect(computeRegen(state0, effects)).toBe(2); expect(computeRegen(state1, effects)).toBe(2 + 2); }); it('skill definition should match description', () => { expect(SKILLS_DEF.manaSpring.desc).toBe("+2 mana regen"); expect(SKILLS_DEF.manaSpring.max).toBe(1); }); }); describe('Elemental Attunement (+50 elem mana cap)', () => { it('should add 50 element mana capacity per level', () => { const state0 = createMockState({ skills: { elemAttune: 0 } }); const state1 = createMockState({ skills: { elemAttune: 1 } }); const state5 = createMockState({ skills: { elemAttune: 5 } }); expect(computeElementMax(state0, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10); expect(computeElementMax(state1, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 50); expect(computeElementMax(state5, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 250); }); it('skill definition should match description', () => { expect(SKILLS_DEF.elemAttune.desc).toBe("+50 elem mana cap"); expect(SKILLS_DEF.elemAttune.max).toBe(10); }); }); describe('Mana Overflow (+25% mana from clicks)', () => { it('skill definition should match description', () => { expect(SKILLS_DEF.manaOverflow.desc).toBe("+25% mana from clicks"); expect(SKILLS_DEF.manaOverflow.max).toBe(5); }); it('should require Mana Well 3', () => { expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 }); }); }); describe('Mana Tap (+1 mana/click)', () => { it('should add 1 mana per click', () => { const state0 = createMockState({ skills: { manaTap: 0 } }); const state1 = createMockState({ skills: { manaTap: 1 } }); expect(computeClickMana(state0, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(1); expect(computeClickMana(state1, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(2); }); it('skill definition should match description', () => { expect(SKILLS_DEF.manaTap.desc).toBe("+1 mana/click"); expect(SKILLS_DEF.manaTap.max).toBe(1); }); }); describe('Mana Surge (+3 mana/click)', () => { it('should add 3 mana per click', () => { const state1 = createMockState({ skills: { manaSurge: 1 } }); expect(computeClickMana(state1, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(1 + 3); }); it('should stack with Mana Tap', () => { const state = createMockState({ skills: { manaTap: 1, manaSurge: 1 } }); expect(computeClickMana(state, { clickManaBonus: 0, clickManaMultiplier: 1 })).toBe(1 + 1 + 3); }); it('should require Mana Tap 1', () => { expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 }); }); }); }); // ─── Study Skills Tests ───────────────────────────────────────────────────────── describe('Study Skills', () => { describe('Quick Learner (+10% study speed)', () => { it('should multiply study speed by 10% per level', () => { expect(getStudySpeedMultiplier({})).toBe(1); expect(getStudySpeedMultiplier({ quickLearner: 1 })).toBe(1.1); expect(getStudySpeedMultiplier({ quickLearner: 3 })).toBe(1.3); expect(getStudySpeedMultiplier({ quickLearner: 5 })).toBe(1.5); }); it('skill definition should match description', () => { expect(SKILLS_DEF.quickLearner.desc).toBe("+10% study speed"); expect(SKILLS_DEF.quickLearner.max).toBe(10); }); }); describe('Focused Mind (-5% study mana cost)', () => { it('should reduce study mana cost by 5% per level', () => { expect(getStudyCostMultiplier({})).toBe(1); expect(getStudyCostMultiplier({ focusedMind: 1 })).toBe(0.95); expect(getStudyCostMultiplier({ focusedMind: 3 })).toBe(0.85); expect(getStudyCostMultiplier({ focusedMind: 5 })).toBe(0.75); }); it('skill definition should match description', () => { expect(SKILLS_DEF.focusedMind.desc).toBe("-5% study mana cost"); expect(SKILLS_DEF.focusedMind.max).toBe(10); }); }); describe('Meditation Focus (Up to 2.5x regen after 4hrs)', () => { it('should provide meditation bonus caps', () => { expect(SKILLS_DEF.meditation.desc).toContain("2.5x"); expect(SKILLS_DEF.meditation.max).toBe(1); }); }); describe('Knowledge Retention (+20% study progress saved)', () => { it('skill definition should match description', () => { expect(SKILLS_DEF.knowledgeRetention.desc).toBe("+20% study progress saved on cancel"); expect(SKILLS_DEF.knowledgeRetention.max).toBe(3); }); }); describe('Deep Trance (Extend to 6hrs for 3x)', () => { it('skill definition should match description', () => { expect(SKILLS_DEF.deepTrance.desc).toContain("6hrs"); expect(SKILLS_DEF.deepTrance.max).toBe(1); }); it('should require Meditation 1', () => { expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 }); }); }); describe('Void Meditation (Extend to 8hrs for 5x)', () => { it('skill definition should match description', () => { expect(SKILLS_DEF.voidMeditation.desc).toContain("8hrs"); expect(SKILLS_DEF.voidMeditation.max).toBe(1); }); it('should require Deep Trance 1', () => { expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 }); }); }); }); // ─── Ascension Skills Tests ───────────────────────────────────────────────────── describe('Ascension Skills', () => { describe('Insight Harvest (+10% insight gain)', () => { it('should multiply insight gain by 10% per level', () => { const state0 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 0 } }); const state1 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 1 } }); const state5 = createMockState({ maxFloorReached: 10, skills: { insightHarvest: 5 } }); const insight0 = calcInsight(state0); const insight1 = calcInsight(state1); const insight5 = calcInsight(state5); expect(insight1).toBeGreaterThan(insight0); expect(insight5).toBeGreaterThan(insight1); }); it('skill definition should match description', () => { expect(SKILLS_DEF.insightHarvest.desc).toBe("+10% insight gain"); expect(SKILLS_DEF.insightHarvest.max).toBe(5); }); }); describe('Guardian Bane (+20% dmg vs guardians)', () => { it('skill definition should match description', () => { expect(SKILLS_DEF.guardianBane.desc).toBe("+20% dmg vs guardians"); expect(SKILLS_DEF.guardianBane.max).toBe(3); }); }); }); // ─── Enchanter Skills Tests ───────────────────────────────────────────────────── describe('Enchanter Skills', () => { describe('Enchanting (Unlock enchantment design)', () => { it('skill definition should exist', () => { expect(SKILLS_DEF.enchanting).toBeDefined(); expect(SKILLS_DEF.enchanting.attunement).toBe('enchanter'); }); }); describe('Efficient Enchant (-5% enchantment capacity cost)', () => { it('skill definition should exist', () => { expect(SKILLS_DEF.efficientEnchant).toBeDefined(); expect(SKILLS_DEF.efficientEnchant.max).toBe(5); }); }); describe('Disenchanting (Recover mana from removed enchantments)', () => { it('skill definition should exist', () => { expect(SKILLS_DEF.disenchanting).toBeDefined(); }); }); }); // ─── Golemancy Skills Tests ──────────────────────────────────────────────────── describe('Golemancy Skills', () => { describe('Golem Mastery (+10% golem damage)', () => { it('skill definition should exist', () => { expect(SKILLS_DEF.golemMastery).toBeDefined(); expect(SKILLS_DEF.golemMastery.attunementReq).toBeDefined(); }); }); describe('Golem Efficiency (+5% attack speed)', () => { it('skill definition should exist', () => { expect(SKILLS_DEF.golemEfficiency).toBeDefined(); }); }); describe('Golem Longevity (+1 floor duration)', () => { it('skill definition should exist', () => { expect(SKILLS_DEF.golemLongevity).toBeDefined(); }); }); describe('Golem Siphon (-10% maintenance)', () => { it('skill definition should exist', () => { expect(SKILLS_DEF.golemSiphon).toBeDefined(); }); }); }); // ─── Meditation Bonus Tests ───────────────────────────────────────────────────── describe('Meditation Bonus', () => { it('should start at 1x with no meditation', () => { expect(getMeditationBonus(0, {})).toBe(1); }); it('should ramp up over time without skills', () => { 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); }); }); // ─── Skill Prerequisites Tests ────────────────────────────────────────────────── describe('Skill Prerequisites', () => { it('Mana Overflow should require Mana Well 3', () => { expect(SKILLS_DEF.manaOverflow.req).toEqual({ manaWell: 3 }); }); it('Mana Surge should require Mana Tap 1', () => { expect(SKILLS_DEF.manaSurge.req).toEqual({ manaTap: 1 }); }); it('Deep Trance should require Meditation 1', () => { expect(SKILLS_DEF.deepTrance.req).toEqual({ meditation: 1 }); }); it('Void Meditation should require Deep Trance 1', () => { expect(SKILLS_DEF.voidMeditation.req).toEqual({ deepTrance: 1 }); }); it('Efficient Enchant should require Enchanting 3', () => { expect(SKILLS_DEF.efficientEnchant.req).toEqual({ enchanting: 3 }); }); }); // ─── Study Time Tests ─────────────────────────────────────────────────────────── describe('Study Times', () => { it('all skills should have reasonable study times', () => { Object.entries(SKILLS_DEF).forEach(([id, skill]) => { expect(skill.studyTime).toBeGreaterThan(0); expect(skill.studyTime).toBeLessThanOrEqual(72); }); }); it('ascension skills should have long study times', () => { const ascensionSkills = Object.entries(SKILLS_DEF).filter(([, s]) => s.cat === 'ascension'); ascensionSkills.forEach(([, skill]) => { expect(skill.studyTime).toBeGreaterThanOrEqual(20); }); }); }); // ─── Prestige Upgrade Tests ───────────────────────────────────────────────────── describe('Prestige Upgrades', () => { it('all prestige upgrades should have valid costs', () => { Object.entries(PRESTIGE_DEF).forEach(([id, upgrade]) => { expect(upgrade.cost).toBeGreaterThan(0); expect(upgrade.max).toBeGreaterThan(0); }); }); it('Mana Well prestige should add 500 starting max mana', () => { const state0 = createMockState({ prestigeUpgrades: { manaWell: 0 } }); const state1 = createMockState({ prestigeUpgrades: { manaWell: 1 } }); const state5 = createMockState({ prestigeUpgrades: { manaWell: 5 } }); expect(computeMaxMana(state0, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100); expect(computeMaxMana(state1, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 500); expect(computeMaxMana(state5, { maxManaBonus: 0, maxManaMultiplier: 1 })).toBe(100 + 2500); }); it('Elemental Attunement prestige should add 25 element cap', () => { const state0 = createMockState({ prestigeUpgrades: { elementalAttune: 0 } }); const state1 = createMockState({ prestigeUpgrades: { elementalAttune: 1 } }); const state10 = createMockState({ prestigeUpgrades: { elementalAttune: 10 } }); expect(computeElementMax(state0, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10); expect(computeElementMax(state1, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 25); expect(computeElementMax(state10, { elementCapBonus: 0, elementCapMultiplier: 1 })).toBe(10 + 250); }); }); // ─── Integration Tests ────────────────────────────────────────────────────────── describe('Integration Tests', () => { it('skill costs should scale with level', () => { const skill = SKILLS_DEF.manaWell; for (let level = 0; level < skill.max; level++) { const cost = skill.base * (level + 1); expect(cost).toBeGreaterThan(0); } }); it('all skills should have valid categories', () => { const validCategories = ['mana', 'study', 'ascension', 'enchant', 'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'research', 'craft', 'hybrid']; Object.values(SKILLS_DEF).forEach(skill => { expect(validCategories).toContain(skill.cat); }); }); it('all prerequisite skills should exist', () => { Object.entries(SKILLS_DEF).forEach(([id, skill]) => { if (skill.req) { Object.keys(skill.req).forEach(reqId => { expect(SKILLS_DEF[reqId]).toBeDefined(); }); } }); }); it('all prerequisite levels should be within skill max', () => { Object.entries(SKILLS_DEF).forEach(([id, skill]) => { if (skill.req) { Object.entries(skill.req).forEach(([reqId, reqLevel]) => { expect(reqLevel).toBeLessThanOrEqual(SKILLS_DEF[reqId].max); }); } }); }); it('all attunement-requiring skills should have valid attunement', () => { const validAttunements = ['enchanter', 'invoker', 'fabricator']; Object.entries(SKILLS_DEF).forEach(([id, skill]) => { if (skill.attunement) { expect(validAttunements).toContain(skill.attunement); } }); }); }); // ─── Skill Evolution Tests ────────────────────────────────────────────────────── describe('Skill Evolution', () => { it('skills with max > 1 should have evolution paths', () => { const skillsWithMaxGt1 = Object.entries(SKILLS_DEF) .filter(([_, def]) => def.max > 1) .map(([id]) => id); for (const skillId of skillsWithMaxGt1) { expect(SKILL_EVOLUTION_PATHS[skillId], `Missing evolution path for ${skillId}`).toBeDefined(); } }); it('tier multiplier should be 10^(tier-1)', () => { expect(getTierMultiplier('manaWell')).toBe(1); expect(getTierMultiplier('manaWell_t2')).toBe(10); expect(getTierMultiplier('manaWell_t3')).toBe(100); expect(getTierMultiplier('manaWell_t4')).toBe(1000); expect(getTierMultiplier('manaWell_t5')).toBe(10000); }); it('getNextTierSkill should return correct next tier', () => { expect(getNextTierSkill('manaWell')).toBe('manaWell_t2'); expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3'); expect(getNextTierSkill('manaWell_t5')).toBeNull(); }); it('generateTierSkillDef should return valid definitions', () => { const tier1 = generateTierSkillDef('manaWell', 1); expect(tier1).not.toBeNull(); expect(tier1?.name).toBe('Mana Well'); expect(tier1?.multiplier).toBe(1); const tier2 = generateTierSkillDef('manaWell', 2); expect(tier2).not.toBeNull(); expect(tier2?.name).toBe('Deep Reservoir'); expect(tier2?.multiplier).toBe(10); }); }); console.log('✅ All skill tests defined.');