diff --git a/lang/en.json b/lang/en.json index 36b68ad7..d3d707d6 100755 --- a/lang/en.json +++ b/lang/en.json @@ -96,6 +96,7 @@ "attackName": "Attack Name", "includeBase": { "label": "Include Item Damage" }, "multiplier": "Multiplier", + "saveHint": "Set a default Trait to enable Reaction Roll. It can be changed later in Reaction Roll Dialog.", "resultBased": { "label": "Formula based on Hope/Fear result." }, @@ -310,7 +311,8 @@ "toLoadout": "Send to Loadout", "toVault": "Send to Vault", "unequip": "Unequip", - "useItem": "Use Item" + "useItem": "Use Item", + "cancelBeastform": "Cancel Beastform" }, "Countdown": { "addCountdown": "Add Countdown", @@ -1908,6 +1910,7 @@ "tier4": "tier 4", "domains": "Domains", "downtime": "Downtime", + "roll": "Roll", "rules": "Rules", "types": "Types" }, @@ -1961,6 +1964,7 @@ "fear": "Fear", "features": "Features", "formula": "Formula", + "gm": "GM", "healing": "Healing", "healingRoll": "Healing Roll", "hit": { @@ -1997,6 +2001,10 @@ "none": "None", "noTarget": "No current target", "partner": "Partner", + "player": { + "single": "Player", + "plurial": "Players" + }, "proficiency": "Proficiency", "quantity": "Quantity", "range": "Range", @@ -2022,6 +2030,7 @@ }, "title": "Title", "total": "Total", + "traitModifier": "Trait Modifier", "true": "True", "type": "Type", "unarmed": "Unarmed", @@ -2204,10 +2213,35 @@ "playerCanEditSheet": { "label": "Players Can Manually Edit Character Settings", "hint": "Players are allowed to access the manual Character Settings and change their statistics beyond the rules." + }, + "roll": { + "roll": { + "label": "Roll", + "hint": "Auto behavior for rolls like Attack, Spellcast, etc." + }, + "damage": { + "label": "Damage/Healing Roll", + "hint": "Auto behavior for Damage & Healing rolls after the Attack/Spellcast." + }, + "save": { + "label": "Reaction Roll", + "hint": "Auto behavior if a Reaction Roll is needed. Targets must be selected before the action is made" + }, + "damageApply": { + "label": "Apply Damage/Healing", + "hint": "Automatically apply damages & healings. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions." + }, + "effect": { + "label": "Apply Effects", + "hint": "Automatically apply effects. Targets must be selected before the action is made and Reaction Roll Automation must be different than Never. Bypass users permissions." + } } }, "defeated": { "title": "Defeated Handling" + }, + "roll": { + "title": "Actions" } }, "Homebrew": { @@ -2407,6 +2441,7 @@ "beastformInapplicable": "A beastform can only be applied to a Character.", "beastformAlreadyApplied": "The character already has a beastform applied!", "noTargetsSelected": "No targets are selected.", + "noTargetsSelectedOrPerm": "No targets are selected or with the update permission.", "attackTargetDoesNotExist": "The target token no longer exists", "insufficentAdvancements": "You don't have enough advancements left.", "noAssignedPlayerCharacter": "You have no assigned character.", diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index d445f24c..ed8fd6a1 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -1,3 +1,5 @@ +import { abilities } from "../../config/actorConfig.mjs"; + const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class D20RollDialog extends HandlebarsApplicationMixin(ApplicationV2) { @@ -7,7 +9,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.roll = roll; this.config = config; this.config.experiences = []; - this.reactionOverride = config.roll?.type === 'reaction'; + this.reactionOverride = config.actionType === 'reaction'; if (config.source?.action) { this.item = config.data.parent.items.get(config.source.item) ?? config.data.parent; @@ -20,7 +22,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio static DEFAULT_OPTIONS = { tag: 'form', - id: 'roll-selection', + // id: 'roll-selection', classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'roll-selection'], position: { width: 'auto' @@ -42,7 +44,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio }; get title() { - return this.config.title; + return `${this.config.title}${this.actor ? `: ${this.actor.name}` : ''}`; } get actor() { @@ -113,15 +115,21 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.isLite = this.config.roll?.lite; context.extraFormula = this.config.extraFormula; context.formula = this.roll.constructFormula(this.config); + if(this.actor.system.traits) context.abilities = this.getTraitModifiers(); - context.showReaction = !context.rollConfig.type && context.rollType === 'DualityRoll'; + context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll'; context.reactionOverride = this.reactionOverride; } return context; } + getTraitModifiers() { + return Object.values(abilities).map(a => ({ id: a.id, label: `${game.i18n.localize(a.label)} (${this.actor.system.traits[a.id]?.value.signedString() ?? 0})` })) + } + static updateRollConfiguration(event, _, formData) { const { ...rest } = foundry.utils.expandObject(formData.object); + this.config.selectedRollMode = rest.selectedRollMode; if (this.config.costs) { @@ -133,6 +141,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.roll[key] = value; }); } + if(rest.hasOwnProperty("trait")) { + this.config.roll.trait = rest.trait; + this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: game.i18n.localize(abilities[this.config.roll.trait]?.label) + }); + } this.config.extraFormula = rest.extraFormula; this.render(); } @@ -151,31 +165,29 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config.experiences.indexOf(button.dataset.key) > -1 ? this.config.experiences.filter(x => x !== button.dataset.key) : [...this.config.experiences, button.dataset.key]; - if (this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') { - this.config.costs = - this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1 - ? this.config.costs.filter(x => x.extKey !== button.dataset.key) - : [ - ...this.config.costs, - { - extKey: button.dataset.key, - key: 'hope', - value: 1, - name: this.config.data?.experiences?.[button.dataset.key]?.name - } - ]; - } + this.config.costs = + this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1 + ? this.config.costs.filter(x => x.extKey !== button.dataset.key) + : [ + ...this.config.costs, + { + extKey: button.dataset.key, + key: this.config?.data?.parent?.isNPC ? 'fear' : 'hope', + value: 1, + name: this.config.data?.experiences?.[button.dataset.key]?.name + } + ]; this.render(); } static toggleReaction() { if (this.config.roll) { this.reactionOverride = !this.reactionOverride; - this.config.roll.type = this.reactionOverride + this.config.actionType = this.reactionOverride ? CONFIG.DH.ITEM.actionTypes.reaction.id - : this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id + : this.config.actionType === CONFIG.DH.ITEM.actionTypes.reaction.id ? null - : this.config.roll.type; + : this.config.actionType; this.render(); } } diff --git a/module/applications/settings/automationSettings.mjs b/module/applications/settings/automationSettings.mjs index 0157e016..4407897d 100644 --- a/module/applications/settings/automationSettings.mjs +++ b/module/applications/settings/automationSettings.mjs @@ -35,13 +35,14 @@ export default class DhAutomationSettings extends HandlebarsApplicationMixin(App header: { template: 'systems/daggerheart/templates/settings/automation-settings/header.hbs' }, general: { template: 'systems/daggerheart/templates/settings/automation-settings/general.hbs' }, rules: { template: 'systems/daggerheart/templates/settings/automation-settings/rules.hbs' }, + roll: { template: 'systems/daggerheart/templates/settings/automation-settings/roll.hbs' }, footer: { template: 'systems/daggerheart/templates/settings/automation-settings/footer.hbs' } }; /** @inheritdoc */ static TABS = { main: { - tabs: [{ id: 'general' }, { id: 'rules' }], + tabs: [{ id: 'general' }, { id: 'rules' }, { id: 'roll' }], initial: 'general', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index f575a2f2..64f48d02 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -146,9 +146,9 @@ export default class AdversarySheet extends DHBaseActorSheet { title: `Reaction Roll: ${this.actor.name}`, headerTitle: 'Adversary Reaction Roll', roll: { - type: 'reaction' + type: 'trait' }, - type: 'trait', + actionType: 'reaction', hasRoll: true, data: this.actor.getRollData() }; diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 6c2d7c95..308faee7 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -679,31 +679,7 @@ export default class CharacterSheet extends DHBaseActorSheet { }) }); - this.consumeResource(result?.costs); - } - - // Remove when Action Refactor part #2 done - async consumeResource(costs) { - if (!costs?.length) return; - const usefulResources = { - ...foundry.utils.deepClone(this.actor.system.resources), - fear: { - value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), - max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear, - reversed: false - } - }; - const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => { - const resource = usefulResources[c.key]; - return { - key: c.key, - value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), - target: resource.target, - keyIsID: resource.keyIsID - }; - }); - - await this.actor.modifyResource(resources); + if(result) game.system.api.fields.ActionFields.CostField.execute.call(this, result); } //TODO: redo toggleEquipItem method diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index de9fe53d..7f338ac1 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -412,6 +412,16 @@ export default function DHApplicationMixin(Base) { ]; if (usable) { + options.unshift({ + name: 'DAGGERHEART.APPLICATIONS.ContextMenu.cancelBeastform', + icon: 'fa-solid fa-ban', + condition: target => { + const doc = getDocFromElementSync(target); + return doc && doc.system?.actions?.some(a => a.type === "beastform"); + }, + callback: async target => game.system.api.fields.ActionFields.BeastformField.handleActiveTransformations.call(await getDocFromElement(target)) + }); + options.unshift({ name: 'DAGGERHEART.GENERAL.damage', icon: 'fa-solid fa-explosion', @@ -422,7 +432,9 @@ export default function DHApplicationMixin(Base) { callback: async (target, event) => { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; - return action && action.use(event, { byPassRoll: true }); + const config = action.prepareConfig(event); + config.hasRoll = false; + return action && action.workflow.get("damage").execute(config, null, true); } }); diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index a80974ed..21762818 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -1,5 +1,3 @@ -import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; - export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { constructor(options) { super(options); @@ -55,21 +53,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } addChatListeners = async (app, html, data) => { - html.querySelectorAll('.duality-action-damage').forEach(element => - element.addEventListener('click', event => this.onRollDamage(event, data.message)) - ); - html.querySelectorAll('.target-save').forEach(element => - element.addEventListener('click', event => this.onRollSave(event, data.message)) - ); - html.querySelectorAll('.roll-all-save-button').forEach(element => - element.addEventListener('click', event => this.onRollAllSave(event, data.message)) - ); html.querySelectorAll('.simple-roll-button').forEach(element => element.addEventListener('click', event => this.onRollSimple(event, data.message)) ); - html.querySelectorAll('.healing-button').forEach(element => - element.addEventListener('click', event => this.onHealing(event, data.message)) - ); html.querySelectorAll('.ability-use-button').forEach(element => element.addEventListener('click', event => this.abilityUseButton(event, data.message)) ); @@ -90,80 +76,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo super.close(options); } - async getActor(uuid) { - return await foundry.utils.fromUuid(uuid); - } - - getAction(actor, itemId, actionId) { - const item = actor.items.get(itemId), - action = - actor.system.attack?._id === actionId - ? actor.system.attack - : item.system.attack?._id === actionId - ? item.system.attack - : item?.system?.actions?.get(actionId); - return action; - } - - async onRollDamage(event, message) { - event.stopPropagation(); - const actor = await this.getActor(message.system.source.actor); - if(!actor.isOwner) return true; - if (message.system.source.item && message.system.source.action) { - const action = this.getAction(actor, message.system.source.item, message.system.source.action); - if (!action || !action?.rollDamage) return; - await action.rollDamage(event, message); - } - } - - async onRollSave(event, message) { - event.stopPropagation(); - const actor = await this.getActor(message.system.source.actor), - tokenId = event.target.closest('[data-token]')?.dataset.token, - token = game.canvas.tokens.get(tokenId); - if (!token?.actor || !token.isOwner) return true; - if (message.system.source.item && message.system.source.action) { - const action = this.getAction(actor, message.system.source.item, message.system.source.action); - if (!action || !action?.hasSave) return; - action.rollSave(token.actor, event, message).then(result => - emitAsGM( - GMUpdateEvent.UpdateSaveMessage, - action.updateSaveMessage.bind(action, result, message, token.id), - { - action: action.uuid, - message: message._id, - token: token.id, - result - } - ) - ); - } - } - - async onRollAllSave(event, message) { - event.stopPropagation(); - if (!game.user.isGM) return; - const targets = event.target.parentElement.querySelectorAll('[data-token] .target-save'); - const actor = await this.getActor(message.system.source.actor), - action = this.getAction(actor, message.system.source.item, message.system.source.action); - targets.forEach(async el => { - const tokenId = el.closest('[data-token]')?.dataset.token, - token = game.canvas.tokens.get(tokenId); - if (!token.actor) return; - if (game.user === token.actor.owner) el.dispatchEvent(new PointerEvent('click', { shiftKey: true })); - else { - token.actor.owner - .query('reactionRoll', { - actionId: action.uuid, - actorId: token.actor.uuid, - event, - message - }) - .then(result => action.updateSaveMessage(result, message, token.id)); - } - }); - } - async onRollSimple(event, message) { const buttonType = event.target.dataset.type ?? 'damage', total = message.rolls.reduce((a, c) => a + Roll.fromJSON(c).total, 0), @@ -197,8 +109,11 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo item.system.attack?.id === event.currentTarget.id ? item.system.attack : item.system.actions.get(event.currentTarget.id); - if (event.currentTarget.dataset.directDamage) action.use(event, { byPassRoll: true }); - else action.use(event); + if (event.currentTarget.dataset.directDamage) { + const config = action.prepareConfig(event); + config.hasRoll = false; + action.workflow.get("damage").execute(config, null, true); + } else action.use(event); } async actionUseButton(event, message) { diff --git a/module/applications/ui/fearTracker.mjs b/module/applications/ui/fearTracker.mjs index ace2bbb2..a346aa66 100644 --- a/module/applications/ui/fearTracker.mjs +++ b/module/applications/ui/fearTracker.mjs @@ -78,7 +78,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV /** @override */ async _preRender(context, options) { - if (this.currentFear > this.maxFear) + if (this.currentFear > this.maxFear && game.user.isGM) await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, this.maxFear); } @@ -107,18 +107,5 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV async updateFear(value) { return emitAsGM(GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value); - /* if(!game.user.isGM) - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateFear, - update: value - } - }); - else - game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */ - /* if (!game.user.isGM) return; - value = Math.max(0, Math.min(this.maxFear, value)); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear, value); */ } } diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 76234a97..0f148aeb 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -29,3 +29,22 @@ export const gameSettings = { Countdowns: 'Countdowns', LastMigrationVersion: 'LastMigrationVersion' }; + +export const actionAutomationChoices = { + never: { + id: "never", + label: "Never" + }, + showDialog: { + id: "showDialog", + label: "Show Dialog only" + }, + // npcOnly: { + // id: "npcOnly", + // label: "Always for non-characters" + // }, + always: { + id: "always", + label: "Always" + } +} diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index ea619bcf..6e522ceb 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -1,14 +1,9 @@ import DhpActor from '../../documents/actor.mjs'; import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs'; import { ActionMixin } from '../fields/actionField.mjs'; -import { abilities } from '../../config/actorConfig.mjs'; const fields = foundry.data.fields; -/* - !!! I'm currently refactoring the whole Action thing, it's a WIP !!! -*/ - /* ToDo - Target Check / Target Picker @@ -20,6 +15,7 @@ const fields = foundry.data.fields; export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) { static extraSchemas = ['cost', 'uses', 'range']; + /** @inheritDoc */ static defineSchema() { const schemaFields = { _id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }), @@ -37,31 +33,76 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel }; this.extraSchemas.forEach(s => { - let clsField; - if ((clsField = this.getActionField(s))) schemaFields[s] = new clsField(); + let clsField = this.getActionField(s); + if (clsField) + schemaFields[s] = new clsField(); }); return schemaFields; } + /** + * Create a Map containing each Action step based on fields define in schema. Ordered by Fields order property. + * + * Each step can be called individually as long as needed config is provided. + * Ex: .workflow.get("damage").execute(config) + * @returns {Map} + */ + defineWorkflow() { + const workflow = new Map(); + this.constructor.extraSchemas.forEach(s => { + let clsField = this.constructor.getActionField(s); + if (clsField?.execute) { + workflow.set(s, { order: clsField.order, execute: clsField.execute.bind(this) } ); + if( s === "damage" ) workflow.set("applyDamage", { order: 75, execute: clsField.applyDamage.bind(this) } ); + } + }); + return new Map([...workflow.entries()].sort(([aKey, aValue], [bKey, bValue]) => aValue.order - bValue.order)); + } + + /** + * Getter returning the workflow property or creating it the first time the property is called + */ + get workflow() { + if ( this.hasOwnProperty("_workflow") ) return this._workflow; + const workflow = Object.freeze(this.defineWorkflow()); + Object.defineProperty(this, "_workflow", {value: workflow, writable: false}); + return workflow; + } + + /** + * Get the Field class from ActionFields global config + * @param {string} name Field short name, equal to Action property + * @returns Action Field + */ static getActionField(name) { const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`]; return fields.DataField.isPrototypeOf(field) && field; } + /** @inheritDoc */ prepareData() { this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name); this.img = this.img ?? this.parent?.parent?.img; } + /** + * Get Action ID + */ get id() { return this._id; } + /** + * Return Item the action is attached too. + */ get item() { return this.parent.parent; } + /** + * Return the first Actor parent found. + */ get actor() { return this.item instanceof DhpActor ? this.item @@ -74,6 +115,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return 'trait'; } + /** + * Prepare base data based on Action Type & Parent Type + * @param {object} parent + * @returns {object} + */ static getSourceConfig(parent) { const updateSource = {}; if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) { @@ -96,6 +142,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return updateSource; } + /** + * Obtain a data object used to evaluate any dice rolls associated with this particular Action + * @param {object} [data ={}] Optional data object from previous configuration/rolls + * @returns {object} + */ getRollData(data = {}) { if (!this.actor) return null; const actorData = this.actor.getRollData(false); @@ -111,19 +162,30 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return actorData; } - async use(event, options = {}) { + /** + * Execute each part of the Action workflow in order, calling a specific event before and after each part. + * @param {object} config Config object usually created from prepareConfig method + */ + async executeWorkflow(config) { + for(const [key, part] of this.workflow) { + if (Hooks.call(`${CONFIG.DH.id}.pre${key.capitalize()}Action`, this, config) === false) return; + if(await part.execute(config) === false) return; + if (Hooks.call(`${CONFIG.DH.id}.post${key.capitalize()}Action`, this, config) === false) return; + } + } + + /** + * Main method to use the Action + * @param {Event} event Event from the button used to trigger the Action + * @returns {object} + */ + async use(event) { if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); if (this.chatDisplay) await this.toChat(); - let { byPassRoll } = options, - config = this.prepareConfig(event, byPassRoll); - for (let i = 0; i < this.constructor.extraSchemas.length; i++) { - let clsField = this.constructor.getActionField(this.constructor.extraSchemas[i]); - if (clsField?.prepareConfig) { - const keep = clsField.prepareConfig.call(this, config); - if (config.isFastForward && !keep) return; - } - } + + let config = this.prepareConfig(event); + if(!config) return; if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; @@ -132,36 +194,22 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel config = await D20RollDialog.configure(null, config); if (!config) return; } - - if (config.hasRoll) { - const rollConfig = this.prepareRoll(config); - config.roll = rollConfig; - config = await this.actor.diceRoll(config); - if (!config) return; - } - - if (this.doFollowUp(config)) { - if (this.rollDamage && this.damage.parts.length) await this.rollDamage(event, config); - else if (this.trigger) await this.trigger(event, config); - else if (this.hasSave || this.hasEffect) { - const roll = new CONFIG.Dice.daggerheart.DHRoll(''); - roll._evaluated = true; - await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); - } - } - - // Consume resources - await this.consume(config); + + // Execute the Action Worflow in order based of schema fields + await this.executeWorkflow(config); if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; return config; } - /* */ - prepareConfig(event, byPass = false) { - const hasRoll = this.getUseHasRoll(byPass); - return { + /** + * Create the basic config common to every action type + * @param {Event} event Event from the button used to trigger the Action + * @returns {object} + */ + prepareBaseConfig(event) { + const config = { event, title: `${this.item instanceof CONFIG.Actor.documentClass ? '' : `${this.item.name}: `}${game.i18n.localize(this.name)}`, source: { @@ -169,238 +217,95 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel action: this._id, actor: this.actor.uuid }, - dialog: { - configure: hasRoll - }, - type: this.roll?.type ?? this.type, - hasRoll: hasRoll, - hasDamage: this.damage?.parts?.length && this.type !== 'healing', - hasHealing: this.damage?.parts?.length && this.type === 'healing', - hasEffect: !!this.effects?.length, - isDirect: !!this.damage?.direct, + dialog: {}, + actionType: this.actionType, + hasRoll: this.hasRoll, + hasDamage: this.hasDamage, + hasHealing: this.hasHealing, + hasEffect: this.hasEffect, hasSave: this.hasSave, + isDirect: !!this.damage?.direct, selectedRollMode: game.settings.get('core', 'rollMode'), - isFastForward: event.shiftKey, data: this.getRollData(), - evaluate: hasRoll + evaluate: this.hasRoll }; + DHBaseAction.applyKeybindings(config); + return config; } + /** + * Create the config for that action used for its workflow + * @param {Event} event Event from the button used to trigger the Action + * @returns {object} + */ + prepareConfig(event) { + const config = this.prepareBaseConfig(event); + for(const clsField of Object.values(this.schema.fields)) { + if (clsField?.prepareConfig) + if(clsField.prepareConfig.call(this, config) === false) return false; + } + return config; + } + + /** + * 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. + * @returns {boolean} + */ requireConfigurationDialog(config) { return !config.event.shiftKey && !config.hasRoll && (config.costs?.length || config.uses); } - prepareRoll() { - const roll = { - baseModifiers: this.roll.getModifier(), - label: 'Attack', - type: this.actionType, - difficulty: this.roll?.difficulty, - formula: this.roll.getFormula(), - advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value - }; - if (this.roll?.type === 'diceSet' || !this.hasRoll) roll.lite = true; - - return roll; - } - - doFollowUp(config) { - return !config.hasRoll; - } - + /** + * Consume Action configured resources & uses. + * That method is only used when we want those resources to be consumed outside of the use method workflow. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + * @param {boolean} successCost + */ async consume(config, successCost = false) { - const actor = this.actor.system.partner ?? this.actor, - usefulResources = { - ...foundry.utils.deepClone(actor.system.resources), - fear: { - value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), - max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear, - reversed: false - } - }; + await this.workflow.get("cost")?.execute(config, successCost); + await this.workflow.get("uses")?.execute(config, successCost); - for (var cost of config.costs) { - if (cost.keyIsID) { - usefulResources[cost.key] = { - value: cost.value, - target: this.parent.parent, - keyIsID: true - }; - } - } - - const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(config.costs) - .filter( - c => - (!successCost && (!c.consumeOnSuccess || config.roll?.success)) || - (successCost && c.consumeOnSuccess) - ) - .reduce((a, c) => { - const resource = usefulResources[c.key]; - if (resource) { - a.push({ - key: c.key, - value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), - target: resource.target, - keyIsID: resource.keyIsID - }); - return a; - } - }, []); - - await actor.modifyResource(resources); - if ( - config.uses?.enabled && - ((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) || - (successCost && config.uses?.consumeOnSuccess)) - ) - this.update({ 'uses.value': this.uses.value + 1 }); - - if (config.roll?.success || successCost) { + if (config.roll && !config.roll.success && successCost) { setTimeout(() => { (config.message ?? config.parent).update({ 'system.successConsumed': true }); }, 50); } } - /* */ - /* ROLL */ - getUseHasRoll(byPass = false) { - return this.hasRoll && !byPass; + /** + * Set if a configuration dialog must be shown or not if a special keyboard key is pressed. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + */ + static applyKeybindings(config) { + config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey); } + /** + * Getters to know which parts the action is composed of. A field can exist but configured to not be used. + * @returns {boolean} If that part is in the action. + */ + get hasRoll() { return !!this.roll?.type; } - get modifiers() { - if (!this.actor) return []; - const modifiers = []; - /** Placeholder for specific bonuses **/ - return modifiers; + get hasDamage() { + return this.damage?.parts?.length && this.type !== 'healing' } - /* ROLL */ - /* SAVE */ + get hasHealing() { + return this.damage?.parts?.length && this.type === 'healing' + } + get hasSave() { return !!this.save?.trait; } - /* SAVE */ - /* EFFECTS */ get hasEffect() { return this.effects?.length > 0; } - async applyEffects(event, data, targets) { - targets ??= data.system.targets; - const force = true; /* Where should this come from? */ - if (!this.effects?.length || !targets.length) return; - let effects = this.effects; - targets.forEach(async token => { - if (!token.hit && !force) return; - if (this.hasSave && token.saved.success === true) { - effects = this.effects.filter(e => e.onSave === true); - } - if (!effects.length) return; - effects.forEach(async e => { - const actor = canvas.tokens.get(token.id)?.actor, - effect = this.item.effects.get(e._id); - if (!actor || !effect) return; - await this.applyEffect(effect, actor); - }); - }); - } - - async applyEffect(effect, actor) { - const existingEffect = actor.effects.find(e => e.origin === effect.uuid); - if (existingEffect) { - return effect.update( - foundry.utils.mergeObject({ - ...effect.constructor.getInitialDuration(), - disabled: false - }) - ); - } - - // Otherwise, create a new effect on the target - const effectData = foundry.utils.mergeObject({ - ...effect.toObject(), - disabled: false, - transfer: false, - origin: effect.uuid - }); - await ActiveEffect.implementation.create(effectData, { parent: actor }); - } - /* EFFECTS */ - - /* SAVE */ - async rollSave(actor, event, message) { - if (!actor) return; - const title = actor.isNPC - ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') - : game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: game.i18n.localize(abilities[this.save.trait]?.label) - }); - return actor.diceRoll({ - event, - title, - roll: { - trait: this.save.trait, - difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty, - type: 'reaction' - }, - type: 'trait', - hasRoll: true, - data: actor.getRollData() - }); - } - - updateSaveMessage(result, message, targetId) { - if (!result) return; - const updateMsg = this.updateChatMessage.bind(this, message, targetId, { - result: result.roll.total, - success: result.roll.success - }); - if (game.modules.get('dice-so-nice')?.active) - game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(() => updateMsg()); - else updateMsg(); - } - - static rollSaveQuery({ actionId, actorId, event, message }) { - return new Promise(async (resolve, reject) => { - const actor = await fromUuid(actorId), - action = await fromUuid(actionId); - if (!actor || !actor?.isOwner) reject(); - action.rollSave(actor, event, message).then(result => resolve(result)); - }); - } - /* SAVE */ - - async updateChatMessage(message, targetId, changes, chain = true) { - setTimeout(async () => { - const chatMessage = ui.chat.collection.get(message._id); - - await chatMessage.update({ - flags: { - [game.system.id]: { - reactionRolls: { - [targetId]: changes - } - } - } - }); - }, 100); - if (chain) { - if (message.system.source.message) - this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false); - const relatedChatMessages = ui.chat.collection.filter(c => c.system.source?.message === message._id); - relatedChatMessages.forEach(c => { - this.updateChatMessage(c, targetId, changes, false); - }); - } - } - /** * Generates a list of localized tags for this action. * @returns {string[]} An array of localized tag strings. diff --git a/module/data/action/beastformAction.mjs b/module/data/action/beastformAction.mjs index 8c2dd31e..657cfde2 100644 --- a/module/data/action/beastformAction.mjs +++ b/module/data/action/beastformAction.mjs @@ -1,10 +1,9 @@ -import BeastformDialog from '../../applications/dialogs/beastformDialog.mjs'; import DHBaseAction from './baseAction.mjs'; export default class DhBeastformAction extends DHBaseAction { static extraSchemas = [...super.extraSchemas, 'beastform']; - async use(event, options) { + /* async use(event, options) { const beastformConfig = this.prepareBeastformConfig(); const abort = await this.handleActiveTransformations(); @@ -82,5 +81,5 @@ export default class DhBeastformAction extends DHBaseAction { beastformEffects.map(x => x.id) ); return existingEffects; - } + } */ } diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 7deeb006..b4b3e17c 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -1,65 +1,5 @@ -import { setsEqual } from '../../helpers/utils.mjs'; import DHBaseAction from './baseAction.mjs'; export default class DHDamageAction extends DHBaseAction { static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects']; - - getFormulaValue(part, data) { - let formulaValue = part.value; - - if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt; - - const isAdversary = this.actor.type === 'adversary'; - if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) { - const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde'); - if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt; - } - - return formulaValue; - } - - formatFormulas(formulas, systemData) { - const formattedFormulas = []; - formulas.forEach(formula => { - if (isNaN(formula.formula)) - formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(systemData)); - const same = formattedFormulas.find( - f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo - ); - if (same) same.formula += ` + ${formula.formula}`; - else formattedFormulas.push(formula); - }); - return formattedFormulas; - } - - async rollDamage(event, data) { - const systemData = data.system ?? data; - - let formulas = this.damage.parts.map(p => ({ - formula: this.getFormulaValue(p, systemData).getFormula(this.actor), - damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type, - applyTo: p.applyTo - })); - - if (!formulas.length) return; - - formulas = this.formatFormulas(formulas, systemData); - - delete systemData.evaluate; - const config = { - ...systemData, - roll: formulas, - dialog: {}, - data: this.getRollData() - }; - if (this.hasSave) config.onSave = this.save.damageMod; - if (data.system) { - config.source.message = data._id; - config.directDamage = false; - } else { - config.directDamage = true; - } - - return CONFIG.Dice.daggerheart.DamageRoll.build(config); - } } diff --git a/module/data/action/macroAction.mjs b/module/data/action/macroAction.mjs index 58b8eba5..b8338cd8 100644 --- a/module/data/action/macroAction.mjs +++ b/module/data/action/macroAction.mjs @@ -2,15 +2,4 @@ import DHBaseAction from './baseAction.mjs'; export default class DHMacroAction extends DHBaseAction { static extraSchemas = [...super.extraSchemas, 'macro']; - - async trigger(event, ...args) { - const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro, - macro = await fromUuid(fixUUID); - try { - if (!macro) throw new Error(`No macro found for the UUID: ${this.macro}.`); - macro.execute(); - } catch (error) { - ui.notifications.error(error); - } - } } diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index a4e2c1fd..36c6ee9d 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -1,5 +1,5 @@ import DHAbilityUse from "./abilityUse.mjs"; -import DHActorRoll from "./adversaryRoll.mjs"; +import DHActorRoll from "./actorRoll.mjs"; export const config = { abilityUse: DHAbilityUse, diff --git a/module/data/chat-message/adversaryRoll.mjs b/module/data/chat-message/actorRoll.mjs similarity index 98% rename from module/data/chat-message/adversaryRoll.mjs rename to module/data/chat-message/actorRoll.mjs index d87bab6a..b6512fbd 100644 --- a/module/data/chat-message/adversaryRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -95,7 +95,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { } registerTargetHook() { - if (!this.parent.isAuthor) return; + if (!this.parent.isAuthor || !this.hasTarget) return; if (this.targetMode && this.parent.targetHook !== null) { Hooks.off('targetToken', this.parent.targetHook); return (this.parent.targetHook = null); diff --git a/module/data/fields/action/_module.mjs b/module/data/fields/action/_module.mjs index e6caa963..7a33e147 100644 --- a/module/data/fields/action/_module.mjs +++ b/module/data/fields/action/_module.mjs @@ -6,6 +6,5 @@ export { default as EffectsField } from './effectsField.mjs'; export { default as SaveField } from './saveField.mjs'; export { default as BeastformField } from './beastformField.mjs'; export { default as DamageField } from './damageField.mjs'; -export { default as HealingField } from './healingField.mjs'; export { default as RollField } from './rollField.mjs'; export { default as MacroField } from './macroField.mjs'; diff --git a/module/data/fields/action/beastformField.mjs b/module/data/fields/action/beastformField.mjs index 832bd9f6..62c735d0 100644 --- a/module/data/fields/action/beastformField.mjs +++ b/module/data/fields/action/beastformField.mjs @@ -1,6 +1,13 @@ +import BeastformDialog from "../../../applications/dialogs/beastformDialog.mjs"; + const fields = foundry.data.fields; export default class BeastformField extends fields.SchemaField { + /** + * Action Workflow order + */ + static order = 90; + constructor(options = {}, context = {}) { const beastformFields = { tierAccess: new fields.SchemaField({ @@ -27,4 +34,96 @@ export default class BeastformField extends fields.SchemaField { }; super(beastformFields, options, context); } + + /** + * Beastform Transformation Action Workflow part. + * Must be called within Action context or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + */ + static async execute(config) { + // Should not be useful anymore here + await BeastformField.handleActiveTransformations.call(this); + + const { selected, evolved, hybrid } = await BeastformDialog.configure(config, this.item); + if (!selected) return false; + + return await BeastformField.transform.call(this, selected, evolved, hybrid); + } + + /** + * Update Action Workflow config object. + * Must be called within Action context. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + */ + prepareConfig(config) { + if(this.actor.effects.find(x => x.type === 'beastform')) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformAlreadyApplied')); + return false; + } + + const settingsTiers = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers; + const actorLevel = this.actor.system.levelData.level.current; + const actorTier = + Object.values(settingsTiers).find( + tier => actorLevel >= tier.levels.start && actorLevel <= tier.levels.end + ) ?? 1; + + config.tierLimit = this.beastform.tierAccess.exact ?? actorTier; + } + + /** + * TODO by Harry + * @param {*} selectedForm + * @param {*} evolvedData + * @param {*} hybridData + * @returns + */ + static async transform(selectedForm, evolvedData, hybridData) { + const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject(); + const beastformEffect = formData.effects.find(x => x.type === 'beastform'); + if (!beastformEffect) { + ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect'); + return false; + } + + if (evolvedData?.form) { + const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform'); + if (!evolvedForm) { + ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect'); + return false; + } + + beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes]; + formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)]; + } + + if (selectedForm.system.beastformType === CONFIG.DH.ITEM.beastformTypes.hybrid.id) { + formData.system.advantageOn = Object.values(hybridData.advantages).reduce((advantages, formCategory) => { + Object.keys(formCategory).forEach(advantageKey => { + advantages[advantageKey] = formCategory[advantageKey]; + }); + return advantages; + }, {}); + formData.system.features = [ + ...formData.system.features, + ...Object.values(hybridData.features).flatMap(x => Object.keys(x)) + ]; + } + + this.actor.createEmbeddedDocuments('Item', [formData]); + } + + /** + * Remove existing beastform effect and return true if there was one + * @returns {boolean} + */ + static async handleActiveTransformations() { + const beastformEffects = this.actor.effects.filter(x => x.type === 'beastform'); + const existingEffects = beastformEffects.length > 0; + await this.actor.deleteEmbeddedDocuments( + 'ActiveEffect', + beastformEffects.map(x => x.id) + ); + return existingEffects; + } } diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index c224fff0..ca942909 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -1,6 +1,12 @@ const fields = foundry.data.fields; export default class CostField extends fields.ArrayField { + /** + * Action Workflow order + */ + static order = 150; + + /** @inheritDoc */ constructor(options = {}, context = {}) { const element = new fields.SchemaField({ key: new fields.StringField({ @@ -20,15 +26,80 @@ export default class CostField extends fields.ArrayField { super(element, options, context); } - static prepareConfig(config) { + /** + * Cost Consumption Action Workflow part. + * Consume configured action resources. + * Must be called within Action context or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + * @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed. + */ + static async execute(config, successCost = false) { + const actor= this.actor.system.partner ?? this.actor, + usefulResources = { + ...foundry.utils.deepClone(actor.system.resources), + fear: { + value: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), + max: game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear, + reversed: false + } + }; + + if(this.parent?.parent) { + for (var cost of config.costs) { + if (cost.keyIsID) { + usefulResources[cost.key] = { + value: cost.value, + target: this.parent.parent, + keyIsID: true + }; + } + } + } + + const resources = CostField.getRealCosts(config.costs) + .filter( + c => + (!successCost && (!c.consumeOnSuccess || config.roll?.success)) || + (successCost && c.consumeOnSuccess) + ) + .reduce((a, c) => { + const resource = usefulResources[c.key]; + if (resource) { + a.push({ + key: c.key, + value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), + target: resource.target, + keyIsID: resource.keyIsID + }); + return a; + } + }, []); + + await actor.modifyResource(resources); + } + + /** + * Update Action Workflow config object. + * Must be called within Action context or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + * @returns {boolean} Return false if fast-forwarded and no more uses. + */ + prepareConfig(config) { const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : []; config.costs = CostField.calcCosts.call(this, costs); const hasCost = CostField.hasCost.call(this, config.costs); - if (config.isFastForward && !hasCost) - return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources')); - return hasCost; + if (config.dialog.configure === false && !hasCost) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.insufficientResources')); + return hasCost; + } } + /** + * + * Must be called within Action context. + * @param {*} costs + * @returns + */ static calcCosts(costs) { const resources = CostField.getResources.call(this, costs); return costs.map(c => { @@ -40,13 +111,19 @@ export default class CostField extends fields.ArrayField { c.key === 'fear' ? game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) : resources[c.key].isReversed - ? resources[c.key].max + ? resources[c.key].max - resources[c.key].value : resources[c.key].value; if (c.scalable) c.maxStep = Math.floor((c.max - c.value) / c.step); return c; }); } + /** + * Check if the current Actor currently has all needed resources. + * Must be called within Action context. + * @param {*} costs + * @returns {boolean} + */ static hasCost(costs) { const realCosts = CostField.getRealCosts.call(this, costs), hasFearCost = realCosts.findIndex(c => c.key === 'fear'); @@ -73,6 +150,12 @@ export default class CostField extends fields.ArrayField { ); } + /** + * Get all Actor resources + parent Item potential one. + * Must be called within Action context. + * @param {*} costs + * @returns + */ static getResources(costs) { const actorResources = foundry.utils.deepClone(this.actor.system.resources); if (this.actor.system.partner) @@ -93,6 +176,11 @@ export default class CostField extends fields.ArrayField { }; } + /** + * + * @param {*} costs + * @returns + */ static getRealCosts(costs) { const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; let mergedCosts = []; @@ -104,6 +192,12 @@ export default class CostField extends fields.ArrayField { return mergedCosts; } + /** + * Format scalable max cost, inject Action datas if it's a formula. + * Must be called within Action context. + * @param {number|string} max Configured maximum for that resource. + * @returns {number} The max cost value. + */ static formatMax(max) { max ??= 0; if (isNaN(max)) { diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index 4b4dee7d..db43cfb0 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -1,8 +1,15 @@ import FormulaField from '../formulaField.mjs'; +import { setsEqual } from '../../../helpers/utils.mjs'; const fields = foundry.data.fields; export default class DamageField extends fields.SchemaField { + /** + * Action Workflow order + */ + static order = 20; + + /** @inheritDoc */ constructor(options, context = {}) { const damageFields = { parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)), @@ -14,6 +21,141 @@ export default class DamageField extends fields.SchemaField { }; super(damageFields, options, context); } + + /** + * Roll Damage/Healing Action Workflow part. + * Must be called within Action context or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + * @param {string} [messageId=null] ChatMessage Id where the clicked button belong. + * @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example. + */ + static async execute(config, messageId = null, force = false) { + if(!this.hasDamage && !this.hasHealing) return; + if((this.hasRoll && DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.never.id) && !force) return; + + let formulas = this.damage.parts.map(p => ({ + formula: DamageField.getFormulaValue.call(this, p, config).getFormula(this.actor), + damageTypes: p.applyTo === 'hitPoints' && !p.type.size ? new Set(['physical']) : p.type, + applyTo: p.applyTo + })); + + if (!formulas.length) return false; + + formulas = DamageField.formatFormulas.call(this, formulas, config); + + const damageConfig = { + ...config, + roll: formulas, + dialog: {}, + data: this.getRollData() + }; + delete damageConfig.evaluate; + + if(DamageField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) damageConfig.dialog.configure = false; + if (config.hasSave) config.onSave = damageConfig.onSave = this.save.damageMod; + + damageConfig.source.message = config.message?._id ?? messageId; + damageConfig.directDamage = !!damageConfig.source?.message; + + if(damageConfig.source?.message && game.modules.get('dice-so-nice')?.active) + await game.dice3d.waitFor3DAnimationByMessageID(damageConfig.source.message); + + const damageResult = await CONFIG.Dice.daggerheart.DamageRoll.build(damageConfig); + if(!damageResult) return false; + config.damage = damageResult.damage; + config.message ??= damageConfig.message; + } + + /** + * Apply Damage/Healing Action Worflow part. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + * @param {*[]} targets Arrays of targets to bypass pre-selected ones. + * @param {boolean} force If the method should be executed outside of Action workflow, for ChatMessage button for example. + */ + static async applyDamage(config, targets = null, force = false) { + targets ??= config.targets.filter(target => target.hit); + if(!config.damage || !targets?.length || (!DamageField.getApplyAutomation() && !force)) return; + for (let target of targets) { + const actor = fromUuidSync(target.actorId); + if(!actor) continue; + if ( + !config.hasHealing && + config.onSave && + target.saved?.success === true + ) { + const mod = CONFIG.DH.ACTIONS.damageOnSave[config.onSave]?.mod ?? 1; + Object.entries(config.damage).forEach(([k, v]) => { + v.total = 0; + v.parts.forEach(part => { + part.total = Math.ceil(part.total * mod); + v.total += part.total; + }); + }); + } + + if (config.hasHealing) actor.takeHealing(config.damage); + else actor.takeDamage(config.damage, config.isDirect); + } + } + + /** + * Return value or valueAlt from damage part + * Must be called within Action context or similar. + * @param {object} part Damage Part + * @param {object} data Action getRollData + * @returns Formula value object + */ + static getFormulaValue(part, data) { + let formulaValue = part.value; + + if (data.hasRoll && part.resultBased && data.roll.result.duality === -1) return part.valueAlt; + + const isAdversary = this.actor.type === 'adversary'; + if (isAdversary && this.actor.system.type === CONFIG.DH.ACTOR.adversaryTypes.horde.id) { + const hasHordeDamage = this.actor.effects.find(x => x.type === 'horde'); + if (hasHordeDamage && !hasHordeDamage.disabled) return part.valueAlt; + } + + return formulaValue; + } + + /** + * Prepare formulas for Damage Roll + * Must be called within Action context or similar. + * @param {object[]} formulas Array of formatted formulas object + * @param {object} data Action getRollData + * @returns + */ + static formatFormulas(formulas, data) { + const formattedFormulas = []; + formulas.forEach(formula => { + if (isNaN(formula.formula)) + formula.formula = Roll.replaceFormulaData(formula.formula, this.getRollData(data)); + const same = formattedFormulas.find( + f => setsEqual(f.damageTypes, formula.damageTypes) && f.applyTo === formula.applyTo + ); + if (same) same.formula += ` + ${formula.formula}`; + else formattedFormulas.push(formula); + }); + return formattedFormulas; + } + + /** + * Return the automation setting for execute method for current user role + * @returns {string} Id from settingsConfig.mjs actionAutomationChoices + */ + static getAutomation() { + return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damage.players) + } + + + /** + * Return the automation setting for applyDamage method for current user role + * @returns {boolean} If applyDamage should be triggered automatically + */ + static getApplyAutomation() { + return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.damageApply.players) + } } export class DHActionDiceData extends foundry.abstract.DataModel { diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index ddc69d2d..3b8c5e43 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -1,6 +1,14 @@ +import { emitAsGM, GMUpdateEvent } from "../../../systemRegistration/socket.mjs"; + const fields = foundry.data.fields; export default class EffectsField extends fields.ArrayField { + /** + * Action Workflow order + */ + static order = 100; + + /** @inheritDoc */ constructor(options = {}, context = {}) { const element = new fields.SchemaField({ _id: new fields.DocumentIdField(), @@ -8,4 +16,86 @@ export default class EffectsField extends fields.ArrayField { }); super(element, options, context); } + + /** + * Apply Effects Action Workflow part. + * Must be called within Action context or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + * @param {object[]} [targets=null] Array of targets to override pre-selected ones. + * @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example. + */ + static async execute(config, targets = null, force = false) { + if(!config.hasEffect) return; + let message = config.message ?? ui.chat.collection.get(config.parent?._id); + if(!message) { + const roll = new CONFIG.Dice.daggerheart.DHRoll(''); + roll._evaluated = true; + message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); + } + if(EffectsField.getAutomation() || force) { + targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit); + await emitAsGM( + GMUpdateEvent.UpdateEffect, + EffectsField.applyEffects.bind(this), + targets, + this.uuid + ); + // EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit)); + } + } + + /** + * Apply Action Effects to a list of Targets + * Must be called within Action context or similar. + * @param {object[]} targets Array of formatted targets + */ + static async applyEffects(targets) { + if (!this.effects?.length || !targets?.length) return; + let effects = this.effects; + targets.forEach(async token => { + if (this.hasSave && token.saved.success === true) + effects = this.effects.filter(e => e.onSave === true); + if (!effects.length) return; + effects.forEach(async e => { + const actor = canvas.tokens.get(token.id)?.actor, + effect = this.item.effects.get(e._id); + if (!actor || !effect) return; + await EffectsField.applyEffect(effect, actor); + }); + }); + } + + /** + * Apply an Effect to a target or enable it if already on it + * @param {object} effect Effect object containing ActiveEffect UUID + * @param {object} actor Actor Document + */ + static async applyEffect(effect, actor) { + const existingEffect = actor.effects.find(e => e.origin === effect.uuid); + if (existingEffect) { + return effect.update( + foundry.utils.mergeObject({ + ...effect.constructor.getInitialDuration(), + disabled: false + }) + ); + } + + // Otherwise, create a new effect on the target + const effectData = foundry.utils.mergeObject({ + ...effect.toObject(), + disabled: false, + transfer: false, + origin: effect.uuid + }); + await ActiveEffect.implementation.create(effectData, { parent: actor }); + } + + /** + * Return the automation setting for execute method for current user role + * @returns {boolean} If execute should be triggered automatically + */ + static getAutomation() { + return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.effect.players) + } } diff --git a/module/data/fields/action/healingField.mjs b/module/data/fields/action/healingField.mjs deleted file mode 100644 index 98f4f5ea..00000000 --- a/module/data/fields/action/healingField.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import { DHDamageData } from './damageField.mjs'; - -const fields = foundry.data.fields; - -export default class HealingField extends fields.SchemaField { - constructor(options, context = {}) { - const healingFields = { - parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)) - }; - super(healingFields, options, context); - } -} diff --git a/module/data/fields/action/macroField.mjs b/module/data/fields/action/macroField.mjs index 62da0da0..222feb2a 100644 --- a/module/data/fields/action/macroField.mjs +++ b/module/data/fields/action/macroField.mjs @@ -1,7 +1,29 @@ const fields = foundry.data.fields; export default class MacroField extends fields.DocumentUUIDField { + /** + * Action Workflow order + */ + static order = 70; + + /** @inheritDoc */ constructor(context = {}) { super({ type: "Macro" }, context); } + + /** + * Macro Action Workflow part. + * Must be called within Action context or similar or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. Currently not used. + */ + static async execute(config) { + const fixUUID = !this.macro.includes('Macro.') ? `Macro.${this.macro}` : this.macro, + macro = await fromUuid(fixUUID); + try { + if (!macro) throw new Error(`No macro found for the UUID: ${this.macro}.`); + macro.execute(); + } catch (error) { + ui.notifications.error(error); + } + } } diff --git a/module/data/fields/action/rangeField.mjs b/module/data/fields/action/rangeField.mjs index 221f00af..1237e507 100644 --- a/module/data/fields/action/rangeField.mjs +++ b/module/data/fields/action/rangeField.mjs @@ -1,6 +1,8 @@ const fields = foundry.data.fields; export default class RangeField extends fields.StringField { + + /** @inheritDoc */ constructor(context = {}) { const options = { choices: CONFIG.DH.GENERAL.range, @@ -11,7 +13,12 @@ export default class RangeField extends fields.StringField { super(options, context); } - static prepareConfig(config) { - return true; + /** + * Update Action Workflow config object. + * NOT YET IMPLEMENTED. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + */ + prepareConfig(config) { + return; } } diff --git a/module/data/fields/action/rollField.mjs b/module/data/fields/action/rollField.mjs index a4df2a9e..cfcfb56b 100644 --- a/module/data/fields/action/rollField.mjs +++ b/module/data/fields/action/rollField.mjs @@ -71,29 +71,6 @@ export class DHActionRollData extends foundry.abstract.DataModel { const modifiers = []; if (!this.parent?.actor) return modifiers; switch (this.parent.actor.type) { - case 'character': - const spellcastingTrait = - this.type === 'spellcast' - ? (this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility') - : null; - const trait = - this.useDefault || !this.trait - ? (spellcastingTrait ?? this.parent.item.system.attack?.roll?.trait ?? 'agility') - : this.trait; - if ( - this.type === CONFIG.DH.GENERAL.rollTypes.attack.id || - this.type === CONFIG.DH.GENERAL.rollTypes.trait.id - ) - modifiers.push({ - label: `DAGGERHEART.CONFIG.Traits.${trait}.name`, - value: this.parent.actor.system.traits[trait].value - }); - else if (this.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id) - modifiers.push({ - label: `DAGGERHEART.CONFIG.RollTypes.spellcast.name`, - value: this.parent.actor.system.spellcastModifier - }); - break; case 'companion': case 'adversary': if (this.type === CONFIG.DH.GENERAL.rollTypes.attack.id) @@ -107,10 +84,74 @@ export class DHActionRollData extends foundry.abstract.DataModel { } return modifiers; } + + get rollTrait() { + if(this.parent?.actor?.type !== "character") return null; + switch (this.type) { + case CONFIG.DH.GENERAL.rollTypes.spellcast.id: + return this.parent.actor?.system?.spellcastModifierTrait?.key ?? 'agility'; + case CONFIG.DH.GENERAL.rollTypes.attack.id: + case CONFIG.DH.GENERAL.rollTypes.trait.id: + return this.useDefault || !this.trait + ? this.parent.item.system.attack?.roll?.trait ?? 'agility' + : this.trait; + default: + return null; + } + } } export default class RollField extends fields.EmbeddedDataField { + /** + * Action Workflow order + */ + static order = 10; + + /** @inheritDoc */ constructor(options, context = {}) { super(DHActionRollData, options, context); } + + /** + * Roll Action Workflow part. + * Must be called within Action context or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + */ + static async execute(config) { + if(!config.hasRoll) return; + config = await this.actor.diceRoll(config); + if(!config) return false; + } + + /** + * Update Action Workflow config object. + * Must be called within Action context. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + */ + prepareConfig(config) { + if(!config.hasRoll) return; + + config.dialog.configure = RollField.getAutomation() ? !config.dialog.configure : config.dialog.configure; + + const roll = { + baseModifiers: this.roll.getModifier(), + label: 'Attack', + type: this.roll?.type, + trait: this.roll?.rollTrait, + difficulty: this.roll?.difficulty, + formula: this.roll.getFormula(), + advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value + }; + if (this.roll.type === 'diceSet' || !this.hasRoll) roll.lite = true; + + config.roll = roll; + } + + /** + * Return the automation setting for execute method for current user role + * @returns {boolean} If execute should be triggered automatically + */ + static getAutomation() { + return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.roll.players) + } } diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs index e93a82a9..0003a4d5 100644 --- a/module/data/fields/action/saveField.mjs +++ b/module/data/fields/action/saveField.mjs @@ -1,6 +1,14 @@ +import { abilities } from "../../../config/actorConfig.mjs"; + const fields = foundry.data.fields; export default class SaveField extends fields.SchemaField { + /** + * Action Workflow order + */ + static order = 50; + + /** @inheritDoc */ constructor(options = {}, context = {}) { const saveFields = { trait: new fields.StringField({ @@ -16,4 +24,151 @@ export default class SaveField extends fields.SchemaField { }; super(saveFields, options, context); } + + /** + * Reaction Roll Action Workflow part. + * Must be called within Action context or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + * @param {object[]} [targets=null] Array of targets to override pre-selected ones. + * @param {boolean} [force=false] If the method should be executed outside of Action workflow, for ChatMessage button for example. + */ + static async execute(config, targets = null, force = false) { + if(!config.hasSave) return; + let message = config.message ?? ui.chat.collection.get(config.parent?._id); + + if(!message) { + const roll = new CONFIG.Dice.daggerheart.DHRoll(''); + roll._evaluated = true; + message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); + } + if(SaveField.getAutomation() !== CONFIG.DH.SETTINGS.actionAutomationChoices.never.id || force) { + targets ??= config.targets.filter(t => !config.hasRoll || t.hit); + await SaveField.rollAllSave.call(this, targets, config.event, message); + } else return false; + } + + /** + * Roll a Reaction Roll for all targets. Send a query to the owner if the User is not. + * Must be called within Action context. + * @param {object[]} targets Array of formatted targets. + * @param {Event} event Triggering event + * @param {ChatMessage} message The ChatMessage the triggered button comes from. + */ + static async rollAllSave(targets, event, message) { + if(!targets) return; + return new Promise(resolve => { + const aPromise = []; + targets.forEach(target => { + aPromise.push( + new Promise(async subResolve => { + const actor = fromUuidSync(target.actorId); + if(actor) { + const rollSave = game.user === actor.owner ? + SaveField.rollSave.call(this, actor, event) + : actor.owner + .query('reactionRoll', { + actionId: this.uuid, + actorId: actor.uuid, + event, + message + }); + const result = await rollSave; + await SaveField.updateSaveMessage.call(this, result, message, target.id); + subResolve(); + } else subResolve(); + }) + ) + }); + Promise.all(aPromise).then(result => resolve()); + }) + } + + /** + * Roll a Reaction Roll for the specified Actor against the Action difficulty. + * Must be called within Action context. + * @param {*} actor Actor document + * @param {Event} event Triggering event + * @returns {object} Actor diceRoll config result. + */ + static async rollSave(actor, event) { + if (!actor) return; + const title = actor.isNPC + ? game.i18n.localize('DAGGERHEART.GENERAL.reactionRoll') + : game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: game.i18n.localize(abilities[this.save.trait]?.label) + }), + rollConfig = { + event, + title, + roll: { + trait: this.save.trait, + difficulty: this.save.difficulty ?? this.actor?.baseSaveDifficulty, + type: 'trait' + }, + actionType: 'reaction', + hasRoll: true, + data: actor.getRollData() + }; + if(SaveField.getAutomation() === CONFIG.DH.SETTINGS.actionAutomationChoices.always.id) rollConfig.dialog = { configure: false }; + return actor.diceRoll(rollConfig); + } + + /** + * Update a Roll ChatMessage for a token according to his Reaction Roll result. + * @param {object} result Result from the Reaction Roll + * @param {object} message ChatMessage to update + * @param {string} targetId Token ID + */ + static async updateSaveMessage(result, message, targetId) { + if (!result) return; + const updateMsg = async function(message, targetId, result) { + // setTimeout(async () => { + const chatMessage = ui.chat.collection.get(message._id), + changes = { + flags: { + [game.system.id]: { + reactionRolls: { + [targetId]: + { + result: result.roll.total, + success: result.roll.success + } + } + } + } + }; + await chatMessage.update(changes); + // }, 100); + }; + if (game.modules.get('dice-so-nice')?.active) + game.dice3d.waitFor3DAnimationByMessageID(result.message.id ?? result.message._id).then(async () => await updateMsg(message, targetId, result)); + else await updateMsg(message, targetId, result); + } + + /** + * Return the automation setting for execute method for current user role + * @returns {string} Id from settingsConfig.mjs actionAutomationChoices + */ + static getAutomation() { + return (game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.gm) || (!game.user.isGM && game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).roll.save.players) + } + + /** + * Send a query to an Actor owner to roll a Reaction Roll then send back the result. + * @param {object} param0 + * @param {string} param0.actionId Action ID + * @param {string} param0.actorId Actor ID + * @param {Event} param0.event Triggering event + * @param {ChatMessage} param0.message Chat Message to update + * @returns + */ + static rollSaveQuery({ actionId, actorId, event, message }) { + return new Promise(async (resolve, reject) => { + const actor = await fromUuid(actorId), + action = await fromUuid(actionId); + if (!actor || !actor?.isOwner) reject(); + SaveField.rollSave.call(action, actor, event, message) + .then(result => resolve(result)); + }); + } } diff --git a/module/data/fields/action/targetField.mjs b/module/data/fields/action/targetField.mjs index bfb01db9..4499dcc8 100644 --- a/module/data/fields/action/targetField.mjs +++ b/module/data/fields/action/targetField.mjs @@ -1,6 +1,8 @@ const fields = foundry.data.fields; export default class TargetField extends fields.SchemaField { + + /** @inheritDoc */ constructor(options = {}, context = {}) { const targetFields = { type: new fields.StringField({ @@ -13,44 +15,70 @@ export default class TargetField extends fields.SchemaField { super(targetFields, options, context); } - static prepareConfig(config) { - if (!this.target?.type) return []; + /** + * Update Action Workflow config object. + * Must be called within Action context. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + */ + prepareConfig(config) { + if (!this.target?.type) return config.targets = []; config.hasTarget = true; let targets; + // If the Action is configured as self-targeted, set targets as the owner. if (this.target?.type === CONFIG.DH.GENERAL.targetTypes.self.id) targets = [this.actor.token ?? this.actor.prototypeToken]; else { targets = Array.from(game.user.targets); if (this.target.type !== CONFIG.DH.GENERAL.targetTypes.any.id) { - targets = targets.filter(t => TargetField.isTargetFriendly.call(this, t)); + targets = targets.filter(target => TargetField.isTargetFriendly(this.actor, target, this.target.type)); if (this.target.amount && targets.length > this.target.amount) targets = []; } } config.targets = targets.map(t => TargetField.formatTarget.call(this, t)); const hasTargets = TargetField.checkTargets.call(this, this.target.amount, config.targets); - if (config.isFastForward && !hasTargets) - return ui.notifications.warn('Too many targets selected for that actions.'); - return hasTargets; + if (config.dialog.configure === false && !hasTargets) { + ui.notifications.warn('Too many targets selected for that actions.'); + return hasTargets; + } } + /** + * Check if the number of selected targets respect the amount set in the Action. + * NOT YET IMPLEMENTED. Will be with Target Picker. + * @param {number} amount Max amount of targets configured in the action. + * @param {*[]} targets Array of targeted tokens. + * @returns {boolean} If the amount of targeted tokens does not exceed action configured one. + */ static checkTargets(amount, targets) { return true; // return !amount || (targets.length > amount); } - static isTargetFriendly(target) { - const actorDisposition = this.actor.token - ? this.actor.token.disposition - : this.actor.prototypeToken.disposition, + /** + * Compare 2 Actors disposition between each other + * @param {*} actor First actor document. + * @param {*} target Second actor document. + * @param {string} type Disposition id to compare (friendly/hostile). + * @returns {boolean} If both actors respect the provided type. + */ + static isTargetFriendly(actor, target, type) { + const actorDisposition = actor.token + ? actor.token.disposition + : actor.prototypeToken.disposition, targetDisposition = target.document.disposition; return ( - (this.target.type === CONFIG.DH.GENERAL.targetTypes.friendly.id && + (type === CONFIG.DH.GENERAL.targetTypes.friendly.id && actorDisposition === targetDisposition) || - (this.target.type === CONFIG.DH.GENERAL.targetTypes.hostile.id && + (type === CONFIG.DH.GENERAL.targetTypes.hostile.id && actorDisposition + targetDisposition === 0) ); } + /** + * Format actor to useful datas for Action roll workflow. + * @param {*} actor Actor object to format. + * @returns {*} Formatted Actor. + */ static formatTarget(actor) { return { id: actor.id, diff --git a/module/data/fields/action/usesField.mjs b/module/data/fields/action/usesField.mjs index 5c2bfb61..d180ddf8 100644 --- a/module/data/fields/action/usesField.mjs +++ b/module/data/fields/action/usesField.mjs @@ -3,6 +3,12 @@ import FormulaField from '../formulaField.mjs'; const fields = foundry.data.fields; export default class UsesField extends fields.SchemaField { + /** + * Action Workflow order + */ + static order = 160; + + /** @inheritDoc */ constructor(options = {}, context = {}) { const usesFields = { value: new fields.NumberField({ nullable: true, initial: null }), @@ -20,15 +26,45 @@ export default class UsesField extends fields.SchemaField { super(usesFields, options, context); } - static prepareConfig(config) { + /** + * Uses Consumption Action Workflow part. + * Increment Action spent uses by 1. + * Must be called within Action context or similar or similar. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + * @param {boolean} [successCost=false] Consume only resources configured as "On Success only" if not already consumed. + */ + static async execute(config, successCost = false) { + if ( + config.uses?.enabled && + ((!successCost && (!config.uses?.consumeOnSuccess || config.roll?.success)) || + (successCost && config.uses?.consumeOnSuccess)) + ) + this.update({ 'uses.value': this.uses.value + 1 }); + } + + /** + * Update Action Workflow config object. + * Must be called within Action context. + * @param {object} config Object that contains workflow datas. Usually made from Action Fields prepareConfig methods. + * @returns {boolean} Return false if fast-forwarded and no more uses. + */ + prepareConfig(config) { const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null; if (uses && !uses.value) uses.value = 0; config.uses = uses; const hasUses = UsesField.hasUses.call(this, config.uses); - if (config.isFastForward && !hasUses) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionNoUsesRemaining')); - return hasUses; + if (config.dialog.configure === false && !hasUses) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionNoUsesRemaining')); + return hasUses; + } } + /** + * Prepare Uses object for Action Workflow + * Must be called within Action context. + * @param {object} uses + * @returns {object} + */ static calcUses(uses) { if (!uses) return null; return { @@ -38,6 +74,12 @@ export default class UsesField extends fields.SchemaField { }; } + /** + * Check if the Action still get atleast one unspent uses. + * Must be called within Action context. + * @param {*} uses + * @returns {boolean} + */ static hasUses(uses) { if (!uses) return true; let max = uses.max ?? 0; diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index e1d63669..da71e899 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -80,6 +80,72 @@ export default class DhAutomation extends foundry.abstract.DataModel { initial: CONFIG.DH.GENERAL.defeatedConditions.defeated.id, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label' }) + }), + roll: new fields.SchemaField({ + roll: new fields.SchemaField({ + gm: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.GENERAL.gm' + }), + players: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.GENERAL.player.plurial' + }) + }), + damage: new fields.SchemaField({ + gm: new fields.StringField({ + required: true, + initial: "never", + choices: CONFIG.DH.SETTINGS.actionAutomationChoices, + label: 'DAGGERHEART.GENERAL.gm' + }), + players: new fields.StringField({ + required: true, + initial: "never", + choices: CONFIG.DH.SETTINGS.actionAutomationChoices, + label: 'DAGGERHEART.GENERAL.player.plurial' + }) + }), + save: new fields.SchemaField({ + gm: new fields.StringField({ + required: true, + initial: "never", + choices: CONFIG.DH.SETTINGS.actionAutomationChoices, + label: 'DAGGERHEART.GENERAL.gm' + }), + players: new fields.StringField({ + required: true, + initial: "never", + choices: CONFIG.DH.SETTINGS.actionAutomationChoices, + label: 'DAGGERHEART.GENERAL.player.plurial' + }) + }), + damageApply: new fields.SchemaField({ + gm: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.GENERAL.gm' + }), + players: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.GENERAL.player.plurial' + }) + }), + effect: new fields.SchemaField({ + gm: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.GENERAL.gm' + }), + players: new fields.BooleanField({ + required: true, + initial: false, + label: 'DAGGERHEART.GENERAL.player.plurial' + }) + }) }) }; } diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 63d84744..f0660106 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -128,7 +128,9 @@ export default class D20Roll extends DHRoll { applyBaseBonus() { const modifiers = foundry.utils.deepClone(this.options.roll.baseModifiers) ?? []; - modifiers.push(...this.getBonus(`roll.${this.options.type}`, `${this.options.type?.capitalize()} Bonus`)); + 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`) ); @@ -138,7 +140,7 @@ export default class D20Roll extends DHRoll { static postEvaluate(roll, config = {}) { const data = super.postEvaluate(roll, config); - data.type = config.roll?.type; + data.type = config.actionType; data.difficulty = config.roll.difficulty; if (config.targets?.length) { config.targets.forEach(target => { @@ -147,6 +149,7 @@ export default class D20Roll extends DHRoll { }); data.success = config.targets.some(target => target.hit); } else if (config.roll.difficulty) data.success = roll.isCritical || roll.total >= config.roll.difficulty; + config.successConsumed = data.success; data.advantage = { type: config.roll.advantage, diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index aa9e1d94..4d293d9d 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -9,6 +9,7 @@ export default class DamageRoll extends DHRoll { static DefaultDialog = DamageDialog; static async buildEvaluate(roll, config = {}, message = {}) { + if (config.dialog.configure === false) roll.constructFormula(config); if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate(); roll._evaluated = true; @@ -46,9 +47,8 @@ export default class DamageRoll extends DHRoll { ); } await super.buildPost(roll, config, message); - if (config.source?.message) { + if (config.source?.message) chatMessage.update({ 'system.damage': config.damage }); - } } static unifyDamageRoll(rolls) { diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index ac340c64..0dcdd316 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -28,6 +28,7 @@ export default class DHRoll extends Roll { static async buildConfigure(config = {}, message = {}) { config.hooks = [...this.getHooks(), '']; config.dialog ??= {}; + for (const hook of config.hooks) { if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; } @@ -45,10 +46,7 @@ export default class DHRoll extends Roll { } for (const hook of config.hooks) { - if ( - Hooks.call(`${CONFIG.DH.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false - ) - return []; + if (Hooks.call(`${CONFIG.DH.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false) return []; } return roll; } @@ -88,11 +86,12 @@ export default class DHRoll extends Roll { type: this.messageType, user: game.user.id, title: roll.title, - speaker: cls.getSpeaker(), + speaker: cls.getSpeaker({ actor: roll.data?.parent }), sound: config.mute ? null : CONFIG.sounds.dice, system: config, rolls: [roll] }; + config.selectedRollMode ??= game.settings.get('core', 'rollMode'); if (roll._evaluated) { @@ -226,7 +225,7 @@ export const registerRollDiceHooks = () => { if ( !config.source?.actor || (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || - config.roll.type === 'reaction' + config.actionType === 'reaction' ) return; diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index ac1047ab..93ac231e 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -19,7 +19,7 @@ export default class DualityRoll extends D20Roll { get title() { return game.i18n.localize( - `DAGGERHEART.GENERAL.${this.options?.roll?.type === CONFIG.DH.ITEM.actionTypes.reaction.id ? 'reactionRoll' : 'dualityRoll'}` + `DAGGERHEART.GENERAL.${this.options?.actionType === CONFIG.DH.ITEM.actionTypes.reaction.id ? 'reactionRoll' : 'dualityRoll'}` ); } @@ -153,10 +153,10 @@ export default class DualityRoll extends D20Roll { applyBaseBonus() { const modifiers = super.applyBaseBonus(); - - if (this.options.roll.trait && this.data.traits[this.options.roll.trait]) + + if (this.options.roll.trait && this.data.traits?.[this.options.roll.trait]) modifiers.unshift({ - label: `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`, + label: this.options.roll.type === CONFIG.DH.GENERAL.rollTypes.spellcast.id ? "DAGGERHEART.CONFIG.RollTypes.spellcast.name" : `DAGGERHEART.CONFIG.Traits.${this.options.roll.trait}.name`, value: this.data.traits[this.options.roll.trait].value }); diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 08735a45..950ed621 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -14,7 +14,7 @@ export default class DhpActor extends Actor { get owner() { const user = this.hasPlayerOwner && game.users.players.find(u => this.testUserPermission(u, 'OWNER') && u.active); - if (!user) return game.user.isGM ? game.user : null; + if (!user) return game.users.activeGM; return user; } diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index c7f30e48..1a619a9c 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -1,3 +1,5 @@ +import { emitAsGM, GMUpdateEvent } from "../systemRegistration/socket.mjs"; + export default class DhpChatMessage extends foundry.documents.ChatMessage { targetHook = null; @@ -102,20 +104,30 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { }); if (itemDesc && autoExpandRoll.desc) itemDesc.setAttribute('open', ''); } - - if (!game.user.isGM) { - const applyButtons = html.querySelector('.apply-buttons'); + + if(!this.isAuthor && !this.speakerActor?.isOwner) { + const applyButtons = html.querySelector(".apply-buttons"); applyButtons?.remove(); - if (!this.isAuthor && !this.speakerActor?.isOwner) { - const buttons = html.querySelectorAll('.ability-card-footer > .ability-use-button'); - buttons.forEach(b => b.remove()); - } + const buttons = html.querySelectorAll(".ability-card-footer > .ability-use-button"); + buttons.forEach(b => b.remove()); } } addChatListeners(html) { + html.querySelectorAll('.duality-action-damage').forEach(element => + element.addEventListener('click', this.onRollDamage.bind(this)) + ); + html.querySelectorAll('.damage-button').forEach(element => - element.addEventListener('click', this.onDamage.bind(this)) + element.addEventListener('click', this.onApplyDamage.bind(this)) + ); + + html.querySelectorAll('.target-save').forEach(element => + element.addEventListener('click', this.onRollSave.bind(this)) + ); + + html.querySelectorAll('.roll-all-save-button').forEach(element => + element.addEventListener('click', this.onRollAllSave.bind(this)) ); html.querySelectorAll('.duality-action-effect').forEach(element => @@ -133,17 +145,21 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { }); } - getTargetList() { - const targets = this.system.hitTargets ?? []; - return targets.map(target => game.canvas.tokens.documentCollection.find(t => t.actor?.uuid === target.actorId)); + async onRollDamage(event) { + event.stopPropagation(); + const config = foundry.utils.deepClone(this.system); + config.event = event; + this.system.action?.workflow.get("damage")?.execute(config, this._id, true); } - async onDamage(event) { + async onApplyDamage(event) { event.stopPropagation(); - const targets = this.getTargetList(); + const targets = this.filterPermTargets(this.system.hitTargets), + config = foundry.utils.deepClone(this.system); + config.event = event; if (this.system.onSave) { - const pendingingSaves = this.system.hitTargets.filter(t => t.saved.success === null); + const pendingingSaves = targets.filter(t => t.saved.success === null); if (pendingingSaves.length) { const confirm = await foundry.applications.api.DialogV2.confirm({ window: { title: 'Pending Reaction Rolls found' }, @@ -154,62 +170,62 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { } if (targets.length === 0) - return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); + return ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); + + this.consumeOnSuccess(); + this.system.action?.workflow.get("applyDamage")?.execute(config, targets, true); + } - for (let target of targets) { - let damages = foundry.utils.deepClone(this.system.damage); - if ( - !this.system.hasHealing && - this.system.onSave && - this.system.hitTargets.find(t => t.id === target.id)?.saved?.success === true - ) { - const mod = CONFIG.DH.ACTIONS.damageOnSave[this.system.onSave]?.mod ?? 1; - Object.entries(damages).forEach(([k, v]) => { - v.total = 0; - v.parts.forEach(part => { - part.total = Math.ceil(part.total * mod); - v.total += part.total; - }); - }); - } - - this.consumeOnSuccess(); - if (this.system.hasHealing) target.actor.takeHealing(damages); - else target.actor.takeDamage(damages, this.system.isDirect); + async onRollSave(event) { + event.stopPropagation(); + const tokenId = event.target.closest('[data-token]')?.dataset.token, + token = game.canvas.tokens.get(tokenId); + if (!token?.actor || !token.isOwner) return true; + if (this.system.source.item && this.system.source.action) { + const action = this.system.action; + if (!action || !action?.hasSave) return; + game.system.api.fields.ActionFields.SaveField.rollSave.call(action, token.actor, event).then(result => + emitAsGM( + GMUpdateEvent.UpdateSaveMessage, + game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind(action, result, this, token.id), + { + action: action.uuid, + message: this._id, + token: token.id, + result + } + ) + ); } } - getAction(actor, itemId, actionId) { - const item = actor.items.get(itemId), - action = - actor.system.attack?._id === actionId - ? actor.system.attack - : item.system.attack?._id === actionId - ? item.system.attack - : item?.system?.actions?.get(actionId); - return action; + async onRollAllSave(event) { + event.stopPropagation(); + if (!game.user.isGM) return; + const targets = this.system.hitTargets, + config = foundry.utils.deepClone(this.system); + config.event = event; + this.system.action?.workflow.get("save")?.execute(config, targets, true); } async onApplyEffect(event) { event.stopPropagation(); - const actor = await foundry.utils.fromUuid(this.system.source.actor); - if (!actor || !game.user.isGM) return true; - if (this.system.source.item && this.system.source.action) { - const action = this.getAction(actor, this.system.source.item, this.system.source.action); - if (!action || !action?.applyEffects) return; - const targets = this.getTargetList(); - if (targets.length === 0) - ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelected')); - this.consumeOnSuccess(); - await action.applyEffects(event, this, targets); - } + const targets = this.filterPermTargets(this.system.hitTargets), + config = foundry.utils.deepClone(this.system); + config.event = event; + if (targets.length === 0) + ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noTargetsSelectedOrPerm')); + this.consumeOnSuccess(); + this.system.action?.workflow.get("effects")?.execute(config, targets, true); + } + + filterPermTargets(targets) { + return targets.filter(t => fromUuidSync(t.actorId)?.canUserModify(game.user, "update")) } consumeOnSuccess() { - if (!this.system.successConsumed && !this.targetSelection) { - const action = this.system.action; - if (action) action.consume(this.system, true); - } + if (!this.system.successConsumed && !this.targetSelection) + this.system.action?.consume(this.system, true); } hoverTarget(event) { diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index b47fee85..f3f9629b 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -22,6 +22,7 @@ export const socketEvent = { export const GMUpdateEvent = { UpdateDocument: 'DhGMUpdateDocument', + UpdateEffect: 'DhGMUpdateEffect', UpdateSetting: 'DhGMUpdateSetting', UpdateFear: 'DhGMUpdateFear', UpdateSaveMessage: 'DhGMUpdateSaveMessage' @@ -37,9 +38,12 @@ export const registerSocketHooks = () => { const document = data.uuid ? await fromUuid(data.uuid) : null; switch (data.action) { case GMUpdateEvent.UpdateDocument: - if (document && data.update) { + if (document && data.update) await document.update(data.update); - } + break; + case GMUpdateEvent.UpdateEffect: + if (document && data.update) + await game.system.api.fields.ActionFields.EffectsField.applyEffects.call(document, data.update); break; case GMUpdateEvent.UpdateSetting: await game.settings.set(CONFIG.DH.id, data.uuid, data.update); @@ -78,7 +82,7 @@ export const registerSocketHooks = () => { export const registerUserQueries = () => { CONFIG.queries.armorSlot = DamageReductionDialog.armorSlotQuery; - CONFIG.queries.reactionRoll = game.system.api.models.actions.actionsTypes.base.rollSaveQuery; + CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery; }; export const emitAsGM = async (eventName, callback, update, uuid = null) => { diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 399715ca..e4a2128c 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -624,6 +624,7 @@ .form-group { justify-content: end; + flex-wrap: nowrap; } .hint { diff --git a/styles/less/global/global.less b/styles/less/global/global.less index cf8431b5..e135846f 100644 --- a/styles/less/global/global.less +++ b/styles/less/global/global.less @@ -18,7 +18,7 @@ color: var(--color-form-hint); } - &:hover { + .form-group:hover { .hint { color: var(--color-form-hint-hover); } diff --git a/styles/less/ui/settings/settings.less b/styles/less/ui/settings/settings.less index 2ac8bfb0..67fe7718 100644 --- a/styles/less/ui/settings/settings.less +++ b/styles/less/ui/settings/settings.less @@ -127,4 +127,4 @@ text-align: center; } } -} +} \ No newline at end of file diff --git a/templates/actionTypes/beastform.hbs b/templates/actionTypes/beastform.hbs index b2710208..b9bea445 100644 --- a/templates/actionTypes/beastform.hbs +++ b/templates/actionTypes/beastform.hbs @@ -1,9 +1,4 @@ -
- -
{{localize "DAGGERHEART.ACTIONS.Config.beastform.label"}}
-
- -
- {{formGroup fields.tierAccess.fields.exact value=beastform.tierAccess.exact labelAttr="label" valueAttr="key" localize=true blank=""}} -
+
+ {{localize "DAGGERHEART.ACTIONS.Config.beastform.label"}} + {{formGroup fields.tierAccess.fields.exact value=source.tierAccess.exact name="beastform.tierAccess.exact" labelAttr="label" valueAttr="key" localize=true blank=""}}
\ No newline at end of file diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index 6a94752b..96bb361c 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -56,7 +56,7 @@
{{formField ../fields.valueAlt.fields.flatMultiplier value=dmg.valueAlt.flatMultiplier name=(concat ../path "damage.parts." realIndex ".valueAlt.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }} - {{formField ../fields.valueAlt.fields.dice value=dmg.valueAlt.dice name=(concat ../path "damage.parts." realIndex ".valueAlt.dice") classes="inline-child"}} + {{formField ../fields.valueAlt.fields.dice value=dmg.valueAlt.dice name=(concat ../path "damage.parts." realIndex ".valueAlt.dice") classes="inline-child" localize=true}} {{formField ../fields.valueAlt.fields.bonus value=dmg.valueAlt.bonus name=(concat ../path "damage.parts." realIndex ".valueAlt.bonus") localize=true classes="inline-child"}}
@@ -70,7 +70,7 @@ {{#*inline "formula"}} {{#unless dmg.base}} - {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat path "damage.parts." realIndex "." target ".custom.enabled") classes="checkbox"}} + {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat path "damage.parts." realIndex "." target ".custom.enabled") classes="checkbox" localize=true}} {{/unless}} {{#if source.custom.enabled}} {{formField fields.custom.fields.formula value=source.custom.formula name=(concat path "damage.parts." realIndex "." target ".custom.formula") localize=true}} @@ -79,8 +79,8 @@ {{#unless @root.isNPC}} {{formField fields.multiplier value=source.multiplier name=(concat path "damage.parts." realIndex "." target ".multiplier") localize=true}} {{/unless}} - {{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat path "damage.parts." realIndex "." target ".flatMultiplier") }}{{/if}} - {{formField fields.dice value=source.dice name=(concat path "damage.parts." realIndex "." target ".dice")}} + {{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat path "damage.parts." realIndex "." target ".flatMultiplier") localize=true }}{{/if}} + {{formField fields.dice value=source.dice name=(concat path "damage.parts." realIndex "." target ".dice") localize=true}} {{formField fields.bonus value=source.bonus name=(concat path "damage.parts." realIndex "." target ".bonus") localize=true}} {{/if}} diff --git a/templates/actionTypes/save.hbs b/templates/actionTypes/save.hbs index 90bc0483..85536c87 100644 --- a/templates/actionTypes/save.hbs +++ b/templates/actionTypes/save.hbs @@ -1,6 +1,9 @@ -
- {{localize "DAGGERHEART.GENERAL.save"}} - {{formField fields.trait label="Trait" name="save.trait" value=source.trait localize=true}} - {{formField fields.difficulty label="Difficulty" name="save.difficulty" value=source.difficulty disabled=(not source.trait) placeholder=@root.baseSaveDifficulty}} - {{formField fields.damageMod label="Damage on Save" name="save.damageMod" value=source.damageMod localize=true disabled=(not source.trait)}} +
+ {{localize "DAGGERHEART.GENERAL.Roll.reaction"}} +

{{localize "DAGGERHEART.ACTIONS.Settings.saveHint"}}

+
+ {{formField fields.trait label="Trait" name="save.trait" value=source.trait localize=true}} + {{formField fields.difficulty label="Difficulty" name="save.difficulty" value=source.difficulty disabled=(not source.trait) placeholder=@root.baseSaveDifficulty}} + {{formField fields.damageMod label="Damage on Save" name="save.damageMod" value=source.damageMod localize=true disabled=(not source.trait)}} +
\ No newline at end of file diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index b2feaa0b..6a4655d6 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -75,16 +75,8 @@ {{#each experiences}} {{#if name}}
- {{#if (includes ../selectedExperiences id)}} - - {{else}} - - {{/if}} - {{#if (eq @root.rollType 'D20Roll')}} - {{name}} +{{modifier}} - {{else}} - {{name}} +{{value}} - {{/if}} + + {{name}} +{{value}}
{{/if}} {{/each}} @@ -126,6 +118,12 @@ {{selectOptions diceOptions selected=@root.roll.dAdvantage.denomination}} + {{#if abilities}} + {{localize "DAGGERHEART.GENERAL.traitModifier"}} + + {{/if}} {{/unless}} {{#if @root.rallyDie.length}} {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} diff --git a/templates/settings/automation-settings/roll.hbs b/templates/settings/automation-settings/roll.hbs new file mode 100644 index 00000000..5769bf61 --- /dev/null +++ b/templates/settings/automation-settings/roll.hbs @@ -0,0 +1,22 @@ +
+
+ + {{localize "DAGGERHEART.SETTINGS.Automation.roll.title"}} + + {{#each settingFields.schema.fields.roll.fields as | field |}} + {{!-- {{formGroup field value=(lookup @root.settingFields.roll field.name) localize=true rootId="automation-roll"}} --}} +
+ + {{#with (lookup @root.settingFields.roll field.name) as | values |}} + {{formGroup field.fields.gm value=values.gm rootId=(concat "automation-roll-" field.name "-gm") localize=true}} + {{formGroup field.fields.players value=values.players rootId=(concat "automation-roll-" field.name "-players") localize=true}} + {{/with}} +

{{localize (concat "DAGGERHEART.SETTINGS.Automation.FIELDS.roll." field.name ".hint")}}

+
+ {{/each}} +
+
\ No newline at end of file diff --git a/templates/sheets-settings/action-settings/effect.hbs b/templates/sheets-settings/action-settings/effect.hbs index 26a097af..51c15aae 100644 --- a/templates/sheets-settings/action-settings/effect.hbs +++ b/templates/sheets-settings/action-settings/effect.hbs @@ -6,7 +6,6 @@ {{#if fields.roll}}{{> 'systems/daggerheart/templates/actionTypes/roll.hbs' fields=fields.roll.fields source=source.roll}}{{/if}} {{#if fields.save}}{{> 'systems/daggerheart/templates/actionTypes/save.hbs' fields=fields.save.fields source=source.save}}{{/if}} {{#if fields.damage}}{{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=fields.damage.fields.parts.element.fields source=source.damage directField=fields.damage.fields.direct }}{{/if}} - {{#if fields.resource}}{{> 'systems/daggerheart/templates/actionTypes/resource.hbs' fields=fields.resource.fields source=source.resource}}{{/if}} {{#if fields.macro}}{{> 'systems/daggerheart/templates/actionTypes/macro.hbs' fields=fields.macro source=source.macro}}{{/if}} {{#if fields.effects}}{{> 'systems/daggerheart/templates/actionTypes/effect.hbs' fields=fields.effects.element.fields source=source.effects}}{{/if}} {{#if fields.beastform}}{{> 'systems/daggerheart/templates/actionTypes/beastform.hbs' fields=fields.beastform.fields source=source.beastform}}{{/if}} diff --git a/templates/ui/chat/parts/button-part.hbs b/templates/ui/chat/parts/button-part.hbs index f83972f7..21317939 100644 --- a/templates/ui/chat/parts/button-part.hbs +++ b/templates/ui/chat/parts/button-part.hbs @@ -1,17 +1,17 @@
{{#if hasDamage}} {{#unless (empty damage)}} - {{#if canButtonApply}}{{/if}} + {{else}} {{/unless}} {{/if}} {{#if hasHealing}} {{#unless (empty damage)}} - {{#if canButtonApply}}{{/if}} + {{else}} {{/unless}} {{/if}} - {{#if (and hasEffect canButtonApply)}}{{/if}} + {{#if (and hasEffect)}}{{/if}}
\ No newline at end of file diff --git a/templates/ui/chat/parts/damage-part.hbs b/templates/ui/chat/parts/damage-part.hbs index 05b6b825..97828bd2 100644 --- a/templates/ui/chat/parts/damage-part.hbs +++ b/templates/ui/chat/parts/damage-part.hbs @@ -13,10 +13,10 @@ {{#each damage as | roll index | }}
- {{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}
{{localize "DAGGERHEART.GENERAL.total"}}: {{roll.total}}
{{#if (and (eq index "hitPoints")../isDirect)}}
{{localize "DAGGERHEART.CONFIG.DamageType.direct.short"}}
{{/if}} + {{#if ../hasHealing}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')}}{{else}}{{localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')}}{{/if}}
{{localize "DAGGERHEART.GENERAL.total"}}: {{roll.total}}
{{#if (and (eq index "hitPoints") ../isDirect)}}
{{localize "DAGGERHEART.CONFIG.DamageType.direct.short"}}
{{/if}}
{{#each roll.parts}} - {{#if damageTypes.length}} + {{#if (and (not @root.hasHealing) damageTypes.length)}}