diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e0cc13..11589cc 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). +## [1.5.1] - 2026-10-22 + +### Added +- Weapons have wound kinds (`system.weapon.woundKinds`: `slashing` sieczne / `piercing` kłute / `blunt` obuchowe, several allowed, empty = unspecified), editable on the gear sheet as a multi-select. Matches Foundry Sync Brief §18. +- Gear parser reads `wound` (string, comma list or YAML list; Polish or English words) from vault weapon notes. Unknown values are dropped; nothing is guessed. +- Packs rebuilt from the current vault: Sztylet piercing+slashing, Bicz slashing, Laska blunt, bows and Włócznia piercing; generic one-/two-handed entries list all three (pick the real type per weapon). + +### Changed +- `applyDamage` takes `woundKinds` (array) instead of a single kind; passive Krew applies when any is slashing or piercing. Blunt (and unarmed) never gives Krew. +- Vault sync of 2026-10-22 needs no further logic changes: runic armour wording, Zakazana Osłona retaliation, Rozerwanie Run artifact destruction and free Immediate Action for Kradzież Życia / Złożenie w Ofierze were already covered in 1.5.0 (spell, talent and discipline text arrives through the rebuilt packs). + +### Notes +- Callers of `applyDamage` still need to pass the equipped weapon's `woundKinds`. +- Kości Losu use is now an Immediate Action that counts toward the limit (vault `Zużywanie Ładunków`); the system has no Fate-dice action code yet. + ## [1.5.0] - 2026-10-20 Vault answers (Foundry Sync Brief §17): blood magic and Runiczny Pancerz. diff --git a/lang/en.json b/lang/en.json index e2c629b..c74db31 100644 --- a/lang/en.json +++ b/lang/en.json @@ -239,6 +239,13 @@ "physical": "Physical", "magical": "Magical", "environmental": "Environmental", + "woundKinds": "Wound kind", + "woundKindsHint": "Several allowed. Only slashing and piercing wounds grant talent Krew (Respect for Life, Crimson Duelist). Empty = unspecified.", + "wound": { + "slashing": "Slashing", + "piercing": "Piercing", + "blunt": "Blunt" + }, "properties": "Properties", "damageReduction": "Damage Reduction", "damageReductionBonus": "DR Bonus", diff --git a/lang/pl.json b/lang/pl.json index 2401b1b..b222e9e 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -259,6 +259,13 @@ "physical": "Fizyczne", "magical": "Magiczne", "environmental": "Środowiskowe", + "woundKinds": "Rodzaj rany", + "woundKindsHint": "Można wybrać kilka. Tylko rany sieczne i kłute dają Krew z talentów (Szacunek do Życia, Krwisty Pojedynkowicz). Puste = nieokreślony.", + "wound": { + "slashing": "Sieczne", + "piercing": "Kłute", + "blunt": "Obuchowe" + }, "properties": "Właściwości", "damageReduction": "Redukcja Obrażeń", "damageReductionBonus": "Premia do Redukcji", diff --git a/package.json b/package.json index 40aab09..c2bae20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hbm-rpg-v3-foundry", - "version": "1.5.0", + "version": "1.5.1", "description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3", "private": true, "type": "module", diff --git a/scripts/parsers/gear-parser.ts b/scripts/parsers/gear-parser.ts index 4033453..7cfecfd 100644 --- a/scripts/parsers/gear-parser.ts +++ b/scripts/parsers/gear-parser.ts @@ -38,6 +38,27 @@ export const parseGear: ParserFn = async (ctx: ParserContext): Promise = { + slashing: 'slashing', sieczne: 'slashing', sieczna: 'slashing', + piercing: 'piercing', kłute: 'piercing', kłuta: 'piercing', kluta: 'piercing', kłujące: 'piercing', + blunt: 'blunt', obuchowe: 'blunt', obuchowa: 'blunt', + }; + const out: WoundKind[] = []; + for (const part of (raw ?? '').split(',')) { + const kind = map[part.trim().toLowerCase()]; + if (kind && !out.includes(kind)) out.push(kind); + } + return out; +} + function extractMarkdown(raw: string): { frontmatter: Record; content: string } { const fm: Record = {}; let content = raw; @@ -48,9 +69,17 @@ function extractMarkdown(raw: string): { frontmatter: Record; co const fmText = raw.slice(3, endIdx).trim(); content = raw.slice(endIdx + 3).trim(); - const lines = fmText.split('\n'); + const lines = fmText.split(/\r?\n/); + let listKey = ''; for (const line of lines) { + // "key:" followed by " - item" lines → comma-joined value. + const item = line.match(/^\s+-\s+(.*)$/); + if (item && listKey) { + fm[listKey] = fm[listKey] ? `${fm[listKey]}, ${item[1].trim()}` : item[1].trim(); + continue; + } const colonIdx = line.indexOf(':'); + listKey = colonIdx > -1 && !line.slice(colonIdx + 1).trim() ? line.slice(0, colonIdx).trim() : ''; if (colonIdx > -1) { const key = line.slice(0, colonIdx).trim(); let val = line.slice(colonIdx + 1).trim(); @@ -91,6 +120,7 @@ function buildGearDoc(name: string, fm: Record, content: string, weapon: { damage: '', damageType: 'physical', + woundKinds: [], properties: [], }, armor: { @@ -105,6 +135,7 @@ function buildGearDoc(name: string, fm: Record, content: string, if (category === 'weapon') { system.weapon.damage = fm.damage || fm.obrazenia || ''; + system.weapon.woundKinds = parseWoundKinds(fm.wound || fm.rana); if (fm.traits || fm.cechy) { system.weapon.properties = parseList(fm.traits || fm.cechy); } diff --git a/src/constants.ts b/src/constants.ts index 11ddd53..aea5abc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -192,6 +192,11 @@ export type GearCategory = (typeof GEAR_CATEGORIES)[number]; export const DAMAGE_TYPES = ['physical', 'magical', 'environmental'] as const; export type DamageType = (typeof DAMAGE_TYPES)[number]; +/** Weapon wound kinds (sieczne / kłute / obuchowe). A weapon may have several; none = unspecified. + * Only slashing and piercing wounds feed the passive Krew talents. */ +export const WOUND_KINDS = ['slashing', 'piercing', 'blunt'] as const; +export type WoundKind = (typeof WOUND_KINDS)[number]; + /** Armor types */ export const ARMOR_TYPES = ['light', 'medium', 'heavy', 'shield'] as const; export type ArmorType = (typeof ARMOR_TYPES)[number]; diff --git a/src/data/item-gear.ts b/src/data/item-gear.ts index 00f10db..980df3b 100644 --- a/src/data/item-gear.ts +++ b/src/data/item-gear.ts @@ -6,7 +6,7 @@ import { makeArrayField, fields, } from './fields'; -import { GEAR_CATEGORIES, DAMAGE_TYPES, ARMOR_TYPES, RARITIES } from '../constants'; +import { GEAR_CATEGORIES, DAMAGE_TYPES, ARMOR_TYPES, RARITIES, WOUND_KINDS } from '../constants'; /** * Unified Gear item - discriminated by `category`. @@ -39,6 +39,8 @@ export class GearData extends foundry.abstract.TypeDataModel { choices: DAMAGE_TYPES as unknown as readonly string[], }), properties: makeArrayField(new f.StringField({ blank: false })), + // Rodzaj rany: slashing (sieczne) / piercing (kłute) / blunt (obuchowe); several allowed, empty = unspecified. + woundKinds: makeArrayField(new f.StringField({ blank: false, choices: WOUND_KINDS as unknown as string[] })), }), armor: new f.SchemaField({ diff --git a/src/logic/blood-magic.ts b/src/logic/blood-magic.ts index 50c2520..58870c3 100644 --- a/src/logic/blood-magic.ts +++ b/src/logic/blood-magic.ts @@ -180,7 +180,7 @@ export async function resolveLifeSteal(actor: CastableActor, hpActuallyLost: num /** * Złożenie w Ofierze: 1 Zapał, free Akcja Natychmiastowa, once per round. Triggers when your attack - * kills a target with a cutting / piercing wound within 1 m. `successes` are the Sukcesy of the + * kills a target with a slashing / piercing wound within 1 m. `successes` are the Sukcesy of the * killing attack's Test: heal Sukcesy + Magia, gain Krew = Sukcesy. */ export async function sacrifice(actor: CastableActor, successes: number): Promise { @@ -256,7 +256,7 @@ export async function absorbBlood( /** * Passive Krew (talents): 1 per 4 HP lost (Szacunek do Życia) / 1 per 2 HP (Krwisty Pojedynkowicz), - * by either side, only from cutting or piercing weapons. Call for the victim and the attacker. + * by either side, only from slashing or piercing weapons. Call for the victim and the attacker. */ export async function gainKrewFromWeaponDamage(actor: CastableActor, hpLost: number): Promise { const blood = (actor.system.attributes as any)?.blood; diff --git a/src/logic/damage.ts b/src/logic/damage.ts index 83efb8a..6fa8344 100644 --- a/src/logic/damage.ts +++ b/src/logic/damage.ts @@ -35,7 +35,8 @@ export interface ApplyDamageOptions { /** Rozerwanie Run: permanently destroys N Runiczny Pancerz points (lowers its maximum). */ destroyRunicPoints?: number; /** Wound kind of a weapon hit (gear data has none yet – callers pass it); drives passive Krew. */ - woundKind?: 'cutting' | 'piercing' | 'blunt'; + /** Wound kinds of the attacking weapon; only slashing / piercing give passive Krew. */ + woundKinds?: ('slashing' | 'piercing' | 'blunt')[]; /** Attacker, for passive Krew gain (Szacunek do Życia / Krwisty Pojedynkowicz). */ sourceActor?: any; postChat?: boolean; @@ -161,8 +162,8 @@ export async function applyDamage(actor: ActorLike, opts: ApplyDamageOptions): P if (Object.keys(updates).length > 0) await actor.update(updates); - // Passive Krew from cutting / piercing weapons (talents): victim and attacker side. - if (report.hpDamage > 0 && (opts.woundKind === 'cutting' || opts.woundKind === 'piercing')) { + // Passive Krew from slashing / piercing weapons (talents): victim and attacker side. + if (report.hpDamage > 0 && (opts.woundKinds ?? []).some((k) => k === 'slashing' || k === 'piercing')) { report.krewGained = { victim: await gainKrewFromWeaponDamage(actor as any, report.hpDamage), source: opts.sourceActor ? await gainKrewFromWeaponDamage(opts.sourceActor, report.hpDamage) : 0, diff --git a/src/logic/rules.ts b/src/logic/rules.ts index c5ffcdb..879a3a6 100644 --- a/src/logic/rules.ts +++ b/src/logic/rules.ts @@ -196,7 +196,7 @@ export function greatSacrificeResult(successes: number, highestAttribute: number if (successes > highestAttribute) return { kills: true, heal: 0, krew: Math.max(0, targetHealth) }; return { kills: false, heal: Math.max(0, successes), krew: 0 }; } -/** Passive Krew: Szacunek do Życia 1 per 4 HP, Krwisty Pojedynkowicz 1 per 2 HP (cutting/piercing only). */ +/** Passive Krew: Szacunek do Życia 1 per 4 HP, Krwisty Pojedynkowicz 1 per 2 HP (slashing/piercing only). */ export function krewFromWeaponHpLoss(hpLost: number, talent: KrewTalent | null): number { if (talent === 'respect-for-life') return Math.floor(Math.max(0, hpLost) / 4); if (talent === 'krwisty-pojedynkowicz') return Math.floor(Math.max(0, hpLost) / 2); diff --git a/src/sheets/item-sheet.ts b/src/sheets/item-sheet.ts index 6f54b2b..db93e9f 100644 --- a/src/sheets/item-sheet.ts +++ b/src/sheets/item-sheet.ts @@ -9,6 +9,7 @@ import { AOE_SHAPES, SPELL_DAMAGE_TYPES, TRIGGER_EVENTS, + WOUND_KINDS, } from '../constants'; const { ItemSheetV2 } = foundry.applications.sheets as unknown as { @@ -67,6 +68,7 @@ export class HbmItemSheet extends HandlebarsApplicationMixin(ItemSheetV2) { itemType: item.type, bodyPartial: TYPE_PARTIAL[item.type] ?? 'systems/hbm-rpg-v3/templates/item/_unknown.hbs', choices: buildChoiceMaps(), + woundChoices: Object.fromEntries(WOUND_KINDS.map((k) => [k, `HBM.gear.wound.${k}`])), effects, supportsEffects, }; diff --git a/system.json b/system.json index c34ccd8..8df3091 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "hbm-rpg-v3", "title": "Homebrew Magic: RPG v3", "description": "System Foundry VTT dla Homebrew Magic: Role Playing Game v3 - autorska gra fabularna z mechaniką puli kości d6 (TS - Trudność:Sukcesy).", - "version": "1.5.0", + "version": "1.5.1", "manifest": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3.json", "download": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3-v1.5.0.zip", "compatibility": { diff --git a/templates/item/gear.hbs b/templates/item/gear.hbs index f19862f..bdf58a4 100644 --- a/templates/item/gear.hbs +++ b/templates/item/gear.hbs @@ -31,6 +31,11 @@ + {{/if}} diff --git a/tests/parsers/gear-parser.test.ts b/tests/parsers/gear-parser.test.ts new file mode 100644 index 0000000..2a7409e --- /dev/null +++ b/tests/parsers/gear-parser.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { parseWoundKinds } from '../../scripts/parsers/gear-parser'; + +describe('gear-parser parseWoundKinds', () => { + it('maps English and Polish labels, single or listed', () => { + expect(parseWoundKinds('slashing')).toEqual(['slashing']); + expect(parseWoundKinds('kłute')).toEqual(['piercing']); + expect(parseWoundKinds('piercing, slashing')).toEqual(['piercing', 'slashing']); + expect(parseWoundKinds('slashing, piercing, blunt, slashing')).toEqual(['slashing', 'piercing', 'blunt']); + }); + + it('never guesses: missing or unknown stays empty', () => { + expect(parseWoundKinds(undefined)).toEqual([]); + expect(parseWoundKinds('')).toEqual([]); + expect(parseWoundKinds('ognista')).toEqual([]); + }); +});