migrate virtual augments to native Daggerheart weapon features and remove legacy injection logic

This commit is contained in:
CPTN Cosmo 2026-04-26 18:43:24 +02:00
parent 689a9ec2ff
commit 5e14c2a178
15 changed files with 62 additions and 137 deletions

View file

@ -42,18 +42,5 @@
"name": "English",
"path": "lang/en.json"
}
],
"packs": [
{
"name": "ikonis-features",
"label": "Ikonis Features",
"path": "packs/ikonis-features",
"type": "Item",
"system": "daggerheart",
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
}
]
}

Binary file not shown.

View file

@ -1 +0,0 @@
MANIFEST-000002

View file

View file

@ -1 +0,0 @@
2026/04/26-14:45:07.673032 146fb55fe6c0 Delete type=3 #1

Binary file not shown.

View file

@ -1,12 +0,0 @@
[
{ "name": "Kinetic Amplifier", "type": "feature", "img": "icons/magic/force/projectile-orb-blue.webp", "system": { "description": "<p>+1 Damage on Melee attacks.</p>", "attribution": { "source": "Ikonis Tech" } } },
{ "name": "Thermal Core", "type": "feature", "img": "icons/magic/fire/orb-magma.webp", "system": { "description": "<p>Deals Fire damage instead of Physical.</p>", "attribution": { "source": "Ikonis Tech" } } },
{ "name": "Static Coil", "type": "feature", "img": "icons/magic/lightning/bolt-blue.webp", "system": { "description": "<p>Targets hit are Dazed.</p>", "attribution": { "source": "Ikonis Tech" } } },
{ "name": "Reactive Plating", "type": "feature", "img": "icons/equipment/chest/breastplate-metal-scaled-silver.webp", "system": { "description": "<p>+1 Armor while equipped.</p>", "attribution": { "source": "Ikonis Tech" } } },
{ "name": "Long-Range Optics", "type": "feature", "img": "icons/tools/navigation/spyglass-brass.webp", "system": { "description": "<p>Increases Range by 1 step.</p>", "attribution": { "source": "Ikonis Tech" } } },
{ "name": "Precision Chip", "type": "feature", "img": "icons/commodities/tech/circuit-board.webp", "system": { "description": "<p>+1 to Crit range.</p>", "attribution": { "source": "Ikonis Tech" } } },
{ "name": "Burst Module", "type": "feature", "img": "icons/magic/fire/explosion-fireball-medium-red-orange.webp", "system": { "description": "<p>Can target 2 enemies (Half damage).</p>", "attribution": { "source": "Ikonis Tech" } } },
{ "name": "Siphon Link", "type": "feature", "img": "icons/magic/unholy/strike-body-life-drain-red.webp", "system": { "description": "<p>Recover 1 Hope on kill.</p>", "attribution": { "source": "Ikonis Tech" } } },
{ "name": "Gravity Plate", "type": "feature", "img": "icons/magic/air/vortex-wind-blue.webp", "system": { "description": "<p>Weapon is Heavy (more damage).</p>", "attribution": { "source": "Ikonis Tech" } } },
{ "name": "Ikonis Bond", "type": "feature", "img": "icons/magic/symbols/rune-sigil-blue-white.webp", "system": { "description": "<p>This weapon is bonded to your biological signature.</p>", "attribution": { "source": "Ikonis Tech" } } }
]

Binary file not shown.

View file

@ -1 +0,0 @@
MANIFEST-000002

View file

@ -1 +0,0 @@
2026/04/26-17:05:00.760293 146f67fff6c0 Delete type=3 #1

View file

@ -128,112 +128,65 @@ export function patchIkonisLogic() {
}
/**
* Internal helper to generate and cache virtual features for an actor.
* Safe to call multiple times; uses a cache to keep it fast.
* Synchronizes Ikonis Augments into the Daggerheart Homebrew settings.
* This makes them "Real" Weapon Features to the system.
*/
function _injectIkonisFeatures(actor) {
if (!actor || actor._isIkonisInjecting) return [];
export async function syncIkonisToHomebrew() {
if (!game.user.isGM) return;
// Initialize the cache if it doesn't exist
if (!actor._ikonisCache) actor._ikonisCache = new Map();
const MODULE_ID = 'dh-ikonis';
const homebrewKey = game.settings.settings.has('daggerheart.Homebrew') ? 'Homebrew' : 'homebrew';
const homebrew = game.settings.get('daggerheart', homebrewKey);
const ikonisFeatures = [];
actor._isIkonisInjecting = true;
if (!homebrew.itemFeatures) homebrew.itemFeatures = { weaponFeatures: {}, armorFeatures: {} };
if (!homebrew.itemFeatures.weaponFeatures) homebrew.itemFeatures.weaponFeatures = {};
try {
const weapons = actor.items.filter(i => i.type === 'weapon');
const allAugs = getAugments();
let updates = false;
const allAugments = getAugments();
for (const item of weapons) {
const isEquipped = item.system.equipped;
const installedIds = item.getFlag('dh-ikonis', 'installedAugments') || [];
const bondedUuid = item.getFlag('dh-ikonis', 'bondedFeatureUuid');
for (const aug of allAugments) {
const feature = _featureCache.get(aug.featureUuid) || await fromUuid(aug.featureUuid);
if (feature && !homebrew.itemFeatures.weaponFeatures[aug.id]) {
console.log(`DH-Ikonis | Registering ${aug.name} as native weapon feature...`);
const processFeature = (uuid, type) => {
if (!uuid) return;
// Extract original feature ID from UUID to build our virtual ID
const featureId = uuid.split('.').pop();
const virtualId = `ikonis-${item.id}-${featureId}`;
// Check cache first
if (actor._ikonisCache.has(virtualId)) {
if (isEquipped) ikonisFeatures.push(actor._ikonisCache.get(virtualId));
return;
// Format actions for the system's Homebrew model
const actions = {};
if (feature.system.actions) {
for (const [id, action] of Object.entries(feature.system.actions)) {
actions[id] = action.toObject();
}
// Resolve original and clone
const feature = _featureCache.get(uuid) || fromUuidSync(uuid);
if (feature) {
const featureClone = feature.clone({ parent: actor }, { keepId: true });
featureClone.system.type = "ikonis";
Object.defineProperty(featureClone, "id", { value: virtualId, enumerable: true });
actor._ikonisCache.set(virtualId, featureClone);
if (isEquipped) ikonisFeatures.push(featureClone);
}
};
if (bondedUuid) processFeature(bondedUuid, "bonded");
for (const id of installedIds) {
const aug = allAugs.find(a => String(a.id) === String(id));
if (aug?.featureUuid) processFeature(aug.featureUuid, "augment");
}
homebrew.itemFeatures.weaponFeatures[aug.id] = {
name: aug.name,
img: aug.img || feature.img,
description: feature.system.description,
actions: actions,
effects: Array.from(feature.effects || []).map(e => e.toObject())
};
updates = true;
}
} catch (err) {
console.error("DH-Ikonis | Error during feature injection:", err);
} finally {
actor._isIkonisInjecting = false;
}
return ikonisFeatures;
if (updates) {
await game.settings.set('daggerheart', homebrewKey, homebrew);
console.log("DH-Ikonis | Homebrew settings synchronized.");
}
}
/**
* Patches the Character Data Model to show features in the UI lists.
* Patches the system's weapon data preparation to handle slot counts.
*/
export function patchIkonisLogic() {
// We no longer need to patch Actor.allApplicableEffects
// because the system handles native weapon features automatically.
}
/**
* Placeholder for character patching - no longer needed for virtual items
*/
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); // This is an Array
if (!this.parent || !Array.isArray(lists)) return lists;
const ikonisFeatures = _injectIkonisFeatures(this.parent);
if (ikonisFeatures.length > 0) {
// Add our custom category to the end of the lists array
lists.push({
title: "Ikonis Augments",
type: "ikonis",
values: ikonisFeatures
});
}
return lists;
},
configurable: true
});
// Patch getEmbeddedDocument to resolve our virtual features
const originalGetEmbedded = Actor.prototype.getEmbeddedDocument;
Actor.prototype.getEmbeddedDocument = function(embeddedName, id, options) {
if (embeddedName === "Item" && typeof id === "string" && id.startsWith("ikonis-")) {
if (!this._ikonisCache?.has(id) && !this._isIkonisInjecting) {
_injectIkonisFeatures(this);
}
return this._ikonisCache?.get(id);
}
return originalGetEmbedded.call(this, embeddedName, id, options);
};
// Injection is deprecated in favor of native weapon features
}
export function patchDHWeapon() {

View file

@ -44,18 +44,15 @@ export function patchIkonisSheet() {
const doc = this.document;
if (!doc) return context;
const installedIds = doc.getFlag('dh-ikonis', 'installedAugments') || [];
const weaponFeatures = doc.system.weaponFeatures || [];
const allAugmentsList = getAugments() || [];
const processedAugments = [];
for (const id of installedIds) {
for (const featureRef of weaponFeatures) {
const id = featureRef.value;
const base = allAugmentsList.find(a => String(a.id) === String(id));
if (!base) continue;
const aug = { ...base, installed: true };
if (aug.featureUuid) {
const item = await getAttachedFeature(aug.featureUuid);
if (item) aug.feature = { name: item.name, img: item.img, uuid: item.uuid };
}
processedAugments.push(aug);
}
@ -97,17 +94,18 @@ export function patchIkonisSheet() {
// 5. Add custom methods
Weapon.prototype._onAddAugment = async function(event, target) {
const installedIds = this.document.getFlag('dh-ikonis', 'installedAugments') || [];
const weaponFeatures = this.document.system.weaponFeatures || [];
const allAugments = getAugments();
const validInstalled = installedIds.filter(id => allAugments.some(a => String(a.id) === String(id)));
// Filter out native features that aren't Ikonis augments if needed,
// but for now we'll just count total weapon features against slots
const maxSlots = getSlotCount(this.document);
if (validInstalled.length >= maxSlots) {
if (weaponFeatures.length >= maxSlots) {
ui.notifications.warn(`No more augment slots available! (Max: ${maxSlots})`);
return;
}
const available = allAugments.filter(a => !validInstalled.includes(String(a.id)));
const available = allAugments.filter(a => !weaponFeatures.some(f => f.value === a.id));
const content = `
<div class="augment-picker" style="max-height: 500px; display: flex; flex-direction: column; background: #0f0f1b; padding: 1rem; border-radius: 8px;">
@ -163,16 +161,16 @@ export function patchIkonisSheet() {
});
if (res && res !== "cancel") {
const newIds = [...validInstalled, String(res)];
await this.document.update({ "flags.dh-ikonis.installedAugments": newIds });
const newFeatures = [...weaponFeatures, { value: res }];
await this.document.update({ "system.weaponFeatures": newFeatures });
this.render(true);
}
};
Weapon.prototype._onRemoveAugment = async function(event, target) {
const installedIds = this.document.getFlag('dh-ikonis', 'installedAugments') || [];
const newIds = installedIds.filter(id => id !== String(target.dataset.id));
await this.document.update({ "flags.dh-ikonis.installedAugments": newIds });
const weaponFeatures = this.document.system.weaponFeatures || [];
const newFeatures = weaponFeatures.filter(f => f.value !== String(target.dataset.id));
await this.document.update({ "system.weaponFeatures": newFeatures });
this.render(true);
};

View file

@ -120,6 +120,9 @@ Hooks.once('ready', async () => {
if (game.settings.get(MODULE_ID, "enableCurrencyOverride")) {
await overrideCurrency();
}
// Sync to Native Homebrew
await syncIkonisToHomebrew();
}
await loadIkonisFeatures();