fix: golem combat runtime - elemental matchup, enchantment effects, spell damage/cost, armor pierce (issue #313)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

This commit is contained in:
2026-06-08 10:12:18 +02:00
parent 0e1e506213
commit 1e99a57496
8 changed files with 691 additions and 73 deletions
@@ -0,0 +1,142 @@
// ─── Golem Combat Helpers Unit Tests (Issue #313) ──────────────────────────────
// Unit tests for computeBasicAttackDamage and resolveEnchantmentEffects.
import { describe, it, expect } from 'vitest';
import { computeBasicAttackDamage, resolveEnchantmentEffects } from './golem-combat-helpers';
// ─── computeBasicAttackDamage ────────────────────────────────────────────────
describe('computeBasicAttackDamage', () => {
it('returns baseDamage with no armor and no elemental effect', () => {
const dmg = computeBasicAttackDamage(
{ baseDamage: 10, armorPierce: 0, element: undefined },
0, 0, 'fire',
);
expect(dmg).toBe(10);
});
it('applies elemental bonus for super effective matchup', () => {
// Water vs Fire = 1.5x (water is opposite of fire)
const dmg = computeBasicAttackDamage(
{ baseDamage: 10, armorPierce: 0, element: 'water' },
0, 0, 'fire',
);
expect(dmg).toBe(15);
});
it('applies elemental penalty for weak matchup', () => {
// Lightning vs Earth = 0.75x (lightning is weak to earth)
const dmg = computeBasicAttackDamage(
{ baseDamage: 10, armorPierce: 0, element: 'lightning' },
0, 0, 'earth',
);
expect(dmg).toBe(7.5);
});
it('applies same-element bonus', () => {
const dmg = computeBasicAttackDamage(
{ baseDamage: 10, armorPierce: 0, element: 'fire' },
0, 0, 'fire',
);
expect(dmg).toBe(12.5);
});
it('never returns negative damage', () => {
const dmg = computeBasicAttackDamage(
{ baseDamage: 10, armorPierce: 0, element: undefined },
0, 0.99, 'fire',
);
expect(dmg).toBeGreaterThanOrEqual(0);
});
it('combines elemental bonus and armor pierce', () => {
// Water vs Fire = 1.5x, then 50% armor with 25% pierce
// effectiveArmor = 0.5 * (1 - 0.25) = 0.375
// dmg = 10 * 1.5 * (1 - 0.375) = 9.375
const dmg = computeBasicAttackDamage(
{ baseDamage: 10, armorPierce: 0.25, element: 'water' },
0, 0.5, 'fire',
);
expect(dmg).toBeCloseTo(9.375, 5);
});
it('fully bypasses armor when armorPierce is 1.0', () => {
const dmg = computeBasicAttackDamage(
{ baseDamage: 10, armorPierce: 1.0, element: undefined },
0, 0.8, 'fire',
);
expect(dmg).toBe(10);
});
it('applies no armor bypass when armorPierce is 0', () => {
const dmg = computeBasicAttackDamage(
{ baseDamage: 10, armorPierce: 0, element: undefined },
0, 0.5, 'fire',
);
expect(dmg).toBe(5);
});
it('stacks enchantment armorPierce with frame armorPierce', () => {
const totalPierce = Math.min(1, 0.5 + 0.15);
const dmg = computeBasicAttackDamage(
{ baseDamage: 20, armorPierce: 0.5, element: undefined },
0.15, 0.4, 'fire',
);
expect(dmg).toBe(20 * (1 - 0.4 * (1 - totalPierce)));
});
});
// ─── resolveEnchantmentEffects ────────────────────────────────────────────────
describe('resolveEnchantmentEffects', () => {
it('resolves sword_fire to burn effect', () => {
const effects = resolveEnchantmentEffects(['sword_fire']);
expect(effects).toHaveLength(1);
expect(effects[0].type).toBe('burn');
expect(effects[0].magnitude).toBe(3);
});
it('resolves sword_frost to slow effect', () => {
const effects = resolveEnchantmentEffects(['sword_frost']);
expect(effects).toHaveLength(1);
expect(effects[0].type).toBe('slow');
});
it('resolves sword_metal to armorPierce effect', () => {
const effects = resolveEnchantmentEffects(['sword_metal']);
expect(effects).toHaveLength(1);
expect(effects[0].type).toBe('armorPierce');
expect(effects[0].magnitude).toBe(0.15);
});
it('resolves sword_lightning to shock effect', () => {
const effects = resolveEnchantmentEffects(['sword_lightning']);
expect(effects).toHaveLength(1);
expect(effects[0].type).toBe('shock');
});
it('resolves sword_shadow to weaken effect', () => {
const effects = resolveEnchantmentEffects(['sword_shadow']);
expect(effects).toHaveLength(1);
expect(effects[0].type).toBe('weaken');
});
it('returns empty array for unknown enchantment IDs', () => {
const effects = resolveEnchantmentEffects(['nonexistent_enchant']);
expect(effects).toHaveLength(0);
});
it('resolves multiple enchantments', () => {
const effects = resolveEnchantmentEffects(['sword_fire', 'sword_lightning', 'sword_shadow']);
expect(effects).toHaveLength(3);
const types = effects.map(e => e.type);
expect(types).toContain('burn');
expect(types).toContain('shock');
expect(types).toContain('weaken');
});
it('handles empty array', () => {
const effects = resolveEnchantmentEffects([]);
expect(effects).toHaveLength(0);
});
});