Files
Mana-Loop/src/lib/game/data/golems/golemancy-data.test.ts
T
2026-06-08 14:14:38 +02:00

384 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ─── Golemancy Data Layer Tests ───────────────────────────────────────────────
// Tests component registries, unlock requirements, computed stats, and slot limits.
import { describe, it, expect } from 'vitest';
import {
CORES, FRAMES, MIND_CIRCUITS, GOLEM_ENCHANTMENTS,
ALL_CORES, ALL_FRAMES, ALL_MIND_CIRCUITS, ALL_GOLEM_ENCHANTMENTS,
} from '@/lib/game/data/golems';
import {
getGolemSlots,
isComponentUnlocked,
computeGolemStats,
canAffordGolemDesign,
createActiveGolem,
} from '@/lib/game/data/golems/utils';
import type { GolemDesign } from '@/lib/game/data/golems/types';
import type { RuntimeActiveGolem } from '@/lib/game/types';
// ─── Helper ───────────────────────────────────────────────────────────────────
function makeDesign(coreId: string, frameId: string, circuitId: string, enchantIds: string[] = []): GolemDesign {
return {
id: `test_${coreId}_${frameId}_${circuitId}`,
name: `Test ${coreId} ${frameId}`,
core: CORES[coreId],
frame: FRAMES[frameId],
mindCircuit: MIND_CIRCUITS[circuitId],
enchantments: enchantIds.map(id => GOLEM_ENCHANTMENTS[id]).filter(Boolean),
selectedManaTypes: [],
selectedSpells: [],
};
}
// ─── Component Registry Completeness ──────────────────────────────────────────
describe('Component registry completeness', () => {
it('has exactly 4 cores', () => {
expect(ALL_CORES.length).toBe(4);
expect(Object.keys(CORES).length).toBe(4);
});
it('has exactly 7 frames', () => {
expect(ALL_FRAMES.length).toBe(7);
expect(Object.keys(FRAMES).length).toBe(7);
});
it('has exactly 4 mind circuits', () => {
expect(ALL_MIND_CIRCUITS.length).toBe(4);
expect(Object.keys(MIND_CIRCUITS).length).toBe(4);
});
it('has exactly 8 enchantments', () => {
expect(ALL_GOLEM_ENCHANTMENTS.length).toBe(8);
expect(Object.keys(GOLEM_ENCHANTMENTS).length).toBe(8);
});
it('all cores have unique tiers 1-4', () => {
const tiers = ALL_CORES.map(c => c.tier).sort();
expect(tiers).toEqual([1, 2, 3, 4]);
});
it('all frames have valid special effects', () => {
const validSpecials = new Set(['none', 'aoe', 'slow', 'guardianConstruct']);
for (const frame of ALL_FRAMES) {
expect(validSpecials.has(frame.specialEffect)).toBe(true);
}
});
it('all circuits have valid behaviors', () => {
const validBehaviors = new Set(['basicOnly', 'castSpell1', 'alternate2', 'cycleAll']);
for (const circuit of ALL_MIND_CIRCUITS) {
expect(validBehaviors.has(circuit.behavior)).toBe(true);
}
});
});
// ─── Unlock Requirements ──────────────────────────────────────────────────────
describe('Unlock requirements', () => {
it('basic core requires Fabricator 2', () => {
const req = CORES.basic.unlockRequirement;
expect(req.type).toBe('attunement_level');
expect(isComponentUnlocked(req, { fabricator: { active: true, level: 2 } }, [], [])).toBe(true);
expect(isComponentUnlocked(req, { fabricator: { active: true, level: 1 } }, [], [])).toBe(false);
});
it('intermediate core requires dual attunement Fabricator 4 + Enchanter 2', () => {
const req = CORES.intermediate.unlockRequirement;
expect(req.type).toBe('dual_attunement');
expect(isComponentUnlocked(req, { fabricator: { active: true, level: 4 }, enchanter: { active: true, level: 2 } }, [], [])).toBe(true);
expect(isComponentUnlocked(req, { fabricator: { active: true, level: 4 }, enchanter: { active: true, level: 1 } }, [], [])).toBe(false);
});
it('guardian core requires guardian pact', () => {
const req = CORES.guardian.unlockRequirement;
expect(req.type).toBe('guardian_pact');
expect(isComponentUnlocked(req, { invoker: { active: true, level: 5 }, fabricator: { active: true, level: 5 } }, [], [1])).toBe(true);
expect(isComponentUnlocked(req, { invoker: { active: true, level: 5 }, fabricator: { active: true, level: 5 } }, [], [])).toBe(false);
});
it('crystalSteelHybrid frame requires Fabricator 5 only', () => {
const req = FRAMES.crystalSteelHybrid.unlockRequirement;
expect(req.type).toBe('attunement_level');
expect(isComponentUnlocked(req, { fabricator: { active: true, level: 5 } }, [], [])).toBe(true);
expect(isComponentUnlocked(req, { fabricator: { active: true, level: 4 } }, [], [])).toBe(false);
});
it('sand frame requires sand mana unlocked', () => {
const req = FRAMES.sand.unlockRequirement;
expect(req.type).toBe('mana_unlocked');
expect(isComponentUnlocked(req, {}, ['sand'], [])).toBe(true);
expect(isComponentUnlocked(req, {}, ['earth'], [])).toBe(false);
});
it('guardian circuit requires Invoker 5 + Fabricator 5', () => {
const req = MIND_CIRCUITS.guardian.unlockRequirement;
expect(req.type).toBe('dual_attunement');
expect(isComponentUnlocked(req, { invoker: { active: true, level: 5 }, fabricator: { active: true, level: 5 } }, [], [])).toBe(true);
expect(isComponentUnlocked(req, { invoker: { active: true, level: 4 }, fabricator: { active: true, level: 5 } }, [], [])).toBe(false);
});
});
// ─── Computed Stats ───────────────────────────────────────────────────────────
describe('Computed stats', () => {
it('basic earth golem has correct stats', () => {
const design = makeDesign('basic', 'earth', 'simple');
const stats = computeGolemStats(design);
expect(stats.manaCapacity).toBe(50);
expect(stats.manaRegen).toBe(0.5);
expect(stats.maxRoomDuration).toBe(3);
expect(stats.baseDamage).toBe(6);
expect(stats.attackSpeed).toBe(1.2);
expect(stats.armorPierce).toBe(0.05);
expect(stats.magicAffinity).toBe(0.3);
expect(stats.aoeTargets).toBe(1);
expect(stats.spellSlots).toBe(0);
expect(stats.enchantmentCapacity).toBeCloseTo(30); // Earth Frame 30% × Basic Core 1.0
expect(stats.specialEffect).toBe('none');
expect(stats.availableManaTypes).toEqual(['earth']);
});
it('advanced steel golem with advanced circuit', () => {
const design = makeDesign('advanced', 'steel', 'advanced');
const stats = computeGolemStats(design);
expect(stats.manaCapacity).toBe(200);
expect(stats.manaRegen).toBe(3.0);
expect(stats.maxRoomDuration).toBe(5);
expect(stats.baseDamage).toBe(18);
expect(stats.attackSpeed).toBe(1.6);
expect(stats.armorPierce).toBe(0.5);
expect(stats.magicAffinity).toBe(0.5);
expect(stats.spellSlots).toBe(2);
expect(stats.enchantmentCapacity).toBeCloseTo(100); // Steel Frame 50% × Advanced Core 2.0
});
it('guardian crystalSteelHybrid golem with guardian circuit', () => {
const design = makeDesign('guardian', 'crystalSteelHybrid', 'guardian');
const stats = computeGolemStats(design);
expect(stats.manaCapacity).toBe(500);
expect(stats.manaRegen).toBe(10.0);
expect(stats.maxRoomDuration).toBe(8);
expect(stats.baseDamage).toBe(22);
expect(stats.attackSpeed).toBe(2.8);
expect(stats.armorPierce).toBe(0.7);
expect(stats.magicAffinity).toBe(1.0);
expect(stats.spellSlots).toBe(4);
expect(stats.enchantmentCapacity).toBeCloseTo(300); // Crystal-Steel Hybrid 100% × Guardian Core 3.0
expect(stats.specialEffect).toBe('guardianConstruct');
});
it('enchantments add to summon cost', () => {
const designNoEnch = makeDesign('basic', 'earth', 'simple');
const designWithEnch = makeDesign('basic', 'earth', 'simple', ['sword_fire', 'sword_frost']);
const statsNoEnch = computeGolemStats(designNoEnch);
const statsWithEnch = computeGolemStats(designWithEnch);
expect(statsWithEnch.totalSummonCost.length).toBeGreaterThan(statsNoEnch.totalSummonCost.length);
});
it('upkeep = core.manaRegen * 2 per hour', () => {
const design = makeDesign('basic', 'earth', 'simple');
const stats = computeGolemStats(design);
// Basic core has 1 mana type (earth), so all upkeep goes to earth
expect(stats.upkeepCostPerHour.length).toBe(1);
expect(stats.upkeepCostPerHour[0].amount).toBe(1.0);
expect(stats.upkeepCostPerHour[0].element).toBe('earth');
});
it('multi-type core splits upkeep evenly across selected mana types', () => {
const design = makeDesign('intermediate', 'earth', 'simple');
design.selectedManaTypes = ['fire', 'water'];
const stats = computeGolemStats(design);
// Intermediate core: manaRegen=1.5, upkeep=3.0/hr split across 2 types = 1.5 each
expect(stats.upkeepCostPerHour.length).toBe(2);
expect(stats.upkeepCostPerHour[0].element).toBe('fire');
expect(stats.upkeepCostPerHour[0].amount).toBe(1.5);
expect(stats.upkeepCostPerHour[1].element).toBe('water');
expect(stats.upkeepCostPerHour[1].amount).toBe(1.5);
});
it('advanced core (3 types) splits upkeep three ways', () => {
const design = makeDesign('advanced', 'steel', 'simple');
design.selectedManaTypes = ['fire', 'water', 'earth'];
const stats = computeGolemStats(design);
// Advanced core: manaRegen=3.0, upkeep=6.0/hr split across 3 types = 2.0 each
expect(stats.upkeepCostPerHour.length).toBe(3);
const totalUpkeep = stats.upkeepCostPerHour.reduce((sum, c) => sum + c.amount, 0);
expect(totalUpkeep).toBe(6.0);
for (const cost of stats.upkeepCostPerHour) {
expect(cost.amount).toBe(2.0);
}
});
it('single-type core does not split upkeep', () => {
const design = makeDesign('basic', 'earth', 'simple');
// Basic core has manaTypes=['earth'], selectedManaTypes=[] => uses core.manaTypes
const stats = computeGolemStats(design);
expect(stats.upkeepCostPerHour.length).toBe(1);
expect(stats.upkeepCostPerHour[0].amount).toBe(1.0); // 0.5 * 2 = 1.0
expect(stats.upkeepCostPerHour[0].element).toBe('earth');
});
it('selected mana types override core defaults', () => {
const design = makeDesign('intermediate', 'earth', 'simple');
design.selectedManaTypes = ['fire', 'water'];
const stats = computeGolemStats(design);
expect(stats.availableManaTypes).toEqual(['fire', 'water']);
});
});
// ─── Golem Slot Calculation ───────────────────────────────────────────────────
describe('Golem slot calculation', () => {
it('returns 0 for fabricator level < 2', () => {
expect(getGolemSlots(0)).toBe(0);
expect(getGolemSlots(1)).toBe(0);
});
it('scales as floor(level/2)', () => {
expect(getGolemSlots(2)).toBe(1);
expect(getGolemSlots(3)).toBe(1);
expect(getGolemSlots(4)).toBe(2);
expect(getGolemSlots(5)).toBe(2);
expect(getGolemSlots(6)).toBe(3);
expect(getGolemSlots(7)).toBe(3);
expect(getGolemSlots(8)).toBe(4);
expect(getGolemSlots(9)).toBe(4);
expect(getGolemSlots(10)).toBe(5);
});
it('total slots capped at 7 (base + discipline bonus)', () => {
const baseSlots = getGolemSlots(10);
const discBonus = 2;
const total = Math.min(7, baseSlots + discBonus);
expect(total).toBe(7);
});
it('total slots cannot exceed 7 even with high fabricator level', () => {
const baseSlots = getGolemSlots(20);
const discBonus = 2;
const total = Math.min(7, baseSlots + discBonus);
expect(total).toBe(7);
});
});
// ─── Affordability ────────────────────────────────────────────────────────────
describe('canAffordGolemDesign', () => {
it('returns true when player has enough resources', () => {
const design = makeDesign('basic', 'earth', 'simple');
const result = canAffordGolemDesign(design, 100, {
earth: { current: 50, max: 100, unlocked: true },
});
expect(result.canAfford).toBe(true);
});
it('returns false when raw mana insufficient', () => {
const design = makeDesign('basic', 'earth', 'simple');
const result = canAffordGolemDesign(design, 3, {
earth: { current: 50, max: 100, unlocked: true },
});
expect(result.canAfford).toBe(false);
expect(result.missing).toContain('raw mana');
});
it('returns false when element mana insufficient', () => {
const design = makeDesign('basic', 'earth', 'simple');
const result = canAffordGolemDesign(design, 100, {
earth: { current: 5, max: 100, unlocked: true },
});
expect(result.canAfford).toBe(false);
expect(result.missing).toContain('earth');
});
it('returns false when element not unlocked', () => {
const design = makeDesign('basic', 'earth', 'simple');
const result = canAffordGolemDesign(design, 100, {});
expect(result.canAfford).toBe(false);
expect(result.missing).toContain('not unlocked');
});
});
// ─── Active Golem Creation ────────────────────────────────────────────────────
describe('createActiveGolem', () => {
it('creates RuntimeActiveGolem with correct initial state', () => {
const design = makeDesign('basic', 'earth', 'simple');
const active = createActiveGolem(design, 5) as RuntimeActiveGolem;
expect(active.designId).toBe(design.id);
expect(active.summonedFloor).toBe(5);
expect(active.attackProgress).toBe(0);
expect(active.roomsRemaining).toBe(3);
expect(active.currentMana).toBe(50);
expect(active.spellCastIndex).toBe(0);
});
it('guardian golem starts with full mana', () => {
const design = makeDesign('guardian', 'crystalSteelHybrid', 'guardian');
const active = createActiveGolem(design, 10) as RuntimeActiveGolem;
expect(active.currentMana).toBe(500);
expect(active.roomsRemaining).toBe(8);
});
});
// ─── Guardian Construct Requirements ──────────────────────────────────────────
describe('Guardian Construct requirements', () => {
it('guardian core has tier 4', () => {
expect(CORES.guardian.tier).toBe(4);
});
it('crystalSteelHybrid frame has guardianConstruct special effect', () => {
expect(FRAMES.crystalSteelHybrid.specialEffect).toBe('guardianConstruct');
});
it('guardian circuit has cycleAll behavior', () => {
expect(MIND_CIRCUITS.guardian.behavior).toBe('cycleAll');
});
it('guardian circuit has 4 spell slots', () => {
expect(MIND_CIRCUITS.guardian.spellSlots).toBe(4);
});
});
// ─── All Component Combinations ───────────────────────────────────────────────
describe('All component combinations', () => {
it('every core+frame+circuit combination produces valid stats', () => {
for (const core of ALL_CORES) {
for (const frame of ALL_FRAMES) {
for (const circuit of ALL_MIND_CIRCUITS) {
const design = makeDesign(core.id, frame.id, circuit.id);
const stats = computeGolemStats(design);
expect(stats.manaCapacity).toBeGreaterThan(0);
expect(stats.manaRegen).toBeGreaterThan(0);
expect(stats.maxRoomDuration).toBeGreaterThan(0);
expect(stats.baseDamage).toBeGreaterThan(0);
expect(stats.attackSpeed).toBeGreaterThan(0);
expect(stats.armorPierce).toBeGreaterThanOrEqual(0);
expect(stats.magicAffinity).toBeGreaterThanOrEqual(0);
expect(stats.aoeTargets).toBeGreaterThanOrEqual(1);
expect(stats.spellSlots).toBeGreaterThanOrEqual(0);
expect(stats.enchantmentCapacity).toBeGreaterThan(0);
expect(stats.totalSummonCost.length).toBeGreaterThan(0);
expect(stats.upkeepCostPerHour.length).toBeGreaterThan(0);
}
}
}
});
});