diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 399fa648d..19e7131d9 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -93,7 +93,8 @@ export default function DHApplicationMixin(Base) { triggerContextMenu: DHSheetV2.#triggerContextMenu, createDoc: DHSheetV2.#onCreateDoc, editDoc: DHSheetV2.#editDoc, - deleteDoc: DHSheetV2.#deleteDoc, + deleteDoc: DHSheetV2.#onDeleteDoc, + deleteInvalidDoc: DHSheetV2.#onDeleteInvalidDoc, toChat: DHSheetV2.#toChat, useItem: DHSheetV2.#useItem, viewItem: DHSheetV2.#viewItem, @@ -845,7 +846,7 @@ export default function DHApplicationMixin(Base) { * Delete an embedded document. * @type {ApplicationClickAction} */ - static async #deleteDoc(event, target) { + static async #onDeleteDoc(event, target) { const doc = await getDocFromElement(target); if (doc) { if (event.shiftKey) return doc.delete(); @@ -853,6 +854,28 @@ export default function DHApplicationMixin(Base) { } } + /** + * Delete an embedded document or action that is invalid. + * @type {ApplicationClickAction} + * @this DHSheetV2 + */ + static async #onDeleteInvalidDoc(event, target) { + const { invalidId, collection } = target.closest('[data-invalid-id]')?.dataset ?? {}; + if (!invalidId) throw Error('Missing invalid document id'); + + if (collection === 'actions') { + this.document.update({ + 'system.actions': { + [invalidId]: _del + } + }); + } else { + if (!this.document[collection]) throw new Error(`Invalid collection ${collection}`); + const document = this.document[collection].getInvalid(invalidId); + await document.delete(); // untested, test when implementing https://github.com/Foundryborne/daggerheart/issues/1891 + } + } + /** * Send item to Chat * @type {ApplicationClickAction} diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index c734cde57..1cec0388f 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -105,6 +105,10 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { secrets: this.item.isOwner }) break; + case 'actions': + context.invalidActions = Object.values(this.document._source.system.actions ?? {}) + .filter(a => !this.document.system.actions?.has(a._id)); + break; case 'effects': await this._prepareEffectsContext(context, options); break; diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 9bd61eeb7..7b679ba5b 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -143,10 +143,12 @@ export default class DhpAdversary extends DhCreature { return tags; } - /** Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. */ + /** + * Returns source data for this actor adjusted to a new tier, which can be used to create a new actor. + * @returns {object} + */ adjustForTier(tier) { - const source = this.parent.toObject(true); - return getTierAdjustedAdversary(source, tier); + return getTierAdjustedAdversary(this.parent, tier); } /** @inheritdoc */ diff --git a/module/data/actor/tierAdjustment.mjs b/module/data/actor/tierAdjustment.mjs index 3b5591939..d99c53b5b 100644 --- a/module/data/actor/tierAdjustment.mjs +++ b/module/data/actor/tierAdjustment.mjs @@ -4,11 +4,12 @@ import { parseInlineParams } from '../../enrichers/parser.mjs'; /** * Accepts source data for an adversary and a target tier, and returns new source data - * @type {object} source + * @type {DhActor} actor * @type {number} tier * @returns {object} adjusted source data */ -export function getTierAdjustedAdversary(source, tier) { +export function getTierAdjustedAdversary(actor, tier) { + const source = actor.toObject(true); const currentTier = source.system.tier ?? 1; /** @type {(2 | 3 | 4)[]} */ @@ -67,8 +68,22 @@ export function getTierAdjustedAdversary(source, tier) { const damageRegex = /@Damage\[([^[\]]*)\]({[^}]*})?/g; for (const item of source.items) { // Replace damage inlines with new formulas. Keep a record for a specific check later + const itemDocument = actor.items.get(item._id); + if (!itemDocument) { + console.error(`Item ${item.name} [${item._id}] was not in actor, possibly due to data preparation errors`); + continue; + } + + const actions = Object.values(item.system.actions).filter(a => { + if (!itemDocument.system.actions?.has(a._id)) { + console.error(`Action ${a._id} in ${itemDocument.name} was not in actor, possibly due to errors in data prep`); + return false; + } + return true; + }); + const descriptionFormulas = []; - for (const withDescription of [item.system, ...Object.values(item.system.actions)]) { + for (const withDescription of [item.system, ...actions]) { withDescription.description = withDescription.description.replace(damageRegex, (match, inner) => { const { value: formula } = parseInlineParams(inner, { first: 'value' }); if (!formula) return match; @@ -84,7 +99,7 @@ export function getTierAdjustedAdversary(source, tier) { } // Update damage in item actions and convert all formula matches in the descriptions to the new damage - for (const action of Object.values(item.system.actions)) { + for (const action of actions) { if (!action.damage?.main) continue; try { // Apply conversions and save a record. If it matches attack damage *and* Its not in the description, use attack conversion instead diff --git a/module/data/fields/_module.mjs b/module/data/fields/_module.mjs index fbe6eb274..08f9cba6d 100644 --- a/module/data/fields/_module.mjs +++ b/module/data/fields/_module.mjs @@ -1,4 +1,4 @@ -export { ActionCollection } from './actionField.mjs'; +export { ActionCollection } from './action-collection-field.mjs'; export { default as IterableTypedObjectField } from './iterableTypedObjectField.mjs'; export { default as FormulaField } from './formulaField.mjs'; export { default as ForeignDocumentUUIDField } from './foreignDocumentUUIDField.mjs'; diff --git a/module/data/fields/action-collection-field.mjs b/module/data/fields/action-collection-field.mjs new file mode 100644 index 000000000..9daebfa4c --- /dev/null +++ b/module/data/fields/action-collection-field.mjs @@ -0,0 +1,92 @@ +import { ActionField } from './actionField.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(); + for (const [key, value] of entries) { + if (!(value instanceof game.system.api.models.actions.actionsTypes.base)) continue; + this.set(key, value); + } + } + + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /* -------------------------------------------- */ + /* 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 ActionCollectionField extends foundry.data.fields.TypedObjectField { + constructor(options) { + super(new ActionField(), options); + } + + /* -------------------------------------------- */ + /** @inheritdoc */ + initialize(value, model, options) { + const actions = Object.entries(super.initialize(value, model, options)); + return new ActionCollection(model, actions); + } + + /** @inheritdoc */ + clean(value, options, state) { + // Before any migrations and such occurs, convert collections to objects + if (value instanceof Collection) { + value = value.entries().reduce((r, [k, v]) => { + r[k] = v; + return r; + }, {}); + } + return super.clean(value, this.options, state); + } + + /** @inheritdoc */ + _cleanType(value, options, state) { + value = super._cleanType(value, options, state); + // Ensure ids match. If ids do not match, this causes issues in things like the action config's resync + // _id is considered more accurate, due to validation rules + if (value && typeof value === 'object') { + for (const [key, objValue] of Object.entries(value)) { + if (objValue._id && key !== objValue._id) { + delete value[key]; + value[objValue._id] = objValue; + } + } + } + return value; + } +} diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index dace3cc38..e016c2554 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -1,98 +1,6 @@ import DHActionConfig from '../../applications/sheets-configs/action-config.mjs'; import { itemAbleRollParse } from '../../helpers/utils.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(); - for (const [key, value] of entries) { - if (!(value instanceof game.system.api.models.actions.actionsTypes.base)) continue; - this.set(key, value); - } - } - - /* -------------------------------------------- */ - /* Properties */ - /* -------------------------------------------- */ - - /* -------------------------------------------- */ - /* 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 foundry.data.fields.TypedObjectField { - constructor(options) { - super(new ActionField(), options); - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - initialize(value, model, options) { - const actions = Object.entries(super.initialize(value, model, options)); - return new ActionCollection(model, actions); - } - - /** @inheritdoc */ - clean(value, options, state) { - // Before any migrations and such occurs, convert collections to objects - if (value instanceof Collection) { - value = value.entries().reduce((r, [k, v]) => { - r[k] = v; - return r; - }, {}); - } - return super.clean(value, this.options, state); - } - - /** @inheritdoc */ - _cleanType(value, options, state) { - value = super._cleanType(value, options, state); - // Ensure ids match. If ids do not match, this causes issues in things like the action config's resync - // _id is considered more accurate, due to validation rules - if (value && typeof value === 'object') { - for (const [key, objValue] of Object.entries(value)) { - if (objValue._id && key !== objValue._id) { - delete value[key]; - value[objValue._id] = objValue; - } - } - } - return value; - } -} - /* -------------------------------------------- */ /** diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 87bc63ce0..2191e8bfc 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -15,7 +15,7 @@ import { updateLinkedItemApps } from '../../helpers/utils.mjs'; import { DHActionDiceData } from '../fields/action/damageField.mjs'; -import { ActionsField } from '../fields/actionField.mjs'; +import { ActionCollectionField } from '../fields/action-collection-field.mjs'; import FormulaField from '../fields/formulaField.mjs'; const fields = foundry.data.fields; @@ -95,7 +95,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { if (this.metadata.isInventoryItem) schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true }); - if (this.metadata.hasActions) schema.actions = new ActionsField(); + if (this.metadata.hasActions) schema.actions = new ActionCollectionField(); return schema; } @@ -267,7 +267,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const triggersToRemove = Object.keys(changed.system.actions).reduce((acc, key) => { const action = changed.system.actions[key]; - if (action && Object.keys(action).length === 0) { + if (action && Object.keys(action).length === 0 && this.actions.has(key)) { acc.push(...this.actions.get(key).triggers.map(x => x.trigger)); } diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index 84b28fcf9..e3cd79537 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -1,6 +1,6 @@ import { defaultRestOptions } from '../../config/generalConfig.mjs'; import { resetAndRerenderActors } from '../../helpers/utils.mjs'; -import { ActionsField } from '../fields/actionField.mjs'; +import { ActionCollectionField } from '../fields/action-collection-field.mjs'; const currencyField = (initial, label, icon, initialAmount = 0) => new foundry.data.fields.SchemaField({ @@ -30,7 +30,7 @@ const restMoveField = () => base64: false }), description: new foundry.data.fields.HTMLField(), - actions: new ActionsField(), + actions: new ActionCollectionField(), effects: new foundry.data.fields.ArrayField(new foundry.data.fields.ObjectField()) }); @@ -181,7 +181,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel { base64: false }), description: new fields.HTMLField(), - actions: new ActionsField(), + actions: new ActionCollectionField(), effects: new fields.ArrayField(new fields.ObjectField()) }) ), @@ -194,7 +194,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel { base64: false }), description: new fields.HTMLField(), - actions: new ActionsField(), + actions: new ActionCollectionField(), effects: new fields.ArrayField(new fields.ObjectField()) }) ) diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index 9e923051c..5a92a0c2c 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -3,7 +3,7 @@ @import '../utils/mixin.less'; .appTheme({}, { - .inventory-item .img-portait .roll-img { + .inventory-item .img-portrait .roll-img { filter: invert(1); } @@ -30,13 +30,13 @@ border: 1px solid; border-radius: 6px; - .inventory-item-header .img-portait img.item-img { + .inventory-item-header .img-portrait img.item-img { border-radius: 6px 0 0 6px; } } &:not(.single-img) { - .img-portait:has(.roll-img):hover { + .img-portrait:has(.roll-img):hover { .roll-img { opacity: 1; } @@ -69,7 +69,7 @@ .inventory-item-header, &.inventory-item-compact { - .img-portait { + .img-portrait { flex: 0 0 40px; height: 40px; position: relative; @@ -223,7 +223,7 @@ / 40px 1fr min-content; column-gap: 8px; - > .img-portait { + > .img-portrait { grid-area: img; } > .item-name { diff --git a/templates/actionTypes/areas.hbs b/templates/actionTypes/areas.hbs index 662716f57..405c69842 100644 --- a/templates/actionTypes/areas.hbs +++ b/templates/actionTypes/areas.hbs @@ -28,7 +28,7 @@