extract ikonis feature injection into a reusable helper with recursion guard

This commit is contained in:
CPTN Cosmo 2026-04-26 18:29:09 +02:00
parent 40cf69e061
commit fec1a6c1ef

View file

@ -127,6 +127,59 @@ export function patchIkonisLogic() {
}; };
} }
// Global recursion guard for feature injection
let _isInjecting = false;
/**
* Internal helper to generate and cache virtual features for an actor.
* Safe to call multiple times; uses a cache to keep it fast.
*/
function _injectIkonisFeatures(actor) {
if (!actor || _isInjecting) return [];
// Use a short-lived cache for the current render cycle
if (!actor._ikonisCache) actor._ikonisCache = new Map();
const ikonisFeatures = [];
_isInjecting = true;
try {
const weapons = actor.items.filter(i => i.type === 'weapon');
for (const item of weapons) {
const isEquipped = item.system.equipped;
const installedIds = item.getFlag('dh-ikonis', 'installedAugments') || [];
const bondedUuid = item.getFlag('dh-ikonis', 'bondedFeatureUuid');
const processFeature = (uuid, type) => {
const feature = _featureCache.get(uuid) || fromUuidSync(uuid);
if (feature) {
const featureClone = feature.clone({ parent: actor }, { keepId: true });
featureClone.system.type = "ikonis";
const virtualId = `ikonis-${item.id}-${feature.id}`;
Object.defineProperty(featureClone, "id", { value: virtualId, enumerable: true });
actor._ikonisCache.set(virtualId, featureClone);
if (isEquipped) ikonisFeatures.push(featureClone);
}
};
if (bondedUuid) processFeature(bondedUuid, "bonded");
const allAugs = getAugments();
for (const id of installedIds) {
const aug = allAugs.find(a => String(a.id) === String(id));
if (aug?.featureUuid) processFeature(aug.featureUuid, "augment");
}
}
} catch (err) {
console.error("DH-Ikonis | Error during feature injection:", err);
} finally {
_isInjecting = false;
}
return Array.from(new Set(ikonisFeatures));
}
/** /**
* Patches the Character Data Model to show features in the UI lists. * Patches the Character Data Model to show features in the UI lists.
*/ */
@ -143,58 +196,16 @@ export function patchDhCharacter(DhCharacter) {
Object.defineProperty(DhCharacter.prototype, 'sheetLists', { Object.defineProperty(DhCharacter.prototype, 'sheetLists', {
get: function() { get: function() {
const lists = originalSheetLists.call(this); const lists = originalSheetLists.call(this);
const ikonisFeatures = [];
if (!this.parent) return lists; if (!this.parent) return lists;
const weapons = this.parent.items.filter(i => i.type === 'weapon'); const ikonisFeatures = _injectIkonisFeatures(this.parent);
if (weapons.length > 0) {
console.log(`DH-Ikonis | Checking ${weapons.length} weapons on ${this.parent.name} for augments...`);
}
for (const item of weapons) {
const isEquipped = item.system.equipped;
const installedIds = item.getFlag('dh-ikonis', 'installedAugments') || [];
const bondedUuid = item.getFlag('dh-ikonis', 'bondedFeatureUuid');
const processFeature = (uuid, type) => {
const feature = _featureCache.get(uuid) || fromUuidSync(uuid);
if (feature) {
// Use clone() to preserve internal action state
const featureClone = feature.clone({ parent: this.parent }, { keepId: true });
// Set system type to 'ikonis' so the original sheetLists ignores it
featureClone.system.type = "ikonis";
// Use unique ID per weapon
const virtualId = `ikonis-${item.id}-${feature.id}`;
Object.defineProperty(featureClone, "id", { value: virtualId, enumerable: true });
// Store in a local cache on the actor for fast lookup by our patch
if (!this.parent._ikonisCache) this.parent._ikonisCache = new Map();
this.parent._ikonisCache.set(virtualId, featureClone);
if (isEquipped) {
ikonisFeatures.push(featureClone);
}
}
};
if (bondedUuid) processFeature(bondedUuid, "bonded");
const allAugs = getAugments();
for (const id of installedIds) {
const aug = allAugs.find(a => String(a.id) === String(id));
if (aug?.featureUuid) processFeature(aug.featureUuid, "augment");
}
}
if (ikonisFeatures.length > 0) { if (ikonisFeatures.length > 0) {
const uniqueFeatures = Array.from(new Set(ikonisFeatures));
if (!lists.features) lists.features = {}; if (!lists.features) lists.features = {};
lists.features["Ikonis Augments"] = { lists.features["Ikonis Augments"] = {
title: "Ikonis Augments", title: "Ikonis Augments",
type: "ikonis", type: "ikonis",
values: uniqueFeatures values: ikonisFeatures
}; };
} }
@ -204,17 +215,11 @@ export function patchDhCharacter(DhCharacter) {
}); });
// Patch getEmbeddedDocument to resolve our virtual features // Patch getEmbeddedDocument to resolve our virtual features
// This is the most compatible way to make fromUuid work without patching it directly
const originalGetEmbedded = Actor.prototype.getEmbeddedDocument; const originalGetEmbedded = Actor.prototype.getEmbeddedDocument;
Actor.prototype.getEmbeddedDocument = function(embeddedName, id, options) { Actor.prototype.getEmbeddedDocument = function(embeddedName, id, options) {
if (embeddedName === "Item" && typeof id === "string" && id.startsWith("ikonis-")) { if (embeddedName === "Item" && typeof id === "string" && id.startsWith("ikonis-")) {
// First check the cache we populated during sheetLists if (!this._ikonisCache?.has(id)) _injectIkonisFeatures(this);
if (this._ikonisCache?.has(id)) return this._ikonisCache.get(id); return this._ikonisCache?.get(id);
// If not in cache, trigger the getter to populate it
// Accessing system.sheetLists will run our patch above
const lists = this.system.sheetLists;
if (this._ikonisCache?.has(id)) return this._ikonisCache.get(id);
} }
return originalGetEmbedded.call(this, embeddedName, id, options); return originalGetEmbedded.call(this, embeddedName, id, options);
}; };