237 lines
11 KiB
JavaScript
237 lines
11 KiB
JavaScript
import { getAugments, getSlotCount } from './ikonis-data.js';
|
|
|
|
/**
|
|
* Patches the Daggerheart Weapon sheet to include the Ikonis tab.
|
|
* This is more robust than extending the class in Foundry V14.
|
|
*/
|
|
export function patchIkonisSheet() {
|
|
const { Weapon } = game.system.api.applications.sheets.items || {};
|
|
if (!Weapon) {
|
|
console.error("DH-Ikonis | Weapon sheet class not found in system API!");
|
|
return;
|
|
}
|
|
|
|
console.log("DH-Ikonis | Patching Weapon sheet prototype...");
|
|
|
|
// 1. Add the Ikonis Tab to TABS (on the static class)
|
|
if (Weapon.TABS?.primary) {
|
|
const hasTab = Weapon.TABS.primary.tabs.some(t => t.id === 'motherboard');
|
|
if (!hasTab) {
|
|
Weapon.TABS.primary.tabs.push({
|
|
id: 'motherboard',
|
|
label: 'DAGGERHEART.ITEMS.Ikonis.Motherboard',
|
|
icon: 'fa-solid fa-microchip'
|
|
});
|
|
}
|
|
}
|
|
|
|
// 2. Add the Motherboard Part to PARTS
|
|
if (!Weapon.PARTS.motherboard) {
|
|
Weapon.PARTS.motherboard = {
|
|
template: 'modules/dh-ikonis/templates/ikonis-motherboard.hbs',
|
|
scrollable: ['.motherboard-content']
|
|
};
|
|
}
|
|
|
|
// 3. Patch _prepareContext
|
|
const originalPrepare = Weapon.prototype._prepareContext;
|
|
Weapon.prototype._prepareContext = async function(options) {
|
|
let context = await originalPrepare.call(this, options);
|
|
|
|
try {
|
|
const doc = this.document;
|
|
if (!doc) return context;
|
|
|
|
const weaponFeatures = doc.system.weaponFeatures || [];
|
|
const allAugmentsList = getAugments() || [];
|
|
|
|
const processedAugments = [];
|
|
let bondedFeature = null;
|
|
|
|
// Identify "Bonded" features by name
|
|
const allNativeFeatures = CONFIG.DH.ITEM.allWeaponFeatures() || {};
|
|
const bondedIds = Object.keys(allNativeFeatures).filter(k => allNativeFeatures[k].name === "Ikonis: Bonded");
|
|
|
|
// Auto-install if missing (check by name/presence of any bonded ID)
|
|
const hasBonded = weaponFeatures.some(f => bondedIds.includes(f.value));
|
|
if (bondedIds.length > 0 && !hasBonded) {
|
|
console.log(`DH-Ikonis | Auto-installing Bonded feature on ${doc.name}`);
|
|
const bondedId = bondedIds.includes("ikonis-bonded") ? "ikonis-bonded" : bondedIds[0];
|
|
const newFeatures = [...weaponFeatures, { value: bondedId }];
|
|
doc.update({ "system.weaponFeatures": newFeatures });
|
|
}
|
|
|
|
for (const featureRef of weaponFeatures) {
|
|
const nativeId = featureRef.value;
|
|
|
|
// Special handling for Bonded
|
|
if (bondedIds.includes(nativeId)) {
|
|
const feature = allNativeFeatures[nativeId];
|
|
bondedFeature = {
|
|
id: nativeId,
|
|
name: "Bonded",
|
|
fullName: feature.name,
|
|
effect: feature.description ? feature.description.replace(/<[^>]*>?/gm, '').substring(0, 100) + "..." : "Primary module",
|
|
installed: true
|
|
};
|
|
continue;
|
|
}
|
|
|
|
const base = allAugmentsList.find(a => String(a.id) === String(nativeId));
|
|
if (!base) continue;
|
|
|
|
processedAugments.push({ ...base, installed: true });
|
|
}
|
|
|
|
context.ikonis = {
|
|
enabled: true,
|
|
augments: processedAugments,
|
|
bonded: bondedFeature,
|
|
isGM: game.user?.isGM || false
|
|
};
|
|
|
|
context.maxSlots = getSlotCount(doc);
|
|
context.usedSlots = processedAugments.length; // Bonded doesn't count
|
|
|
|
// Explicitly pass the active tab to the template
|
|
context.activeTab = this.tabGroups?.primary || "";
|
|
|
|
return context;
|
|
} catch (err) {
|
|
console.error("DH-Ikonis | Error in patched _prepareContext:", err);
|
|
return context;
|
|
}
|
|
};
|
|
|
|
// 4. Patch _onClickAction
|
|
const originalClick = Weapon.prototype._onClickAction;
|
|
Weapon.prototype._onClickAction = function(event, target) {
|
|
const action = target.dataset.action;
|
|
if (action === "addAugment") return this._onAddAugment(event, target);
|
|
if (action === "removeAugment") return this._onRemoveAugment(event, target);
|
|
return originalClick.call(this, event, target);
|
|
};
|
|
|
|
// 5. Add custom methods
|
|
Weapon.prototype._onAddAugment = async function(event, target) {
|
|
const weaponFeatures = this.document.system.weaponFeatures || [];
|
|
const allAugments = getAugments();
|
|
const augmentIds = allAugments.map(a => a.id);
|
|
|
|
// Identify all features that are recognized as augments (and NOT "Bonded")
|
|
const allNativeFeatures = CONFIG.DH.ITEM.allWeaponFeatures() || {};
|
|
const bondedIds = Object.keys(allNativeFeatures).filter(k => allNativeFeatures[k].name === "Ikonis: Bonded");
|
|
|
|
const usedSlotsCount = weaponFeatures.filter(f => {
|
|
if (bondedIds.includes(f.value)) return false;
|
|
return augmentIds.includes(f.value);
|
|
}).length;
|
|
|
|
const maxSlots = getSlotCount(this.document);
|
|
if (usedSlotsCount >= maxSlots) {
|
|
ui.notifications.warn(`No more augment slots available! (Max: ${maxSlots})`);
|
|
return;
|
|
}
|
|
|
|
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;">
|
|
<div class="picker-header" style="margin-bottom: 1rem;">
|
|
<input type="text" id="augment-search" placeholder="Search augments..." style="width: 100%; background: #1a1a2e; color: white; border: 1px solid #2d3436; padding: 0.5rem;">
|
|
</div>
|
|
<div class="picker-list" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 0.75rem;">
|
|
${available.map(a => `
|
|
<div class="picker-item" data-id="${a.id}" style="border: 2px solid #2d3436; background: #1a1a2e; padding: 1rem; border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 1rem;">
|
|
<img src="${a.img}" style="width: 40px; height: 40px; border: none; filter: drop-shadow(0 0 5px rgba(255,255,255,0.1));">
|
|
<div class="augment-info">
|
|
<strong class="aug-name" style="display: block; font-size: 1.1rem; color: #ffffff;">${a.name}</strong>
|
|
<span style="display: block; margin: 2px 0; font-size: 0.9rem; color: #d1d8e0;">${a.effect}</span>
|
|
<div style="display: flex; flex-direction: column; gap: 2px;">
|
|
<small style="color: #888; font-style: italic;">${a.cost}</small>
|
|
${a.precompile ? `<small style="color: #ff9f43; font-weight: bold;">${a.precompile}</small>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
let selectedId = null;
|
|
const res = await foundry.applications.api.DialogV2.wait({
|
|
window: { title: "Install Tech", width: 550 },
|
|
content: content,
|
|
buttons: [
|
|
{ action: "ok", label: "Install", icon: "fa-solid fa-download", callback: () => selectedId },
|
|
{ action: "cancel", label: "Cancel", icon: "fa-solid fa-times" }
|
|
],
|
|
render: (e, app) => {
|
|
const search = app.element.querySelector('#augment-search');
|
|
if (search) search.focus();
|
|
search?.addEventListener('input', (event) => {
|
|
const query = event.target.value.toLowerCase();
|
|
app.element.querySelectorAll('.picker-item').forEach(el => {
|
|
el.style.display = el.innerText.toLowerCase().includes(query) ? 'block' : 'none';
|
|
});
|
|
});
|
|
|
|
app.element.querySelectorAll('.picker-item').forEach(el => {
|
|
el.addEventListener('click', () => {
|
|
app.element.querySelectorAll('.picker-item').forEach(i => {
|
|
i.style.borderColor = "#2d3436";
|
|
i.style.background = "#1a1a2e";
|
|
i.classList.remove('selected');
|
|
});
|
|
el.style.borderColor = "#00d2ff";
|
|
el.style.background = "rgba(0, 210, 255, 0.15)";
|
|
el.classList.add('selected');
|
|
selectedId = el.dataset.id;
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
if (res && res !== "cancel") {
|
|
const newFeatures = [...weaponFeatures, { value: res }];
|
|
await this.document.update({ "system.weaponFeatures": newFeatures });
|
|
this.render(true);
|
|
}
|
|
};
|
|
|
|
Weapon.prototype._onRemoveAugment = async function(event, target) {
|
|
let weaponFeatures = foundry.utils.deepClone(this.document.system.weaponFeatures || []);
|
|
const targetId = target.dataset.id;
|
|
|
|
// 1. Pre-emptive cleanup of all stale effectIds on the weapon.
|
|
// This is necessary because the system crashes if it tries to remove a feature
|
|
// that points to an effect ID that no longer exists in the weapon's effects collection.
|
|
let needsCleanup = false;
|
|
const cleanedFeatures = weaponFeatures.map(f => {
|
|
if (Array.isArray(f.effectIds)) {
|
|
const originalCount = f.effectIds.length;
|
|
f.effectIds = f.effectIds.filter(id => this.document.effects.has(id));
|
|
if (f.effectIds.length !== originalCount) needsCleanup = true;
|
|
}
|
|
return f;
|
|
});
|
|
|
|
if (needsCleanup) {
|
|
console.log("DH-Ikonis | Cleaning up stale effectIds to prevent system crash...");
|
|
await this.document.update({ "system.weaponFeatures": cleanedFeatures });
|
|
// Refresh our local copy after update
|
|
weaponFeatures = foundry.utils.deepClone(this.document.system.weaponFeatures);
|
|
}
|
|
|
|
// 2. Perform the actual removal
|
|
const idx = weaponFeatures.findIndex(f => String(f.value) === String(targetId));
|
|
if (idx !== -1) {
|
|
weaponFeatures.splice(idx, 1);
|
|
await this.document.update({ "system.weaponFeatures": weaponFeatures });
|
|
}
|
|
|
|
this.render(true);
|
|
};
|
|
|
|
console.log("DH-Ikonis | Weapon sheet patched successfully.");
|
|
}
|