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} 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); } }