commit 922814242df4fef67e0da47f6560ce1fcd01528a Author: cosmo Date: Thu Mar 19 21:49:16 2026 +0100 feat: Implement Daggerheart Path Browser module to display and copy Active Effect data paths. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1903b49 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Daggerheart Path Browser + +A Foundry VTT module that adds a browser to easily find and copy Active Effect data paths in the Daggerheart system. + +## Features +- Easily browse available data paths for Active Effects in Daggerheart. +- Search and filter functionality to quickly find what you need. +- Simple, distinct UI accessible directly from your game. + +## Requirements +- **Foundry VTT**: Version 13 or higher +- **System**: Daggerheart 1.9.4 or higher + +## Installation + +### Method 1: Foundry Native +1. Open up the Foundry VTT application and go to the **Add-on Modules** tab. +2. Click **Install Module**. +3. Search for "Daggerheart Path Browser". +4. Click **Install**. + +### Method 2: Manifest URL +1. Open up the Foundry VTT application and go to the **Add-on Modules** tab. +2. Click **Install Module**. +3. Paste the following link into the **Manifest URL** field at the bottom: + `https://git.geeks.gay/cosmo/dh-path-browser/raw/branch/main/module.json` +4. Click **Install**. + +## Usage +Once installed and enabled in your world, you can access the Daggerheart Path Browser to find and copy Active Effect data paths. + +## Author +- **Cosmo** (Discord: `cptn_cosmo`) diff --git a/module.json b/module.json new file mode 100644 index 0000000..36c4bc2 --- /dev/null +++ b/module.json @@ -0,0 +1,39 @@ +{ + "id": "dh-path-browser", + "title": "Daggerheart Path Browser", + "description": "Adds a browser to easily find and copy Active Effect data paths in Daggerheart.", + "url": "https://git.geeks.gay/cosmo/dh-path-browser", + "manifest": "https://git.geeks.gay/cosmo/dh-path-browser/raw/branch/main/module.json", + "download": "https://git.geeks.gay/cosmo/dh-path-browser/releases/download/1.0.0/dh-path-browser.zip", + "version": "1.0.0", + "compatibility": { + "minimum": "13", + "verified": "13" + }, + "authors": [ + { + "name": "Cosmo", + "email": "cptncosmo@gmail.com", + "url": "https://git.geeks.gay/cosmo", + "discord": "cptn_cosmo", + "flags": {} + } + ], + "esmodules": [ + "scripts/main.mjs" + ], + "styles": [ + "styles/styles.css" + ], + "relationships": { + "systems": [ + { + "id": "daggerheart", + "type": "system", + "compatibility": { + "minimum": "1.9.4" + } + } + ] + } +} \ No newline at end of file diff --git a/scripts/app.mjs b/scripts/app.mjs new file mode 100644 index 0000000..c08fba7 --- /dev/null +++ b/scripts/app.mjs @@ -0,0 +1,157 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export class DhPathBrowserApp extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(options) { + super(options); + this.paths = this.constructor.getChangeChoices(); + } + + static DEFAULT_OPTIONS = { + id: "dh-path-browser-app", + classes: ["daggerheart", "dh-path-browser", "dh-style"], + tag: "form", + window: { + title: "Data Path Browser", + icon: "fa-solid fa-code", + resizable: true, + contentClasses: ["standard-form"] + }, + position: { + width: 600, + height: 700 + }, + actions: { + copyPath: DhPathBrowserApp.#copyPath + } + }; + + static PARTS = { + header: { template: "modules/dh-path-browser/templates/header.hbs" }, + list: { + template: "modules/dh-path-browser/templates/list.hbs", + scrollable: [".paths-list"] + } + }; + + async _prepareContext(options) { + const context = await super._prepareContext(options); + + const groups = {}; + for (const p of this.paths) { + if (!groups[p.group]) groups[p.group] = []; + groups[p.group].push(p); + } + + context.groups = Object.entries(groups).map(([group, paths]) => ({ + group, + paths: paths.sort((a, b) => a.label.localeCompare(b.label)) + })).sort((a, b) => a.group.localeCompare(b.group)); + + return context; + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + if (partId === "header") { + const searchInput = htmlElement.querySelector('input[name="search"]'); + if (searchInput) { + searchInput.addEventListener("input", (e) => this.#filterPaths(e.target.value)); + } + } + } + + #filterPaths(query) { + query = query.toLowerCase(); + const listElement = this.element.querySelector(".paths-list"); + if (!listElement) return; + + const items = listElement.querySelectorAll(".path-item"); + items.forEach(item => { + const label = item.dataset.label.toLowerCase(); + const value = item.dataset.value.toLowerCase(); + + if (label.includes(query) || value.includes(query)) { + item.style.display = ""; + } else { + item.style.display = "none"; + } + }); + + const groups = listElement.querySelectorAll(".path-group"); + groups.forEach(group => { + const visibleItems = Array.from(group.querySelectorAll(".path-item")).filter(i => i.style.display !== "none"); + group.style.display = visibleItems.length > 0 ? "" : "none"; + }); + } + + static async #copyPath(event, target) { + const path = target.dataset.value; + if (path) { + const fullPath = `@system.${path}`; + await navigator.clipboard.writeText(fullPath); + ui.notifications.info(`Copied ${fullPath} to clipboard!`); + } + } + + static getChangeChoices() { + const ignoredActorKeys = ['config', 'DhEnvironment', 'DhParty']; + + const getAllLeaves = (root, group, parentPath = '') => { + const leaves = []; + const rootKey = `${parentPath ? `${parentPath}.` : ''}${root.name}`; + for (const field of Object.values(root.fields)) { + if (field instanceof foundry.data.fields.SchemaField) + leaves.push(...getAllLeaves(field, group, rootKey)); + else + leaves.push({ + value: `${rootKey}.${field.name}`, + label: game.i18n.localize(field.label), + hint: game.i18n.localize(field.hint), + group + }); + } + return leaves; + }; + + return Object.keys(game.system.api.models.actors).reduce((acc, key) => { + if (ignoredActorKeys.includes(key)) return acc; + + const model = game.system.api.models.actors[key]; + const group = game.i18n.localize(model.metadata.label); + const attributes = CONFIG.Token.documentClass.getTrackedAttributes(model.metadata.type); + + const getTranslations = path => { + if (path === 'resources.hope.max') + return { + label: game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.FIELDS.maxHope.label'), + hint: '' + }; + + const field = model.schema.getField(path); + return { + label: field ? game.i18n.localize(field.label) : path, + hint: field ? game.i18n.localize(field.hint) : '' + }; + }; + + const bars = attributes.bar.flatMap(x => { + const baseJoined = x.join('.'); + return [ + { value: `${baseJoined}.max`, ...getTranslations(`${baseJoined}.max`), group }, + { value: `${baseJoined}.value`, ...getTranslations(`${baseJoined}.value`), group } + ]; + }); + const values = attributes.value.flatMap(x => { + const joined = x.join('.'); + return { value: joined, ...getTranslations(joined), group }; + }); + + const bonuses = getAllLeaves(model.schema.fields.bonuses, group); + const rules = getAllLeaves(model.schema.fields.rules, group); + + acc.push(...bars, ...values, ...rules, ...bonuses); + return acc; + }, []); + } +} diff --git a/scripts/main.mjs b/scripts/main.mjs new file mode 100644 index 0000000..c1e706d --- /dev/null +++ b/scripts/main.mjs @@ -0,0 +1,21 @@ +import { DhPathBrowserApp } from "./app.mjs"; + +Hooks.on("init", () => { + console.log("Daggerheart Path Browser | Initializing module"); +}); + +Hooks.on("renderDaggerheartMenu", (app, html, data) => { + const button = document.createElement("button"); + button.type = "button"; + button.innerHTML = ` ${game.i18n.localize("Browse Data Paths")}`; + button.classList.add("dh-path-browser-btn"); + + button.addEventListener("click", () => { + new DhPathBrowserApp().render(true); + }); + + const container = html.querySelector("div"); + if (container) { + container.appendChild(button); + } +}); diff --git a/styles/styles.css b/styles/styles.css new file mode 100644 index 0000000..76a0977 --- /dev/null +++ b/styles/styles.css @@ -0,0 +1,109 @@ +.dh-path-browser-btn { + margin-top: 10px; + width: 100%; +} + +.dh-path-browser.application { + display: flex; + flex-direction: column; +} + +.dh-path-browser .window-content { + display: flex; + flex-direction: column; + padding: 10px; + gap: 10px; +} + +.dh-path-browser .paths-header { + flex: 0 0 auto; + padding-bottom: 10px; + border-bottom: 1px solid var(--color-border-light-1); +} + +.dh-path-browser .paths-list { + flex: 1 1 auto; + overflow-y: auto; + padding-right: 5px; +} + +.dh-path-browser .path-group { + margin-bottom: 15px; +} + +.dh-path-browser .path-group .group-header { + margin: 0 0 5px 0; + padding-bottom: 3px; + border-bottom: 1px solid var(--color-border-light-2); + font-size: 1.2em; + font-weight: bold; +} + +.dh-path-browser .path-group .item-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.dh-path-browser .path-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + border: 1px solid transparent; + border-radius: 4px; + background: var(--color-bg-option); +} + +.dh-path-browser .path-item:hover { + background: var(--color-bg-option-hover); + border-color: var(--color-border-light-2); +} + +.dh-path-browser .path-item-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.dh-path-browser .path-label { + font-weight: bold; +} + +.dh-path-browser .path-value { + font-size: 0.9em; + color: var(--color-text-dark-secondary); + background: rgba(0,0,0,0.05); + padding: 2px 4px; + border-radius: 3px; + user-select: all; +} + +.theme-dark .dh-path-browser .path-value, +.dh-style .dh-path-browser .path-value { + color: var(--color-text-light-secondary); + background: rgba(255,255,255,0.1); +} + +.dh-path-browser .path-item .copy-btn { + flex: 0 0 32px; + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid var(--color-border-light-1); + border-radius: 4px; + cursor: pointer; + line-height: 1; + color: inherit; +} + +.dh-path-browser .path-item .copy-btn:hover { + background: rgba(255, 255, 255, 0.1); + box-shadow: 0 0 5px var(--color-shadow-primary); +} diff --git a/templates/header.hbs b/templates/header.hbs new file mode 100644 index 0000000..3400029 --- /dev/null +++ b/templates/header.hbs @@ -0,0 +1,8 @@ +
+
+ +
+ +
+
+
diff --git a/templates/list.hbs b/templates/list.hbs new file mode 100644 index 0000000..a53203e --- /dev/null +++ b/templates/list.hbs @@ -0,0 +1,20 @@ +
+ {{#each groups}} +
+

{{group}}

+
    + {{#each paths}} +
  • +
    + {{label}} + @system.{{value}} +
    + +
  • + {{/each}} +
+
+ {{/each}} +