Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c900e26d | ||
|
|
582b189ce6 | ||
|
|
5e6dc0698e | ||
|
|
96b2935ff5 | ||
|
|
8d5fd7338a | ||
|
|
a5f2683d15 | ||
|
|
23027cf1b3 |
@@ -5,6 +5,26 @@ 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]
|
||||
|
||||
### Added
|
||||
- Spell cast dialog has a **Modyfikator liczby kości** (dice modifier, +/-). It changes the number of dice in the casting test for every casting mode (standard, sacred, witch, blood, prayer); the pool never drops below 0. The over-limit Nadczarowanie test uses the same modified pool. API: `CastOptions.diceModifier`, `applyDiceModifier()` in `rules.ts`.
|
||||
|
||||
## [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.
|
||||
|
||||
@@ -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",
|
||||
@@ -356,6 +363,8 @@
|
||||
},
|
||||
"spellCast": {
|
||||
"title": "Casting spell",
|
||||
"diceModifier": "Dice modifier",
|
||||
"diceModifierHint": "+/- dice in the casting test (the pool never drops below 0)",
|
||||
"manaSpent": "Mana spent",
|
||||
"cannotBeUnraveled": "Cannot be unraveled",
|
||||
"zealSpent": "Zeal spent",
|
||||
|
||||
@@ -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",
|
||||
@@ -376,6 +383,8 @@
|
||||
},
|
||||
"spellCast": {
|
||||
"title": "Rzucanie zaklęcia",
|
||||
"diceModifier": "Modyfikator liczby kości",
|
||||
"diceModifierHint": "+/- kości w Teście rzucania (liczba kości nie spadnie poniżej 0)",
|
||||
"manaSpent": "Wydana mana",
|
||||
"cannotBeUnraveled": "Nie da się rozpleść",
|
||||
"cast": "Rzuć",
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -38,6 +38,27 @@ export const parseGear: ParserFn = async (ctx: ParserContext): Promise<ParsedDoc
|
||||
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 } {
|
||||
const fm: Record<string, string> = {};
|
||||
let content = raw;
|
||||
@@ -48,9 +69,17 @@ function extractMarkdown(raw: string): { frontmatter: Record<string, string>; 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<string, string>, content: string,
|
||||
weapon: {
|
||||
damage: '',
|
||||
damageType: 'physical',
|
||||
woundKinds: [],
|
||||
properties: [],
|
||||
},
|
||||
armor: {
|
||||
@@ -105,6 +135,7 @@ function buildGearDoc(name: string, fm: Record<string, string>, 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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -76,6 +76,12 @@ export async function askCastOptions(spell: SpellForDialog, actor: ActorForDialo
|
||||
<input type="number" name="bloodSpent" value="${baseBlood}" min="${baseBlood}" max="${max}"/>
|
||||
</div>`);
|
||||
}
|
||||
fields.push(`
|
||||
<div class="form-group">
|
||||
<label>${game.i18n.localize('HBM.spellCast.diceModifier')}</label>
|
||||
<input type="number" name="diceModifier" value="0" step="1"/>
|
||||
<small>${game.i18n.localize('HBM.spellCast.diceModifierHint')}</small>
|
||||
</div>`);
|
||||
if (mode !== 'sacred') {
|
||||
fields.push(`
|
||||
<div class="form-group">
|
||||
@@ -162,6 +168,8 @@ export async function askCastOptions(spell: SpellForDialog, actor: ActorForDialo
|
||||
}
|
||||
if (fd.zealSpent != null) opts.zealSpent = Math.max(1, Number(fd.zealSpent) || 1);
|
||||
if (fd.bloodSpent != null) opts.bloodSpent = Math.max(baseBlood, Number(fd.bloodSpent) || baseBlood);
|
||||
const diceMod = Math.trunc(Number(fd.diceModifier) || 0);
|
||||
if (diceMod !== 0) opts.diceModifier = diceMod;
|
||||
if (fd.hekateMode) opts.hekateMode = 'witch';
|
||||
if (fd.castAsPrayer) opts.castAsPrayer = true;
|
||||
if (fd.bypassSuperspellWarning) opts.bypassSuperspellWarning = true;
|
||||
|
||||
@@ -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<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),
|
||||
* 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> {
|
||||
const blood = (actor.system.attributes as any)?.blood;
|
||||
|
||||
+4
-3
@@ -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,
|
||||
|
||||
+6
-1
@@ -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);
|
||||
@@ -259,3 +259,8 @@ export function negotiationBand(successes: number): number {
|
||||
export function fearThresholdMod(sourceId: string | undefined, targetId: string | undefined): number {
|
||||
return sourceId && targetId && sourceId === targetId ? 1 : 0;
|
||||
}
|
||||
|
||||
/** Casting test pool with the player's +/- dice modifier (whole dice, never below 0). */
|
||||
export function applyDiceModifier(pool: number, modifier?: number): number {
|
||||
return Math.max(0, Math.trunc(Number(pool) || 0) + Math.trunc(Number(modifier) || 0));
|
||||
}
|
||||
|
||||
+10
-5
@@ -16,7 +16,7 @@
|
||||
|
||||
import { HbmTSRoll } from '../dice/ts-roll';
|
||||
import { TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
||||
import { manaToBorrow, exhaustionDice, manaExhaustionTier, overLimitTest, SPELL_DAMAGE_PROFILES } from './rules';
|
||||
import { manaToBorrow, exhaustionDice, manaExhaustionTier, overLimitTest, applyDiceModifier, SPELL_DAMAGE_PROFILES } from './rules';
|
||||
import { validateCast } from './spell-validation';
|
||||
import type { ValidationResult } from './spell-validation';
|
||||
import { rollSpellDamage } from './spell-damage';
|
||||
@@ -76,6 +76,8 @@ export interface CastOptions {
|
||||
bypassSuperspellWarning?: boolean;
|
||||
bypassNonCombatBlock?: boolean;
|
||||
groupCasters?: string[];
|
||||
/** Modifier to the number of dice in the casting test (+/-, pool never below 0). */
|
||||
diceModifier?: number;
|
||||
/** Override required successes (used by variableSuccesses summons). */
|
||||
requiredOverride?: number;
|
||||
speaker?: ChatMessage.SpeakerData;
|
||||
@@ -276,9 +278,10 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
||||
const magicDice = typeof a.magic.dicePool === 'number'
|
||||
? a.magic.dicePool
|
||||
: getMagicPowerEntry(a.magic.actual ?? a.magic.value ?? 0).dicePool;
|
||||
const pool = opts.castAsPrayer
|
||||
const basePool = opts.castAsPrayer
|
||||
? a.soul.value + (actor.system.skills?.devotion?.value ?? 0)
|
||||
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
||||
const pool = applyDiceModifier(basePool, opts.diceModifier);
|
||||
const flavor = opts.castAsPrayer
|
||||
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')}`
|
||||
: `${spell.name}`;
|
||||
@@ -349,7 +352,7 @@ async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOpti
|
||||
const zealCost = Math.max(1, opts.zealSpent ?? 1);
|
||||
await actor.update({ 'system.attributes.zeal.value': a.zeal.value - zealCost });
|
||||
|
||||
const pool = a.soul.value + (actor.system.skills?.devotion?.value ?? 0);
|
||||
const pool = applyDiceModifier(a.soul.value + (actor.system.skills?.devotion?.value ?? 0), opts.diceModifier);
|
||||
const flavor = `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.sacred')}`;
|
||||
const required = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
||||
const roll = HbmTSRoll.fromParams({
|
||||
@@ -379,9 +382,10 @@ async function castWitch(actor: CastableActor, spell: SpellLike, opts: CastOptio
|
||||
const magicDice = typeof (a.magic as any).dicePool === 'number'
|
||||
? (a.magic as any).dicePool
|
||||
: getMagicPowerEntry((a.magic as any).actual ?? a.magic.value ?? 0).dicePool;
|
||||
const pool = opts.castAsPrayer
|
||||
const basePool = opts.castAsPrayer
|
||||
? a.soul.value + (actor.system.skills?.devotion?.value ?? 0)
|
||||
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
||||
const pool = applyDiceModifier(basePool, opts.diceModifier);
|
||||
const flavor = opts.castAsPrayer
|
||||
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')} (${required}/${maxSymbols})`
|
||||
: `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.witch')} (${required}/${maxSymbols})`;
|
||||
@@ -421,9 +425,10 @@ async function castBlood(actor: CastableActor, spell: SpellLike, opts: CastOptio
|
||||
const magicDice = typeof (a.magic as any).dicePool === 'number'
|
||||
? (a.magic as any).dicePool
|
||||
: getMagicPowerEntry((a.magic as any).actual ?? a.magic.value ?? 0).dicePool;
|
||||
const pool = opts.castAsPrayer
|
||||
const basePool = opts.castAsPrayer
|
||||
? a.soul.value + (actor.system.skills?.devotion?.value ?? 0)
|
||||
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
||||
const pool = applyDiceModifier(basePool, opts.diceModifier);
|
||||
const flavor = opts.castAsPrayer
|
||||
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')}`
|
||||
: `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.blood')}`;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
<option value="environmental" {{#if (eq system.weapon.damageType 'environmental')}}selected{{/if}}>{{localize 'HBM.gear.environmental'}}</option>
|
||||
</select>
|
||||
</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>
|
||||
{{/if}}
|
||||
|
||||
|
||||
@@ -104,3 +104,14 @@ describe('blood, elixirs, trade', () => {
|
||||
expect([0, 1, 2, 3, 4, 5, 6, 7, 12].map(negotiationBand)).toEqual([0, 1, 1, 2, 2, 3, 3, 4, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyDiceModifier', () => {
|
||||
it('adds a signed modifier and never goes below 0', async () => {
|
||||
const { applyDiceModifier } = await import('../../src/logic/rules');
|
||||
expect(applyDiceModifier(5, 2)).toBe(7);
|
||||
expect(applyDiceModifier(5, -2)).toBe(3);
|
||||
expect(applyDiceModifier(2, -5)).toBe(0);
|
||||
expect(applyDiceModifier(4)).toBe(4);
|
||||
expect(applyDiceModifier(4, 1.9)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user