import { describe, it, expect, vi, beforeEach } from 'vitest'; import { applyDamage } from '../../src/logic/damage'; function makeActor(overrides: Partial = {}) { const updates: Record[] = []; const actor: any = { name: 'Test', type: 'character', system: { attributes: { body: { value: 3 }, mind: { value: 3 }, soul: { value: 3 }, magicalArmor: { value: 0, runicCounter: 0 }, magicalShield: { value: 0 }, physicalArmor: { value: 0 }, health: { value: 30, max: 30 }, ...overrides, }, }, items: [], update: vi.fn(async (u: Record) => { updates.push(u); }), toggleStatusEffect: vi.fn(async () => undefined), }; return { actor, updates }; } describe('applyDamage', () => { beforeEach(() => { (globalThis as any).ChatMessage = { create: vi.fn(async () => undefined) }; }); it('applies plain HP damage when no armor', async () => { const { actor } = makeActor(); const r = await applyDamage(actor, { amount: 7, postChat: false }); expect(r.hpDamage).toBe(7); expect(actor.system.attributes.health.value).toBe(30); // pre-update; check call expect(actor.update).toHaveBeenCalledWith(expect.objectContaining({ 'system.attributes.health.value': 23, })); }); it('absorbs through magical armor + shield + physical armor', async () => { const { actor } = makeActor({ magicalArmor: { value: 3, runicCounter: 0 }, magicalShield: { value: 4 }, physicalArmor: { value: 2 }, }); const r = await applyDamage(actor, { amount: 12, postChat: false }); expect(r.absorbed.magicalArmor).toBe(3); expect(r.absorbed.magicalShield).toBe(4); expect(r.absorbed.physicalArmor).toBe(2); expect(r.hpDamage).toBe(3); }); it('ignoreMagicalArmor skips layer 1', async () => { const { actor } = makeActor({ magicalArmor: { value: 5, runicCounter: 0 }, }); const r = await applyDamage(actor, { amount: 4, ignoreMagicalArmor: true, postChat: false }); expect(r.absorbed.magicalArmor).toBe(0); expect(r.hpDamage).toBe(4); }); it('ignoreMagicalShield skips layer 2', async () => { const { actor } = makeActor({ magicalShield: { value: 10 }, }); const r = await applyDamage(actor, { amount: 5, ignoreMagicalShield: true, postChat: false }); expect(r.absorbed.magicalShield).toBe(0); expect(r.hpDamage).toBe(5); }); it('ignorePhysicalArmor skips layer 3', async () => { const { actor } = makeActor({ physicalArmor: { value: 4 }, }); const r = await applyDamage(actor, { amount: 6, ignorePhysicalArmor: true, postChat: false }); expect(r.absorbed.physicalArmor).toBe(0); expect(r.hpDamage).toBe(6); }); it('one hit above 5 (after Runiczny Pancerz) disables 1 point; hits never accumulate', async () => { const big = makeActor({ magicalArmor: { value: 3, max: 3 } }); const r = await applyDamage(big.actor, { amount: 10, postChat: false }); expect(r.runicArmorDisabled).toBe(1); expect(big.actor.update).toHaveBeenCalledWith(expect.objectContaining({ 'system.attributes.magicalArmor.value': 2 })); const small = makeActor({ magicalArmor: { value: 3, max: 3 } }); await applyDamage(small.actor, { amount: 7, postChat: false }); await applyDamage(small.actor, { amount: 7, postChat: false }); expect(small.updates.every((u) => u['system.attributes.magicalArmor.value'] === 3)).toBe(true); }); it('shield-breakers: every 2 damage disable a point and damage counts double against Magiczna Osłona', async () => { const { actor } = makeActor({ magicalArmor: { value: 4, max: 4 }, magicalShield: { value: 6 } }); const r = await applyDamage(actor, { amount: 10, runicArmorStep: 2, shieldMultiplier: 2, postChat: false }); expect(r.runicArmorDisabled).toBe(4); expect(r.absorbed.magicalShield).toBe(3); expect(r.hpDamage).toBe(3); }); it('Prawdziwa Dezintegracja destroys shield and disables all Runiczny Pancerz', async () => { const { actor } = makeActor({ magicalArmor: { value: 3, max: 3 }, magicalShield: { value: 50 } }); await applyDamage(actor, { amount: 10, destroyMagicalDefenses: true, ignorePhysicalArmor: true, postChat: false }); expect(actor.update).toHaveBeenCalledWith(expect.objectContaining({ 'system.attributes.magicalArmor.value': 0, 'system.attributes.magicalShield.value': 0, })); }); it('Rozerwanie Run permanently lowers the maximum', async () => { const { actor } = makeActor({ magicalArmor: { value: 3, max: 3 } }); const r = await applyDamage(actor, { amount: 1, destroyRunicPoints: 1, postChat: false }); expect(r.runicArmorDestroyed).toBe(1); expect(actor.update).toHaveBeenCalledWith(expect.objectContaining({ 'system.attributes.magicalArmor.max': 2 })); }); it('marks shield as dropped when reduced to 0', async () => { const { actor } = makeActor({ magicalShield: { value: 3 } }); const r = await applyDamage(actor, { amount: 5, postChat: false }); expect(r.shieldDropped).toBe(true); }); });