Changed ItemLinksField makeup

This commit is contained in:
WBHarry 2025-07-23 14:50:24 +02:00
parent 600c08cb23
commit 30f31e77dd
13 changed files with 120 additions and 96 deletions

View file

@ -176,12 +176,13 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #addFeature(_, target) { static async #addFeature(_, target) {
const { type } = target.dataset;
const cls = foundry.documents.Item.implementation; const cls = foundry.documents.Item.implementation;
const featurePath = `system.itemLinks.${CONFIG.DH.ITEM.itemLinkFeatureTypes[target.dataset.type]}`;
const feature = await cls.create({ const feature = await cls.create({
type: 'feature', type: 'feature',
name: cls.defaultName({ type: 'feature' }), name: cls.defaultName({ type: 'feature' }),
[`system.itemLinks.${this.document.uuid}`]: CONFIG.DH.ITEM.itemLinkFeatureTypes[type] [featurePath]: [this.document.uuid]
}); });
await this.document.update({ await this.document.update({
'system.features': [...this.document.system.features, feature].map(f => f.uuid) 'system.features': [...this.document.system.features, feature].map(f => f.uuid)
@ -192,10 +193,15 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* Remove a feature from the item. * Remove a feature from the item.
* @type {ApplicationClickAction} * @type {ApplicationClickAction}
*/ */
static async #deleteFeature(event, target) { static async #deleteFeature(event, element) {
const target = element.closest('[data-item-uuid]');
const feature = getDocFromElement(target); const feature = getDocFromElement(target);
if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); if (!feature) return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing'));
await feature.update({ [`system.itemLinks.-=${this.document.uuid}`]: null });
const featurePath = `system.itemLinks.${CONFIG.DH.ITEM.itemLinkFeatureTypes[target.dataset.type]}`;
await feature.update({
[featurePath]: foundry.utils.getProperty(feature, featurePath).filter(x => x !== this.document.uuid)
});
await this.document.update({ await this.document.update({
'system.features': this.document.system.features.map(x => x.uuid).filter(uuid => uuid !== feature.uuid) 'system.features': this.document.system.features.map(x => x.uuid).filter(uuid => uuid !== feature.uuid)
}); });
@ -267,23 +273,20 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
* @param {DragEvent} event - The drag event * @param {DragEvent} event - The drag event
*/ */
async _onDrop(event) { async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
if (data.fromInternal) return; if (data.fromInternal) return;
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
if (item?.type === 'feature') { if (item?.type === 'feature') {
const { type } = target.dataset; const existing = await item.addItemLink(this.document.uuid, target.dataset.type, true);
const previouslyLinked = item.system.itemLinks[this.document.uuid] !== undefined;
await item.update({
[`system.itemLinks.${this.document.uuid}`]: CONFIG.DH.ITEM.itemLinkFeatureTypes[type]
});
if (!previouslyLinked) { if (existing) {
this.render();
} else {
const current = this.document.system.features.map(x => x.uuid); const current = this.document.system.features.map(x => x.uuid);
await this.document.update({ 'system.features': [...current, item.uuid] }); await this.document.update({ 'system.features': [...current, item.uuid] });
} else {
this.render();
} }
} }
} }

View file

@ -79,23 +79,29 @@ export default class ClassSheet extends DHBaseItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
async linkedItemUpdate(item, property, replace) { async linkedItemUpdate(item, property, replace) {
const removedLinkedItems = []; const removedLinkedItems = [];
const existingLink = item.system.itemLinks[this.document.uuid]; const existing = Object.values(item.system.itemLinks).some(x => x.some(uuid => uuid === this.document.uuid));
if (replace) { if (replace) {
const toRemove = this.document.system.linkedItems.find( const toRemove = this.document.system.linkedItems.find(
x => x.uuid !== item.uuid && x.system.itemLinks[this.document.uuid] === property x => x.uuid !== item.uuid && x.system.itemLinks[property]?.has(this.document.uuid)
); );
if (toRemove) { if (toRemove) {
removedLinkedItems.push(toRemove.uuid); removedLinkedItems.push(toRemove.uuid);
await toRemove.update({ [`system.itemLinks.-=${this.document.uuid}`]: null }); await toRemove.update({
[`system.itemLinks.${property}`]: toRemove.system.itemLinks[property].filter(
x => x !== this.document.uuid
)
});
} }
} }
await item.update({ [`system.itemLinks.${this.document.uuid}`]: CONFIG.DH.ITEM.itemLinkTypes[property] }); await item.addItemLink(this.document.uuid, property, true);
if (!existingLink) { if (!existing) {
await this.document.update({ await this.document.update({
'system.linkedItems': [ 'system.linkedItems': [
...this.document.system.linkedItems.map(x => x.uuid).filter(x => !removedLinkedItems.includes(x)), ...this.document.system.linkedItems
.filter(x => !removedLinkedItems.includes(x.uuid))
.map(x => x.uuid),
item.uuid item.uuid
] ]
}); });
@ -110,13 +116,15 @@ export default class ClassSheet extends DHBaseItemSheet {
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
if (item.type === 'subclass') { if (item.type === 'subclass') {
const previouslyLinked = item.system.itemLinks[this.document.uuid] !== undefined; const existing = await item.addItemLink(this.document.uuid, CONFIG.DH.ITEM.itemLinkTypes.subclass, true);
if (previouslyLinked) return;
await item.update({ [`system.itemLinks.${this.document.uuid}`]: null }); if (existing) {
await this.document.update({ this.render();
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid] } else {
}); await this.document.update({
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
});
}
} else if (item.type === 'feature') { } else if (item.type === 'feature') {
super._onDrop(event); super._onDrop(event);
} else if (item.type === 'weapon') { } else if (item.type === 'weapon') {
@ -159,13 +167,16 @@ export default class ClassSheet extends DHBaseItemSheet {
* @param {HTMLElement} element - The capturing HTML element which defines the [data-action="removeLinkedItem"] * @param {HTMLElement} element - The capturing HTML element which defines the [data-action="removeLinkedItem"]
*/ */
static async #removeLinkedItem(_event, element) { static async #removeLinkedItem(_event, element) {
const { uuid } = element.dataset; const { uuid, target } = element.dataset;
const item = this.document.system.linkedItems.find(x => x.uuid === uuid); const prop = target === 'subclass' ? 'subclasses' : 'linkedItems';
const item = this.document.system[prop].find(x => x.uuid === uuid);
if (!item) return; if (!item) return;
await item.update({ [`system.itemLinks-=${uuid}`]: null });
await this.document.update({ await this.document.update({
'system.linkedItems': this.document.system.linkedItems.filter(x => x.uuid !== uuid).map(x => x.uuid) [`system.${prop}`]: this.document.system[prop].filter(x => x.uuid !== uuid).map(x => x.uuid)
});
await item.update({
[`system.itemLinks.${target}`]: item.system.itemLinks[target].filter(x => x !== this.document.uuid)
}); });
} }
} }

View file

@ -1318,7 +1318,8 @@ export const itemLinkFeatureTypes = {
class: 'class', class: 'class',
foundation: 'foundation', foundation: 'foundation',
specialization: 'specialization', specialization: 'specialization',
mastery: 'mastery' mastery: 'mastery',
subclass: 'subclass'
}; };
export const itemLinkItemTypes = { export const itemLinkItemTypes = {

View file

@ -4,15 +4,7 @@ export default class ItemLinksField extends foundry.data.fields.TypedObjectField
* @param {DataFieldContext} [context] Additional context which describes the field * @param {DataFieldContext} [context] Additional context which describes the field
*/ */
constructor(options, context) { constructor(options, context) {
super( super(new foundry.data.fields.SetField(new foundry.data.fields.DocumentUUIDField()), options, context);
new foundry.data.fields.StringField({
choices: CONFIG.DH.ITEM.itemLinkTypes,
nullable: true,
initial: null
}),
options,
context
);
} }
/** @inheritDoc */ /** @inheritDoc */
@ -24,15 +16,6 @@ export default class ItemLinksField extends foundry.data.fields.TypedObjectField
* @param {Object} [value] The candidate object to be added. * @param {Object} [value] The candidate object to be added.
*/ */
static validateKey(value) { static validateKey(value) {
const parsed = foundry.utils.parseUuid(value); return Boolean(CONFIG.DH.ITEM.itemLinkTypes[value]);
if (!parsed || parsed.type !== foundry.documents.Item.documentName) return false;
if (!foundry.data.validators.isValidId(parsed.documentId)) return false;
return true;
}
/**@inheritdoc */
_cast(value) {
value = super._cast(value);
return foundry.utils.flattenObject(value);
} }
} }

View file

@ -20,14 +20,14 @@ export default class DHAncestry extends BaseDataItem {
} }
get primaryFeature() { get primaryFeature() {
return this.features.find( return this.features.find(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkFeatureTypes.primary x.system.itemLinks[CONFIG.DH.ITEM.itemLinkFeatureTypes.primary]?.has(this.parent.uuid)
); );
} }
get secondaryFeature() { get secondaryFeature() {
return this.features.find( return this.features.find(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkFeatureTypes.secondary x.system.itemLinks[CONFIG.DH.ITEM.itemLinkFeatureTypes.secondary]?.has(this.parent.uuid)
); );
} }
} }

View file

@ -171,18 +171,18 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
); );
} }
if (this.metadata.isItemLinkable) { // if (this.metadata.isItemLinkable) {
const linkEntries = Object.entries(this.itemLinks); // const linkEntries = Object.entries(this.itemLinks);
for (let [uuid, type] of linkEntries) { // for (let [uuid, type] of linkEntries) {
const item = await foundry.utils.fromUuid(uuid); // const item = await foundry.utils.fromUuid(uuid);
const path = CONFIG.DH.ITEM.itemLinkFeatureTypes[type] ? 'system.features' : 'system.linkedItems'; // const path = CONFIG.DH.ITEM.itemLinkFeatureTypes[type] ? 'system.features' : 'system.linkedItems';
await item.update({ // await item.update({
[path]: foundry.utils // [path]: foundry.utils
.getProperty(item, path) // .getProperty(item, path)
.filter(x => x.uuid !== this.parent.uuid) // .filter(x => x.uuid !== this.parent.uuid)
.map(x => x.uuid) // .map(x => x.uuid)
}); // });
} // }
} // }
} }
} }

View file

@ -1,5 +1,4 @@
import BaseDataItem from './base.mjs'; import BaseDataItem from './base.mjs';
import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
export default class DHClass extends BaseDataItem { export default class DHClass extends BaseDataItem {
@ -29,7 +28,7 @@ export default class DHClass extends BaseDataItem {
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
features: new ForeignDocumentUUIDArrayField({ type: 'Item' }), features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
linkedItems: new ForeignDocumentUUIDArrayField({ type: 'Item' }), linkedItems: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
characterGuide: new fields.SchemaField({ characterGuide: new fields.SchemaField({
suggestedTraits: new fields.SchemaField({ suggestedTraits: new fields.SchemaField({
agility: new fields.NumberField({ initial: 0, integer: true }), agility: new fields.NumberField({ initial: 0, integer: true }),
@ -45,46 +44,50 @@ export default class DHClass extends BaseDataItem {
} }
get hopeFeatures() { get hopeFeatures() {
return this.features.filter( return this.features.filter(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkFeatureTypes.hope x.system.itemLinks[CONFIG.DH.ITEM.itemLinkFeatureTypes.hope]?.has(this.parent.uuid)
); );
} }
get classFeatures() { get classFeatures() {
return this.features.filter( return this.features.filter(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkFeatureTypes.class x.system.itemLinks[CONFIG.DH.ITEM.itemLinkFeatureTypes.class]?.has(this.parent.uuid)
); );
} }
get suggestedPrimaryWeapon() { get suggestedPrimaryWeapon() {
return this.linkedItems.find( return this.linkedItems.find(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkTypes.primaryWeapon x.system.itemLinks[CONFIG.DH.ITEM.itemLinkItemTypes.primaryWeapon]?.has(this.parent.uuid)
); );
} }
get suggestedSecondaryWeapon() { get suggestedSecondaryWeapon() {
return this.linkedItems.find( return this.linkedItems.find(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkTypes.secondaryWeapon x.system.itemLinks[CONFIG.DH.ITEM.itemLinkItemTypes.secondaryWeapon]?.has(this.parent.uuid)
); );
} }
get suggestedArmor() { get suggestedArmor() {
return this.linkedItems.find(x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkTypes.armor); return this.linkedItems.find(x =>
x.system.itemLinks[CONFIG.DH.ITEM.itemLinkItemTypes.armor]?.has(this.parent.uuid)
);
} }
get take() { get take() {
return this.linkedItems.filter(x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkTypes.take); return this.linkedItems.filter(x =>
x.system.itemLinks[CONFIG.DH.ITEM.itemLinkItemTypes.take]?.has(this.parent.uuid)
);
} }
get choiceA() { get choiceA() {
return this.linkedItems.filter( return this.linkedItems.filter(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkTypes.choiceA x.system.itemLinks[CONFIG.DH.ITEM.itemLinkItemTypes.choiceA]?.has(this.parent.uuid)
); );
} }
get choiceB() { get choiceB() {
return this.linkedItems.filter( return this.linkedItems.filter(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkTypes.choiceB x.system.itemLinks[CONFIG.DH.ITEM.itemLinkItemTypes.choiceB]?.has(this.parent.uuid)
); );
} }

View file

@ -30,20 +30,20 @@ export default class DHSubclass extends BaseDataItem {
} }
get foundationFeatures() { get foundationFeatures() {
return this.features.filter( return this.features.filter(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkFeatureTypes.foundation x.system.itemLinks[CONFIG.DH.ITEM.itemLinkFeatureTypes.foundation]?.has(this.parent.uuid)
); );
} }
get specializationFeatures() { get specializationFeatures() {
return this.features.filter( return this.features.filter(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkFeatureTypes.specialization x.system.itemLinks[CONFIG.DH.ITEM.itemLinkFeatureTypes.specialization]?.has(this.parent.uuid)
); );
} }
get masteryFeatures() { get masteryFeatures() {
return this.features.filter( return this.features.filter(x =>
x => x.system.itemLinks[this.parent.uuid] === CONFIG.DH.ITEM.itemLinkFeatureTypes.mastery x.system.itemLinks[CONFIG.DH.ITEM.itemLinkFeatureTypes.mastery]?.has(this.parent.uuid)
); );
} }

View file

@ -20,6 +20,27 @@ export default class DHItem extends foundry.documents.Item {
for (const action of this.system.actions ?? []) action.prepareData(); for (const action of this.system.actions ?? []) action.prepareData();
} }
async addItemLink(documentUuid, type, replace) {
if (!this.system.metadata.isItemLinkable) return;
let existing = false;
if (replace) {
await this.update({
'system.itemLinks': Object.keys(CONFIG.DH.ITEM.itemLinkTypes).reduce((acc, key) => {
const filtered = (this.system.itemLinks[key] ?? []).filter(uuid => uuid !== documentUuid);
acc[key] = key === type ? [...filtered, documentUuid] : filtered;
existing = existing ? existing : (this.system.itemLinks[key] ?? []).size > filtered.size;
return acc;
}, {})
});
} else {
await this.update({ [`system.itemLinks.${type}`]: [...(this.system.itemLinks[type] ?? []), documentUuid] });
}
return existing;
}
/** /**
* @inheritdoc * @inheritdoc
* @param {object} options - Options which modify the getRollData method. * @param {object} options - Options which modify the getRollData method.

View file

@ -1,4 +1,4 @@
<li class='feature-item' data-item-uuid='{{feature.uuid}}'> <li class='feature-item' data-item-uuid='{{feature.uuid}}' data-type="{{type}}">
<div class='feature-line'> <div class='feature-line'>
<img class='image' src='{{feature.img}}' /> <img class='image' src='{{feature.img}}' />
<h4> <h4>

View file

@ -13,6 +13,7 @@
<div class="feature-item" <div class="feature-item"
data-action="editDoc" data-action="editDoc"
data-item-uuid="{{document.system.primaryFeature.uuid}}" data-item-uuid="{{document.system.primaryFeature.uuid}}"
data-type="primary"
> >
<img class="image" src="{{document.system.primaryFeature.img}}" /> <img class="image" src="{{document.system.primaryFeature.img}}" />
<span>{{document.system.primaryFeature.name}}</span> <span>{{document.system.primaryFeature.name}}</span>
@ -34,6 +35,7 @@
<div class="feature-item" <div class="feature-item"
data-action="editDoc" data-action="editDoc"
data-item-uuid="{{document.system.secondaryFeature.uuid}}" data-item-uuid="{{document.system.secondaryFeature.uuid}}"
data-type="secondary"
> >
<img class="image" src="{{document.system.secondaryFeature.img}}" /> <img class="image" src="{{document.system.secondaryFeature.img}}" />
<span>{{document.system.secondaryFeature.name}}</span> <span>{{document.system.secondaryFeature.name}}</span>

View file

@ -44,8 +44,8 @@
</a> </a>
<a <a
class='effect-control' class='effect-control'
data-action='removeItemFromCollection' data-action='removeLinkedItem'
data-target="subclasses" data-target="subclass"
data-uuid={{subclass.uuid}} data-uuid={{subclass.uuid}}
data-tooltip='{{localize "CONTROLS.CommonDelete"}}' data-tooltip='{{localize "CONTROLS.CommonDelete"}}'
> >

View file

@ -43,7 +43,7 @@
<img class="image" src="{{this.img}}" /> <img class="image" src="{{this.img}}" />
<span>{{this.name}}</span> <span>{{this.name}}</span>
<div class="controls"> <div class="controls">
<a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" class="fa-solid fa-trash icon-button"></i></a> <a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" data-target="primaryWeapon" class="fa-solid fa-trash icon-button"></i></a>
</div> </div>
</div> </div>
{{/if}} {{/if}}
@ -60,7 +60,7 @@
<img class="image" src="{{this.img}}" /> <img class="image" src="{{this.img}}" />
<span>{{this.name}}</span> <span>{{this.name}}</span>
<div class="controls"> <div class="controls">
<a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" class="fa-solid fa-trash icon-button"></i></a> <a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" data-target="secondaryWeapon" class="fa-solid fa-trash icon-button"></i></a>
</div> </div>
</div> </div>
{{/if}} {{/if}}
@ -77,7 +77,7 @@
<img class="image" src="{{this.img}}" /> <img class="image" src="{{this.img}}" />
<span>{{this.name}}</span> <span>{{this.name}}</span>
<div class="controls"> <div class="controls">
<a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" class="fa-solid fa-trash icon-button"></i></a> <a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" data-target="armor" class="fa-solid fa-trash icon-button"></i></a>
</div> </div>
</div> </div>
{{/if}} {{/if}}
@ -96,7 +96,7 @@
<img class="image" src="{{this.img}}" /> <img class="image" src="{{this.img}}" />
<span>{{this.name}}</span> <span>{{this.name}}</span>
<div class="controls"> <div class="controls">
<a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" class="fa-solid fa-trash icon-button"></i></a> <a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" data-target="take" class="fa-solid fa-trash icon-button"></i></a>
</div> </div>
</div> </div>
{{/each}} {{/each}}
@ -111,7 +111,7 @@
<img class="image" src="{{this.img}}" /> <img class="image" src="{{this.img}}" />
<span>{{this.name}}</span> <span>{{this.name}}</span>
<div class="controls"> <div class="controls">
<a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" class="fa-solid fa-trash icon-button"></i></a> <a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" data-target="choiceA" class="fa-solid fa-trash icon-button"></i></a>
</div> </div>
</div> </div>
{{/each}} {{/each}}
@ -126,7 +126,7 @@
<img class="image" src="{{this.img}}" /> <img class="image" src="{{this.img}}" />
<span>{{this.name}}</span> <span>{{this.name}}</span>
<div class="controls"> <div class="controls">
<a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" class="fa-solid fa-trash icon-button"></i></a> <a><i data-action="removeLinkedItem" data-uuid="{{this.uuid}}" data-target="choiceB" class="fa-solid fa-trash icon-button"></i></a>
</div> </div>
</div> </div>
{{/each}} {{/each}}