diff --git a/daggerheart.mjs b/daggerheart.mjs index 4568c670..3f7dec77 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -3,6 +3,7 @@ import * as applications from './module/applications/_module.mjs'; import * as models from './module/data/_module.mjs'; import * as documents from './module/documents/_module.mjs'; import * as dice from './module/dice/_module.mjs'; +import * as fields from './module/data/fields/_module.mjs' import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; @@ -27,7 +28,8 @@ Hooks.once('init', () => { applications, models, documents, - dice + dice, + fields }; CONFIG.TextEditor.enrichers.push(...enricherConfig); diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 53fc9d69..5cd80c7d 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -13,7 +13,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.action = config.data.attack?._id == config.source.action ? config.data.attack - : this.item.system.actions.find(a => a._id === config.source.action); + : this.item.system.actions.get(config.source.action); } } @@ -68,19 +68,19 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio })); if (this.config.costs?.length) { - const updatedCosts = this.action.calcCosts(this.config.costs); + const updatedCosts = game.system.api.fields.ActionFields.CostField.calcCosts.call(this.action, this.config.costs); context.costs = updatedCosts.map(x => ({ ...x, label: x.keyIsID ? this.action.parent.parent.name : game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label) })); - context.canRoll = this.action.hasCost(updatedCosts); + context.canRoll = game.system.api.fields.ActionFields.CostField.hasCost.call(this.action, updatedCosts); this.config.data.scale = this.config.costs[0].total; } if (this.config.uses?.max) { - context.uses = this.action.calcUses(this.config.uses); - context.canRoll = context.canRoll && this.action.hasUses(context.uses); + context.uses = game.system.api.fields.ActionFields.UsesField.calcUses.call(this.action, this.config.uses); + context.canRoll = context.canRoll && game.system.api.fields.ActionFields.UsesField.hasUses.call(this.action, context.uses); } if (this.roll) { context.roll = this.roll; diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index 3f915e41..f6b9932f 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -105,7 +105,6 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { if (!!this.action.effects) context.effects = this.action.effects.map(e => this.action.item.effects.get(e._id)); if (this.action.damage?.hasOwnProperty('includeBase') && this.action.type === 'attack') context.hasBaseDamage = !!this.action.parent.attack; - context.getRealIndex = this.getRealIndex.bind(this); context.getEffectDetails = this.getEffectDetails.bind(this); context.costOptions = this.getCostOptions(); context.disableOption = this.disableOption.bind(this); @@ -147,11 +146,6 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { return filtered; } - getRealIndex(index) { - const data = this.action.toObject(false); - return data.damage.parts.find(d => d.base) ? index - 1 : index; - } - getEffectDetails(id) { return this.action.item.effects.get(id); } @@ -175,19 +169,8 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { static async updateForm(event, _, formData) { const submitData = this._prepareSubmitData(event, formData), - data = foundry.utils.mergeObject(this.action.toObject(), submitData), - container = foundry.utils.getProperty(this.action.parent, this.action.systemPath); - let newActions; - if (Array.isArray(container)) { - newActions = foundry.utils.getProperty(this.action.parent, this.action.systemPath).map(x => x.toObject()); - if (!newActions.findSplice(x => x._id === data._id, data)) newActions.push(data); - } else newActions = data; - - const updates = await this.action.parent.parent.update({ [`system.${this.action.systemPath}`]: newActions }); - if (!updates) return; - this.action = Array.isArray(container) - ? foundry.utils.getProperty(updates.system, this.action.systemPath)[this.action.index] - : foundry.utils.getProperty(updates.system, this.action.systemPath); + data = foundry.utils.mergeObject(this.action.toObject(), submitData); + this.action = await this.action.update(data); this.render(); } @@ -210,8 +193,10 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { static addDamage(event) { if (!this.action.damage.parts) return; - const data = this.action.toObject(); - data.damage.parts.push({}); + const data = this.action.toObject(), + part = {}; + if(this.action.actor?.isNPC) part.value = { multiplier: 'flat' }; + data.damage.parts.push(part); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); } diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 6597f130..544ab575 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -82,7 +82,6 @@ export default function DHApplicationMixin(Base) { deleteDoc: DHSheetV2.#deleteDoc, toChat: DHSheetV2.#toChat, useItem: DHSheetV2.#useItem, - useAction: DHSheetV2.#useAction, toggleEffect: DHSheetV2.#toggleEffect, toggleExtended: DHSheetV2.#toggleExtended }, @@ -271,65 +270,8 @@ export default function DHApplicationMixin(Base) { */ static #getActionContextOptions() { /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */ - const getAction = target => { - const { actionId } = target.closest('[data-action-id]').dataset; - const { actions, attack } = this.document.system; - return attack?.id === actionId ? attack : actions?.find(a => a.id === actionId); - }; - - const options = [ - { - name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', - icon: 'fa-solid fa-burst', - condition: - this.document instanceof foundry.documents.Actor || - (this.document instanceof foundry.documents.Item && this.document.parent), - callback: (target, event) => getAction(target).use(event) - }, - { - name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', - icon: 'fa-solid fa-message', - callback: target => getAction(target).toChat(this.document.id) - }, - { - name: 'CONTROLS.CommonEdit', - icon: 'fa-solid fa-pen-to-square', - callback: target => new DHActionConfig(getAction(target)).render({ force: true }) - }, - { - name: 'CONTROLS.CommonDelete', - icon: 'fa-solid fa-trash', - condition: target => { - const { actionId } = target.closest('[data-action-id]').dataset; - const { attack } = this.document.system; - return attack?.id !== actionId; - }, - callback: async target => { - const action = getAction(target); - const confirmed = await foundry.applications.api.DialogV2.confirm({ - window: { - title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { - type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`), - name: action.name - }) - }, - content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { - name: action.name - }) - }); - if (!confirmed) return; - - return this.document.update({ - 'system.actions': this.document.system.actions.filter(a => a.id !== action.id) - }); - } - } - ].map(option => ({ - ...option, - icon: `` - })); - - return options; + const options = []; + return [...options, ...this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true })]; } /** @@ -341,6 +283,10 @@ export default function DHApplicationMixin(Base) { { name: 'CONTROLS.CommonEdit', icon: 'fa-solid fa-pen-to-square', + condition: target => { + const doc = getDocFromElement(target); + return !doc.hasOwnProperty('systemPath') || doc.inCollection + }, callback: target => getDocFromElement(target).sheet.render({ force: true }) } ]; @@ -409,7 +355,7 @@ export default function DHApplicationMixin(Base) { ? getDocFromElement(extensibleElement) : this.document.system.attack?.id === actionId ? this.document.system.attack - : this.document.system.actions?.find(a => a.id === actionId); + : this.document.system.actions?.get(actionId); if (!doc) return; const description = doc.system?.description ?? doc.description; @@ -435,61 +381,29 @@ export default function DHApplicationMixin(Base) { static async #createDoc(event, target) { const { documentClass, type, inVault, disabled } = target.dataset; const parentIsItem = this.document.documentName === 'Item'; - const parent = parentIsItem && documentClass === 'Item' ? null : this.document; + const parent = + parentIsItem && documentClass === 'Item' + ? type === 'action' + ? this.document.system + : null + : this.document; - if (type === 'action') { - const { type: actionType } = - (await foundry.applications.api.DialogV2.input({ - window: { title: 'Select Action Type' }, - classes: ['daggerheart', 'dh-style'], - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/actionTypes/actionType.hbs', - { - types: CONFIG.DH.ACTIONS.actionTypes, - itemName: game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectAction') - } - ), - ok: { - label: game.i18n.format('DOCUMENT.Create', { - type: game.i18n.localize('DAGGERHEART.GENERAL.Action.single') - }) - } - })) ?? {}; - if (!actionType) return; - const cls = game.system.api.models.actions.actionsTypes[actionType]; - const action = new cls( - { - _id: foundry.utils.randomID(), - type: actionType, - name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name), - ...cls.getSourceConfig(this.document) - }, - { - parent: this.document - } - ); - await this.document.update({ 'system.actions': [...this.document.system.actions, action] }); - await new DHActionConfig(this.document.system.actions[this.document.system.actions.length - 1]).render({ - force: true + const cls = + type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass); + const data = { + name: cls.defaultName({ type, parent }), + type + }; + if (inVault) data['system.inVault'] = true; + if (disabled) data.disabled = true; + + const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey }); + if (parentIsItem && type === 'feature') { + await this.document.update({ + 'system.features': this.document.system.toObject().features.concat(doc.uuid) }); - return action; - } else { - const cls = getDocumentClass(documentClass); - const data = { - name: cls.defaultName({ type, parent }), - type - }; - if (inVault) data['system.inVault'] = true; - if (disabled) data.disabled = true; - - const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey }); - if (parentIsItem && type === 'feature') { - await this.document.update({ - 'system.features': this.document.system.toObject().features.concat(doc.uuid) - }); - } - return doc; } + return doc; } /** @@ -499,12 +413,6 @@ export default function DHApplicationMixin(Base) { static #editDoc(_event, target) { const doc = getDocFromElement(target); if (doc) return doc.sheet.render({ force: true }); - - // TODO: REDO this - const { actionId } = target.closest('[data-action-id]').dataset; - const { actions, attack } = this.document.system; - const action = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId); - new DHActionConfig(action).render({ force: true }); } /** @@ -513,34 +421,10 @@ export default function DHApplicationMixin(Base) { */ static async #deleteDoc(event, target) { const doc = getDocFromElement(target); - if (doc) { if (event.shiftKey) return doc.delete(); else return await doc.deleteDialog(); } - - // TODO: REDO this - const { actionId } = target.closest('[data-action-id]').dataset; - const { actions, attack } = this.document.system; - if (attack?.id === actionId) return; - const action = actions.find(a => a.id === actionId); - - if (!event.shiftKey) { - const confirmed = await foundry.applications.api.DialogV2.confirm({ - window: { - title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { - type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`), - name: action.name - }) - }, - content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name }) - }); - if (!confirmed) return; - } - - return await this.document.update({ - 'system.actions': actions.filter(a => a.id !== action.id) - }); } /** @@ -549,13 +433,6 @@ export default function DHApplicationMixin(Base) { */ static async #toChat(_event, target) { let doc = getDocFromElement(target); - - // TODO: REDO this - if (!doc) { - const { actionId } = target.closest('[data-action-id]').dataset; - const { actions, attack } = this.document.system; - doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId); - } return doc.toChat(this.document.id); } @@ -565,29 +442,9 @@ export default function DHApplicationMixin(Base) { */ static async #useItem(event, target) { let doc = getDocFromElement(target); - // TODO: REDO this - if (!doc) { - const { actionId } = target.closest('[data-action-id]').dataset; - const { actions, attack } = this.document.system; - doc = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId); - if (this.document instanceof foundry.documents.Item && !this.document.parent) return; - } - await doc.use(event); } - /** - * Use a item - * @type {ApplicationClickAction} - */ - static async #useAction(event, target) { - const doc = getDocFromElement(target); - const { actionId } = target.closest('[data-action-id]').dataset; - const { actions, attack } = doc.system; - const action = attack?.id === actionId ? attack : actions?.find(a => a.id === actionId); - await action.use(event, doc); - } - /** * Toggle a ActiveEffect * @type {ApplicationClickAction} diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 1888be9e..52b63fd9 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -20,7 +20,6 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { submitOnChange: true }, actions: { - removeAction: DHBaseItemSheet.#removeAction, addFeature: DHBaseItemSheet.#addFeature, deleteFeature: DHBaseItemSheet.#deleteFeature, addResource: DHBaseItemSheet.#addResource, @@ -144,33 +143,6 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { /* Application Clicks Actions */ /* -------------------------------------------- */ - /** - * Remove an action from the item. - * @type {ApplicationClickAction} - */ - static async #removeAction(event, button) { - event.stopPropagation(); - const actionIndex = button.closest('[data-index]').dataset.index; - const action = this.document.system.actions[actionIndex]; - - if (!event.shiftKey) { - const confirmed = await foundry.applications.api.DialogV2.confirm({ - window: { - title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { - type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`), - name: action.name - }) - }, - content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name }) - }); - if (!confirmed) return; - } - - await this.document.update({ - 'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex)) - }); - } - /** * Add a new feature to the item, prompting the user for its type. * @type {ApplicationClickAction} diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index c1801097..2b96015f 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -77,7 +77,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo ? actor.system.attack : item.system.attack?._id === actionId ? item.system.attack - : item?.system?.actions?.find(a => a._id === actionId); + : item?.system?.actions?.get(actionId); return action; } @@ -254,8 +254,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const action = message.system.actions[Number.parseInt(event.currentTarget.dataset.index)]; const actor = game.actors.get(message.system.source.actor); - await actor.useAction(action); - } + await actor.use(action); + }; async actionUseButton(event, message) { const { moveIndex, actionIndex } = event.currentTarget.dataset; diff --git a/module/applications/ui/hotbar.mjs b/module/applications/ui/hotbar.mjs index 9d102c6c..366b930b 100644 --- a/module/applications/ui/hotbar.mjs +++ b/module/applications/ui/hotbar.mjs @@ -30,7 +30,7 @@ export default class DhHotbar extends foundry.applications.ui.Hotbar { }); } - const action = item.system.actions.find(x => x.id === actionId); + const action = item.system.actions.get(actionId); if (!action) { return ui.notifications.warn('DAGGERHEART.UI.Notifications.actionIsMissing'); } diff --git a/module/data/action/_module.mjs b/module/data/action/_module.mjs index bc2c16fd..7f7a2c82 100644 --- a/module/data/action/_module.mjs +++ b/module/data/action/_module.mjs @@ -1,4 +1,3 @@ -export * as ActionDice from './actionDice.mjs'; import AttackAction from './attackAction.mjs'; import BaseAction from './baseAction.mjs'; import BeastformAction from './beastformAction.mjs'; diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index e17c0e9d..83613a9e 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -1,11 +1,11 @@ -import { DHDamageData } from './actionDice.mjs'; +import { DHDamageData } from '../fields/action/damageField.mjs'; import DHDamageAction from './damageAction.mjs'; export default class DHAttackAction extends DHDamageAction { - static extraSchemas = [...super.extraSchemas, ...['roll', 'save']]; + static extraSchemas = [...super.extraSchemas, 'roll', 'save']; static getRollType(parent) { - return parent.type === 'weapon' ? 'attack' : 'spellcast'; + return parent.parent.type === 'weapon' ? 'attack' : 'spellcast'; } get chatTemplate() { @@ -46,8 +46,4 @@ export default class DHAttackAction extends DHDamageAction { return result; } - - // get modifiers() { - // return []; - // } } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index ad442951..7300d4ef 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -1,6 +1,6 @@ -import { DHActionDiceData, DHActionRollData, DHDamageData, DHDamageField, DHResourceData } from './actionDice.mjs'; import DhpActor from '../../documents/actor.mjs'; import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs'; +import { ActionMixin } from '../fields/actionField.mjs'; const fields = foundry.data.fields; @@ -16,12 +16,12 @@ const fields = foundry.data.fields; - Summon Action create method */ -export default class DHBaseAction extends foundry.abstract.DataModel { - static extraSchemas = []; +export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) { + static extraSchemas = ['cost', 'uses', 'range']; static defineSchema() { - return { - _id: new fields.DocumentIdField(), + const schemaFields = { + _id: new fields.DocumentIdField({ initial: () => foundry.utils.randomID() }), systemPath: new fields.StringField({ required: true, initial: 'actions' }), type: new fields.StringField({ initial: undefined, readonly: true, required: true }), name: new fields.StringField({ initial: undefined }), @@ -32,87 +32,25 @@ export default class DHBaseAction extends foundry.abstract.DataModel { choices: CONFIG.DH.ITEM.actionTypes, initial: 'action', nullable: true - }), - cost: new fields.ArrayField( - new fields.SchemaField({ - key: new fields.StringField({ - nullable: false, - required: true, - initial: 'hope' - }), - keyIsID: new fields.BooleanField(), - value: new fields.NumberField({ nullable: true, initial: 1 }), - scalable: new fields.BooleanField({ initial: false }), - step: new fields.NumberField({ nullable: true, initial: null }) - }) - ), - uses: new fields.SchemaField({ - value: new fields.NumberField({ nullable: true, initial: null }), - max: new fields.NumberField({ nullable: true, initial: null }), - recovery: new fields.StringField({ - choices: CONFIG.DH.GENERAL.refreshTypes, - initial: null, - nullable: true - }) - }), - range: new fields.StringField({ - choices: CONFIG.DH.GENERAL.range, - required: false, - blank: true - // initial: null - }), - ...this.defineExtraSchema() + }) }; + + this.extraSchemas.forEach(s => { + let clsField; + if(clsField = this.getActionField(s)) schemaFields[s] = new clsField(); + }); + + return schemaFields; } - static defineExtraSchema() { - const extraFields = { - damage: new DHDamageField(), - roll: new fields.EmbeddedDataField(DHActionRollData), - save: new fields.SchemaField({ - trait: new fields.StringField({ - nullable: true, - initial: null, - choices: CONFIG.DH.ACTOR.abilities - }), - difficulty: new fields.NumberField({ nullable: true, initial: 10, integer: true, min: 0 }), - damageMod: new fields.StringField({ - initial: CONFIG.DH.ACTIONS.damageOnSave.none.id, - choices: CONFIG.DH.ACTIONS.damageOnSave - }) - }), - target: new fields.SchemaField({ - type: new fields.StringField({ - choices: CONFIG.DH.ACTIONS.targetTypes, - initial: CONFIG.DH.ACTIONS.targetTypes.any.id, - nullable: true, - initial: null - }), - amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }) - }), - effects: new fields.ArrayField( // ActiveEffect - new fields.SchemaField({ - _id: new fields.DocumentIdField(), - onSave: new fields.BooleanField({ initial: false }) - }) - ), - healing: new fields.EmbeddedDataField(DHResourceData), - beastform: new fields.SchemaField({ - tierAccess: new fields.SchemaField({ - exact: new fields.NumberField({ integer: true, nullable: true, initial: null }) - }) - }) - }, - extraSchemas = {}; - - this.extraSchemas.forEach(s => (extraSchemas[s] = extraFields[s])); - return extraSchemas; + static getActionField(name) { + const field = game.system.api.fields.ActionFields[`${name.capitalize()}Field`]; + return fields.DataField.isPrototypeOf(field) && field; } - prepareData() {} - - get index() { - return foundry.utils.getProperty(this.parent, this.systemPath).indexOf(this); + prepareData() { + this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name); + this.img = this.img ?? this.parent?.parent?.img; } get id() { @@ -141,22 +79,21 @@ export default class DHBaseAction extends foundry.abstract.DataModel { static getSourceConfig(parent) { const updateSource = {}; - updateSource.img ??= parent?.img ?? parent?.system?.img; - if (parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) { + if (parent?.parent?.type === 'weapon' && this === game.system.api.models.actions.actionsTypes.attack) { updateSource['damage'] = { includeBase: true }; - updateSource['range'] = parent?.system?.attack?.range; + updateSource['range'] = parent?.attack?.range; updateSource['roll'] = { useDefault: true }; } else { - if (parent?.system?.trait) { + if (parent?.trait) { updateSource['roll'] = { type: this.getRollType(parent), - trait: parent.system.trait + trait: parent.trait }; } - if (parent?.system?.range) { - updateSource['range'] = parent?.system?.range; + if (parent?.range) { + updateSource['range'] = parent?.range; } } return updateSource; @@ -180,38 +117,14 @@ export default class DHBaseAction extends foundry.abstract.DataModel { async use(event, ...args) { if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); - const isFastForward = event.shiftKey || (!this.hasRoll && !this.hasSave); - // Prepare base Config - const initConfig = this.initActionConfig(event); - - // Prepare Targets - const targetConfig = this.prepareTarget(); - if (isFastForward && !targetConfig) return ui.notifications.warn('Too many targets selected for that actions.'); - - // Prepare Range - const rangeConfig = this.prepareRange(); - - // Prepare Costs - const costsConfig = this.prepareCost(); - if (isFastForward && !(await this.hasCost(costsConfig))) - return ui.notifications.warn("You don't have the resources to use that action."); - - // Prepare Uses - const usesConfig = this.prepareUse(); - if (isFastForward && !this.hasUses(usesConfig)) - return ui.notifications.warn("That action doesn't have remaining uses."); - - // Prepare Roll Data - const actorData = this.getRollData(); - - let config = { - ...initConfig, - targets: targetConfig, - range: rangeConfig, - costs: costsConfig, - uses: usesConfig, - data: actorData - }; + let config = this.prepareConfig(event); + 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; + } + } if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; @@ -243,7 +156,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel { } /* */ - initActionConfig(event) { + prepareConfig(event) { return { event, title: this.item.name, @@ -257,7 +170,9 @@ export default class DHBaseAction extends foundry.abstract.DataModel { hasHealing: !!this.healing, hasEffect: !!this.effects?.length, hasSave: this.hasSave, - selectedRollMode: game.settings.get('core', 'rollMode') + selectedRollMode: game.settings.get('core', 'rollMode'), + isFastForward: event.shiftKey, + data: this.getRollData() }; } @@ -265,36 +180,6 @@ export default class DHBaseAction extends foundry.abstract.DataModel { return !config.event.shiftKey && !this.hasRoll && (config.costs?.length || config.uses); } - prepareCost() { - const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : []; - return this.calcCosts(costs); - } - - prepareUse() { - const uses = this.uses?.max ? foundry.utils.deepClone(this.uses) : null; - if (uses && !uses.value) uses.value = 0; - return uses; - } - - prepareTarget() { - if (!this.target?.type) return []; - let targets; - if (this.target?.type === CONFIG.DH.ACTIONS.targetTypes.self.id) - targets = this.constructor.formatTarget(this.actor.token ?? this.actor.prototypeToken); - targets = Array.from(game.user.targets); - if (this.target.type !== CONFIG.DH.ACTIONS.targetTypes.any.id) { - targets = targets.filter(t => this.isTargetFriendly(t)); - if (this.target.amount && targets.length > this.target.amount) targets = []; - } - targets = targets.map(t => this.constructor.formatTarget(t)); - return targets; - } - - prepareRange() { - const range = this.range ?? null; - return range; - } - prepareRoll() { const roll = { modifiers: this.modifiers, @@ -366,108 +251,6 @@ export default class DHBaseAction extends foundry.abstract.DataModel { } /* SAVE */ - /* COST */ - - getRealCosts(costs) { - const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; - return realCosts; - } - - calcCosts(costs) { - return costs.map(c => { - c.scale = c.scale ?? 1; - c.step = c.step ?? 1; - c.total = c.value * c.scale * c.step; - c.enabled = c.hasOwnProperty('enabled') ? c.enabled : true; - return c; - }); - } - - async getResources(costs) { - const actorResources = this.actor.system.resources; - const itemResources = {}; - for (var itemResource of costs) { - if (itemResource.keyIsID) { - itemResources[itemResource.key] = { - value: this.parent.resource.value ?? 0 - }; - } - } - - return { - ...actorResources, - ...itemResources - }; - } - - /* COST */ - async hasCost(costs) { - const realCosts = this.getRealCosts(costs), - hasFearCost = realCosts.findIndex(c => c.key === 'fear'); - if (hasFearCost > -1) { - const fearCost = realCosts.splice(hasFearCost, 1)[0]; - if ( - !game.user.isGM || - fearCost.total > game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) - ) - return false; - } - - /* isReversed is a sign that the resource is inverted, IE it counts upwards instead of down */ - const resources = await this.getResources(realCosts); - return realCosts.reduce( - (a, c) => - a && resources[c.key].isReversed - ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max - : resources[c.key]?.value >= (c.total ?? c.value), - true - ); - } - - /* USES */ - calcUses(uses) { - if (!uses) return null; - return { - ...uses, - enabled: uses.hasOwnProperty('enabled') ? uses.enabled : true - }; - } - - hasUses(uses) { - if (!uses) return true; - return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max; - } - - /* TARGET */ - isTargetFriendly(target) { - const actorDisposition = this.actor.token - ? this.actor.token.disposition - : this.actor.prototypeToken.disposition, - targetDisposition = target.document.disposition; - return ( - (this.target.type === CONFIG.DH.ACTIONS.targetTypes.friendly.id && - actorDisposition === targetDisposition) || - (this.target.type === CONFIG.DH.ACTIONS.targetTypes.hostile.id && - actorDisposition + targetDisposition === 0) - ); - } - - static formatTarget(actor) { - return { - id: actor.id, - actorId: actor.actor.uuid, - name: actor.actor.name, - img: actor.actor.img, - difficulty: actor.actor.system.difficulty, - evasion: actor.actor.system.evasion - }; - } - /* TARGET */ - - /* RANGE */ - - /* RANGE */ - /* EFFECTS */ async applyEffects(event, data, targets) { targets ??= data.system.targets; @@ -552,27 +335,4 @@ export default class DHBaseAction extends foundry.abstract.DataModel { }); } } - - async toChat(origin) { - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: game.i18n.localize('DAGGERHEART.CONFIG.ActionType.action'), - origin: origin, - img: this.img, - name: this.name, - description: this.description, - actions: [] - }; - const msg = new cls({ - type: 'abilityUse', - user: game.user.id, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/ui/chat/ability-use.hbs', - systemData - ) - }); - - cls.create(msg.toObject()); - } } diff --git a/module/data/action/beastformAction.mjs b/module/data/action/beastformAction.mjs index bb926dac..ad09e2fb 100644 --- a/module/data/action/beastformAction.mjs +++ b/module/data/action/beastformAction.mjs @@ -2,7 +2,7 @@ import BeastformDialog from '../../applications/dialogs/beastformDialog.mjs'; import DHBaseAction from './baseAction.mjs'; export default class DhBeastformAction extends DHBaseAction { - static extraSchemas = ['beastform']; + static extraSchemas = [...super.extraSchemas, 'beastform']; async use(event, ...args) { const beastformConfig = this.prepareBeastformConfig(); diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 988e1844..cb813114 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -2,7 +2,7 @@ import { setsEqual } from '../../helpers/utils.mjs'; import DHBaseAction from './baseAction.mjs'; export default class DHDamageAction extends DHBaseAction { - static extraSchemas = ['damage', 'target', 'effects']; + static extraSchemas = [...super.extraSchemas, 'damage', 'target', 'effects']; getFormulaValue(part, data) { let formulaValue = part.value; @@ -49,7 +49,7 @@ export default class DHDamageAction extends DHBaseAction { const config = { title: game.i18n.format('DAGGERHEART.UI.Chat.damageRoll.title', { damage: game.i18n.localize(this.name) }), roll: formulas, - targets: systemData.targets.filter(t => t.hit) ?? data.targets, + targets: systemData.targets?.filter(t => t.hit) ?? data.targets, hasSave: this.hasSave, isCritical: systemData.roll?.isCritical ?? false, source: systemData.source, diff --git a/module/data/action/effectAction.mjs b/module/data/action/effectAction.mjs index 65425a6f..de84224c 100644 --- a/module/data/action/effectAction.mjs +++ b/module/data/action/effectAction.mjs @@ -1,7 +1,7 @@ import DHBaseAction from './baseAction.mjs'; export default class DHEffectAction extends DHBaseAction { - static extraSchemas = ['effects', 'target']; + static extraSchemas = [...super.extraSchemas, 'effects', 'target']; async trigger(event, data) { if(this.effects.length) { diff --git a/module/data/action/healingAction.mjs b/module/data/action/healingAction.mjs index b7cc4a75..b2cdf385 100644 --- a/module/data/action/healingAction.mjs +++ b/module/data/action/healingAction.mjs @@ -1,7 +1,7 @@ import DHBaseAction from './baseAction.mjs'; export default class DHHealingAction extends DHBaseAction { - static extraSchemas = ['target', 'effects', 'healing', 'roll']; + static extraSchemas = [...super.extraSchemas, 'target', 'effects', 'healing', 'roll']; static getRollType(parent) { return 'spellcast'; diff --git a/module/data/action/subDatas/rollData.mjs b/module/data/action/subDatas/rollData.mjs new file mode 100644 index 00000000..e69de29b diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 7360e42f..ac65df41 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -1,5 +1,5 @@ import DHAdversarySettings from '../../applications/sheets-configs/adversary-settings.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionField } from '../fields/actionField.mjs'; import BaseDataActor from './base.mjs'; import { resourceField, bonusField } from '../fields/actorField.mjs'; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 34a1f525..5134c5e1 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -3,7 +3,7 @@ import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import DhLevelData from '../levelData.mjs'; import BaseDataActor from './base.mjs'; import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionField } from '../fields/actionField.mjs'; export default class DhCharacter extends BaseDataActor { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character']; @@ -334,6 +334,7 @@ export default class DhCharacter extends BaseDataActor { return !primaryWeaponEquipped && !secondaryWeaponEquipped ? { ...this.attack, + uuid: this.attack.uuid, id: this.attack.id, name: this.activeBeastform ? 'DAGGERHEART.ITEMS.Beastform.attackName' : this.attack.name, img: this.activeBeastform ? 'icons/creatures/claws/claw-straight-brown.webp' : this.attack.img, diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index b7774284..8e7a6a88 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -1,7 +1,7 @@ import BaseDataActor from './base.mjs'; import DhLevelData from '../levelData.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionField, ActionsField } from '../fields/actionField.mjs'; import { adjustDice, adjustRange } from '../../helpers/utils.mjs'; import DHCompanionSettings from '../../applications/sheets-configs/companion-settings.mjs'; import { resourceField, bonusField } from '../fields/actorField.mjs'; @@ -76,7 +76,6 @@ export default class DhCompanion extends BaseDataActor { } } }), - actions: new fields.ArrayField(new ActionField()), levelData: new fields.EmbeddedDataField(DhLevelData), bonuses: new fields.SchemaField({ damage: new fields.SchemaField({ diff --git a/module/data/fields/_module.mjs b/module/data/fields/_module.mjs index 682ff1c4..8d36b76d 100644 --- a/module/data/fields/_module.mjs +++ b/module/data/fields/_module.mjs @@ -1,3 +1,6 @@ +export { ActionCollection } from './actionField.mjs'; export { default as FormulaField } from './formulaField.mjs'; export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs'; export { default as ForeignDocumentUUIDArrayField } from './foreignDocumentUUIDArrayField.mjs'; +export { default as MappingField } from './mappingField.mjs'; +export * as ActionFields from './action/_module.mjs'; diff --git a/module/data/fields/action/_module.mjs b/module/data/fields/action/_module.mjs new file mode 100644 index 00000000..f9a24d69 --- /dev/null +++ b/module/data/fields/action/_module.mjs @@ -0,0 +1,10 @@ +export { default as CostField } from './costField.mjs'; +export { default as UsesField } from './usesField.mjs'; +export { default as RangeField } from './rangeField.mjs'; +export { default as TargetField } from './targetField.mjs'; +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'; \ No newline at end of file diff --git a/module/data/fields/action/beastformField.mjs b/module/data/fields/action/beastformField.mjs new file mode 100644 index 00000000..19836f33 --- /dev/null +++ b/module/data/fields/action/beastformField.mjs @@ -0,0 +1,12 @@ +const fields = foundry.data.fields; + +export default class BeastformField extends fields.SchemaField { + constructor(options={}, context={}) { + const beastformFields = { + tierAccess: new fields.SchemaField({ + exact: new fields.NumberField({ integer: true, nullable: true, initial: null }) + }) + }; + super(beastformFields, options, context); + } +} \ No newline at end of file diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs new file mode 100644 index 00000000..e96a88e1 --- /dev/null +++ b/module/data/fields/action/costField.mjs @@ -0,0 +1,82 @@ +const fields = foundry.data.fields; + +export default class CostField extends fields.ArrayField { + constructor(options={}, context={}) { + const element = new fields.SchemaField({ + key: new fields.StringField({ + nullable: false, + required: true, + initial: 'hope' + }), + keyIsID: new fields.BooleanField(), + value: new fields.NumberField({ nullable: true, initial: 1 }), + scalable: new fields.BooleanField({ initial: false }), + step: new fields.NumberField({ nullable: true, initial: null }) + }); + super(element, options, context); + } + + static 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("You don't have the resources to use that action."); + return hasCost; + } + + static calcCosts(costs) { + return costs.map(c => { + c.scale = c.scale ?? 1; + c.step = c.step ?? 1; + c.total = c.value * c.scale * c.step; + c.enabled = c.hasOwnProperty('enabled') ? c.enabled : true; + return c; + }); + } + + static hasCost(costs) { + const realCosts = CostField.getRealCosts.call(this, costs), + hasFearCost = realCosts.findIndex(c => c.key === 'fear'); + if (hasFearCost > -1) { + const fearCost = realCosts.splice(hasFearCost, 1)[0]; + if ( + !game.user.isGM || + fearCost.total > game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + ) + return false; + } + + /* isReversed is a sign that the resource is inverted, IE it counts upwards instead of down */ + const resources = CostField.getResources.call(this, realCosts); + return realCosts.reduce( + (a, c) => + a && resources[c.key].isReversed + ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max + : resources[c.key]?.value >= (c.total ?? c.value), + true + ); + } + + static getResources(costs) { + const actorResources = this.actor.system.resources; + const itemResources = {}; + for (var itemResource of costs) { + if (itemResource.keyIsID) { + itemResources[itemResource.key] = { + value: this.parent.resource.value ?? 0 + }; + } + } + + return { + ...actorResources, + ...itemResources + }; + } + + static getRealCosts(costs) { + const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; + return realCosts; + } +} \ No newline at end of file diff --git a/module/data/action/actionDice.mjs b/module/data/fields/action/damageField.mjs similarity index 55% rename from module/data/action/actionDice.mjs rename to module/data/fields/action/damageField.mjs index 1d2b204b..f08fcd5e 100644 --- a/module/data/action/actionDice.mjs +++ b/module/data/fields/action/damageField.mjs @@ -1,62 +1,20 @@ -import FormulaField from '../fields/formulaField.mjs'; +import FormulaField from "../formulaField.mjs"; const fields = foundry.data.fields; -/* Roll Field */ - -export class DHActionRollData extends foundry.abstract.DataModel { - /** @override */ - static defineSchema() { - return { - type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }), - trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities }), - difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }), - bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }), - advState: new fields.StringField({ choices: CONFIG.DH.ACTIONS.advandtageState, initial: 'neutral' }), - diceRolling: new fields.SchemaField({ - multiplier: new fields.StringField({ - choices: CONFIG.DH.GENERAL.diceSetNumbers, - initial: 'prof', - label: 'Dice Number' - }), - flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }), - dice: new fields.StringField({ - choices: CONFIG.DH.GENERAL.diceTypes, - initial: 'd6', - label: 'Dice Type' - }), - compare: new fields.StringField({ - choices: CONFIG.DH.ACTIONS.diceCompare, - initial: 'above', - label: 'Should be' - }), - treshold: new fields.NumberField({ initial: 1, integer: true, min: 1, label: 'Treshold' }) - }), - useDefault: new fields.BooleanField({ initial: false }) +export default class DamageField extends fields.SchemaField { + constructor(options, context = {}) { + const damageFields = { + parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)), + includeBase: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label' + }) }; - } - - getFormula() { - if (!this.type) return; - let formula = ''; - switch (this.type) { - case 'diceSet': - const multiplier = - this.diceRolling.multiplier === 'flat' - ? this.diceRolling.flatMultiplier - : `@${this.diceRolling.multiplier}`; - formula = `${multiplier}${this.diceRolling.dice}cs${CONFIG.DH.ACTIONS.diceCompare[this.diceRolling.compare].operator}${this.diceRolling.treshold}`; - break; - default: - formula = ''; - break; - } - return formula; + super(damageFields, options, context); } } -/* Damage & Healing Field */ - export class DHActionDiceData extends foundry.abstract.DataModel { /** @override */ static defineSchema() { @@ -83,19 +41,6 @@ export class DHActionDiceData extends foundry.abstract.DataModel { } } -export class DHDamageField extends fields.SchemaField { - constructor(options, context = {}) { - const damageFields = { - parts: new fields.ArrayField(new fields.EmbeddedDataField(DHDamageData)), - includeBase: new fields.BooleanField({ - initial: false, - label: 'DAGGERHEART.ACTIONS.Settings.includeBase.label' - }) - }; - super(damageFields, options, context); - } -} - export class DHResourceData extends foundry.abstract.DataModel { /** @override */ static defineSchema() { @@ -136,4 +81,4 @@ export class DHDamageData extends DHResourceData { ) }; } -} +} \ No newline at end of file diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs new file mode 100644 index 00000000..968d753c --- /dev/null +++ b/module/data/fields/action/effectsField.mjs @@ -0,0 +1,11 @@ +const fields = foundry.data.fields; + +export default class EffectsField extends fields.ArrayField { + constructor(options={}, context={}) { + const element = new fields.SchemaField({ + _id: new fields.DocumentIdField(), + onSave: new fields.BooleanField({ initial: false }) + }); + super(element, options, context); + } +} \ No newline at end of file diff --git a/module/data/fields/action/healingField.mjs b/module/data/fields/action/healingField.mjs new file mode 100644 index 00000000..926663b9 --- /dev/null +++ b/module/data/fields/action/healingField.mjs @@ -0,0 +1,9 @@ +import { DHDamageData } from "./damageField.mjs"; + +const fields = foundry.data.fields; + +export default class HealingField extends fields.EmbeddedDataField { + constructor(options, context = {}) { + super(DHDamageData, options, context); + } +} \ No newline at end of file diff --git a/module/data/fields/action/rangeField.mjs b/module/data/fields/action/rangeField.mjs new file mode 100644 index 00000000..30469efa --- /dev/null +++ b/module/data/fields/action/rangeField.mjs @@ -0,0 +1,16 @@ +const fields = foundry.data.fields; + +export default class RangeField extends fields.StringField { + constructor(context={}) { + const options = { + choices: CONFIG.DH.GENERAL.range, + required: false, + blank: true + }; + super(options, context); + } + + static prepareConfig(config) { + return true; + } +} \ No newline at end of file diff --git a/module/data/fields/action/rollField.mjs b/module/data/fields/action/rollField.mjs new file mode 100644 index 00000000..597e804b --- /dev/null +++ b/module/data/fields/action/rollField.mjs @@ -0,0 +1,58 @@ +const fields = foundry.data.fields; + +export class DHActionRollData extends foundry.abstract.DataModel { + /** @override */ + static defineSchema() { + return { + type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }), + trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities }), + difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }), + bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }), + advState: new fields.StringField({ choices: CONFIG.DH.ACTIONS.advandtageState, initial: 'neutral' }), + diceRolling: new fields.SchemaField({ + multiplier: new fields.StringField({ + choices: CONFIG.DH.GENERAL.diceSetNumbers, + initial: 'prof', + label: 'Dice Number' + }), + flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }), + dice: new fields.StringField({ + choices: CONFIG.DH.GENERAL.diceTypes, + initial: 'd6', + label: 'Dice Type' + }), + compare: new fields.StringField({ + choices: CONFIG.DH.ACTIONS.diceCompare, + initial: 'above', + label: 'Should be' + }), + treshold: new fields.NumberField({ initial: 1, integer: true, min: 1, label: 'Treshold' }) + }), + useDefault: new fields.BooleanField({ initial: false }) + }; + } + + getFormula() { + if (!this.type) return; + let formula = ''; + switch (this.type) { + case 'diceSet': + const multiplier = + this.diceRolling.multiplier === 'flat' + ? this.diceRolling.flatMultiplier + : `@${this.diceRolling.multiplier}`; + formula = `${multiplier}${this.diceRolling.dice}cs${CONFIG.DH.ACTIONS.diceCompare[this.diceRolling.compare].operator}${this.diceRolling.treshold}`; + break; + default: + formula = ''; + break; + } + return formula; + } +} + +export default class RollField extends fields.EmbeddedDataField { + constructor(options, context = {}) { + super(DHActionRollData, options, context); + } +} \ No newline at end of file diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs new file mode 100644 index 00000000..c70ba3b1 --- /dev/null +++ b/module/data/fields/action/saveField.mjs @@ -0,0 +1,19 @@ +const fields = foundry.data.fields; + +export default class SaveField extends fields.SchemaField { + constructor(options={}, context={}) { + const saveFields = { + trait: new fields.StringField({ + nullable: true, + initial: null, + choices: CONFIG.DH.ACTOR.abilities + }), + difficulty: new fields.NumberField({ nullable: true, initial: 10, integer: true, min: 0 }), + damageMod: new fields.StringField({ + initial: CONFIG.DH.ACTIONS.damageOnSave.none.id, + choices: CONFIG.DH.ACTIONS.damageOnSave + }) + }; + super(saveFields, options, context); + } +} \ No newline at end of file diff --git a/module/data/fields/action/targetField.mjs b/module/data/fields/action/targetField.mjs new file mode 100644 index 00000000..647cf51c --- /dev/null +++ b/module/data/fields/action/targetField.mjs @@ -0,0 +1,62 @@ +const fields = foundry.data.fields; + +export default class TargetField extends fields.SchemaField { + constructor(options={}, context={}) { + const targetFields = { + type: new fields.StringField({ + choices: CONFIG.DH.ACTIONS.targetTypes, + initial: CONFIG.DH.ACTIONS.targetTypes.any.id, + nullable: true, + initial: null + }), + amount: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }) + }; + super(targetFields, options, context); + } + + static prepareConfig(config) { + if (!this.target?.type) return []; + let targets; + if (this.target?.type === CONFIG.DH.ACTIONS.targetTypes.self.id) + targets = TargetField.formatTarget.call(this, this.actor.token ?? this.actor.prototypeToken); + targets = Array.from(game.user.targets); + if (this.target.type !== CONFIG.DH.ACTIONS.targetTypes.any.id) { + targets = targets.filter(t => TargetField.isTargetFriendly.call(this, t)); + 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; + } + + 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, + targetDisposition = target.document.disposition; + return ( + (this.target.type === CONFIG.DH.ACTIONS.targetTypes.friendly.id && + actorDisposition === targetDisposition) || + (this.target.type === CONFIG.DH.ACTIONS.targetTypes.hostile.id && + actorDisposition + targetDisposition === 0) + ); + } + + static formatTarget(actor) { + return { + id: actor.id, + actorId: actor.actor.uuid, + name: actor.actor.name, + img: actor.actor.img, + difficulty: actor.actor.system.difficulty, + evasion: actor.actor.system.evasion + }; + } +} \ No newline at end of file diff --git a/module/data/fields/action/usesField.mjs b/module/data/fields/action/usesField.mjs new file mode 100644 index 00000000..5f05adcb --- /dev/null +++ b/module/data/fields/action/usesField.mjs @@ -0,0 +1,39 @@ +const fields = foundry.data.fields; + +export default class UsesField extends fields.SchemaField { + constructor(options={}, context={}) { + const usesFields = { + value: new fields.NumberField({ nullable: true, initial: null }), + max: new fields.NumberField({ nullable: true, initial: null }), + recovery: new fields.StringField({ + choices: CONFIG.DH.GENERAL.refreshTypes, + initial: null, + nullable: true + }) + }; + super(usesFields, options, context); + } + + static 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("That action doesn't have remaining uses."); + return hasUses; + } + + static calcUses(uses) { + if (!uses) return null; + return { + ...uses, + enabled: uses.hasOwnProperty('enabled') ? uses.enabled : true + }; + } + + static hasUses(uses) { + if (!uses) return true; + return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max; + } +} \ No newline at end of file diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index b83acc2e..bc28943a 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -1,4 +1,85 @@ -export default class ActionField extends foundry.data.fields.ObjectField { +import DHActionConfig from "../../applications/sheets-configs/action-config.mjs"; +import MappingField from "./mappingField.mjs"; + +/** + * Specialized collection type for stored actions. + * @param {DataModel} model The parent DataModel to which this ActionCollection belongs. + * @param {Action[]} entries The actions to store. + */ +export class ActionCollection extends Collection { + constructor(model, entries) { + super(); + this.#model = model; + for ( const entry of entries ) { + if ( !(entry instanceof game.system.api.models.actions.actionsTypes.base) ) continue; + this.set(entry._id, entry); + } + } + + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /** + * The parent DataModel to which this ActionCollection belongs. + * @type {DataModel} + */ + #model; + + /* -------------------------------------------- */ + + /* -------------------------------------------- */ + /* Methods */ + /* -------------------------------------------- */ + + /* -------------------------------------------- */ + + /** + * Test the given predicate against every entry in the Collection. + * @param {function(*, number, ActionCollection): boolean} predicate The predicate. + * @returns {boolean} + */ + every(predicate) { + return this.reduce((pass, v, i) => pass && predicate(v, i, this), true); + } + + /* -------------------------------------------- */ + + /** + * Convert the ActionCollection to an array of simple objects. + * @param {boolean} [source=true] Draw data for contained Documents from the underlying data source? + * @returns {object[]} The extracted array of primitive objects. + */ + toObject(source=true) { + return this.map(doc => doc.toObject(source)); + } +} + +/* -------------------------------------------- */ + +/** + * Field that stores actions. + */ +export class ActionsField extends MappingField { + constructor(options) { + super(new ActionField(), options); + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + initialize(value, model, options) { + const actions = Object.values(super.initialize(value, model, options)); + return new ActionCollection(model, actions); + } +} + +/* -------------------------------------------- */ + +/** + * Field that stores action data and swaps class based on action type. + */ +export class ActionField extends foundry.data.fields.ObjectField { getModel(value) { return game.system.api.models.actions.actionsTypes[value.type] ?? game.system.api.models.actions.actionsTypes.attack; } @@ -35,3 +116,141 @@ export default class ActionField extends foundry.data.fields.ObjectField { if (cls) cls.migrateDataSafe(fieldData); } } + +/* -------------------------------------------- */ + +export function ActionMixin(Base) { + class Action extends Base { + static metadata = Object.freeze({ + name: "Action", + label: "DAGGERHEART.GENERAL.Action.single", + sheetClass: DHActionConfig + }); + + static _sheets = new Map(); + + static get documentName() { + return this.metadata.name; + } + + get documentName() { + return this.constructor.documentName; + } + + static defaultName() { + return this.documentName; + } + + get relativeUUID() { + return `.Item.${this.item.id}.Action.${this.id}`; + } + + get uuid() { + return `${this.item.uuid}.${this.documentName}.${this.id}`; + } + + get sheet() { + if(!this.constructor._sheets.has(this.uuid)) { + const sheet = new this.constructor.metadata.sheetClass(this); + this.constructor._sheets.set(this.uuid, sheet); + } + return this.constructor._sheets.get(this.uuid); + } + + get inCollection() { + return foundry.utils.getProperty(this.parent, this.systemPath) instanceof Collection; + } + + static async create(data, operation={}) { + const { parent, renderSheet } = operation; + let { type } = data; + if(!type || !game.system.api.models.actions.actionsTypes[type]) { + ({ type } = + (await foundry.applications.api.DialogV2.input({ + window: { title: 'Select Action Type' }, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/actionTypes/actionType.hbs', + { types: CONFIG.DH.ACTIONS.actionTypes } + ), + ok: { + label: game.i18n.format('DOCUMENT.Create', { + type: game.i18n.localize('DAGGERHEART.GENERAL.Action.single') + }) + } + })) ?? {}); + } + if (!type) return; + + const cls = game.system.api.models.actions.actionsTypes[type]; + const action = new cls( + { + type, + ...cls.getSourceConfig(parent) + }, + { + parent + } + ); + const created = await parent.parent.update({ [`system.actions.${action.id}`]: action.toObject() }); + const newAction = parent.actions.get(action.id); + if(!newAction) return null; + if( renderSheet ) newAction.sheet.render({ force: true }); + return newAction; + } + + async update(updates, options={}) { + const path = this.inCollection ? `system.${this.systemPath}.${this.id}` : `system.${this.systemPath}`, + result = await this.item.update({[path]: updates}, options); + return this.inCollection ? foundry.utils.getProperty(result, `system.${this.systemPath}`).get(this.id) : foundry.utils.getProperty(result, `system.${this.systemPath}`); + } + + delete() { + if(!this.inCollection) return this.item; + const action = foundry.utils.getProperty(this.item, `system.${this.systemPath}`)?.get(this.id); + if ( !action ) return this.item; + this.item.update({ [`system.${this.systemPath}.-=${this.id}`]: null }); + this.constructor._sheets.get(this.uuid)?.close(); + } + + async deleteDialog() { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`), + name: this.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { + name: this.name + }) + }); + if (!confirmed) return; + return this.delete(); + } + + async toChat(origin) { + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: game.i18n.localize('DAGGERHEART.CONFIG.ActionType.action'), + origin: origin, + img: this.img, + name: this.name, + description: this.description, + actions: [] + }; + const msg = { + type: 'abilityUse', + user: game.user.id, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/ability-use.hbs', + systemData + ) + }; + + cls.create(msg); + } + } + + return Action; +} diff --git a/module/data/fields/mappingField.mjs b/module/data/fields/mappingField.mjs new file mode 100644 index 00000000..83504536 --- /dev/null +++ b/module/data/fields/mappingField.mjs @@ -0,0 +1,128 @@ +/** + * A subclass of ObjectField that represents a mapping of keys to the provided DataField type. + * + * @param {DataField} model The class of DataField which should be embedded in this field. + * @param {MappingFieldOptions} [options={}] Options which configure the behavior of the field. + * @property {string[]} [initialKeys] Keys that will be created if no data is provided. + * @property {MappingFieldInitialValueBuilder} [initialValue] Function to calculate the initial value for a key. + * @property {boolean} [initialKeysOnly=false] Should the keys in the initialized data be limited to the keys provided + * by `options.initialKeys`? + */ +export default class MappingField extends foundry.data.fields.ObjectField { + constructor(model, options) { + if ( !(model instanceof foundry.data.fields.DataField) ) { + throw new Error("MappingField must have a DataField as its contained element"); + } + super(options); + + /** + * The embedded DataField definition which is contained in this field. + * @type {DataField} + */ + this.model = model; + model.parent = this; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + static get _defaults() { + return foundry.utils.mergeObject(super._defaults, { + initialKeys: null, + initialValue: null, + initialKeysOnly: false + }); + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + _cleanType(value, options) { + Object.entries(value).forEach(([k, v]) => { + if ( k.startsWith("-=") ) return; + value[k] = this.model.clean(v, options); + }); + return value; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + getInitialValue(data) { + let keys = this.initialKeys; + const initial = super.getInitialValue(data); + if ( !keys || !foundry.utils.isEmpty(initial) ) return initial; + if ( !(keys instanceof Array) ) keys = Object.keys(keys); + for ( const key of keys ) initial[key] = this._getInitialValueForKey(key); + return initial; + } + + /* -------------------------------------------- */ + + /** + * Get the initial value for the provided key. + * @param {string} key Key within the object being built. + * @param {object} [object] Any existing mapping data. + * @returns {*} Initial value based on provided field type. + */ + _getInitialValueForKey(key, object) { + const initial = this.model.getInitialValue(); + return this.initialValue?.(key, initial, object) ?? initial; + } + + /* -------------------------------------------- */ + + /** @override */ + _validateType(value, options={}) { + if ( foundry.utils.getType(value) !== "Object" ) throw new Error("must be an Object"); + const errors = this._validateValues(value, options); + if ( !foundry.utils.isEmpty(errors) ) { + const failure = new foundry.data.validation.DataModelValidationFailure(); + failure.elements = Object.entries(errors).map(([id, failure]) => ({ id, failure })); + throw failure.asError(); + } + } + + /* -------------------------------------------- */ + + /** + * Validate each value of the object. + * @param {object} value The object to validate. + * @param {object} options Validation options. + * @returns {Record} An object of value-specific errors by key. + */ + _validateValues(value, options) { + const errors = {}; + for ( const [k, v] of Object.entries(value) ) { + if ( k.startsWith("-=") ) continue; + const error = this.model.validate(v, options); + if ( error ) errors[k] = error; + } + return errors; + } + + /* -------------------------------------------- */ + + /** @override */ + initialize(value, model, options={}) { + if ( !value ) return value; + const obj = {}; + const initialKeys = (this.initialKeys instanceof Array) ? this.initialKeys : Object.keys(this.initialKeys ?? {}); + const keys = this.initialKeysOnly ? initialKeys : Object.keys(value); + for ( const key of keys ) { + const data = value[key] ?? this._getInitialValueForKey(key, value); + obj[key] = this.model.initialize(data, model, options); + } + return obj; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + _getField(path) { + if ( path.length === 0 ) return this; + else if ( path.length === 1 ) return this.model; + path.shift(); + return this.model._getField(path); + } +} \ No newline at end of file diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 8522c8fc..2639ae1e 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -1,7 +1,6 @@ import AttachableItem from './attachableItem.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionsField } from '../fields/actionField.mjs'; import { armorFeatures } from '../../config/itemConfig.mjs'; -import { actionsTypes } from '../action/_module.mjs'; export default class DHArmor extends AttachableItem { /** @inheritDoc */ @@ -10,7 +9,8 @@ export default class DHArmor extends AttachableItem { label: 'TYPES.Item.armor', type: 'armor', hasDescription: true, - isInventoryItem: true + isInventoryItem: true, + hasActions: true }); } @@ -39,8 +39,7 @@ export default class DHArmor extends AttachableItem { baseThresholds: new fields.SchemaField({ major: new fields.NumberField({ integer: true, initial: 0 }), severe: new fields.NumberField({ integer: true, initial: 0 }) - }), - actions: new fields.ArrayField(new ActionField()) + }) }; } @@ -65,7 +64,10 @@ export default class DHArmor extends AttachableItem { actionIds.push(...feature.actionIds); } await this.parent.deleteEmbeddedDocuments('ActiveEffect', effectIds); - changes.system.actions = this.actions.filter(x => !actionIds.includes(x._id)); + changes.system.actions = actionIds.reduce((acc, id) => { + acc[`-=${id}`] = null; + return acc; + }, {}); for (var feature of added) { const featureData = armorFeatures[feature.value]; @@ -79,17 +81,38 @@ export default class DHArmor extends AttachableItem { ]); feature.effectIds = embeddedItems.map(x => x.id); } + + const newActions = {}; if (featureData.actions?.length > 0) { - const newActions = featureData.actions.map(action => { - const cls = actionsTypes[action.type]; - return new cls( - { ...action, _id: foundry.utils.randomID(), name: game.i18n.localize(action.name) }, + for (let action of featureData.actions) { + const embeddedEffects = await this.parent.createEmbeddedDocuments( + 'ActiveEffect', + (action.effects ?? []).map(effect => ({ + ...effect, + transfer: false, + name: game.i18n.localize(effect.name), + description: game.i18n.localize(effect.description) + })) + ); + + const cls = game.system.api.models.actions.actionsTypes[action.type]; + const actionId = foundry.utils.randomID(); + newActions[actionId] = new cls( + { + ...cls.getSourceConfig(this), + ...action, + _id: actionId, + name: game.i18n.localize(action.name), + description: game.i18n.localize(action.description), + effects: embeddedEffects.map(x => ({ _id: x.id })) + }, { parent: this } ); - }); - changes.system.actions = [...this.actions, ...newActions]; - feature.actionIds = newActions.map(x => x._id); + } } + + changes.system.actions = newActions; + feature.actionIds = Object.keys(newActions); } } } diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 24e5e0cc..0b2d8ddf 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -8,6 +8,8 @@ * @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item */ +import { ActionsField } from "../fields/actionField.mjs"; + const fields = foundry.data.fields; export default class BaseDataItem extends foundry.abstract.TypeDataModel { @@ -21,7 +23,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { hasDescription: false, hasResource: false, isQuantifiable: false, - isInventoryItem: false + isInventoryItem: false, + hasActions: false }; } @@ -69,6 +72,9 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { if (this.metadata.isQuantifiable) schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true }); + if (this.metadata.hasActions) + schema.actions = new ActionsField() + return schema; } diff --git a/module/data/item/consumable.mjs b/module/data/item/consumable.mjs index 3e70f97a..cd192dfe 100644 --- a/module/data/item/consumable.mjs +++ b/module/data/item/consumable.mjs @@ -1,5 +1,5 @@ import BaseDataItem from './base.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionField } from '../fields/actionField.mjs'; export default class DHConsumable extends BaseDataItem { /** @inheritDoc */ @@ -9,7 +9,8 @@ export default class DHConsumable extends BaseDataItem { type: 'consumable', hasDescription: true, isQuantifiable: true, - isInventoryItem: true + isInventoryItem: true, + hasActions: true }); } @@ -18,8 +19,7 @@ export default class DHConsumable extends BaseDataItem { const fields = foundry.data.fields; return { ...super.defineSchema(), - consumeOnUse: new fields.BooleanField({ initial: false }), - actions: new fields.ArrayField(new ActionField()) + consumeOnUse: new fields.BooleanField({ initial: false }) }; } } diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index df60b9d1..d366b7a0 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -1,5 +1,5 @@ import BaseDataItem from './base.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionField } from '../fields/actionField.mjs'; export default class DHDomainCard extends BaseDataItem { /** @inheritDoc */ @@ -8,7 +8,8 @@ export default class DHDomainCard extends BaseDataItem { label: 'TYPES.Item.domainCard', type: 'domainCard', hasDescription: true, - hasResource: true + hasResource: true, + hasActions: true }); } @@ -29,8 +30,7 @@ export default class DHDomainCard extends BaseDataItem { required: true, initial: CONFIG.DH.DOMAIN.cardTypes.ability.id }), - inVault: new fields.BooleanField({ initial: false }), - actions: new fields.ArrayField(new ActionField()) + inVault: new fields.BooleanField({ initial: false }) }; } diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index 62b955e9..93a2c0bd 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -1,5 +1,5 @@ import BaseDataItem from './base.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionField, ActionsField } from '../fields/actionField.mjs'; export default class DHFeature extends BaseDataItem { /** @inheritDoc */ @@ -8,7 +8,8 @@ export default class DHFeature extends BaseDataItem { label: 'TYPES.Item.feature', type: 'feature', hasDescription: true, - hasResource: true + hasResource: true, + hasActions: true }); } @@ -24,8 +25,7 @@ export default class DHFeature extends BaseDataItem { }), subType: new fields.StringField({ choices: CONFIG.DH.ITEM.featureSubTypes, nullable: true, initial: null }), originId: new fields.StringField({ nullable: true, initial: null }), - identifier: new fields.StringField(), - actions: new fields.ArrayField(new ActionField()) + identifier: new fields.StringField() }; } diff --git a/module/data/item/miscellaneous.mjs b/module/data/item/miscellaneous.mjs index cad07f48..c40ac60b 100644 --- a/module/data/item/miscellaneous.mjs +++ b/module/data/item/miscellaneous.mjs @@ -1,5 +1,5 @@ import BaseDataItem from './base.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionField } from '../fields/actionField.mjs'; export default class DHMiscellaneous extends BaseDataItem { /** @inheritDoc */ @@ -9,16 +9,15 @@ export default class DHMiscellaneous extends BaseDataItem { type: 'miscellaneous', hasDescription: true, isQuantifiable: true, - isInventoryItem: true + isInventoryItem: true, + hasActions: true }); } /** @inheritDoc */ static defineSchema() { - const fields = foundry.data.fields; return { - ...super.defineSchema(), - actions: new fields.ArrayField(new ActionField()) + ...super.defineSchema() }; } } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 0d0c7f76..71d1e08d 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -1,6 +1,5 @@ import AttachableItem from './attachableItem.mjs'; -import { actionsTypes } from '../action/_module.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionsField, ActionField } from '../fields/actionField.mjs'; export default class DHWeapon extends AttachableItem { /** @inheritDoc */ @@ -9,8 +8,8 @@ export default class DHWeapon extends AttachableItem { label: 'TYPES.Item.weapon', type: 'weapon', hasDescription: true, - isInventoryItem: true - // hasInitialAction: true + isInventoryItem: true, + hasActions: true }); } @@ -64,8 +63,7 @@ export default class DHWeapon extends AttachableItem { ] } } - }), - actions: new fields.ArrayField(new ActionField()) + }) }; } @@ -95,7 +93,10 @@ export default class DHWeapon extends AttachableItem { } await this.parent.deleteEmbeddedDocuments('ActiveEffect', removedEffectsUpdate); - changes.system.actions = this.actions.filter(x => !removedActionsUpdate.includes(x._id)); + changes.system.actions = removedActionsUpdate.reduce((acc, id) => { + acc[`-=${id}`] = null; + return acc; + }, {}); for (let weaponFeature of added) { const featureData = CONFIG.DH.ITEM.weaponFeatures[weaponFeature.value]; @@ -110,7 +111,7 @@ export default class DHWeapon extends AttachableItem { weaponFeature.effectIds = embeddedItems.map(x => x.id); } - const newActions = []; + const newActions = {}; if (featureData.actions?.length > 0) { for (let action of featureData.actions) { const embeddedEffects = await this.parent.createEmbeddedDocuments( @@ -122,24 +123,25 @@ export default class DHWeapon extends AttachableItem { description: game.i18n.localize(effect.description) })) ); - const cls = actionsTypes[action.type]; - newActions.push( - new cls( - { - ...action, - _id: foundry.utils.randomID(), - name: game.i18n.localize(action.name), - description: game.i18n.localize(action.description), - effects: embeddedEffects.map(x => ({ _id: x.id })) - }, - { parent: this } - ) + + const cls = game.system.api.models.actions.actionsTypes[action.type]; + const actionId = foundry.utils.randomID(); + newActions[actionId] = new cls( + { + ...cls.getSourceConfig(this), + ...action, + _id: actionId, + name: game.i18n.localize(action.name), + description: game.i18n.localize(action.description), + effects: embeddedEffects.map(x => ({ _id: x.id })) + }, + { parent: this } ); } } - changes.system.actions = [...this.actions, ...newActions]; - weaponFeature.actionIds = newActions.map(x => x._id); + changes.system.actions = newActions; + weaponFeature.actionIds = Object.keys(newActions); } } } diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 34fe77d7..4f5d9172 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -12,6 +12,7 @@ export default class DamageRoll extends DHRoll { static async buildEvaluate(roll, config = {}, message = {}) { if (config.evaluate !== false) { + if(config.dialog.configure === false) roll.constructFormula(config); for (const roll of config.roll) await roll.roll.evaluate(); } roll._evaluated = true; diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 52759316..4bc1a5bd 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -56,13 +56,13 @@ export default class DHRoll extends Roll { } // Create Chat Message + if (roll instanceof CONFIG.Dice.daggerheart.DamageRoll && Object.values(config.roll)?.length) { + const pool = foundry.dice.terms.PoolTerm.fromRolls( + Object.values(config.roll).flatMap(r => r.parts.map(p => p.roll)) + ); + roll = Roll.fromTerms([pool]); + } if (config.source?.message) { - if (Object.values(config.roll)?.length) { - const pool = foundry.dice.terms.PoolTerm.fromRolls( - Object.values(config.roll).flatMap(r => r.parts.map(p => p.roll)) - ); - roll = Roll.fromTerms([pool]); - } if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); } else config.message = await this.toMessage(roll, config); } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 490f53eb..f0b374f7 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -23,6 +23,23 @@ export default class DhpActor extends Actor { return this.system.metadata.isNPC; } + /** @inheritDoc */ + getEmbeddedDocument(embeddedName, id, options) { + let doc; + switch ( embeddedName ) { + case "Action": + doc = this.system.actions?.get(id); + if(!doc && this.system.attack?.id === id) doc = this.system.attack; + break; + default: + return super.getEmbeddedDocument(embeddedName, id, options); + } + if ( options?.strict && !doc ) { + throw new Error(`The key ${id} does not exist in the ${embeddedName} Collection`); + } + return doc; + } + async _preCreate(data, options, user) { if ((await super._preCreate(data, options, user)) === false) return false; diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 6c3732db..4bad4fdc 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -9,6 +9,23 @@ export default class DHItem extends foundry.documents.Item { for (const action of this.system.actions ?? []) action.prepareData(); } + /** @inheritDoc */ + getEmbeddedDocument(embeddedName, id, options) { + let doc; + switch (embeddedName) { + case 'Action': + doc = this.system.actions?.get(id); + if (!doc && this.system.attack?.id === id) doc = this.system.attack; + break; + default: + return super.getEmbeddedDocument(embeddedName, id, options); + } + if (options?.strict && !doc) { + throw new Error(`The key ${id} does not exist in the ${embeddedName} Collection`); + } + return doc; + } + /** * @inheritdoc * @param {object} options - Options which modify the getRollData method. @@ -106,10 +123,10 @@ export default class DHItem extends foundry.documents.Item { } async use(event) { - const actions = this.system.actionsList; - if (actions?.length) { - let action = actions[0]; - if (actions.length > 1 && !event?.shiftKey) { + const actions = new Set(this.system.actionsList); + if (actions?.size) { + let action = actions.first(); + if (actions.size > 1 && !event?.shiftKey) { // Actions Choice Dialog action = await this.selectActionDialog(event); } diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index 8e1c729e..71dd71d2 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -4,19 +4,18 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti let html = options.html; if (element.dataset.tooltip?.startsWith('#item#')) { - const splitValues = element.dataset.tooltip.slice(6).split('#action#'); - const itemUuid = splitValues[0]; - const actionId = splitValues.length > 1 ? splitValues[1] : null; - - const baseItem = await foundry.utils.fromUuid(itemUuid); - const item = actionId ? baseItem.system.actions.find(x => x.id === actionId) : baseItem; + const itemUuid = element.dataset.tooltip.slice(6); + const item = await foundry.utils.fromUuid(itemUuid); if (item) { - const type = actionId ? 'action' : item.type; - const description = await TextEditor.enrichHTML(item.system.description); - for (let feature of item.system.features) { - feature.system.enrichedDescription = await TextEditor.enrichHTML(feature.system.description); + const isAction = item instanceof game.system.api.models.actions.actionsTypes.base; + const description = await TextEditor.enrichHTML(isAction ? item.description : item.system.description); + if (item.system?.features) { + for (let feature of item.system.features) { + feature.system.enrichedDescription = await TextEditor.enrichHTML(feature.system.description); + } } + const type = isAction ? 'action' : item.type; html = await foundry.applications.handlebars.renderTemplate( `systems/daggerheart/templates/ui/tooltip/${type}.hbs`, { diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 0c919191..9e769d2e 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -9,7 +9,8 @@ export default class RegisterHandlebarsHelpers { damageFormula: this.damageFormula, damageSymbols: this.damageSymbols, rollParsed: this.rollParsed, - hasProperty: foundry.utils.hasProperty + hasProperty: foundry.utils.hasProperty, + setVar: this.setVar }); } static add(a, b) { @@ -50,4 +51,8 @@ export default class RegisterHandlebarsHelpers { const result = itemAbleRollParse(value, actor, item); return isNumerical && !result ? 0 : result; } + + static setVar(name, value, context) { + this[name] = value; + } } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 7e73695e..5ee52018 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -96,7 +96,7 @@ export const tagifyElement = (element, options, onChange, tagifyOptions = {}) => mapValueTo: 'name', searchKeys: ['name'], enabled: 0, - maxItems: 20, + maxItems: 100, closeOnSelect: true, highlightFirst: false }, diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 29a2c1dd..4c319ed5 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -346,6 +346,12 @@ &:has(.list-w-img) { gap: 0; } + + &.no-style { + border-width: 0; + margin: 0; + padding: 0; + } } .two-columns { diff --git a/templates/actionTypes/damage.hbs b/templates/actionTypes/damage.hbs index ad45fc33..9ae25e0e 100644 --- a/templates/actionTypes/damage.hbs +++ b/templates/actionTypes/damage.hbs @@ -4,90 +4,78 @@ {{localize "DAGGERHEART.GENERAL.damage"}} {{#unless (eq path 'system.attack.')}}{{/unless}} - {{#unless (or @root.isNPC path)}} - {{#if @root.hasBaseDamage}} - {{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase name="damage.includeBase" classes="checkbox" localize=true }} - {{/if}} - {{/unless}} + {{#if @root.hasBaseDamage}} + {{formField @root.fields.damage.fields.includeBase value=@root.source.damage.includeBase name="damage.includeBase" classes="checkbox" localize=true }} + {{/if}} {{#each source.parts as |dmg index|}} - {{#if (or @root.isNPC ../path)}} - {{formField ../fields.value.fields.custom.fields.enabled value=dmg.value.custom.enabled name=(concat ../path "damage.parts." index ".value.custom.enabled") classes="checkbox"}} - - {{#if dmg.value.custom.enabled}} - {{formField ../fields.value.fields.custom.fields.formula value=dmg.value.custom.formula name=(concat ../path "damage.parts." index ".value.custom.formula") localize=true}} - {{else}} -
- {{#if @root.isNPC}}{{formField ../fields.value.fields.flatMultiplier value=dmg.value.flatMultiplier name=(concat ../path "damage.parts." index ".value.flatMultiplier") label="DAGGERHEART.ACTIONS.Settings.multiplier" classes="inline-child" localize=true }}{{/if}} - {{formField ../fields.value.fields.dice value=dmg.value.dice name=(concat ../path "damage.parts." index ".value.dice") classes="inline-child"}} - {{formField ../fields.value.fields.bonus value=dmg.value.bonus name=(concat ../path "damage.parts." index ".value.bonus") localize=true classes="inline-child"}} -
- {{/if}} -
- {{formField ../fields.applyTo value=dmg.applyTo name=(concat ../path "damage.parts." realIndex ".applyTo") localize=true}} - {{#if (eq dmg.applyTo 'hitPoints')}} - {{formField ../fields.type value=dmg.type name=(concat ../path "damage.parts." index ".type") localize=true}} - {{/if}} -
- {{#if ../horde}} -
- {{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}} -
- {{formField ../fields.valueAlt.fields.flatMultiplier value=dmg.valueAlt.flatMultiplier name=(concat ../path "damage.parts." index ".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." index ".valueAlt.dice") classes="inline-child"}} - {{formField ../fields.valueAlt.fields.bonus value=dmg.valueAlt.bonus name=(concat ../path "damage.parts." index ".valueAlt.bonus") localize=true classes="inline-child"}} -
-
- {{/if}} + {{#if (and @root.hasBaseDamage @root.source.damage.includeBase)}} + {{setVar 'realIndex' (add index -1)}} {{else}} - {{#with (@root.getRealIndex index) as | realIndex |}} -
- - {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base))}} - {{formField ../../fields.resultBased value=dmg.resultBased name=(concat "damage.parts." realIndex ".resultBased") localize=true classes="checkbox"}} - {{/if}} - {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}} -
-
- {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}} - {{> formula fields=../../fields.value.fields type=../../fields.type dmg=dmg source=dmg.value target="value" realIndex=realIndex}} -
-
- {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}} - {{> formula fields=../../fields.valueAlt.fields type=../../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" realIndex=realIndex}} -
-
- {{else}} - - {{> formula fields=../../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" realIndex=realIndex}} - - {{/if}} -
- {{formField ../../fields.applyTo value=dmg.applyTo name=(concat "damage.parts." realIndex ".applyTo") localize=true}} - {{#if (eq dmg.applyTo 'hitPoints')}} - {{formField ../../fields.type value=dmg.type name=(concat "damage.parts." realIndex ".type") localize=true}} - {{/if}} -
- - - {{#unless dmg.base}}
{{/unless}} -
- {{/with}} + {{setVar 'realIndex' index}} {{/if}} +
+ + {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base))}} + {{formField ../fields.resultBased value=dmg.resultBased name=(concat "damage.parts." realIndex ".resultBased") localize=true classes="checkbox"}} + {{/if}} + {{#if (and (not @root.isNPC) @root.hasRoll (not dmg.base) dmg.resultBased)}} +
+
+ {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.hope")}} + {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" realIndex=realIndex path=../path}} +
+
+ {{localize "DAGGERHEART.GENERAL.withThing" thing=(localize "DAGGERHEART.GENERAL.fear")}} + {{> formula fields=../fields.valueAlt.fields type=../fields.type dmg=dmg source=dmg.valueAlt target="valueAlt" realIndex=realIndex path=../path}} +
+
+ {{else}} + + {{localize "DAGGERHEART.GENERAL.formula"}} + {{> formula fields=../fields.value.fields type=../fields.type dmg=dmg source=dmg.value target="value" realIndex=realIndex path=../path}} + + {{/if}} +
+ {{formField ../fields.applyTo value=dmg.applyTo name=(concat ../path "damage.parts." realIndex ".applyTo") localize=true}} + {{#if (eq dmg.applyTo 'hitPoints')}} + {{formField ../fields.type value=dmg.type name=(concat ../path "damage.parts." realIndex ".type") localize=true}} + {{/if}} +
+ {{#if ../horde}} +
+ {{localize "DAGGERHEART.ACTORS.Adversary.hordeDamage"}} +
+ + {{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.bonus value=dmg.valueAlt.bonus name=(concat ../path "damage.parts." realIndex ".valueAlt.bonus") localize=true classes="inline-child"}} +
+
+ {{/if}} + + + {{#unless (or dmg.base ../path)}}
{{/unless}} +
{{/each}} {{#*inline "formula"}} {{#unless dmg.base}} - {{formField fields.custom.fields.enabled value=source.custom.enabled name=(concat "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"}} {{/unless}} {{#if source.custom.enabled}} - {{formField fields.custom.fields.formula value=source.custom.formula name=(concat "damage.parts." realIndex "." target ".custom.formula") localize=true}} + {{formField fields.custom.fields.formula value=source.custom.formula name=(concat path "damage.parts." realIndex "." target ".custom.formula") localize=true}} {{else}}
- {{formField fields.multiplier value=source.multiplier name=(concat "damage.parts." realIndex "." target ".multiplier") localize=true}} - {{#if (eq source.multiplier 'flat')}}{{formField fields.flatMultiplier value=source.flatMultiplier name=(concat "damage.parts." realIndex ".flatMultiplier") }}{{/if}} - {{formField fields.dice value=source.dice name=(concat "damage.parts." realIndex "." target ".dice")}} - {{formField fields.bonus value=source.bonus name=(concat "damage.parts." realIndex "." target ".bonus") localize=true}} + {{#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")}} + {{formField fields.bonus value=source.bonus name=(concat path "damage.parts." realIndex "." target ".bonus") localize=true}}
{{/if}} + {{#if @root.isNPC}} + + {{/if}} {{/inline}} \ No newline at end of file diff --git a/templates/sheets-settings/adversary-settings/attack.hbs b/templates/sheets-settings/adversary-settings/attack.hbs index bdb6da5b..a51128a1 100644 --- a/templates/sheets-settings/adversary-settings/attack.hbs +++ b/templates/sheets-settings/adversary-settings/attack.hbs @@ -19,7 +19,5 @@ {{/if}} {{/if}} - {{#if (eq document.system.type 'horde')}} - {{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields.parts.element.fields source=document.system.attack.damage path="system.attack." horde=true}} - {{/if}} + {{> 'systems/daggerheart/templates/actionTypes/damage.hbs' fields=systemFields.attack.fields.damage.fields.parts.element.fields source=document.system.attack.damage path="system.attack." horde=(eq document.system.type 'horde')}} \ No newline at end of file diff --git a/templates/sheets/actors/adversary/sidebar.hbs b/templates/sheets/actors/adversary/sidebar.hbs index 131fb33b..0bb1f129 100644 --- a/templates/sheets/actors/adversary/sidebar.hbs +++ b/templates/sheets/actors/adversary/sidebar.hbs @@ -54,7 +54,7 @@ {{/if}}
-

{{localize DAGGERHEART.GENERAL.difficulty}}

+

{{localize "DAGGERHEART.GENERAL.difficulty"}}

diff --git a/templates/sheets/global/partials/inventory-item-V2.hbs b/templates/sheets/global/partials/inventory-item-V2.hbs index 73ae5ae7..71b06202 100644 --- a/templates/sheets/global/partials/inventory-item-V2.hbs +++ b/templates/sheets/global/partials/inventory-item-V2.hbs @@ -249,7 +249,7 @@ Parameters: {{#if (and showActions (eq item.type 'feature'))}}
{{#each item.system.actions as | action |}} - {{/each}} diff --git a/templates/ui/chat/adversary-roll.hbs b/templates/ui/chat/adversary-roll.hbs index 63187a52..70307ef1 100644 --- a/templates/ui/chat/adversary-roll.hbs +++ b/templates/ui/chat/adversary-roll.hbs @@ -34,7 +34,7 @@
- {{localize "DAGGEHEART.GENERAL.damage"}} + {{localize "DAGGERHEART.GENERAL.damage"}}