From d7e024be02b3b938c0dc898ed0fa8eeb2fea8954 Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Thu, 17 Jul 2025 19:01:15 +0200
Subject: [PATCH 1/3] [Feature] Advantage/Disadvantage Tooltips (#362)
* Added advantage/disadvantageSource to Character model. It's shown from a tooltip icon on rolls
* Added support for beastform advantageOn
---
module/data/actor/character.mjs | 2 ++
module/data/item/beastform.mjs | 1 +
module/documents/tooltipManager.mjs | 18 ++++++++++++++++++
.../less/dialog/dice-roll/roll-selection.less | 4 ++++
templates/dialogs/dice-roll/rollSelection.hbs | 6 ++++++
templates/ui/tooltip/advantage.hbs | 5 +++++
6 files changed, 36 insertions(+)
create mode 100644 templates/ui/tooltip/advantage.hbs
diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs
index 20bada01..6fe66a53 100644
--- a/module/data/actor/character.mjs
+++ b/module/data/actor/character.mjs
@@ -87,6 +87,8 @@ export default class DhCharacter extends BaseDataActor {
value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }),
subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true })
}),
+ advantageSources: new fields.ArrayField(new fields.StringField()),
+ disadvantageSources: new fields.ArrayField(new fields.StringField()),
levelData: new fields.EmbeddedDataField(DhLevelData),
bonuses: new fields.SchemaField({
roll: new fields.SchemaField({
diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs
index b7ea5cb9..d19521d0 100644
--- a/module/data/item/beastform.mjs
+++ b/module/data/item/beastform.mjs
@@ -69,6 +69,7 @@ export default class DHBeastform extends BaseDataItem {
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
await beastformEffect.updateSource({
+ changes: [...beastformEffect.changes, { key: 'system.advantageSources', mode: 2, value: this.advantageOn }],
system: {
characterTokenData: {
tokenImg: this.parent.parent.prototypeToken.texture.src,
diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs
index d9444207..f24823f4 100644
--- a/module/documents/tooltipManager.mjs
+++ b/module/documents/tooltipManager.mjs
@@ -21,6 +21,24 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti
this.tooltip.innerHTML = html;
options.direction = this._determineItemTooltipDirection(element);
}
+ } else {
+ const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#');
+ const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#');
+ if (isAdvantage || isDisadvantage) {
+ const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14);
+ const actor = await foundry.utils.fromUuid(actorUuid);
+
+ if (actor) {
+ html = await foundry.applications.handlebars.renderTemplate(
+ `systems/daggerheart/templates/ui/tooltip/advantage.hbs`,
+ {
+ sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources
+ }
+ );
+
+ this.tooltip.innerHTML = html;
+ }
+ }
}
super.activate(element, { ...options, html: html });
diff --git a/styles/less/dialog/dice-roll/roll-selection.less b/styles/less/dialog/dice-roll/roll-selection.less
index 55db8bb7..af6c3c20 100644
--- a/styles/less/dialog/dice-roll/roll-selection.less
+++ b/styles/less/dialog/dice-roll/roll-selection.less
@@ -100,6 +100,10 @@
font-size: 14px;
line-height: 17px;
}
+
+ .advantage-chip-tooltip {
+ pointer-events: all;
+ }
}
.advantage-chip {
diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs
index b4c7ccac..7c894fd8 100644
--- a/templates/dialogs/dice-roll/rollSelection.hbs
+++ b/templates/dialogs/dice-roll/rollSelection.hbs
@@ -98,6 +98,9 @@
{{/if}}
{{localize "DAGGERHEART.GENERAL.Advantage.full"}}
+ {{#if @root.rollConfig.data.advantageSources.length}}
+
+ {{/if}}
{{#if (eq advantage -1)}}
@@ -106,6 +109,9 @@
{{/if}}
{{localize "DAGGERHEART.GENERAL.Disadvantage.full"}}
+ {{#if @root.rollConfig.data.disadvantageSources.length}}
+
+ {{/if}}
{{#unless (eq @root.rollType 'D20Roll')}}
diff --git a/templates/ui/tooltip/advantage.hbs b/templates/ui/tooltip/advantage.hbs
new file mode 100644
index 00000000..886f336d
--- /dev/null
+++ b/templates/ui/tooltip/advantage.hbs
@@ -0,0 +1,5 @@
+
\ No newline at end of file
From f15483c7229a782c6b3b8f9be1d2d0f79673e105 Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Thu, 17 Jul 2025 19:03:54 +0200
Subject: [PATCH 2/3] Feature/349 items actions macro use (#356)
* Added itemUse macro on drag to hotbar
* Fixed item.type logic
* Added support for actionMacro drag from items
* Added MacroDrag for Attacks
* Fixed so UseItem macros get the img set
---
daggerheart.mjs | 1 +
lang/en.json | 6 +-
module/applications/sheets/api/base-actor.mjs | 25 +++-
module/applications/sheets/api/base-item.mjs | 20 ++-
module/applications/ui/_module.mjs | 1 +
module/applications/ui/hotbar.mjs | 129 ++++++++++++++++++
module/data/countdowns.mjs | 4 +-
7 files changed, 181 insertions(+), 5 deletions(-)
create mode 100644 module/applications/ui/hotbar.mjs
diff --git a/daggerheart.mjs b/daggerheart.mjs
index 0d78e8fe..22586e90 100644
--- a/daggerheart.mjs
+++ b/daggerheart.mjs
@@ -140,6 +140,7 @@ Hooks.once('init', () => {
CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = applications.ui.DhCombatTracker;
CONFIG.ui.chat = applications.ui.DhChatLog;
+ CONFIG.ui.hotbar = applications.ui.DhHotbar;
CONFIG.Token.rulerClass = placeables.DhTokenRuler;
CONFIG.ui.resources = applications.ui.DhFearTracker;
diff --git a/lang/en.json b/lang/en.json
index 75f2b886..3456b6b5 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -1537,7 +1537,11 @@
"noAvailableArmorMarks": "You have no more available armor marks",
"notEnoughStress": "You don't have enough stress",
"damageIgnore": "{character} did not take damage",
- "featureIsMissing": "Feature is missing"
+ "featureIsMissing": "Feature is missing",
+ "actionIsMissing": "Action is missing",
+ "attackIsMissing": "Attack is missing",
+ "unownedActionMacro": "Cannot make a Use macro for an Action not on your character",
+ "unownedAttackMacro": "Cannot make a Use macro for an Attack that doesn't belong to one of your characters"
},
"Tooltip": {
"openItemWorld": "Open Item World",
diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs
index 7102fa1c..78df0aac 100644
--- a/module/applications/sheets/api/base-actor.mjs
+++ b/module/applications/sheets/api/base-actor.mjs
@@ -23,7 +23,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
actions: {
openSettings: DHBaseActorSheet.#openSettings
},
- dragDrop: []
+ dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }]
};
/**@type {typeof DHBaseActorSettings}*/
@@ -49,4 +49,27 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) {
static async #openSettings() {
await this.settingSheet.render({ force: true });
}
+
+ /* -------------------------------------------- */
+ /* Application Drag/Drop */
+ /* -------------------------------------------- */
+
+ /**
+ * On dragStart on the item.
+ * @param {DragEvent} event - The drag event
+ */
+ async _onDragStart(event) {
+ const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]');
+
+ if (attackItem) {
+ const attackData = {
+ type: 'Attack',
+ actorUuid: this.document.uuid,
+ img: this.document.system.attack.img,
+ fromInternal: true
+ };
+ event.dataTransfer.setData('text/plain', JSON.stringify(attackData));
+ event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0);
+ }
+ }
}
diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs
index 0c25a44d..723b4802 100644
--- a/module/applications/sheets/api/base-item.mjs
+++ b/module/applications/sheets/api/base-item.mjs
@@ -30,7 +30,8 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
},
dragDrop: [
{ dragSelector: null, dropSelector: '.tab.features .drop-section' },
- { dragSelector: '.feature-item', dropSelector: null }
+ { dragSelector: '.feature-item', dropSelector: null },
+ { dragSelector: '.action-item', dropSelector: null }
]
};
@@ -258,6 +259,23 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
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
+ };
+ event.dataTransfer.setData('text/plain', JSON.stringify(actionData));
+ event.dataTransfer.setDragImage(actionItem.querySelector('img'), 60, 0);
+ }
}
}
diff --git a/module/applications/ui/_module.mjs b/module/applications/ui/_module.mjs
index f1c32840..6a17a61e 100644
--- a/module/applications/ui/_module.mjs
+++ b/module/applications/ui/_module.mjs
@@ -2,3 +2,4 @@ export { default as DhChatLog } from './chatLog.mjs';
export { default as DhCombatTracker } from './combatTracker.mjs';
export * as DhCountdowns from './countdowns.mjs';
export { default as DhFearTracker } from './fearTracker.mjs';
+export { default as DhHotbar } from './hotbar.mjs';
diff --git a/module/applications/ui/hotbar.mjs b/module/applications/ui/hotbar.mjs
new file mode 100644
index 00000000..b4ebc05c
--- /dev/null
+++ b/module/applications/ui/hotbar.mjs
@@ -0,0 +1,129 @@
+export default class DhHotbar extends foundry.applications.ui.Hotbar {
+ constructor(options) {
+ super(options);
+
+ this.setupHooks();
+ }
+
+ static async useItem(uuid) {
+ const item = await fromUuid(uuid);
+ if (!item) {
+ return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
+ format: {
+ name: game.i18n.localize('Document'),
+ identifier: uuid
+ }
+ });
+ }
+
+ await item.use({});
+ }
+
+ static async useAction(itemUuid, actionId) {
+ const item = await foundry.utils.fromUuid(itemUuid);
+ if (!item) {
+ return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
+ format: {
+ name: game.i18n.localize('Document'),
+ identifier: itemUuid
+ }
+ });
+ }
+
+ const action = item.system.actions.find(x => x.id === actionId);
+ if (!action) {
+ return ui.notifications.warn('DAGGERHEART.UI.Notifications.actionIsMissing');
+ }
+
+ await action.use({});
+ }
+
+ static async useAttack(actorUuid) {
+ const actor = await foundry.utils.fromUuid(actorUuid);
+ if (!actor) {
+ return ui.notifications.warn('WARNING.ObjectDoesNotExist', {
+ format: {
+ name: game.i18n.localize('Document'),
+ identifier: actorUuid
+ }
+ });
+ }
+
+ const attack = actor.system.attack;
+ if (!attack) {
+ return ui.notifications.warn('DAGGERHEART.UI.Notifications.attackIsMissing');
+ }
+
+ await attack.use({});
+ }
+
+ setupHooks() {
+ Hooks.on('hotbarDrop', (bar, data, slot) => {
+ if (data.type === 'Item') {
+ const item = foundry.utils.fromUuidSync(data.uuid);
+ if (item.uuid.startsWith('Compendium') || !item.isOwned || !item.isOwner) return true;
+
+ switch (item.type) {
+ case 'ancestry':
+ case 'community':
+ case 'class':
+ case 'subclass':
+ return true;
+ default:
+ this.createItemMacro(item, slot);
+ return false;
+ }
+ } else if (data.type === 'Action') {
+ const item = foundry.utils.fromUuidSync(data.data.itemUuid);
+ if (item.uuid.startsWith('Compendium')) return true;
+ if (!item.isOwned || !item.isOwner) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedActionMacro'));
+ return false;
+ }
+
+ this.createActionMacro(data, slot);
+ return false;
+ } else if (data.type === 'Attack') {
+ const actor = foundry.utils.fromUuidSync(data.actorUuid);
+ if (actor.uuid.startsWith('Compendium')) return true;
+ if (!actor.isOwner) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedAttackMacro'));
+ return false;
+ }
+
+ this.createAttackMacro(data, slot);
+ return false;
+ }
+ });
+ }
+
+ async createItemMacro(data, slot) {
+ const macro = await Macro.implementation.create({
+ name: `${game.i18n.localize('Display')} ${name}`,
+ type: CONST.MACRO_TYPES.SCRIPT,
+ img: data.img,
+ command: `await game.system.api.applications.ui.DhHotbar.useItem("${data.uuid}");`
+ });
+ await game.user.assignHotbarMacro(macro, slot);
+ }
+
+ async createActionMacro(data, slot) {
+ const macro = await Macro.implementation.create({
+ name: `${game.i18n.localize('Display')} ${name}`,
+ type: CONST.MACRO_TYPES.SCRIPT,
+ img: data.data.img,
+ command: `await game.system.api.applications.ui.DhHotbar.useAction("${data.data.itemUuid}", "${data.data.id}");`
+ });
+ await game.user.assignHotbarMacro(macro, slot);
+ }
+
+ async createAttackMacro(data, slot) {
+ const macro = await Macro.implementation.create({
+ name: `${game.i18n.localize('Display')} ${name}`,
+ type: CONST.MACRO_TYPES.SCRIPT,
+ img: data.img,
+ command: `await game.system.api.applications.ui.DhHotbar.useAttack("${data.actorUuid}");`
+ });
+ await game.user.assignHotbarMacro(macro, slot);
+ }
+}
diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs
index 881ecf20..34e8b790 100644
--- a/module/data/countdowns.mjs
+++ b/module/data/countdowns.mjs
@@ -135,8 +135,8 @@ export const registerCountdownHooks = () => {
if (application) {
foundry.applications.instances.get(application)?.render();
} else {
- foundry.applications.instances.get('narrative-countdowns').render();
- foundry.applications.instances.get('encounter-countdowns').render();
+ foundry.applications.instances.get('narrative-countdowns')?.render();
+ foundry.applications.instances.get('encounter-countdowns')?.render();
}
return false;
From 1d5e26728540b065034a4e9551dad65b28adef91 Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Thu, 17 Jul 2025 19:07:11 +0200
Subject: [PATCH 3/3] Improved the datastructure some to avoid errors and
simplify useage (#361)
---
module/applications/dialogs/resourceDiceDialog.mjs | 2 +-
module/data/actor/character.mjs | 10 +++++-----
module/data/item/base.mjs | 7 +++++--
templates/dialogs/dice-roll/resourceDice.hbs | 8 ++++----
templates/sheets/global/partials/item-resource.hbs | 2 +-
templates/sheets/global/partials/resource-section.hbs | 2 +-
6 files changed, 17 insertions(+), 14 deletions(-)
diff --git a/module/applications/dialogs/resourceDiceDialog.mjs b/module/applications/dialogs/resourceDiceDialog.mjs
index b79ff895..8205dee5 100644
--- a/module/applications/dialogs/resourceDiceDialog.mjs
+++ b/module/applications/dialogs/resourceDiceDialog.mjs
@@ -67,7 +67,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli
static async rerollDice() {
const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item);
- const diceFormula = `${max}d${this.item.system.resource.dieFaces}`;
+ const diceFormula = `${max}${this.item.system.resource.dieFaces}`;
const roll = await new Roll(diceFormula).evaluate();
if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true);
this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false }));
diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs
index 6fe66a53..6b0a2fd7 100644
--- a/module/data/actor/character.mjs
+++ b/module/data/actor/character.mjs
@@ -252,13 +252,13 @@ export default class DhCharacter extends BaseDataActor {
features = [];
for (let item of this.parent.items) {
- if (item.system.type === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
+ if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) {
ancestryFeatures.push(item);
- } else if (item.system.type === CONFIG.DH.ITEM.featureTypes.community.id) {
+ } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) {
communityFeatures.push(item);
- } else if (item.system.type === CONFIG.DH.ITEM.featureTypes.class.id) {
+ } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) {
classFeatures.push(item);
- } else if (item.system.type === CONFIG.DH.ITEM.featureTypes.subclass.id) {
+ } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
const subclassState = this.class.subclass.system.featureState;
const identifier = item.system.identifier;
if (
@@ -268,7 +268,7 @@ export default class DhCharacter extends BaseDataActor {
) {
subclassFeatures.push(item);
}
- } else if (item.system.type === CONFIG.DH.ITEM.featureTypes.companion.id) {
+ } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) {
companionFeatures.push(item);
} else if (item.type === 'feature' && !item.system.type) {
features.push(item);
diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs
index e99c85c3..24e5e0cc 100644
--- a/module/data/item/base.mjs
+++ b/module/data/item/base.mjs
@@ -53,11 +53,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
}),
diceStates: new fields.TypedObjectField(
new fields.SchemaField({
- value: new fields.NumberField({ integer: true, nullable: true, initial: null }),
+ value: new fields.NumberField({ integer: true, initial: 1, min: 1 }),
used: new fields.BooleanField({ initial: false })
})
),
- dieFaces: new fields.StringField({ initial: '4' })
+ dieFaces: new fields.StringField({
+ choices: CONFIG.DH.GENERAL.diceTypes,
+ initial: CONFIG.DH.GENERAL.diceTypes.d4
+ })
},
{ nullable: true, initial: null }
);
diff --git a/templates/dialogs/dice-roll/resourceDice.hbs b/templates/dialogs/dice-roll/resourceDice.hbs
index 33c93386..bebe8f4e 100644
--- a/templates/dialogs/dice-roll/resourceDice.hbs
+++ b/templates/dialogs/dice-roll/resourceDice.hbs
@@ -3,14 +3,14 @@
{{#times (rollParsed item.system.resource.max actor item numerical=true)}}
{{#with (ifThen (lookup ../diceStates this) (lookup ../diceStates this) this) as | state |}}
{{/with}}
{{/times}}
- {{localize 'Save'}}
- {{localize "DAGGERHEART.APPLICATIONS.ResourceDice.rerollDice"}}
+ {{localize 'Save'}}
+ {{localize "DAGGERHEART.APPLICATIONS.ResourceDice.rerollDice"}}
\ No newline at end of file
diff --git a/templates/sheets/global/partials/item-resource.hbs b/templates/sheets/global/partials/item-resource.hbs
index 9b92dea0..d90f0b3f 100644
--- a/templates/sheets/global/partials/item-resource.hbs
+++ b/templates/sheets/global/partials/item-resource.hbs
@@ -10,7 +10,7 @@
{{ifThen state.value state.value '?'}}
-
+
{{#if state.used}}
{{/if}}
diff --git a/templates/sheets/global/partials/resource-section.hbs b/templates/sheets/global/partials/resource-section.hbs
index f0d322d3..ab329efc 100644
--- a/templates/sheets/global/partials/resource-section.hbs
+++ b/templates/sheets/global/partials/resource-section.hbs
@@ -19,7 +19,7 @@
{{formGroup systemFields.resource.fields.value value=source.system.resource.value localize=true}}
{{formGroup systemFields.resource.fields.max value=source.system.resource.max localize=true}}
{{else}}
- {{formGroup systemFields.resource.fields.dieFaces value=source.system.resource.dieFaces localize=true}}
+ {{formGroup systemFields.resource.fields.dieFaces value=source.system.resource.dieFaces localize=true blank=false}}
{{formGroup systemFields.resource.fields.max value=source.system.resource.max label="DAGGERHEART.ITEMS.FIELDS.resource.amount.label" localize=true}}
{{/if}}