diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 34ca02cd..d8306923 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -10,6 +10,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config = config; this.config.experiences = []; this.reactionOverride = config.actionType === 'reaction'; + this.selectedEffects = this.config.bonusEffects; if (config.source?.action) { 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, toggleReaction: this.toggleReaction, toggleTagTeamRoll: this.toggleTagTeamRoll, + toggleSelectedEffect: this.toggleSelectedEffect, submitRoll: this.submitRoll }, form: { @@ -76,6 +78,9 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio icon })); + context.hasSelectedEffects = Boolean(this.selectedEffects && Object.keys(this.selectedEffects).length); + context.selectedEffects = this.selectedEffects; + this.config.costs ??= []; if (this.config.costs?.length) { const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call( @@ -208,6 +213,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.render(); } + static toggleSelectedEffect(_event, button) { + this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; + this.render(); + } + static async submitRoll() { await this.close({ submitted: true }); } diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs index fbc584e4..b24570cc 100644 --- a/module/applications/dialogs/damageDialog.mjs +++ b/module/applications/dialogs/damageDialog.mjs @@ -6,6 +6,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application this.roll = roll; this.config = config; + this.selectedEffects = this.config.bonusEffects; } static DEFAULT_OPTIONS = { @@ -20,6 +21,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application icon: 'fa-solid fa-dice' }, actions: { + toggleSelectedEffect: this.toggleSelectedEffect, submitRoll: this.submitRoll }, form: { @@ -57,6 +59,9 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application icon })); context.modifiers = this.config.modifiers; + context.hasSelectedEffects = Boolean(Object.keys(this.selectedEffects).length); + context.selectedEffects = this.selectedEffects; + return context; } @@ -69,6 +74,11 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application this.render(); } + static toggleSelectedEffect(_event, button) { + this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; + this.render(); + } + static async submitRoll() { await this.close({ submitted: true }); } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 594269be..0c46f0a1 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -338,15 +338,20 @@ export default class CharacterSheet extends DHBaseActorSheet { } const type = 'effect'; const cls = game.system.api.models.actions.actionsTypes[type]; - const action = new cls({ - ...cls.getSourceConfig(doc.system), - type: type, - chatDisplay: false, - cost: [{ - key: 'stress', - value: doc.system.recallCost - }] - }, { parent: doc.system }); + const action = new cls( + { + ...cls.getSourceConfig(doc.system), + type: type, + chatDisplay: false, + cost: [ + { + key: 'stress', + value: doc.system.recallCost + } + ] + }, + { parent: doc.system } + ); const config = await action.use(event); if (config) { return doc.update({ 'system.inVault': false }); @@ -707,8 +712,10 @@ export default class CharacterSheet extends DHBaseActorSheet { headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: abilityLabel }), + effects: Array.from(await this.document.allApplicableEffects()), roll: { - trait: button.dataset.attribute + trait: button.dataset.attribute, + type: 'trait' }, hasRoll: true, actionType: 'action', diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 18a09904..a935468a 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -197,6 +197,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel let config = this.prepareConfig(event); if (!config) return; + await this.addEffects(config); + if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; // Display configuration window if necessary @@ -263,6 +265,16 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel 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. * @param {*} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 0256f281..f679d725 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -127,15 +127,55 @@ export default class D20Roll extends DHRoll { const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? []; modifiers.push( - ...this.getBonus(`roll.${this.options.actionType}`, `${this.options.actionType?.capitalize()} Bonus`) - ); - modifiers.push( - ...this.getBonus(`roll.${this.options.roll.type}`, `${this.options.roll.type?.capitalize()} Bonus`) + ...this.getBonus( + `system.bonuses.roll.${this.options.actionType}`, + `${this.options.actionType?.capitalize()} Bonus` + ) ); + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) { + modifiers.push( + ...this.getBonus( + `system.bonuses.roll.${this.options.roll.type}`, + `${this.options.roll.type?.capitalize()} Bonus` + ) + ); + } + + if ( + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id || + (this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage) + ) { + modifiers.push( + ...this.getBonus(`system.bonuses.roll.attack`, `${this.options.roll.type?.capitalize()} Bonus`) + ); + } + return modifiers; } + getActionChangeKeys() { + const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]); + + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) { + changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`); + } + + if ( + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id || + (this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage) + ) { + changeKeys.add(`system.bonuses.roll.attack`); + } + + if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) { + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id) + changeKeys.add('system.bonuses.roll.trait'); + } + + return changeKeys; + } + static postEvaluate(roll, config = {}) { const data = super.postEvaluate(roll, config); data.type = config.actionType; diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index c10ee6ff..f1da7654 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -93,7 +93,6 @@ export default class DamageRoll extends DHRoll { type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'), options = part ?? this.options; - modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`)); if (!this.options.hasHealing) { options.damageTypes?.forEach(t => { modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`)); @@ -108,6 +107,29 @@ export default class DamageRoll extends DHRoll { return modifiers; } + getActionChangeKeys() { + const type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'); + const changeKeys = []; + + for (const roll of this.options.roll) { + for (const damageType of roll.damageTypes) changeKeys.push(`system.bonuses.${type}.${damageType}`); + } + + const item = this.data.parent.items?.get(this.options.source.item); + if (item) { + switch (item.type) { + case 'weapon': + if (!this.options.hasHealing) + ['primaryWeapon', 'secondaryWeapon'].forEach(w => + changeKeys.push(`system.bonuses.damage.${w}`) + ); + break; + } + } + + return changeKeys; + } + constructFormula(config) { this.options.roll.forEach((part, index) => { part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); @@ -143,7 +165,7 @@ export default class DamageRoll extends DHRoll { } if (config.isCritical && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { - const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0); + let total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0); if (total > 0) { part.roll.terms.push(...this.formatModifier(total)); } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index ea24f238..1485085e 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -4,6 +4,7 @@ export default class DHRoll extends Roll { baseTerms = []; constructor(formula, data = {}, options = {}) { super(formula, data, options); + options.bonusEffects = this.bonusEffectBuilder(); if (!this.data || !Object.keys(this.data).length) this.data = options.data; } @@ -164,12 +165,18 @@ export default class DHRoll extends Roll { new foundry.dice.terms.OperatorTerm({ operator: '+' }), ...this.constructor.parse(modifier.join(' + '), this.options.data) ]; - } else { + } else if (Number.isNumeric(modifier)) { const numTerm = modifier < 0 ? '-' : '+'; return [ new foundry.dice.terms.OperatorTerm({ operator: numTerm }), 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 +192,22 @@ export default class DHRoll extends Roll { } getBonus(path, label) { - const bonus = foundry.utils.getProperty(this.data.bonuses, path), - modifiers = []; - if (bonus?.bonus) - modifiers.push({ - label: label, - value: bonus?.bonus - }); - if (bonus?.dice?.length) - modifiers.push({ - label: label, - value: bonus?.dice - }); + const modifiers = []; + for (const effect of Object.values(this.options.bonusEffects)) { + if (effect.selected) { + for (const change of effect.changes) { + if (change.key.includes(path)) { + const changeValue = game.system.api.documents.DhActiveEffect.getChangeValue( + this.data, + change, + effect.origEffect + ); + modifiers.push({ label: label, value: changeValue }); + } + } + } + } + return modifiers; } @@ -235,4 +246,28 @@ export default class DHRoll extends Roll { static temporaryModifierBuilder(config) { return {}; } + + bonusEffectBuilder() { + const changeKeys = this.getActionChangeKeys(); + return ( + this.options.effects?.reduce((acc, effect) => { + if (effect.changes.some(x => changeKeys.some(key => x.key.includes(key)))) { + acc[effect.id] = { + id: effect.id, + name: effect.name, + description: effect.description, + changes: effect.changes, + origEffect: effect, + selected: !effect.disabled + }; + } + + return acc; + }, {}) ?? [] + ); + } + + getActionChangeKeys() { + return []; + } } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index d2e20213..9490292a 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -173,6 +173,34 @@ export default class DualityRoll extends D20Roll { return modifiers; } + getActionChangeKeys() { + const changeKeys = new Set([`system.bonuses.roll.${this.options.actionType}`]); + + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.attack.id) { + changeKeys.add(`system.bonuses.roll.${this.options.roll.type}`); + } + + if ( + this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.attack.id || + (this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id && this.options.hasDamage) + ) { + changeKeys.add(`system.bonuses.roll.attack`); + } + + if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) { + if (this.options.roll.type !== CONFIG.DH.GENERAL.rollTypes.spellcast.id) + changeKeys.add('system.bonuses.roll.trait'); + } + + const weapons = ['primaryWeapon', 'secondaryWeapon']; + weapons.forEach(w => { + if (this.options.source.item && this.options.source.item === this.data[w]?.id) + changeKeys.add(`system.bonuses.roll.${w}`); + }); + + return changeKeys; + } + static async buildEvaluate(roll, config = {}, message = {}) { await super.buildEvaluate(roll, config, message); diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 2297ea27..c5abb77c 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -106,23 +106,29 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { /**@inheritdoc*/ 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; - if (isOriginTarget && change.effect.origin) { - change.value = change.value.replaceAll(/origin\.@/gi, '@'); + if (isOriginTarget && effect.origin) { + value = change.value.replaceAll(/origin\.@/gi, '@'); try { - const effect = foundry.utils.fromUuidSync(change.effect.origin); + const originEffect = foundry.utils.fromUuidSync(effect.origin); const doc = - effect.parent?.parent instanceof game.system.api.documents.DhpActor - ? effect.parent - : effect.parent.parent; + originEffect.parent?.parent instanceof game.system.api.documents.DhpActor + ? originEffect.parent + : originEffect.parent.parent; if (doc) parseModel = doc; } catch (_) {} } - const evalValue = this.effectSafeEval(itemAbleRollParse(change.value, parseModel, change.effect.parent)); - change.value = evalValue ?? change.value; - super.applyField(model, change, field); + const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent)); + return evalValue ?? value; } /** diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 7e313891..d85bcb45 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -157,7 +157,10 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { event.stopPropagation(); const config = foundry.utils.deepClone(this.system); 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 }); await game.socket.emit(`system.${CONFIG.DH.id}`, { diff --git a/styles/less/global/dialog.less b/styles/less/global/dialog.less index 8c532c2b..a3400700 100644 --- a/styles/less/global/dialog.less +++ b/styles/less/global/dialog.less @@ -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 { font-family: @font-body; } diff --git a/templates/dialogs/dice-roll/damageSelection.hbs b/templates/dialogs/dice-roll/damageSelection.hbs index ba542666..c0dbae62 100644 --- a/templates/dialogs/dice-roll/damageSelection.hbs +++ b/templates/dialogs/dice-roll/damageSelection.hbs @@ -2,6 +2,20 @@

{{title}}

+ + {{#if hasSelectedEffects}} +
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} + + {{#each selectedEffects as |effect id|}} +
+ + {{effect.name}} +
+ {{/each}} +
+ {{/if}} + {{#each @root.formula}}
{{localize "DAGGERHEART.GENERAL.formula"}}: {{roll.formula}} diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index c7a9b0f9..e60f4683 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -70,6 +70,19 @@ {{/if}}
+ {{#if hasSelectedEffects}} +
+ {{localize "DAGGERHEART.GENERAL.Effect.plural"}} + + {{#each selectedEffects as |effect id|}} +
+ + {{effect.name}} +
+ {{/each}} +
+ {{/if}} + {{#if experiences.length}}
{{localize "DAGGERHEART.GENERAL.experience.plural"}}