Feature/344 bardic rally (#363)

* 2

* Dardic Rally Dice
This commit is contained in:
Dapoulp 2025-07-17 00:45:53 +02:00 committed by GitHub
parent 3176438293
commit ad9e0aa558
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 153 additions and 40 deletions

View file

@ -407,7 +407,11 @@
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"
} }
}, },
"CLASS": {
"Feature": {
"rallyDice": "Bardic Rally Dice"
}
},
"CONFIG": { "CONFIG": {
"ActionType": { "ActionType": {
"passive": "Passive", "passive": "Passive",

View file

@ -89,6 +89,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
if (this.roll) { if (this.roll) {
context.roll = this.roll; context.roll = this.roll;
context.rollType = this.roll?.constructor.name; context.rollType = this.roll?.constructor.name;
context.rallyDie = this.roll.rallyChoices;
context.experiences = Object.keys(this.config.data.experiences).map(id => ({ context.experiences = Object.keys(this.config.data.experiences).map(id => ({
id, id,
...this.config.data.experiences[id] ...this.config.data.experiences[id]

View file

@ -102,7 +102,7 @@ export default class DhCharacter extends BaseDataActor {
physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'), physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'),
magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage'), magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage'),
primaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon'), primaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon'),
secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon') secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.secondaryWeapon')
}), }),
healing: bonusField('DAGGERHEART.GENERAL.Healing.healingAmount'), healing: bonusField('DAGGERHEART.GENERAL.Healing.healingAmount'),
range: new fields.SchemaField({ range: new fields.SchemaField({
@ -121,7 +121,13 @@ export default class DhCharacter extends BaseDataActor {
initial: 0, initial: 0,
label: 'DAGGERHEART.GENERAL.Range.other' 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 }), companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({ rules: new fields.SchemaField({

View file

@ -25,8 +25,11 @@ const stressDamageReductionRule = localizationPath =>
const bonusField = label => const bonusField = label =>
new fields.SchemaField({ new fields.SchemaField({
bonus: new fields.NumberField({ integer: true, initial: 0, label }), bonus: new fields.NumberField({ integer: true, initial: 0, label: `${game.i18n.localize(label)} Value` }),
dice: new fields.ArrayField(new fields.StringField()) dice: new fields.ArrayField(
new fields.StringField(),
{ label: `${game.i18n.localize(label)} Dice` }
)
}); });
export { attributeField, resourceField, stressDamageReductionRule, bonusField }; export { attributeField, resourceField, stressDamageReductionRule, bonusField };

View file

@ -39,11 +39,13 @@ export default class D20Roll extends DHRoll {
} }
get hasAdvantage() { 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() { 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) { static applyKeybindings(config) {
@ -91,7 +93,7 @@ export default class D20Roll extends DHRoll {
configureModifiers() { configureModifiers() {
this.applyAdvantage(); this.applyAdvantage();
this.baseTerms = foundry.utils.deepClone(this.terms); this.baseTerms = foundry.utils.deepClone(this.dice);
this.options.roll.modifiers = this.applyBaseBonus(); this.options.roll.modifiers = this.applyBaseBonus();

View file

@ -4,9 +4,12 @@ import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs';
export default class DualityRoll extends D20Roll { export default class DualityRoll extends D20Roll {
_advantageFaces = 6; _advantageFaces = 6;
_advantageNumber = 1;
_rallyIndex;
constructor(formula, data = {}, options = {}) { constructor(formula, data = {}, options = {}) {
super(formula, data, options); super(formula, data, options);
this.rallyChoices = this.setRallyChoices();
} }
static messageType = 'dualityRoll'; static messageType = 'dualityRoll';
@ -51,6 +54,35 @@ export default class DualityRoll extends D20Roll {
this._advantageFaces = this.getFaces(faces); 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() { get isCritical() {
if (!this.dHope._evaluated || !this.dFear._evaluated) return; if (!this.dHope._evaluated || !this.dFear._evaluated) return;
return this.dHope.total === this.dFear.total; return this.dHope.total === this.dFear.total;
@ -66,10 +98,6 @@ export default class DualityRoll extends D20Roll {
return this.dHope.total < this.dFear.total; return this.dHope.total < this.dFear.total;
} }
get hasBarRally() {
return null;
}
get totalLabel() { get totalLabel() {
const label = this.withHope const label = this.withHope
? 'DAGGERHEART.GENERAL.hope' ? 'DAGGERHEART.GENERAL.hope'
@ -98,24 +126,20 @@ export default class DualityRoll extends D20Roll {
} }
applyAdvantage() { applyAdvantage() {
const dieFaces = this.advantageFaces, if (this.hasAdvantage || this.hasDisadvantage) {
bardRallyFaces = this.hasBarRally, const dieFaces = this.advantageFaces,
advDie = new foundry.dice.terms.Die({ faces: dieFaces }); advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber });
if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces) if(this.advantageNumber > 1) advDie.modifiers = ['kh'];
this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' })); this.terms.push(
if (bardRallyFaces) { new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces }); advDie
if (this.hasAdvantage) { );
this.terms.push( }
new foundry.dice.terms.PoolTerm({ if(this.rallyFaces)
terms: [advDie.formula, rallyDie.formula], this.terms.push(
modifiers: ['kh'] new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }),
}) new foundry.dice.terms.Die({ faces: this.rallyFaces })
); );
} 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);
} }
applyBaseBonus() { applyBaseBonus() {
@ -138,6 +162,7 @@ export default class DualityRoll extends D20Roll {
static postEvaluate(roll, config = {}) { static postEvaluate(roll, config = {}) {
super.postEvaluate(roll, config); super.postEvaluate(roll, config);
config.roll.hope = { config.roll.hope = {
dice: roll.dHope.denomination, dice: roll.dHope.denomination,
value: roll.dHope.total value: roll.dHope.total
@ -146,12 +171,19 @@ export default class DualityRoll extends D20Roll {
dice: roll.dFear.denomination, dice: roll.dFear.denomination,
value: roll.dFear.total value: roll.dFear.total
}; };
config.roll.rally = {
dice: roll.dRally?.denomination,
value: roll.dRally?.total
};
config.roll.result = { config.roll.result = {
duality: roll.withHope ? 1 : roll.withFear ? -1 : 0, duality: roll.withHope ? 1 : roll.withFear ? -1 : 0,
total: roll.dHope.total + roll.dFear.total, total: roll.dHope.total + roll.dFear.total,
label: roll.totalLabel label: roll.totalLabel
}; };
if(roll._rallyIndex && roll.data?.parent)
roll.data.parent.deleteEmbeddedDocuments('ActiveEffect', [roll._rallyIndex]);
setDiceSoNiceForDualityRoll(roll, config.roll.advantage.type); setDiceSoNiceForDualityRoll(roll, config.roll.advantage.type);
} }
} }

View file

@ -32,4 +32,26 @@ export default class DHToken extends TokenDocument {
return bars.concat(values); 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;
}
} }

View file

@ -88,7 +88,7 @@
{{/if}} {{/if}}
{{/each}} {{/each}}
</fieldset> </fieldset>
<fieldset class="modifier-container one-column"> <fieldset class="modifier-container {{#if (eq @root.rollType 'DualityRoll')}}two-columns{{else}}one-column{{/if}}">
<legend>Modifiers</legend> <legend>Modifiers</legend>
<div class="nest-inputs"> <div class="nest-inputs">
<button class="advantage-chip flex1 {{#if (eq advantage 1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="1"> <button class="advantage-chip flex1 {{#if (eq advantage 1)}}selected{{/if}}" data-action="updateIsAdvantage" data-advantage="1">
@ -107,13 +107,27 @@
{{/if}} {{/if}}
<span class="label">{{localize "DAGGERHEART.GENERAL.Disadvantage.full"}}</span> <span class="label">{{localize "DAGGERHEART.GENERAL.Disadvantage.full"}}</span>
</button> </button>
{{#unless (eq @root.rollType 'D20Roll')}} </div>
<select name="roll.dice.advantageFaces"> {{#unless (eq @root.rollType 'D20Roll')}}
<div class="nest-inputs">
<select name="roll.dice.advantageNumber"{{#unless advantage}} disabled{{/unless}}>
{{#times 10}}
<option value="{{add this 1}}" {{#if (eq @root.roll.advantageNumber (add this 1))}} selected{{/if}}>{{add this 1}}</option>
{{/times}}
</select>
<select name="roll.dice.advantageFaces"{{#unless advantage}} disabled{{/unless}}>
{{selectOptions diceOptions selected=@root.roll.dAdvantage.denomination}} {{selectOptions diceOptions selected=@root.roll.dAdvantage.denomination}}
</select> </select>
{{/unless}} </div>
</div> {{/unless}}
<input type="text" value="{{extraFormula}}" name="extraFormula" placeholder="Situational Bonus"> {{#if @root.rallyDie.length}}
<span class="formula-label">{{localize "DAGGERHEART.CLASS.Feature.rallyDice"}}</span>
<select name="roll.dice._rallyIndex">
{{selectOptions @root.rallyDie blank="" selected=@root.roll._rallyIndex}}
</select>
{{/if}}
{{#if (eq @root.rollType 'DualityRoll')}}<span class="formula-label">Situational Bonus</span>{{/if}}
<input type="text" value="{{extraFormula}}" name="extraFormula" placeholder="{{#if (eq @root.rollType 'DualityRoll')}}Ex: 1d6 + 5{{else}}Situational Bonus{{/if}}">
</fieldset> </fieldset>
{{/unless}} {{/unless}}
<span class="formula-label"><b>Formula:</b> {{@root.formula}}</span> <span class="formula-label"><b>Formula:</b> {{@root.formula}}</span>

View file

@ -16,6 +16,11 @@
{{localize "DAGGERHEART.GENERAL.Disadvantage.full"}} {{localize "DAGGERHEART.GENERAL.Disadvantage.full"}}
</div> </div>
{{/if}} {{/if}}
{{#if roll.rally.dice}}
<div class="duality-modifier">
{{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} {{roll.rally.dice}}
</div>
{{/if}}
</div> </div>
<div class="dice-result"> <div class="dice-result">
<div class="dice-formula">{{roll.formula}}</div> <div class="dice-formula">{{roll.formula}}</div>
@ -38,7 +43,7 @@
<div class="dice-title">{{localize "DAGGERHEART.GENERAL.hope"}}</div> <div class="dice-title">{{localize "DAGGERHEART.GENERAL.hope"}}</div>
<div class="dice-inner-container hope" title="{{localize "DAGGERHEART.GENERAL.hope"}}"> <div class="dice-inner-container hope" title="{{localize "DAGGERHEART.GENERAL.hope"}}">
<div class="dice-wrapper"> <div class="dice-wrapper">
<img class="dice" src="../icons/svg/d12-grey.svg"/> <img class="dice" src="../icons/svg/{{roll.hope.dice}}-grey.svg"/>
</div> </div>
<div class="dice-value">{{roll.hope.value}}</div> <div class="dice-value">{{roll.hope.value}}</div>
</div> </div>
@ -49,7 +54,7 @@
<div class="dice-title">{{localize "DAGGERHEART.GENERAL.fear"}}</div> <div class="dice-title">{{localize "DAGGERHEART.GENERAL.fear"}}</div>
<div class="dice-inner-container fear" title="{{localize "DAGGERHEART.GENERAL.fear"}}"> <div class="dice-inner-container fear" title="{{localize "DAGGERHEART.GENERAL.fear"}}">
<div class="dice-wrapper"> <div class="dice-wrapper">
<img class="dice" src="../icons/svg/d12-grey.svg"/> <img class="dice" src="../icons/svg/{{roll.fear.dice}}-grey.svg"/>
</div> </div>
<div class="dice-value">{{roll.fear.value}}</div> <div class="dice-value">{{roll.fear.value}}</div>
</div> </div>
@ -72,7 +77,7 @@
<div class="dice-container"> <div class="dice-container">
<div class="dice-inner-container {{#if (eq roll.advantage.type 1)}}advantage{{else}}disadvantage{{/if}}"> <div class="dice-inner-container {{#if (eq roll.advantage.type 1)}}advantage{{else}}disadvantage{{/if}}">
<div class="dice-wrapper"> <div class="dice-wrapper">
<img class="dice" src="../icons/svg/d6-grey.svg"/> <img class="dice" src="../icons/svg/{{roll.advantage.dice}}-grey.svg"/>
</div> </div>
<div class="dice-value">{{roll.advantage.value}}</div> <div class="dice-value">{{roll.advantage.value}}</div>
</div> </div>
@ -82,6 +87,30 @@
</div> </div>
</div> </div>
{{/if}} {{/if}}
{{#if roll.rally.dice}}
<div class="dice">
<header class="part-header flexrow">
<span class="part-formula">
<span>1{{roll.rally.dice}}</span>
</span>
<span class="part-total">{{roll.rally.value}}</span>
</header>
<div class="flexrow">
<ol class="dice-rolls">
<li class="roll die {{roll.rally.dice}}">
<div class="dice-container">
<div class="dice-inner-container">
<div class="dice-wrapper">
<img class="dice" src="../icons/svg/{{roll.rally.dice}}-grey.svg"/>
</div>
<div class="dice-value">{{roll.rally.value}}</div>
</div>
</div>
</li>
</ol>
</div>
</div>
{{/if}}
{{#each roll.extra as | extra | }} {{#each roll.extra as | extra | }}
<div class="dice"> <div class="dice">
<header class="part-header flexrow"> <header class="part-header flexrow">