Files
Mana-Loop/src/lib/game/store.test.ts
Z User b78c979647
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Has been cancelled
Redesign skill system with upgrade trees and tier progression
Major changes:
- Created docs/skills.md with comprehensive skill system documentation
- Rewrote skill-evolution.ts with new upgrade tree structure:
  - Upgrades organized in branching paths with prerequisites
  - Each choice can lead to upgraded versions at future milestones
  - Support for upgrade children and requirement chains
- Added getBaseSkillId and generateTierSkillDef helper functions
- Fixed getFloorElement to use FLOOR_ELEM_CYCLE.length
- Updated test files to match current skill definitions
- Removed tests for non-existent skills

Skill system now supports:
- Levels 1-10 for most skills, level 5 caps for specialized, level 1 for research
- Tier up system: Tier N Level 1 = Tier N-1 Level 10 in power
- Milestone upgrades at levels 5 and 10 with branching upgrade trees
- Attunement requirements for skill access and tier up
- Study costs and time for leveling
2026-04-03 11:08:58 +00:00

2080 lines
77 KiB
TypeScript
Executable File

/**
* Unit Tests for Mana Loop Game Logic
*
* This file contains comprehensive tests for the game's core mechanics.
*/
import { describe, it, expect, beforeEach } from 'bun:test';
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: {},
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,
...overrides,
};
}
// ─── 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', () => {
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('life');
expect(getFloorElement(8)).toBe('death');
});
it('should wrap around after 8 floors', () => {
expect(getFloorElement(9)).toBe('fire');
expect(getFloorElement(10)).toBe('water');
expect(getFloorElement(17)).toBe('fire');
});
});
});
// ─── 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 deepReservoir skill', () => {
const state = createMockState({ skills: { deepReservoir: 3 } });
expect(computeMaxMana(state)).toBe(100 + 3 * 500);
});
it('should add mana from prestige upgrades', () => {
const state = createMockState({ prestigeUpgrades: { manaWell: 3 } });
expect(computeMaxMana(state)).toBe(100 + 3 * 500);
});
it('should stack all mana bonuses', () => {
const state = createMockState({
skills: { manaWell: 5, deepReservoir: 2 },
prestigeUpgrades: { manaWell: 2 },
});
expect(computeMaxMana(state)).toBe(100 + 5 * 100 + 2 * 500 + 2 * 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', () => {
const state = createMockState();
expect(computeRegen(state)).toBe(2);
});
it('should add regen from manaFlow skill', () => {
const state = createMockState({ skills: { manaFlow: 5 } });
expect(computeRegen(state)).toBe(2 + 5 * 1);
});
it('should add regen from manaSpring skill', () => {
const state = createMockState({ skills: { manaSpring: 1 } });
expect(computeRegen(state)).toBe(2 + 2);
});
it('should multiply by temporal echo prestige', () => {
const state = createMockState({ prestigeUpgrades: { temporalEcho: 2 } });
expect(computeRegen(state)).toBe(2 * 1.2);
});
});
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 add damage from combatTrain skill', () => {
const state = createMockState({ skills: { combatTrain: 5 } });
const dmg = calcDamage(state, 'manaBolt');
expect(dmg).toBeGreaterThanOrEqual(5 + 5 * 5);
});
it('should multiply by arcaneFury skill', () => {
const state = createMockState({ skills: { arcaneFury: 3 } });
// Without crit: base * 1.3
const minDmg = 5 * 1.3;
const maxDmg = 5 * 1.3 * 1.5; // With crit
const dmg = calcDamage(state, 'manaBolt');
expect(dmg).toBeGreaterThanOrEqual(minDmg);
expect(dmg).toBeLessThanOrEqual(maxDmg);
});
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
// ELEMENT_OPPOSITES['fire'] === 'water' -> 1.5x
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
// ELEMENT_OPPOSITES['water'] === 'fire' -> 1.5x
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', () => {
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.life).toBeDefined();
expect(ELEMENTS.death).toBeDefined();
});
it('should have composite elements with recipes', () => {
expect(ELEMENTS.blood.recipe).toEqual(['life', 'water']);
expect(ELEMENTS.metal.recipe).toEqual(['fire', 'earth']);
expect(ELEMENTS.wood.recipe).toEqual(['life', 'earth']);
expect(ELEMENTS.sand.recipe).toEqual(['earth', 'water']);
});
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);
});
});
describe('GUARDIANS', () => {
it('should have guardians every 10 floors', () => {
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100].forEach(floor => {
expect(GUARDIANS[floor]).toBeDefined();
});
});
it('should have increasing HP', () => {
let prevHP = 0;
[10, 20, 30, 40, 50, 60, 70, 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, 70, 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 each base element', () => {
const elements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'life', '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', 'combat', 'study', 'craft', 'research', 'ascension'];
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);
});
}
});
});
});
// ─── Individual Skill Tests ─────────────────────────────────────────────────────
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);
});
});
describe('manaFlow', () => {
it('should add +1 regen/hr per level', () => {
const state0 = createMockState();
expect(computeRegen(state0)).toBe(2);
const state3 = createMockState({ skills: { manaFlow: 3 } });
expect(computeRegen(state3)).toBe(2 + 3);
const state10 = createMockState({ skills: { manaFlow: 10 } });
expect(computeRegen(state10)).toBe(2 + 10);
});
});
describe('deepReservoir', () => {
it('should add +500 max mana per level', () => {
const state1 = createMockState({ skills: { deepReservoir: 1 } });
expect(computeMaxMana(state1)).toBe(100 + 500);
const state5 = createMockState({ skills: { deepReservoir: 5 } });
expect(computeMaxMana(state5)).toBe(100 + 2500);
});
it('should stack with manaWell', () => {
const state = createMockState({ skills: { manaWell: 5, deepReservoir: 2 } });
expect(computeMaxMana(state)).toBe(100 + 500 + 1000);
});
});
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 add +25% mana from clicks per level', () => {
// Note: manaOverflow is applied in gatherMana, not computeClickMana
// computeClickMana returns base click mana
const baseClick = computeClickMana(createMockState());
expect(baseClick).toBe(1);
// The actual bonus is applied in gatherMana:
// cm = Math.floor(cm * (1 + manaOverflow * 0.25))
// So level 1 = 1.25x, level 5 = 2.25x
const level1Bonus = 1 + 1 * 0.25;
const level5Bonus = 1 + 5 * 0.25;
expect(Math.floor(baseClick * level1Bonus)).toBe(1);
expect(Math.floor(baseClick * level5Bonus)).toBe(2);
});
});
// ─── Combat Skills ──────────────────────────────────────────────────────────
describe('combatTrain', () => {
it('should add +5 base damage per level', () => {
const state0 = createMockState();
const dmg0 = calcDamage(state0, 'manaBolt');
// Base manaBolt dmg is 5, combatTrain adds 5 per level
// But there's randomness from crit, so check minimum
expect(dmg0).toBeGreaterThanOrEqual(5);
const state5 = createMockState({ skills: { combatTrain: 5 } });
const dmg5 = calcDamage(state5, 'manaBolt');
// 5 base + 5*5 = 30 minimum (no crit)
expect(dmg5).toBeGreaterThanOrEqual(30);
});
});
describe('arcaneFury', () => {
it('should add +10% spell damage per level', () => {
const state0 = createMockState();
const dmg0 = calcDamage(state0, 'manaBolt');
const state3 = createMockState({ skills: { arcaneFury: 3 } });
const dmg3 = calcDamage(state3, 'manaBolt');
// arcaneFury gives 1 + 0.1*level = 1.3x for level 3
// Without crit, damage should be 5 * 1.3 = 6.5
expect(dmg3).toBeGreaterThanOrEqual(5 * 1.3 * 0.5); // Min (no crit)
});
});
describe('precision', () => {
it('should add +5% crit chance per level', () => {
// Precision affects crit chance in calcDamage
// This is probabilistic, so we test the formula
// critChance = skills.precision * 0.05
// Level 1 = 5%, Level 5 = 25%
// Run many samples to verify crit rate
let crits = 0;
const samples = 1000;
const state = createMockState({ skills: { precision: 2 } }); // 10% crit
for (let i = 0; i < samples; i++) {
const dmg = calcDamage(state, 'manaBolt');
if (dmg > 6) crits++; // Crit does 1.5x damage
}
// Should be around 10% with some variance
expect(crits).toBeGreaterThan(50); // At least 5%
expect(crits).toBeLessThan(200); // At most 20%
});
});
describe('quickCast', () => {
it('should be defined and have correct max', () => {
expect(SKILLS_DEF.quickCast).toBeDefined();
expect(SKILLS_DEF.quickCast.max).toBe(5);
expect(SKILLS_DEF.quickCast.desc).toContain('5% attack speed');
});
// Note: quickCast affects attack speed, which would need integration tests
});
describe('elementalMastery', () => {
it('should add +15% elemental damage bonus per level', () => {
// Test with fireball (fire spell) vs fire floor (same element)
const state0 = createMockState({ spells: { fireball: { learned: true, level: 1 } } });
const dmg0 = calcDamage(state0, 'fireball', 'fire');
const state2 = createMockState({
skills: { elementalMastery: 2 },
spells: { fireball: { learned: true, level: 1 } }
});
const dmg2 = calcDamage(state2, 'fireball', 'fire');
// elementalMastery gives 1 + 0.15*level bonus
// Level 2 = 1.3x elemental damage
expect(dmg2).toBeGreaterThan(dmg0 * 0.9);
});
});
describe('spellEcho', () => {
it('should give 10% chance to cast twice per level', () => {
expect(SKILLS_DEF.spellEcho).toBeDefined();
expect(SKILLS_DEF.spellEcho.max).toBe(3);
expect(SKILLS_DEF.spellEcho.desc).toContain('10% chance to cast twice');
});
// Note: spellEcho is probabilistic, verified in combat tick
});
// ─── 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', () => {
// Skill base cost is 100 for manaWell
// With focusedMind 5, cost should be 100 * 0.75 = 75
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', () => {
// Spell unlock cost is 100 for fireball
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');
});
// Note: This is tested in cancelStudy integration
});
// ─── 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);
});
});
describe('manaSpring', () => {
it('should add +2 mana regen', () => {
const state = createMockState({ skills: { manaSpring: 1 } });
expect(computeRegen(state)).toBe(2 + 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);
});
});
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);
});
});
// ─── 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');
});
// Note: guardianBane is checked in calcDamage when floor is guardian floor
});
// ─── Crafting Skills ────────────────────────────────────────────────────────
describe('effCrafting', () => {
it('should reduce craft time by 10% per level', () => {
expect(SKILLS_DEF.effCrafting).toBeDefined();
expect(SKILLS_DEF.effCrafting.max).toBe(5);
expect(SKILLS_DEF.effCrafting.desc).toContain('10% craft time');
});
});
describe('durableConstruct', () => {
it('should add +1 max durability per level', () => {
expect(SKILLS_DEF.durableConstruct).toBeDefined();
expect(SKILLS_DEF.durableConstruct.max).toBe(5);
expect(SKILLS_DEF.durableConstruct.desc).toContain('+1 max durability');
});
});
describe('fieldRepair', () => {
it('should add +15% repair efficiency per level', () => {
expect(SKILLS_DEF.fieldRepair).toBeDefined();
expect(SKILLS_DEF.fieldRepair.max).toBe(5);
expect(SKILLS_DEF.fieldRepair.desc).toContain('15% repair efficiency');
});
});
describe('elemCrafting', () => {
it('should add +25% craft output per level', () => {
expect(SKILLS_DEF.elemCrafting).toBeDefined();
expect(SKILLS_DEF.elemCrafting.max).toBe(3);
expect(SKILLS_DEF.elemCrafting.desc).toContain('25% craft output');
});
});
});
// ─── Skill Requirement Tests ─────────────────────────────────────────────────
describe('Skill Requirements', () => {
it('deepReservoir should require manaWell 5', () => {
expect(SKILLS_DEF.deepReservoir.req).toEqual({ manaWell: 5 });
});
it('arcaneFury should require combatTrain 3', () => {
expect(SKILLS_DEF.arcaneFury.req).toEqual({ combatTrain: 3 });
});
it('elementalMastery should require arcaneFury 2', () => {
expect(SKILLS_DEF.elementalMastery.req).toEqual({ arcaneFury: 2 });
});
it('spellEcho should require quickCast 3', () => {
expect(SKILLS_DEF.spellEcho.req).toEqual({ quickCast: 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('elemCrafting should require effCrafting 3', () => {
expect(SKILLS_DEF.elemCrafting.req).toEqual({ effCrafting: 3 });
});
});
// ─── Skill Evolution Tests ─────────────────────────────────────────────────────
describe('Skill Evolution System', () => {
describe('SKILL_EVOLUTION_PATHS', () => {
it('should have evolution paths for 6 base skills', () => {
expect(SKILL_EVOLUTION_PATHS.manaWell).toBeDefined();
expect(SKILL_EVOLUTION_PATHS.manaFlow).toBeDefined();
expect(SKILL_EVOLUTION_PATHS.combatTrain).toBeDefined();
expect(SKILL_EVOLUTION_PATHS.quickLearner).toBeDefined();
expect(SKILL_EVOLUTION_PATHS.focusedMind).toBeDefined();
expect(SKILL_EVOLUTION_PATHS.elemAttune).toBeDefined();
});
it('should have 5 tiers for each base skill', () => {
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
expect(path.tiers).toHaveLength(5);
});
});
it('should have increasing multipliers for each tier', () => {
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
let prevMult = 0;
path.tiers.forEach(tier => {
expect(tier.multiplier).toBeGreaterThan(prevMult);
prevMult = tier.multiplier;
});
});
});
});
describe('getUpgradesForSkillAtMilestone', () => {
it('should return 4 upgrades at each milestone', () => {
const upgradesL5 = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const upgradesL10 = getUpgradesForSkillAtMilestone('manaWell', 10, {});
expect(upgradesL5).toHaveLength(4);
expect(upgradesL10).toHaveLength(4);
});
it('should return empty array for non-evolvable skills', () => {
const upgrades = getUpgradesForSkillAtMilestone('nonexistent', 5, {});
expect(upgrades).toHaveLength(0);
});
it('should have unique upgrade IDs for each milestone', () => {
const upgradesL5 = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const upgradesL10 = getUpgradesForSkillAtMilestone('manaWell', 10, {});
const l5Ids = upgradesL5.map(u => u.id);
const l10Ids = upgradesL10.map(u => u.id);
// No overlap between milestones
expect(l5Ids.find(id => l10Ids.includes(id))).toBeUndefined();
});
it('should have valid effect types', () => {
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
path.tiers.forEach(tier => {
tier.upgrades.forEach(upgrade => {
expect(['multiplier', 'bonus', 'special']).toContain(upgrade.effect.type);
});
});
});
});
});
describe('getNextTierSkill', () => {
it('should return next tier for base skills', () => {
expect(getNextTierSkill('manaWell')).toBe('manaWell_t2');
expect(getNextTierSkill('manaFlow')).toBe('manaFlow_t2');
});
it('should return next tier for tier skills', () => {
expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3');
expect(getNextTierSkill('manaWell_t3')).toBe('manaWell_t4');
expect(getNextTierSkill('manaWell_t4')).toBe('manaWell_t5');
});
it('should return null for max tier', () => {
expect(getNextTierSkill('manaWell_t5')).toBeNull();
});
it('should return null for non-evolvable skills', () => {
expect(getNextTierSkill('nonexistent')).toBeNull();
});
});
describe('getTierMultiplier', () => {
it('should return 1 for tier 1 skills', () => {
expect(getTierMultiplier('manaWell')).toBe(1);
expect(getTierMultiplier('manaWell_t1')).toBe(1);
});
it('should return correct multiplier for higher tiers', () => {
// Each tier is 10x more powerful so tier N level 1 = tier N-1 level 10
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('should return 1 for non-evolvable skills', () => {
expect(getTierMultiplier('nonexistent')).toBe(1);
});
});
describe('generateTierSkillDef', () => {
it('should generate valid skill definition for each tier', () => {
for (let tier = 1; tier <= 5; tier++) {
const def = generateTierSkillDef('manaWell', tier);
expect(def).toBeDefined();
expect(def?.tier).toBe(tier);
expect(def?.baseSkill).toBe('manaWell');
expect(def?.tierMultiplier).toBe(SKILL_EVOLUTION_PATHS.manaWell.tiers[tier - 1].multiplier);
}
});
it('should return null for non-evolvable base skill', () => {
expect(generateTierSkillDef('nonexistent', 1)).toBeNull();
});
it('should return null for invalid tier', () => {
expect(generateTierSkillDef('manaWell', 0)).toBeNull();
expect(generateTierSkillDef('manaWell', 6)).toBeNull();
});
it('should have correct tier names', () => {
expect(generateTierSkillDef('manaWell', 1)?.name).toBe('Mana Well');
expect(generateTierSkillDef('manaWell', 2)?.name).toBe('Deep Reservoir');
expect(generateTierSkillDef('manaWell', 3)?.name).toBe('Abyssal Pool');
expect(generateTierSkillDef('manaWell', 4)?.name).toBe('Ocean of Power');
expect(generateTierSkillDef('manaWell', 5)?.name).toBe('Infinite Reservoir');
});
it('should have scaling study time and cost', () => {
const def1 = generateTierSkillDef('manaWell', 1);
const def2 = generateTierSkillDef('manaWell', 2);
const def5 = generateTierSkillDef('manaWell', 5);
expect(def2?.studyTime).toBeGreaterThan(def1?.studyTime || 0);
expect(def5?.studyTime).toBeGreaterThan(def2?.studyTime || 0);
expect(def5?.base).toBeGreaterThan(def1?.base || 0);
});
});
describe('Upgrade Effect Validation', () => {
it('should have valid effect structures', () => {
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
path.tiers.forEach(tier => {
tier.upgrades.forEach(upgrade => {
const eff = upgrade.effect;
if (eff.type === 'multiplier' || eff.type === 'bonus') {
expect(eff.value).toBeDefined();
expect(eff.value).toBeGreaterThan(0);
}
if (eff.type === 'special') {
expect(eff.specialId).toBeDefined();
}
});
});
});
});
it('should have descriptive names for all upgrades', () => {
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
path.tiers.forEach(tier => {
tier.upgrades.forEach(upgrade => {
expect(upgrade.name.length).toBeGreaterThan(0);
expect(upgrade.desc.length).toBeGreaterThan(0);
});
});
});
});
});
describe('Milestone Upgrade Choices', () => {
it('should only allow 2 upgrades per milestone', () => {
// This is enforced by canSelectUpgrade in the store
// We just verify there are 4 options available
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
path.tiers.forEach(tier => {
const l5Upgrades = tier.upgrades.filter(u => u.milestone === 5);
const l10Upgrades = tier.upgrades.filter(u => u.milestone === 10);
expect(l5Upgrades).toHaveLength(4);
expect(l10Upgrades).toHaveLength(4);
});
});
});
it('should have unique upgrade IDs within each path', () => {
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
const allIds: string[] = [];
path.tiers.forEach(tier => {
tier.upgrades.forEach(upgrade => {
expect(allIds).not.toContain(upgrade.id);
allIds.push(upgrade.id);
});
});
});
});
});
});
// ─── Milestone Upgrade Effect Tests ─────────────────────────────────────────────
describe('Milestone Upgrade Effects', () => {
describe('Multiplier Effect Upgrades', () => {
it('should have correct multiplier effect structure', () => {
// Get the Expanded Capacity upgrade (+25% max mana bonus)
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const capacityUpgrade = upgrades.find(u => u.id === 'mw_t1_l5_capacity');
expect(capacityUpgrade).toBeDefined();
expect(capacityUpgrade?.effect.type).toBe('multiplier');
expect(capacityUpgrade?.effect.stat).toBe('maxMana');
expect(capacityUpgrade?.effect.value).toBe(1.25);
});
it('should have multiplier upgrades for different stats', () => {
// Mana Well: +25% max mana
const mwUpgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const mwMult = mwUpgrades.find(u => u.effect.type === 'multiplier');
expect(mwMult?.effect.stat).toBe('maxMana');
// Mana Flow: +25% regen speed
const mfUpgrades = getUpgradesForSkillAtMilestone('manaFlow', 5, {});
const mfMult = mfUpgrades.find(u => u.effect.type === 'multiplier');
expect(mfMult?.effect.stat).toBe('regen');
// Combat Training: +25% base damage
const ctUpgrades = getUpgradesForSkillAtMilestone('combatTrain', 5, {});
const ctMult = ctUpgrades.find(u => u.effect.type === 'multiplier');
expect(ctMult?.effect.stat).toBe('baseDamage');
// Quick Learner: +25% study speed
const qlUpgrades = getUpgradesForSkillAtMilestone('quickLearner', 5, {});
const qlMult = qlUpgrades.find(u => u.effect.type === 'multiplier');
expect(qlMult?.effect.stat).toBe('studySpeed');
});
it('should have multiplier values greater than 1 for positive effects', () => {
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
path.tiers.forEach(tier => {
tier.upgrades.forEach(upgrade => {
if (upgrade.effect.type === 'multiplier') {
// Most multipliers should be > 1 (bonuses) or < 1 (reductions)
expect(upgrade.effect.value).toBeGreaterThan(0);
}
});
});
});
});
it('should have cost reduction multipliers less than 1', () => {
// Mana Efficiency: -5% spell costs = 0.95 multiplier
const mwUpgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {});
const efficiency = mwUpgrades.find(u => u.id === 'mw_t1_l10_efficiency');
expect(efficiency).toBeDefined();
expect(efficiency?.effect.type).toBe('multiplier');
expect(efficiency?.effect.value).toBeLessThan(1);
expect(efficiency?.effect.value).toBe(0.95);
});
});
describe('Bonus Effect Upgrades', () => {
it('should have correct bonus effect structure', () => {
// Get the Natural Spring upgrade (+0.5 regen per hour)
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const regenUpgrade = upgrades.find(u => u.id === 'mw_t1_l5_regen');
expect(regenUpgrade).toBeDefined();
expect(regenUpgrade?.effect.type).toBe('bonus');
expect(regenUpgrade?.effect.stat).toBe('regen');
expect(regenUpgrade?.effect.value).toBe(0.5);
});
it('should have bonus upgrades for different stats', () => {
// Mana Well: +0.5 regen per hour
const mwUpgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const mwBonus = mwUpgrades.find(u => u.effect.type === 'bonus' && u.effect.stat === 'regen');
expect(mwBonus).toBeDefined();
// Mana Flow: +1 regen permanently
const mfUpgrades = getUpgradesForSkillAtMilestone('manaFlow', 10, {});
const mfBonus = mfUpgrades.find(u => u.effect.type === 'bonus' && u.effect.stat === 'permanentRegen');
expect(mfBonus).toBeDefined();
expect(mfBonus?.effect.value).toBe(1);
});
it('should have positive bonus values', () => {
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
path.tiers.forEach(tier => {
tier.upgrades.forEach(upgrade => {
if (upgrade.effect.type === 'bonus') {
expect(upgrade.effect.value).toBeGreaterThan(0);
}
});
});
});
});
it('should have tier 2+ bonus upgrades that scale appropriately', () => {
// Tier 2 Mana Well L10: +1000 max mana
const mwT2Upgrades = SKILL_EVOLUTION_PATHS.manaWell.tiers[1].upgrades;
const oceanUpgrade = mwT2Upgrades.find(u => u.id === 'mw_t2_l10_ocean');
expect(oceanUpgrade).toBeDefined();
expect(oceanUpgrade?.effect.type).toBe('bonus');
expect(oceanUpgrade?.effect.stat).toBe('maxMana');
expect(oceanUpgrade?.effect.value).toBe(1000);
});
});
describe('Special Effect Upgrades', () => {
it('should have correct special effect structure', () => {
// Get the Mana Threshold upgrade (special)
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const thresholdUpgrade = upgrades.find(u => u.id === 'mw_t1_l5_threshold');
expect(thresholdUpgrade).toBeDefined();
expect(thresholdUpgrade?.effect.type).toBe('special');
expect(thresholdUpgrade?.effect.specialId).toBe('manaThreshold');
expect(thresholdUpgrade?.effect.specialDesc).toBeDefined();
});
it('should have unique special IDs for each special effect', () => {
const allSpecialIds: string[] = [];
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
path.tiers.forEach(tier => {
tier.upgrades.forEach(upgrade => {
if (upgrade.effect.type === 'special') {
expect(allSpecialIds).not.toContain(upgrade.effect.specialId);
allSpecialIds.push(upgrade.effect.specialId!);
}
});
});
});
});
it('should have descriptive special descriptions', () => {
Object.values(SKILL_EVOLUTION_PATHS).forEach(path => {
path.tiers.forEach(tier => {
tier.upgrades.forEach(upgrade => {
if (upgrade.effect.type === 'special') {
expect(upgrade.effect.specialDesc).toBeDefined();
expect(upgrade.effect.specialDesc!.length).toBeGreaterThan(0);
}
});
});
});
});
it('should have special effects at both milestones', () => {
// Level 5 special
const l5Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const l5Special = l5Upgrades.find(u => u.effect.type === 'special');
expect(l5Special).toBeDefined();
expect(l5Special?.milestone).toBe(5);
// Level 10 special
const l10Upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {});
const l10Special = l10Upgrades.find(u => u.effect.type === 'special');
expect(l10Special).toBeDefined();
expect(l10Special?.milestone).toBe(10);
});
});
});
// ─── Upgrade Selection Tests ───────────────────────────────────────────────────
describe('Upgrade Selection System', () => {
describe('getSkillUpgradeChoices', () => {
it('should return 4 available upgrades at each milestone', () => {
const l5Choices = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const l10Choices = getUpgradesForSkillAtMilestone('manaWell', 10, {});
expect(l5Choices).toHaveLength(4);
expect(l10Choices).toHaveLength(4);
});
it('should return empty array for non-evolvable skills', () => {
const upgrades = getUpgradesForSkillAtMilestone('nonexistent', 5, {});
expect(upgrades).toHaveLength(0);
});
it('should return correct upgrades for tier skills', () => {
// Tier 2 should have different upgrades than tier 1
const t1Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 1 });
const t2Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 2 });
// Both should return tier 1 upgrades since we're asking for base skill
// The skillTiers parameter tells us what tier the skill is currently at
expect(t1Upgrades).toHaveLength(4);
expect(t2Upgrades).toHaveLength(4);
});
it('should have upgrades with correct milestone values', () => {
const l5Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const l10Upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {});
l5Upgrades.forEach(u => expect(u.milestone).toBe(5));
l10Upgrades.forEach(u => expect(u.milestone).toBe(10));
});
});
describe('Upgrade Selection Constraints', () => {
it('should allow selecting upgrades when at milestone level', () => {
// The canSelectUpgrade function requires the skill level to be >= milestone
// This is tested by checking the logic in the store
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
expect(upgrades.length).toBe(4);
});
it('should only allow 2 upgrades per milestone', () => {
// The store enforces: selected.length >= 2 returns false
// We verify there are enough choices (4) to select 2 from
const l5Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const l10Upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {});
// Each milestone should have exactly 4 options to choose 2 from
expect(l5Upgrades.length).toBeGreaterThanOrEqual(2);
expect(l10Upgrades.length).toBeGreaterThanOrEqual(2);
});
it('should have unique upgrade IDs to prevent duplicate selection', () => {
const l5Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, {});
const l10Upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, {});
const l5Ids = l5Upgrades.map(u => u.id);
const l10Ids = l10Upgrades.map(u => u.id);
// No duplicate IDs within same milestone
expect(new Set(l5Ids).size).toBe(l5Ids.length);
expect(new Set(l10Ids).size).toBe(l10Ids.length);
// No overlap between milestones
const overlap = l5Ids.filter(id => l10Ids.includes(id));
expect(overlap).toHaveLength(0);
});
});
describe('Upgrade Persistence', () => {
it('should store upgrades in skillUpgrades record', () => {
// Create a mock state with upgrades
const state = createMockState({
skills: { manaWell: 5 },
skillUpgrades: { manaWell: ['mw_t1_l5_capacity', 'mw_t1_l5_regen'] }
});
expect(state.skillUpgrades['manaWell']).toBeDefined();
expect(state.skillUpgrades['manaWell']).toHaveLength(2);
expect(state.skillUpgrades['manaWell']).toContain('mw_t1_l5_capacity');
expect(state.skillUpgrades['manaWell']).toContain('mw_t1_l5_regen');
});
it('should persist upgrades across state changes', () => {
// Simulate state changes
const state1 = createMockState({
skills: { manaWell: 5 },
skillUpgrades: { manaWell: ['mw_t1_l5_capacity'] }
});
// State changes shouldn't affect upgrades
const state2 = {
...state1,
rawMana: 500,
day: 10,
};
expect(state2.skillUpgrades['manaWell']).toEqual(['mw_t1_l5_capacity']);
});
});
});
// ─── Tier Up System Tests ─────────────────────────────────────────────────────
describe('Tier Up System', () => {
describe('getNextTierSkill', () => {
it('should return correct next tier for base skills', () => {
expect(getNextTierSkill('manaWell')).toBe('manaWell_t2');
expect(getNextTierSkill('manaFlow')).toBe('manaFlow_t2');
expect(getNextTierSkill('combatTrain')).toBe('combatTrain_t2');
expect(getNextTierSkill('quickLearner')).toBe('quickLearner_t2');
expect(getNextTierSkill('focusedMind')).toBe('focusedMind_t2');
expect(getNextTierSkill('elemAttune')).toBe('elemAttune_t2');
});
it('should return correct next tier for tier skills', () => {
expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3');
expect(getNextTierSkill('manaWell_t3')).toBe('manaWell_t4');
expect(getNextTierSkill('manaWell_t4')).toBe('manaWell_t5');
});
it('should return null for max tier (tier 5)', () => {
expect(getNextTierSkill('manaWell_t5')).toBeNull();
});
it('should return null for non-evolvable skills', () => {
expect(getNextTierSkill('nonexistent')).toBeNull();
});
});
describe('getTierMultiplier', () => {
it('should return 1 for tier 1 skills', () => {
expect(getTierMultiplier('manaWell')).toBe(1);
expect(getTierMultiplier('manaFlow')).toBe(1);
});
it('should return increasing multipliers for higher tiers', () => {
// Each tier is 10x more powerful
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('should return consistent multipliers across skill paths', () => {
// All skills should have same tier multipliers
expect(getTierMultiplier('manaWell_t2')).toBe(getTierMultiplier('manaFlow_t2'));
expect(getTierMultiplier('manaWell_t3')).toBe(getTierMultiplier('combatTrain_t3'));
expect(getTierMultiplier('manaWell_t4')).toBe(getTierMultiplier('quickLearner_t4'));
});
});
describe('Tier Up Mechanics', () => {
it('should start new tier at level 1 after tier up', () => {
// Simulate the tier up result
const baseSkillId = 'manaWell';
const nextTierId = getNextTierSkill(baseSkillId);
expect(nextTierId).toBe('manaWell_t2');
// After tier up, the skill should be at level 1
const newSkills = { [nextTierId!]: 1 };
expect(newSkills['manaWell_t2']).toBe(1);
});
it('should carry over upgrades to new tier', () => {
// Simulate tier up with existing upgrades
const oldUpgrades = ['mw_t1_l5_capacity', 'mw_t1_l10_echo'];
const nextTierId = 'manaWell_t2';
// Upgrades should be carried over
const newSkillUpgrades = { [nextTierId]: oldUpgrades };
expect(newSkillUpgrades['manaWell_t2']).toEqual(oldUpgrades);
});
it('should update skillTiers when tiering up', () => {
// Tier up from base to tier 2
const baseSkillId = 'manaWell';
const nextTier = 2;
const newSkillTiers = { [baseSkillId]: nextTier };
expect(newSkillTiers['manaWell']).toBe(2);
});
it('should remove old skill when tiering up', () => {
// After tier up, old skill should be removed
const oldSkillId = 'manaWell';
const newSkillId = 'manaWell_t2';
const newSkills: Record<string, number> = {};
newSkills[newSkillId] = 1;
expect(newSkills[oldSkillId]).toBeUndefined();
expect(newSkills[newSkillId]).toBe(1);
});
});
describe('generateTierSkillDef', () => {
it('should generate valid skill definitions for all tiers', () => {
for (let tier = 1; tier <= 5; tier++) {
const def = generateTierSkillDef('manaWell', tier);
expect(def).toBeDefined();
expect(def?.tier).toBe(tier);
expect(def?.max).toBe(10);
}
});
it('should have correct tier names', () => {
expect(generateTierSkillDef('manaWell', 1)?.name).toBe('Mana Well');
expect(generateTierSkillDef('manaWell', 2)?.name).toBe('Deep Reservoir');
expect(generateTierSkillDef('manaWell', 3)?.name).toBe('Abyssal Pool');
expect(generateTierSkillDef('manaWell', 4)?.name).toBe('Ocean of Power');
expect(generateTierSkillDef('manaWell', 5)?.name).toBe('Infinite Reservoir');
});
it('should have scaling costs and study times', () => {
const tier1 = generateTierSkillDef('manaWell', 1);
const tier5 = generateTierSkillDef('manaWell', 5);
expect(tier5?.base).toBeGreaterThan(tier1?.base || 0);
expect(tier5?.studyTime).toBeGreaterThan(tier1?.studyTime || 0);
});
it('should return null for invalid tiers', () => {
expect(generateTierSkillDef('manaWell', 0)).toBeNull();
expect(generateTierSkillDef('manaWell', 6)).toBeNull();
expect(generateTierSkillDef('nonexistent', 1)).toBeNull();
});
});
});
// ─── Tier Effect Computation Tests ─────────────────────────────────────────────
describe('Tier Effect Computation', () => {
describe('Tiered Skill Bonuses', () => {
it('should apply tier multiplier to skill bonuses', () => {
// Tier 1 manaWell level 5 should give +500 mana
const state1 = createMockState({
skills: { manaWell: 5 },
skillTiers: {}
});
const mana1 = computeMaxMana(state1);
// Base 100 + 5 * 100 = 600
expect(mana1).toBe(600);
});
it('should have tier multiplier affect effective level', () => {
// Tier 2 skill should have 10x multiplier
const state2 = createMockState({
skills: { manaWell_t2: 5 },
skillTiers: { manaWell: 2 }
});
// The computation should use tier multiplier
// This tests that the state structure is correct
expect(state2.skillTiers['manaWell']).toBe(2);
expect(state2.skills['manaWell_t2']).toBe(5);
});
});
describe('Tier Upgrade Access', () => {
it('should provide correct upgrades for current tier', () => {
// Tier 1 should access tier 1 upgrades
const t1Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 1 });
expect(t1Upgrades[0]?.id).toContain('mw_t1');
// Tier 2 should access tier 2 upgrades
const t2Upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 2 });
expect(t2Upgrades[0]?.id).toContain('mw_t2');
});
it('should have more powerful upgrades at higher tiers', () => {
const t1L5 = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 1 });
const t2L5 = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 2 });
// Find multiplier upgrades
const t1Mult = t1L5.find(u => u.effect.type === 'multiplier');
const t2Mult = t2L5.find(u => u.effect.type === 'multiplier');
// Tier 2 should have stronger effects
if (t1Mult && t2Mult) {
expect(t2Mult.effect.value).toBeGreaterThanOrEqual(t1Mult.effect.value);
}
});
});
});
// ─── Upgrade Effect Application Tests ─────────────────────────────────────────────
describe('Upgrade Effect Application', () => {
it('should apply max mana multiplier upgrade to computeMaxMana', () => {
// Without upgrade
const stateNoUpgrade = createMockState({
skills: { manaWell: 5 },
skillUpgrades: {}
});
const manaNoUpgrade = computeMaxMana(stateNoUpgrade);
// With Expanded Capacity (+25% max mana)
const stateWithUpgrade = createMockState({
skills: { manaWell: 5 },
skillUpgrades: { manaWell: ['mw_t1_l5_capacity'] }
});
const manaWithUpgrade = computeMaxMana(stateWithUpgrade);
// Should be 25% more
expect(manaWithUpgrade).toBe(Math.floor(manaNoUpgrade * 1.25));
});
it('should apply max mana bonus upgrade to computeMaxMana', () => {
// Without upgrade
const stateNoUpgrade = createMockState({
skills: { manaWell: 10 },
skillUpgrades: {}
});
const manaNoUpgrade = computeMaxMana(stateNoUpgrade);
// With Ocean of Mana (+1000 max mana at tier 2 level 10)
const stateWithUpgrade = createMockState({
skills: { manaWell_t2: 10 },
skillTiers: { manaWell: 2 },
skillUpgrades: { manaWell_t2: ['mw_t2_l10_ocean'] }
});
const manaWithUpgrade = computeMaxMana(stateWithUpgrade);
// Should have bonus added
expect(manaWithUpgrade).toBeGreaterThan(manaNoUpgrade);
});
it('should apply regen multiplier upgrade to computeRegen', () => {
// Without upgrade
const stateNoUpgrade = createMockState({
skills: { manaFlow: 5 },
skillUpgrades: {}
});
const regenNoUpgrade = computeRegen(stateNoUpgrade);
// With Rapid Flow (+25% regen)
const stateWithUpgrade = createMockState({
skills: { manaFlow: 5 },
skillUpgrades: { manaFlow: ['mf_t1_l5_rapid'] }
});
const regenWithUpgrade = computeRegen(stateWithUpgrade);
// Should be 25% more
expect(regenWithUpgrade).toBe(regenNoUpgrade * 1.25);
});
it('should apply regen bonus upgrade to computeRegen', () => {
// Without upgrade - base regen only
const stateNoUpgrade = createMockState({
skills: {},
skillUpgrades: {}
});
const regenNoUpgrade = computeRegen(stateNoUpgrade);
// With Natural Spring (+0.5 regen) - from manaWell upgrades
const stateWithUpgrade = createMockState({
skills: {},
skillUpgrades: { manaWell: ['mw_t1_l5_regen'] }
});
const regenWithUpgrade = computeRegen(stateWithUpgrade);
// Should have +0.5 bonus added
expect(regenNoUpgrade).toBe(2); // Base regen is 2
expect(regenWithUpgrade).toBe(2.5); // 2 + 0.5
});
it('should apply element cap multiplier upgrade to computeElementMax', () => {
// Without upgrade
const stateNoUpgrade = createMockState({
skills: { elemAttune: 5 },
skillUpgrades: {}
});
const elemNoUpgrade = computeElementMax(stateNoUpgrade);
// With Expanded Attunement (+25% element cap)
const stateWithUpgrade = createMockState({
skills: { elemAttune: 5 },
skillUpgrades: { elemAttune: ['ea_t1_l5_expand'] }
});
const elemWithUpgrade = computeElementMax(stateWithUpgrade);
// Should be 25% more
expect(elemWithUpgrade).toBe(Math.floor(elemNoUpgrade * 1.25));
});
it('should apply permanent regen bonus from upgrade', () => {
// With Ambient Absorption (+1 permanent regen)
const stateWithUpgrade = createMockState({
skills: { manaFlow: 10 },
skillUpgrades: { manaFlow: ['mf_t1_l10_ambient'] }
});
const regenWithUpgrade = computeRegen(stateWithUpgrade);
// Without upgrade
const stateNoUpgrade = createMockState({
skills: { manaFlow: 10 },
skillUpgrades: {}
});
const regenNoUpgrade = computeRegen(stateNoUpgrade);
// Should have +1 from permanent regen bonus
expect(regenWithUpgrade).toBe(regenNoUpgrade + 1);
});
it('should stack multiple upgrades correctly', () => {
// With two upgrades: +25% max mana AND +25% max mana (from tier 2)
const stateWithUpgrades = createMockState({
skills: { manaWell: 5 },
skillUpgrades: { manaWell: ['mw_t1_l5_capacity', 'mw_t1_l5_regen'] }
});
// The +25% max mana multiplier should be applied once
// The +0.5 regen bonus should be applied
const mana = computeMaxMana(stateWithUpgrades);
const regen = computeRegen(stateWithUpgrades);
// Base mana: 100 + 5*100 = 600, with 1.25x = 750
expect(mana).toBe(750);
// Base regen: 2 + 0 = 2, with +0.5 = 2.5
expect(regen).toBe(2.5);
});
it('should apply click mana bonuses', () => {
// Without upgrades
const stateNoUpgrade = createMockState({
skills: {},
skillUpgrades: {}
});
const clickNoUpgrade = computeClickMana(stateNoUpgrade);
// Base click mana is 1
expect(clickNoUpgrade).toBe(1);
});
});
// ─── Special Effect Tests ─────────────────────────────────────────────────────────
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from './upgrade-effects';
describe('Special Effect Application', () => {
describe('Mana Cascade', () => {
it('should add regen based on max mana', () => {
// Mana Cascade: +0.1 regen per 100 max mana
// With 1000 max mana, should add 1.0 regen
const state = createMockState({
skills: { manaFlow: 5, manaWell: 5 }, // manaWell 5 gives 500 mana, base 100 = 600
skillUpgrades: { manaFlow: ['mf_t1_l5_cascade'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(hasSpecial(effects, SPECIAL_EFFECTS.MANA_CASCADE)).toBe(true);
// Compute max mana to check cascade calculation
const maxMana = computeMaxMana(state);
// Expected cascade bonus: floor(maxMana / 100) * 0.1
const expectedCascadeBonus = Math.floor(maxMana / 100) * 0.1;
// Base regen: 2 + manaFlow level (5) = 7
// With cascade: 7 + cascadeBonus
const baseRegen = computeRegen(state);
// The regen should be increased by cascade bonus
// Note: computeRegen doesn't include cascade - computeEffectiveRegen does
expect(baseRegen).toBeGreaterThan(0);
});
});
describe('Steady Stream', () => {
it('should be recognized as active when selected', () => {
const state = createMockState({
skills: { manaFlow: 5 },
skillUpgrades: { manaFlow: ['mf_t1_l5_steady'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)).toBe(true);
});
});
describe('Mana Echo', () => {
it('should be recognized as active when selected', () => {
const state = createMockState({
skills: { manaWell: 10 },
skillUpgrades: { manaWell: ['mw_t1_l10_echo'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(hasSpecial(effects, SPECIAL_EFFECTS.MANA_ECHO)).toBe(true);
});
});
describe('Combat Special Effects', () => {
it('should recognize Berserker special effect', () => {
const state = createMockState({
skills: { combatTrain: 10 },
skillUpgrades: { combatTrain: ['ct_t1_l10_berserker'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER)).toBe(true);
});
it('should recognize Adrenaline Rush special effect', () => {
const state = createMockState({
skills: { combatTrain: 10 },
skillUpgrades: { combatTrain: ['ct_t1_l10_adrenaline'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(hasSpecial(effects, SPECIAL_EFFECTS.ADRENALINE_RUSH)).toBe(true);
});
});
describe('Study Special Effects', () => {
it('should recognize Mental Clarity special effect', () => {
const state = createMockState({
skills: { focusedMind: 5 },
skillUpgrades: { focusedMind: ['fm_t1_l5_clarity'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(hasSpecial(effects, SPECIAL_EFFECTS.MENTAL_CLARITY)).toBe(true);
});
it('should recognize Study Rush special effect', () => {
const state = createMockState({
skills: { focusedMind: 10 },
skillUpgrades: { focusedMind: ['fm_t1_l10_rush'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(hasSpecial(effects, SPECIAL_EFFECTS.STUDY_RUSH)).toBe(true);
});
it('should apply study speed multiplier from upgrades', () => {
// With Deep Focus (+25% study speed)
const state = createMockState({
skills: { quickLearner: 5 },
skillUpgrades: { quickLearner: ['ql_t1_l5_focus'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(effects.studySpeedMultiplier).toBe(1.25);
});
});
describe('Effect Stacking', () => {
it('should track multiple special effects simultaneously', () => {
const state = createMockState({
skills: { manaFlow: 10 },
skillUpgrades: { manaFlow: ['mf_t1_l5_cascade', 'mf_t1_l5_steady', 'mf_t1_l10_ambient'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(hasSpecial(effects, SPECIAL_EFFECTS.MANA_CASCADE)).toBe(true);
expect(hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)).toBe(true);
expect(effects.permanentRegenBonus).toBe(1); // Ambient Absorption gives +1 permanent regen
});
it('should stack multipliers correctly', () => {
// Rapid Flow (+25% regen) + River of Mana (+50% regen) at tier 2
const state = createMockState({
skills: { manaFlow_t2: 5 },
skillTiers: { manaFlow: 2 },
skillUpgrades: { manaFlow_t2: ['mf_t2_l5_river'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(effects.regenMultiplier).toBe(1.5); // River of Mana gives 1.5x
});
});
describe('Meditation Efficiency', () => {
it('should apply Deep Wellspring meditation efficiency', () => {
// Deep Wellspring: +50% meditation efficiency
const state = createMockState({
skills: { manaWell: 10 },
skillUpgrades: { manaWell: ['mw_t1_l10_meditation'] }
});
const effects = computeEffects(state.skillUpgrades, state.skillTiers || {});
expect(effects.meditationEfficiency).toBe(1.5); // +50% efficiency
});
it('should boost meditation bonus with efficiency', () => {
// Without efficiency
const baseBonus = getMeditationBonus(100, { meditation: 1 }, 1); // 4 hours with meditation skill
expect(baseBonus).toBe(2.5); // Base with meditation skill
// With Deep Wellspring (+50% efficiency)
const boostedBonus = getMeditationBonus(100, { meditation: 1 }, 1.5);
expect(boostedBonus).toBe(3.75); // 2.5 * 1.5 = 3.75
});
});
});
console.log('✅ All tests defined. Run with: bun test src/lib/game/store.test.ts');