// ─── 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); } } } }); });