Skip to content

Commit 8aa4753

Browse files
authored
[#6261] Add modifiers to DamageData (#6834)
* [#6261] Add modifiers to `DamageData` Adds a new modifiers field for damage that is applied automatically to the formula. The `DamageData` model now automatically adds these modifiers to the formula, attaching to the first roll (if one can be found when using a custom formula). The `DamageData#scaledFormula` supports passing in additional modifiers that are also appended. This is to support a future Rule active effect that adds modifiers to certain rolls at evaluation time (such as Elemental Adept, which would add `min2` to all damage rolls from spells that deal a certain damage type). Closes #6261 * [#6261] Tweak manual formula regex, pass modifiers everywhere * [#6261] Use simpler damage formula for labels w/o modifiers * [#6261] Fix issue with joining modifier lists * [#6261] Fix double background on `<string-tags>`
1 parent 36d1697 commit 8aa4753

11 files changed

Lines changed: 98 additions & 42 deletions

File tree

lang/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2161,6 +2161,10 @@
21612161
"label": "Die Denomination",
21622162
"hint": "Denomination of the dice to roll."
21632163
},
2164+
"modifiers": {
2165+
"label": "Modifiers",
2166+
"hint": "Roll modifiers added to the base die."
2167+
},
21642168
"number": {
21652169
"label": "Die Number",
21662170
"hint": "Number of dice to roll."
@@ -3914,6 +3918,7 @@
39143918
},
39153919
"DND5E.RollExample": "e.g. 1d4",
39163920
"DND5E.RollMode": "Roll Mode",
3921+
"DND5E.RollModifiers": "Modifiers",
39173922
"DND5E.RollSituationalBonus": "Situational Bonus?",
39183923
"DND5E.Rule": {
39193924
"Tooltip": "Tooltip",

less/v2/forms.less

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,13 @@
336336
&.multi-select {
337337
position: relative;
338338

339-
:is(multi-select, string-tags) .tags.input-element-tags {
340-
min-height: 16px;
341-
padding-left: 40%;
342-
&:empty { display: flex; }
339+
:is(multi-select, string-tags) {
340+
&:has(.input-element-tags:empty) { margin-block-start: 8px; }
341+
.tags.input-element-tags {
342+
min-height: 16px;
343+
padding-left: 40%;
344+
&:empty { display: flex; }
345+
}
343346
}
344347

345348
> label {
@@ -538,6 +541,8 @@
538541
}
539542

540543
:is(document-tags, multi-select, string-tags) {
544+
input { --input-background-color: transparent; }
545+
541546
.tags.input-element-tags .tag {
542547
cursor: var(--cursor-pointer);
543548
font-size: var(--font-size-10);
@@ -559,7 +564,7 @@
559564
&.hidden-tags .tags.input-element-tags { display: none; }
560565
}
561566

562-
:is(file-picker, document-tags) > button {
567+
:is(file-picker, document-tags, string-tags) > button {
563568
height: var(--form-field-height);
564569
border: var(--dnd5e-border-gold);
565570
}
@@ -745,7 +750,7 @@
745750
&:not(.copy-button, .unbutton, .reveal) { line-height: 28px; }
746751
}
747752

748-
:is(document-tags, multi-select) .tags.input-element-tags .tag {
753+
:is(document-tags, multi-select, string-tags) .tags.input-element-tags .tag {
749754
padding: 1px 4px;
750755
color: var(--color-text-primary);
751756
height: unset;

module/data/activity/attack-data.mjs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,9 @@ export default class BaseAttackActivityData extends BaseActivityData {
280280

281281
/* -------------------------------------------- */
282282

283-
/**
284-
* Get the roll parts used to create the damage rolls.
285-
* @param {Partial<AttackDamageRollProcessConfiguration>} [config={}]
286-
* @returns {AttackDamageRollProcessConfiguration}
287-
*/
288-
getDamageConfig(config={}) {
289-
const rollConfig = super.getDamageConfig(config);
283+
/** @override */
284+
getDamageConfig(config={}, options={}) {
285+
const rollConfig = super.getDamageConfig(config, options);
290286

291287
// Handle ammunition
292288
const ammo = config.ammunition?.system;
@@ -310,14 +306,18 @@ export default class BaseAttackActivityData extends BaseActivityData {
310306
// If mode is "replace" and base part is present, replace the base part
311307
if ( ammo.damage.replace & (basePartIndex !== -1) ) {
312308
damage.base = true;
313-
rollConfig.rolls.splice(basePartIndex, 1, this._processDamagePart(damage, config, rollData, basePartIndex));
309+
rollConfig.rolls.splice(
310+
basePartIndex, 1,
311+
this._processDamagePart(damage, config, rollData, basePartIndex, options.formulaOptions)
312+
);
314313
}
315314

316315
// Otherwise stick the ammo damage after base part (or as first part)
317316
else {
318317
damage.ammo = true;
319318
rollConfig.rolls.splice(
320-
basePartIndex + 1, 0, this._processDamagePart(damage, rollConfig, rollData, basePartIndex + 1)
319+
basePartIndex + 1, 0,
320+
this._processDamagePart(damage, rollConfig, rollData, basePartIndex + 1, options.formulaOptions)
321321
);
322322
}
323323
}
@@ -368,8 +368,8 @@ export default class BaseAttackActivityData extends BaseActivityData {
368368
/* -------------------------------------------- */
369369

370370
/** @inheritDoc */
371-
_processDamagePart(damage, rollConfig, rollData, index=0) {
372-
if ( !damage.base ) return super._processDamagePart(damage, rollConfig, rollData, index);
371+
_processDamagePart(damage, rollConfig, rollData, index=0, options={}) {
372+
if ( !damage.base ) return super._processDamagePart(damage, rollConfig, rollData, index, options);
373373

374374
// Swap base damage for versatile if two-handed attack is made on versatile weapon
375375
if ( this.item.system.isVersatile && (rollConfig.attackMode === "twoHanded") ) {
@@ -381,7 +381,7 @@ export default class BaseAttackActivityData extends BaseActivityData {
381381
damage = versatile;
382382
}
383383

384-
const roll = super._processDamagePart(damage, rollConfig, rollData, index);
384+
const roll = super._processDamagePart(damage, rollConfig, rollData, index, options);
385385
roll.base = true;
386386

387387
if ( this.item.type === "weapon" ) {

module/data/activity/base-activity.mjs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
/**
2020
* @import { DamageRollConfiguration, DamageRollProcessConfiguration } from "../../dice/_types.mjs";
2121
* @import { ActivityRollData } from "../../documents/_types.mjs";
22+
* @import { DamageFormulaOptions } from "../shared/_types.mjs";
2223
* @import { ActivityData } from "./_types.mjs";
2324
*/
2425

@@ -636,7 +637,7 @@ export default class BaseActivityData extends foundry.abstract.DataModel {
636637
* @param {ActivityRollData} rollData Deterministic roll data from the item.
637638
*/
638639
prepareDamageLabel(rollData) {
639-
const config = this.getDamageConfig({}, { rollData });
640+
const config = this.getDamageConfig({}, { formulaOptions: { modifiers: false }, rollData });
640641
const rolls = aggregateDamageRolls(config.rolls.map(({ base, data, options, parts }) => {
641642
const formula = parts.join(" + ");
642643
try {
@@ -728,16 +729,17 @@ export default class BaseActivityData extends foundry.abstract.DataModel {
728729
* Get the roll parts used to create the damage rolls.
729730
* @param {Partial<DamageRollProcessConfiguration>} [config={}] Existing damage configuration to merge into this one.
730731
* @param {object} [options] Damage configuration options.
732+
* @param {DamageFormulaOptions} [options.formulaOptions] Options to configure the formula.
731733
* @param {ActivityRollData} [options.rollData] Use pre-existing roll data.
732734
* @returns {DamageRollProcessConfiguration}
733735
*/
734-
getDamageConfig(config={}, { rollData }={}) {
736+
getDamageConfig(config={}, { formulaOptions, rollData }={}) {
735737
if ( !this.damage?.parts ) return foundry.utils.mergeObject({ rolls: [] }, config);
736738

737739
const rollConfig = foundry.utils.deepClone(config);
738740
rollData ??= this.getRollData();
739741
rollConfig.rolls = this.damage.parts
740-
.map((d, index) => this._processDamagePart(d, rollConfig, rollData, index))
742+
.map((d, index) => this._processDamagePart(d, rollConfig, rollData, index, formulaOptions))
741743
.filter(d => d.parts.length)
742744
.concat(config.rolls ?? []);
743745

@@ -752,11 +754,12 @@ export default class BaseActivityData extends foundry.abstract.DataModel {
752754
* @param {Partial<DamageRollProcessConfiguration>} rollConfig Roll configuration being built.
753755
* @param {ActivityRollData} rollData Roll data to populate with damage data.
754756
* @param {number} [index=0] Index of the damage part.
757+
* @param {DamageFormulaOptions} [options={}] Options to configure the formula.
755758
* @returns {DamageRollConfiguration}
756759
* @protected
757760
*/
758-
_processDamagePart(damage, rollConfig, rollData, index=0) {
759-
const scaledFormula = damage.scaledFormula(rollConfig.scaling ?? rollData.scaling);
761+
_processDamagePart(damage, rollConfig, rollData, index=0, options={}) {
762+
const scaledFormula = damage.scaledFormula(rollConfig.scaling ?? rollData.scaling, options);
760763
const parts = scaledFormula ? [scaledFormula] : [];
761764
const data = { ...rollData };
762765

module/data/activity/damage-data.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ export default class BaseDamageActivityData extends BaseActivityData {
6363
/* -------------------------------------------- */
6464

6565
/** @inheritDoc */
66-
getDamageConfig(config={}) {
67-
const rollConfig = super.getDamageConfig(config);
66+
getDamageConfig(config={}, options={}) {
67+
const rollConfig = super.getDamageConfig(config, options);
6868

6969
rollConfig.critical ??= {};
7070
rollConfig.critical.allow ??= this.damage.critical.allow;

module/data/activity/heal-data.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ export default class BaseHealActivityData extends BaseActivityData {
4646
/* -------------------------------------------- */
4747

4848
/** @override */
49-
getDamageConfig(config={}) {
49+
getDamageConfig(config={}, options={}) {
5050
if ( !this.healing.formula ) return foundry.utils.mergeObject({ rolls: [] }, config);
5151

5252
const rollConfig = foundry.utils.mergeObject({ critical: { allow: false } }, config);
53-
const rollData = this.getRollData();
54-
rollConfig.rolls = [this._processDamagePart(this.healing, rollConfig, rollData)].concat(config.rolls ?? []);
53+
const rollData = options.rollData ?? this.getRollData();
54+
rollConfig.rolls = [
55+
this._processDamagePart(this.healing, rollConfig, rollData, options.formulaOptions)
56+
].concat(config.rolls ?? []);
5557

5658
return rollConfig;
5759
}

module/data/activity/save-data.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ export default class BaseSaveActivityData extends BaseActivityData {
138138
/* -------------------------------------------- */
139139

140140
/** @inheritDoc */
141-
getDamageConfig(config={}) {
142-
const rollConfig = super.getDamageConfig(config);
141+
getDamageConfig(config={}, options={}) {
142+
const rollConfig = super.getDamageConfig(config, options);
143143

144144
rollConfig.critical ??= {};
145145
rollConfig.critical.allow ??= false;

module/data/shared/_types.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,19 @@
3232
* @property {object} custom
3333
* @property {boolean} custom.enabled Should the custom formula be used?
3434
* @property {string} custom.formula Custom damage formula.
35+
* @property {Set<string>} modifiers Modifiers to apply to damage roll.
3536
* @property {object} scaling
3637
* @property {string} scaling.mode How the damage scales in relation with levels.
3738
* @property {number} scaling.number Number of dice to add per scaling level.
3839
* @property {string} scaling.formula Arbitrary scaling formula which will be multiplied by scaling increase.
3940
*/
4041

42+
/**
43+
* @typedef DamageFormulaOptions
44+
* @property {Set<string>|false} modifiers Additional modifiers to apply to the formula, if possible.
45+
* A `false` value will remove modifiers provided by damage data.
46+
*/
47+
4148
/**
4249
* @typedef DurationData
4350
* @property {string} value Scalar value for the activity's duration.

module/data/shared/damage-field.mjs

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import FormulaField from "../fields/formula-field.mjs";
33

44
const { BooleanField, EmbeddedDataField, NumberField, SchemaField, SetField, StringField } = foundry.data.fields;
55

6+
/**
7+
* @import { DamageFormulaOptions } from "./_types.mjs";
8+
*/
9+
610
/**
711
* Field for storing damage data.
812
*/
@@ -34,6 +38,7 @@ export class DamageData extends foundry.abstract.DataModel {
3438
enabled: new BooleanField(),
3539
formula: new FormulaField()
3640
}),
41+
modifiers: new SetField(new StringField()),
3742
scaling: new SchemaField({
3843
mode: new StringField(),
3944
number: new NumberField({ initial: 1, min: 0, integer: true }),
@@ -51,7 +56,7 @@ export class DamageData extends foundry.abstract.DataModel {
5156
* @type {string}
5257
*/
5358
get formula() {
54-
if ( this.custom.enabled ) return this.custom.formula ?? "";
59+
if ( this.custom.enabled ) return this._manualFormula();
5560
return this._automaticFormula();
5661
}
5762

@@ -60,44 +65,65 @@ export class DamageData extends foundry.abstract.DataModel {
6065
/* -------------------------------------------- */
6166

6267
/**
63-
* Produce the auto-generated formula from the `number`, `denomination`, and `bonus`.
64-
* @param {number} [increase=0] Amount to increase the die count.
68+
* Produce the auto-generated formula from the `number`, `denomination`, `modifiers`, and `bonus`.
69+
* @param {number} [increase=0] Amount to increase the die count.
70+
* @param {DamageFormulaOptions} [options={}] Options to configure the formula.
6571
* @returns {string}
6672
* @protected
6773
*/
68-
_automaticFormula(increase=0) {
74+
_automaticFormula(increase=0, { modifiers }={}) {
6975
let formula;
7076
const number = (this.number ?? 0) + increase;
71-
if ( number && this.denomination ) formula = `${number}d${this.denomination}`;
77+
if ( number && this.denomination ) {
78+
formula = `${number}d${this.denomination}${modifiers !== false
79+
? Array.from(this.modifiers).concat(...(modifiers ?? [])).join("") : ""}`;
80+
}
7281
if ( this.bonus ) formula = formula ? `${formula} + ${this.bonus}` : this.bonus;
7382
return formula ?? "";
7483
}
7584

7685
/* -------------------------------------------- */
7786

87+
/**
88+
* Produce the manual formula from the `custom.formula` and `modifiers` (if possible).
89+
* @param {DamageFormulaOptions} [options={}] Options to configure the formula.
90+
* @returns {string}
91+
* @protected
92+
*/
93+
_manualFormula({ modifiers }={}) {
94+
if ( !this.custom.formula ) return "";
95+
if ( modifiers === false ) return this.custom.formula;
96+
modifiers = Array.from(this.modifiers).concat(...(modifiers ?? [])).join("");
97+
return this.custom.formula.replace(/(?:\d|\))?d(?:\d+\w*|\([^)]+\)\d*\w*)/, `$&${modifiers}`);
98+
}
99+
100+
/* -------------------------------------------- */
101+
78102
/**
79103
* Scale the damage by a number of steps using its configured scaling configuration.
80-
* @param {number|Scaling} increase Number of steps above base damage to scaling.
104+
* @param {number|Scaling} increase Number of steps above base damage to scaling.
105+
* @param {DamageFormulaOptions} [options={}] Options to configure the formula.
81106
* @returns {string}
82107
*/
83-
scaledFormula(increase) {
108+
scaledFormula(increase, options={}) {
84109
if ( increase instanceof Scaling ) increase = increase.increase;
85110

86111
switch ( this.scaling.mode ) {
87112
case "whole": break;
88113
case "half": increase = Math.floor(increase * .5); break;
89114
default: increase = 0; break;
90115
}
91-
if ( !increase ) return this.formula;
116+
if ( !increase ) return this.custom.enabled
117+
? this._manualFormula(options) : this._automaticFormula(0, options);
92118
let formula;
93119

94120
// If dice count scaling, increase the count on the first die rolled
95121
const dieIncrease = (this.scaling.number ?? 0) * increase;
96122
if ( this.custom.enabled ) {
97-
formula = this.custom.formula;
123+
formula = this._manualFormula(options);
98124
formula = formula.replace(/^(\d)+d/, (match, number) => `${Number(number) + dieIncrease}d`);
99125
} else {
100-
formula = this._automaticFormula(dieIncrease);
126+
formula = this._automaticFormula(dieIncrease, options);
101127
}
102128

103129
// If custom scaling included, modify to match increase and append for formula

templates/activity/parts/damage-part.hbs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,14 @@
3434
{{/unless}}
3535
</div>
3636

37+
<div class="field-group">
38+
{{ formField fields.modifiers name=(concat prefix "modifiers") value=source.modifiers label="DND5E.RollModifiers"
39+
localize=true classes="label-top multi-select" rootId=@root.partId }}
40+
</div>
41+
3742
<div class="field-group">
3843
{{ formField fields.types name=(concat prefix "types") value=source.types options=typeOptions hint=false
39-
label="DND5E.Type" localize=true classes="label-top multi-select"
40-
rootId=@root.partId }}
44+
label="DND5E.Type" localize=true classes="label-top multi-select" rootId=@root.partId }}
4145
</div>
4246

4347
{{#if canScale}}

0 commit comments

Comments
 (0)