feat: sync rules with vault 2026-10-20 (borrowed mana, Odetchnięcie limit, Oszołomiony, Akcje Natychmiastowe)
Adds src/logic/rules.ts, counters, migration 1.4.0, III/IV mana swap, thaumaturgy school, tests, recompiled translations. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,7 @@ function makeActor(overrides: Partial<any> = {}) {
|
||||
}
|
||||
|
||||
describe('rest', () => {
|
||||
it('take a breather restores all mana and 1d6 + endurance HP, leaves zeal/blood alone', async () => {
|
||||
it('take a breather restores 1d6 mana and 1d6 + endurance HP, leaves zeal/blood alone', async () => {
|
||||
const { actor, updates } = makeActor({
|
||||
attributes: {
|
||||
health: { value: 10, max: 30 },
|
||||
@@ -43,13 +43,21 @@ describe('rest', () => {
|
||||
const r = await rest(actor, 'breather');
|
||||
expect(r.kind).toBe('breather');
|
||||
expect(r.hpRestored).toBe(6); // 4 (stub roll default) + 2 (endurance) = 6
|
||||
expect(r.manaRestored).toBe(19);
|
||||
expect(r.manaRestored).toBe(4); // stub 1d6 = 4
|
||||
expect(updates['system.attributes.health.value']).toBe(16);
|
||||
expect(updates['system.attributes.mana.value']).toBe(20);
|
||||
expect(updates['system.attributes.mana.value']).toBe(5);
|
||||
expect(updates['system.attributes.zeal.value']).toBeUndefined();
|
||||
expect(updates['system.attributes.breathersTaken']).toBe(1);
|
||||
});
|
||||
|
||||
it('short rest restores all mana, 1/3 HP, and removes unconscious state', async () => {
|
||||
it('refuses a third breather between two long rests', async () => {
|
||||
const { actor, updates } = makeActor({ attributes: { breathersTaken: 2 } });
|
||||
const r = await rest(actor, 'breather');
|
||||
expect(r.blocked).toBe(true);
|
||||
expect(updates['system.attributes.health.value']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('short rest restores ⌈⅓⌉ of the mana pool, ⌈½⌉ HP, and removes unconscious state', async () => {
|
||||
const effects = [
|
||||
{ id: 'nieprzytomny', statuses: new Set(['nieprzytomny']) },
|
||||
{ id: 'przewrocony', statuses: new Set(['przewrocony']) },
|
||||
@@ -63,10 +71,10 @@ describe('rest', () => {
|
||||
});
|
||||
const r = await rest(actor, 'short');
|
||||
expect(r.kind).toBe('short');
|
||||
expect(r.hpRestored).toBe(10); // 30 / 3 = 10
|
||||
expect(r.manaRestored).toBe(19);
|
||||
expect(updates['system.attributes.health.value']).toBe(20);
|
||||
expect(updates['system.attributes.mana.value']).toBe(20);
|
||||
expect(r.hpRestored).toBe(15); // ⌈30 / 2⌉
|
||||
expect(r.manaRestored).toBe(7); // ⌈20 / 3⌉
|
||||
expect(updates['system.attributes.health.value']).toBe(25);
|
||||
expect(updates['system.attributes.mana.value']).toBe(8);
|
||||
expect(deletedEffects).toContain('nieprzytomny');
|
||||
expect(deletedEffects).not.toContain('przewrocony');
|
||||
expect(r.effectsCleared).toBe(1);
|
||||
@@ -97,7 +105,9 @@ describe('rest', () => {
|
||||
expect(updates['system.attributes.blood.value']).toBe(6);
|
||||
expect(updates['system.attributes.magicalArmor.runicCounter']).toBe(0);
|
||||
expect(r.toleranceRecovered).toBe(1);
|
||||
|
||||
expect(updates['system.attributes.breathersTaken']).toBe(0);
|
||||
expect(updates['system.attributes.manaBorrowed']).toBe(0);
|
||||
|
||||
expect(deletedEffects).toContain('nieprzytomny');
|
||||
expect(deletedEffects).toContain('przewrocony');
|
||||
expect(deletedEffects).toContain('eff-1');
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
manaExhaustionTier, manaToBorrow, exhaustionDice, overLimitTest, overcastBonus, taperedDiceBonus,
|
||||
spellSurplus, dispelSuccessesNeeded, triggersStun, concentrationTest, dodgeResult, parrySucceeds,
|
||||
standUpCost, immediateActionsZealTotal, manaPassThreshold, helpContributionCap, bloodAbsorbHp,
|
||||
bloodToZeal, manaElixirGain, negotiationBand,
|
||||
} from '../../src/logic/rules';
|
||||
import { getMagicPowerEntry } from '../../src/constants';
|
||||
|
||||
describe('magic power table', () => {
|
||||
it('has III=24 and IV=21 mana per day', () => {
|
||||
expect(getMagicPowerEntry(3).manaPerDay).toBe(24);
|
||||
expect(getMagicPowerEntry(4).manaPerDay).toBe(21);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Wyczerpanie Many', () => {
|
||||
it('maps tiers', () => {
|
||||
expect(manaExhaustionTier(3).id).toBe('zdyszanie');
|
||||
expect(manaExhaustionTier(8).id).toBe('zmeczenie');
|
||||
expect(manaExhaustionTier(12).id).toBe('wyczerpanie');
|
||||
expect(manaExhaustionTier(16).id).toBe('zalamanie-sil');
|
||||
expect(manaExhaustionTier(20).id).toBe('zapasc');
|
||||
expect(manaExhaustionTier(24).id).toBe('ciezka-zapasc');
|
||||
expect(manaExhaustionTier(25).id).toBe('wypalenie');
|
||||
});
|
||||
it('borrows only the shortfall and accumulates dice', () => {
|
||||
expect(manaToBorrow(5, 2)).toBe(3);
|
||||
expect(manaToBorrow(2, 5)).toBe(0);
|
||||
expect(exhaustionDice(4, 3)).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('overcast', () => {
|
||||
it('over-limit test T by base cost, S triangular', () => {
|
||||
expect(overLimitTest(2, 3)).toEqual({ threshold: 4, successes: 6 });
|
||||
expect(overLimitTest(4, 1)).toEqual({ threshold: 5, successes: 1 });
|
||||
expect(overLimitTest(6, 5)).toEqual({ threshold: 6, successes: 15 });
|
||||
});
|
||||
it('tapers from level 3 (halved down, min 1)', () => {
|
||||
expect(overcastBonus(2, 4)).toBe(8);
|
||||
expect(overcastBonus(3, 4)).toBe(10);
|
||||
expect(overcastBonus(3, 1)).toBe(3);
|
||||
expect(taperedDiceBonus(5)).toBe(2);
|
||||
expect(taperedDiceBonus(1)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Zasada Potężniejszych Zaklęć', () => {
|
||||
it('computes surplus and dispel needs', () => {
|
||||
expect(spellSurplus(5, 3)).toBe(2);
|
||||
expect(spellSurplus(2, 3)).toBe(0);
|
||||
expect(dispelSuccessesNeeded(2, true)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combat rules', () => {
|
||||
it('Oszołomiony at half max Żywotność', () => {
|
||||
expect(triggersStun(5, 10)).toBe(true);
|
||||
expect(triggersStun(4, 10)).toBe(false);
|
||||
expect(triggersStun(0, 0)).toBe(false);
|
||||
});
|
||||
it('Koncentracja S = ceil(lost/3), min 1', () => {
|
||||
expect(concentrationTest(0)).toBeNull();
|
||||
expect(concentrationTest(1)).toEqual({ threshold: 4, successes: 1 });
|
||||
expect(concentrationTest(7)).toEqual({ threshold: 4, successes: 3 });
|
||||
});
|
||||
it('Unik adds 2 to a positive difference', () => {
|
||||
expect(dodgeResult(3, 3)).toEqual({ hit: false, damageSuccesses: 0 });
|
||||
expect(dodgeResult(5, 2)).toEqual({ hit: true, damageSuccesses: 5 });
|
||||
});
|
||||
it('Parowanie tolerance is ceil(½ Walka Wręcz)', () => {
|
||||
expect(parrySucceeds(3, 5, 3)).toBe(true);
|
||||
expect(parrySucceeds(2, 5, 3)).toBe(false);
|
||||
});
|
||||
it('rounds movement up', () => {
|
||||
expect(standUpCost(5)).toBe(3);
|
||||
});
|
||||
it('1 Zapał buys two extra Immediate Actions', () => {
|
||||
expect([1, 2, 3, 4, 5].map(immediateActionsZealTotal)).toEqual([0, 1, 1, 2, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pomoc / Źródło Mocy', () => {
|
||||
it('caps contribution and sets pass threshold', () => {
|
||||
expect(helpContributionCap(5)).toBe(3);
|
||||
expect(helpContributionCap(5, true)).toBe(5);
|
||||
expect(manaPassThreshold(0.5)).toBe(4);
|
||||
expect(manaPassThreshold(2.5)).toBe(6);
|
||||
expect(manaPassThreshold(3)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('blood, elixirs, trade', () => {
|
||||
it('Wchłanianie Krwi 2 HP/Krew, Większe 3, 5:1 Zapał', () => {
|
||||
expect(bloodAbsorbHp(4)).toBe(8);
|
||||
expect(bloodAbsorbHp(4, true)).toBe(12);
|
||||
expect(bloodToZeal(12)).toBe(2);
|
||||
});
|
||||
it('mana elixir is capped by the pool', () => {
|
||||
expect(manaElixirGain(8, 10, 3)).toBe(2);
|
||||
});
|
||||
it('negotiation bands', () => {
|
||||
expect([0, 1, 2, 3, 4, 5, 6, 7, 12].map(negotiationBand)).toEqual([0, 1, 1, 2, 2, 3, 3, 4, 4]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user