From 23027cf1b300937aa107879bc398b54bf86430af Mon Sep 17 00:00:00 2001 From: Octoturge Date: Thu, 22 Oct 2026 00:05:57 +0200 Subject: [PATCH 1/2] feat: weapon wound kind (1.5.1) Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 9 +++++++++ lang/en.json | 7 +++++++ lang/pl.json | 7 +++++++ package.json | 2 +- scripts/parsers/gear-parser.ts | 11 +++++++++++ src/constants.ts | 4 ++++ src/data/item-gear.ts | 4 +++- system.json | 2 +- templates/item/gear.hbs | 8 ++++++++ tests/parsers/gear-parser.test.ts | 17 +++++++++++++++++ 10 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 tests/parsers/gear-parser.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e0cc13..2a51fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ 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 a wound kind (`system.weapon.woundKind`: tnąca / kłująca / obuchowa, empty = unspecified), editable on the gear sheet. +- Gear parser reads `wound` / `rana` frontmatter from vault weapon notes. Missing or unknown values stay unspecified; nothing is guessed. + +### Notes +- Only cutting and piercing weapons feed passive Krew; unspecified weapons give none. Callers of `applyDamage` still need to pass the weapon's `woundKind`. + ## [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..2e04e62 100644 --- a/lang/en.json +++ b/lang/en.json @@ -239,6 +239,13 @@ "physical": "Physical", "magical": "Magical", "environmental": "Environmental", + "woundKind": "Wound kind", + "wound": { + "none": "Unspecified", + "cutting": "Cutting", + "piercing": "Piercing", + "blunt": "Blunt" + }, "properties": "Properties", "damageReduction": "Damage Reduction", "damageReductionBonus": "DR Bonus", diff --git a/lang/pl.json b/lang/pl.json index 2401b1b..fd7f65e 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -259,6 +259,13 @@ "physical": "Fizyczne", "magical": "Magiczne", "environmental": "Środowiskowe", + "woundKind": "Rodzaj rany", + "wound": { + "none": "Nieokreślony", + "cutting": "Tnąca", + "piercing": "Kłująca", + "blunt": "Obuchowa" + }, "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..96b4c6b 100644 --- a/scripts/parsers/gear-parser.ts +++ b/scripts/parsers/gear-parser.ts @@ -38,6 +38,15 @@ export const parseGear: ParserFn = async (ctx: ParserContext): Promise; content: string } { const fm: Record = {}; let content = raw; @@ -91,6 +100,7 @@ function buildGearDoc(name: string, fm: Record, content: string, weapon: { damage: '', damageType: 'physical', + woundKind: '', properties: [], }, armor: { @@ -105,6 +115,7 @@ function buildGearDoc(name: string, fm: Record, content: string, if (category === 'weapon') { system.weapon.damage = fm.damage || fm.obrazenia || ''; + system.weapon.woundKind = parseWoundKind(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..93b3ae3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -192,6 +192,10 @@ 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 ('' = unspecified). Cutting / piercing feed the passive Krew talents. */ +export const WOUND_KINDS = ['', 'cutting', '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..a501ae9 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: cutting (tnąca) / piercing (kłująca) / blunt (obuchowa); '' = not specified. + woundKind: makeStringField('', { blank: true, choices: WOUND_KINDS as unknown as readonly string[] }), }), armor: new f.SchemaField({ 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..aeae6e1 100644 --- a/templates/item/gear.hbs +++ b/templates/item/gear.hbs @@ -31,6 +31,14 @@ + {{/if}} diff --git a/tests/parsers/gear-parser.test.ts b/tests/parsers/gear-parser.test.ts new file mode 100644 index 0000000..6cbf81b --- /dev/null +++ b/tests/parsers/gear-parser.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { parseWoundKind } from '../../scripts/parsers/gear-parser'; + +describe('gear-parser parseWoundKind', () => { + it('maps Polish and English labels', () => { + expect(parseWoundKind('tnąca')).toBe('cutting'); + expect(parseWoundKind('Kłująca')).toBe('piercing'); + expect(parseWoundKind('obuchowa')).toBe('blunt'); + expect(parseWoundKind('piercing')).toBe('piercing'); + }); + + it('never guesses: missing or unknown stays unspecified', () => { + expect(parseWoundKind(undefined)).toBe(''); + expect(parseWoundKind('')).toBe(''); + expect(parseWoundKind('ognista')).toBe(''); + }); +}); From a5f2683d15ec0b614832c51962c31800f28069e0 Mon Sep 17 00:00:00 2001 From: Octoturge Date: Thu, 22 Oct 2026 00:23:15 +0200 Subject: [PATCH 2/2] feat: weapon wound kinds as vault slashing/piercing/blunt list Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 12 +++++++--- lang/en.json | 6 ++--- lang/pl.json | 10 ++++---- scripts/parsers/gear-parser.ts | 40 +++++++++++++++++++++++-------- src/constants.ts | 5 ++-- src/data/item-gear.ts | 4 ++-- src/logic/blood-magic.ts | 4 ++-- src/logic/damage.ts | 7 +++--- src/logic/rules.ts | 2 +- src/sheets/item-sheet.ts | 2 ++ templates/item/gear.hbs | 11 ++++----- tests/parsers/gear-parser.test.ts | 22 ++++++++--------- 12 files changed, 76 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a51fb2..11589cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,17 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [1.5.1] - 2026-10-22 ### Added -- Weapons have a wound kind (`system.weapon.woundKind`: tnąca / kłująca / obuchowa, empty = unspecified), editable on the gear sheet. -- Gear parser reads `wound` / `rana` frontmatter from vault weapon notes. Missing or unknown values stay unspecified; nothing is guessed. +- 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 -- Only cutting and piercing weapons feed passive Krew; unspecified weapons give none. Callers of `applyDamage` still need to pass the weapon's `woundKind`. +- 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 diff --git a/lang/en.json b/lang/en.json index 2e04e62..c74db31 100644 --- a/lang/en.json +++ b/lang/en.json @@ -239,10 +239,10 @@ "physical": "Physical", "magical": "Magical", "environmental": "Environmental", - "woundKind": "Wound kind", + "woundKinds": "Wound kind", + "woundKindsHint": "Several allowed. Only slashing and piercing wounds grant talent Krew (Respect for Life, Crimson Duelist). Empty = unspecified.", "wound": { - "none": "Unspecified", - "cutting": "Cutting", + "slashing": "Slashing", "piercing": "Piercing", "blunt": "Blunt" }, diff --git a/lang/pl.json b/lang/pl.json index fd7f65e..b222e9e 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -259,12 +259,12 @@ "physical": "Fizyczne", "magical": "Magiczne", "environmental": "Środowiskowe", - "woundKind": "Rodzaj rany", + "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": { - "none": "Nieokreślony", - "cutting": "Tnąca", - "piercing": "Kłująca", - "blunt": "Obuchowa" + "slashing": "Sieczne", + "piercing": "Kłute", + "blunt": "Obuchowe" }, "properties": "Właściwości", "damageReduction": "Redukcja Obrażeń", diff --git a/scripts/parsers/gear-parser.ts b/scripts/parsers/gear-parser.ts index 96b4c6b..7cfecfd 100644 --- a/scripts/parsers/gear-parser.ts +++ b/scripts/parsers/gear-parser.ts @@ -38,13 +38,25 @@ 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 } { @@ -57,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(); @@ -100,7 +120,7 @@ function buildGearDoc(name: string, fm: Record, content: string, weapon: { damage: '', damageType: 'physical', - woundKind: '', + woundKinds: [], properties: [], }, armor: { @@ -115,7 +135,7 @@ function buildGearDoc(name: string, fm: Record, content: string, if (category === 'weapon') { system.weapon.damage = fm.damage || fm.obrazenia || ''; - system.weapon.woundKind = parseWoundKind(fm.wound || fm.rana); + 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 93b3ae3..aea5abc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -192,8 +192,9 @@ 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 ('' = unspecified). Cutting / piercing feed the passive Krew talents. */ -export const WOUND_KINDS = ['', 'cutting', 'piercing', 'blunt'] as const; +/** 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 */ diff --git a/src/data/item-gear.ts b/src/data/item-gear.ts index a501ae9..980df3b 100644 --- a/src/data/item-gear.ts +++ b/src/data/item-gear.ts @@ -39,8 +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: cutting (tnąca) / piercing (kłująca) / blunt (obuchowa); '' = not specified. - woundKind: makeStringField('', { blank: true, choices: WOUND_KINDS as unknown as readonly string[] }), + // 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/templates/item/gear.hbs b/templates/item/gear.hbs index aeae6e1..bdf58a4 100644 --- a/templates/item/gear.hbs +++ b/templates/item/gear.hbs @@ -31,13 +31,10 @@ -