From f78cf12f6e6ea77f956d9b82225d787115cf13cb Mon Sep 17 00:00:00 2001 From: psitacus Date: Tue, 8 Jul 2025 22:50:07 -0600 Subject: [PATCH] add weapons to attachables --- module/applications/sheets/items/weapon.mjs | 157 +++++++++++++++++- module/data/item/weapon.mjs | 62 +++++++ templates/sheets/items/weapon/attachments.hbs | 30 ++++ 3 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 templates/sheets/items/weapon/attachments.hbs diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index d5d09dab..b07f377c 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -4,6 +4,13 @@ export default class WeaponSheet extends DHBaseItemSheet { /**@inheritdoc */ static DEFAULT_OPTIONS = { classes: ['weapon'], + dragDrop: [ + { dragSelector: null, dropSelector: null }, + { dragSelector: null, dropSelector: '.attachments-section' } + ], + actions: { + removeAttachment: WeaponSheet.#removeAttachment + }, tagifyConfigs: [ { selector: '.features-input', @@ -25,17 +32,52 @@ export default class WeaponSheet extends DHBaseItemSheet { settings: { template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs', scrollable: ['.settings'] + }, + attachments: { + template: 'systems/daggerheart/templates/sheets/items/weapon/attachments.hbs', + scrollable: ['.attachments'] + } + }; + + /** @override */ + static TABS = { + primary: { + tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'settings' }, { id: 'attachments' }], + initial: 'description', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } }; /**@inheritdoc */ async _preparePartContext(partId, context) { - super._preparePartContext(partId, context); + await super._preparePartContext(partId, context); switch (partId) { case 'settings': context.features = this.document.system.features.map(x => x.value); context.systemFields.attack.fields = this.document.system.attack.schema.fields; break; + case 'attachments': + // Prepare attached items for display + const attachedUUIDs = this.document.system.attached || []; + context.attachedItems = await Promise.all( + attachedUUIDs.map(async uuid => { + try { + const item = await fromUuid(uuid); + return { + uuid: uuid, + name: item?.name || 'Unknown Item', + img: item?.img || 'icons/svg/item-bag.svg' + }; + } catch (error) { + return { + uuid: uuid, + name: 'Unknown Item', + img: 'icons/svg/item-bag.svg' + }; + } + }) + ); + break; } return context; } @@ -47,4 +89,117 @@ export default class WeaponSheet extends DHBaseItemSheet { static async #onFeatureSelect(selectedOptions) { await this.document.update({ 'system.features': selectedOptions.map(x => ({ value: x.value })) }); } + + /** + * Handle dropping items onto the attachments section + * @param {DragEvent} event - The drop event + */ + async _onDrop(event) { + const data = TextEditor.getDragEventData(event); + + // Check if dropped on attachments section + const attachmentsSection = event.target.closest('.attachments-section'); + if (!attachmentsSection) return super._onDrop(event); + + // Prevent event bubbling + event.preventDefault(); + event.stopPropagation(); + + // Get the item being dropped + const item = await Item.implementation.fromDropData(data); + if (!item) return; + + console.log(`Dropping item ${item.name} (${item.uuid}) onto weapon ${this.document.name}`); + + // Get current attached UUIDs + const currentAttached = this.document.system.attached || []; + const newUUID = item.uuid; + + // Don't attach if already attached + if (currentAttached.includes(newUUID)) { + ui.notifications.warn(`${item.name} is already attached to this weapon.`); + return; + } + + console.log(`Current attached items:`, currentAttached); + console.log(`Adding new UUID:`, newUUID); + + const updatedAttached = [...currentAttached, newUUID]; + console.log(`Updating weapon with attached items:`, updatedAttached); + + await this.document.update({ + 'system.attached': updatedAttached + }); + + // Copy ALL effects from attached item to actor (only if weapon is equipped) + // Both attachment-only and regular effects should be copied when attached + const actor = this.document.parent; + if (actor && item.effects.size > 0 && this.document.system.equipped) { + console.log(`Checking ${item.effects.size} effects from attached item ${item.name}`); + + const effectsToCreate = []; + for (const effect of item.effects) { + // Copy ALL effects when item is attached - attachment-only flag only matters for non-attached items + const effectData = effect.toObject(); + effectData.origin = `${this.document.uuid}:${newUUID}`; // Track which weapon and which item this came from + effectData.flags = { + ...effectData.flags, + daggerheart: { + ...effectData.flags?.daggerheart, + attachmentSource: { + weaponUuid: this.document.uuid, + itemUuid: newUUID, + originalEffectId: effect.id + } + } + }; + effectsToCreate.push(effectData); + + const isAttachmentOnly = effect.flags?.daggerheart?.attachmentOnly === true; + console.log(`Effect ${effect.name} (attachment-only: ${isAttachmentOnly}) will be copied to actor`); + } + + if (effectsToCreate.length > 0) { + const createdEffects = await actor.createEmbeddedDocuments('ActiveEffect', effectsToCreate); + console.log(`Created ${createdEffects.length} effects on actor from attached item`); + } else { + console.log(`No effects found on ${item.name}, no effects copied to actor`); + } + } else if (item.effects.size > 0 && !this.document.system.equipped) { + console.log(`Weapon ${this.document.name} is not equipped, attachment effects will be applied when equipped`); + } + + console.log(`Weapon updated successfully`); + } + + /** + * Remove an attached item + * @param {Event} event - The click event + * @param {HTMLElement} target - The clicked element + */ + static async #removeAttachment(event, target) { + const uuid = target.dataset.uuid; + const currentAttached = this.document.system.attached || []; + + // Remove the attachment from the weapon + await this.document.update({ + 'system.attached': currentAttached.filter(attachedUuid => attachedUuid !== uuid) + }); + + // Remove any effects on the actor that came from this attached item + const actor = this.document.parent; + if (actor) { + const effectsToRemove = actor.effects.filter(effect => { + const attachmentSource = effect.flags?.daggerheart?.attachmentSource; + return attachmentSource && + attachmentSource.weaponUuid === this.document.uuid && + attachmentSource.itemUuid === uuid; + }); + + if (effectsToRemove.length > 0) { + console.log(`Removing ${effectsToRemove.length} effects from actor that came from detached item ${uuid}`); + await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); + } + } + } } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 3fb562e0..ad8a8c48 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -66,6 +66,7 @@ export default class DHWeapon extends BaseDataItem { } } }), + attached: new fields.ArrayField(new fields.DocumentUUIDField({ type: "Item", nullable: true })), actions: new fields.ArrayField(new ActionField()) }; } @@ -78,6 +79,11 @@ export default class DHWeapon extends BaseDataItem { 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); + } + if (changes.system?.features) { const removed = this.features.filter(x => !changes.system.features.includes(x)); const added = changes.system.features.filter(x => !this.features.includes(x)); @@ -116,4 +122,60 @@ export default class DHWeapon extends BaseDataItem { } } } + + /** + * Handle adding/removing attachment effects when weapon is equipped/unequipped + * @param {boolean} newEquippedStatus - The new equipped status + */ + async _handleAttachmentEffectsOnEquipChange(newEquippedStatus) { + const actor = this.parent.parent; + if (!actor || !this.attached?.length) return; + + if (newEquippedStatus) { + // Weapon is being equipped - add attachment effects + console.log(`Weapon ${this.parent.name} being equipped, adding attachment effects`); + + const effectsToCreate = []; + for (const attachedUuid of this.attached) { + const attachedItem = await fromUuid(attachedUuid); + if (attachedItem && attachedItem.effects.size > 0) { + 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 = `${this.parent.uuid}:${attachedUuid}`; + effectData.flags = { + ...effectData.flags, + daggerheart: { + ...effectData.flags?.daggerheart, + attachmentSource: { + weaponUuid: this.parent.uuid, + itemUuid: attachedUuid, + originalEffectId: effect.id + } + } + }; + effectsToCreate.push(effectData); + } + } + } + + if (effectsToCreate.length > 0) { + await actor.createEmbeddedDocuments('ActiveEffect', effectsToCreate); + console.log(`Created ${effectsToCreate.length} attachment effects on actor`); + } + } else { + // Weapon is being unequipped - remove attachment effects + console.log(`Weapon ${this.parent.name} being unequipped, removing attachment effects`); + + const effectsToRemove = actor.effects.filter(effect => { + const attachmentSource = effect.flags?.daggerheart?.attachmentSource; + return attachmentSource && attachmentSource.weaponUuid === this.parent.uuid; + }); + + if (effectsToRemove.length > 0) { + await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); + console.log(`Removed ${effectsToRemove.length} attachment effects from actor`); + } + } + } } diff --git a/templates/sheets/items/weapon/attachments.hbs b/templates/sheets/items/weapon/attachments.hbs new file mode 100644 index 00000000..5b632d8b --- /dev/null +++ b/templates/sheets/items/weapon/attachments.hbs @@ -0,0 +1,30 @@ +
+
+ {{localize tabs.attachments.label}} + + {{#if attachedItems}} +
+ {{#each attachedItems as |item|}} +
+ {{item.name}} +
+
{{item.name}}
+
+
+ +
+
+ {{/each}} +
+ {{/if}} + +
+ + Drop items here to attach them +
+
+