export const DEFAULT_AUGMENTS = [ { id: "force", name: "Kinetic Amplifier", effect: "+1 Damage on Melee attacks", cost: "2 Iron", precompile: 1 }, { id: "fire", name: "Thermal Core", effect: "Deals Fire damage instead of Physical", cost: "1 Blaze Glass", precompile: 1 }, { id: "shock", name: "Static Coil", effect: "Targets hit are Dazed", cost: "3 Copper", precompile: 2 }, { id: "shield", name: "Reactive Plating", effect: "+1 Armor while equipped", cost: "2 Steel", precompile: 1 }, { id: "range", name: "Long-Range Optics", effect: "Increases Range by 1 step", cost: "1 Lens", precompile: 2 }, { id: "crit", name: "Precision Chip", effect: "+1 to Crit range", cost: "1 Gold", precompile: 3 }, { id: "multi", name: "Burst Module", effect: "Can target 2 enemies (Half damage)", cost: "2 Gears", precompile: 4 }, { id: "drain", name: "Siphon Link", effect: "Recover 1 Hope on kill", cost: "1 Soul Gem", precompile: 4 }, { id: "weight", name: "Gravity Plate", effect: "Weapon is Heavy (more damage)", cost: "4 Lead", precompile: 2 } ]; // Global caches for resolved features to keep getters fast const _featureCache = new Map(); export function getAugments() { return game.settings.get('dh-ikonis', 'augmentsList') || DEFAULT_AUGMENTS; } /** * Pre-loads all features from the compendium into memory. * This is necessary because sheetLists getter is synchronous. */ export async function loadIkonisFeatures() { console.log("DH-Ikonis | Pre-loading features into cache..."); const allAugs = getAugments(); const bondedUuid = game.settings.get('dh-ikonis', 'defaultBondedUuid'); const uuids = new Set(allAugs.map(a => a.featureUuid).filter(Boolean)); if (bondedUuid) uuids.add(bondedUuid); for (const uuid of uuids) { try { const item = await fromUuid(uuid); if (item) { _featureCache.set(uuid, item); console.log(`DH-Ikonis | Cached feature: ${item.name} [${uuid}]`); } } catch (e) { console.error(`DH-Ikonis | Failed to load feature ${uuid}`, e); } } console.log(`DH-Ikonis | Cache size: ${_featureCache.size}`); } /** * Robust feature fetching. Tries cache first, then async fromUuid. */ export async function getAttachedFeature(uuid) { if (!uuid) return null; if (_featureCache.has(uuid)) return _featureCache.get(uuid); return await fromUuid(uuid); } export function getSlotCount(item) { const flags = item.getFlag('dh-ikonis') || {}; if (typeof flags.slotOverride === "number") return flags.slotOverride; let tier = item.system?.tier?.value; if (tier === undefined) tier = item.system?.tier; const tierNum = parseInt(tier) || 1; const settingKey = `slotsTier${tierNum}`; try { return game.settings.get('dh-ikonis', settingKey); } catch (e) { return tierNum >= 2 ? 3 : 2; } } /** * Patches the system to ensure Ikonis effects are applied to the actor. */ export function patchIkonisLogic() { console.log("DH-Ikonis | Patching Actor for Ikonis effect injection..."); const originalAllEffects = Actor.prototype.allApplicableEffects; Actor.prototype.allApplicableEffects = function* (options) { yield* originalAllEffects.call(this, options); for (const item of this.items) { if (item.type !== 'weapon' || !item.system.equipped) continue; const installedIds = item.getFlag('dh-ikonis', 'installedAugments') || []; const allAugs = getAugments(); for (const id of installedIds) { const aug = allAugs.find(a => String(a.id) === String(id)); if (!aug?.featureUuid) continue; // Try cache first, then sync const feature = _featureCache.get(aug.featureUuid) || fromUuidSync(aug.featureUuid); if (!feature || !feature.effects) continue; for (const effect of feature.effects) { if (effect.transfer) { console.log(`DH-Ikonis | Injecting effect '${effect.name}' from augment '${feature.name}' on ${this.name}`); // Clone the effect and set this actor as parent to ensure application const effectData = effect.toObject(); effectData.disabled = false; const ActiveEffectClass = getDocumentClass("ActiveEffect"); const effectInstance = new ActiveEffectClass(effectData, { parent: this }); yield effectInstance; } } } const bondedUuid = item.getFlag('dh-ikonis', 'bondedFeatureUuid'); if (bondedUuid) { const feature = _featureCache.get(bondedUuid) || fromUuidSync(bondedUuid); if (feature?.effects) { for (const effect of feature.effects) { if (effect.transfer) { console.log(`DH-Ikonis | Injecting bonded effect '${effect.name}' from '${feature.name}' on ${this.name}`); const effectData = effect.toObject(); effectData.disabled = false; const ActiveEffectClass = getDocumentClass("ActiveEffect"); const effectInstance = new ActiveEffectClass(effectData, { parent: this }); yield effectInstance; } } } } } }; } /** * Patches the Character Data Model to show features in the UI lists. */ export function patchDhCharacter(DhCharacter) { console.log("DH-Ikonis | Applying DhCharacter.sheetLists patch..."); const descriptor = Object.getOwnPropertyDescriptor(DhCharacter.prototype, 'sheetLists'); if (!descriptor) { console.error("DH-Ikonis | FAILED to find sheetLists descriptor on DhCharacter prototype!"); return; } const originalSheetLists = descriptor.get; Object.defineProperty(DhCharacter.prototype, 'sheetLists', { get: function() { const lists = originalSheetLists.call(this); const ikonisFeatures = []; if (!this.parent) return lists; const weapons = this.parent.items.filter(i => i.type === 'weapon'); if (weapons.length > 0) { console.log(`DH-Ikonis | Checking ${weapons.length} weapons on ${this.parent.name} for augments...`); } for (const item of weapons) { if (!item.system.equipped) continue; const installedIds = item.getFlag('dh-ikonis', 'installedAugments') || []; const bondedUuid = item.getFlag('dh-ikonis', 'bondedFeatureUuid'); if (bondedUuid) { const feature = _featureCache.get(bondedUuid) || fromUuidSync(bondedUuid); if (feature) { // Use constructor to ensure compatibility const ItemClass = getDocumentClass("Item"); const featureClone = new ItemClass(feature.toObject(), { parent: this.parent }); ikonisFeatures.push(featureClone); console.log(`DH-Ikonis | Resolved bonded feature: ${feature.name}`); } } if (installedIds.length > 0) { console.log(`DH-Ikonis | Found ${installedIds.length} installed augments on ${item.name}`); } const allAugs = getAugments(); for (const id of installedIds) { const aug = allAugs.find(a => String(a.id) === String(id)); if (!aug?.featureUuid) continue; const feature = _featureCache.get(aug.featureUuid) || fromUuidSync(aug.featureUuid); if (feature) { // Use constructor to ensure compatibility const ItemClass = getDocumentClass("Item"); const featureClone = new ItemClass(feature.toObject(), { parent: this.parent }); ikonisFeatures.push(featureClone); console.log(`DH-Ikonis | Resolved augment feature: ${feature.name}`); } } } if (ikonisFeatures.length > 0) { const uniqueFeatures = Array.from(new Set(ikonisFeatures)); console.log(`DH-Ikonis | Injecting ${uniqueFeatures.length} features for ${this.parent.name}`); lists.ikonis = { title: "Ikonis Augments", type: "ikonis", values: uniqueFeatures }; } return lists; }, configurable: true }); } export function patchDHWeapon() { // Future: Add damage type override logic here }