From b57e98071f237a4b307ed77846f53162b067f979 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Sat, 6 Dec 2025 06:05:10 -0800 Subject: [PATCH] [Feature] Sortable inventories and adversary/environment drag/drop (#1357) * Add ability to sort inventories in player and party sheets * Format base actor sheet * Check item validity when creating on an actor * Block dragdrop on adversaries and environments * Support drag and drop in adversary and environment sheets * Fix regression with dropping to character sheet * Move vault when created handling to domain card preCreate --- .../applications/sheets/actors/adversary.mjs | 3 +- .../applications/sheets/actors/character.mjs | 95 +++-------- .../sheets/actors/environment.mjs | 5 +- module/applications/sheets/actors/party.mjs | 46 +----- .../sheets/api/application-mixin.mjs | 6 +- module/applications/sheets/api/base-actor.mjs | 148 ++++++++++-------- module/data/actor/adversary.mjs | 4 + module/data/actor/companion.mjs | 4 + module/data/actor/environment.mjs | 4 + module/data/actor/party.mjs | 4 + module/data/item/domainCard.mjs | 4 + module/documents/item.mjs | 7 + .../sheets/actors/character/inventory.hbs | 8 +- templates/sheets/actors/party/inventory.hbs | 8 +- 14 files changed, 151 insertions(+), 195 deletions(-) diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index 95d77787..6b6354ef 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -25,7 +25,8 @@ export default class AdversarySheet extends DHBaseActorSheet { action: 'editAttribution' } ] - } + }, + dragDrop: [{ dragSelector: '[data-item-id][draggable="true"]', dropSelector: null }] }; static PARTS = { diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 7da49eb7..b59fc7a4 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -214,34 +214,8 @@ export default class CharacterSheet extends DHBaseActorSheet { context.resources.stress.emptyPips = context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0; - context.inventory = { currencies: {} }; - const { title, ...currencies } = game.settings.get( - CONFIG.DH.id, - CONFIG.DH.SETTINGS.gameSettings.Homebrew - ).currency; - for (let key in currencies) { - context.inventory.currencies[key] = { - ...currencies[key], - field: context.systemFields.gold.fields[key], - value: context.source.system.gold[key] - }; - } - // context.inventory = { - // currency: { - // title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'), - // coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'), - // handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'), - // bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'), - // chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests') - // } - // }; - context.beastformActive = this.document.effects.find(x => x.type === 'beastform'); - // if (context.inventory.length === 0) { - // context.inventory = Array(1).fill(Array(5).fill([])); - // } - return context; } @@ -903,47 +877,9 @@ export default class CharacterSheet extends DHBaseActorSheet { }); } - async _onDragStart(event) { - const item = await getDocFromElement(event.target); - - const dragData = { - originActor: this.document.uuid, - originId: item.id, - type: item.documentName, - uuid: item.uuid - }; - - event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); - - super._onDragStart(event); - } - - async _onDrop(event) { - // Prevent event bubbling to avoid duplicate handling - event.preventDefault(); - event.stopPropagation(); - const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - - const { cancel } = await super._onDrop(event); - if (cancel) return; - - this._onDropItem(event, data); - } - - async _onDropItem(event, data) { - const item = await Item.implementation.fromDropData(data); - const itemData = item.toObject(); - - if (item.type === 'domainCard' && !this.document.system.loadoutSlot.available) { - itemData.system.inVault = true; - } - - const typesThatReplace = ['ancestry', 'community']; - if (typesThatReplace.includes(item.type)) { - await this.document.deleteEmbeddedDocuments( - 'Item', - this.document.items.filter(x => x.type === item.type).map(x => x.id) - ); + async _onDropItem(event, item) { + if (this.document.uuid === item.parent?.uuid) { + return super._onDropItem(event, item); } if (item.type === 'beastform') { @@ -953,20 +889,27 @@ export default class CharacterSheet extends DHBaseActorSheet { ); } + const itemData = item.toObject(); const data = await game.system.api.data.items.DHBeastform.getWildcardImage(this.document, itemData); - if (data) { - if (!data.selectedImage) return; - else { - if (data.usesDynamicToken) itemData.system.tokenRingImg = data.selectedImage; - else itemData.system.tokenImg = data.selectedImage; - } + if (!data?.selectedImage) { + return; + } else if (data) { + if (data.usesDynamicToken) itemData.system.tokenRingImg = data.selectedImage; + else itemData.system.tokenImg = data.selectedImage; + return await this._onDropItemCreate(itemData); } } - if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData); - const createdItem = await this._onDropItemCreate(itemData); + // If this is a type that gets deleted, delete it first (but still defer to super) + const typesThatReplace = ['ancestry', 'community']; + if (typesThatReplace.includes(item.type)) { + await this.document.deleteEmbeddedDocuments( + 'Item', + this.document.items.filter(x => x.type === item.type).map(x => x.id) + ); + } - return createdItem; + return super._onDropItem(event, item); } async _onDropItemCreate(itemData, event) { diff --git a/module/applications/sheets/actors/environment.mjs b/module/applications/sheets/actors/environment.mjs index 98bc873d..9a09cd94 100644 --- a/module/applications/sheets/actors/environment.mjs +++ b/module/applications/sheets/actors/environment.mjs @@ -130,12 +130,13 @@ export default class DhpEnvironment extends DHBaseActorSheet { /* -------------------------------------------- */ async _onDragStart(event) { - const item = event.currentTarget.closest('.inventory-item'); - + const item = event.currentTarget.closest('.inventory-item[data-type=adversary]'); if (item) { const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid }; event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData)); event.dataTransfer.setDragImage(item, 60, 0); + } else { + return super._onDragStart(event); } } diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index 8a0b756d..5c448b49 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -93,25 +93,6 @@ export default class Party extends DHBaseActorSheet { /* Prepare Context */ /* -------------------------------------------- */ - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - - context.inventory = { currencies: {} }; - const { title, ...currencies } = game.settings.get( - CONFIG.DH.id, - CONFIG.DH.SETTINGS.gameSettings.Homebrew - ).currency; - for (let key in currencies) { - context.inventory.currencies[key] = { - ...currencies[key], - field: context.systemFields.gold.fields[key], - value: context.source.system.gold[key] - }; - } - - return context; - } - async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); switch (partId) { @@ -438,30 +419,9 @@ export default class Party extends DHBaseActorSheet { } /* -------------------------------------------- */ - async _onDragStart(event) { - const item = await getDocFromElement(event.target); - const dragData = { - originActor: this.document.uuid, - originId: item.id, - type: item.documentName, - uuid: item.uuid - }; - event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); - super._onDragStart(event); - } - - async _onDrop(event) { - // Prevent event bubbling to avoid duplicate handling - event.preventDefault(); - event.stopPropagation(); + async _onDropActor(event, document) { const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - - const { cancel } = await super._onDrop(event); - if (cancel) return; - - const document = await foundry.utils.fromUuid(data.uuid); - if (document instanceof DhpActor && Party.ALLOWED_ACTOR_TYPES.includes(document.type)) { const currentMembers = this.document.system.partyMembers.map(x => x.uuid); if (currentMembers.includes(data.uuid)) { @@ -469,11 +429,11 @@ export default class Party extends DHBaseActorSheet { } await this.document.update({ 'system.partyMembers': [...currentMembers, document.uuid] }); - } else if (document instanceof DHItem) { - this.document.createEmbeddedDocuments('Item', [document.toObject()]); } else { ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet')); } + + return null; } static async #deletePartyMember(event, target) { diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index b229d249..761f509e 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -322,10 +322,10 @@ export default function DHApplicationMixin(Base) { _onDrop(event) { event.stopPropagation(); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - if (data.fromInternal === this.document.uuid) return; - - if (data.type === 'ActiveEffect') { + if (data.type === 'ActiveEffect' && data.fromInternal !== this.document.uuid) { this.document.createEmbeddedDocuments('ActiveEffect', [data.data]); + } else { + return super._onDrop(event); } } diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 348ffa99..02dcc448 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -1,4 +1,4 @@ -import { itemIsIdentical } from '../../../helpers/utils.mjs'; +import { getDocFromElement, itemIsIdentical } from '../../../helpers/utils.mjs'; import DHBaseActorSettings from './actor-setting.mjs'; import DHApplicationMixin from './application-mixin.mjs'; @@ -69,6 +69,28 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) .hideAttribution; + // Prepare inventory data + if (['party', 'character'].includes(this.document.type)) { + context.inventory = { + currencies: {}, + weapons: this.document.itemTypes.weapon.sort((a, b) => a.sort - b.sort), + armor: this.document.itemTypes.armor.sort((a, b) => a.sort - b.sort), + consumables: this.document.itemTypes.consumable.sort((a, b) => a.sort - b.sort), + loot: this.document.itemTypes.loot.sort((a, b) => a.sort - b.sort) + }; + const { title, ...currencies } = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.Homebrew + ).currency; + for (const key in currencies) { + context.inventory.currencies[key] = { + ...currencies[key], + field: context.systemFields.gold.fields[key], + value: context.source.system.gold[key] + }; + } + } + return context; } @@ -218,68 +240,59 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { /* Application Drag/Drop */ /* -------------------------------------------- */ - async _onDrop(event) { + async _onDropItem(event, item) { const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - if (data.originActor === this.document.uuid) return { cancel: true }; - - /* Handling transfer of inventoryItems */ - let cancel = false; const physicalActorTypes = ['character', 'party']; - if (physicalActorTypes.includes(this.document.type)) { - const originActor = data.originActor ? await foundry.utils.fromUuid(data.originActor) : null; - if (data.originId && originActor && physicalActorTypes.includes(originActor.type)) { - const dropDocument = await foundry.utils.fromUuid(data.uuid); - - if (dropDocument.system.metadata.isInventoryItem) { - cancel = true; - if (dropDocument.system.metadata.isQuantifiable) { - const actorItem = originActor.items.get(data.originId); - const quantityTransfered = - actorItem.system.quantity === 1 - ? 1 - : await game.system.api.applications.dialogs.ItemTransferDialog.configure(dropDocument); - - if (quantityTransfered) { - if (quantityTransfered === actorItem.system.quantity) { - await originActor.deleteEmbeddedDocuments('Item', [data.originId]); - } else { - cancel = true; - await actorItem.update({ - 'system.quantity': actorItem.system.quantity - quantityTransfered - }); - } - - const existingItem = this.document.items.find(x => itemIsIdentical(x, dropDocument)); - if (existingItem) { - cancel = true; - await existingItem.update({ - 'system.quantity': existingItem.system.quantity + quantityTransfered - }); - } else { - const createData = dropDocument.toObject(); - await this.document.createEmbeddedDocuments('Item', [ - { - ...createData, - system: { - ...createData.system, - quantity: quantityTransfered - } - } - ]); - } - } else { - cancel = true; - } - } else { - await originActor.deleteEmbeddedDocuments('Item', [data.originId]); - const createData = dropDocument.toObject(); - await this.document.createEmbeddedDocuments('Item', [createData]); - } - } - } + const originActor = item.actor; + if ( + item.actor?.uuid === this.document.uuid || + !originActor || + !physicalActorTypes.includes(this.document.type) + ) { + return super._onDropItem(event, item); } - return { cancel }; + /* Handling transfer of inventoryItems */ + if (item.system.metadata.isInventoryItem) { + if (item.system.metadata.isQuantifiable) { + const actorItem = originActor.items.get(data.originId); + const quantityTransfered = + actorItem.system.quantity === 1 + ? 1 + : await game.system.api.applications.dialogs.ItemTransferDialog.configure(item); + + if (quantityTransfered) { + if (quantityTransfered === actorItem.system.quantity) { + await originActor.deleteEmbeddedDocuments('Item', [data.originId]); + } else { + await actorItem.update({ + 'system.quantity': actorItem.system.quantity - quantityTransfered + }); + } + + const existingItem = this.document.items.find(x => itemIsIdentical(x, item)); + if (existingItem) { + await existingItem.update({ + 'system.quantity': existingItem.system.quantity + quantityTransfered + }); + } else { + const createData = item.toObject(); + await this.document.createEmbeddedDocuments('Item', [ + { + ...createData, + system: { + ...createData.system, + quantity: quantityTransfered + } + } + ]); + } + } + } else { + await originActor.deleteEmbeddedDocuments('Item', [data.originId]); + await this.document.createEmbeddedDocuments('Item', [item.toObject()]); + } + } } /** @@ -288,7 +301,6 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { */ async _onDragStart(event) { const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]'); - if (attackItem) { const attackData = { type: 'Attack', @@ -298,8 +310,20 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { }; event.dataTransfer.setData('text/plain', JSON.stringify(attackData)); event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0); - } else if (this.document.type !== 'environment') { - super._onDragStart(event); + return; + } + + const item = await getDocFromElement(event.target); + if (item) { + const dragData = { + originActor: this.document.uuid, + originId: item.id, + type: item.documentName, + uuid: item.uuid + }; + event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); } + + super._onDragStart(event); } } diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 7d9ac951..bb8df3ee 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -141,6 +141,10 @@ export default class DhpAdversary extends BaseDataActor { return this.parent.items.filter(x => x.type === 'feature'); } + isItemValid(source) { + return source.type === "feature"; + } + async _preUpdate(changes, options, user) { const allowed = await super._preUpdate(changes, options, user); if (allowed === false) return false; diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index cd81fab5..fa1965bd 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -108,6 +108,10 @@ export default class DhCompanion extends BaseDataActor { get proficiency() { return this.partner?.system?.proficiency ?? 1; } + + isItemValid() { + return false; + } prepareBaseData() { this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0; diff --git a/module/data/actor/environment.mjs b/module/data/actor/environment.mjs index ce1df7cd..4ed3819e 100644 --- a/module/data/actor/environment.mjs +++ b/module/data/actor/environment.mjs @@ -51,4 +51,8 @@ export default class DhEnvironment extends BaseDataActor { get features() { return this.parent.items.filter(x => x.type === 'feature'); } + + isItemValid(source) { + return source.type === "feature"; + } } diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs index 18fe9959..b306c486 100644 --- a/module/data/actor/party.mjs +++ b/module/data/actor/party.mjs @@ -25,6 +25,10 @@ export default class DhParty extends BaseDataActor { /* -------------------------------------------- */ + isItemValid(source) { + return ["weapon", "armor", "consumable", "loot"].includes(source.type); + } + prepareBaseData() { super.prepareBaseData(); diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index 5c471ca1..92d8828c 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -66,6 +66,10 @@ export default class DHDomainCard extends BaseDataItem { ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateDomainCard')); return false; } + + if (!this.actor.system.loadoutSlot.available) { + data.system.inVault = true; + } } } diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 33daf52a..2c6d68b5 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -28,6 +28,13 @@ export default class DHItem extends foundry.documents.Item { return doc; } + static async createDocuments(sources, operation) { + // Ensure that items being created are valid to the actor its being added to + const actor = operation.parent; + sources = actor?.system?.isItemValid ? sources.filter((s) => actor.system.isItemValid(s)) : sources; + return super.createDocuments(sources, operation); + } + /* -------------------------------------------- */ /** @inheritDoc */ diff --git a/templates/sheets/actors/character/inventory.hbs b/templates/sheets/actors/character/inventory.hbs index ff595737..52de7f3c 100644 --- a/templates/sheets/actors/character/inventory.hbs +++ b/templates/sheets/actors/character/inventory.hbs @@ -27,7 +27,7 @@ {{> 'daggerheart.inventory-items' title='TYPES.Item.weapon' type='weapon' - collection=document.itemTypes.weapon + collection=@root.inventory.weapons isGlassy=true canCreate=true hideResources=true @@ -35,7 +35,7 @@ {{> 'daggerheart.inventory-items' title='TYPES.Item.armor' type='armor' - collection=document.itemTypes.armor + collection=@root.inventory.armor isGlassy=true canCreate=true hideResources=true @@ -43,14 +43,14 @@ {{> 'daggerheart.inventory-items' title='TYPES.Item.consumable' type='consumable' - collection=document.itemTypes.consumable + collection=@root.inventory.consumables isGlassy=true canCreate=true }} {{> 'daggerheart.inventory-items' title='TYPES.Item.loot' type='loot' - collection=document.itemTypes.loot + collection=@root.inventory.loot isGlassy=true canCreate=true showActions=true diff --git a/templates/sheets/actors/party/inventory.hbs b/templates/sheets/actors/party/inventory.hbs index 9a299536..1596d47e 100644 --- a/templates/sheets/actors/party/inventory.hbs +++ b/templates/sheets/actors/party/inventory.hbs @@ -52,7 +52,7 @@ title='TYPES.Item.weapon' type='weapon' actorType='party' - collection=document.itemTypes.weapon + collection=@root.inventory.weapons isGlassy=true canCreate=true hideResources=true @@ -62,7 +62,7 @@ title='TYPES.Item.armor' type='armor' actorType='party' - collection=document.itemTypes.armor + collection=@root.inventory.armor isGlassy=true canCreate=true hideResources=true @@ -72,7 +72,7 @@ title='TYPES.Item.consumable' type='consumable' actorType='party' - collection=document.itemTypes.consumable + collection=@root.inventory.consumables isGlassy=true canCreate=true hideContextMenu=true @@ -81,7 +81,7 @@ title='TYPES.Item.loot' type='loot' actorType='party' - collection=document.itemTypes.loot + collection=@root.inventory.loot isGlassy=true canCreate=true hideContextMenu=true