From 404640a0a3fa23463101447768c61c4fd2edb3b9 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 1 May 2026 17:45:50 +0200 Subject: [PATCH 1/7] Fixed SRD DireBat experience value --- .../adversary_Dire_Bat_tBWHW00epmMnkawe.json | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/packs/adversaries/adversary_Dire_Bat_tBWHW00epmMnkawe.json b/src/packs/adversaries/adversary_Dire_Bat_tBWHW00epmMnkawe.json index c2064395..a1107f7c 100644 --- a/src/packs/adversaries/adversary_Dire_Bat_tBWHW00epmMnkawe.json +++ b/src/packs/adversaries/adversary_Dire_Bat_tBWHW00epmMnkawe.json @@ -40,7 +40,8 @@ "experiences": { "ti3Z1mq2M92KK4GJ": { "name": "Bloodthirsty", - "description": "" + "description": "", + "value": 3 } }, "bonuses": { @@ -242,27 +243,24 @@ "type": "withinRange", "target": "hostile", "range": "melee" - } + }, + "changes": [ + { + "key": "system.difficulty", + "value": 3, + "priority": null, + "type": "add" + } + ] }, "_id": "qZfNiqw1iAIxeuYg", "img": "icons/commodities/biological/wing-lizard-brown.webp", - "changes": [ - { - "key": "system.difficulty", - "mode": 2, - "value": "3", - "priority": null - } - ], "disabled": false, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "description": "

While flying, the Bat gains a +3 bonus to their Difficulty.

", "origin": null, @@ -274,6 +272,9 @@ "_stats": { "compendiumSource": null }, + "start": null, + "showIcon": 1, + "folder": null, "_key": "!actors.items.effects!tBWHW00epmMnkawe.gx22MpD8fWoi8klZ.qZfNiqw1iAIxeuYg" } ], From b22ce9697dd6c8d809b3cb4f336f4acd9b24a1e7 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 1 May 2026 14:54:18 -0400 Subject: [PATCH 2/7] Fix detection of negative modifiers (#1847) --- module/dice/dhRoll.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 209631f6..83dbbaf2 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -257,7 +257,7 @@ export default class DHRoll extends Roll { if (!roll.terms[i].isDeterministic) continue; const termTotal = roll.terms[i].total; if (typeof termTotal === 'number') { - const multiplier = roll.terms[i - 1]?.operator === ' - ' ? -1 : 1; + const multiplier = roll.terms[i - 1]?.operator === '-' ? -1 : 1; modifierTotal += multiplier * termTotal; } } From 905d1f7e885fb91bcba255348620dac7a0a35d33 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 1 May 2026 20:58:21 +0200 Subject: [PATCH 3/7] Corrected a typo in Greater Earth Elemental and Huge Green Ooze --- ...rsary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json | 2 +- .../adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json b/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json index e59d2683..72ad8ae2 100644 --- a/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json +++ b/src/packs/adversaries/adversary_Greater_Earth_Elemental_dsfB3YhoL5SudvS2.json @@ -249,7 +249,7 @@ "name": "Crushing Blows", "type": "feature", "system": { - "description": "

When the @Lookup[@name] makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the @Lookup[@name] makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "resource": null, "actions": { "0sXciTiPc30v8czv": { diff --git a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json index 1615dec8..183719f2 100644 --- a/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json +++ b/src/packs/adversaries/adversary_Huge_Green_Ooze_6hbqmxDXFOzZJDk4.json @@ -138,12 +138,9 @@ "src": "systems/daggerheart/assets/icons/documents/actors/dragon-head.svg", "anchorX": 0.5, "anchorY": 0.5, - "offsetX": 0, - "offsetY": 0, "fit": "contain", "scaleX": 1, "scaleY": 1, - "rotation": 0, "tint": "#ffffff", "alphaThreshold": 0.75 }, @@ -194,7 +191,7 @@ "saturation": 0, "contrast": 0 }, - "detectionModes": [], + "detectionModes": {}, "occludable": { "radius": 0 }, @@ -220,7 +217,8 @@ "flags": {}, "randomImg": false, "appendNumber": false, - "prependAdjective": false + "prependAdjective": false, + "depth": 1 }, "items": [ { @@ -257,7 +255,7 @@ "name": "Acidic Form", "type": "feature", "system": { - "description": "

When the @Lookup[@name] makes a successful attack, the target must mark an Armor Slot without receiving its benefi ts (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", + "description": "

When the @Lookup[@name] makes a successful attack, the target must mark an Armor Slot without receiving its benefits (they can still use armor to reduce the damage). If they can’t mark an Armor Slot, they must mark an additional HP.

", "resource": null, "actions": { "gtT2oHSyZg9OHHJD": { From d0c2c783f1fbf3ab1ab6057bbadf8febf828d865 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 1 May 2026 22:53:20 +0200 Subject: [PATCH 4/7] Improved armor source names (#1851) --- module/applications/dialogs/damageReductionDialog.mjs | 11 +++-------- module/helpers/utils.mjs | 5 ++++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index 930ca1a1..b916a5de 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -22,9 +22,10 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap ); const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled); - const armor = orderedArmorSources.reduce((acc, { document }) => { + const armor = orderedArmorSources.reduce((acc, { name, document }) => { const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData; acc.push({ + name, effect: document, marks: [...Array(max).keys()].reduce((acc, _, index) => { const spent = index < current; @@ -152,14 +153,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap const armorSources = []; for (const source of this.marks.armor) { - const parent = source.effect.origin - ? await foundry.utils.fromUuid(source.effect.origin) - : source.effect.parent; - - const useEffectName = parent.type === 'armor' || parent instanceof Actor; - const label = useEffectName ? source.effect.name : parent.name; armorSources.push({ - label: label, + label: source.name, uuid: source.effect.uuid, marks: source.marks }); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 1650b505..faa046ff 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -757,9 +757,12 @@ export function getArmorSources(actor) { // Get the origin item. Since the actor is already loaded, it should already be cached // Consider the relative function versions if this causes an issue const origin = doc.origin ? foundry.utils.fromUuidSync(doc.origin) : doc; + const useParentName = doc.parent && !(doc.parent instanceof Actor); + const name = doc.origin || !useParentName ? doc.name : doc.parent.name; + return { origin, - name: origin.name, + name, document: doc, data: doc.system.armor ?? doc.system.armorData, disabled: !!doc.disabled || !!doc.isSuppressed From c7159eff112154b3213e1b72a209ddff5942bb5c Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 1 May 2026 17:00:03 -0400 Subject: [PATCH 5/7] Fix retrieving parent documents when the model is null (#1853) --- module/documents/activeEffect.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 08463818..f9239a90 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -171,6 +171,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { /** Recursively finds the first parent document of the given object */ static #resolveParentDocument(model, documentClass) { + if (!model) return null; return model instanceof documentClass ? model : model.parent From 4558fbdcf618314c1460360377015b3229e17b0c Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 1 May 2026 23:09:45 +0200 Subject: [PATCH 6/7] Raised version --- system.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system.json b/system.json index 9b8a8403..deb30b53 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "2.2.1", + "version": "2.2.2", "compatibility": { "minimum": "14.359", "verified": "14.360", @@ -10,7 +10,7 @@ }, "url": "https://github.com/Foundryborne/daggerheart", "manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json", - "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.2.1/system.zip", + "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.2.2/system.zip", "authors": [ { "name": "WBHarry" From 20f42e8a0d0bf45cd998396fb0366dd5941cbf92 Mon Sep 17 00:00:00 2001 From: Carlos Fernandez Date: Fri, 1 May 2026 22:09:21 -0400 Subject: [PATCH 7/7] Continue work on updating identifier --- lang/en.json | 3 ++ module/applications/sheets/items/class.mjs | 24 +++++----- module/applications/sheets/items/subclass.mjs | 23 ++++++++++ module/data/item/class.mjs | 27 +++++------- module/data/item/subclass.mjs | 6 ++- module/helpers/utils.mjs | 8 ++++ styles/less/global/feature-section.less | 44 +++++++++---------- .../less/sheets/items/item-sheet-shared.less | 4 ++ templates/sheets/items/class/features.hbs | 25 +++++------ templates/sheets/items/class/header.hbs | 4 -- templates/sheets/items/class/settings.hbs | 9 ++++ templates/sheets/items/subclass/features.hbs | 14 ++++++ 12 files changed, 122 insertions(+), 69 deletions(-) diff --git a/lang/en.json b/lang/en.json index b91d426e..09cbef70 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2532,6 +2532,9 @@ "recovery": { "label": "Recovery" }, "type": { "label": "Type" }, "value": { "label": "Value" } + }, + "identifier": { + "label": "Identifier" } }, "Ancestry": { diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index 154f9ad9..0e253ddd 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -9,7 +9,8 @@ export default class ClassSheet extends DHBaseItemSheet { position: { width: 700 }, actions: { removeItemFromCollection: ClassSheet.#removeItemFromCollection, - removeSuggestedItem: ClassSheet.#removeSuggestedItem + removeSuggestedItem: ClassSheet.#removeSuggestedItem, + resetIdentifier: ClassSheet.#resetIdentifier }, tagifyConfigs: [ { @@ -107,6 +108,7 @@ export default class ClassSheet extends DHBaseItemSheet { async _prepareContext(_options) { const context = await super._prepareContext(_options); context.domains = this.document.system.domains; + context.subclasses = await this.document.system.getSubclasses(); return context; } @@ -129,19 +131,7 @@ export default class ClassSheet extends DHBaseItemSheet { const itemType = data.type === 'ActiveEffect' ? data.type : item.type; const target = event.target.closest('fieldset.drop-section'); - if (itemType === 'subclass') { - if (!this.document.system.identifier) { - return ui.notifications.error( - game.i18n.localize('DAGGERHEART.UI.Notifications.classMissingIdentifier') - ); - } - - if (item.system.classIdentifiers.includes(this.document.system.identifier)) return; - - await item.update({ - 'system.classIdentifiers': [...item.system.classIdentifiers, this.document.system.identifier] - }); - } else if (['feature', 'ActiveEffect'].includes(itemType)) { + if (['feature', 'ActiveEffect'].includes(itemType)) { super._onDrop(event); } else if (this.document.parent?.type !== 'character') { if (itemType === 'weapon') { @@ -218,4 +208,10 @@ export default class ClassSheet extends DHBaseItemSheet { const { target } = element.dataset; await this.document.update({ [`system.characterGuide.${target}`]: null }); } + + static async #resetIdentifier() { + const document = this.document; + const initial = document.system.schema.fields.identifier.getInitialValue(document._source); + document.update({ 'system.identifier': initial }); + } } diff --git a/module/applications/sheets/items/subclass.mjs b/module/applications/sheets/items/subclass.mjs index 5c731777..db01bfae 100644 --- a/module/applications/sheets/items/subclass.mjs +++ b/module/applications/sheets/items/subclass.mjs @@ -40,4 +40,27 @@ export default class SubclassSheet extends DHBaseItemSheet { get relatedDocs() { return this.document.system.features.map(x => x.item); } + + async _onDrop(event) { + event.stopPropagation(); + const data = TextEditor.getDragEventData(event); + const item = await fromUuid(data.uuid); + const itemType = data.type === 'ActiveEffect' ? data.type : item.type; + if (itemType === 'class') { + const identifier = item.system.identifier; + if (!identifier) { + return ui.notifications.error( + game.i18n.localize('DAGGERHEART.UI.Notifications.classMissingIdentifier') + ); + } + + if (this.document.system.classLink.identifier !== identifier) { + const { img, name } = item; + await this.document.update({ 'system.classLink': { identifier, img, name } }); + } + return; + } + + return super._onDrop(event); + } } diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index e53b0b70..49195b5a 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ItemLinkFields from '../fields/itemLinkFields.mjs'; -import { addLinkedItemsDiff, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; +import { addLinkedItemsDiff, camelize, getFeaturesHTMLData, updateLinkedItemApps } from '../../helpers/utils.mjs'; export default class DHClass extends BaseDataItem { /** @inheritDoc */ @@ -51,7 +51,7 @@ export default class DHClass extends BaseDataItem { backgroundQuestions: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }), connections: new fields.ArrayField(new fields.StringField(), { initial: ['', '', ''] }), isMulticlass: new fields.BooleanField({ initial: false }), - identifier: new fields.StringField(), + identifier: new fields.StringField({ blank: false, initial: obj => camelize(obj?.name ?? '') }), /* Subclasses is legacy. If we can safetely migrate it away at some point we could remove it*/ subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }) }; @@ -73,24 +73,21 @@ export default class DHClass extends BaseDataItem { } async getSubclasses() { - const oldLinkedSubclasses = this.subclasses.filter(x => x); - if (!this.identifier) return oldLinkedSubclasses; + const oldLinkedSubclasses = this.subclasses; + if (oldLinkedSubclasses.length) return oldLinkedSubclasses; const subclasses = game.items.filter( - x => x.type === 'subclass' && x.system.classIdentifiers.includes(this.identifier) + x => x.type === 'subclass' && x.system.classLink.identifier === this.identifier ); for (const pack of game.packs) { - const indexes = await pack.getIndex({ fields: ['system.classIdentifiers'] }); + const indexes = await pack.getIndex({ fields: ['system.classLink.identifier'] }); for (const index of indexes) { - if ( - index.type === 'subclass' && - (index.system.classIdentifiers ?? []).includes( - this.identifier && !subclasses.find(x => x.uuid === index.uuid) - ) - ) { - const subclass = await foundry.utils.fromUuid(index.uuid); - subclasses.push(subclass); - } + if (index.type !== 'subclass') continue; + if (index.system?.classLink?.identifier !== this.identifier) continue; + if (subclasses.find(x => x.uuid === index.uuid)) continue; + + const subclass = await foundry.utils.fromUuid(index.uuid); + subclasses.push(subclass); } } diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index f166d01b..96c9f5de 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -28,7 +28,11 @@ export default class DHSubclass extends BaseDataItem { features: new ItemLinkFields(), featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }), isMulticlass: new fields.BooleanField({ initial: false }), - classIdentifiers: new fields.ArrayField(new fields.StringField({ nullable: false })), + classLink: new fields.SchemaField({ + identifier: new fields.StringField({ nullable: true, initial: null }), + name: new fields.StringField(), + img: new fields.StringField() + }), /* Linked class is legacy. If we can safetely migrate it away at some point we could remove it */ linkedClass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true, initial: null }) }; diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index faa046ff..c993dce3 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -841,3 +841,11 @@ export function createShallowProxy(obj) { } }); } + +export function camelize(str) { + return str + .replace(/(?:^\w|[A-Z]|\b\w)/g, (part, index) => { + return index === 0 ? part.toLowerCase() : part.toUpperCase(); + }) + .replace(/\s+/g, ''); +} diff --git a/styles/less/global/feature-section.less b/styles/less/global/feature-section.less index 13feb92c..937f0cde 100644 --- a/styles/less/global/feature-section.less +++ b/styles/less/global/feature-section.less @@ -19,28 +19,28 @@ &:last-child { margin-bottom: 0px; } - .feature-line { - display: grid; - align-items: center; - grid-template-columns: 1fr 4fr 1fr; - h4 { - font-weight: lighter; - color: light-dark(@dark, @beige); - } - .image { - height: 40px; - width: 40px; - object-fit: cover; - border-radius: 6px; - border: none; - } - .controls { - display: flex; - justify-content: center; - gap: 10px; - a { - text-shadow: none; - } + } + .feature-line { + display: grid; + align-items: center; + grid-template-columns: 1fr 4fr 1fr; + h4 { + font-weight: lighter; + color: light-dark(@dark, @beige); + } + .image { + height: 40px; + width: 40px; + object-fit: cover; + border-radius: 6px; + border: none; + } + .controls { + display: flex; + justify-content: center; + gap: 10px; + a { + text-shadow: none; } } } diff --git a/styles/less/sheets/items/item-sheet-shared.less b/styles/less/sheets/items/item-sheet-shared.less index d0a8cc48..5155ad70 100644 --- a/styles/less/sheets/items/item-sheet-shared.less +++ b/styles/less/sheets/items/item-sheet-shared.less @@ -10,4 +10,8 @@ font-family: @font-body; color: light-dark(@chat-blue-bg, @beige-50); } + + button.plain.inline-control { + flex: 0 0 auto; + } } diff --git a/templates/sheets/items/class/features.hbs b/templates/sheets/items/class/features.hbs index 9d037b65..5da6e368 100644 --- a/templates/sheets/items/class/features.hbs +++ b/templates/sheets/items/class/features.hbs @@ -27,10 +27,7 @@
{{localize "TYPES.Item.subclass"}}
- {{#unless source.system.subclasses}} -
{{localize "DAGGERHEART.GENERAL.missingDragDropThing" thing=(localize "DAGGERHEART.GENERAL.subclasses")}}
- {{/unless}} - {{#each source.system.subclasses as |subclass index|}} + {{#each subclasses as |subclass index|}}
  • @@ -46,15 +43,17 @@ > - - - + {{#if document.system.subclasses}} + + + + {{/if}}
  • diff --git a/templates/sheets/items/class/header.hbs b/templates/sheets/items/class/header.hbs index 12c08bc3..019825f8 100644 --- a/templates/sheets/items/class/header.hbs +++ b/templates/sheets/items/class/header.hbs @@ -3,10 +3,6 @@

    -

    - {{localize 'TYPES.Item.class'}} - {{formInput systemFields.identifier value=source.system.identifier}} -

    {{localize "DAGGERHEART.GENERAL.Domain.plural"}} diff --git a/templates/sheets/items/class/settings.hbs b/templates/sheets/items/class/settings.hbs index b224ec1f..d44b12dd 100644 --- a/templates/sheets/items/class/settings.hbs +++ b/templates/sheets/items/class/settings.hbs @@ -3,6 +3,15 @@ data-tab='{{tabs.settings.id}}' data-group='{{tabs.settings.group}}' > +
    + {{localize "DAGGERHEART.GENERAL.general"}} + {{localize "DAGGERHEART.ITEMS.FIELDS.identifier.label"}} +
    + {{formInput systemFields.identifier value=source.system.identifier}} + +
    +
    +
    {{localize tabs.settings.label}} {{formGroup systemFields.hitPoints value=source.system.hitPoints localize=true}} diff --git a/templates/sheets/items/subclass/features.hbs b/templates/sheets/items/subclass/features.hbs index 1a75974e..5221c8ae 100644 --- a/templates/sheets/items/subclass/features.hbs +++ b/templates/sheets/items/subclass/features.hbs @@ -3,6 +3,20 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > +
    + {{localize "TYPES.Item.class"}} + {{#if document.system.classLink.identifier}} +
    +
  • + + {{document.system.classLink.name}} +
  • +
    + {{else}} +
    {{localize "DAGGERHEART.GENERAL.missingDragDropThing" thing=(localize "TYPES.Item.class")}}
    + {{/if}} +
    +
    {{localize "DAGGERHEART.GENERAL.Tabs.foundation"}}