feat: Add Daggerheart actor item updater module for automatic and manual item synchronization with compendiums.
This commit is contained in:
commit
1c5f990d64
7 changed files with 594 additions and 0 deletions
171
scripts/updater.js
Normal file
171
scripts/updater.js
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
export class DHUpdater {
|
||||
|
||||
static PACK_MAPPING = {
|
||||
'class': 'daggerheart.classes',
|
||||
'subclass': 'daggerheart.subclasses',
|
||||
'ancestry': 'daggerheart.ancestries',
|
||||
'community': 'daggerheart.communities',
|
||||
'domainCard': 'daggerheart.domains',
|
||||
'weapon': 'daggerheart.weapons',
|
||||
'armor': 'daggerheart.armors',
|
||||
'consumable': 'daggerheart.consumables',
|
||||
'loot': 'daggerheart.loot',
|
||||
'beastform': 'daggerheart.beastforms'
|
||||
};
|
||||
|
||||
static IGNORED_FIELDS = [
|
||||
'_id',
|
||||
'sort',
|
||||
'ownership',
|
||||
'flags',
|
||||
'_stats',
|
||||
'system.quantity',
|
||||
'system.equipped',
|
||||
'system.attuned',
|
||||
'system.favorite',
|
||||
'system.features',
|
||||
'system.subclasses',
|
||||
'system.inventory',
|
||||
'system.characterGuide'
|
||||
];
|
||||
|
||||
/**
|
||||
* Check for updates for a specific actor
|
||||
* @param {Actor} actor
|
||||
* @returns {Promise<Array>} List of updates found
|
||||
*/
|
||||
static async checkActor(actor) {
|
||||
if (!actor) return [];
|
||||
|
||||
const updates = [];
|
||||
const ignoredItems = game.settings.get('dh-actor-updater', 'ignoredItems') || {};
|
||||
|
||||
for (const item of actor.items) {
|
||||
// Skip if ignored
|
||||
if (ignoredItems[item.uuid]) continue;
|
||||
|
||||
const packId = this.PACK_MAPPING[item.type];
|
||||
if (!packId) continue; // Not a trackable item type
|
||||
|
||||
const pack = game.packs.get(packId);
|
||||
if (!pack) continue;
|
||||
|
||||
// Try to find the source item in the compendium
|
||||
// We search by name first.
|
||||
// Ideally we'd match by flags.core.sourceId if available,
|
||||
// but Daggerheart items might have just been dropped without keeping link,
|
||||
// or we want to match by name for "generic" updates.
|
||||
// The user prompt implies "compare them to the SRD compendium version", likely by name or origin.
|
||||
|
||||
let compendiumItem = null;
|
||||
const sourceId = item.flags.core?.sourceId;
|
||||
|
||||
if (sourceId && sourceId.startsWith(packId)) {
|
||||
const id = sourceId.split('.').pop();
|
||||
compendiumItem = await pack.getDocument(id);
|
||||
}
|
||||
|
||||
if (!compendiumItem) {
|
||||
// Fallback to name match
|
||||
const index = pack.index.find(i => i.name === item.name);
|
||||
if (index) {
|
||||
compendiumItem = await pack.getDocument(index._id);
|
||||
}
|
||||
}
|
||||
|
||||
if (compendiumItem) {
|
||||
const diff = this.compareItems(item, compendiumItem);
|
||||
if (diff) {
|
||||
updates.push({
|
||||
actor: actor,
|
||||
item: item,
|
||||
compendiumItem: compendiumItem,
|
||||
diff: diff
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare actor item with compendium item
|
||||
* @param {Item} item
|
||||
* @param {Item} compendiumItem
|
||||
* @returns {Object|null} Diff object or null if identical
|
||||
*/
|
||||
static compareItems(item, compendiumItem) {
|
||||
const itemData = item.toObject();
|
||||
const compendiumData = compendiumItem.toObject();
|
||||
|
||||
const diff = {};
|
||||
let hasChanges = false;
|
||||
|
||||
// Basic fields
|
||||
if (itemData.name !== compendiumData.name) {
|
||||
// If we matched by ID, name might have changed
|
||||
diff.name = { old: itemData.name, new: compendiumData.name };
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (itemData.img !== compendiumData.img) {
|
||||
diff.img = { old: itemData.img, new: compendiumData.img };
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
// System data comparison
|
||||
const flatItemSystem = foundry.utils.flattenObject(itemData.system);
|
||||
const flatCompendiumSystem = foundry.utils.flattenObject(compendiumData.system);
|
||||
|
||||
for (const [key, value] of Object.entries(flatCompendiumSystem)) {
|
||||
// Check for ignored fields
|
||||
if (this.IGNORED_FIELDS.some(ignored => key.startsWith(ignored) || `system.${key}` === ignored)) continue;
|
||||
|
||||
// Should probably check if the field exists in compendium but is different in item
|
||||
if (flatItemSystem[key] !== value) {
|
||||
// Check if it's "effectively" the same (e.g. null vs undefined or html differences)
|
||||
// Simple strict equality for now
|
||||
|
||||
// Handle embedded arrays explicitly??
|
||||
// flattenObject handles arrays with numeric indices e.g. "array.0.field"
|
||||
|
||||
diff[`system.${key}`] = { old: flatItemSystem[key], new: value };
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if item has keys that compendium doesn't (removed fields)
|
||||
// Might be tricky if users added custom data.
|
||||
// We generally care if the Compendium version is "Standard" and we want to enforce it.
|
||||
|
||||
return hasChanges ? diff : null;
|
||||
}
|
||||
|
||||
static async updateItem(item, compendiumItem) {
|
||||
const itemData = compendiumItem.toObject();
|
||||
delete itemData._id;
|
||||
delete itemData.ownership;
|
||||
delete itemData.folder;
|
||||
delete itemData.sort;
|
||||
delete itemData.flags;
|
||||
delete itemData._stats;
|
||||
|
||||
// Fields to preserve from Actor (do not overwrite with Compendium data)
|
||||
const PRESERVED_SYSTEM_FIELDS = [
|
||||
// State fields
|
||||
'quantity', 'equipped', 'attuned', 'favorite',
|
||||
// Link fields (referencing other items)
|
||||
'features', 'subclasses', 'inventory', 'characterGuide'
|
||||
];
|
||||
|
||||
if (itemData.system) {
|
||||
for (const field of PRESERVED_SYSTEM_FIELDS) {
|
||||
if (field in itemData.system) {
|
||||
delete itemData.system[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return item.update(itemData);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue