// ─── Room Enchantments System Tests ──────────────────────────────────────────── // Tests for coverage computation, effect application, and equipment scanning. import { describe, it, expect } from 'vitest'; import { computeCoveragePerTick, applyRoomEnchantmentTick, getEquippedRoomEnchantments, isRoomEnchantmentSpecialId, getRoomEnchantmentBaseMagnitude, getRoomEnchantmentName, getRoomEnchantmentColor, BASE_COVERAGE_RATE, } from '../utils/room-enchantments-utils'; import type { EnemyState, EquipmentInstance } from '../types'; // ─── Helpers ─────────────────────────────────────────────────────────────────── function makeEnemy(overrides: Partial = {}): EnemyState { return { id: 'enemy-1', name: 'Test Enemy', hp: 100, maxHP: 100, armor: 0.1, dodgeChance: 0.05, element: 'fire', activeEffects: [], effectiveArmor: 0.1, ...overrides, }; } function makeEquipmentInstance(enchantmentIds: string[]): EquipmentInstance { return { instanceId: 'test-instance', typeId: 'test-boots', name: 'Test Boots', enchantments: enchantmentIds.map(id => ({ effectId: id, stacks: 1, capacityCost: 20, })), usedCapacity: 20, totalCapacity: 30, rarity: 'common', quality: 1, }; } // ─── computeCoveragePerTick ──────────────────────────────────────────────────── describe('computeCoveragePerTick', () => { it('returns base rate (0.2) with no discipline bonus', () => { expect(computeCoveragePerTick(0)).toBe(BASE_COVERAGE_RATE); expect(computeCoveragePerTick(0)).toBe(0.2); }); it('adds discipline bonus to base rate', () => { expect(computeCoveragePerTick(0.03)).toBe(0.23); expect(computeCoveragePerTick(0.12)).toBe(0.32); }); }); // ─── isRoomEnchantmentSpecialId ──────────────────────────────────────────────── describe('isRoomEnchantmentSpecialId', () => { it('returns true for valid room enchantment specialIds', () => { expect(isRoomEnchantmentSpecialId('room_fire_damage')).toBe(true); expect(isRoomEnchantmentSpecialId('room_frost_debuff')).toBe(true); expect(isRoomEnchantmentSpecialId('room_death_damage')).toBe(true); expect(isRoomEnchantmentSpecialId('room_lightning_damage')).toBe(true); expect(isRoomEnchantmentSpecialId('room_dark_dodge_debuff')).toBe(true); expect(isRoomEnchantmentSpecialId('room_earth_armor_debuff')).toBe(true); expect(isRoomEnchantmentSpecialId('room_transference_buff')).toBe(true); expect(isRoomEnchantmentSpecialId('room_transference_regen')).toBe(true); }); it('returns false for non-room enchantment specialIds', () => { expect(isRoomEnchantmentSpecialId('spellEcho10')).toBe(false); expect(isRoomEnchantmentSpecialId('fireBlade')).toBe(false); expect(isRoomEnchantmentSpecialId('')).toBe(false); }); }); // ─── getRoomEnchantmentBaseMagnitude ─────────────────────────────────────────── describe('getRoomEnchantmentBaseMagnitude', () => { it('returns correct base magnitudes', () => { expect(getRoomEnchantmentBaseMagnitude('room_fire_damage')).toBe(5); expect(getRoomEnchantmentBaseMagnitude('room_frost_debuff')).toBe(0.10); expect(getRoomEnchantmentBaseMagnitude('room_death_damage')).toBe(3); expect(getRoomEnchantmentBaseMagnitude('room_lightning_damage')).toBe(3); expect(getRoomEnchantmentBaseMagnitude('room_dark_dodge_debuff')).toBe(0.10); expect(getRoomEnchantmentBaseMagnitude('room_earth_armor_debuff')).toBe(0.05); expect(getRoomEnchantmentBaseMagnitude('room_transference_buff')).toBe(0.05); expect(getRoomEnchantmentBaseMagnitude('room_transference_regen')).toBe(0.10); }); it('returns 0 for unknown specialId', () => { expect(getRoomEnchantmentBaseMagnitude('unknown')).toBe(0); }); }); // ─── getRoomEnchantmentName ──────────────────────────────────────────────────── describe('getRoomEnchantmentName', () => { it('returns correct display names', () => { expect(getRoomEnchantmentName('room_fire_damage')).toBe('Blazing Footsteps'); expect(getRoomEnchantmentName('room_frost_debuff')).toBe('Frozen Trail'); expect(getRoomEnchantmentName('room_death_damage')).toBe('Necrotic Tread'); expect(getRoomEnchantmentName('room_lightning_damage')).toBe('Shocking Stride'); expect(getRoomEnchantmentName('room_dark_dodge_debuff')).toBe('Shadow Patch'); expect(getRoomEnchantmentName('room_earth_armor_debuff')).toBe('Scoured Earth'); expect(getRoomEnchantmentName('room_transference_buff')).toBe('Transference Grounds'); expect(getRoomEnchantmentName('room_transference_regen')).toBe('Conductive Path'); }); }); // ─── getEquippedRoomEnchantments ─────────────────────────────────────────────── describe('getEquippedRoomEnchantments', () => { it('returns empty array when no feet equipped', () => { const result = getEquippedRoomEnchantments({}, {}); expect(result).toEqual([]); }); it('returns empty array when feet has no room enchantments', () => { const instances = { 'inst-1': makeEquipmentInstance(['spell_echo_10']) }; const result = getEquippedRoomEnchantments({ feet: 'inst-1' }, instances); expect(result).toEqual([]); }); it('returns room enchantments from equipped footwear', () => { const instances = { 'inst-1': makeEquipmentInstance(['boots_sigil_fire']) }; const result = getEquippedRoomEnchantments({ feet: 'inst-1' }, instances); expect(result).toHaveLength(1); expect(result[0].specialId).toBe('room_fire_damage'); expect(result[0].baseMagnitude).toBe(5); }); it('returns multiple room enchantments from equipped footwear', () => { const instances = { 'inst-1': makeEquipmentInstance(['boots_sigil_fire', 'boots_sigil_frost']) }; const result = getEquippedRoomEnchantments({ feet: 'inst-1' }, instances); expect(result).toHaveLength(2); expect(result[0].specialId).toBe('room_fire_damage'); expect(result[1].specialId).toBe('room_frost_debuff'); }); }); // ─── applyRoomEnchantmentTick ────────────────────────────────────────────────── describe('applyRoomEnchantmentTick', () => { it('does nothing when no enemies', () => { const result = applyRoomEnchantmentTick({ coverage: 50, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_fire_damage', baseMagnitude: 5 }], enemies: [], rawMana: 100, }); expect(result.enemies).toEqual([]); expect(result.playerBuffs.castSpeedMultiplier).toBe(0); }); it('does nothing when no equipped room enchantments', () => { const enemies = [makeEnemy()]; const result = applyRoomEnchantmentTick({ coverage: 50, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [], enemies, rawMana: 100, }); expect(result.enemies[0].hp).toBe(100); }); it('AC-11: fire DoT scales linearly with coverage', () => { const enemies = [makeEnemy()]; // At 0% coverage, no damage const r0 = applyRoomEnchantmentTick({ coverage: 0, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_fire_damage', baseMagnitude: 5 }], enemies: [makeEnemy()], rawMana: 100, }); expect(r0.enemies[0].hp).toBe(100); // At 50% coverage: 5 * 0.5 = 2.5 damage const r50 = applyRoomEnchantmentTick({ coverage: 50, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_fire_damage', baseMagnitude: 5 }], enemies: [makeEnemy()], rawMana: 100, }); expect(r50.enemies[0].hp).toBeCloseTo(97.5, 1); // At 100% coverage: 5 * 1.0 = 5 damage const r100 = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_fire_damage', baseMagnitude: 5 }], enemies: [makeEnemy()], rawMana: 100, }); expect(r100.enemies[0].hp).toBe(95); }); it('AC-8: fire DoT hits all living enemies', () => { const enemies = [makeEnemy({ id: 'e1' }), makeEnemy({ id: 'e2' }), makeEnemy({ id: 'e3' })]; const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_fire_damage', baseMagnitude: 5 }], enemies, rawMana: 100, }); expect(result.enemies[0].hp).toBe(95); expect(result.enemies[1].hp).toBe(95); expect(result.enemies[2].hp).toBe(95); }); it('AC-8: fire DoT does not damage dead enemies', () => { const enemies = [makeEnemy({ id: 'e1', hp: 0 }), makeEnemy({ id: 'e2' })]; const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_fire_damage', baseMagnitude: 5 }], enemies, rawMana: 100, }); expect(result.enemies[0].hp).toBe(0); expect(result.enemies[1].hp).toBe(95); }); it('AC-11: aura magnitude multiplier increases effect', () => { // At 100% coverage with 1.5x multiplier: 5 * 1.0 * 1.5 = 7.5 damage const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1.5, equippedRoomEnchantments: [{ specialId: 'room_fire_damage', baseMagnitude: 5 }], enemies: [makeEnemy()], rawMana: 100, }); expect(result.enemies[0].hp).toBeCloseTo(92.5, 1); }); it('AC-10: transference buff returns cast speed multiplier', () => { const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_transference_buff', baseMagnitude: 0.05 }], enemies: [makeEnemy()], rawMana: 100, }); expect(result.playerBuffs.castSpeedMultiplier).toBe(0.05); }); it('AC-10: transference regen returns regen per hour', () => { const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_transference_regen', baseMagnitude: 0.10 }], enemies: [makeEnemy()], rawMana: 100, }); expect(result.playerBuffs.transferenceRegenPerHour).toBe(0.10); }); it('AC-9: frost debuff reduces enemy dodge chance', () => { const enemies = [makeEnemy({ dodgeChance: 0.3 })]; const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_frost_debuff', baseMagnitude: 0.10 }], enemies, rawMana: 100, }); // dodgeChance reduced by 0.10 (10% of 100% coverage) expect(result.enemies[0].dodgeChance).toBeCloseTo(0.20, 2); }); it('AC-9: earth debuff reduces enemy effective armor', () => { const enemies = [makeEnemy({ effectiveArmor: 0.3 })]; const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_earth_armor_debuff', baseMagnitude: 0.05 }], enemies, rawMana: 100, }); expect(result.enemies[0].effectiveArmor).toBeCloseTo(0.25, 2); }); it('AC-12: multiple enchantments share coverage and apply independently', () => { const enemies = [makeEnemy({ dodgeChance: 0.3 })]; const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [ { specialId: 'room_fire_damage', baseMagnitude: 5 }, { specialId: 'room_frost_debuff', baseMagnitude: 0.10 }, ], enemies, rawMana: 100, }); // Fire damage applied expect(result.enemies[0].hp).toBe(95); // Frost debuff applied expect(result.enemies[0].dodgeChance).toBeCloseTo(0.20, 2); }); it('death DoT deals 3 dmg/tick at 100% coverage', () => { const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_death_damage', baseMagnitude: 3 }], enemies: [makeEnemy()], rawMana: 100, }); expect(result.enemies[0].hp).toBe(97); }); it('dark dodge debuff reduces dodge chance', () => { const enemies = [makeEnemy({ dodgeChance: 0.5 })]; const result = applyRoomEnchantmentTick({ coverage: 100, auraMagnitudeMultiplier: 1, equippedRoomEnchantments: [{ specialId: 'room_dark_dodge_debuff', baseMagnitude: 0.10 }], enemies, rawMana: 100, }); expect(result.enemies[0].dodgeChance).toBeCloseTo(0.40, 2); }); }); // ─── Coverage cap ────────────────────────────────────────────────────────────── describe('coverage cap at 100', () => { it('coverage cannot exceed 100 in computeCoveragePerTick usage', () => { // Simulate what the combat tick does: min(100, coverage + rate) const rate = computeCoveragePerTick(0.12); // max tier rate const newCoverage = Math.min(100, 99.9 + rate); expect(newCoverage).toBe(100); }); });