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] 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}} -
+
Modifiers
- {{#unless (eq @root.rollType 'D20Roll')}} - + {{#times 10}} + + {{/times}} + + - {{/unless}} -
- + + {{/unless}} + {{#if @root.rallyDie.length}} + {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} + + {{/if}} + {{#if (eq @root.rollType 'DualityRoll')}}Situational Bonus{{/if}} +
{{/unless}} Formula: {{@root.formula}} diff --git a/templates/ui/chat/duality-roll.hbs b/templates/ui/chat/duality-roll.hbs index 66d32e95..8b44039b 100644 --- a/templates/ui/chat/duality-roll.hbs +++ b/templates/ui/chat/duality-roll.hbs @@ -16,6 +16,11 @@ {{localize "DAGGERHEART.GENERAL.Disadvantage.full"}} {{/if}} + {{#if roll.rally.dice}} +
+ {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} {{roll.rally.dice}} +
+ {{/if}}
{{roll.formula}}
@@ -38,7 +43,7 @@
{{localize "DAGGERHEART.GENERAL.hope"}}
- +
{{roll.hope.value}}
@@ -49,7 +54,7 @@
{{localize "DAGGERHEART.GENERAL.fear"}}
- +
{{roll.fear.value}}
@@ -72,7 +77,7 @@
- +
{{roll.advantage.value}}
@@ -82,6 +87,30 @@
{{/if}} + {{#if roll.rally.dice}} +
+
+ + 1{{roll.rally.dice}} + + {{roll.rally.value}} +
+
+
    +
  1. +
    +
    +
    + +
    +
    {{roll.rally.value}}
    +
    +
    +
  2. +
+
+
+ {{/if}} {{#each roll.extra as | extra | }}