diff --git a/module/applications/sheets/api/item-attachment-sheet.mjs b/module/applications/sheets/api/item-attachment-sheet.mjs index c4677ac4..44d240f9 100644 --- a/module/applications/sheets/api/item-attachment-sheet.mjs +++ b/module/applications/sheets/api/item-attachment-sheet.mjs @@ -43,163 +43,35 @@ export default function ItemAttachmentSheet(Base) { const item = await Item.implementation.fromDropData(data); if (!item) return; - await addAttachmentToItem({ - parentItem: this.document, - droppedItem: item, - parentType: this.document.type - }); + // Call the data model's public method + await this.document.system.addAttachment(item); } + static async #removeAttachment(event, target) { - await removeAttachmentFromItem({ - parentItem: this.document, - attachedUuid: target.dataset.uuid, - parentType: this.document.type - }); - } + // Call the data model's public method + await this.document.system.removeAttachment(target.dataset.uuid); +} - async removeAttachmentEffectsFromActor({ parentItem, attachedUuid, parentType }) { - const actor = parentItem.parent; - if (!actor) return; - - const parentUuidProperty = `${parentType}Uuid`; - const effectsToRemove = actor.effects.filter(effect => { - const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource); - return attachmentSource && - attachmentSource[parentUuidProperty] === parentItem.uuid && - attachmentSource.itemUuid === attachedUuid; - }); - - if (effectsToRemove.length > 0) { - await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); - } - } - - async removeAttachmentFromItem({ parentItem, attachedUuid, parentType }) { - const currentAttached = parentItem.system.attached; + async _preparePartContext(partId, context) { + await super._preparePartContext(partId, context); - // Remove the attachment from the parent item's attached array - await parentItem.update({ - 'system.attached': currentAttached.filter(uuid => uuid !== attachedUuid) - }); - - // Remove any effects that came from this attachment - await removeAttachmentEffectsFromActor({ - parentItem, - attachedUuid, - parentType - }); - } - async prepareAttachmentContext(parentItem) { - const attachedUUIDs = parentItem.system.attached; - return await Promise.all( - attachedUUIDs.map(async uuid => { - const item = await fromUuid(uuid); - return { - uuid: uuid, - name: item?.name || 'Unknown Item', - img: item?.img || 'icons/svg/item-bag.svg' - }; - }) - ); - } - - async addAttachmentToItem({ parentItem, droppedItem, parentType }) { - const currentAttached = parentItem.system.attached; - const newUUID = droppedItem.uuid; - - if (currentAttached.includes(newUUID)) { - ui.notifications.warn(`${droppedItem.name} is already attached to this ${parentType}.`); - return; + if (partId === 'attachments') { + // Keep this simple UI preparation in the mixin + const attachedUUIDs = this.document.system.attached; + context.attachedItems = await Promise.all( + attachedUUIDs.map(async uuid => { + const item = await fromUuid(uuid); + return { + uuid: uuid, + name: item?.name || 'Unknown Item', + img: item?.img || 'icons/svg/item-bag.svg' + }; + }) + ); } - const updatedAttached = [...currentAttached, newUUID]; - - await parentItem.update({ - 'system.attached': updatedAttached - }); - - // Copy effects from attached item to actor (only if parent item is equipped) - const actor = parentItem.parent; - if (actor && droppedItem.effects.size > 0 && parentItem.system.equipped) { - await copyAttachmentEffectsToActor({ - parentItem, - attachedItem: droppedItem, - attachedUuid: newUUID, - parentType - }); - } + return context; } - - async copyAttachmentEffectsToActor({ parentItem, attachedItem, attachedUuid, parentType }) { - const actor = parentItem.parent; - if (!actor || !attachedItem.effects.size > 0 || !parentItem.system.equipped) { - return []; - } - - const effectsToCreate = []; - for (const effect of attachedItem.effects) { - // Copy ALL effects when item is attached - attachment-only flag only matters for non-attached items - const effectData = effect.toObject(); - effectData.origin = `${parentItem.uuid}:${attachedUuid}`; - - // Set up attachment source metadata with the appropriate property name - const attachmentSource = { - itemUuid: attachedUuid, - originalEffectId: effect.id - }; - attachmentSource[`${parentType}Uuid`] = parentItem.uuid; - - effectData.flags = { - ...effectData.flags, - [CONFIG.DH.id]: { - ...effectData.flags?.[CONFIG.DH.id], - [CONFIG.DH.FLAGS.itemAttachmentSource]: attachmentSource - } - }; - effectsToCreate.push(effectData); - } - - if (effectsToCreate.length > 0) { - return await actor.createEmbeddedDocuments('ActiveEffect', effectsToCreate); - } - - return []; - } - - async handleAttachmentEffectsOnEquipChange({ parentItem, newEquippedStatus, parentType }) { - // Try to get the actor - it might be parentItem.parent instead of parentItem.parent.parent - const actor = parentItem.parent?.type === 'character' ? parentItem.parent : parentItem.parent?.parent; - - if (!actor || !parentItem.system.attached?.length) { - return; - } - - if (newEquippedStatus) { - // Item is being equipped - add attachment effects - for (const attachedUuid of parentItem.system.attached) { - const attachedItem = await fromUuid(attachedUuid); - if (attachedItem && attachedItem.effects.size > 0) { - this.copyAttachmentEffectsToActor({ - parentItem, - attachedItem, - attachedUuid, - parentType - }); - } - } - } else { - // Item is being unequipped - remove attachment effects - const parentUuidProperty = `${parentType}Uuid`; - const effectsToRemove = actor.effects.filter(effect => { - const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource); - return attachmentSource && attachmentSource[parentUuidProperty] === parentItem.uuid; - }); - - if (effectsToRemove.length > 0) { - await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); - } - } - } }; } \ No newline at end of file diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index 1c8c2bfa..4891b3c9 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -1,6 +1,5 @@ import DHBaseItemSheet from '../api/base-item.mjs'; import ItemAttachmentSheet from '../api/item-attachment-sheet.mjs'; -import { copyAttachmentEffectsToActor, removeAttachmentFromItem, prepareAttachmentContext, addAttachmentToItem } from '../../../helpers/attachmentHelper.mjs'; export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) { /**@inheritdoc */ diff --git a/module/data/item/attachableItem.mjs b/module/data/item/attachableItem.mjs index 90fd50fd..2b0608eb 100644 --- a/module/data/item/attachableItem.mjs +++ b/module/data/item/attachableItem.mjs @@ -8,26 +8,60 @@ export default class AttachableItem extends BaseDataItem { attached: new fields.ArrayField(new fields.DocumentUUIDField({ type: "Item", nullable: true })) }; } - - async copyAttachmentEffectsToActor({ parentItem, attachedItem, attachedUuid, parentType }) { - const actor = parentItem.parent; - if (!actor || !attachedItem.effects.size > 0 || !parentItem.system.equipped) { + + async _preUpdate(changes, options, user) { + const allowed = await super._preUpdate(changes, options, user); + if (allowed === false) return false; + + // Handle equipped status changes for attachment effects + if (changes.system?.equipped !== undefined && changes.system.equipped !== this.equipped) { + await this.#handleAttachmentEffectsOnEquipChange(changes.system.equipped); + } + } + + async #handleAttachmentEffectsOnEquipChange(newEquippedStatus) { + const actor = this.parent.parent?.type === 'character' ? this.parent.parent : this.parent.parent?.parent; + const parentType = this.parent.type; + + if (!actor || !this.attached?.length) { + return; + } + + if (newEquippedStatus) { + // Item is being equipped - add attachment effects + for (const attachedUuid of this.attached) { + const attachedItem = await fromUuid(attachedUuid); + if (attachedItem && attachedItem.effects.size > 0) { + await this.#copyAttachmentEffectsToActor({ + attachedItem, + attachedUuid, + parentType + }); + } + } + } else { + // Item is being unequipped - remove attachment effects + await this.#removeAllAttachmentEffects(parentType); + } + } + + async #copyAttachmentEffectsToActor({ attachedItem, attachedUuid, parentType }) { + const actor = this.parent.parent; + if (!actor || !attachedItem.effects.size > 0 || !this.equipped) { return []; } const effectsToCreate = []; for (const effect of attachedItem.effects) { - // Copy ALL effects when item is attached - attachment-only flag only matters for non-attached items const effectData = effect.toObject(); - effectData.origin = `${parentItem.uuid}:${attachedUuid}`; - - // Set up attachment source metadata with the appropriate property name + effectData.origin = `${this.parent.uuid}:${attachedUuid}`; + const attachmentSource = { itemUuid: attachedUuid, originalEffectId: effect.id }; - attachmentSource[`${parentType}Uuid`] = parentItem.uuid; - + attachmentSource[`${parentType}Uuid`] = this.parent.uuid; + effectData.flags = { ...effectData.flags, [CONFIG.DH.id]: { @@ -41,56 +75,78 @@ export default class AttachableItem extends BaseDataItem { if (effectsToCreate.length > 0) { return await actor.createEmbeddedDocuments('ActiveEffect', effectsToCreate); } - + return []; } - async handleAttachmentEffectsOnEquipChange({ parentItem, newEquippedStatus, parentType }) { - // Try to get the actor - it might be parentItem.parent instead of parentItem.parent.parent - const actor = parentItem.parent?.type === 'character' ? parentItem.parent : parentItem.parent?.parent; - - if (!actor || !parentItem.system.attached?.length) { - return; - } + async #removeAllAttachmentEffects(parentType) { + const actor = this.parent.parent; + if (!actor) return; - if (newEquippedStatus) { - // Item is being equipped - add attachment effects - for (const attachedUuid of parentItem.system.attached) { - const attachedItem = await fromUuid(attachedUuid); - if (attachedItem && attachedItem.effects.size > 0) { - this.copyAttachmentEffectsToActor({ - parentItem, - attachedItem, - attachedUuid, - parentType - }); - } - } - } else { - // Item is being unequipped - remove attachment effects - const parentUuidProperty = `${parentType}Uuid`; - const effectsToRemove = actor.effects.filter(effect => { - const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource); - return attachmentSource && attachmentSource[parentUuidProperty] === parentItem.uuid; - }); + const parentUuidProperty = `${parentType}Uuid`; + const effectsToRemove = actor.effects.filter(effect => { + const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource); + return attachmentSource && attachmentSource[parentUuidProperty] === this.parent.uuid; + }); - if (effectsToRemove.length > 0) { - await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); - } + if (effectsToRemove.length > 0) { + await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); } } - async _preUpdate(changes, options, user) { - const allowed = await super._preUpdate(changes, options, user); - if (allowed === false) return false; + /** + * Public method for adding an attachment + */ + async addAttachment(droppedItem) { + const newUUID = droppedItem.uuid; - // Handle equipped status changes for attachment effects - if (changes.system?.equipped !== undefined && changes.system.equipped !== this.equipped) { - await this.handleAttachmentEffectsOnEquipChange({ - parentItem: this.parent, - newEquippedStatus: changes.system.equipped, + if (this.attached.includes(newUUID)) { + ui.notifications.warn(`${droppedItem.name} is already attached to this ${this.parent.type}.`); + return; + } + + const updatedAttached = [...this.attached, newUUID]; + await this.parent.update({ + 'system.attached': updatedAttached + }); + + // Copy effects if equipped + if (this.equipped && droppedItem.effects.size > 0) { + await this.#copyAttachmentEffectsToActor({ + attachedItem: droppedItem, + attachedUuid: newUUID, parentType: this.parent.type }); } } + + /** + * Public method for removing an attachment + */ + async removeAttachment(attachedUuid) { + await this.parent.update({ + 'system.attached': this.attached.filter(uuid => uuid !== attachedUuid) + }); + + // Remove effects + await this.#removeAttachmentEffects(attachedUuid); + } + + async #removeAttachmentEffects(attachedUuid) { + const actor = this.parent.parent; + if (!actor) return; + + const parentType = this.parent.type; + const parentUuidProperty = `${parentType}Uuid`; + const effectsToRemove = actor.effects.filter(effect => { + const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource); + return attachmentSource && + attachmentSource[parentUuidProperty] === this.parent.uuid && + attachmentSource.itemUuid === attachedUuid; + }); + + if (effectsToRemove.length > 0) { + await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); + } + } } \ No newline at end of file diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index cd8de09b..2154ceab 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -1,7 +1,6 @@ import AttachableItem from './attachableItem.mjs'; import { actionsTypes } from '../action/_module.mjs'; import ActionField from '../fields/actionField.mjs'; -import { handleAttachmentEffectsOnEquipChange } from '../../helpers/attachmentHelper.mjs'; export default class DHWeapon extends AttachableItem { /** @inheritDoc */ @@ -39,7 +38,7 @@ export default class DHWeapon extends AttachableItem { actionIds: new fields.ArrayField(new fields.StringField({ required: true })) }) ), - attack: new ActionField({ + attack: new ActionField({ initial: { name: 'Attack', img: 'icons/skills/melee/blood-slash-foam-red.webp', diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 68602db5..35b9e32f 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -9,46 +9,6 @@ export default class DHItem extends foundry.documents.Item { for (const action of this.system.actions ?? []) action.prepareData(); } - /** - * @inheritdoc - */ - async _preUpdate(changed, options, user) { - const result = await super._preUpdate(changed, options, user); - - // Store the previous equipped status for attachment handling - if (changed.system?.equipped !== undefined) { - options.previousEquipped = this.system.equipped; - } - - return result; - } - - /** - * @inheritdoc - */ - async _onUpdate(changed, options, user) { - await super._onUpdate(changed, options, user); - - // Handle attachment effects when equipped status changes - if (changed.system?.equipped !== undefined && options.previousEquipped !== changed.system.equipped) { - const newEquippedStatus = changed.system.equipped; - const parentType = this.type === 'armor' ? 'armor' : this.type === 'weapon' ? 'weapon' : null; - - if (parentType) { - try { - const { handleAttachmentEffectsOnEquipChange } = await import('../helpers/attachmentHelper.mjs'); - await handleAttachmentEffectsOnEquipChange({ - parentItem: this, - newEquippedStatus, - parentType - }); - } catch (error) { - console.error('DH | Error in handleAttachmentEffectsOnEquipChange:', error); - } - } - } - } - /** * @inheritdoc * @param {object} options - Options which modify the getRollData method. diff --git a/module/helpers/attachmentHelper.mjs b/module/helpers/attachmentHelper.mjs deleted file mode 100644 index 1b9b1f71..00000000 --- a/module/helpers/attachmentHelper.mjs +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Utility functions for handling item attachments and their effects - */ - -/** - * Copy all effects from an attached item to the actor when the parent item is equipped - * @param {Object} options - Configuration options - * @param {Item} options.parentItem - The item (armor/weapon) that the item is being attached to - * @param {Item} options.attachedItem - The item being attached - * @param {string} options.attachedUuid - UUID of the attached item - * @param {string} options.parentType - Type of parent item ("armor" or "weapon") - * @returns {Promise} Created effects - */ -export async function copyAttachmentEffectsToActor({ parentItem, attachedItem, attachedUuid, parentType }) { - const actor = parentItem.parent; - if (!actor || !attachedItem.effects.size > 0 || !parentItem.system.equipped) { - return []; - } - - const effectsToCreate = []; - for (const effect of attachedItem.effects) { - // Copy ALL effects when item is attached - attachment-only flag only matters for non-attached items - const effectData = effect.toObject(); - effectData.origin = `${parentItem.uuid}:${attachedUuid}`; - - // Set up attachment source metadata with the appropriate property name - const attachmentSource = { - itemUuid: attachedUuid, - originalEffectId: effect.id - }; - attachmentSource[`${parentType}Uuid`] = parentItem.uuid; - - effectData.flags = { - ...effectData.flags, - [CONFIG.DH.id]: { - ...effectData.flags?.[CONFIG.DH.id], - [CONFIG.DH.FLAGS.itemAttachmentSource]: attachmentSource - } - }; - effectsToCreate.push(effectData); - } - - if (effectsToCreate.length > 0) { - return await actor.createEmbeddedDocuments('ActiveEffect', effectsToCreate); - } - - return []; -} - -/** - * Remove effects from the actor that came from a specific attached item - * @param {Object} options - Configuration options - * @param {Item} options.parentItem - The item (armor/weapon) that the item was attached to - * @param {string} options.attachedUuid - UUID of the attached item being removed - * @param {string} options.parentType - Type of parent item ("armor" or "weapon") - * @returns {Promise} - */ -export async function removeAttachmentEffectsFromActor({ parentItem, attachedUuid, parentType }) { - const actor = parentItem.parent; - if (!actor) return; - - const parentUuidProperty = `${parentType}Uuid`; - const effectsToRemove = actor.effects.filter(effect => { - const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource); - return attachmentSource && - attachmentSource[parentUuidProperty] === parentItem.uuid && - attachmentSource.itemUuid === attachedUuid; - }); - - if (effectsToRemove.length > 0) { - await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); - } -} - -/** - * Remove an attachment from an item and clean up its effects - * @param {Object} options - Configuration options - * @param {Item} options.parentItem - The item (armor/weapon) that the item is attached to - * @param {string} options.attachedUuid - UUID of the attached item being removed - * @param {string} options.parentType - Type of parent item ("armor" or "weapon") - * @returns {Promise} - */ -export async function removeAttachmentFromItem({ parentItem, attachedUuid, parentType }) { - const currentAttached = parentItem.system.attached; - - // Remove the attachment from the parent item's attached array - await parentItem.update({ - 'system.attached': currentAttached.filter(uuid => uuid !== attachedUuid) - }); - - // Remove any effects that came from this attachment - await removeAttachmentEffectsFromActor({ - parentItem, - attachedUuid, - parentType - }); -} - -/** - * Handle adding/removing attachment effects when a parent item is equipped/unequipped - * @param {Object} options - Configuration options - * @param {Item} options.parentItem - The item (armor/weapon) being equipped/unequipped - * @param {boolean} options.newEquippedStatus - The new equipped status - * @param {string} options.parentType - Type of parent item ("armor" or "weapon") - * @returns {Promise} - */ -export async function handleAttachmentEffectsOnEquipChange({ parentItem, newEquippedStatus, parentType }) { - // Try to get the actor - it might be parentItem.parent instead of parentItem.parent.parent - const actor = parentItem.parent?.type === 'character' ? parentItem.parent : parentItem.parent?.parent; - - if (!actor || !parentItem.system.attached?.length) { - return; - } - - if (newEquippedStatus) { - // Item is being equipped - add attachment effects - for (const attachedUuid of parentItem.system.attached) { - const attachedItem = await fromUuid(attachedUuid); - if (attachedItem && attachedItem.effects.size > 0) { - await copyAttachmentEffectsToActor({ - parentItem, - attachedItem, - attachedUuid, - parentType - }); - } - } - } else { - // Item is being unequipped - remove attachment effects - const parentUuidProperty = `${parentType}Uuid`; - const effectsToRemove = actor.effects.filter(effect => { - const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource); - return attachmentSource && attachmentSource[parentUuidProperty] === parentItem.uuid; - }); - - if (effectsToRemove.length > 0) { - await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); - } - } -} - -/** - * Prepare attachment context data for rendering - * @param {Item} parentItem - The item (armor/weapon) that has attachments - * @returns {Promise} Array of attachment data objects - */ -export async function prepareAttachmentContext(parentItem) { - const attachedUUIDs = parentItem.system.attached; - return await Promise.all( - attachedUUIDs.map(async uuid => { - const item = await fromUuid(uuid); - return { - uuid: uuid, - name: item?.name || 'Unknown Item', - img: item?.img || 'icons/svg/item-bag.svg' - }; - }) - ); -} - -/** - * Add an attachment to an item via drag and drop - * @param {Object} options - Configuration options - * @param {Item} options.parentItem - The item (armor/weapon) that the item is being attached to - * @param {Item} options.droppedItem - The item being attached - * @param {string} options.parentType - Type of parent item ("armor" or "weapon") - * @returns {Promise} - */ -export async function addAttachmentToItem({ parentItem, droppedItem, parentType }) { - const currentAttached = parentItem.system.attached; - const newUUID = droppedItem.uuid; - - if (currentAttached.includes(newUUID)) { - ui.notifications.warn(`${droppedItem.name} is already attached to this ${parentType}.`); - return; - } - - const updatedAttached = [...currentAttached, newUUID]; - - await parentItem.update({ - 'system.attached': updatedAttached - }); - - // Copy effects from attached item to actor (only if parent item is equipped) - const actor = parentItem.parent; - if (actor && droppedItem.effects.size > 0 && parentItem.system.equipped) { - await copyAttachmentEffectsToActor({ - parentItem, - attachedItem: droppedItem, - attachedUuid: newUUID, - parentType - }); - } -} - -