feat: Implement Daggerheart Path Browser module to display and copy Active Effect data paths.
This commit is contained in:
commit
922814242d
7 changed files with 387 additions and 0 deletions
33
README.md
Normal file
33
README.md
Normal file
|
|
@ -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`)
|
||||||
39
module.json
Normal file
39
module.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
157
scripts/app.mjs
Normal file
157
scripts/app.mjs
Normal file
|
|
@ -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;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
scripts/main.mjs
Normal file
21
scripts/main.mjs
Normal file
|
|
@ -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 = `<i class="fa-solid fa-code"></i> ${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);
|
||||||
|
}
|
||||||
|
});
|
||||||
109
styles/styles.css
Normal file
109
styles/styles.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
8
templates/header.hbs
Normal file
8
templates/header.hbs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<header class="paths-header">
|
||||||
|
<div class="form-group">
|
||||||
|
<label><i class="fa-solid fa-search"></i> Search Paths</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input type="text" name="search" placeholder="Filter by name or path...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
20
templates/list.hbs
Normal file
20
templates/list.hbs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div class="paths-list">
|
||||||
|
{{#each groups}}
|
||||||
|
<div class="path-group">
|
||||||
|
<h3 class="group-header">{{group}}</h3>
|
||||||
|
<ul class="item-list">
|
||||||
|
{{#each paths}}
|
||||||
|
<li class="path-item" data-label="{{label}}" data-value="{{value}}" {{#if hint}}data-tooltip="{{hint}}"{{/if}}>
|
||||||
|
<div class="path-item-info">
|
||||||
|
<span class="path-label">{{label}}</span>
|
||||||
|
<code class="path-value">@system.{{value}}</code>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="copy-btn" data-action="copyPath" data-value="{{value}}" data-tooltip="Copy Data Path">
|
||||||
|
<i class="fa-solid fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue