This commit is contained in:
WBHarry 2026-01-07 11:31:58 +01:00
parent 9564edb244
commit 80595f4e79
12 changed files with 182 additions and 25 deletions

View file

@ -10,6 +10,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.config = config; this.config = config;
this.config.experiences = []; this.config.experiences = [];
this.reactionOverride = config.actionType === 'reaction'; this.reactionOverride = config.actionType === 'reaction';
this.selectedEffects = this.config.bonusEffects;
if (config.source?.action) { if (config.source?.action) {
this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent; this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent;
@ -35,6 +36,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
selectExperience: this.selectExperience, selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction, toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll, toggleTagTeamRoll: this.toggleTagTeamRoll,
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
form: { form: {
@ -76,6 +78,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
icon icon
})); }));
context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
this.config.costs ??= []; this.config.costs ??= [];
if (this.config.costs?.length) { if (this.config.costs?.length) {
const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call( const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(
@ -208,6 +213,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
this.render(); this.render();
} }
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();
}
static async submitRoll() { static async submitRoll() {
await this.close({ submitted: true }); await this.close({ submitted: true });
} }

View file

@ -6,6 +6,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.roll = roll; this.roll = roll;
this.config = config; this.config = config;
this.selectedEffects = this.config.bonusEffects;
} }
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
@ -20,6 +21,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon: 'fa-solid fa-dice' icon: 'fa-solid fa-dice'
}, },
actions: { actions: {
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
form: { form: {
@ -57,6 +59,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
icon icon
})); }));
context.modifiers = this.config.modifiers; context.modifiers = this.config.modifiers;
context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length);
context.selectedEffects = this.selectedEffects;
return context; return context;
} }
@ -69,6 +74,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
this.render(); this.render();
} }
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();
}
static async submitRoll() { static async submitRoll() {
await this.close({ submitted: true }); await this.close({ submitted: true });
} }

View file

@ -197,6 +197,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
let config = this.prepareConfig(event); let config = this.prepareConfig(event);
if (!config) return; if (!config) return;
await this.addEffects(config);
if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return;
// Display configuration window if necessary // Display configuration window if necessary
@ -263,6 +265,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
return config; return config;
} }
/** */
async addEffects(config) {
let effects = [];
if (this.actor) {
effects = Array.from(await this.actor.allApplicableEffects());
}
config.effects = effects;
}
/** /**
* Method used to know if a configuration dialog must be shown or not when there is no roll. * Method used to know if a configuration dialog must be shown or not when there is no roll.
* @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. * @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods.

View file

@ -136,6 +136,13 @@ export default class D20Roll extends DHRoll {
return modifiers; return modifiers;
} }
bonusEffectBuilder(config) {
const changeKeys = [`roll.${this.options.actionType}`, `roll.${this.options.roll.type}`];
config.bonusEffects = foundry.utils.deepClone(
config.effects.filter(x => x.changes.some(x => changeKeys.includes(x.key)))
);
}
static postEvaluate(roll, config = {}) { static postEvaluate(roll, config = {}) {
const data = super.postEvaluate(roll, config); const data = super.postEvaluate(roll, config);
data.type = config.actionType; data.type = config.actionType;

View file

@ -93,7 +93,6 @@ export default class DamageRoll extends DHRoll {
type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'), type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'),
options = part ?? this.options; options = part ?? this.options;
modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`));
if (!this.options.hasHealing) { if (!this.options.hasHealing) {
options.damageTypes?.forEach(t => { options.damageTypes?.forEach(t => {
modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`)); modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`));
@ -108,6 +107,25 @@ export default class DamageRoll extends DHRoll {
return modifiers; return modifiers;
} }
bonusEffectBuilder() {
const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage');
return this.options.effects.reduce((acc, effect) => {
if (effect.changes.some(x => x.key.includes(`system.bonuses.${type}`))) {
acc[effect.id] = {
id: effect.id,
name: effect.name,
description: effect.description,
changes: effect.changes,
origEffect: effect,
selected: !effect.disabled
};
}
return acc;
}, {});
}
constructFormula(config) { constructFormula(config) {
this.options.roll.forEach((part, index) => { this.options.roll.forEach((part, index) => {
part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data));

View file

@ -1,9 +1,11 @@
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
import { BonusFields } from '../data/actor/character.mjs';
export default class DHRoll extends Roll { export default class DHRoll extends Roll {
baseTerms = []; baseTerms = [];
constructor(formula, data = {}, options = {}) { constructor(formula, data = {}, options = {}) {
super(formula, data, options); super(formula, data, options);
options.bonusEffects = this.bonusEffectBuilder();
if (!this.data || !Object.keys(this.data).length) this.data = options.data; if (!this.data || !Object.keys(this.data).length) this.data = options.data;
} }
@ -164,12 +166,18 @@ export default class DHRoll extends Roll {
new foundry.dice.terms.OperatorTerm({ operator: '+' }), new foundry.dice.terms.OperatorTerm({ operator: '+' }),
...this.constructor.parse(modifier.join(' + '), this.options.data) ...this.constructor.parse(modifier.join(' + '), this.options.data)
]; ];
} else { } else if (Number.isNumeric(modifier)) {
const numTerm = modifier < 0 ? '-' : '+'; const numTerm = modifier < 0 ? '-' : '+';
return [ return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }), new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) }) new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
]; ];
} else {
const numTerm = modifier < 0 ? '-' : '+';
return [
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
...this.constructor.parse(modifier, this.options.data)
];
} }
} }
@ -185,18 +193,22 @@ export default class DHRoll extends Roll {
} }
getBonus(path, label) { getBonus(path, label) {
const bonus = foundry.utils.getProperty(this.data.bonuses, path), const modifiers = [];
modifiers = []; for (const effect of Object.values(this.options.bonusEffects)) {
if (bonus?.bonus) if (effect.selected) {
modifiers.push({ for (const change of effect.changes) {
label: label, if (change.key.includes(path)) {
value: bonus?.bonus const changeValue = game.system.api.documents.DhActiveEffect.getChangeValue(
}); this.data,
if (bonus?.dice?.length) change,
modifiers.push({ effect.origEffect
label: label, );
value: bonus?.dice modifiers.push({ label: label, value: changeValue });
}); }
}
}
}
return modifiers; return modifiers;
} }
@ -235,4 +247,8 @@ export default class DHRoll extends Roll {
static temporaryModifierBuilder(config) { static temporaryModifierBuilder(config) {
return {}; return {};
} }
bonusEffectBuilder() {
return [];
}
} }

View file

@ -173,6 +173,23 @@ export default class DualityRoll extends D20Roll {
return modifiers; return modifiers;
} }
bonusEffectBuilder() {
return this.options.effects.reduce((acc, effect) => {
if (effect.changes.some(x => x.key.includes(`system.bonuses.roll`))) {
acc[effect.id] = {
id: effect.id,
name: effect.name,
description: effect.description,
changes: effect.changes,
origEffect: effect,
selected: !effect.disabled
};
}
return acc;
}, {});
}
static async buildEvaluate(roll, config = {}, message = {}) { static async buildEvaluate(roll, config = {}, message = {}) {
await super.buildEvaluate(roll, config, message); await super.buildEvaluate(roll, config, message);

View file

@ -106,23 +106,29 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/**@inheritdoc*/ /**@inheritdoc*/
static applyField(model, change, field) { static applyField(model, change, field) {
const isOriginTarget = change.value.toLowerCase().includes('origin.@'); change.value = DhActiveEffect.getChangeValue(model, change, change.effect);
super.applyField(model, change, field);
}
/** */
static getChangeValue(model, change, effect) {
let value = change.value;
const isOriginTarget = value.toLowerCase().includes('origin.@');
let parseModel = model; let parseModel = model;
if (isOriginTarget && change.effect.origin) { if (isOriginTarget && effect.origin) {
change.value = change.value.replaceAll(/origin\.@/gi, '@'); value = change.value.replaceAll(/origin\.@/gi, '@');
try { try {
const effect = foundry.utils.fromUuidSync(change.effect.origin); const originEffect = foundry.utils.fromUuidSync(effect.origin);
const doc = const doc =
effect.parent?.parent instanceof game.system.api.documents.DhpActor originEffect.parent?.parent instanceof game.system.api.documents.DhpActor
? effect.parent ? originEffect.parent
: effect.parent.parent; : originEffect.parent.parent;
if (doc) parseModel = doc; if (doc) parseModel = doc;
} catch (_) {} } catch (_) {}
} }
const evalValue = this.effectSafeEval(itemAbleRollParse(change.value, parseModel, change.effect.parent)); const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent));
change.value = evalValue ?? change.value; return evalValue ?? value;
super.applyField(model, change, field);
} }
/** /**

View file

@ -157,7 +157,10 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
event.stopPropagation(); event.stopPropagation();
const config = foundry.utils.deepClone(this.system); const config = foundry.utils.deepClone(this.system);
config.event = event; config.event = event;
await this.system.action?.workflow.get('damage')?.execute(config, this._id, true); if (this.system.action) {
await this.system.action.addEffects(config);
await this.system.action.workflow.get('damage')?.execute(config, this._id, true);
}
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, { await game.socket.emit(`system.${CONFIG.DH.id}`, {

View file

@ -67,6 +67,35 @@
} }
} }
.dialog-selection-container {
display: flex;
gap: 10px;
flex-wrap: wrap;
.selection-chip {
display: flex;
align-items: center;
border-radius: 5px;
width: fit-content;
gap: 5px;
cursor: pointer;
padding: 5px;
background: light-dark(@dark-blue-10, @golden-10);
color: light-dark(@dark-blue, @golden);
.label {
font-style: normal;
font-weight: 400;
font-size: var(--font-size-14);
line-height: 17px;
}
&.selected {
background: light-dark(@dark-blue-40, @golden-40);
}
}
}
.standard-form { .standard-form {
font-family: @font-body; font-family: @font-body;
} }

View file

@ -2,6 +2,20 @@
<header class="dialog-header"> <header class="dialog-header">
<h1>{{title}}</h1> <h1>{{title}}</h1>
</header> </header>
{{#if hasSelectedEffects}}
<fieldset class="dialog-selection-container">
<legend>{{localize "DAGGERHEART.GENERAL.Effect.plural"}}</legend>
{{#each selectedEffects as |effect id|}}
<div class="selection-chip {{#if effect.selected}}selected{{/if}}" data-action="toggleSelectedEffect" data-key="{{id}}" data-tooltip="{{this.description}}">
<span><i class="{{ifThen effect.selected "fa-solid" "fa-regular"}} fa-circle"></i></span>
<span class="label">{{effect.name}}</span>
</div>
{{/each}}
</fieldset>
{{/if}}
{{#each @root.formula}} {{#each @root.formula}}
<div class="damage-formula"> <div class="damage-formula">
<span class="damage-resource"><b>{{localize "DAGGERHEART.GENERAL.formula"}}:</b> {{roll.formula}}</span> <span class="damage-resource"><b>{{localize "DAGGERHEART.GENERAL.formula"}}:</b> {{roll.formula}}</span>

View file

@ -70,6 +70,21 @@
{{/if}} {{/if}}
</div> </div>
{{#if hasSelectedEffects}}
<fieldset class="experience-container">
<legend>{{localize "DAGGERHEART.GENERAL.Effect.plural"}}</legend>
<div>
{{#each selectedEffects as |effect id|}}
<div class="experience-chip {{#if effect.selected}}selected{{/if}}" data-action="toggleSelectedEffect" data-key="{{id}}" data-tooltip="{{this.description}}">
<span><i class="{{ifThen effect.selected "fa-solid" "fa-regular"}} fa-circle"></i></span>
<span class="label">{{effect.name}}</span>
</div>
{{/each}}
</div>
</fieldset>
{{/if}}
{{#if experiences.length}} {{#if experiences.length}}
<fieldset class="experience-container"> <fieldset class="experience-container">
<legend>{{localize "DAGGERHEART.GENERAL.experience.plural"}}</legend> <legend>{{localize "DAGGERHEART.GENERAL.experience.plural"}}</legend>