[PR] [Feature] Support drag dropping currencies to actor sheets (#1431)

* Support drag dropping currencies to actor sheets

* Adjust sizing and spacing

* Restore ItemTransferDialog subclass for module use

* Bigger is better
This commit is contained in:
Carlos Fernandez 2025-12-21 11:37:00 -05:00 committed by GitHub
parent f786ee5f06
commit 99d0eab5bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 128 additions and 70 deletions

View file

@ -1,69 +1,61 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class ItemTransferDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(item) {
constructor(data) {
super({});
this.item = item;
this.quantity = item.system.quantity;
this.data = data;
}
get title() {
return this.item.name;
return this.data.title;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'item-transfer'],
position: { width: 300, height: 'auto' },
position: { width: 400, height: 'auto' },
window: { icon: 'fa-solid fa-hand-holding-hand' },
actions: {
finish: ItemTransferDialog.#finish
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
}
};
static PARTS = {
main: { template: 'systems/daggerheart/templates/dialogs/item-transfer.hbs' }
main: { template: 'systems/daggerheart/templates/dialogs/item-transfer.hbs', root: true }
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
htmlElement.querySelector('.number-display').addEventListener('change', event => {
this.quantity = isNaN(event.target.value) ? this.quantity : Number(event.target.value);
this.render();
});
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.item = this.item;
context.quantity = this.quantity;
return context;
}
static async updateData(_event, _element, formData) {
const { quantity } = foundry.utils.expandObject(formData.object);
this.quantity = quantity;
this.render();
return foundry.utils.mergeObject(context, this.data);
}
static async #finish() {
this.close({ submitted: true });
this.selected = this.form.elements.quantity.valueAsNumber || null;
this.close();
}
close(options = {}) {
if (!options.submitted) this.quantity = null;
static #determineTransferOptions({ originActor, targetActor, item, currency }) {
originActor ??= item?.actor;
const homebrewKey = CONFIG.DH.SETTINGS.gameSettings.Homebrew;
const currencySetting = game.settings.get(CONFIG.DH.id, homebrewKey).currency?.[currency] ?? null;
super.close();
return {
originActor,
targetActor,
itemImage: item?.img,
currencyIcon: currencySetting?.icon,
max: item?.system.quantity ?? originActor.system.gold[currency] ?? 0,
title: item?.name ?? currencySetting?.label
};
}
static async configure(item) {
static async configure(options) {
return new Promise(resolve => {
const app = new this(item);
app.addEventListener('close', () => resolve(app.quantity), { once: true });
const data = this.#determineTransferOptions(options);
if (data.max <= 1) return resolve(data.max);
const app = new this(data);
app.addEventListener('close', () => resolve(app.selected), { once: true });
app.render({ force: true });
});
}

View file

@ -34,7 +34,10 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
}
}
],
dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
dragDrop: [
{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null },
{ dragSelector: ".currency[data-currency] .drag-handle", dropSelector: null }
]
};
/* -------------------------------------------- */
@ -254,14 +257,35 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
/* Application Drag/Drop */
/* -------------------------------------------- */
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.type === 'Currency' && ['character', 'party'].includes(this.document.type)) {
const originActor = await foundry.utils.fromUuid(data.originActor);
if (!originActor || originActor.uuid === this.document.uuid) return;
const currency = data.currency;
const quantity = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
originActor,
targetActor: this.document,
currency
});
if (quantity) {
originActor.update({ [`system.gold.${currency}`]: Math.max(0, originActor.system.gold[currency] - quantity) });
this.document.update({ [`system.gold.${currency}`]: this.document.system.gold[currency] + quantity });
}
return;
}
return super._onDrop(event);
}
async _onDropItem(event, item) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const physicalActorTypes = ['character', 'party'];
const originActor = item.actor;
if (
item.actor?.uuid === this.document.uuid ||
!originActor ||
!physicalActorTypes.includes(this.document.type)
!['character', 'party'].includes(this.document.type)
) {
return super._onDropItem(event, item);
}
@ -270,10 +294,10 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
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);
const quantityTransfered = await game.system.api.applications.dialogs.ItemTransferDialog.configure({
item,
targetActor: this.document
});
if (quantityTransfered) {
if (quantityTransfered === actorItem.system.quantity) {
@ -314,6 +338,16 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
* @param {DragEvent} event - The drag event
*/
async _onDragStart(event) {
// Handle drag/dropping currencies
const currencyEl = event.currentTarget.closest(".currency[data-currency]");
if (currencyEl) {
const currency = currencyEl.dataset.currency;
const data = { type: 'Currency', currency, originActor: this.document.uuid };
event.dataTransfer.setData('text/plain', JSON.stringify(data));
return;
}
// Handle drag/dropping attacks
const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
if (attackItem) {
const attackData = {
@ -340,4 +374,4 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
super._onDragStart(event);
}
}
}

View file

@ -1,20 +1,35 @@
.daggerheart.dh-style.dialog.item-transfer {
.item-transfer-container {
display: grid;
grid-template-columns: 1fr 42px;
gap: 4px;
.summary {
display: flex;
align-items: center;
justify-content: center;
gap: 3rem;
.actor-img {
border-radius: 50%;
width: 5rem;
height: 5rem;
object-fit: cover;
object-position: top center;
}
.number-display {
text-align: center;
.granted-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: var(--font-size-32);
img {
width: 2rem;
height: 2rem;
object-fit: contain;
border-radius: 3px;
}
}
}
.item-sheet-footer {
padding-top: 8px;
display: flex;
button {
flex: 1;
}
label {
flex: 0;
margin-right: 0.5rem;
}
}

View file

@ -1,9 +1,26 @@
<div>
<div class="item-transfer-container">
<input type="range" step="1" min="0" max="{{item.system.quantity}}" name="quantity" value="{{quantity}}" />
<input type="text" value="{{quantity}}" class="number-display" />
<div class="dialog-content standard-form">
<div class="summary">
<img class="actor-img" src="{{originActor.img}}" />
<div class="granted-item">
{{#if itemImage}}
<img src="{{itemImage}}" />
{{else}}
<i inert class="currency-icon {{currencyIcon}}"></i>
{{/if}}
<i inert class="fa-solid fa-hand-holding"></i>
</div>
<img class="actor-img" src="{{targetActor.img}}" />
</div>
<footer class="item-sheet-footer">
<button type="button" data-action="finish">{{localize "DAGGERHEART.APPLICATIONS.ItemTransfer.transfer"}}</button>
</footer>
<div class="form-group">
<label>{{localize "DAGGERHEART.GENERAL.quantity"}}</label>
<div class="form-fields">
<range-picker step="1" min="1" max="{{max}}" name="quantity" value="{{max}}"></range-picker>
</div>
</div>
</div>
<div class="dialog-buttons">
<button type="button" data-action="finish">
<i inert class="fa-solid fa-gift"></i>
{{localize "DAGGERHEART.APPLICATIONS.ItemTransfer.transfer"}}
</button>
</div>

View file

@ -14,10 +14,10 @@
{{#if this.inventory.hasCurrency}}
<div class="currency-section">
{{#each this.inventory.currencies as | currency |}}
{{#each this.inventory.currencies as |currency key|}}
{{#if currency.enabled}}
<div class="input">
<span>
<div class="input currency" data-currency="{{key}}">
<span class="drag-handle">
<i class="{{currency.icon}}" inert></i> {{localize currency.label}}
</span>
<input type="text" name="{{currency.field.fieldPath}}" data-allow-delta value="{{currency.value}}" data-dtype="Number" min="0" step="1" />

View file

@ -17,10 +17,10 @@
{{#if inventory.hasCurrency}}
<div class="currency-section">
{{#each this.inventory.currencies as | currency |}}
{{#each this.inventory.currencies as |currency key|}}
{{#if currency.enabled}}
<div class="input">
<span>
<div class="input currency" data-currency="{{key}}">
<span class="drag-handle">
<i class="{{currency.icon}}" inert></i> {{localize currency.label}}
</span>
<input type="text" name="{{currency.field.fieldPath}}" data-allow-delta value="{{currency.value}}" data-dtype="Number" min="0" step="1" />