commit 7246e3f892431f0364ba6920f48aed99a8c48635 Author: cosmo Date: Sun Apr 26 21:01:44 2026 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d13f59 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Daggerheart - Ikonis + +A Foundry VTT module for the **Daggerheart** system that adds a customizable "Ikonis" personalized weapon system. + +The Ikonis system allows weapons to be upgraded with "Augments" (Tech) that provide various bonuses, effects, and modifications, managed through a dedicated hardware interface on the weapon sheet. + +## Features + +### The Ikonis Hardware Tab +Every weapon sheet now includes a dedicated **Ikonis** tab. +- **Install Tech**: Drag and drop features or select from the pre-defined list. +- **Bonded Features**: Link a specific "Bonded" feature to the weapon that follows the user. +- **Dynamic Slot Scaling**: The number of available augment slots automatically scales based on the weapon's **Tier** (1-4). + +### Global Hardware Manager +GMs can access the **Global Hardware Manager** in the module settings to: +- Define the global pool of available Augments. +- Set the cost, effect, and name of each augment. +- Link augments to specific **Features** (items) for automated description and icon fetching. +- Configure the default "Bonded" feature for all Ikonis weapons. + +### Integrated Compendium +Includes an **Ikonis Features** compendium containing 10 ready-to-use augments and bond effects with custom icons and descriptions. + +### Per-Tier Configuration +Fine-tune the balance of your game by setting exactly how many slots are provided at each tier (1, 2, 3, and 4) in the module settings. + +### Quantum Currency +The module automatically overrides the game's homebrew settings to establish a streamlined, high-tech economy: +- **Quantum**: Replaces traditional gold with "Quantum" credits. +- **Unified Economy**: Disables multi-tier currency (bags, chests) in favor of a single, tech-focused value system. +- **Visual Integration**: Features a custom atomic icon for the currency display across all sheets. + +## Installation + +1. Open the Foundry VTT Setup screen. +2. Go to the **Add-on Modules** tab. +3. Click **Install Module**. +4. Paste the following manifest URL: + `https://git.geeks.gay/cosmo/dh-ikonis/raw/branch/main/module.json` +5. Click **Install**. + +## Usage + +1. **For GMs**: + - Go to **Configure Settings** > **Daggerheart - Ikonis**. + - Click **Open Augment Manager** to customize your global tech list. + - Adjust the **Slots per Tier** settings to match your desired power level. +2. **For Players**: + - Open a Weapon sheet. + - Click the **Ikonis** tab (microchip icon). + - Use the **Install Tech** button to add augments from the global list. + - Drag and drop features from the compendium into the Ikonis tab to link them. + +## Credits + +- Developed for the Daggerheart community. +- Built by Antigravity & Cosmo. +- Icons from the Foundry VTT core library. + +--- +*This is an unofficial module for the Daggerheart system.* diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..85a0e5c --- /dev/null +++ b/lang/en.json @@ -0,0 +1,71 @@ +{ + "DAGGERHEART": { + "CONFIG": { + "DamageType": { + "magical": { + "name": "Tech", + "abbreviation": "TECH" + }, + "magicalDamage": "Tech Damage" + }, + "ArmorFeature": { + "magical": { + "name": "Tech", + "description": "You can't mark an Armor Slot to reduce tech damage.", + "effects": { + "magical": { + "name": "Tech", + "description": "You can't mark an Armor Slot to reduce tech damage." + } + } + } + } + }, + "GENERAL": { + "Damage": { + "magicalDamage": "Tech Damage" + }, + "DamageResistance": { + "magicalResistance": { + "label": "Damage Resistance: Tech", + "hint": "Tech Damage is halved if this is set to 1" + }, + "magicalImmunity": { + "label": "Damage Immunity: Tech", + "hint": "Immune to Tech Damage if this is set to 1" + }, + "magicalReduction": { + "label": "Damage Reduction: Tech", + "hint": "Tech Damage is reduced by the amount set here" + } + }, + "Rules": { + "damageReduction": { + "magical": { + "label": "Damage Reduction: Only Tech", + "hint": "Armor can only be used to reduce tech damage" + }, + "reduceSeverity": { + "magical": { + "label": "Reduce Damage Severity: Tech", + "hint": "Lowers any tech damage received by the set amount of severity degrees" + } + } + } + } + }, + "ITEMS": { + "Ikonis": { + "Label": "Ikonis", + "Bonded": "Bonded", + "BondedHint": "Gain a bonus to your damage rolls equal to your level.", + "Augments": "Augments", + "AugmentSlots": "Augment Slots", + "Trait": "Trait", + "Range": "Range", + "Damage": "Damage", + "Motherboard": "Ikonis" + } + } + } +} \ No newline at end of file diff --git a/module.json b/module.json new file mode 100644 index 0000000..4977959 --- /dev/null +++ b/module.json @@ -0,0 +1,46 @@ +{ + "id": "dh-ikonis", + "title": "Daggerheart - Ikonis", + "description": "Adds the Ikonis personalized weapon and Motherboard module. Renames Magic damage to Tech damage.", + "version": "1.0.0", + "url": "https://git.geeks.gay/cosmo/dh-ikonis", + "manifest": "https://git.geeks.gay/cosmo/dh-ikonis/raw/branch/main/module.json", + "download": "https://git.geeks.gay/cosmo/dh-ikonis/releases/download/1.0.0/dh-ikonis.zip", + "authors": [ + { + "name": "Cosmo", + "email": "cptncosmo@gmail.com", + "url": "https://git.geeks.gay/cosmo/", + "discord": "@cptn_cosmo", + "flags": {} + } + ], + "compatibility": { + "minimum": "14", + "verified": "14" + }, + "relationships": { + "systems": [ + { + "id": "daggerheart", + "type": "system", + "compatibility": { + "minimum": "1.0.0" + } + } + ] + }, + "esmodules": [ + "scripts/main.js" + ], + "styles": [ + "styles/ikonis.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ] +} \ No newline at end of file diff --git a/scripts/augments.js b/scripts/augments.js new file mode 100644 index 0000000..84cfd09 --- /dev/null +++ b/scripts/augments.js @@ -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: {} + } + ] + } +]; diff --git a/scripts/ikonis-data.js b/scripts/ikonis-data.js new file mode 100644 index 0000000..8a9c2a1 --- /dev/null +++ b/scripts/ikonis-data.js @@ -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 ? `

${aug.effect}

${aug.cost}

` : `

${aug.effect}

`, + 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 = `

${aug.effect}

`; + if (aug.cost) description += `

${aug.cost}

`; + if (aug.precompile) description += `

Precompile: ${aug.precompile}

`; + + 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>|/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 +} diff --git a/scripts/ikonis-sheet.js b/scripts/ikonis-sheet.js new file mode 100644 index 0000000..10ddbd8 --- /dev/null +++ b/scripts/ikonis-sheet.js @@ -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 = ` +
+
+ +
+
+ ${available.map(a => ` +
+ +
+ ${a.name} + ${a.effect} +
+ ${a.cost} + ${a.precompile ? `${a.precompile}` : ''} +
+
+
+ `).join('')} +
+
+ `; + + 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."); +} diff --git a/scripts/main.js b/scripts/main.js new file mode 100644 index 0000000..e625476 --- /dev/null +++ b/scripts/main.js @@ -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.`); +} diff --git a/styles/ikonis.css b/styles/ikonis.css new file mode 100644 index 0000000..e60d711 --- /dev/null +++ b/styles/ikonis.css @@ -0,0 +1,185 @@ + +/* --- Motherboard Specific Styling --- */ + +.motherboard-content { + padding: 1.5rem; + height: 100%; + overflow-y: auto; + background: #0d0d16; + color: #ffffff; +} + +.motherboard-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 2px solid #ff2e63; + padding-bottom: 1rem; + margin-bottom: 2rem; + position: sticky; + top: -1.5rem; + background: #0d0d16; + z-index: 5; +} + +.motherboard-header h2 { + margin: 0; + color: #ff2e63; + border: none; + font-size: 1.8rem; + text-transform: uppercase; +} + +.slots-info { + display: flex; + align-items: center; + gap: 0.75rem; + background: #ff2e63; + padding: 0.5rem 1.2rem; + border-radius: 20px; + box-shadow: 0 0 15px rgba(255, 46, 99, 0.4); +} + +.slot-override-btn { + background: rgba(0,0,0,0.3); + color: white; + border: none; + cursor: pointer; + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; +} + +.motherboard-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid #2d3436; + border-radius: 12px; + overflow: hidden; + margin-bottom: 1.5rem; + transition: border-color 0.2s; +} + +.motherboard-card:hover { + border-color: #ff2e63; +} + +.card-header { + background: rgba(255, 255, 255, 0.05); + padding: 0.75rem 1rem; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #2d3436; +} + +.card-header h3 { + margin: 0; + font-size: 1.1rem; + color: #ff2e63; + border: none; +} + +.card-body { + padding: 1rem; +} + +.aug-stats { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-bottom: 1rem; +} + +.aug-effect { + font-size: 1.05rem; + color: #e0e0e0; +} + +.aug-cost { + font-size: 0.85rem; + color: #888; + font-style: italic; +} + +/* --- Feature Slots --- */ + +.attached-feature { + border: 2px dashed #333; + border-radius: 8px; + min-height: 50px; + display: flex; + align-items: center; + justify-content: center; + margin-top: 0.5rem; + transition: all 0.2s; +} + +.attached-feature.empty:hover { + border-color: #ff2e63; + background: rgba(255, 46, 99, 0.05); +} + +.attached-feature.filled { + border-style: solid; + border-color: #00d2ff; + background: rgba(0, 210, 255, 0.05); + justify-content: flex-start; + padding: 0.5rem; +} + +.feature-content { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; +} + +.feature-content img { + width: 32px; + height: 32px; + border: 1px solid #00d2ff; + border-radius: 4px; +} + +.feature-content .name { + flex: 1; + font-size: 0.95rem; + font-weight: bold; +} + +.feature-content .remove { + color: #ff2e63; + cursor: pointer; + padding: 0.2rem 0.5rem; +} + +.drop-zone { + color: #444; + font-size: 0.9rem; + text-transform: uppercase; + pointer-events: none; +} + +/* --- Sections --- */ + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 2rem; + margin-bottom: 1rem; +} + +.add-augment-btn { + background: #ff2e63; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-weight: bold; +} diff --git a/templates/ikonis-item-manager.hbs b/templates/ikonis-item-manager.hbs new file mode 100644 index 0000000..04c102e --- /dev/null +++ b/templates/ikonis-item-manager.hbs @@ -0,0 +1,66 @@ + +
+ +
+

+ Bonded Feature Configuration +

+
+

Attach the core feature of this motherboard.

+
+ {{#if ikonis.bonded.feature}} +
+ + {{ikonis.bonded.feature.name}} + +
+ {{else}} + Drop Feature Item Here + {{/if}} +
+
+
+ +
+

+ Augment Slots ({{usedSlots}} / {{maxSlots}}) +

+ +
+ {{#each ikonis.slots as |slot|}} +
+
+ Slot {{slot.index}} + {{#if slot.aug}} + + {{/if}} +
+
+ {{#if slot.aug}} +
+
{{slot.aug.name}}
+
{{slot.aug.effect}}
+
Cost: {{slot.aug.cost}}
+
+
+ {{#if slot.aug.feature}} +
+ + {{slot.aug.feature.name}} + +
+ {{else}} + Drop Feature + {{/if}} +
+ {{else}} + + {{/if}} +
+
+ {{/each}} +
+
+
diff --git a/templates/ikonis-motherboard.hbs b/templates/ikonis-motherboard.hbs new file mode 100644 index 0000000..2d0b0d0 --- /dev/null +++ b/templates/ikonis-motherboard.hbs @@ -0,0 +1,49 @@ + +
+
+

{{localize "DAGGERHEART.ITEMS.Ikonis.Motherboard"}}

+
+ {{usedSlots}} / {{maxSlots}} {{localize "DAGGERHEART.ITEMS.Ikonis.AugmentSlots"}} +
+
+ + {{#if ikonis.bonded}} +
+
+
+

{{ikonis.bonded.fullName}} (Primary)

+

{{ikonis.bonded.effect}}

+
+
+
+ {{/if}} + +
+
+

Installed Augments

+ +
+ +
+ {{#each ikonis.augments as |aug|}} +
+ +
+ {{aug.name}} +
+
+
{{aug.effect}}
+
{{aug.cost}}
+ {{#if aug.precompile}}
{{aug.precompile}}
{{/if}} +
+
+ {{else}} +
+ No hardware modules detected. Click Install Tech to begin. +
+ {{/each}} +
+
+