Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c900e26d |
@@ -5,6 +5,11 @@ 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).
|
||||||
|
|
||||||
|
## [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
|
## [1.5.1] - 2026-10-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -363,6 +363,8 @@
|
|||||||
},
|
},
|
||||||
"spellCast": {
|
"spellCast": {
|
||||||
"title": "Casting spell",
|
"title": "Casting spell",
|
||||||
|
"diceModifier": "Dice modifier",
|
||||||
|
"diceModifierHint": "+/- dice in the casting test (the pool never drops below 0)",
|
||||||
"manaSpent": "Mana spent",
|
"manaSpent": "Mana spent",
|
||||||
"cannotBeUnraveled": "Cannot be unraveled",
|
"cannotBeUnraveled": "Cannot be unraveled",
|
||||||
"zealSpent": "Zeal spent",
|
"zealSpent": "Zeal spent",
|
||||||
|
|||||||
@@ -383,6 +383,8 @@
|
|||||||
},
|
},
|
||||||
"spellCast": {
|
"spellCast": {
|
||||||
"title": "Rzucanie zaklęcia",
|
"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",
|
"manaSpent": "Wydana mana",
|
||||||
"cannotBeUnraveled": "Nie da się rozpleść",
|
"cannotBeUnraveled": "Nie da się rozpleść",
|
||||||
"cast": "Rzuć",
|
"cast": "Rzuć",
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ export async function askCastOptions(spell: SpellForDialog, actor: ActorForDialo
|
|||||||
<input type="number" name="bloodSpent" value="${baseBlood}" min="${baseBlood}" max="${max}"/>
|
<input type="number" name="bloodSpent" value="${baseBlood}" min="${baseBlood}" max="${max}"/>
|
||||||
</div>`);
|
</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') {
|
if (mode !== 'sacred') {
|
||||||
fields.push(`
|
fields.push(`
|
||||||
<div class="form-group">
|
<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.zealSpent != null) opts.zealSpent = Math.max(1, Number(fd.zealSpent) || 1);
|
||||||
if (fd.bloodSpent != null) opts.bloodSpent = Math.max(baseBlood, Number(fd.bloodSpent) || baseBlood);
|
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.hekateMode) opts.hekateMode = 'witch';
|
||||||
if (fd.castAsPrayer) opts.castAsPrayer = true;
|
if (fd.castAsPrayer) opts.castAsPrayer = true;
|
||||||
if (fd.bypassSuperspellWarning) opts.bypassSuperspellWarning = true;
|
if (fd.bypassSuperspellWarning) opts.bypassSuperspellWarning = true;
|
||||||
|
|||||||
@@ -259,3 +259,8 @@ export function negotiationBand(successes: number): number {
|
|||||||
export function fearThresholdMod(sourceId: string | undefined, targetId: string | undefined): number {
|
export function fearThresholdMod(sourceId: string | undefined, targetId: string | undefined): number {
|
||||||
return sourceId && targetId && sourceId === targetId ? 1 : 0;
|
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 { HbmTSRoll } from '../dice/ts-roll';
|
||||||
import { TS_DEFAULT_REQUIRED, TS_DEFAULT_THRESHOLD, getMagicPowerEntry } from '../constants';
|
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 { validateCast } from './spell-validation';
|
||||||
import type { ValidationResult } from './spell-validation';
|
import type { ValidationResult } from './spell-validation';
|
||||||
import { rollSpellDamage } from './spell-damage';
|
import { rollSpellDamage } from './spell-damage';
|
||||||
@@ -76,6 +76,8 @@ export interface CastOptions {
|
|||||||
bypassSuperspellWarning?: boolean;
|
bypassSuperspellWarning?: boolean;
|
||||||
bypassNonCombatBlock?: boolean;
|
bypassNonCombatBlock?: boolean;
|
||||||
groupCasters?: string[];
|
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). */
|
/** Override required successes (used by variableSuccesses summons). */
|
||||||
requiredOverride?: number;
|
requiredOverride?: number;
|
||||||
speaker?: ChatMessage.SpeakerData;
|
speaker?: ChatMessage.SpeakerData;
|
||||||
@@ -276,9 +278,10 @@ async function castStandard(actor: CastableActor, spell: SpellLike, opts: CastOp
|
|||||||
const magicDice = typeof a.magic.dicePool === 'number'
|
const magicDice = typeof a.magic.dicePool === 'number'
|
||||||
? a.magic.dicePool
|
? a.magic.dicePool
|
||||||
: getMagicPowerEntry(a.magic.actual ?? a.magic.value ?? 0).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)
|
? a.soul.value + (actor.system.skills?.devotion?.value ?? 0)
|
||||||
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
||||||
|
const pool = applyDiceModifier(basePool, opts.diceModifier);
|
||||||
const flavor = opts.castAsPrayer
|
const flavor = opts.castAsPrayer
|
||||||
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')}`
|
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')}`
|
||||||
: `${spell.name}`;
|
: `${spell.name}`;
|
||||||
@@ -349,7 +352,7 @@ async function castSacred(actor: CastableActor, spell: SpellLike, opts: CastOpti
|
|||||||
const zealCost = Math.max(1, opts.zealSpent ?? 1);
|
const zealCost = Math.max(1, opts.zealSpent ?? 1);
|
||||||
await actor.update({ 'system.attributes.zeal.value': a.zeal.value - zealCost });
|
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 flavor = `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.sacred')}`;
|
||||||
const required = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
const required = opts.requiredOverride ?? spell.system.difficulty.successes ?? TS_DEFAULT_REQUIRED;
|
||||||
const roll = HbmTSRoll.fromParams({
|
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'
|
const magicDice = typeof (a.magic as any).dicePool === 'number'
|
||||||
? (a.magic as any).dicePool
|
? (a.magic as any).dicePool
|
||||||
: getMagicPowerEntry((a.magic as any).actual ?? a.magic.value ?? 0).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)
|
? a.soul.value + (actor.system.skills?.devotion?.value ?? 0)
|
||||||
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
||||||
|
const pool = applyDiceModifier(basePool, opts.diceModifier);
|
||||||
const flavor = opts.castAsPrayer
|
const flavor = opts.castAsPrayer
|
||||||
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')} (${required}/${maxSymbols})`
|
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')} (${required}/${maxSymbols})`
|
||||||
: `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.witch')} (${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'
|
const magicDice = typeof (a.magic as any).dicePool === 'number'
|
||||||
? (a.magic as any).dicePool
|
? (a.magic as any).dicePool
|
||||||
: getMagicPowerEntry((a.magic as any).actual ?? a.magic.value ?? 0).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)
|
? a.soul.value + (actor.system.skills?.devotion?.value ?? 0)
|
||||||
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
: magicDice + (actor.system.skills?.magicalAbilities?.value ?? 0);
|
||||||
|
const pool = applyDiceModifier(basePool, opts.diceModifier);
|
||||||
const flavor = opts.castAsPrayer
|
const flavor = opts.castAsPrayer
|
||||||
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')}`
|
? `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.prayer')}`
|
||||||
: `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.blood')}`;
|
: `${spell.name} - ${game.i18n.localize('HBM.spell.castingMode.blood')}`;
|
||||||
|
|||||||
@@ -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]);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user