Files
Mana-Loop/src/lib/game/data/golems/golemancy-data.test.ts
T
n8n-gitea 1a0886f702
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
chore: golemancy redesign cleanup — remove orphaned legacy code and update docs
2026-06-07 12:54:12 +02:00

346 lines
14 KiB
TypeScript

// ─── 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 + Enchanter 5', () => {
const req = FRAMES.crystalSteelHybrid.unlockRequirement;
expect(req.type).toBe('dual_attunement');
expect(isComponentUnlocked(req, { fabricator: { active: true, level: 5 }, enchanter: { active: true, level: 5 } }, [], [])).toBe(true);
expect(isComponentUnlocked(req, { fabricator: { active: true, level: 5 }, enchanter: { 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(0.3);
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(1.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(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);
expect(stats.upkeepCostPerHour.length).toBe(1);
expect(stats.upkeepCostPerHour[0].amount).toBe(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);
}
}
}
});
});