feat: blood magic and Runiczny Pancerz per vault answers (1.5.0)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
absorbBlood,
|
||||
endOfFightKrew,
|
||||
gainKrewFromWeaponDamage,
|
||||
greatSacrifice,
|
||||
resolveLifeSteal,
|
||||
sacrifice,
|
||||
selfHarm,
|
||||
startOfFightKrew,
|
||||
useLifeSteal,
|
||||
} from '../../src/logic/blood-magic';
|
||||
import { krewFromWeaponHpLoss, runicArmorPointsDisabled, startingKrew } from '../../src/logic/rules';
|
||||
|
||||
function makeActor(opts: { slugs?: string[]; blood?: number; hp?: number; zeal?: number; used?: Record<string, number> } = {}) {
|
||||
const flags: Record<string, unknown> = {};
|
||||
const actor: any = {
|
||||
system: {
|
||||
attributes: {
|
||||
blood: { value: opts.blood ?? 0, max: 20 },
|
||||
health: { value: opts.hp ?? 20, max: 30 },
|
||||
zeal: { value: opts.zeal ?? 3, max: 5 },
|
||||
magic: { value: 3, actual: 3 },
|
||||
bloodUsed: { selfHarm: 0, lifeSteal: 0, sacrifice: 0, absorb: 0, ...(opts.used ?? {}) },
|
||||
greatSacrificeUsed: 0,
|
||||
},
|
||||
},
|
||||
items: (opts.slugs ?? []).map((slug) => ({ type: 'talent', system: { slug } })),
|
||||
update: vi.fn(async () => undefined),
|
||||
setFlag: vi.fn(async (_s: string, k: string, v: unknown) => { flags[k] = v; }),
|
||||
getFlag: vi.fn((_s: string, k: string) => flags[k]),
|
||||
unsetFlag: vi.fn(async (_s: string, k: string) => { delete flags[k]; }),
|
||||
};
|
||||
return actor;
|
||||
}
|
||||
|
||||
describe('Samookaleczenie', () => {
|
||||
it('loses HP = Sukcesy and gains 1 Krew per HP, once per round', async () => {
|
||||
const actor = makeActor();
|
||||
const res = await selfHarm(actor, { roller: async () => 3 });
|
||||
expect(res).toMatchObject({ ok: true, hpLost: 3, krewGained: 3 });
|
||||
expect(actor.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
'system.attributes.health.value': 17,
|
||||
'system.attributes.blood.value': 3,
|
||||
'system.attributes.bloodUsed.selfHarm': 1,
|
||||
}));
|
||||
const again = makeActor({ used: { selfHarm: 1 } });
|
||||
expect((await selfHarm(again, { roller: async () => 3 })).reason).toBe('already-used');
|
||||
});
|
||||
|
||||
it('Większe Samookaleczenie needs the talent and gives 2 Krew per HP', async () => {
|
||||
expect((await selfHarm(makeActor(), { greater: true, roller: async () => 2 })).reason).toBe('missing-talent');
|
||||
const res = await selfHarm(makeActor({ slugs: ['krwisty-pojedynkowicz'] }), { greater: true, roller: async () => 2 });
|
||||
expect(res.krewGained).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kradzież Życia', () => {
|
||||
it('costs 1 Zapał, arms the next attack and pays Krew for HP actually lost', async () => {
|
||||
const actor = makeActor({ zeal: 2 });
|
||||
expect(await useLifeSteal(actor)).toEqual({ ok: true, stepsEasier: 1 });
|
||||
expect(actor.update).toHaveBeenCalledWith({ 'system.attributes.zeal.value': 1 });
|
||||
expect(await resolveLifeSteal(actor, 4)).toBe(4);
|
||||
});
|
||||
|
||||
it('greater version pays 2 Krew per HP and may be used twice per round', async () => {
|
||||
const actor = makeActor({ slugs: ['wieksza-kradziez-zycia'], used: { lifeSteal: 1 } });
|
||||
expect((await useLifeSteal(actor, { greater: true })).stepsEasier).toBe(2);
|
||||
expect(await resolveLifeSteal(actor, 3)).toBe(6);
|
||||
const spent = makeActor({ slugs: ['wieksza-kradziez-zycia'], used: { lifeSteal: 2 } });
|
||||
expect((await useLifeSteal(spent, { greater: true })).reason).toBe('already-used');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Złożenie w Ofierze / Wielka Ofiara', () => {
|
||||
it('heals Sukcesy + Magia and gains Krew = Sukcesy', async () => {
|
||||
const actor = makeActor({ hp: 10 });
|
||||
expect(await sacrifice(actor, 2)).toMatchObject({ ok: true, hpGained: 5, krewGained: 2 });
|
||||
});
|
||||
|
||||
it('Wielka Ofiara kills when Sukcesy exceed the highest Atrybut, else heals', async () => {
|
||||
const actor = makeActor({ slugs: ['rytualista-krwi'], hp: 10 });
|
||||
const kill = await greatSacrifice(actor, 6, { highestAttribute: 5, health: 12 });
|
||||
expect(kill).toMatchObject({ ok: true, kills: true, targetHpLost: 12, krewGained: 12 });
|
||||
const other = makeActor({ slugs: ['rytualista-krwi'], hp: 10 });
|
||||
expect(await greatSacrifice(other, 4, { highestAttribute: 5, health: 12 })).toMatchObject({ kills: false, hpGained: 4, krewGained: 0 });
|
||||
expect((await sacrifice(actor, 2)).reason).toBe('replaced-by-wielka-ofiara');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Wchłanianie Krwi', () => {
|
||||
it('needs at least 1 Krew; 2 HP per Krew once per round, 3 HP greater', async () => {
|
||||
expect((await absorbBlood(makeActor({ blood: 5 }), 0)).reason).toBe('minimum-one-blood');
|
||||
expect((await absorbBlood(makeActor({ blood: 5, hp: 10 }), 3))).toMatchObject({ ok: true, hpGained: 6 });
|
||||
expect((await absorbBlood(makeActor({ blood: 5, used: { absorb: 1 } }), 1)).reason).toBe('already-used');
|
||||
expect((await absorbBlood(makeActor({ blood: 5, hp: 10 }), 2, { greater: true }))).toMatchObject({ hpGained: 6 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('passive Krew', () => {
|
||||
it('Szacunek do Życia 1 per 4 HP, Krwisty Pojedynkowicz 1 per 2 HP', async () => {
|
||||
expect(krewFromWeaponHpLoss(9, 'respect-for-life')).toBe(2);
|
||||
expect(krewFromWeaponHpLoss(9, 'krwisty-pojedynkowicz')).toBe(4);
|
||||
expect(krewFromWeaponHpLoss(9, 'rytualista-krwi')).toBe(0);
|
||||
expect(await gainKrewFromWeaponDamage(makeActor({ slugs: ['respect-for-life'] }), 9)).toBe(2);
|
||||
expect(await gainKrewFromWeaponDamage(makeActor(), 9)).toBe(0);
|
||||
});
|
||||
|
||||
it('fight start/end: Rytualista starts with 4, leftover Krew heals 2 HP each', async () => {
|
||||
expect(startingKrew('rytualista-krwi')).toBe(4);
|
||||
const rit = makeActor({ slugs: ['rytualista-krwi'], blood: 0 });
|
||||
await startOfFightKrew(rit);
|
||||
expect(rit.update).toHaveBeenCalledWith({ 'system.attributes.blood.value': 4 });
|
||||
const end = makeActor({ slugs: ['respect-for-life'], blood: 5, hp: 10 });
|
||||
expect(await endOfFightKrew(end)).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Runiczny Pancerz – single hit', () => {
|
||||
it('> 5 disables 1 point; shield-breakers disable per 2 damage', () => {
|
||||
expect(runicArmorPointsDisabled(5)).toBe(0);
|
||||
expect(runicArmorPointsDisabled(6)).toBe(1);
|
||||
expect(runicArmorPointsDisabled(40)).toBe(1);
|
||||
expect(runicArmorPointsDisabled(7, 2)).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -80,6 +80,41 @@ describe('applyDamage', () => {
|
||||
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 });
|
||||
|
||||
@@ -17,7 +17,7 @@ function makeActor(overrides: Partial<any> = {}) {
|
||||
zeal: { value: 0, max: 5 },
|
||||
blood: { value: 0, max: 6 },
|
||||
elixirTolerance: 2,
|
||||
magicalArmor: { value: 4, runicCounter: 3 },
|
||||
magicalArmor: { value: 4, max: 4, runicCounter: 3 },
|
||||
...overrides.attributes,
|
||||
},
|
||||
skills: overrides.skills ?? {},
|
||||
@@ -94,7 +94,7 @@ describe('rest', () => {
|
||||
mana: { value: 1, max: 20 },
|
||||
zeal: { value: 0, max: 5 },
|
||||
blood: { value: 0, max: 6 },
|
||||
magicalArmor: { value: 4, runicCounter: 3 },
|
||||
magicalArmor: { value: 1, max: 4, runicCounter: 3 },
|
||||
},
|
||||
});
|
||||
const r = await rest(actor, 'long');
|
||||
@@ -103,7 +103,7 @@ describe('rest', () => {
|
||||
expect(updates['system.attributes.mana.value']).toBe(20);
|
||||
expect(updates['system.attributes.zeal.value']).toBe(5);
|
||||
expect(updates['system.attributes.blood.value']).toBe(6);
|
||||
expect(updates['system.attributes.magicalArmor.runicCounter']).toBe(0);
|
||||
expect(updates['system.attributes.magicalArmor.value']).toBe(4);
|
||||
expect(r.toleranceRecovered).toBe(1);
|
||||
expect(updates['system.attributes.breathersTaken']).toBe(0);
|
||||
expect(updates['system.attributes.manaBorrowed']).toBe(0);
|
||||
|
||||
Reference in New Issue
Block a user