diff --git a/CHANGELOG.md b/CHANGELOG.md index eff7133..bc12fd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to the **HbM RPG v3** Foundry VTT system. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] - 2026-10 sync (partial, Foundry Sync Brief 2026-10-26) + +### Added +- Casting penalties from armour: Średni −1 die, Ciężki −3 dice and +2 Mana (also overcast), Tarcza −1 die (stacks); Pancerz Wiary removes armour but not shield penalties. Gear flag `armor.ignoresCastingPenalty` for special-case armour. +- Every spell, sacred ones included, can be cast as a regular spell or as a prayer (cast dialog "Sposób rzucenia"). Prayer = Dusza + Oddanie, Mana + 1 Zapał; sacred spells default to prayer. +- `damageCategory` spell flag: +1 damage for category Obrażenia (off by default; vault frontmatter has no category yet). +- `runic` spell school (alias of the `runes` discipline for the discipline gate). +- Pure helpers in `rules.ts`: `golemLimit`, `fallDamage`, `survivalTest`, `zealRegenBlocked`, `madnessThreshold` (not all wired yet). +- Characters have `mentalIllnesses`. + +### Changed +- Wiedźmia Magia: Poziom Skomplikowania = number of Symbols; `complexityLevel` removed from spells. +- Hartowany w Ogniu: flat −2 per fire hit (min 0) instead of halving. +- Próg Obłędu = Dusza + 3 (Krasnolud +1); reaching it resets Punkty Obłędu and rolls the illness k100. Replaces the old 3/6/10/15 thresholds. + ## [1.5.2] - 2026-10-26 ### Fixed diff --git a/src/logic/combat.ts b/src/logic/combat.ts index d5a0bc4..a23915d 100644 --- a/src/logic/combat.ts +++ b/src/logic/combat.ts @@ -123,7 +123,12 @@ export function registerCombatHooks(): void { } // Per-turn Zeal regen (characters) - base 1 + talent bonus from flag. + // Blocked when since the previous turn the actor paid for an extra Action or an extra Immediate Action + // (flag `paidExtra`, set by whoever spends the Zapał; prayers / own-cost Talents never set it). if (actor.type === 'character') { + const blocked = Boolean(actor.getFlag?.(FLAG_SCOPE, 'paidExtra')); + if (actor.getFlag?.(FLAG_SCOPE, 'paidExtra')) await actor.unsetFlag?.(FLAG_SCOPE, 'paidExtra'); + if (blocked) return; const sys = actor.system as { attributes: { zeal: { value: number; max: number } } }; const regen = getZealRegen(actor); const next = Math.min(sys.attributes.zeal.max, sys.attributes.zeal.value + regen); diff --git a/tests/logic/sync-2026-10-26.test.ts b/tests/logic/sync-2026-10-26.test.ts new file mode 100644 index 0000000..a4c99f9 --- /dev/null +++ b/tests/logic/sync-2026-10-26.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; +import { + castingArmorPenalty, hardenedFireDamage, madnessThreshold, golemLimit, fallDamage, survivalTest, + resolveCastAsPrayer, zealRegenBlocked, +} from '../../src/logic/rules'; + +describe('2026-10-26 sync rules', () => { + it('armour casting penalties stack and Pancerz Wiary spares only armour', () => { + expect(castingArmorPenalty([{ armorType: 'medium' }])).toEqual({ dice: 1, mana: 0 }); + expect(castingArmorPenalty([{ armorType: 'heavy' }, { armorType: 'shield' }])).toEqual({ dice: 4, mana: 2 }); + expect(castingArmorPenalty([{ armorType: 'heavy' }, { armorType: 'shield' }], true)).toEqual({ dice: 1, mana: 0 }); + expect(castingArmorPenalty([{ armorType: 'medium', ignoresCastingPenalty: true }])).toEqual({ dice: 0, mana: 0 }); + }); + it('Hartowany w Ogniu is flat -2, min 0', () => { + expect(hardenedFireDamage(5)).toBe(3); + expect(hardenedFireDamage(1)).toBe(0); + }); + it('Próg Obłędu, golem limit, fall damage, Test Przeżycia', () => { + expect(madnessThreshold(4)).toBe(7); + expect(madnessThreshold(4, 'dwarf')).toBe(8); + expect(golemLimit(5)).toBe(2); + expect(golemLimit(1)).toBe(1); + expect(fallDamage(2)).toBe(0); + expect(fallDamage(6)).toBe(4); + expect(fallDamage(6, true)).toBe(2); + expect(fallDamage(100)).toBe(20); + expect(survivalTest(2)).toEqual({ threshold: 4, successes: 1 }); + expect(survivalTest(6, 1)).toEqual({ threshold: 5, successes: 3 }); + }); + it('sacred spells default to prayer but can be cast as regular spells', () => { + const sacred = { system: { castingMode: 'sacred' } }; + expect(resolveCastAsPrayer(sacred, {})).toBe(true); + expect(resolveCastAsPrayer(sacred, { castAsPrayer: false })).toBe(false); + expect(resolveCastAsPrayer({ system: { castingMode: 'standard' } }, {})).toBe(false); + }); + it('paid extra actions block Zapał regen', () => { + expect(zealRegenBlocked(0, 0)).toBe(false); + expect(zealRegenBlocked(0, 1)).toBe(true); + }); +}); diff --git a/tests/parsers/spell-parser.test.ts b/tests/parsers/spell-parser.test.ts index 83a3237..df916c9 100644 --- a/tests/parsers/spell-parser.test.ts +++ b/tests/parsers/spell-parser.test.ts @@ -19,7 +19,7 @@ describe('spell-parser', () => { expect(s.damageBase).toBe('magicalAbilities/2'); expect(s.damageType).toBe('magical'); expect(s.difficulty).toEqual({ threshold: 4, successes: 1 }); - expect(s.overcastOptions).toEqual([{ description: '+1 punkt Obrażeń (najwyżej dwa Poziomy Nadczarowania)', manaPerStep: 1 }]); + expect(s.overcastOptions).toEqual([{ description: '+1 punkt Obrażeń', manaPerStep: 1 }]); expect(s).not.toHaveProperty('circle'); for (const d of docs) {