17 changed files with 135 additions and 14 deletions
+21
View File
@@ -5,6 +5,27 @@ 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 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). this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.5.2] - 2026-10-26
### Fixed
- A character with the discipline Magia Sakralna - Egzorcyzmy (`sacredExorcism`) is now treated as a Sacred Magic caster in the spell discipline gate. Before, it only matched its own id, so exorcists failed the requirement for Sacred spells. The match is one-way: a plain Sacred mage is not an exorcist.
- The "new round" chat message and README no longer claim that Mana is reset. Mana is a daily pool in the vault and has not been reset per round since 1.3.x; the message now says which counters really reset.
## [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 ## [1.5.0] - 2026-10-20
Vault answers (Foundry Sync Brief §17): blood magic and Runiczny Pancerz. Vault answers (Foundry Sync Brief §17): blood magic and Runiczny Pancerz.
+1 -1
View File
@@ -16,7 +16,7 @@ A companion lore module ([`foundry-lore`](https://github.com/iwo-strzebonski/hbm
- **TS d6 dice pool roller** (`HbmTSRoll`) - `1` auto-fail, `6` auto-success, configurable Threshold (T) and required Successes (Y), plus T-step / Y-step modifiers driven by Active Effects. - **TS d6 dice pool roller** (`HbmTSRoll`) - `1` auto-fail, `6` auto-success, configurable Threshold (T) and required Successes (Y), plus T-step / Y-step modifiers driven by Active Effects.
- **Derived stats** - `health.max = 3 × (body + mind + soul)`, `zeal.max = ceil(soul/2)`, initiative pool, mana table lookup (Magic Power Level 0–X), blood pool (`body + soul`), elixir tolerance cap (`body + 1`), insanity counter. - **Derived stats** - `health.max = 3 × (body + mind + soul)`, `zeal.max = ceil(soul/2)`, initiative pool, mana table lookup (Magic Power Level 0–X), blood pool (`body + soul`), elixir tolerance cap (`body + 1`), insanity counter.
- **4-layer damage model** - Magical Armor (with runic counter) → Magical Shield → Physical Armor (DR + condition) → Health. `Apply Damage` dialog with bypass flags; auto-applies `Oszołomiony` on heavy HP loss. - **4-layer damage model** - Magical Armor (with runic counter) → Magical Shield → Physical Armor (DR + condition) → Health. `Apply Damage` dialog with bypass flags; auto-applies `Oszołomiony` on heavy HP loss.
- **Combat hooks** - Mana resets each round; Zeal +1 each turn; turns auto-skipped for Nieprzytomny / Obezwładniony; Podpalony ticks 1 environmental damage; Umierający prompts a Death Save. - **Combat hooks** - Mana is a daily pool (not reset per round; recovered by rests); Zeal +1 each turn; Immediate Action and Krew counters reset each round; turns auto-skipped for Nieprzytomny / Obezwładniony; Podpalony ticks 1 environmental damage; Umierający prompts a Death Save.
- **Spell-cast workflow** - `validateCast()` gates on resources / race / talent / discipline / deity / group / inCombat / superspell / witch-symbol; dispatcher routes to standard / Sacred / Witch / Blood / Hekate hybrid; `rollSpellDamage()` parses formula tokens; reactive triggers (`killWithWeapon`, `targetCastsSpell`, `damageTaken`, `turnStart`). - **Spell-cast workflow** - `validateCast()` gates on resources / race / talent / discipline / deity / group / inCombat / superspell / witch-symbol; dispatcher routes to standard / Sacred / Witch / Blood / Hekate hybrid; `rollSpellDamage()` parses formula tokens; reactive triggers (`killWithWeapon`, `targetCastsSpell`, `damageTaken`, `turnStart`).
- **Cast dialog** - mana / zeal / blood inputs, group-caster picker, Hekate toggle, GM bypass checkboxes (superspell, non-combat block), inline validation feedback. - **Cast dialog** - mana / zeal / blood inputs, group-caster picker, Hekate toggle, GM bypass checkboxes (superspell, non-combat block), inline validation feedback.
- **Mechanics modules** (`game.hbm.*`) - `bloodMagic` (Self-Harm, Life Steal, talent integration), `abyssMagic` (Aspects vs Primal, Mistrz Losu penalty, `Dary Otchłani`, insanity gain), `brewing` (TS test + tolerance counter), `rest` (short / long), `trade` (founding company workflow, transactions, smuggling). - **Mechanics modules** (`game.hbm.*`) - `bloodMagic` (Self-Harm, Life Steal, talent integration), `abyssMagic` (Aspects vs Primal, Mistrz Losu penalty, `Dary Otchłani`, insanity gain), `brewing` (TS test + tolerance counter), `rest` (short / long), `trade` (founding company workflow, transactions, smuggling).
+8 -1
View File
@@ -239,6 +239,13 @@
"physical": "Physical", "physical": "Physical",
"magical": "Magical", "magical": "Magical",
"environmental": "Environmental", "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", "properties": "Properties",
"damageReduction": "Damage Reduction", "damageReduction": "Damage Reduction",
"damageReductionBonus": "DR Bonus", "damageReductionBonus": "DR Bonus",
@@ -416,7 +423,7 @@
} }
}, },
"combat": { "combat": {
"newRound": "New round - Mana reset", "newRound": "New round - Immediate Action and Krew counters reset",
"newTurn": "New turn - Zeal +1", "newTurn": "New turn - Zeal +1",
"turnSkipped": "Turn skipped (condition)", "turnSkipped": "Turn skipped (condition)",
"deathSavePrompt": "Make a Death Saving Throw (Body, T:4, Y:1)" "deathSavePrompt": "Make a Death Saving Throw (Body, T:4, Y:1)"
+8 -1
View File
@@ -259,6 +259,13 @@
"physical": "Fizyczne", "physical": "Fizyczne",
"magical": "Magiczne", "magical": "Magiczne",
"environmental": "Środowiskowe", "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", "properties": "Właściwości",
"damageReduction": "Redukcja Obrażeń", "damageReduction": "Redukcja Obrażeń",
"damageReductionBonus": "Premia do Redukcji", "damageReductionBonus": "Premia do Redukcji",
@@ -419,7 +426,7 @@
} }
}, },
"combat": { "combat": {
"newRound": "Nowa runda - Mana zresetowana", "newRound": "Nowa runda - liczniki Akcji Natychmiastowych i Krwi zresetowane",
"newTurn": "Nowa tura - Zapał +1", "newTurn": "Nowa tura - Zapał +1",
"turnSkipped": "Tura pominięta (stan)", "turnSkipped": "Tura pominięta (stan)",
"deathSavePrompt": "Wykonaj Rzut Obronny przeciw Śmierci (Ciało, T:4, Y:1)" "deathSavePrompt": "Wykonaj Rzut Obronny przeciw Śmierci (Ciało, T:4, Y:1)"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hbm-rpg-v3-foundry", "name": "hbm-rpg-v3-foundry",
"version": "1.5.0", "version": "1.5.2",
"description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3", "description": "Foundry VTT system for Homebrew Magic: Role Playing Game v3",
"private": true, "private": true,
"type": "module", "type": "module",
+32 -1
View File
@@ -38,6 +38,27 @@ export const parseGear: ParserFn = async (ctx: ParserContext): Promise<ParsedDoc
return docs; return docs;
}; };
type WoundKind = 'slashing' | 'piercing' | 'blunt';
/**
* Vault `wound` frontmatter → woundKinds. Accepts one value, a comma list ("piercing, slashing"),
* or a YAML list (flattened to a comma list by extractMarkdown). Unknown values are dropped,
* absent stays empty – never guessed.
*/
export function parseWoundKinds(raw: string | undefined): WoundKind[] {
const map: Record<string, WoundKind> = {
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<string, string>; content: string } { function extractMarkdown(raw: string): { frontmatter: Record<string, string>; content: string } {
const fm: Record<string, string> = {}; const fm: Record<string, string> = {};
let content = raw; let content = raw;
@@ -48,9 +69,17 @@ function extractMarkdown(raw: string): { frontmatter: Record<string, string>; co
const fmText = raw.slice(3, endIdx).trim(); const fmText = raw.slice(3, endIdx).trim();
content = raw.slice(endIdx + 3).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) { 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(':'); const colonIdx = line.indexOf(':');
listKey = colonIdx > -1 && !line.slice(colonIdx + 1).trim() ? line.slice(0, colonIdx).trim() : '';
if (colonIdx > -1) { if (colonIdx > -1) {
const key = line.slice(0, colonIdx).trim(); const key = line.slice(0, colonIdx).trim();
let val = line.slice(colonIdx + 1).trim(); let val = line.slice(colonIdx + 1).trim();
@@ -91,6 +120,7 @@ function buildGearDoc(name: string, fm: Record<string, string>, content: string,
weapon: { weapon: {
damage: '', damage: '',
damageType: 'physical', damageType: 'physical',
woundKinds: [],
properties: [], properties: [],
}, },
armor: { armor: {
@@ -105,6 +135,7 @@ function buildGearDoc(name: string, fm: Record<string, string>, content: string,
if (category === 'weapon') { if (category === 'weapon') {
system.weapon.damage = fm.damage || fm.obrazenia || ''; system.weapon.damage = fm.damage || fm.obrazenia || '';
system.weapon.woundKinds = parseWoundKinds(fm.wound || fm.rana);
if (fm.traits || fm.cechy) { if (fm.traits || fm.cechy) {
system.weapon.properties = parseList(fm.traits || fm.cechy); system.weapon.properties = parseList(fm.traits || fm.cechy);
} }
+5
View File
@@ -192,6 +192,11 @@ export type GearCategory = (typeof GEAR_CATEGORIES)[number];
export const DAMAGE_TYPES = ['physical', 'magical', 'environmental'] as const; export const DAMAGE_TYPES = ['physical', 'magical', 'environmental'] as const;
export type DamageType = (typeof DAMAGE_TYPES)[number]; 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 */ /** Armor types */
export const ARMOR_TYPES = ['light', 'medium', 'heavy', 'shield'] as const; export const ARMOR_TYPES = ['light', 'medium', 'heavy', 'shield'] as const;
export type ArmorType = (typeof ARMOR_TYPES)[number]; export type ArmorType = (typeof ARMOR_TYPES)[number];
+3 -1
View File
@@ -6,7 +6,7 @@ import {
makeArrayField, makeArrayField,
fields, fields,
} from './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`. * 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[], choices: DAMAGE_TYPES as unknown as readonly string[],
}), }),
properties: makeArrayField(new f.StringField({ blank: false })), 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({ armor: new f.SchemaField({
+2 -2
View File
@@ -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 * 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. * killing attack's Test: heal Sukcesy + Magia, gain Krew = Sukcesy.
*/ */
export async function sacrifice(actor: CastableActor, successes: number): Promise<BloodActionResult> { export async function sacrifice(actor: CastableActor, successes: number): Promise<BloodActionResult> {
@@ -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), * 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<number> { export async function gainKrewFromWeaponDamage(actor: CastableActor, hpLost: number): Promise<number> {
const blood = (actor.system.attributes as any)?.blood; const blood = (actor.system.attributes as any)?.blood;
+4 -3
View File
@@ -35,7 +35,8 @@ export interface ApplyDamageOptions {
/** Rozerwanie Run: permanently destroys N Runiczny Pancerz points (lowers its maximum). */ /** Rozerwanie Run: permanently destroys N Runiczny Pancerz points (lowers its maximum). */
destroyRunicPoints?: number; destroyRunicPoints?: number;
/** Wound kind of a weapon hit (gear data has none yet – callers pass it); drives passive Krew. */ /** 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). */ /** Attacker, for passive Krew gain (Szacunek do Życia / Krwisty Pojedynkowicz). */
sourceActor?: any; sourceActor?: any;
postChat?: boolean; 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); if (Object.keys(updates).length > 0) await actor.update(updates);
// Passive Krew from cutting / piercing weapons (talents): victim and attacker side. // Passive Krew from slashing / piercing weapons (talents): victim and attacker side.
if (report.hpDamage > 0 && (opts.woundKind === 'cutting' || opts.woundKind === 'piercing')) { if (report.hpDamage > 0 && (opts.woundKinds ?? []).some((k) => k === 'slashing' || k === 'piercing')) {
report.krewGained = { report.krewGained = {
victim: await gainKrewFromWeaponDamage(actor as any, report.hpDamage), victim: await gainKrewFromWeaponDamage(actor as any, report.hpDamage),
source: opts.sourceActor ? await gainKrewFromWeaponDamage(opts.sourceActor, report.hpDamage) : 0, source: opts.sourceActor ? await gainKrewFromWeaponDamage(opts.sourceActor, report.hpDamage) : 0,
+1 -1
View File
@@ -196,7 +196,7 @@ export function greatSacrificeResult(successes: number, highestAttribute: number
if (successes > highestAttribute) return { kills: true, heal: 0, krew: Math.max(0, targetHealth) }; if (successes > highestAttribute) return { kills: true, heal: 0, krew: Math.max(0, targetHealth) };
return { kills: false, heal: Math.max(0, successes), krew: 0 }; 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 { export function krewFromWeaponHpLoss(hpLost: number, talent: KrewTalent | null): number {
if (talent === 'respect-for-life') return Math.floor(Math.max(0, hpLost) / 4); 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); if (talent === 'krwisty-pojedynkowicz') return Math.floor(Math.max(0, hpLost) / 2);
+5 -1
View File
@@ -41,7 +41,7 @@ function actorHasTalent(actor: CastableActor, talentId: string): boolean {
return false; return false;
} }
function matchDiscipline(disciplineId: string, targetId: string): boolean { export function matchDiscipline(disciplineId: string, targetId: string): boolean {
const d1 = disciplineId.toLowerCase().trim(); const d1 = disciplineId.toLowerCase().trim();
const d2 = targetId.toLowerCase().trim(); const d2 = targetId.toLowerCase().trim();
if (d1 === d2) return true; if (d1 === d2) return true;
@@ -52,6 +52,8 @@ function matchDiscipline(disciplineId: string, targetId: string): boolean {
abyssprimal: ['abyssprimal', 'magia otchłani', 'primal', 'eldritch', 'eldritch magic', 'abyss - primal', 'magia otchłani - pierwotna magia', 'pierwotna magia'], abyssprimal: ['abyssprimal', 'magia otchłani', 'primal', 'eldritch', 'eldritch magic', 'abyss - primal', 'magia otchłani - pierwotna magia', 'pierwotna magia'],
witch: ['witch', 'wiedźmia magia', 'witch magic', 'dzika wiedźmia magia', 'wildwitch'], witch: ['witch', 'wiedźmia magia', 'witch magic', 'dzika wiedźmia magia', 'wildwitch'],
blood: ['blood', 'magia krwi', 'blood magic'], blood: ['blood', 'magia krwi', 'blood magic'],
sacred: ['sacred', 'magia sakralna', 'sacred magic'],
sacredexorcism: ['sacredexorcism', 'magia sakralna - egzorcyzmy', 'egzorcyzmy', 'exorcism', 'exorcisms', 'sacred - exorcisms'],
}; };
let key1: string | null = null; let key1: string | null = null;
@@ -63,6 +65,8 @@ function matchDiscipline(disciplineId: string, targetId: string): boolean {
} }
if (key1 && key2 && key1 === key2) return true; if (key1 && key2 && key1 === key2) return true;
// Egzorcyzmy are a sub-discipline of Magia Sakralna: an exorcist counts as Sacred, not the other way round.
if (key1 === 'sacredexorcism' && key2 === 'sacred') return true;
return false; return false;
} }
+2
View File
@@ -9,6 +9,7 @@ import {
AOE_SHAPES, AOE_SHAPES,
SPELL_DAMAGE_TYPES, SPELL_DAMAGE_TYPES,
TRIGGER_EVENTS, TRIGGER_EVENTS,
WOUND_KINDS,
} from '../constants'; } from '../constants';
const { ItemSheetV2 } = foundry.applications.sheets as unknown as { const { ItemSheetV2 } = foundry.applications.sheets as unknown as {
@@ -67,6 +68,7 @@ export class HbmItemSheet extends HandlebarsApplicationMixin(ItemSheetV2) {
itemType: item.type, itemType: item.type,
bodyPartial: TYPE_PARTIAL[item.type] ?? 'systems/hbm-rpg-v3/templates/item/_unknown.hbs', bodyPartial: TYPE_PARTIAL[item.type] ?? 'systems/hbm-rpg-v3/templates/item/_unknown.hbs',
choices: buildChoiceMaps(), choices: buildChoiceMaps(),
woundChoices: Object.fromEntries(WOUND_KINDS.map((k) => [k, `HBM.gear.wound.${k}`])),
effects, effects,
supportsEffects, supportsEffects,
}; };
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "hbm-rpg-v3", "id": "hbm-rpg-v3",
"title": "Homebrew Magic: 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).", "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.2",
"manifest": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3.json", "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", "download": "https://vtt-content.octoturge.com/packages/system/hbm-rpg-v3-v1.5.0.zip",
"compatibility": { "compatibility": {
+5
View File
@@ -31,6 +31,11 @@
<option value="environmental" {{#if (eq system.weapon.damageType 'environmental')}}selected{{/if}}>{{localize 'HBM.gear.environmental'}}</option> <option value="environmental" {{#if (eq system.weapon.damageType 'environmental')}}selected{{/if}}>{{localize 'HBM.gear.environmental'}}</option>
</select> </select>
</label> </label>
<label title="{{localize 'HBM.gear.woundKindsHint'}}">{{localize 'HBM.gear.woundKinds'}}:
<multi-select name="system.weapon.woundKinds">
{{selectOptions woundChoices selected=system.weapon.woundKinds localize=true}}
</multi-select>
</label>
</fieldset> </fieldset>
{{/if}} {{/if}}
+19
View File
@@ -0,0 +1,19 @@
import { describe, it, expect } from 'vitest';
import { matchDiscipline } from '../../src/logic/spell-validation';
describe('matchDiscipline: Magia Sakralna - Egzorcyzmy', () => {
it('an exorcist counts as Sacred Magic for casting', () => {
expect(matchDiscipline('sacredExorcism', 'sacred')).toBe(true);
expect(matchDiscipline('Magia Sakralna - Egzorcyzmy', 'sacred')).toBe(true);
expect(matchDiscipline('Egzorcyzmy', 'Magia Sakralna')).toBe(true);
});
it('a plain Sacred mage is not an exorcist', () => {
expect(matchDiscipline('sacred', 'sacredExorcism')).toBe(false);
expect(matchDiscipline('Magia Sakralna', 'egzorcyzmy')).toBe(false);
});
it('does not leak into other disciplines', () => {
expect(matchDiscipline('sacredExorcism', 'witch')).toBe(false);
});
});
+17
View File
@@ -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([]);
});
});