From e77f538ab79de0c50e2dc93a099c780a565289c1 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:46:44 +0100 Subject: [PATCH 01/10] Fixed so that using the loop button actually increases/decreases max on looping instead of cocatenating to string (#1314) --- module/applications/ui/countdowns.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index d7d3d25f..f9f7036c 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -184,9 +184,9 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application const countdown = settings.countdowns[target.id]; const newMax = countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.increasing.id - ? countdown.progress.max + 1 + ? Number(countdown.progress.max) + 1 : countdown.progress.looping === CONFIG.DH.GENERAL.countdownLoopingTypes.decreasing.id - ? Math.max(countdown.progress.max - 1, 0) + ? Math.max(Number(countdown.progress.max) - 1, 0) : countdown.progress.max; await settings.updateSource({ [`countdowns.${target.id}.progress`]: { From 87643dc662b4c0a8b436ec5d1bbc7a5040ca9a33 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 22 Nov 2025 15:24:11 +0100 Subject: [PATCH 02/10] Fixed so that effects applied to self will work again (#1320) --- module/data/fields/action/effectsField.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index 887607ba..528a8658 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -53,9 +53,10 @@ export default class EffectsField extends fields.ArrayField { if (this.hasSave && token.saved.success === true) effects = this.effects.filter(e => e.onSave === true); if (!effects.length) return; - const token = canvas.tokens.get(baseToken.id); + const token = + canvas.tokens.get(baseToken.id) ?? foundry.utils.fromUuidSync(baseToken.actorId).prototypeToken; if (!token) return; - messageTargets.push(token.document); + messageTargets.push(token.document ?? token); effects.forEach(async e => { const effect = this.item.effects.get(e._id); From 9f7554cdffd67c1cf9c6fc730e520544b90b5087 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sun, 23 Nov 2025 02:19:57 +0100 Subject: [PATCH 03/10] [Feature] DiceSoNice Fonts (#1318) * Added a font choice to system diceSoNice settings * . --- lang/en.json | 3 ++- .../applications/settings/appearanceSettings.mjs | 1 + module/data/settings/Appearance.mjs | 3 ++- .../settings/appearance-settings/diceSoNice.hbs | 14 +++++++++----- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lang/en.json b/lang/en.json index a63f25dd..490b7419 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2445,7 +2445,8 @@ "texture": "Texture", "colorset": "Theme", "material": "Material", - "system": "Dice Preset" + "system": "Dice Preset", + "font": "Font" } }, "variantRules": { diff --git a/module/applications/settings/appearanceSettings.mjs b/module/applications/settings/appearanceSettings.mjs index 5950f961..9eb0cfbf 100644 --- a/module/applications/settings/appearanceSettings.mjs +++ b/module/applications/settings/appearanceSettings.mjs @@ -144,6 +144,7 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App context.diceSoNiceSystems = Object.fromEntries( [...game.dice3d.DiceFactory.systems].map(([k, v]) => [k, v.name]) ); + context.diceSoNiceFonts = game.dice3d.exports.Utils.prepareFontList(); foundry.utils.mergeObject( context.dsnTabs, diff --git a/module/data/settings/Appearance.mjs b/module/data/settings/Appearance.mjs index 7a5c730a..2b8d3b27 100644 --- a/module/data/settings/Appearance.mjs +++ b/module/data/settings/Appearance.mjs @@ -14,7 +14,8 @@ export default class DhAppearance extends foundry.abstract.DataModel { texture: new StringField({ initial: 'astralsea', required: true, blank: false }), colorset: new StringField({ initial: 'inspired', required: true, blank: false }), material: new StringField({ initial: 'metal', required: true, blank: false }), - system: new StringField({ initial: 'standard', required: true, blank: false }) + system: new StringField({ initial: 'standard', required: true, blank: false }), + font: new StringField({ initial: 'auto', required: true, blank: false }) }); return { diff --git a/templates/settings/appearance-settings/diceSoNice.hbs b/templates/settings/appearance-settings/diceSoNice.hbs index 6321332d..6c89fd2f 100644 --- a/templates/settings/appearance-settings/diceSoNice.hbs +++ b/templates/settings/appearance-settings/diceSoNice.hbs @@ -53,13 +53,17 @@ {{formInput fields.material value=values.material choices=@root.diceSoNiceMaterials localize=true}} -
+ {{/each}} From 2920ead81a2153d68cf5c15a9e6aa75e85748059 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:04:42 +0100 Subject: [PATCH 04/10] [Fix] Inventory Item Transfer (#1316) * Fixed so items from the inventory tab of the Party sheet can be dragged out * Added transfer logic * Added translation * Improved item transfer dialog title * Simplified title * Updated image * Simplified the handlebars templates for itemTransfer * Improved item-identicial check * Slight improved rendering * . --- lang/en.json | 5 +- module/applications/dialogs/_module.mjs | 1 + module/applications/dialogs/itemTransfer.mjs | 70 +++++++++++++++++++ .../applications/sheets/actors/character.mjs | 10 ++- module/applications/sheets/actors/party.mjs | 26 ++++--- module/applications/sheets/api/base-actor.mjs | 65 +++++++++++++++++ module/applications/sheets/api/base-item.mjs | 47 ++++++------- module/applications/sheets/items/class.mjs | 4 +- module/helpers/utils.mjs | 8 +++ styles/less/dialog/index.less | 2 + styles/less/dialog/item-transfer/sheet.less | 20 ++++++ templates/dialogs/item-transfer.hbs | 9 +++ .../global/partials/feature-section-item.hbs | 2 +- templates/sheets/items/ancestry/features.hbs | 2 + templates/sheets/items/class/settings.hbs | 4 +- 15 files changed, 228 insertions(+), 47 deletions(-) create mode 100644 module/applications/dialogs/itemTransfer.mjs create mode 100644 styles/less/dialog/item-transfer/sheet.less create mode 100644 templates/dialogs/item-transfer.hbs diff --git a/lang/en.json b/lang/en.json index 490b7419..2ee02386 100755 --- a/lang/en.json +++ b/lang/en.json @@ -464,6 +464,9 @@ "title": "Select Image", "selectImage": "Select Image" }, + "ItemTransfer": { + "transfer": "Transfer" + }, "Levelup": { "actions": { "creatureComfort": { @@ -2651,7 +2654,7 @@ "cardTooHighLevel": "The card is too high level!", "duplicateCard": "You cannot select the same card more than once.", "duplicateCharacter": "This actor is already registered in the party members list.", - "onlyCharactersInPartySheet": "You can only drag characters, companions and adverasries to the party sheet.", + "onlyCharactersInPartySheet": "You can only drag characters, companions and adversaries to the party sheet.", "notPrimary": "The weapon is not a primary weapon!", "notSecondary": "The weapon is not a secondary weapon!", "itemTooHighTier": "The item must be from Tier1", diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index 43faa68a..92038c41 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -6,6 +6,7 @@ export { default as DamageReductionDialog } from './damageReductionDialog.mjs'; export { default as DeathMove } from './deathMove.mjs'; export { default as Downtime } from './downtime.mjs'; export { default as ImageSelectDialog } from './imageSelectDialog.mjs'; +export { default as ItemTransferDialog } from './itemTransfer.mjs'; export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as RerollDamageDialog } from './rerollDamageDialog.mjs'; diff --git a/module/applications/dialogs/itemTransfer.mjs b/module/applications/dialogs/itemTransfer.mjs new file mode 100644 index 00000000..aba43d27 --- /dev/null +++ b/module/applications/dialogs/itemTransfer.mjs @@ -0,0 +1,70 @@ +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class ItemTransferDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(item) { + super({}); + + this.item = item; + this.quantity = item.system.quantity; + } + + get title() { + return this.item.name; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dh-style', 'dialog', 'item-transfer'], + position: { width: 300, 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' } + }; + + _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(); + } + + static async #finish() { + this.close({ submitted: true }); + } + + close(options = {}) { + if (!options.submitted) this.quantity = null; + + super.close(); + } + + static async configure(item) { + return new Promise(resolve => { + const app = new this(item); + app.addEventListener('close', () => resolve(app.quantity), { once: true }); + app.render({ force: true }); + }); + } +} diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 85267944..a9db2fc4 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -8,7 +8,6 @@ import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ -const { TextEditor } = foundry.applications.ux; export default class CharacterSheet extends DHBaseActorSheet { /**@inheritdoc */ static DEFAULT_OPTIONS = { @@ -881,6 +880,8 @@ export default class CharacterSheet extends DHBaseActorSheet { const item = await getDocFromElement(event.target); const dragData = { + originActor: this.document.uuid, + originId: item.id, type: item.documentName, uuid: item.uuid }; @@ -894,9 +895,12 @@ export default class CharacterSheet extends DHBaseActorSheet { // Prevent event bubbling to avoid duplicate handling event.preventDefault(); event.stopPropagation(); + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - super._onDrop(event); - this._onDropItem(event, TextEditor.getDragEventData(event)); + const { cancel } = await super._onDrop(event); + if (cancel) return; + + this._onDropItem(event, data); } async _onDropItem(event, data) { diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index 9b48a797..8a0b756d 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -7,7 +7,6 @@ import { socketEvent } from '../../../systemRegistration/socket.mjs'; import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs'; import DhpActor from '../../../documents/actor.mjs'; import DHItem from '../../../documents/item.mjs'; -import DhParty from '../../../data/actor/party.mjs'; export default class Party extends DHBaseActorSheet { constructor(options) { @@ -21,7 +20,7 @@ export default class Party extends DHBaseActorSheet { classes: ['party'], position: { width: 550, - height: 900, + height: 900 }, window: { resizable: true @@ -41,7 +40,7 @@ export default class Party extends DHBaseActorSheet { selectRefreshable: DaggerheartMenu.selectRefreshable, refreshActors: DaggerheartMenu.refreshActors }, - dragDrop: [{ dragSelector: '.actors-section .inventory-item', dropSelector: null }] + dragDrop: [{ dragSelector: '[data-item-id][draggable="true"]', dropSelector: null }] }; /**@override */ @@ -439,23 +438,28 @@ export default class Party extends DHBaseActorSheet { } /* -------------------------------------------- */ - async _onDragStart(event) { - const item = event.currentTarget.closest('.inventory-item'); + const item = await getDocFromElement(event.target); + const dragData = { + originActor: this.document.uuid, + originId: item.id, + type: item.documentName, + uuid: item.uuid + }; - if (item) { - const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid }; - event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData)); - event.dataTransfer.setDragImage(item, 60, 0); - } + 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; + const document = await foundry.utils.fromUuid(data.uuid); if (document instanceof DhpActor && Party.ALLOWED_ACTOR_TYPES.includes(document.type)) { diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index e1226416..e11d841d 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -1,3 +1,4 @@ +import { itemIsIdentical } from '../../../helpers/utils.mjs'; import DHBaseActorSettings from './actor-setting.mjs'; import DHApplicationMixin from './application-mixin.mjs'; @@ -217,6 +218,70 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { /* Application Drag/Drop */ /* -------------------------------------------- */ + async _onDrop(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']; + 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], { render: false }); + const createData = dropDocument.toObject(); + await this.document.createEmbeddedDocuments('Item', [createData]); + } + } + } + } + + return { cancel }; + } + /** * On dragStart on the item. * @param {DragEvent} event - The drag event diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 21ccbbf1..e46522c0 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -33,9 +33,9 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { removeResource: DHBaseItemSheet.#removeResource }, dragDrop: [ - { dragSelector: null, dropSelector: '.tab.features .drop-section' }, + { dragSelector: null, dropSelector: '.drop-section' }, { dragSelector: '.feature-item', dropSelector: null }, - { dragSelector: '.action-item', dropSelector: null } + { dragSelector: '.inventory-item', dropSelector: null } ], contextMenus: [ { @@ -242,37 +242,30 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { * @param {DragEvent} event - The drag event */ async _onDragStart(event) { + /* Can prolly be improved a lot, but I don't wanna >_< */ const featureItem = event.currentTarget.closest('.feature-item'); + const inventoryItem = event.currentTarget.closest('.inventory-item'); + const lineItem = event.currentTarget.closest('.item-line'); + const dragItemData = featureItem ?? inventoryItem ?? lineItem; - if (featureItem) { - const feature = this.document.system.features.find(x => x?.id === featureItem.id); - if (!feature) { + const dragItem = await foundry.utils.fromUuid(dragItemData.dataset.itemUuid); + if (dragItem) { + if (!dragItem) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); return; } - const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true }; - event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); - event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); - } else { - const actionItem = event.currentTarget.closest('.action-item'); - if (actionItem) { - const action = this.document.system.actions[actionItem.dataset.index]; - if (!action) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionIsMissing')); - return; - } - - const actionData = { - type: 'Action', - data: { ...action.toObject(), id: action.id, itemUuid: this.document.uuid }, - fromInternal: true + let dragData = {}; + if (dragItemData.dataset.type === 'effect') + dragData = { + type: 'ActiveEffect', + fromInternal: this.document.uuid, + data: { ...dragItem, uuid: dragItem.uuid, id: dragItem.id } }; - event.dataTransfer.setData('text/plain', JSON.stringify(actionData)); - event.dataTransfer.setDragImage(actionItem.querySelector('img'), 60, 0); - } else { - super._onDragStart(event); - } + else dragData = { type: 'Item', uuid: dragItem.uuid, id: dragItem.id, fromInternal: this.document.id }; + + event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); + event.dataTransfer.setDragImage(dragItemData.querySelector('img'), 60, 0); } } @@ -284,7 +277,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { super._onDrop(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - if (data.fromInternal) return; + if (data.fromInternal === this.document.id) return; const target = event.target.closest('fieldset.drop-section'); let item = await fromUuid(data.uuid); diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index e7e00c42..05bb0229 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -125,8 +125,8 @@ export default class ClassSheet extends DHBaseItemSheet { async _onDrop(event) { event.stopPropagation(); const data = TextEditor.getDragEventData(event); - const item = data.data ?? (await fromUuid(data.uuid)); - const itemType = data.data ? data.type : item.type; + const item = await fromUuid(data.uuid); + const itemType = data.type === 'ActiveEffect' ? data.type : item.type; const target = event.target.closest('fieldset.drop-section'); if (itemType === 'subclass') { if (item.system.linkedClass) { diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index f1bec45c..c0ca20f9 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -437,3 +437,11 @@ export function shuffleArray(array) { return array; } + +export function itemIsIdentical(a, b) { + const compendiumSource = a._stats.compendiumSource === b._stats.compendiumSource; + const name = a.name === b.name; + const description = a.system.description === b.system.description; + + return compendiumSource && name & description; +} diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 0f2b053b..91b2846b 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -35,3 +35,5 @@ @import './tag-team-dialog/sheet.less'; @import './image-select/sheet.less'; + +@import './item-transfer/sheet.less'; diff --git a/styles/less/dialog/item-transfer/sheet.less b/styles/less/dialog/item-transfer/sheet.less new file mode 100644 index 00000000..dd55fdb2 --- /dev/null +++ b/styles/less/dialog/item-transfer/sheet.less @@ -0,0 +1,20 @@ +.daggerheart.dh-style.dialog.item-transfer { + .item-transfer-container { + display: grid; + grid-template-columns: 1fr 42px; + gap: 4px; + + .number-display { + text-align: center; + } + } + + .item-sheet-footer { + padding-top: 8px; + display: flex; + + button { + flex: 1; + } + } +} diff --git a/templates/dialogs/item-transfer.hbs b/templates/dialogs/item-transfer.hbs new file mode 100644 index 00000000..2b9fa19c --- /dev/null +++ b/templates/dialogs/item-transfer.hbs @@ -0,0 +1,9 @@ +