From ad9e0aa558899d74882342ba3a4447ee3029d028 Mon Sep 17 00:00:00 2001
From: Dapoulp <74197441+Dapoulp@users.noreply.github.com>
Date: Thu, 17 Jul 2025 00:45:53 +0200
Subject: [PATCH 1/7] Feature/344 bardic rally (#363)
* 2
* Dardic Rally Dice
---
lang/en.json | 6 +-
module/applications/dialogs/d20RollDialog.mjs | 1 +
module/data/actor/character.mjs | 10 ++-
module/data/fields/actorField.mjs | 7 +-
module/dice/d20Roll.mjs | 10 ++-
module/dice/dualityRoll.mjs | 76 +++++++++++++------
module/documents/token.mjs | 22 ++++++
templates/dialogs/dice-roll/rollSelection.hbs | 26 +++++--
templates/ui/chat/duality-roll.hbs | 35 ++++++++-
9 files changed, 153 insertions(+), 40 deletions(-)
diff --git a/lang/en.json b/lang/en.json
index 4af86abe..75f2b886 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -407,7 +407,11 @@
"rerollDice": "Reroll Dice"
}
},
-
+ "CLASS": {
+ "Feature": {
+ "rallyDice": "Bardic Rally Dice"
+ }
+ },
"CONFIG": {
"ActionType": {
"passive": "Passive",
diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs
index 7987dd6b..67ca77e6 100644
--- a/module/applications/dialogs/d20RollDialog.mjs
+++ b/module/applications/dialogs/d20RollDialog.mjs
@@ -89,6 +89,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
if (this.roll) {
context.roll = this.roll;
context.rollType = this.roll?.constructor.name;
+ context.rallyDie = this.roll.rallyChoices;
context.experiences = Object.keys(this.config.data.experiences).map(id => ({
id,
...this.config.data.experiences[id]
diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs
index 804eabec..20bada01 100644
--- a/module/data/actor/character.mjs
+++ b/module/data/actor/character.mjs
@@ -102,7 +102,7 @@ export default class DhCharacter extends BaseDataActor {
physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage'),
primaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon'),
- secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon')
+ secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.secondaryWeapon')
}),
healing: bonusField('DAGGERHEART.GENERAL.Healing.healingAmount'),
range: new fields.SchemaField({
@@ -121,7 +121,13 @@ export default class DhCharacter extends BaseDataActor {
initial: 0,
label: 'DAGGERHEART.GENERAL.Range.other'
})
- })
+ }),
+ rally: new fields.ArrayField(
+ new fields.StringField(),
+ {
+ label: 'DAGGERHEART.CLASS.Feature.rallyDice'
+ }
+ )
}),
companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({
diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs
index fe00e251..047b6f4f 100644
--- a/module/data/fields/actorField.mjs
+++ b/module/data/fields/actorField.mjs
@@ -25,8 +25,11 @@ const stressDamageReductionRule = localizationPath =>
const bonusField = label =>
new fields.SchemaField({
- bonus: new fields.NumberField({ integer: true, initial: 0, label }),
- dice: new fields.ArrayField(new fields.StringField())
+ bonus: new fields.NumberField({ integer: true, initial: 0, label: `${game.i18n.localize(label)} Value` }),
+ dice: new fields.ArrayField(
+ new fields.StringField(),
+ { label: `${game.i18n.localize(label)} Dice` }
+ )
});
export { attributeField, resourceField, stressDamageReductionRule, bonusField };
diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs
index 0c29fc42..58d45f95 100644
--- a/module/dice/d20Roll.mjs
+++ b/module/dice/d20Roll.mjs
@@ -39,11 +39,13 @@ export default class D20Roll extends DHRoll {
}
get hasAdvantage() {
- return this.options.roll.advantage === this.constructor.ADV_MODE.ADVANTAGE;
+ const adv = this.options.roll.advantage.type ?? this.options.roll.advantage;
+ return adv === this.constructor.ADV_MODE.ADVANTAGE;
}
get hasDisadvantage() {
- return this.options.roll.advantage === this.constructor.ADV_MODE.DISADVANTAGE;
+ const adv = this.options.roll.advantage.type ?? this.options.roll.advantage;
+ return adv === this.constructor.ADV_MODE.DISADVANTAGE;
}
static applyKeybindings(config) {
@@ -90,8 +92,8 @@ export default class D20Roll extends DHRoll {
configureModifiers() {
this.applyAdvantage();
-
- this.baseTerms = foundry.utils.deepClone(this.terms);
+
+ this.baseTerms = foundry.utils.deepClone(this.dice);
this.options.roll.modifiers = this.applyBaseBonus();
diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs
index d983b2d6..5fd71e6c 100644
--- a/module/dice/dualityRoll.mjs
+++ b/module/dice/dualityRoll.mjs
@@ -4,9 +4,12 @@ import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
export default class DualityRoll extends D20Roll {
_advantageFaces = 6;
+ _advantageNumber = 1;
+ _rallyIndex;
constructor(formula, data = {}, options = {}) {
super(formula, data, options);
+ this.rallyChoices = this.setRallyChoices();
}
static messageType = 'dualityRoll';
@@ -51,6 +54,35 @@ export default class DualityRoll extends D20Roll {
this._advantageFaces = this.getFaces(faces);
}
+ get advantageNumber() {
+ return this._advantageNumber;
+ }
+
+ set advantageNumber(value) {
+ this._advantageNumber = Number(value);
+ }
+
+ setRallyChoices() {
+ return this.data?.parent?.effects.reduce((a,c) => {
+ const change = c.changes.find(ch => ch.key === 'system.bonuses.rally');
+ if(change) a.push({ value: c.id, label: change.value });
+ return a;
+ }, []);
+ }
+
+ get dRally() {
+ if(!this.rallyFaces) return null;
+ if(this.hasDisadvantage || this.hasAdvantage)
+ return this.dice[3];
+ else
+ return this.dice[2];
+ }
+
+ get rallyFaces() {
+ const rallyChoice = this.rallyChoices?.find(r => r.value === this._rallyIndex)?.label;
+ return rallyChoice ? this.getFaces(rallyChoice) : null;
+ }
+
get isCritical() {
if (!this.dHope._evaluated || !this.dFear._evaluated) return;
return this.dHope.total === this.dFear.total;
@@ -66,10 +98,6 @@ export default class DualityRoll extends D20Roll {
return this.dHope.total < this.dFear.total;
}
- get hasBarRally() {
- return null;
- }
-
get totalLabel() {
const label = this.withHope
? 'DAGGERHEART.GENERAL.hope'
@@ -98,24 +126,20 @@ export default class DualityRoll extends D20Roll {
}
applyAdvantage() {
- const dieFaces = this.advantageFaces,
- bardRallyFaces = this.hasBarRally,
- advDie = new foundry.dice.terms.Die({ faces: dieFaces });
- if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces)
- this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }));
- if (bardRallyFaces) {
- const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces });
- if (this.hasAdvantage) {
- this.terms.push(
- new foundry.dice.terms.PoolTerm({
- terms: [advDie.formula, rallyDie.formula],
- modifiers: ['kh']
- })
- );
- } else if (this.hasDisadvantage) {
- this.terms.push(advDie, new foundry.dice.terms.OperatorTerm({ operator: '+' }), rallyDie);
- }
- } else if (this.hasAdvantage || this.hasDisadvantage) this.terms.push(advDie);
+ if (this.hasAdvantage || this.hasDisadvantage) {
+ const dieFaces = this.advantageFaces,
+ advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber });
+ if(this.advantageNumber > 1) advDie.modifiers = ['kh'];
+ this.terms.push(
+ new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
+ advDie
+ );
+ }
+ if(this.rallyFaces)
+ this.terms.push(
+ new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
+ new foundry.dice.terms.Die({ faces: this.rallyFaces })
+ );
}
applyBaseBonus() {
@@ -138,6 +162,7 @@ export default class DualityRoll extends D20Roll {
static postEvaluate(roll, config = {}) {
super.postEvaluate(roll, config);
+
config.roll.hope = {
dice: roll.dHope.denomination,
value: roll.dHope.total
@@ -146,12 +171,19 @@ export default class DualityRoll extends D20Roll {
dice: roll.dFear.denomination,
value: roll.dFear.total
};
+ config.roll.rally = {
+ dice: roll.dRally?.denomination,
+ value: roll.dRally?.total
+ };
config.roll.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: roll.dHope.total + roll.dFear.total,
label: roll.totalLabel
};
+ if(roll._rallyIndex && roll.data?.parent)
+ roll.data.parent.deleteEmbeddedDocuments('ActiveEffect', [roll._rallyIndex]);
+
setDiceSoNiceForDualityRoll(roll, config.roll.advantage.type);
}
}
diff --git a/module/documents/token.mjs b/module/documents/token.mjs
index 4592c843..3e7b49ea 100644
--- a/module/documents/token.mjs
+++ b/module/documents/token.mjs
@@ -32,4 +32,26 @@ export default class DHToken extends TokenDocument {
return bars.concat(values);
}
+
+ static _getTrackedAttributesFromSchema(schema, _path=[]) {
+ const attributes = {bar: [], value: []};
+ for ( const [name, field] of Object.entries(schema.fields) ) {
+ const p = _path.concat([name]);
+ if ( field instanceof foundry.data.fields.NumberField ) attributes.value.push(p);
+ if ( field instanceof foundry.data.fields.ArrayField ) attributes.value.push(p);
+ const isSchema = field instanceof foundry.data.fields.SchemaField;
+ const isModel = field instanceof foundry.data.fields.EmbeddedDataField;
+ if ( isSchema || isModel ) {
+ const schema = isModel ? field.model.schema : field;
+ const isBar = schema.has && schema.has("value") && schema.has("max");
+ if ( isBar ) attributes.bar.push(p);
+ else {
+ const inner = this.getTrackedAttributes(schema, p);
+ attributes.bar.push(...inner.bar);
+ attributes.value.push(...inner.value);
+ }
+ }
+ }
+ return attributes;
+ }
}
diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs
index 0d2f3f48..b4c7ccac 100644
--- a/templates/dialogs/dice-roll/rollSelection.hbs
+++ b/templates/dialogs/dice-roll/rollSelection.hbs
@@ -88,7 +88,7 @@
{{/if}}
{{/each}}
-