fe97ef60b4
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 7m37s
- Implement Pact-Weaving (Invoker + Enchanter) hybrid skill * Paths: The Weaver, The Warp, The World-Weaver * Weave Guardian essence into weapon enchantments/world-effects * Requires: Invoker 5+, Enchanter 5+ - Implement Guardian Constructs (Fabricator + Invoker) hybrid skill * Paths: The Architect, The Monumentalist, The Eternal * Only 1 active at a time, vastly more durable * Requires: Fabricator 5+, Invoker 5+ - Implement Enchanted Golemancy (Fabricator + Enchanter) hybrid skill * Paths: The Battle-Smith, The Enchanter-Smith, The Spell-Smith * Imbue golems with elemental spell logic * Requires: Fabricator 5+, Enchanter 5+ - All 512 tests passing
348 lines
12 KiB
TypeScript
348 lines
12 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
SKILL_EVOLUTION_PATHS,
|
|
getBaseSkillId,
|
|
generateTierSkillDef,
|
|
getUpgradesForSkillAtMilestone,
|
|
getNextTierSkill,
|
|
getTierMultiplier,
|
|
canTierUp,
|
|
getAvailableUpgrades,
|
|
} from '../skill-evolution';
|
|
import { SKILLS_DEF } from '../constants';
|
|
|
|
describe('Skill Evolution Paths', () => {
|
|
it('should have evolution paths for all skills with max > 1', () => {
|
|
const skillsWithMaxGt1 = Object.entries(SKILLS_DEF)
|
|
.filter(([_, def]) => def.max > 1)
|
|
.map(([id]) => id);
|
|
|
|
for (const skillId of skillsWithMaxGt1) {
|
|
expect(SKILL_EVOLUTION_PATHS[skillId], `Missing evolution path for ${skillId}`).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('should have at least one tier for each evolution path', () => {
|
|
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
expect(path.tiers.length, `${skillId} should have at least one tier`).toBeGreaterThanOrEqual(1);
|
|
expect(path.baseSkillId, `${skillId} baseSkillId should match`).toBe(skillId);
|
|
}
|
|
});
|
|
|
|
it('should have correct tier multipliers (10x per tier)', () => {
|
|
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
for (const tier of path.tiers) {
|
|
const expectedMultiplier = Math.pow(10, tier.tier - 1);
|
|
expect(tier.multiplier, `${skillId} tier ${tier.tier} multiplier`).toBe(expectedMultiplier);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should have valid skill IDs for each tier', () => {
|
|
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
for (const tier of path.tiers) {
|
|
if (tier.tier === 1) {
|
|
expect(tier.skillId, `${skillId} tier 1 skillId`).toBe(skillId);
|
|
} else {
|
|
expect(tier.skillId, `${skillId} tier ${tier.tier} skillId`).toContain('_t');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getBaseSkillId', () => {
|
|
it('should return the same ID for base skills', () => {
|
|
expect(getBaseSkillId('manaWell')).toBe('manaWell');
|
|
expect(getBaseSkillId('manaFlow')).toBe('manaFlow');
|
|
expect(getBaseSkillId('enchanting')).toBe('enchanting');
|
|
});
|
|
|
|
it('should extract base ID from tiered skills', () => {
|
|
expect(getBaseSkillId('manaWell_t2')).toBe('manaWell');
|
|
expect(getBaseSkillId('manaFlow_t3')).toBe('manaFlow');
|
|
expect(getBaseSkillId('enchanting_t5')).toBe('enchanting');
|
|
});
|
|
});
|
|
|
|
describe('generateTierSkillDef', () => {
|
|
it('should return null for non-existent skills', () => {
|
|
expect(generateTierSkillDef('nonexistent', 1)).toBeNull();
|
|
});
|
|
|
|
it('should return null for non-existent tiers', () => {
|
|
// Most skills don't have tier 10
|
|
expect(generateTierSkillDef('manaWell', 10)).toBeNull();
|
|
});
|
|
|
|
it('should return correct tier definition', () => {
|
|
const tier1 = generateTierSkillDef('manaWell', 1);
|
|
expect(tier1).not.toBeNull();
|
|
expect(tier1?.name).toBe('Mana Well');
|
|
expect(tier1?.tier).toBe(1);
|
|
expect(tier1?.multiplier).toBe(1);
|
|
|
|
const tier2 = generateTierSkillDef('manaWell', 2);
|
|
expect(tier2).not.toBeNull();
|
|
expect(tier2?.name).toBe('Deep Reservoir');
|
|
expect(tier2?.tier).toBe(2);
|
|
expect(tier2?.multiplier).toBe(10);
|
|
});
|
|
});
|
|
|
|
describe('getUpgradesForSkillAtMilestone', () => {
|
|
it('should return empty array for non-existent skills', () => {
|
|
const upgrades = getUpgradesForSkillAtMilestone('nonexistent', 5, {});
|
|
expect(upgrades).toEqual([]);
|
|
});
|
|
|
|
it('should return upgrades for manaWell at milestone 5', () => {
|
|
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 1 });
|
|
expect(upgrades.length).toBeGreaterThan(0);
|
|
|
|
// All should be milestone 5
|
|
for (const upgrade of upgrades) {
|
|
expect(upgrade.milestone).toBe(5);
|
|
}
|
|
});
|
|
|
|
it('should return upgrades for manaWell at milestone 10', () => {
|
|
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 10, { manaWell: 1 });
|
|
expect(upgrades.length).toBeGreaterThan(0);
|
|
|
|
// All should be milestone 10
|
|
for (const upgrade of upgrades) {
|
|
expect(upgrade.milestone).toBe(10);
|
|
}
|
|
});
|
|
|
|
it('should return tier 2 upgrades when at tier 2', () => {
|
|
const upgrades = getUpgradesForSkillAtMilestone('manaWell', 5, { manaWell: 2 });
|
|
expect(upgrades.length).toBeGreaterThan(0);
|
|
|
|
// Should have tier 2 specific upgrades
|
|
const upgradeIds = upgrades.map(u => u.id);
|
|
expect(upgradeIds.some(id => id.startsWith('mw_t2'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getNextTierSkill', () => {
|
|
it('should return null for non-existent skills', () => {
|
|
expect(getNextTierSkill('nonexistent')).toBeNull();
|
|
});
|
|
|
|
it('should return next tier for tier 1 skills', () => {
|
|
expect(getNextTierSkill('manaWell')).toBe('manaWell_t2');
|
|
expect(getNextTierSkill('manaFlow')).toBe('manaFlow_t2');
|
|
});
|
|
|
|
it('should return next tier for higher tier skills', () => {
|
|
expect(getNextTierSkill('manaWell_t2')).toBe('manaWell_t3');
|
|
expect(getNextTierSkill('manaWell_t3')).toBe('manaWell_t4');
|
|
});
|
|
|
|
it('should return null for max tier skills', () => {
|
|
// manaWell has 5 tiers
|
|
expect(getNextTierSkill('manaWell_t5')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getTierMultiplier', () => {
|
|
it('should return 1 for tier 1 skills', () => {
|
|
expect(getTierMultiplier('manaWell')).toBe(1);
|
|
expect(getTierMultiplier('manaFlow')).toBe(1);
|
|
});
|
|
|
|
it('should return 10 for tier 2 skills', () => {
|
|
expect(getTierMultiplier('manaWell_t2')).toBe(10);
|
|
expect(getTierMultiplier('manaFlow_t2')).toBe(10);
|
|
});
|
|
|
|
it('should return correct multiplier for higher tiers', () => {
|
|
expect(getTierMultiplier('manaWell_t3')).toBe(100);
|
|
expect(getTierMultiplier('manaWell_t4')).toBe(1000);
|
|
expect(getTierMultiplier('manaWell_t5')).toBe(10000);
|
|
});
|
|
});
|
|
|
|
describe('canTierUp', () => {
|
|
const mockAttunements = {
|
|
enchanter: { level: 10, active: true },
|
|
fabricator: { level: 10, active: true },
|
|
invoker: { level: 10, active: true },
|
|
};
|
|
|
|
it('should return false for non-existent skills', () => {
|
|
const result = canTierUp('nonexistent', 10, {}, mockAttunements);
|
|
expect(result.canTierUp).toBe(false);
|
|
expect(result.reason).toBe('No evolution path');
|
|
});
|
|
|
|
it('should return false if not at max level', () => {
|
|
const result = canTierUp('manaWell', 5, { manaWell: 1 }, mockAttunements);
|
|
expect(result.canTierUp).toBe(false);
|
|
expect(result.reason).toBe('Need level 10 to tier up');
|
|
});
|
|
|
|
it('should return true when at max level with attunement', () => {
|
|
const result = canTierUp('manaWell', 10, { manaWell: 1 }, mockAttunements);
|
|
expect(result.canTierUp).toBe(true);
|
|
});
|
|
|
|
it('should return false if already at max tier', () => {
|
|
const result = canTierUp('manaWell_t5', 10, { manaWell: 5 }, mockAttunements);
|
|
expect(result.canTierUp).toBe(false);
|
|
expect(result.reason).toBe('Already at max tier');
|
|
});
|
|
});
|
|
|
|
describe('getAvailableUpgrades', () => {
|
|
const manaWellTier1Tree = SKILL_EVOLUTION_PATHS.manaWell.tiers[0].upgrades;
|
|
|
|
it('should return only upgrades for specified milestone', () => {
|
|
const available = getAvailableUpgrades(
|
|
manaWellTier1Tree as any[],
|
|
[],
|
|
5,
|
|
[]
|
|
);
|
|
|
|
for (const upgrade of available) {
|
|
expect(upgrade.milestone).toBe(5);
|
|
}
|
|
});
|
|
|
|
it('should exclude already chosen upgrades', () => {
|
|
const available = getAvailableUpgrades(
|
|
manaWellTier1Tree as any[],
|
|
['mw_t1_l5_capacity'],
|
|
5,
|
|
['mw_t1_l5_capacity']
|
|
);
|
|
|
|
const ids = available.map(u => u.id);
|
|
expect(ids).not.toContain('mw_t1_l5_capacity');
|
|
});
|
|
});
|
|
|
|
describe('Skill Definitions', () => {
|
|
it('should have valid max levels for all skills', () => {
|
|
for (const [skillId, def] of Object.entries(SKILLS_DEF)) {
|
|
expect(def.max, `${skillId} max level`).toBeGreaterThanOrEqual(1);
|
|
expect(def.max, `${skillId} max level`).toBeLessThanOrEqual(10);
|
|
}
|
|
});
|
|
|
|
it('should have study time defined for all skills', () => {
|
|
for (const [skillId, def] of Object.entries(SKILLS_DEF)) {
|
|
expect(def.studyTime, `${skillId} study time`).toBeGreaterThanOrEqual(0);
|
|
}
|
|
});
|
|
|
|
it('should have base cost defined for all skills', () => {
|
|
for (const [skillId, def] of Object.entries(SKILLS_DEF)) {
|
|
expect(def.base, `${skillId} base cost`).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should have correct categories for skills', () => {
|
|
const validCategories = ['mana', 'study', 'research', 'ascension', 'enchant',
|
|
'effectResearch', 'invocation', 'pact', 'fabrication', 'golemancy', 'craft', 'hybrid'];
|
|
|
|
for (const [skillId, def] of Object.entries(SKILLS_DEF)) {
|
|
expect(validCategories, `${skillId} category`).toContain(def.cat);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Upgrade Tree Structure', () => {
|
|
it('should have valid effect types for all upgrades', () => {
|
|
const validTypes = ['multiplier', 'bonus', 'special'];
|
|
|
|
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
for (const tier of path.tiers) {
|
|
for (const upgrade of tier.upgrades) {
|
|
expect(validTypes, `${upgrade.id} effect type`).toContain(upgrade.effect.type);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should have milestone 5 or 10 for all upgrades', () => {
|
|
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
for (const tier of path.tiers) {
|
|
for (const upgrade of tier.upgrades) {
|
|
expect([5, 10], `${upgrade.id} milestone`).toContain(upgrade.milestone);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should have unique upgrade IDs within each skill', () => {
|
|
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
const allIds: string[] = [];
|
|
for (const tier of path.tiers) {
|
|
for (const upgrade of tier.upgrades) {
|
|
expect(allIds, `${upgrade.id} should be unique in ${skillId}`).not.toContain(upgrade.id);
|
|
allIds.push(upgrade.id);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should have descriptions for all upgrades', () => {
|
|
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
for (const tier of path.tiers) {
|
|
for (const upgrade of tier.upgrades) {
|
|
expect(upgrade.desc, `${upgrade.id} should have description`).toBeTruthy();
|
|
expect(upgrade.desc.length, `${upgrade.id} description length`).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should have special descriptions for special effects', () => {
|
|
for (const [skillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) {
|
|
for (const tier of path.tiers) {
|
|
for (const upgrade of tier.upgrades) {
|
|
if (upgrade.effect.type === 'special') {
|
|
expect(upgrade.effect.specialDesc, `${upgrade.id} should have special description`).toBeTruthy();
|
|
expect(upgrade.effect.specialId, `${upgrade.id} should have special ID`).toBeTruthy();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Skill Level and Tier Calculations', () => {
|
|
it('should calculate tier 2 level 1 as equivalent to tier 1 level 10', () => {
|
|
// Base skill gives +100 max mana per level
|
|
// At tier 1 level 10: 10 * 100 = 1000 max mana
|
|
// At tier 2 level 1 with 10x multiplier: 1 * 100 * 10 = 1000 max mana
|
|
const tier1Level10Effect = 10 * 100; // level * base
|
|
const tier2Level1Effect = 1 * 100 * 10; // level * base * tierMultiplier
|
|
|
|
expect(tier2Level1Effect).toBe(tier1Level10Effect);
|
|
});
|
|
|
|
it('should have increasing power with tiers', () => {
|
|
// Tier 3 level 1 should be stronger than tier 2 level 10
|
|
const tier2Level10 = 10 * 100 * 10; // level * base * tierMultiplier
|
|
const tier3Level1 = 1 * 100 * 100; // level * base * tierMultiplier
|
|
|
|
expect(tier3Level1).toBe(tier2Level10);
|
|
});
|
|
|
|
it('should have correct max tier for skills', () => {
|
|
// Skills with max 5 should typically have 2-3 tiers
|
|
// Skills with max 10 should typically have 3-5 tiers
|
|
|
|
const manaWellPath = SKILL_EVOLUTION_PATHS.manaWell;
|
|
expect(manaWellPath.tiers.length).toBe(5); // manaWell has 5 tiers
|
|
|
|
const manaFlowPath = SKILL_EVOLUTION_PATHS.manaFlow;
|
|
expect(manaFlowPath.tiers.length).toBe(5); // manaFlow has 5 tiers
|
|
});
|
|
});
|