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/4] [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/4] 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/4] 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}}
From 0cc1597dfe1a37566180dca810f892c28556610e Mon Sep 17 00:00:00 2001
From: WBHarry <89362246+WBHarry@users.noreply.github.com>
Date: Thu, 17 Jul 2025 19:43:35 +0200
Subject: [PATCH 4/4] [Fix] Class Feature Change (#364)
* Changed to just use a features field
* Subclass now uses a simple Features field
---
lang/en.json | 11 ++-
.../characterCreation/characterCreation.mjs | 4 +-
module/applications/sheets/items/ancestry.mjs | 25 ++-----
module/applications/sheets/items/class.mjs | 48 ++++++++-----
module/applications/sheets/items/subclass.mjs | 69 +++++++++++++++----
module/config/actorConfig.mjs | 2 +-
module/config/itemConfig.mjs | 7 +-
module/data/actor/character.mjs | 17 ++---
module/data/item/class.mjs | 17 +++--
module/data/item/domainCard.mjs | 1 -
module/data/item/subclass.mjs | 22 +++---
.../sheets/items/domainCard/settings.hbs | 2 -
templates/sheets/items/subclass/features.hbs | 30 ++++----
13 files changed, 158 insertions(+), 97 deletions(-)
diff --git a/lang/en.json b/lang/en.json
index 3456b6b5..80cc67f7 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -1325,8 +1325,8 @@
},
"DomainCard": {
"type": "Type",
- "foundation": "Foundation",
"recallCost": "Recall Cost",
+ "foundationTitle": "Foundation",
"specializationTitle": "Specialization",
"masteryTitle": "Mastery"
},
@@ -1541,7 +1541,14 @@
"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"
+ "unownedAttackMacro": "Cannot make a Use macro for an Attack that doesn't belong to one of your characters",
+ "featureNotHope": "This feature is used as something else than a Hope feature and cannot be used here.",
+ "featureNotClass": "This feature is used as something else than a Class feature and cannot be used here.",
+ "featureNotPrimary": "This feature is used as something else than a Primary feature and cannot be used here.",
+ "featureNotSecondary": "This feature is used as something else than a Secondary feature and cannot be used here.",
+ "featureNotFoundation": "This feature is used as something else than a Foundation feature and cannot be used here.",
+ "featureNotSpecialization": "This feature is used as something else than a Specialization feature and cannot be used here.",
+ "featureNotMastery": "This feature is used as something else than a Mastery feature and cannot be used here."
},
"Tooltip": {
"openItemWorld": "Open Item World",
diff --git a/module/applications/characterCreation/characterCreation.mjs b/module/applications/characterCreation/characterCreation.mjs
index ed0ee5a7..b8759cc5 100644
--- a/module/applications/characterCreation/characterCreation.mjs
+++ b/module/applications/characterCreation/characterCreation.mjs
@@ -506,9 +506,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
name: this.setup.ancestryName ?? this.setup.primaryAncestry.name,
system: {
...this.setup.primaryAncestry.system,
- features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid],
- primaryFeature: primaryAncestryFeature.uuid,
- secondaryFeature: secondaryAncestryFeature.uuid
+ features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid]
}
};
diff --git a/module/applications/sheets/items/ancestry.mjs b/module/applications/sheets/items/ancestry.mjs
index bd9e3792..de55301d 100644
--- a/module/applications/sheets/items/ancestry.mjs
+++ b/module/applications/sheets/items/ancestry.mjs
@@ -63,22 +63,8 @@ export default class AncestrySheet extends DHHeritageSheet {
event.stopPropagation();
const target = button.closest('.feature-item');
const feature = this.document.system[`${target.dataset.type}Feature`];
- const featureExists = feature && Object.keys(feature).length > 0;
- if (featureExists) {
- const confirmed = await foundry.applications.api.DialogV2.confirm({
- window: {
- title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
- type: game.i18n.localize(`TYPES.Item.feature`),
- name: feature.name
- })
- },
- content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name })
- });
- if (!confirmed) return;
- }
-
- if (featureExists && target.dataset.type === 'primary') await feature.update({ 'system.primary': null });
+ if (feature) await feature.update({ 'system.subType': null });
await this.document.update({
'system.features': this.document.system.features.filter(x => x && x.uuid !== feature.uuid).map(x => x.uuid)
});
@@ -94,15 +80,18 @@ export default class AncestrySheet extends DHHeritageSheet {
*/
async _onDrop(event) {
event.stopPropagation();
- event.preventDefault();
-
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (item?.type === 'feature') {
const subType = event.target.closest('.primary-feature') ? 'primary' : 'secondary';
- await item.update({ 'system.subType': subType });
+ if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes[subType]) {
+ const error = subType === 'primary' ? 'featureNotPrimary' : 'featureNotSecondary';
+ ui.notifications.warn(game.i18n.localize(`DAGGERHEART.UI.Notifications.${error}`));
+ return;
+ }
+ await item.update({ 'system.subType': subType });
await this.document.update({
'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs
index b49105be..3a34bcec 100644
--- a/module/applications/sheets/items/class.mjs
+++ b/module/applications/sheets/items/class.mjs
@@ -78,6 +78,7 @@ export default class ClassSheet extends DHBaseItemSheet {
/* -------------------------------------------- */
async _onDrop(event) {
+ event.stopPropagation();
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
const target = event.target.closest('fieldset.drop-section');
@@ -87,12 +88,24 @@ export default class ClassSheet extends DHBaseItemSheet {
});
} else if (item.type === 'feature') {
if (target.classList.contains('hope-feature')) {
+ if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.hope) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotHope'));
+ return;
+ }
+
+ await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.hope });
await this.document.update({
- 'system.hopeFeatures': [...this.document.system.hopeFeatures.map(x => x.uuid), item.uuid]
+ 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
} else if (target.classList.contains('class-feature')) {
+ if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.class) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotClass'));
+ return;
+ }
+
+ await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.class });
await this.document.update({
- 'system.classFeatures': [...this.document.system.classFeatures.map(x => x.uuid), item.uuid]
+ 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
});
}
} else if (item.type === 'weapon') {
@@ -177,28 +190,25 @@ export default class ClassSheet extends DHBaseItemSheet {
doc.sheet.render({ force: true });
}
- getActionPath(type) {
- return type === 'hope' ? 'hopeFeatures' : 'classFeatures';
- }
-
static async addFeature(_, target) {
- const actionPath = this.getActionPath(target.dataset.type);
const feature = await game.items.documentClass.create({
type: 'feature',
- name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') })
+ name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }),
+ system: {
+ subType:
+ target.dataset.type === 'hope'
+ ? CONFIG.DH.ITEM.featureSubTypes.hope
+ : CONFIG.DH.ITEM.featureSubTypes.class
+ }
});
await this.document.update({
- [`system.${actionPath}`]: [
- ...this.document.system[actionPath].filter(x => x).map(x => x.uuid),
- feature.uuid
- ]
+ [`system.features`]: [...this.document.system.features.filter(x => x).map(x => x.uuid), feature.uuid]
});
}
static async editFeature(_, button) {
const target = button.closest('.feature-item');
- const actionPath = this.getActionPath(button.dataset.type);
- const feature = this.document.system[actionPath].find(x => x?.id === target.dataset.featureId);
+ const feature = this.document.system.features.find(x => x?.id === target.dataset.featureId);
if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return;
@@ -210,10 +220,16 @@ export default class ClassSheet extends DHBaseItemSheet {
static async deleteFeature(event, button) {
event.stopPropagation();
const target = button.closest('.feature-item');
- const actionPath = this.getActionPath(button.dataset.type);
+
+ const feature = this.document.system.features.find(
+ feature => feature && feature.id === target.dataset.featureId
+ );
+ if (feature) {
+ await feature.update({ 'system.subType': null });
+ }
await this.document.update({
- [`system.${actionPath}`]: this.document.system[actionPath]
+ [`system.features`]: this.document.system.features
.filter(feature => feature && feature.id !== target.dataset.featureId)
.map(x => x.uuid)
});
diff --git a/module/applications/sheets/items/subclass.mjs b/module/applications/sheets/items/subclass.mjs
index 31eca43a..fcf62f69 100644
--- a/module/applications/sheets/items/subclass.mjs
+++ b/module/applications/sheets/items/subclass.mjs
@@ -40,28 +40,46 @@ export default class SubclassSheet extends DHBaseItemSheet {
static async addFeature(_, target) {
const feature = await game.items.documentClass.create({
type: 'feature',
- name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') })
+ name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }),
+ system: {
+ subType:
+ target.dataset.type === 'foundation'
+ ? CONFIG.DH.ITEM.featureSubTypes.foundation
+ : target.dataset.type === 'specialization'
+ ? CONFIG.DH.ITEM.featureSubTypes.specialization
+ : CONFIG.DH.ITEM.featureSubTypes.mastery
+ }
});
await this.document.update({
- [`system.${target.dataset.type}`]: feature.uuid
+ [`system.features`]: [...this.document.system.features.map(x => x.uuid), feature.uuid]
});
}
static async editFeature(_, button) {
- const feature = this.document.system[button.dataset.type];
+ const feature = this.document.system.features.find(x => x.id === button.dataset.feature);
if (!feature) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
return;
}
+ if (feature) {
+ await feature.update({ 'system.subType': null });
+ }
+
feature.sheet.render(true);
}
- static async deleteFeature(event, button) {
+ static async deleteFeature(event, target) {
event.stopPropagation();
+ const feature = this.document.system.features.find(feature => feature.id === target.dataset.feature);
+ if (feature) {
+ await feature.update({ 'system.subType': null });
+ }
await this.document.update({
- [`system.${button.dataset.type}`]: null
+ [`system.features`]: this.document.system.features
+ .filter(feature => feature && feature.id !== target.dataset.feature)
+ .map(x => x.uuid)
});
}
@@ -82,18 +100,45 @@ export default class SubclassSheet extends DHBaseItemSheet {
}
async _onDrop(event) {
+ event.stopPropagation();
+
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal) return;
const item = await fromUuid(data.uuid);
- if (item?.type === 'feature') {
- const dropSection = event.target.closest('.drop-section');
- if (this.document.system[dropSection.dataset.type]) {
- ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsFull'));
- return;
- }
+ const target = event.target.closest('fieldset.drop-section');
+ if (item.type === 'feature') {
+ if (target.dataset.type === 'foundation') {
+ if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.foundation) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotFoundation'));
+ return;
+ }
- await this.document.update({ [`system.${dropSection.dataset.type}`]: item.uuid });
+ await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.foundation });
+ await this.document.update({
+ 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
+ });
+ } else if (target.dataset.type === 'specialization') {
+ if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.specialization) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotSpecialization'));
+ return;
+ }
+
+ await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.specialization });
+ await this.document.update({
+ 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
+ });
+ } else if (target.dataset.type === 'mastery') {
+ if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.mastery) {
+ ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotMastery'));
+ return;
+ }
+
+ await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.mastery });
+ await this.document.update({
+ 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid]
+ });
+ }
}
}
}
diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs
index 02cfd4a9..d05dc81c 100644
--- a/module/config/actorConfig.mjs
+++ b/module/config/actorConfig.mjs
@@ -411,7 +411,7 @@ export const levelupData = {
};
export const subclassFeatureLabels = {
- 1: 'DAGGERHEART.ITEMS.DomainCard.foundation',
+ 1: 'DAGGERHEART.ITEMS.DomainCard.foundationTitle',
2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle',
3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle'
};
diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs
index ede5ef08..b26a26ca 100644
--- a/module/config/itemConfig.mjs
+++ b/module/config/itemConfig.mjs
@@ -1322,7 +1322,12 @@ export const featureTypes = {
export const featureSubTypes = {
primary: 'primary',
- secondary: 'secondary'
+ secondary: 'secondary',
+ hope: 'hope',
+ class: 'class',
+ foundation: 'foundation',
+ specialization: 'specialization',
+ mastery: 'mastery'
};
export const actionTypes = {
diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs
index 6b0a2fd7..1c15d036 100644
--- a/module/data/actor/character.mjs
+++ b/module/data/actor/character.mjs
@@ -124,12 +124,9 @@ export default class DhCharacter extends BaseDataActor {
label: 'DAGGERHEART.GENERAL.Range.other'
})
}),
- rally: new fields.ArrayField(
- new fields.StringField(),
- {
- label: 'DAGGERHEART.CLASS.Feature.rallyDice'
- }
- )
+ rally: new fields.ArrayField(new fields.StringField(), {
+ label: 'DAGGERHEART.CLASS.Feature.rallyDice'
+ })
}),
companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }),
rules: new fields.SchemaField({
@@ -260,11 +257,11 @@ export default class DhCharacter extends BaseDataActor {
classFeatures.push(item);
} else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) {
const subclassState = this.class.subclass.system.featureState;
- const identifier = item.system.identifier;
+ const subType = item.system.subType;
if (
- identifier === 'foundationFeature' ||
- (identifier === 'specializationFeature' && subclassState >= 2) ||
- (identifier === 'masterFeature' && subclassState >= 3)
+ subType === CONFIG.DH.ITEM.featureSubTypes.foundation ||
+ (subType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) ||
+ (subType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3)
) {
subclassFeatures.push(item);
}
diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs
index 281b0a48..b8a9ab81 100644
--- a/module/data/item/class.mjs
+++ b/module/data/item/class.mjs
@@ -27,8 +27,7 @@ export default class DHClass extends BaseDataItem {
label: 'DAGGERHEART.GENERAL.hitPoints.plural'
}),
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
- hopeFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
- classFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
+ features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
inventory: new fields.SchemaField({
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
@@ -52,12 +51,18 @@ export default class DHClass extends BaseDataItem {
};
}
- get hopeFeature() {
- return this.hopeFeatures.length > 0 ? this.hopeFeatures[0] : null;
+ get hopeFeatures() {
+ return (
+ this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.hope) ??
+ (this.features.filter(x => !x).length > 0 ? {} : null)
+ );
}
- get features() {
- return [...this.hopeFeatures.filter(x => x), ...this.classFeatures.filter(x => x)];
+ get classFeatures() {
+ return (
+ this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.class) ??
+ (this.features.filter(x => !x).length > 0 ? {} : null)
+ );
}
async _preCreate(data, options, user) {
diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs
index 89bbfb40..df60b9d1 100644
--- a/module/data/item/domainCard.mjs
+++ b/module/data/item/domainCard.mjs
@@ -29,7 +29,6 @@ export default class DHDomainCard extends BaseDataItem {
required: true,
initial: CONFIG.DH.DOMAIN.cardTypes.ability.id
}),
- foundation: new fields.BooleanField({ initial: false }),
inVault: new fields.BooleanField({ initial: false }),
actions: new fields.ArrayField(new ActionField())
};
diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs
index 265c2566..e0a76092 100644
--- a/module/data/item/subclass.mjs
+++ b/module/data/item/subclass.mjs
@@ -1,4 +1,4 @@
-import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
+import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import BaseDataItem from './base.mjs';
export default class DHSubclass extends BaseDataItem {
@@ -22,20 +22,22 @@ export default class DHSubclass extends BaseDataItem {
nullable: true,
initial: null
}),
- foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
- specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
- masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }),
+ features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
isMulticlass: new fields.BooleanField({ initial: false })
};
}
- get features() {
- return [
- { ...this.foundationFeature?.toObject(), identifier: 'foundationFeature' },
- { ...this.specializationFeature?.toObject(), identifier: 'specializationFeature' },
- { ...this.masteryFeature?.toObject(), identifier: 'masteryFeature' }
- ];
+ get foundationFeatures() {
+ return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.foundation);
+ }
+
+ get specializationFeatures() {
+ return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.specialization);
+ }
+
+ get masteryFeatures() {
+ return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.mastery);
}
async _preCreate(data, options, user) {
diff --git a/templates/sheets/items/domainCard/settings.hbs b/templates/sheets/items/domainCard/settings.hbs
index 2faa6934..5518b4c3 100644
--- a/templates/sheets/items/domainCard/settings.hbs
+++ b/templates/sheets/items/domainCard/settings.hbs
@@ -8,8 +8,6 @@
{{localize "DAGGERHEART.GENERAL.type"}}
{{formField systemFields.type value=source.system.type localize=true}}
- {{localize "DAGGERHEART.ITEMS.DomainCard.foundation"}}
- {{formField systemFields.foundation value=source.system.foundation }}
{{localize "DAGGERHEART.GENERAL.Domain.single"}}
{{formField systemFields.domain value=source.system.domain localize=true}}
{{localize "DAGGERHEART.GENERAL.level"}}
diff --git a/templates/sheets/items/subclass/features.hbs b/templates/sheets/items/subclass/features.hbs
index d2424d01..1a75974e 100644
--- a/templates/sheets/items/subclass/features.hbs
+++ b/templates/sheets/items/subclass/features.hbs
@@ -3,42 +3,42 @@
data-tab='{{tabs.features.id}}'
data-group='{{tabs.features.group}}'
>
-
+
{{localize "DAGGERHEART.GENERAL.Tabs.foundation"}}
-
+
- {{#if source.system.foundationFeature}}
- {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='foundationFeature' feature=source.system.foundationFeature}}
- {{/if}}
+ {{#each source.system.foundationFeatures as | feature | }}
+ {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='foundation' feature=feature}}
+ {{/each}}
-
+
{{localize "DAGGERHEART.GENERAL.Tabs.specialization"}}
-
+
- {{#if source.system.specializationFeature}}
- {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='specializationFeature' feature=source.system.specializationFeature}}
- {{/if}}
+ {{#each source.system.specializationFeatures as | feature |}}
+ {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='specialization' feature=feature}}
+ {{/each}}
-
+
{{localize "DAGGERHEART.GENERAL.Tabs.mastery"}}
-
+
- {{#if source.system.masteryFeature}}
- {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='masteryFeature' feature=source.system.masteryFeature}}
- {{/if}}
+ {{#each source.system.masteryFeatures as | feature |}}
+ {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='mastery' feature=feature}}
+ {{/each}}
\ No newline at end of file