diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 67ca77e6..6ce103fe 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); } } diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index 3f915e41..bc2d83b0 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -175,19 +175,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(); } diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 65a43123..67dddc50 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -271,65 +271,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 })]; } /** @@ -409,7 +352,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,57 +378,23 @@ 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' }, - 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 (!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; } /** @@ -495,12 +404,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 }); } /** @@ -509,34 +412,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) - }); } /** @@ -545,13 +424,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); } @@ -561,14 +433,6 @@ 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); } @@ -578,9 +442,6 @@ export default function DHApplicationMixin(Base) { */ 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); } 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 41f9ef20..50fe8904 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -81,7 +81,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; } 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/attackAction.mjs b/module/data/action/attackAction.mjs index e17c0e9d..a4f65735 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -5,7 +5,7 @@ export default class DHAttackAction extends DHDamageAction { static extraSchemas = [...super.extraSchemas, ...['roll', 'save']]; static getRollType(parent) { - return parent.type === 'weapon' ? 'attack' : 'spellcast'; + return parent.parent.type === 'weapon' ? 'attack' : 'spellcast'; } get chatTemplate() { diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index ad442951..48e70939 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -1,6 +1,7 @@ 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 +17,12 @@ const fields = foundry.data.fields; - Summon Action create method */ -export default class DHBaseAction extends foundry.abstract.DataModel { +export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) { static extraSchemas = []; static defineSchema() { return { - _id: new fields.DocumentIdField(), + _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 }), @@ -109,7 +110,10 @@ export default class DHBaseAction extends foundry.abstract.DataModel { return extraSchemas; } - prepareData() {} + prepareData() { + this.name = this.name || game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[this.type].name); + this.img = this.img ?? this.parent?.parent?.img; + } get index() { return foundry.utils.getProperty(this.parent, this.systemPath).indexOf(this); @@ -141,22 +145,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; @@ -552,27 +555,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/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..8b197b11 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 } 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'; diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index b83acc2e..8e02f4e6 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -1,4 +1,215 @@ -export default class ActionField extends foundry.data.fields.ObjectField { +import DHActionConfig from "../../applications/sheets-configs/action-config.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)); + } +} + +/* -------------------------------------------- */ + +/** + * 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 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); + } +} + +/* -------------------------------------------- */ + +/** + * 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 +246,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/item/armor.mjs b/module/data/item/armor.mjs index 8522c8fc..85026121 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -1,5 +1,5 @@ import AttachableItem from './attachableItem.mjs'; -import ActionField from '../fields/actionField.mjs'; +import { ActionField } from '../fields/actionField.mjs'; import { armorFeatures } from '../../config/itemConfig.mjs'; import { actionsTypes } from '../action/_module.mjs'; diff --git a/module/data/item/consumable.mjs b/module/data/item/consumable.mjs index 3e70f97a..8c3c0dae 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 */ diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index df60b9d1..1ecc167f 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 */ diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index 62b955e9..3f7fdad8 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 } from '../fields/actionField.mjs'; export default class DHFeature extends BaseDataItem { /** @inheritDoc */ diff --git a/module/data/item/miscellaneous.mjs b/module/data/item/miscellaneous.mjs index cad07f48..bdf608c6 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 */ diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 0d0c7f76..fd26975b 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -1,6 +1,6 @@ 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 */ @@ -65,7 +65,8 @@ export default class DHWeapon extends AttachableItem { } } }), - actions: new fields.ArrayField(new ActionField()) + actions: new ActionsField() + // actions: new fields.ArrayField(new ActionField()) }; } 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..ba1c73ae 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. diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index 8e1c729e..bf6083ab 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -9,7 +9,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti 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 item = actionId ? baseItem.system.actions.get(actionId) : baseItem; if (item) { const type = actionId ? 'action' : item.type; const description = await TextEditor.enrichHTML(item.system.description); diff --git a/templates/sheets/global/partials/inventory-item-V2.hbs b/templates/sheets/global/partials/inventory-item-V2.hbs index b58f6f44..8d7fb1a8 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 a37f93e3..accc22cc 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"}}