initial commit

This commit is contained in:
CPTN Cosmo 2026-04-26 21:01:44 +02:00
commit 7246e3f892
10 changed files with 1728 additions and 0 deletions

754
scripts/augments.js Normal file
View file

@ -0,0 +1,754 @@
export const DEFAULT_AUGMENTS = [
{
id: "bonded",
name: "Bonded",
effect: "Primary module for Ikonis hardware synchronization.",
cost: "",
img: "icons/magic/control/debuff-energy-hold-blue-yellow.webp",
effects: [
{
_id: "IkonisBondedEffX",
name: "Ikonis: Bonded",
type: "base",
img: "icons/magic/control/debuff-energy-hold-blue-yellow.webp",
system: {
changes: [
{
key: "system.bonuses.damage.primaryWeapon.bonus",
type: "add",
value: "@tier",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "force",
name: "Force",
effect: "+1 Damage",
cost: "Cost: 3 gears, 2 lenses, 4 aluminum, 1 capacitor",
img: "icons/commodities/gems/gem-cut-faceted-princess-purple.webp",
effects: [
{
_id: "IkonisForceEffXX",
name: "Ikonis: Force",
type: "base",
img: "icons/commodities/gems/gem-cut-faceted-princess-purple.webp",
system: {
changes: [
{
key: "system.bonuses.damage.physical.bonus",
type: "add",
value: "1",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "guard",
name: "Guard",
effect: "+1 Armor Score",
cost: "Cost: 3 wires, 2 silver, 2 platinum, 3 fuses",
img: "icons/commodities/gems/gem-cut-faceted-square-purple.webp",
effects: [
{
_id: "IkonisGuardEffXX",
name: "Ikonis: Guard",
type: "base",
img: "icons/commodities/gems/gem-cut-faceted-square-purple.webp",
system: {
changes: [
{
type: "armor",
phase: "initial",
value: {
current: 0,
max: "1",
interaction: "none",
damageThresholds: null
},
priority: 20
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "converge",
name: "Converge",
effect: "+1 to attack rolls",
cost: "Cost: 4 coils, 2 crystals, 5 gold, 3 discs",
img: "icons/commodities/gems/gem-cut-square-green.webp",
effects: [
{
_id: "IkonisConvergeXX",
name: "Ikonis: Converge",
type: "base",
img: "icons/commodities/gems/gem-cut-square-green.webp",
system: {
changes: [
{
key: "system.bonuses.roll.attack.bonus",
type: "add",
value: "1",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "amplify",
name: "Amplify",
effect: "On a successful attack, roll an additional damage die and drop the lowest result.",
cost: "Cost: 4 crystals, 4 cobalt, 4 copper, 4 capacitors",
img: "icons/commodities/gems/gem-cut-table-green.webp",
effects: [
{
_id: "IkonisAmplifyXXX",
name: "Ikonis: Amplify",
type: "base",
img: "icons/commodities/gems/gem-cut-table-green.webp",
system: {
changes: [
{
key: "system.bonuses.damage.primaryWeapon.dice",
type: "add",
value: "1",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "scope",
name: "Scope",
effect: " Increase range by one step (Melee to Very Close, Close to Far, etc.).",
cost: "Cost: 5 lenses, 3 silver, 2 circuits, 2 relays",
img: "icons/commodities/gems/gem-faceted-asscher-blue.webp",
effects: [
{
_id: "IkonisScopeEffXX",
name: "Ikonis: Scope",
type: "base",
img: "icons/commodities/gems/gem-faceted-asscher-blue.webp",
system: {
changes: [],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "deny",
name: "Deny",
effect: "+2 Armor Score",
cost: "6 coils, 3 wire, 2 copper, 4 batteries",
img: "icons/commodities/gems/gem-faceted-cushion-teal-black.webp",
effects: [
{
_id: "IkonisGuardEffXX",
name: "Ikonis: Guard",
type: "base",
img: "icons/commodities/gems/gem-faceted-cushion-teal-black.webp",
system: {
changes: [
{
type: "armor",
phase: "initial",
value: {
current: 0,
max: "2",
interaction: "none",
damageThresholds: null
},
priority: 20
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "target",
name: "Target",
effect: "+2 to attack rolls",
cost: "Cost: 10 wires, 7 gold, 5 fuses, 5 circuits, 2 batteries",
img: "icons/commodities/gems/gem-faceted-cushion-teal.webp",
effects: [
{
_id: "IkonisConvergeXX",
name: "Ikonis: Converge",
type: "base",
img: "icons/commodities/gems/gem-faceted-cushion-teal.webp",
system: {
changes: [
{
key: "system.bonuses.roll.attack.bonus",
type: "add",
value: "2",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "split",
name: "Split",
effect: "When you make an attack, mark a Stress to target another creature within range.",
cost: "12 gears, 5 lenses, 15 aluminum, 9 relays",
img: "icons/commodities/gems/gem-faceted-diamond-blue.webp",
actions: {
"IkonisSplitActXX": {
"name": "Split Attack",
"type": "action",
"_id": "IkonisSplitActXX",
"img": "icons/commodities/gems/gem-faceted-diamond-blue.webp",
"systemPath": "actions",
"baseAction": false,
"description": "When you make an attack, mark a Stress to target another creature within range.",
"chatDisplay": true,
"actionType": "action",
"name": "Mark a Stress",
"cost": [
{
"scalable": false,
"key": "stress",
"value": 1,
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
],
"target": {
"type": "any",
"amount": 1
}
}
},
effects: [
{
_id: "IkonisSplitEffXX",
name: "Ikonis: Split",
type: "base",
img: "icons/commodities/gems/gem-faceted-diamond-blue.webp",
system: {
changes: [],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "fix",
name: "Fix",
effect: "When you deal Severe damage, clear a Hit Point.",
cost: "Cost: 6 coils, 4 wires, 1 crystal, 5 cobalt, 5 silver, 7 relays, 2 batteries",
img: "icons/commodities/gems/gem-faceted-diamond-green.webp",
actions: {
"IkonisFixActXXXX": {
"name": "Clear a Hit Point",
"type": "healing",
"_id": "IkonisFixActXXXX",
"img": "icons/commodities/gems/gem-faceted-diamond-green.webp",
"systemPath": "actions",
"baseAction": false,
"description": "Clear a Hit Point when you deal Severe damage.",
"chatDisplay": true,
"actionType": "action",
"healing": {
"parts": {
"hitPoints": {
"applyTo": "hitPoints",
"resultBased": false,
"value": {
"multiplier": "flat",
"flatMultiplier": 1,
"dice": "d6",
"bonus": null,
"custom": {
"enabled": true,
"formula": "1"
}
}
}
},
"includeBase": false,
"direct": false
}
}
},
effects: [
{
_id: "IkonisFixEffXXXX",
name: "Ikonis: Fix",
type: "base",
img: "icons/commodities/gems/gem-faceted-diamond-green.webp",
system: {
changes: [],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "scare",
name: "Scare",
effect: "When you critically succeed on an attack, the target must mark a Stress.",
cost: "Cost: 6 triggers, 8 copper, 9 aluminum, 10 discs",
img: "icons/commodities/gems/gem-faceted-diamond-pink.webp",
effects: [
{
_id: "IkonisScareEffXX",
name: "Ikonis: Scare",
type: "base",
img: "icons/commodities/gems/gem-faceted-diamond-pink.webp",
system: {
changes: [],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "sear",
name: "Sear",
effect: "+2 damage",
cost: "Cost: 11 triggers, 11 platinum, 11 circuits, 7 discs",
precompile: "Tier 2",
img: "icons/commodities/gems/gem-faceted-navette-red.webp",
effects: [
{
_id: "IkonisForceEffXX",
name: "Ikonis: Force",
type: "base",
img: "icons/commodities/gems/gem-faceted-navette-red.webp",
system: {
changes: [
{
key: "system.bonuses.damage.physical.bonus",
type: "add",
value: "2",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "absorb",
name: "Absorb",
effect: "You can mark an additional Armor Slot against incoming damage.",
cost: "Cost: 26 gears, 13 gold, 15 relays, 8 batteries",
precompile: "Tier 2",
img: "icons/commodities/gems/gem-faceted-hexagon-blue.webp",
effects: [
{
_id: "IkonisAbsorbEffX",
name: "Ikonis: Absorb",
type: "base",
img: "icons/commodities/gems/gem-faceted-hexagon-blue.webp",
system: {
changes: [
{
key: "system.rules.damageReduction.maxArmorMarked.value",
type: "add",
value: "1",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "kick",
name: "Kick",
effect: "On a successful attack, you can mark 2 Stress to force the target to mark an additional Hit Point.",
cost: "Cost: 33 triggers, 13 crystals, 23 cobalt, 16 discs",
precompile: "Tier 2",
img: "icons/commodities/gems/gem-faceted-oval-blue.webp",
actions: {
"IkonisKickActXX": {
"name": "Kick",
"type": "action",
"_id": "IkonisKickActXX",
"img": "icons/commodities/gems/gem-faceted-oval-blue.webp",
"systemPath": "actions",
"baseAction": false,
"description": "On a successful attack, you can mark 2 Stress to force the target to mark an additional Hit Point.",
"chatDisplay": true,
"actionType": "action",
"name": "Mark 2 Stress",
"cost": [
{
"scalable": false,
"key": "stress",
"value": 2,
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
],
"target": {
"type": "any",
"amount": 1
}
}
},
effects: [
{
_id: "IkonisKickEffXXX",
name: "Ikonis: Kick",
type: "base",
img: "icons/commodities/gems/gem-faceted-oval-blue.webp",
system: {
changes: [],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "block",
name: "Block",
effect: "+3 Armor Score; 1 Evasion",
cost: "Cost: 27 crystals, 67 aluminum, 33 relays, 4 capacitors, 5 batteries",
precompile: "Tier 3",
img: "icons/commodities/gems/gem-faceted-octagon-yellow.webp",
effects: [
{
_id: "IkonisBlockEffXX",
name: "Ikonis: Block",
type: "base",
img: "icons/commodities/gems/gem-faceted-octagon-yellow.webp",
system: {
changes: [
{
type: "armor",
phase: "initial",
value: {
current: 0,
max: "3",
interaction: "none",
damageThresholds: null
},
priority: 20
},
{
key: "system.evasion",
type: "add",
value: "-1",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "zip",
name: "Zip",
effect: "Move up to Far range as part of an attack.",
cost: "Cost: 37 coils, 43 silver, 67 fuses, 12 capacitors",
precompile: "Tier 3",
img: "icons/commodities/gems/gem-faceted-oval-blue.webp",
effects: [
{
_id: "IkonisZipEffXXXX",
name: "Ikonis: Zip",
type: "base",
img: "icons/commodities/gems/gem-faceted-oval-blue.webp",
system: {
changes: [],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "bury",
name: "Bury",
effect: "+3 damage",
cost: "Cost: 28 triggers, 28 circuits, 28 relays, 1 relic",
precompile: "Tier 3",
img: "icons/commodities/gems/gem-faceted-radiant-blue.webp",
effects: [
{
_id: "IkonisBuryEffXXX",
name: "Ikonis: Bury",
type: "base",
img: "icons/commodities/gems/gem-faceted-radiant-blue.webp",
system: {
changes: [
{
key: "system.bonuses.damage.physical.bonus",
type: "add",
value: "3",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "follow",
name: "Follow",
effect: "Mark 2 Stress to reroll your attack.",
cost: "Cost: 75 gears, 67 lenses, 30 copper, 33 circuits",
precompile: "Tier 4",
img: "icons/commodities/gems/gem-faceted-radiant-red.webp",
actions: {
"IkonisFollowActXX": {
"name": "Follow",
"type": "action",
"_id": "IkonisFollowActXX",
"img": "icons/commodities/gems/gem-faceted-radiant-red.webp",
"systemPath": "actions",
"baseAction": false,
"description": "Mark 2 Stress to reroll your attack.",
"chatDisplay": true,
"actionType": "reroll",
"cost": [
{
"scalable": false,
"key": "stress",
"value": 2,
"itemId": null,
"step": null,
"consumeOnSuccess": false
}
],
"target": {
"type": "any",
"amount": 1
}
}
},
effects: [
{
_id: "IkonisFollowEffX",
name: "Ikonis: Follow",
type: "base",
img: "icons/commodities/gems/gem-faceted-radiant-red.webp",
system: {
changes: [],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
},
{
id: "override",
name: "Override",
effect: "Attack rolls have advantage.",
cost: "Cost: 63 wires, 71 gold, 58 discs, 5 relics",
precompile: "Tier 4",
img: "icons/commodities/gems/gem-faceted-round-white.webp",
effects: [
{
_id: "IkonisOverrideXX",
name: "Ikonis: Override",
type: "base",
img: "icons/commodities/gems/gem-faceted-round-white.webp",
system: {
changes: [
{
key: "system.advantageSources",
type: "add",
value: "1",
priority: null,
phase: "initial"
}
],
duration: { description: "", type: "" },
rangeDependence: { enabled: false, type: "withinRange", target: "hostile", range: "melee" },
stacking: null,
targetDispositions: []
},
disabled: false,
transfer: true,
statuses: [],
showIcon: 1,
flags: {}
}
]
}
];

157
scripts/ikonis-data.js Normal file
View file

@ -0,0 +1,157 @@
import { DEFAULT_AUGMENTS } from './augments.js';
// Global caches for resolved features to keep getters fast
const _featureCache = new Map();
/**
* Injects Ikonis defaults directly into the system configuration.
*/
export function registerIkonisFeatures() {
if (!CONFIG.DH?.ITEM?.weaponFeatures) return;
console.log("DH-Ikonis | Registering features in CONFIG...");
for (const aug of DEFAULT_AUGMENTS) {
const nativeId = `ikonis-${aug.id}`;
CONFIG.DH.ITEM.weaponFeatures[nativeId] = {
name: `Ikonis: ${aug.name}`,
img: aug.img || "systems/daggerheart/assets/icons/documents/items/chip.svg",
description: aug.cost ? `<p>${aug.effect}</p><p>${aug.cost}</p>` : `<p>${aug.effect}</p>`,
actions: aug.actions || {},
effects: aug.effects || []
};
}
console.log("DH-Ikonis | Features registered in CONFIG.");
}
/**
* Seeds the Daggerheart Homebrew settings with Ikonis defaults if they don't exist.
*/
export async function seedIkonisHomebrew() {
if (!game.user.isGM) return;
let homebrewKey = 'Homebrew';
if (!game.settings.settings.has('daggerheart.Homebrew')) {
if (game.settings.settings.has('daggerheart.homebrew')) homebrewKey = 'homebrew';
else {
console.warn("DH-Ikonis | Daggerheart Homebrew setting not found.");
return;
}
}
const current = game.settings.get('daggerheart', homebrewKey) || {};
const homebrew = (typeof current.toObject === 'function') ? current.toObject() : foundry.utils.deepClone(current);
console.log(`DH-Ikonis | Seeding homebrew features (key: ${homebrewKey})...`);
if (!homebrew.itemFeatures) homebrew.itemFeatures = { weaponFeatures: {}, armorFeatures: {} };
if (!homebrew.itemFeatures.weaponFeatures) homebrew.itemFeatures.weaponFeatures = {};
let updates = false;
const recognizedIds = DEFAULT_AUGMENTS.map(aug => `ikonis-${aug.id}`);
// Purge undefined Ikonis features
for (const [id, feature] of Object.entries(homebrew.itemFeatures.weaponFeatures)) {
if (id.startsWith('ikonis-') && !recognizedIds.includes(id)) {
console.log(`DH-Ikonis | Purging undefined feature: ${id}`);
delete homebrew.itemFeatures.weaponFeatures[id];
updates = true;
}
}
for (const aug of DEFAULT_AUGMENTS) {
const nativeId = `ikonis-${aug.id}`;
// Force update to ensure 16-character IDs and correct mechanical effects are synced
console.log(`DH-Ikonis | Seeding/Updating feature: ${aug.name}`);
let description = `<p>${aug.effect}</p>`;
if (aug.cost) description += `<p>${aug.cost}</p>`;
if (aug.precompile) description += `<p>Precompile: ${aug.precompile}</p>`;
homebrew.itemFeatures.weaponFeatures[nativeId] = {
name: `Ikonis: ${aug.name}`,
img: aug.img || "systems/daggerheart/assets/icons/documents/items/chip.svg",
description: description,
actions: aug.actions || {},
effects: aug.effects || []
};
updates = true;
}
if (updates) {
await game.settings.set('daggerheart', homebrewKey, homebrew);
console.log("DH-Ikonis | Default blueprints seeded into Homebrew settings.");
} else {
console.log("DH-Ikonis | Homebrew features are already up to date.");
}
}
/**
* Scans the Daggerheart system config for any weapon features starting with "Ikonis:".
* These are treated as available augments for our slot system.
*/
export function getAugments() {
// We get the resolved features from CONFIG.DH.ITEM which includes system + homebrew
const allFeatures = CONFIG.DH.ITEM.allWeaponFeatures() || {};
const augments = [];
for (const [id, feature] of Object.entries(allFeatures)) {
const name = feature.label || feature.name || "";
if (name.startsWith("Ikonis:")) {
let desc = feature.description || "";
// Replace common line-breaking tags with actual newlines before stripping
desc = desc.replace(/<\/p>|<br\s*\/?>/gi, '\n');
desc = desc.replace(/<[^>]*>?/gm, '').trim();
const lines = desc.split('\n').map(l => l.trim()).filter(l => l.length > 0);
const costLine = lines.find(l => l.toLowerCase().startsWith("cost:"));
const precompileLine = lines.find(l => l.toLowerCase().startsWith("precompile:"));
const effectLine = lines.find(l => !l.toLowerCase().startsWith("cost:") && !l.toLowerCase().startsWith("precompile:"));
augments.push({
id: id,
name: name.replace("Ikonis:", "").trim(),
fullName: name,
img: feature.img || "systems/daggerheart/assets/icons/documents/items/chip.svg",
effect: effectLine || "Native Feature",
cost: costLine || "",
precompile: precompileLine || ""
});
}
}
return augments;
}
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 + 1;
}
}
/**
* Patches the system's weapon data preparation to handle slot counts.
*/
export function patchIkonisLogic() {
// Current slot logic is handled via getSlotCount in the sheet context
}
/**
* Placeholder for character patching - no longer needed for virtual items
*/
export function patchDhCharacter(DhCharacter) {
// Injection is deprecated in favor of native weapon features
}
export function patchDHWeapon() {
// Future: Add damage type override logic here
}

237
scripts/ikonis-sheet.js Normal file
View file

@ -0,0 +1,237 @@
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.");
}

101
scripts/main.js Normal file
View file

@ -0,0 +1,101 @@
import { patchDHWeapon, patchIkonisLogic, patchDhCharacter, seedIkonisHomebrew, registerIkonisFeatures } from './ikonis-data.js';
import { patchIkonisSheet } from './ikonis-sheet.js';
const MODULE_ID = 'dh-ikonis';
Hooks.once('init', () => {
console.log(`${MODULE_ID} | Initializing Ikonis Module`);
// Register default features in CONFIG immediately
registerIkonisFeatures();
// --- Slot Settings ---
[1, 2, 3, 4].forEach(tier => {
game.settings.register(MODULE_ID, `slotsTier${tier}`, {
name: `Slots at Tier ${tier}`,
hint: `How many augment slots a weapon gets at Tier ${tier}.`,
scope: "world",
config: true,
type: Number,
default: tier + 1
});
});
// Toggle for Tech Rename
game.settings.register(MODULE_ID, "enableTechRename", {
name: "Rename Magic to Tech",
hint: "Renames 'Magical' damage to 'Tech' damage and updates the icon to a microchip.",
scope: "world",
config: true,
type: Boolean,
default: true
});
// Toggle for Currency Override
game.settings.register(MODULE_ID, "enableCurrencyOverride", {
name: "Enable Quantum Currency",
hint: "Automatically overrides homebrew settings to use 'Quantum' credits and atomic icons.",
scope: "world",
config: true,
type: Boolean,
default: true
});
const DhCharacter = CONFIG.Actor.dataModels?.character;
if (DhCharacter) patchDhCharacter(DhCharacter);
});
Hooks.on('setup', () => {
// Damage Type Rename
if (game.settings.get(MODULE_ID, "enableTechRename")) {
const mag = CONFIG.DH?.GENERAL?.damageTypes?.magical;
if (mag) {
mag.label = "Tech";
mag.abbreviation = "TCH";
mag.icon = "fa-solid fa-microchip";
}
}
patchDHWeapon();
patchIkonisLogic();
patchIkonisSheet();
});
Hooks.once('ready', async () => {
console.log(`${MODULE_ID} | Ready.`);
if (game.user.isGM) {
// Seed default Ikonis features into native Homebrew if missing
await seedIkonisHomebrew();
if (game.settings.get(MODULE_ID, "enableCurrencyOverride")) {
await overrideCurrency();
}
}
const DhCharacter = game.system.api?.models?.actors?.DhCharacter || CONFIG.Actor.dataModels?.character;
if (DhCharacter) patchDhCharacter(DhCharacter);
});
async function overrideCurrency() {
let key = 'Homebrew';
if (!game.settings.settings.has('daggerheart.Homebrew')) {
if (game.settings.settings.has('daggerheart.homebrew')) key = 'homebrew';
else return;
}
const homebrew = game.settings.get('daggerheart', key);
if (!homebrew || homebrew.currency?.title === "Quantum") return;
const newHomebrew = (typeof homebrew.toObject === 'function') ? homebrew.toObject() : foundry.utils.deepClone(homebrew);
newHomebrew.currency = {
title: "Quantum",
coins: { enabled: true, label: "Quantum", icon: "fa-solid fa-atom" },
handfuls: { enabled: false, label: "Handfuls", icon: "fa-solid fa-coins" },
bags: { enabled: false, label: "Bags", icon: "fa-solid fa-sack" },
chests: { enabled: false, label: "Chests", icon: "fa-solid fa-treasure-chest" }
};
await game.settings.set('daggerheart', key, newHomebrew);
console.log(`${MODULE_ID} | Currency system updated to Quantum Credits.`);
}