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
1043 lines
38 KiB
TypeScript
Executable File
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 });
|
|
});
|
|
});
|