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