[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
This commit is contained in:
Carlos Fernandez 2025-12-06 06:05:10 -08:00 committed by GitHub
parent 2171c1b433
commit b57e98071f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 151 additions and 195 deletions

View file

@ -25,7 +25,8 @@ export default class AdversarySheet extends DHBaseActorSheet {
action: 'editAttribution' action: 'editAttribution'
} }
] ]
} },
dragDrop: [{ dragSelector: '[data-item-id][draggable="true"]', dropSelector: null }]
}; };
static PARTS = { static PARTS = {

View file

@ -214,34 +214,8 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.resources.stress.emptyPips = context.resources.stress.emptyPips =
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0; 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'); 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; return context;
} }
@ -903,47 +877,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
}); });
} }
async _onDragStart(event) { async _onDropItem(event, item) {
const item = await getDocFromElement(event.target); if (this.document.uuid === item.parent?.uuid) {
return super._onDropItem(event, 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);
}
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)
);
} }
if (item.type === 'beastform') { 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); const data = await game.system.api.data.items.DHBeastform.getWildcardImage(this.document, itemData);
if (data) { if (!data?.selectedImage) {
if (!data.selectedImage) return; return;
else { } else if (data) {
if (data.usesDynamicToken) itemData.system.tokenRingImg = data.selectedImage; if (data.usesDynamicToken) itemData.system.tokenRingImg = data.selectedImage;
else itemData.system.tokenImg = 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); // If this is a type that gets deleted, delete it first (but still defer to super)
const createdItem = await this._onDropItemCreate(itemData); 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) { async _onDropItemCreate(itemData, event) {

View file

@ -130,12 +130,13 @@ export default class DhpEnvironment extends DHBaseActorSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
async _onDragStart(event) { async _onDragStart(event) {
const item = event.currentTarget.closest('.inventory-item'); const item = event.currentTarget.closest('.inventory-item[data-type=adversary]');
if (item) { if (item) {
const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid }; const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData)); event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
event.dataTransfer.setDragImage(item, 60, 0); event.dataTransfer.setDragImage(item, 60, 0);
} else {
return super._onDragStart(event);
} }
} }

View file

@ -93,25 +93,6 @@ export default class Party extends DHBaseActorSheet {
/* Prepare Context */ /* 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) { async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { 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)); async _onDropActor(event, document) {
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 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)) { if (document instanceof DhpActor && Party.ALLOWED_ACTOR_TYPES.includes(document.type)) {
const currentMembers = this.document.system.partyMembers.map(x => x.uuid); const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
if (currentMembers.includes(data.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] }); await this.document.update({ 'system.partyMembers': [...currentMembers, document.uuid] });
} else if (document instanceof DHItem) {
this.document.createEmbeddedDocuments('Item', [document.toObject()]);
} else { } else {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet'));
} }
return null;
} }
static async #deletePartyMember(event, target) { static async #deletePartyMember(event, target) {

View file

@ -322,10 +322,10 @@ export default function DHApplicationMixin(Base) {
_onDrop(event) { _onDrop(event) {
event.stopPropagation(); event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal === this.document.uuid) return; if (data.type === 'ActiveEffect' && data.fromInternal !== this.document.uuid) {
if (data.type === 'ActiveEffect') {
this.document.createEmbeddedDocuments('ActiveEffect', [data.data]); this.document.createEmbeddedDocuments('ActiveEffect', [data.data]);
} else {
return super._onDrop(event);
} }
} }

View file

@ -1,4 +1,4 @@
import { itemIsIdentical } from '../../../helpers/utils.mjs'; import { getDocFromElement, itemIsIdentical } from '../../../helpers/utils.mjs';
import DHBaseActorSettings from './actor-setting.mjs'; import DHBaseActorSettings from './actor-setting.mjs';
import DHApplicationMixin from './application-mixin.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) context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance)
.hideAttribution; .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; return context;
} }
@ -218,68 +240,59 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* Application Drag/Drop */ /* Application Drag/Drop */
/* -------------------------------------------- */ /* -------------------------------------------- */
async _onDrop(event) { async _onDropItem(event, item) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); 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']; const physicalActorTypes = ['character', 'party'];
if (physicalActorTypes.includes(this.document.type)) { const originActor = item.actor;
const originActor = data.originActor ? await foundry.utils.fromUuid(data.originActor) : null; if (
if (data.originId && originActor && physicalActorTypes.includes(originActor.type)) { item.actor?.uuid === this.document.uuid ||
const dropDocument = await foundry.utils.fromUuid(data.uuid); !originActor ||
!physicalActorTypes.includes(this.document.type)
if (dropDocument.system.metadata.isInventoryItem) { ) {
cancel = true; return super._onDropItem(event, item);
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]);
}
}
}
} }
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) { async _onDragStart(event) {
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]'); const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
if (attackItem) { if (attackItem) {
const attackData = { const attackData = {
type: 'Attack', type: 'Attack',
@ -298,8 +310,20 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}; };
event.dataTransfer.setData('text/plain', JSON.stringify(attackData)); event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0); event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
} else if (this.document.type !== 'environment') { return;
super._onDragStart(event); }
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);
} }
} }

View file

@ -141,6 +141,10 @@ export default class DhpAdversary extends BaseDataActor {
return this.parent.items.filter(x => x.type === 'feature'); return this.parent.items.filter(x => x.type === 'feature');
} }
isItemValid(source) {
return source.type === "feature";
}
async _preUpdate(changes, options, user) { async _preUpdate(changes, options, user) {
const allowed = await super._preUpdate(changes, options, user); const allowed = await super._preUpdate(changes, options, user);
if (allowed === false) return false; if (allowed === false) return false;

View file

@ -108,6 +108,10 @@ export default class DhCompanion extends BaseDataActor {
get proficiency() { get proficiency() {
return this.partner?.system?.proficiency ?? 1; return this.partner?.system?.proficiency ?? 1;
} }
isItemValid() {
return false;
}
prepareBaseData() { prepareBaseData() {
this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0; this.attack.roll.bonus = this.partner?.system?.spellcastModifier ?? 0;

View file

@ -51,4 +51,8 @@ export default class DhEnvironment extends BaseDataActor {
get features() { get features() {
return this.parent.items.filter(x => x.type === 'feature'); return this.parent.items.filter(x => x.type === 'feature');
} }
isItemValid(source) {
return source.type === "feature";
}
} }

View file

@ -25,6 +25,10 @@ export default class DhParty extends BaseDataActor {
/* -------------------------------------------- */ /* -------------------------------------------- */
isItemValid(source) {
return ["weapon", "armor", "consumable", "loot"].includes(source.type);
}
prepareBaseData() { prepareBaseData() {
super.prepareBaseData(); super.prepareBaseData();

View file

@ -66,6 +66,10 @@ export default class DHDomainCard extends BaseDataItem {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateDomainCard')); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateDomainCard'));
return false; return false;
} }
if (!this.actor.system.loadoutSlot.available) {
data.system.inVault = true;
}
} }
} }

View file

@ -28,6 +28,13 @@ export default class DHItem extends foundry.documents.Item {
return doc; 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 */ /** @inheritDoc */

View file

@ -27,7 +27,7 @@
{{> 'daggerheart.inventory-items' {{> 'daggerheart.inventory-items'
title='TYPES.Item.weapon' title='TYPES.Item.weapon'
type='weapon' type='weapon'
collection=document.itemTypes.weapon collection=@root.inventory.weapons
isGlassy=true isGlassy=true
canCreate=true canCreate=true
hideResources=true hideResources=true
@ -35,7 +35,7 @@
{{> 'daggerheart.inventory-items' {{> 'daggerheart.inventory-items'
title='TYPES.Item.armor' title='TYPES.Item.armor'
type='armor' type='armor'
collection=document.itemTypes.armor collection=@root.inventory.armor
isGlassy=true isGlassy=true
canCreate=true canCreate=true
hideResources=true hideResources=true
@ -43,14 +43,14 @@
{{> 'daggerheart.inventory-items' {{> 'daggerheart.inventory-items'
title='TYPES.Item.consumable' title='TYPES.Item.consumable'
type='consumable' type='consumable'
collection=document.itemTypes.consumable collection=@root.inventory.consumables
isGlassy=true isGlassy=true
canCreate=true canCreate=true
}} }}
{{> 'daggerheart.inventory-items' {{> 'daggerheart.inventory-items'
title='TYPES.Item.loot' title='TYPES.Item.loot'
type='loot' type='loot'
collection=document.itemTypes.loot collection=@root.inventory.loot
isGlassy=true isGlassy=true
canCreate=true canCreate=true
showActions=true showActions=true

View file

@ -52,7 +52,7 @@
title='TYPES.Item.weapon' title='TYPES.Item.weapon'
type='weapon' type='weapon'
actorType='party' actorType='party'
collection=document.itemTypes.weapon collection=@root.inventory.weapons
isGlassy=true isGlassy=true
canCreate=true canCreate=true
hideResources=true hideResources=true
@ -62,7 +62,7 @@
title='TYPES.Item.armor' title='TYPES.Item.armor'
type='armor' type='armor'
actorType='party' actorType='party'
collection=document.itemTypes.armor collection=@root.inventory.armor
isGlassy=true isGlassy=true
canCreate=true canCreate=true
hideResources=true hideResources=true
@ -72,7 +72,7 @@
title='TYPES.Item.consumable' title='TYPES.Item.consumable'
type='consumable' type='consumable'
actorType='party' actorType='party'
collection=document.itemTypes.consumable collection=@root.inventory.consumables
isGlassy=true isGlassy=true
canCreate=true canCreate=true
hideContextMenu=true hideContextMenu=true
@ -81,7 +81,7 @@
title='TYPES.Item.loot' title='TYPES.Item.loot'
type='loot' type='loot'
actorType='party' actorType='party'
collection=document.itemTypes.loot collection=@root.inventory.loot
isGlassy=true isGlassy=true
canCreate=true canCreate=true
hideContextMenu=true hideContextMenu=true