From b7bc452bf5fd801781f5e859b09db01fc03d47c8 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 5 May 2026 22:15:21 +0200 Subject: [PATCH] [Fix] Improve Class-Subclass Linkage (#1846) * Initial thoughts * . * Fixed linting * Continue work on updating identifier * Change to uuid approach * Localization and minor fix * Fixed CompendiumBrowser Class filter for Subclass view * Fixed the class name display in the subclass view * Improved missing class visual for subclass * Fixed character creation * Rerender class sheets when subclass link is changed * Use compendium source over actual uuid in search --------- Co-authored-by: Carlos Fernandez --- lang/en.json | 5 +- .../characterCreation/characterCreation.mjs | 10 ++-- module/applications/sheets/items/class.mjs | 27 ++-------- module/applications/sheets/items/subclass.mjs | 32 ++++++++++++ module/applications/ui/itemBrowser.mjs | 11 ++-- module/config/itemBrowserConfig.mjs | 22 ++++---- module/data/item/base.mjs | 2 +- module/data/item/class.mjs | 19 ++++++- module/data/item/subclass.mjs | 5 +- module/helpers/utils.mjs | 8 +++ styles/less/global/feature-section.less | 50 +++++++++++-------- .../less/sheets/items/item-sheet-shared.less | 4 ++ templates/sheets/items/class/features.hbs | 16 +----- templates/sheets/items/class/header.hbs | 1 - .../sheets/items/subclass/description.hbs | 10 ++-- templates/sheets/items/subclass/features.hbs | 29 +++++++++++ 16 files changed, 167 insertions(+), 84 deletions(-) diff --git a/lang/en.json b/lang/en.json index aff0d60a..20c66a32 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2442,6 +2442,7 @@ "single": "Miss", "plural": "Miss" }, + "missingX": "Missing {x}", "maxWithThing": "Max {thing}", "missingDragDropThing": "Drop {thing} here", "multiclass": "Multiclass", @@ -2532,6 +2533,9 @@ "recovery": { "label": "Recovery" }, "type": { "label": "Type" }, "value": { "label": "Value" } + }, + "identifier": { + "label": "Identifier" } }, "Ancestry": { @@ -3219,7 +3223,6 @@ "subclassesAlreadyPresent": "You already have a class and multiclass subclass", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice", "gmMenuRefresh": "You refreshed all actions and resources {types}", - "subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.", "gmRequired": "This action requires an online GM", "gmOnly": "This can only be accessed by the GM", "noActorOwnership": "You do not have permissions for this character", diff --git a/module/applications/characterCreation/characterCreation.mjs b/module/applications/characterCreation/characterCreation.mjs index 936bb79d..82ca9ccb 100644 --- a/module/applications/characterCreation/characterCreation.mjs +++ b/module/applications/characterCreation/characterCreation.mjs @@ -439,10 +439,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl 'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null } }; - if (type === 'subclasses') + if (type === 'subclasses') { + const classItem = this.setup.class; + const uuid = classItem?._stats.compendiumSource ?? classItem?.uuid; presets.filter = { - 'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid } + 'system.linkedClass': { key: 'system.linkedClass', value: uuid } }; + } if (equipment.includes(type)) presets.filter = { @@ -610,7 +613,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl [foundry.utils.randomID()]: {} }; } else if (item.type === 'subclass' && event.target.closest('.subclass-card')) { - if (this.setup.class.system.subclasses.every(subclass => subclass.uuid !== item.uuid)) { + const classSubclasses = await this.setup.class.system.fetchSubclasses(); + if (classSubclasses.every(subclass => subclass.uuid !== item.uuid)) { ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass')); return; } diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index 05bb0229..25c631fe 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -104,9 +104,10 @@ export default class ClassSheet extends DHBaseItemSheet { } /**@inheritdoc */ - async _prepareContext(_options) { - const context = await super._prepareContext(_options); + async _prepareContext(options) { + const context = await super._prepareContext(options); context.domains = this.document.system.domains; + context.subclasses = await this.document.system.fetchSubclasses(); return context; } @@ -128,20 +129,8 @@ export default class ClassSheet extends DHBaseItemSheet { 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) { - return ui.notifications.warn( - game.i18n.format('DAGGERHEART.UI.Notifications.subclassAlreadyLinked', { - name: item.name, - class: this.document.name - }) - ); - } - await item.update({ 'system.linkedClass': this.document.uuid }); - await this.document.update({ - 'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid] - }); - } else if (['feature', 'ActiveEffect'].includes(itemType)) { + + if (['feature', 'ActiveEffect'].includes(itemType)) { super._onDrop(event); } else if (this.document.parent?.type !== 'character') { if (itemType === 'weapon') { @@ -200,12 +189,6 @@ export default class ClassSheet extends DHBaseItemSheet { static async #removeItemFromCollection(_event, element) { const { uuid, target } = element.dataset; const prop = foundry.utils.getProperty(this.document.system, target); - - if (target === 'subclasses') { - const subclass = await foundry.utils.fromUuid(uuid); - await subclass?.update({ 'system.linkedClass': null }); - } - await this.document.update({ [`system.${target}`]: prop.filter(i => i && i.uuid !== uuid).map(x => x.uuid) }); } diff --git a/module/applications/sheets/items/subclass.mjs b/module/applications/sheets/items/subclass.mjs index 5c731777..e9d8370e 100644 --- a/module/applications/sheets/items/subclass.mjs +++ b/module/applications/sheets/items/subclass.mjs @@ -40,4 +40,36 @@ export default class SubclassSheet extends DHBaseItemSheet { get relatedDocs() { return this.document.system.features.map(x => x.item); } + + async _prepareContext(options) { + const context = await super._prepareContext(options); + if (this.document.system.linkedClass) { + const classData = await fromUuid(this.document.system.linkedClass); + context.class = classData ?? { + name: _loc('DAGGERHEART.GENERAL.missingX', { x: _loc('TYPES.Item.class') }), + missing: true + }; + } + return context; + } + + 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 uuid = item._stats.compendiumSource ?? item.uuid; + if (this.document.system.linkedClass !== uuid) { + await this.document.update({ 'system.linkedClass': uuid }); + // Re-render all class sheets for instant feedback + for (const app of foundry.applications.instances.values()) { + if (app.document?.type === 'class') app.render(); + } + } + return; + } + + return super._onDrop(event); + } } diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index 67a16f6a..99b9a23d 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -277,7 +277,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { (await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description)); } - this.fieldFilter = this._createFieldFilter(); + this.fieldFilter = await this._createFieldFilter(); if (this.presets?.filter) { Object.entries(this.presets.filter).forEach(([k, v]) => { @@ -355,12 +355,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { ); } - _createFieldFilter() { + async _createFieldFilter() { const filters = ItemBrowser.getFolderConfig(this.selectedMenu.data, 'filters'); - filters.forEach(f => { + for (const f of filters) { if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field); else if (typeof f.choices === 'function') { - f.choices = f.choices(this.items); + f.choices = await f.choices(this.items); } // Clear field label so template uses our custom label parameter @@ -370,7 +370,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { f.name ??= f.key; f.value = this.presets?.filter?.[f.name]?.value ?? null; - }); + } + return filters; } diff --git a/module/config/itemBrowserConfig.mjs b/module/config/itemBrowserConfig.mjs index 04b387cb..3e40c97b 100644 --- a/module/config/itemBrowserConfig.mjs +++ b/module/config/itemBrowserConfig.mjs @@ -383,7 +383,8 @@ export const typeConfig = { { key: 'system.linkedClass', label: 'TYPES.Item.class', - format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing' + format: linkedClass => + foundry.utils.fromUuidSync(linkedClass)?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing' }, { key: 'system.spellcastingTrait', @@ -393,15 +394,18 @@ export const typeConfig = { ], filters: [ { - key: 'system.linkedClass.uuid', + key: 'system.linkedClass', label: 'TYPES.Item.class', - choices: items => { - const list = items - .filter(item => item.system.linkedClass) - .map(item => ({ - value: item.system.linkedClass.uuid, - label: item.system.linkedClass.name - })); + choices: async items => { + const list = []; + for (const item of items.filter(item => item.system.linkedClass)) { + const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass); + list.push({ + value: linkedClass.uuid, + label: linkedClass.name + }); + } + return list.reduce((a, c) => { if (!a.find(i => i.value === c.value)) a.push(c); return a; diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index aebf33bf..ba114fda 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -200,7 +200,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const features = []; for (let f of this.features) { const fBase = f.item ?? f; - const feature = fBase.system ? fBase : await foundry.utils.fromUuid(fBase.uuid); + const feature = fBase.pack ? await foundry.utils.fromUuid(fBase.uuid) : fBase; features.push( foundry.utils.mergeObject( feature.toObject(), diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index d3738318..7014e011 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -30,7 +30,6 @@ export default class DHClass extends BaseDataItem { }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), features: new ItemLinkFields(), - subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), inventory: new fields.SchemaField({ take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), choiceA: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), @@ -70,6 +69,24 @@ export default class DHClass extends BaseDataItem { return this.features.filter(x => x.type === CONFIG.DH.ITEM.featureSubTypes.class).map(x => x.item); } + async fetchSubclasses() { + const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u); + const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass)); + for (const pack of game.packs) { + const indexes = await pack.getIndex({ fields: ['system.linkedClass'] }); + for (const index of indexes) { + if (index.type !== 'subclass') continue; + if (!uuids.includes(index.system?.linkedClass)) continue; + if (subclasses.find(x => x.uuid === index.uuid)) continue; + + const subclass = await foundry.utils.fromUuid(index.uuid); + subclasses.push(subclass); + } + } + + return subclasses; + } + async _preCreate(data, options, user) { if (this.actor?.type === 'character') { const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index d421cc6d..12d85c1e 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -28,7 +28,7 @@ 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 }), - linkedClass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true, initial: null }) + linkedClass: new fields.DocumentUUIDField({ type: 'Item', nullable: true, initial: null }) }; } @@ -83,7 +83,8 @@ export default class DHSubclass extends BaseDataItem { ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass')); return false; } - if (actorClass.system.subclasses.every(x => x.uuid !== dataUuid)) { + + if ((await actorClass.system.fetchSubclasses()).every(x => x.uuid !== dataUuid)) { ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass')); return false; } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index cec493b4..90937db4 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -861,3 +861,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..7d5099e1 100644 --- a/styles/less/global/feature-section.less +++ b/styles/less/global/feature-section.less @@ -19,28 +19,36 @@ &:last-child { margin-bottom: 0px; } - .feature-line { - display: grid; + } + .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; + } + .image-icon { + font-size: 26px; + width: 40px; + height: 40px; + display: flex; + justify-content: center; 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; - } + } + .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..279ff52c 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|}}
  • @@ -44,16 +41,7 @@ data-item-uuid={{subclass.uuid}} data-tooltip='{{localize "DAGGERHEART.UI.Tooltip.openItemWorld"}}' > - - - - +
  • diff --git a/templates/sheets/items/class/header.hbs b/templates/sheets/items/class/header.hbs index 6b60ada6..019825f8 100644 --- a/templates/sheets/items/class/header.hbs +++ b/templates/sheets/items/class/header.hbs @@ -3,7 +3,6 @@

    -

    {{localize 'TYPES.Item.class'}}

    {{localize "DAGGERHEART.GENERAL.Domain.plural"}} diff --git a/templates/sheets/items/subclass/description.hbs b/templates/sheets/items/subclass/description.hbs index 4591bd1a..0267eb9b 100644 --- a/templates/sheets/items/subclass/description.hbs +++ b/templates/sheets/items/subclass/description.hbs @@ -1,8 +1,10 @@
    -
    -

    {{localize "DAGGERHEART.ITEMS.Subclass.spellcastTrait"}}

    - {{spellcastTrait}} -
    + {{#if spellcastTrait}} +
    +

    {{localize "DAGGERHEART.ITEMS.Subclass.spellcastTrait"}}

    + {{spellcastTrait}} +
    + {{/if}}

    {{localize "DAGGERHEART.ITEMS.Subclass.foundationFeatures"}}

    {{#each foundationFeatures as | feature |}} diff --git a/templates/sheets/items/subclass/features.hbs b/templates/sheets/items/subclass/features.hbs index 1a75974e..c54e702e 100644 --- a/templates/sheets/items/subclass/features.hbs +++ b/templates/sheets/items/subclass/features.hbs @@ -3,6 +3,35 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > +
    + {{localize "TYPES.Item.class"}} + {{#if class}} +
    +
  • + {{#if class.missing}} + + {{class.name}} + {{else}} + + {{class.name}} +
    + + + +
    + {{/if}} +
  • +
    + {{else}} +
    {{localize "DAGGERHEART.GENERAL.missingDragDropThing" thing=(localize "TYPES.Item.class")}}
    + {{/if}} +
    +
    {{localize "DAGGERHEART.GENERAL.Tabs.foundation"}}