Initial commit
This commit is contained in:
commit
60b0072bcc
47 changed files with 6462 additions and 0 deletions
58
.github/draft-release.yaml
vendored
Normal file
58
.github/draft-release.yaml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
name: Create Draft Release
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
jobs:
|
||||||
|
everything:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Checkout the repository
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Install node and NPM
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
|
||||||
|
# Install required packages
|
||||||
|
- run: npm install
|
||||||
|
|
||||||
|
- name: Reading the system.json for the version
|
||||||
|
id: "version"
|
||||||
|
run: cat system.json | echo version=`jq -r ".version"` >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Check that tag doesn't exist
|
||||||
|
- uses: mukunku/tag-exists-action@v1.5.0
|
||||||
|
id: check-tag
|
||||||
|
with:
|
||||||
|
tag: "v${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: "Ensure that the tag doesn't exist"
|
||||||
|
if: ${{ steps.check-tag.outputs.exists == 'true' }}
|
||||||
|
run: exit 1
|
||||||
|
|
||||||
|
- name: Ensure there are specific files to release
|
||||||
|
if: ${{ vars.files_to_release == '' }}
|
||||||
|
run: exit 1
|
||||||
|
|
||||||
|
# Compile the stuff that needs to be compiled
|
||||||
|
- run: npm run build
|
||||||
|
- run: node scripts/buildCompendia.mjs
|
||||||
|
|
||||||
|
- name: Move system.json to a temp file
|
||||||
|
id: manifest-move
|
||||||
|
run: mv system.json system.temp.json
|
||||||
|
|
||||||
|
- name: Update the download property in the manifest
|
||||||
|
id: manifest-update
|
||||||
|
run: cat system.temp.json | jq -r --tab '.download = "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/${{ vars.zip_name }}.zip"' > system.json
|
||||||
|
|
||||||
|
- name: Create the zip
|
||||||
|
run: zip -r ${{ vars.zip_name || 'release' }}.zip ${{ vars.files_to_release }}
|
||||||
|
|
||||||
|
- name: Create the draft release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
tag: "v${{ steps.version.outputs.version }}"
|
||||||
|
commit: ${{ github.ref }}
|
||||||
|
draft: true
|
||||||
|
generateReleaseNotes: true
|
||||||
|
artifacts: "${{vars.zip_name || 'release'}}.zip,system.json"
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
||||||
27
.vscode/components.html-data.json
vendored
Normal file
27
.vscode/components.html-data.json
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"version": 1.1,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "dd-incrementer",
|
||||||
|
"description": "A number input that allows more flexible increase/decrease buttons",
|
||||||
|
"attributes": [
|
||||||
|
{ "name": "value", "description": "The initial value to put in the input" },
|
||||||
|
{ "name": "name", "description": "The form name to use when this input is used to submit data" },
|
||||||
|
{ "name": "min", "description": "The minimum value that this input can contain" },
|
||||||
|
{ "name": "max", "description": "The maximum value that this input can contain" },
|
||||||
|
{ "name": "smallStep", "description": "The value that the input is changed by when clicking a delta button or using the up/down arrow key" },
|
||||||
|
{ "name": "largeStep", "description": "The value that the input is changed by when clicking a delta button with control held or using the page up/ page down arrow key" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dd-icon",
|
||||||
|
"description": "Loads an icon asynchronously, caching the result for future uses",
|
||||||
|
"attributes": [
|
||||||
|
{ "name": "name", "description": "The name of the icon, this is relative to the assets folder of the dotdungeon system" },
|
||||||
|
{ "name": "path", "description": "The full path of the icon, this will only be used if `name` isn't provided or fails to fetch." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalAttributes": [],
|
||||||
|
"valueSets": []
|
||||||
|
}
|
||||||
37
.vscode/handlebars.code-snippets
vendored
Normal file
37
.vscode/handlebars.code-snippets
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
// Place your foundry.dungeon workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||||
|
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||||
|
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||||
|
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||||
|
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||||
|
// Placeholders with the same ids are connected.
|
||||||
|
// Example:
|
||||||
|
// "Print to console": {
|
||||||
|
// "scope": "javascript,typescript",
|
||||||
|
// "prefix": "log",
|
||||||
|
// "body": [
|
||||||
|
// "console.log('$1');",
|
||||||
|
// "$2"
|
||||||
|
// ],
|
||||||
|
// "description": "Log output to console"
|
||||||
|
// }
|
||||||
|
"Localization Shortcut (concat)": {
|
||||||
|
"scope": "handlebars,html",
|
||||||
|
"prefix": "i18n",
|
||||||
|
"body": ["localize (concat \"dotdungeon.$1\" $2)"]
|
||||||
|
},
|
||||||
|
"Localization Shortcut (no concat)": {
|
||||||
|
"scope": "handlebars,html",
|
||||||
|
"prefix": "i18n",
|
||||||
|
"body": ["localize \"dotdungeon.$1\""]
|
||||||
|
},
|
||||||
|
"Icon": {
|
||||||
|
"scope": "handlebars,html",
|
||||||
|
"prefix": "icon",
|
||||||
|
"body": [
|
||||||
|
"<div aria-hidden=\"true\" class=\"icon icon--${1:20}\">",
|
||||||
|
"\t{{{ $2 }}}",
|
||||||
|
"</div>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
18
.vscode/settings.json
vendored
Normal file
18
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"files.autoSave": "onWindowChange",
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"[yaml,yml]": {
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"git.branchProtection": [],
|
||||||
|
"files.exclude": {
|
||||||
|
"*.lock": true,
|
||||||
|
".styles": true,
|
||||||
|
"node_modules": true,
|
||||||
|
"packs": true,
|
||||||
|
},
|
||||||
|
"html.customData": [
|
||||||
|
"./.vscode/components.html-data.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
README.md
Normal file
1
README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# foundry-system-template
|
||||||
9
augments.d.ts
vendored
Normal file
9
augments.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
interface Actor {
|
||||||
|
/** The system-specific data */
|
||||||
|
system: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Item {
|
||||||
|
/** The system-specific data */
|
||||||
|
system: any;
|
||||||
|
};
|
||||||
88
eslint.config.mjs
Normal file
88
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import globals from "globals";
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
import stylistic from "@stylistic/eslint-plugin";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// Tell eslint to ignore files that I don't mind being formatted slightly differently
|
||||||
|
{ ignores: [ `scripts/` ] },
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
// MARK: Foundry Globals
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
CONFIG: `writable`,
|
||||||
|
game: `readonly`,
|
||||||
|
Handlebars: `readonly`,
|
||||||
|
Hooks: `readonly`,
|
||||||
|
ui: `readonly`,
|
||||||
|
Actor: `readonly`,
|
||||||
|
Actors: `readonly`,
|
||||||
|
Item: `readonly`,
|
||||||
|
Items: `readonly`,
|
||||||
|
ActorSheet: `readonly`,
|
||||||
|
ItemSheet: `readonly`,
|
||||||
|
foundry: `readonly`,
|
||||||
|
ChatMessage: `readonly`,
|
||||||
|
ActiveEffect: `readonly`,
|
||||||
|
Dialog: `readonly`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// MARK: Project Specific
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
"@stylistic": stylistic,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
Logger: `readonly`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"curly": `error`,
|
||||||
|
"func-names": [`warn`, `as-needed`],
|
||||||
|
"grouped-accessor-pairs": `error`,
|
||||||
|
"no-alert": `error`,
|
||||||
|
"no-implied-eval": `error`,
|
||||||
|
"no-invalid-this": `error`,
|
||||||
|
"no-lonely-if": `error`,
|
||||||
|
"no-unneeded-ternary": `error`,
|
||||||
|
"no-nested-ternary": `error`,
|
||||||
|
"no-var": `error`,
|
||||||
|
"no-unused-vars": [
|
||||||
|
`error`,
|
||||||
|
{
|
||||||
|
"vars": `local`,
|
||||||
|
"args": `after-used`,
|
||||||
|
"varsIgnorePattern": `^_`,
|
||||||
|
"argsIgnorePattern": `^_`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"sort-imports": [`warn`, { "ignoreCase": true, "allowSeparatedGroups": true }],
|
||||||
|
"@stylistic/semi": [`warn`, `always`, { "omitLastInOneLineBlock": true }],
|
||||||
|
"@stylistic/no-trailing-spaces": `warn`,
|
||||||
|
"@stylistic/space-before-blocks": [`warn`, `always`],
|
||||||
|
"@stylistic/space-infix-ops": `warn`,
|
||||||
|
"@stylistic/eol-last": `warn`,
|
||||||
|
"@stylistic/operator-linebreak": [`warn`, `before`],
|
||||||
|
"@stylistic/indent": [`warn`, `tab`],
|
||||||
|
"@stylistic/brace-style": [`warn`, `1tbs`, { "allowSingleLine": true }],
|
||||||
|
"@stylistic/quotes": [`warn`, `backtick`, { "avoidEscape": true }],
|
||||||
|
"@stylistic/comma-dangle": [`warn`, { arrays: `always-multiline`, objects: `always-multiline`, imports: `always-multiline`, exports: `always-multiline`, functions: `always-multiline` }],
|
||||||
|
"@stylistic/comma-style": [`warn`, `last`],
|
||||||
|
"@stylistic/dot-location": [`error`, `property`],
|
||||||
|
"@stylistic/no-confusing-arrow": `error`,
|
||||||
|
"@stylistic/no-whitespace-before-property": `error`,
|
||||||
|
"@stylistic/nonblock-statement-body-position": [
|
||||||
|
`error`,
|
||||||
|
`beside`,
|
||||||
|
{ "overrides": { "while": `below` } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
7
jsconfig.json
Normal file
7
jsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": [
|
||||||
|
"./augments.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
5058
package-lock.json
generated
Normal file
5058
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
17
package.json
Normal file
17
package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.8.0",
|
||||||
|
"@foundryvtt/foundryvtt-cli": "^1.0.3",
|
||||||
|
"@league-of-foundry-developers/foundry-vtt-types": "^9.280.0",
|
||||||
|
"@stylistic/eslint-plugin": "^2.6.1",
|
||||||
|
"eslint": "^9.8.0",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"sass": "^1.77.8"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"css": "sass --watch --embed-source-map --no-error-css styles/:.styles/",
|
||||||
|
"build": "sass --embed-source-map --no-error-css styles/:.styles/",
|
||||||
|
"lint": "eslint --fix",
|
||||||
|
"lint:nofix": "eslint"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
scripts/buildCompendia.mjs
Normal file
32
scripts/buildCompendia.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { compilePack } from "@foundryvtt/foundryvtt-cli";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const system = JSON.parse(await readFile(`./system.json`, `utf-8`));
|
||||||
|
|
||||||
|
if (!system.packs || system.packs.length === 0) {
|
||||||
|
console.log(`No compendium packs defined`);
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const compendium of system.packs) {
|
||||||
|
console.debug(`Packing ${compendium.label} (${compendium.name})`);
|
||||||
|
let src = join(process.cwd(), compendium.path, `_source`);
|
||||||
|
if (!existsSync(src)) {
|
||||||
|
console.warn(`${compendium.path} doesn't exist, skipping.`)
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
await compilePack(
|
||||||
|
src,
|
||||||
|
join(process.cwd(), compendium.path),
|
||||||
|
{ recursive: true },
|
||||||
|
);
|
||||||
|
console.debug(`Finished packing ${compendium.name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Finished packing compendia`)
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
27
scripts/extractCompendia.mjs
Normal file
27
scripts/extractCompendia.mjs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import { extractPack } from "@foundryvtt/foundryvtt-cli";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const system = JSON.parse(await readFile(`./system.json`, `utf-8`));
|
||||||
|
|
||||||
|
if (!system.packs || system.packs.length === 0) {
|
||||||
|
console.log(`No compendium packs defined`);
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const compendium of system.packs) {
|
||||||
|
console.debug(`Unpacking ${compendium.label} (${compendium.name})`);
|
||||||
|
let src = join(process.cwd(), compendium.path, `_source`);
|
||||||
|
await extractPack(
|
||||||
|
join(process.cwd(), compendium.path),
|
||||||
|
src,
|
||||||
|
{ recursive: true },
|
||||||
|
);
|
||||||
|
console.debug(`Finished packing ${compendium.name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Finished unpacking compendia`);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
4
scripts/macros/deleteInvalidActors.mjs
Normal file
4
scripts/macros/deleteInvalidActors.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
const invalids = game.actors.invalidDocumentIds;
|
||||||
|
invalids.forEach(id => {
|
||||||
|
game.actors.getInvalid(id).delete();
|
||||||
|
});
|
||||||
4
scripts/macros/deleteInvalidItems.mjs
Normal file
4
scripts/macros/deleteInvalidItems.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
const invalids = game.items.invalidDocumentIds;
|
||||||
|
invalids.forEach(id => {
|
||||||
|
game.items.getInvalid(id).delete();
|
||||||
|
});
|
||||||
32
src/components/_index.mjs
Normal file
32
src/components/_index.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { SystemIcon } from "./icon.mjs";
|
||||||
|
import { SystemIncrementer } from "./incrementer.mjs";
|
||||||
|
import { SystemRange } from "./range.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of element classes to register, expects all of them to have a static
|
||||||
|
* property of "elementName" that is the namespaced name that the component will
|
||||||
|
* be registered under. Any elements that are formAssociated have their name added
|
||||||
|
* to the "CONFIG.CACHE.componentListeners" array and should be listened to for
|
||||||
|
* "change" events in sheets.
|
||||||
|
*/
|
||||||
|
const components = [
|
||||||
|
SystemIcon,
|
||||||
|
SystemIncrementer,
|
||||||
|
SystemRange,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function registerCustomComponents() {
|
||||||
|
(CONFIG.CACHE ??= {}).componentListeners ??= [];
|
||||||
|
for (const component of components) {
|
||||||
|
if (!window.customElements.get(component.elementName)) {
|
||||||
|
console.debug(`${game.system.id} | Registering component "${component.elementName}"`);
|
||||||
|
window.customElements.define(
|
||||||
|
component.elementName,
|
||||||
|
component,
|
||||||
|
);
|
||||||
|
if (component.formAssociated) {
|
||||||
|
CONFIG.CACHE.componentListeners.push(component.elementName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
125
src/components/icon.mjs
Normal file
125
src/components/icon.mjs
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
Attributes:
|
||||||
|
@property {string} name - The name of the icon, takes precedence over the path
|
||||||
|
@property {string} path - The path of the icon file
|
||||||
|
*/
|
||||||
|
export class SystemIcon extends StyledShadowElement(HTMLElement) {
|
||||||
|
static elementName = `dd-icon`;
|
||||||
|
static formAssociated = false;
|
||||||
|
|
||||||
|
/* Stuff for the mixin to use */
|
||||||
|
static _stylePath = ``;
|
||||||
|
|
||||||
|
|
||||||
|
static _cache = new Map();
|
||||||
|
#container;
|
||||||
|
/** @type {null | string} */
|
||||||
|
_name;
|
||||||
|
/** @type {null | string} */
|
||||||
|
_path;
|
||||||
|
|
||||||
|
/* Stored IDs for all of the hooks that are in this component */
|
||||||
|
#svgHmr;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
// this._shadow = this.attachShadow({ mode: `open`, delegatesFocus: true });
|
||||||
|
|
||||||
|
this.#container = document.createElement(`div`);
|
||||||
|
this._shadow.appendChild(this.#container);
|
||||||
|
};
|
||||||
|
|
||||||
|
_mounted = false;
|
||||||
|
async connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
if (this._mounted) { return }
|
||||||
|
|
||||||
|
this._name = this.getAttribute(`name`);
|
||||||
|
this._path = this.getAttribute(`path`);
|
||||||
|
|
||||||
|
/*
|
||||||
|
This converts all of the double-dash prefixed properties on the element to
|
||||||
|
CSS variables so that they don't all need to be provided by doing style=""
|
||||||
|
*/
|
||||||
|
for (const attrVar of this.attributes) {
|
||||||
|
if (attrVar.name?.startsWith(`var:`)) {
|
||||||
|
const prop = attrVar.name.replace(`var:`, ``);
|
||||||
|
this.style.setProperty(`--` + prop, attrVar.value);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Try to retrieve the icon if it isn't present, try the path then default to
|
||||||
|
the slot content, as then we can have a default per-icon usage
|
||||||
|
*/
|
||||||
|
let content;
|
||||||
|
if (this._name) {
|
||||||
|
content = await this.#getIcon(`./systems/dotdungeon/assets/${this._name}.svg`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._path && !content) {
|
||||||
|
content = await this.#getIcon(this._path);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
this.#container.appendChild(content.cloneNode(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is so that when we get an HMR event from Foundry we can appropriately
|
||||||
|
handle it using our logic to update the component and the icon cache.
|
||||||
|
*/
|
||||||
|
if (game.settings.get(game.system.id, `devMode`)) {
|
||||||
|
this.#svgHmr = Hooks.on(`${game.system.id}-hmr:svg`, (iconName, data) => {
|
||||||
|
if (this._name === iconName || this._path?.endsWith(data.path)) {
|
||||||
|
const svg = this.#parseSVG(data.content);
|
||||||
|
this.constructor._cache.set(iconName, svg);
|
||||||
|
this.#container.replaceChildren(svg.cloneNode(true));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this._mounted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (!this._mounted) { return }
|
||||||
|
|
||||||
|
Hooks.off(`${game.system.id}-hmr:svg`, this.#svgHmr);
|
||||||
|
|
||||||
|
this._mounted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
async #getIcon(path) {
|
||||||
|
// Cache hit!
|
||||||
|
if (this.constructor._cache.has(path)) {
|
||||||
|
Logger.debug(`Icon ${path} cache hit`);
|
||||||
|
return this.constructor._cache.get(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const r = await fetch(path);
|
||||||
|
switch (r.status) {
|
||||||
|
case 200:
|
||||||
|
case 201:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Logger.error(`Failed to fetch icon: ${path}`);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.debug(`Adding icon ${path} to the cache`);
|
||||||
|
const svg = this.#parseSVG(await r.text());
|
||||||
|
this.constructor._cache.set(path, svg);
|
||||||
|
return svg;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Takes an SVG string and returns it as a DOM node */
|
||||||
|
#parseSVG(content) {
|
||||||
|
const temp = document.createElement(`div`);
|
||||||
|
temp.innerHTML = content;
|
||||||
|
return temp.querySelector(`svg`);
|
||||||
|
};
|
||||||
|
};
|
||||||
153
src/components/incrementer.mjs
Normal file
153
src/components/incrementer.mjs
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
||||||
|
import { SystemIcon } from "./icon.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
Attributes:
|
||||||
|
@property {string} name - The path to the value to update
|
||||||
|
@property {number} value - The actual value of the input
|
||||||
|
@property {number} min - The minimum value of the input
|
||||||
|
@property {number} max - The maximum value of the input
|
||||||
|
@property {number?} smallStep - The step size used for the buttons and arrow keys
|
||||||
|
@property {number?} largeStep - The step size used for the buttons + Ctrl and page up / down
|
||||||
|
|
||||||
|
Styling:
|
||||||
|
- `--height`: Controls the height of the element + the width of the buttons (default: 1.25rem)
|
||||||
|
- `--width`: Controls the width of the number input (default 50px)
|
||||||
|
*/
|
||||||
|
export class SystemIncrementer extends StyledShadowElement(HTMLElement) {
|
||||||
|
static elementName = `dd-incrementer`;
|
||||||
|
static formAssociated = true;
|
||||||
|
|
||||||
|
static _stylePath = `v1/components/incrementer.scss`;
|
||||||
|
|
||||||
|
_internals;
|
||||||
|
#input;
|
||||||
|
|
||||||
|
_min;
|
||||||
|
_max;
|
||||||
|
_smallStep;
|
||||||
|
_largeStep;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Form internals
|
||||||
|
this._internals = this.attachInternals();
|
||||||
|
this._internals.role = `spinbutton`;
|
||||||
|
};
|
||||||
|
|
||||||
|
get form() {
|
||||||
|
return this._internals.form;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.getAttribute(`name`);
|
||||||
|
}
|
||||||
|
set name(value) {
|
||||||
|
this.setAttribute(`name`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.getAttribute(`value`);
|
||||||
|
};
|
||||||
|
set value(value) {
|
||||||
|
this.setAttribute(`value`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return `number`;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.replaceChildren();
|
||||||
|
|
||||||
|
// Attribute parsing / registration
|
||||||
|
const value = this.getAttribute(`value`);
|
||||||
|
this._min = parseInt(this.getAttribute(`min`) ?? 0);
|
||||||
|
this._max = parseInt(this.getAttribute(`max`) ?? 0);
|
||||||
|
this._smallStep = parseInt(this.getAttribute(`smallStep`) ?? 1);
|
||||||
|
this._largeStep = parseInt(this.getAttribute(`largeStep`) ?? 5);
|
||||||
|
|
||||||
|
this._internals.ariaValueMin = this._min;
|
||||||
|
this._internals.ariaValueMax = this._max;
|
||||||
|
|
||||||
|
const container = document.createElement(`div`);
|
||||||
|
|
||||||
|
// The input that the user can see / modify
|
||||||
|
const input = document.createElement(`input`);
|
||||||
|
this.#input = input;
|
||||||
|
input.type = `number`;
|
||||||
|
input.ariaHidden = true;
|
||||||
|
input.min = this.getAttribute(`min`);
|
||||||
|
input.max = this.getAttribute(`max`);
|
||||||
|
input.addEventListener(`change`, this.#updateValue.bind(this));
|
||||||
|
input.value = value;
|
||||||
|
|
||||||
|
// plus button
|
||||||
|
const increment = document.createElement(SystemIcon.elementName);
|
||||||
|
increment.setAttribute(`name`, `ui/plus`);
|
||||||
|
increment.setAttribute(`var:size`, `0.75rem`);
|
||||||
|
increment.setAttribute(`var:fill`, `currentColor`);
|
||||||
|
increment.ariaHidden = true;
|
||||||
|
increment.classList.value = `increment`;
|
||||||
|
increment.addEventListener(`mousedown`, this.#increment.bind(this));
|
||||||
|
|
||||||
|
// minus button
|
||||||
|
const decrement = document.createElement(SystemIcon.elementName);
|
||||||
|
decrement.setAttribute(`name`, `ui/minus`);
|
||||||
|
decrement.setAttribute(`var:size`, `0.75rem`);
|
||||||
|
decrement.setAttribute(`var:fill`, `currentColor`);
|
||||||
|
decrement.ariaHidden = true;
|
||||||
|
decrement.classList.value = `decrement`;
|
||||||
|
decrement.addEventListener(`mousedown`, this.#decrement.bind(this));
|
||||||
|
|
||||||
|
// Construct the DOM
|
||||||
|
container.appendChild(decrement);
|
||||||
|
container.appendChild(input);
|
||||||
|
container.appendChild(increment);
|
||||||
|
this._shadow.appendChild(container);
|
||||||
|
|
||||||
|
/*
|
||||||
|
This converts all of the namespace prefixed properties on the element to
|
||||||
|
CSS variables so that they don't all need to be provided by doing style=""
|
||||||
|
*/
|
||||||
|
for (const attrVar of this.attributes) {
|
||||||
|
if (attrVar.name?.startsWith(`var:`)) {
|
||||||
|
const prop = attrVar.name.replace(`var:`, ``);
|
||||||
|
this.style.setProperty(`--` + prop, attrVar.value);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#updateValue() {
|
||||||
|
let value = parseInt(this.#input.value);
|
||||||
|
if (this.getAttribute(`min`)) {
|
||||||
|
value = Math.max(this._min, value);
|
||||||
|
}
|
||||||
|
if (this.getAttribute(`max`)) {
|
||||||
|
value = Math.min(this._max, value);
|
||||||
|
}
|
||||||
|
this.#input.value = value;
|
||||||
|
this.value = value;
|
||||||
|
this.dispatchEvent(new Event(`change`, { bubbles: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @param {Event} $e */
|
||||||
|
#increment($e) {
|
||||||
|
$e.preventDefault();
|
||||||
|
let value = parseInt(this.#input.value);
|
||||||
|
value += $e.ctrlKey ? this._largeStep : this._smallStep;
|
||||||
|
this.#input.value = value;
|
||||||
|
this.#updateValue();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @param {Event} $e */
|
||||||
|
#decrement($e) {
|
||||||
|
$e.preventDefault();
|
||||||
|
let value = parseInt(this.#input.value);
|
||||||
|
value -= $e.ctrlKey ? this._largeStep : this._smallStep;
|
||||||
|
this.#input.value = value;
|
||||||
|
this.#updateValue();
|
||||||
|
};
|
||||||
|
};
|
||||||
80
src/components/mixins/Styles.mjs
Normal file
80
src/components/mixins/Styles.mjs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} Base
|
||||||
|
*/
|
||||||
|
export function StyledShadowElement(Base) {
|
||||||
|
return class extends Base {
|
||||||
|
/**
|
||||||
|
* The path to the CSS that is loaded
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
static _stylePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stringified CSS to use
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
static _styles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HTML element of the stylesheet
|
||||||
|
* @type {HTMLStyleElement}
|
||||||
|
*/
|
||||||
|
_style;
|
||||||
|
|
||||||
|
/** @type {ShadowRoot} */
|
||||||
|
_shadow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hook ID for this element's CSS hot reload
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
#cssHmr;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._shadow = this.attachShadow({ mode: `open` });
|
||||||
|
this._style = document.createElement(`style`);
|
||||||
|
this._shadow.appendChild(this._style);
|
||||||
|
};
|
||||||
|
|
||||||
|
#mounted = false;
|
||||||
|
connectedCallback() {
|
||||||
|
if (this.#mounted) { return }
|
||||||
|
|
||||||
|
this._getStyles();
|
||||||
|
|
||||||
|
if (game.settings.get(`dotdungeon`, `devMode`)) {
|
||||||
|
this.#cssHmr = Hooks.on(`dd-hmr:css`, (data) => {
|
||||||
|
if (data.path.endsWith(this.constructor._stylePath)) {
|
||||||
|
this._style.innerHTML = data.content;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#mounted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (!this.#mounted) { return }
|
||||||
|
if (this.#cssHmr != null) {
|
||||||
|
Hooks.off(`dd-hmr:css`, this.#cssHmr);
|
||||||
|
this.#cssHmr = null;
|
||||||
|
};
|
||||||
|
this.#mounted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
_getStyles() {
|
||||||
|
if (this.constructor._styles) {
|
||||||
|
this._style.innerHTML = this.constructor._styles;
|
||||||
|
} else {
|
||||||
|
fetch(`./systems/dotdungeon/.styles/${this.constructor._stylePath}`)
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(t => {
|
||||||
|
this.constructor._styles = t;
|
||||||
|
this._style.innerHTML = t;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
138
src/components/range.mjs
Normal file
138
src/components/range.mjs
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
Attributes:
|
||||||
|
@property {string} name - The path to the value to update in the datamodel
|
||||||
|
@property {number} value - The actual value of the input
|
||||||
|
@property {number} max - The maximum value that this range has
|
||||||
|
|
||||||
|
@extends {HTMLElement}
|
||||||
|
*/
|
||||||
|
export class SystemRange
|
||||||
|
extends StyledShadowElement(
|
||||||
|
HTMLElement,
|
||||||
|
{ mode: `open`, delegatesFocus: true },
|
||||||
|
) {
|
||||||
|
static elementName = `dd-range`;
|
||||||
|
static formAssociated = true;
|
||||||
|
|
||||||
|
static observedAttributes = [`max`];
|
||||||
|
|
||||||
|
static _stylePath = `v3/components/range.css`;
|
||||||
|
|
||||||
|
_internals;
|
||||||
|
#input;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Form internals
|
||||||
|
this._internals = this.attachInternals();
|
||||||
|
this._internals.role = `spinbutton`;
|
||||||
|
};
|
||||||
|
|
||||||
|
get form() {
|
||||||
|
return this._internals.form;
|
||||||
|
};
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.getAttribute(`name`);
|
||||||
|
};
|
||||||
|
set name(value) {
|
||||||
|
this.setAttribute(`name`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
try {
|
||||||
|
return parseInt(this.getAttribute(`value`));
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Failed to parse attribute: "value" - Make sure it's an integer`);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
set value(value) {
|
||||||
|
this.setAttribute(`value`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
get max() {
|
||||||
|
try {
|
||||||
|
return parseInt(this.getAttribute(`max`));
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Failed to parse attribute: "max" - Make sure it's an integer`);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
set max(value) {
|
||||||
|
this.setAttribute(`max`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return `number`;
|
||||||
|
};
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
// Attribute validation
|
||||||
|
if (!this.hasAttribute(`max`)) {
|
||||||
|
throw new Error(`dotdungeon | Cannot have a range without a maximum value`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard accessible input for the thing
|
||||||
|
this.#input = document.createElement(`input`);
|
||||||
|
this.#input.type = `number`;
|
||||||
|
this.#input.min = 0;
|
||||||
|
this.#input.max = this.max;
|
||||||
|
this.#input.value = this.value;
|
||||||
|
this.#input.addEventListener(`change`, () => {
|
||||||
|
const inputValue = parseInt(this.#input.value);
|
||||||
|
if (inputValue === this.value) { return };
|
||||||
|
this._updateValue.bind(this)(Math.sign(this.value - inputValue));
|
||||||
|
this._updateValue(Math.sign(this.value - inputValue));
|
||||||
|
});
|
||||||
|
this._shadow.appendChild(this.#input);
|
||||||
|
|
||||||
|
// Shadow-DOM construction
|
||||||
|
this._elements = new Array(this.max);
|
||||||
|
const container = document.createElement(`div`);
|
||||||
|
container.classList.add(`container`);
|
||||||
|
|
||||||
|
// Creating the node for filled content
|
||||||
|
const filledContainer = document.createElement(`div`);
|
||||||
|
filledContainer.classList.add(`range-increment`, `filled`);
|
||||||
|
const filledNode = this.querySelector(`[slot="filled"]`);
|
||||||
|
if (filledNode) { filledContainer.appendChild(filledNode) };
|
||||||
|
|
||||||
|
const emptyContainer = document.createElement(`div`);
|
||||||
|
emptyContainer.classList.add(`range-increment`, `empty`);
|
||||||
|
const emptyNode = this.querySelector(`[slot="empty"]`);
|
||||||
|
if (emptyNode) { emptyContainer.appendChild(emptyNode) };
|
||||||
|
|
||||||
|
this._elements.fill(filledContainer, 0, this.value);
|
||||||
|
this._elements.fill(emptyContainer, this.value);
|
||||||
|
container.append(...this._elements.map((slot, i) => {
|
||||||
|
const node = slot.cloneNode(true);
|
||||||
|
node.setAttribute(`data-index`, i + 1);
|
||||||
|
node.addEventListener(`click`, () => {
|
||||||
|
const filled = node.classList.contains(`filled`);
|
||||||
|
this._updateValue(filled ? -1 : 1);
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}));
|
||||||
|
this._shadow.appendChild(container);
|
||||||
|
|
||||||
|
/*
|
||||||
|
This converts all of the namespace prefixed properties on the element to
|
||||||
|
CSS variables so that they don't all need to be provided by doing style=""
|
||||||
|
*/
|
||||||
|
for (const attrVar of this.attributes) {
|
||||||
|
if (attrVar.name?.startsWith(`var:`)) {
|
||||||
|
const prop = attrVar.name.replace(`var:`, ``);
|
||||||
|
this.style.setProperty(`--` + prop, attrVar.value);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
_updateValue(delta) {
|
||||||
|
this.value += delta;
|
||||||
|
this.dispatchEvent(new Event(`change`, { bubbles: true }));
|
||||||
|
};
|
||||||
|
};
|
||||||
11
src/documents/ActiveEffect/_proxy.mjs
Normal file
11
src/documents/ActiveEffect/_proxy.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of Foundry-types to in-code Document classes.
|
||||||
|
*/
|
||||||
|
const classes = {};
|
||||||
|
|
||||||
|
/** The class that will be used if no type-specific class is defined */
|
||||||
|
const defaultClass = ActiveEffect;
|
||||||
|
|
||||||
|
export const ActiveEffectProxy = createDocumentProxy(defaultClass, classes);
|
||||||
5
src/documents/Actor/Player/Document.mjs
Normal file
5
src/documents/Actor/Player/Document.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export class Player extends Actor {
|
||||||
|
getRollData() {
|
||||||
|
return this.system;
|
||||||
|
};
|
||||||
|
};
|
||||||
6
src/documents/Actor/Player/Model.mjs
Normal file
6
src/documents/Actor/Player/Model.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export class PlayerData extends foundry.abstract.TypeDataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
};
|
||||||
11
src/documents/Actor/_proxy.mjs
Normal file
11
src/documents/Actor/_proxy.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of Foundry-types to in-code Document classes.
|
||||||
|
*/
|
||||||
|
const classes = {};
|
||||||
|
|
||||||
|
/** The class that will be used if no type-specific class is defined */
|
||||||
|
const defaultClass = Actor;
|
||||||
|
|
||||||
|
export const ActorProxy = createDocumentProxy(defaultClass, classes);
|
||||||
11
src/documents/ChatMessage/_proxy.mjs
Normal file
11
src/documents/ChatMessage/_proxy.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of Foundry-types to in-code Document classes.
|
||||||
|
*/
|
||||||
|
const classes = {};
|
||||||
|
|
||||||
|
/** The class that will be used if no type-specific class is defined */
|
||||||
|
const defaultClass = ChatMessage;
|
||||||
|
|
||||||
|
export const ChatMessageProxy = createDocumentProxy(defaultClass, classes);
|
||||||
11
src/documents/Item/_proxy.mjs
Normal file
11
src/documents/Item/_proxy.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of Foundry-types to in-code Document classes.
|
||||||
|
*/
|
||||||
|
const classes = {};
|
||||||
|
|
||||||
|
/** The class that will be used if no type-specific class is defined */
|
||||||
|
const defaultClass = Item;
|
||||||
|
|
||||||
|
export const ItemProxy = createDocumentProxy(defaultClass, classes);
|
||||||
18
src/helpers/_index.mjs
Normal file
18
src/helpers/_index.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { handlebarsLocalizer, localizer } from "../utils/localizer.mjs";
|
||||||
|
import { options } from "./options.mjs";
|
||||||
|
|
||||||
|
export function registerHandlebarsHelpers() {
|
||||||
|
const helperPrefix = game.system.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// MARK: Complex helpers
|
||||||
|
[`${helperPrefix}-i18n`]: handlebarsLocalizer,
|
||||||
|
[`${helperPrefix}-options`]: options,
|
||||||
|
|
||||||
|
// MARK: Simple helpers
|
||||||
|
[`${helperPrefix}-stringify`]: v => JSON.stringify(v, null, ` `),
|
||||||
|
[`${helperPrefix}-empty`]: v => v.length == 0,
|
||||||
|
[`${helperPrefix}-set-has`]: (s, k) => s.has(k),
|
||||||
|
[`${helperPrefix}-empty-state`]: (v) => v ?? localizer(`${game.system.id}.common.empty`),
|
||||||
|
};
|
||||||
|
};
|
||||||
35
src/helpers/options.mjs
Normal file
35
src/helpers/options.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { localizer } from "../utils/localizer.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} Option
|
||||||
|
* @property {string} [label]
|
||||||
|
* @property {string|number} value
|
||||||
|
* @property {boolean} [disabled]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} selected
|
||||||
|
* @param {Array<Option | string>} opts
|
||||||
|
*/
|
||||||
|
export function options(selected, opts, meta) {
|
||||||
|
const { localize = false } = meta.hash;
|
||||||
|
selected = Handlebars.escapeExpression(selected);
|
||||||
|
const htmlOptions = [];
|
||||||
|
|
||||||
|
for (let opt of opts) {
|
||||||
|
if (foundry.utils.getType(opt) === `string`) {
|
||||||
|
opt = { label: opt, value: opt };
|
||||||
|
};
|
||||||
|
opt.value = Handlebars.escapeExpression(opt.value);
|
||||||
|
htmlOptions.push(
|
||||||
|
`<option
|
||||||
|
value="${opt.value}"
|
||||||
|
${selected === opt.value ? `selected` : ``}
|
||||||
|
${opt.disabled ? `disabled` : ``}
|
||||||
|
>
|
||||||
|
${localize ? localizer(opt.label) : opt.label}
|
||||||
|
</option>`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return htmlOptions.join(`\n`);
|
||||||
|
};
|
||||||
18
src/hooks/hotReload.mjs
Normal file
18
src/hooks/hotReload.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
const loaders = {
|
||||||
|
svg(data) {
|
||||||
|
const iconName = data.path.split(`/`).slice(-1)[0].slice(0, -4);
|
||||||
|
Logger.debug(`hot-reloading icon: ${iconName}`);
|
||||||
|
Hooks.call(`${game.system.id}-hmr:svg`, iconName, data);
|
||||||
|
},
|
||||||
|
js() {window.location.reload()},
|
||||||
|
mjs() {window.location.reload()},
|
||||||
|
css(data) {
|
||||||
|
Logger.debug(`Hot-reloading CSS: ${data.path}`);
|
||||||
|
Hooks.call(`${game.system.id}-hmr:css`, data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Hooks.on(`hotReload`, async (data) => {
|
||||||
|
if (!loaders[data.extension]) {return}
|
||||||
|
return loaders[data.extension](data);
|
||||||
|
});
|
||||||
48
src/main.mjs
Normal file
48
src/main.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Document Imports
|
||||||
|
import { ActiveEffectProxy } from "./documents/ActiveEffect/_proxy.mjs";
|
||||||
|
import { ActorProxy } from "./documents/Actor/_proxy.mjs";
|
||||||
|
import { ChatMessageProxy } from "./documents/ChatMessage/_proxy.mjs";
|
||||||
|
import { ItemProxy } from "./documents/Item/_proxy.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
// Misc Imports
|
||||||
|
import "./utils/logger.mjs";
|
||||||
|
import { registerCustomComponents } from "./components/_index.mjs";
|
||||||
|
import { registerHandlebarsHelpers } from "./helpers/_index.mjs";
|
||||||
|
import { registerSettings } from "./settings/_index.mjs";
|
||||||
|
import { registerSheets } from "./sheets/_index.mjs";
|
||||||
|
|
||||||
|
// MARK: init hook
|
||||||
|
Hooks.once(`init`, () => {
|
||||||
|
Logger.info(`Initializing`);
|
||||||
|
CONFIG.ActiveEffect.legacyTransferral = false;
|
||||||
|
|
||||||
|
registerSettings();
|
||||||
|
|
||||||
|
// Update document classes
|
||||||
|
CONFIG.Actor.documentClass = ActorProxy;
|
||||||
|
CONFIG.Item.documentClass = ItemProxy;
|
||||||
|
CONFIG.ActiveEffect.documentClass = ActiveEffectProxy;
|
||||||
|
CONFIG.ChatMessage.documentClass = ChatMessageProxy;
|
||||||
|
registerSheets();
|
||||||
|
|
||||||
|
registerHandlebarsHelpers();
|
||||||
|
|
||||||
|
registerCustomComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: ready hook
|
||||||
|
Hooks.once( `ready`, () => {
|
||||||
|
Logger.info(`Ready`);
|
||||||
|
|
||||||
|
let defaultTab = game.settings.get(game.system.id, `defaultTab`);
|
||||||
|
if (defaultTab) {
|
||||||
|
if (!ui.sidebar?.tabs?.[defaultTab]) {
|
||||||
|
Logger.error(`Couldn't find a sidebar tab with ID:`, defaultTab);
|
||||||
|
} else {
|
||||||
|
Logger.debug(`Switching sidebar tab to:`, defaultTab);
|
||||||
|
ui.sidebar.tabs[defaultTab].activate();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
10
src/settings/_index.mjs
Normal file
10
src/settings/_index.mjs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { registerClientSettings } from "./client_settings.mjs";
|
||||||
|
import { registerDevSettings } from "./dev_settings.mjs";
|
||||||
|
import { registerWorldSettings } from "./world_settings.mjs";
|
||||||
|
|
||||||
|
export function registerSettings() {
|
||||||
|
Logger.debug(`Registering settings`);
|
||||||
|
registerClientSettings();
|
||||||
|
registerWorldSettings();
|
||||||
|
registerDevSettings();
|
||||||
|
};
|
||||||
2
src/settings/client_settings.mjs
Normal file
2
src/settings/client_settings.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export function registerClientSettings() {
|
||||||
|
};
|
||||||
16
src/settings/dev_settings.mjs
Normal file
16
src/settings/dev_settings.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export function registerDevSettings() {
|
||||||
|
game.settings.register(game.system.id, `devMode`, {
|
||||||
|
scope: `client`,
|
||||||
|
type: Boolean,
|
||||||
|
config: false,
|
||||||
|
default: false,
|
||||||
|
requiresReload: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.register(game.system.id, `defaultTab`, {
|
||||||
|
scope: `client`,
|
||||||
|
type: String,
|
||||||
|
config: false,
|
||||||
|
requiresReload: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
1
src/settings/world_settings.mjs
Normal file
1
src/settings/world_settings.mjs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export function registerWorldSettings() {};
|
||||||
12
src/sheets/Player/v1.mjs
Normal file
12
src/sheets/Player/v1.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export class PlayerSheetv1 extends ActorSheet {
|
||||||
|
static get defaultOptions() {
|
||||||
|
let opts = foundry.utils.mergeObject(
|
||||||
|
super.defaultOptions,
|
||||||
|
{
|
||||||
|
template: `systems/${game.system.id}/templates/Player/v1/main.hbs`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
opts.classes.push(`style-v1`);
|
||||||
|
return opts;
|
||||||
|
};
|
||||||
|
}
|
||||||
11
src/sheets/_index.mjs
Normal file
11
src/sheets/_index.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { PlayerSheetv1 } from "./Player/v1.mjs";
|
||||||
|
|
||||||
|
export function registerSheets() {
|
||||||
|
Logger.debug(`Registering sheets`);
|
||||||
|
|
||||||
|
Actors.registerSheet(game.system.id, PlayerSheetv1, {
|
||||||
|
makeDefault: true,
|
||||||
|
types: [`player`],
|
||||||
|
label: `Hello`,
|
||||||
|
});
|
||||||
|
};
|
||||||
82
src/utils/DialogManager.mjs
Normal file
82
src/utils/DialogManager.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { localizer } from "./localizer.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class that allows managing Dialogs that are created for various
|
||||||
|
* purposes such as deleting items, help popups, etc. This is a singleton class
|
||||||
|
* that upon instantiating after the first time will just return the first instance
|
||||||
|
*/
|
||||||
|
export class DialogManager {
|
||||||
|
|
||||||
|
/** @type {Map<string, Dialog>} */
|
||||||
|
static #dialogs = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses a dialog if it already exists, or creates a new one and renders it.
|
||||||
|
*
|
||||||
|
* @param {string} dialogId The ID to associate with the dialog, should be unique
|
||||||
|
* @param {object} data The data to pass to the Dialog constructor
|
||||||
|
* @param {DialogOptions} opts The options to pass to the Dialog constructor
|
||||||
|
* @returns {Dialog} The Dialog instance
|
||||||
|
*/
|
||||||
|
static async createOrFocus(dialogId, data, opts = {}) {
|
||||||
|
if (DialogManager.#dialogs.has(dialogId)) {
|
||||||
|
const dialog = DialogManager.#dialogs.get(dialogId);
|
||||||
|
dialog.bringToTop();
|
||||||
|
return dialog;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
This makes sure that if I provide a close function as a part of the data,
|
||||||
|
that the dialog still gets removed from the set once it's closed, otherwise
|
||||||
|
it could lead to dangling references that I don't care to keep. Or if I don't
|
||||||
|
provide the close function, it just sets the function as there isn't anything
|
||||||
|
extra that's needed to be called.
|
||||||
|
*/
|
||||||
|
if (data?.close) {
|
||||||
|
const provided = data.close;
|
||||||
|
data.close = () => {
|
||||||
|
DialogManager.#dialogs.delete(dialogId);
|
||||||
|
provided();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data.close = () => DialogManager.#dialogs.delete(dialogId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the Dialog with the modified data
|
||||||
|
const dialog = new Dialog(data, opts);
|
||||||
|
DialogManager.#dialogs.set(dialogId, dialog);
|
||||||
|
dialog.render(true);
|
||||||
|
return dialog;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes a dialog if it is rendered
|
||||||
|
*
|
||||||
|
* @param {string} dialogId The ID of the dialog to close
|
||||||
|
*/
|
||||||
|
static async close(dialogId) {
|
||||||
|
const dialog = DialogManager.#dialogs.get(dialogId);
|
||||||
|
dialog?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
static async helpDialog(
|
||||||
|
helpId,
|
||||||
|
helpContent,
|
||||||
|
helpTitle = `dotdungeon.common.help`,
|
||||||
|
localizationData = {},
|
||||||
|
) {
|
||||||
|
DialogManager.createOrFocus(
|
||||||
|
helpId,
|
||||||
|
{
|
||||||
|
title: localizer(helpTitle, localizationData),
|
||||||
|
content: localizer(helpContent, localizationData),
|
||||||
|
buttons: {},
|
||||||
|
},
|
||||||
|
{ resizable: true },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
static get size() {
|
||||||
|
return DialogManager.#dialogs.size;
|
||||||
|
}
|
||||||
|
};
|
||||||
39
src/utils/createDocumentProxy.mjs
Normal file
39
src/utils/createDocumentProxy.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
export function createDocumentProxy(defaultClass, classes) {
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
return new Proxy(function () {}, {
|
||||||
|
construct(_target, args) {
|
||||||
|
const [data] = args;
|
||||||
|
|
||||||
|
if (!classes[data.type]) {
|
||||||
|
return new defaultClass(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new classes[data.type](...args);
|
||||||
|
},
|
||||||
|
get(_target, prop, _receiver) {
|
||||||
|
|
||||||
|
if ([`create`, `createDocuments`].includes(prop)) {
|
||||||
|
return (data, options) => {
|
||||||
|
if (data.constructor === Array) {
|
||||||
|
return data.map(i => this.constructor.create(i, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!classes[data.type]) {
|
||||||
|
return defaultClass.create(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes[data.type].create(data, options);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (prop == Symbol.hasInstance) {
|
||||||
|
return (instance) => {
|
||||||
|
if (instance instanceof defaultClass) {return true}
|
||||||
|
return Object.values(classes).some(i => instance instanceof i);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return defaultClass[prop];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
45
src/utils/localizer.mjs
Normal file
45
src/utils/localizer.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/** A handlebars helper that utilizes the recursive localizer */
|
||||||
|
export function handlebarsLocalizer(key, ...args) {
|
||||||
|
let data = args[0];
|
||||||
|
if (args.length === 1) { data = args[0].hash }
|
||||||
|
if (key instanceof Handlebars.SafeString) {key = key.toString()}
|
||||||
|
const localized = localizer(key, data);
|
||||||
|
return localized;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A localizer that allows recursively localizing strings so that localized strings
|
||||||
|
* that want to use other localized strings can.
|
||||||
|
*
|
||||||
|
* @param {string} key The localization key to retrieve
|
||||||
|
* @param {object?} args The arguments provided to the localizer for replacement
|
||||||
|
* @param {number?} depth The current depth of the localizer
|
||||||
|
* @returns The localized string
|
||||||
|
*/
|
||||||
|
export function localizer(key, args = {}, depth = 0) {
|
||||||
|
/** @type {string} */
|
||||||
|
let localized = game.i18n.format(key, args);
|
||||||
|
const subkeys = localized.matchAll(/@(?<key>[a-zA-Z.]+)/gm);
|
||||||
|
|
||||||
|
// Short-cut to help prevent infinite recursion
|
||||||
|
if (depth > 10) {
|
||||||
|
return localized;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Helps prevent localization on the same key so that we aren't doing excess work.
|
||||||
|
*/
|
||||||
|
const localizedSubkeys = new Map();
|
||||||
|
for (const match of subkeys) {
|
||||||
|
const subkey = match.groups.key;
|
||||||
|
if (localizedSubkeys.has(subkey)) {continue}
|
||||||
|
localizedSubkeys.set(subkey, localizer(subkey, args, depth + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
return localized.replace(
|
||||||
|
/@(?<key>[a-zA-Z.]+)/gm,
|
||||||
|
(_fullMatch, subkey) => {
|
||||||
|
return localizedSubkeys.get(subkey);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
22
src/utils/logger.mjs
Normal file
22
src/utils/logger.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
const augmentedProps = new Set([
|
||||||
|
`debug`,
|
||||||
|
`log`,
|
||||||
|
`error`,
|
||||||
|
`info`,
|
||||||
|
`warn`,
|
||||||
|
`group`,
|
||||||
|
`time`,
|
||||||
|
`timeEnd`,
|
||||||
|
`timeLog`,
|
||||||
|
`timeStamp`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @type {Console} */
|
||||||
|
globalThis.Logger = new Proxy(console, {
|
||||||
|
get(target, prop, _receiver) {
|
||||||
|
if (augmentedProps.has(prop)) {
|
||||||
|
return (...args) => target[prop](game.system.id, `|`, ...args);
|
||||||
|
};
|
||||||
|
return target[prop];
|
||||||
|
},
|
||||||
|
});
|
||||||
0
styles/root.scss
Normal file
0
styles/root.scss
Normal file
7
styles/v1/components/common.scss
Normal file
7
styles/v1/components/common.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Disclaimer: This CSS is used by a custom web component and is scoped to JUST
|
||||||
|
// the corresponding web component. This should only be imported by web component
|
||||||
|
// style files.
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
23
styles/v1/components/icon.scss
Normal file
23
styles/v1/components/icon.scss
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
Disclaimer: This CSS is used by a custom web component and is scoped to JUST
|
||||||
|
the corresponding web component. Importing this into other files is forbidden
|
||||||
|
*/
|
||||||
|
|
||||||
|
$default-size: 1rem;
|
||||||
|
|
||||||
|
@use "./common.scss";
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: var(--size, $default-size);
|
||||||
|
height: var(--size, $default-size);
|
||||||
|
fill: var(--fill);
|
||||||
|
stroke: var(--stroke);
|
||||||
|
}
|
||||||
63
styles/v1/components/incrementer.scss
Normal file
63
styles/v1/components/incrementer.scss
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
Disclaimer: This CSS is used by a custom web component and is scoped to JUST
|
||||||
|
the corresponding web component. Importing this into other files is forbidden
|
||||||
|
*/
|
||||||
|
|
||||||
|
$default-border-radius: 4px;
|
||||||
|
$default-height: 1.5rem;
|
||||||
|
|
||||||
|
@use "../mixins/material";
|
||||||
|
@use "./common.scss";
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--height, $default-height) var(--width, 50px) var(--height, $default-height);
|
||||||
|
grid-template-rows: var(--height, 1fr);
|
||||||
|
border-radius: var(--border-radius, $default-border-radius);
|
||||||
|
@include material.elevate(2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@include material.elevate(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
@include material.elevate(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span, input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-family: var(--font-family, inherit);
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-size, inherit);
|
||||||
|
padding: 2px 4px;
|
||||||
|
|
||||||
|
&::-webkit-inner-spin-button, &::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.increment, .decrement {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.increment {
|
||||||
|
border-radius: 0 var(--border-radius, $default-border-radius) var(--border-radius, 4px) 0;
|
||||||
|
}
|
||||||
|
.decrement {
|
||||||
|
border-radius: var(--border-radius, $default-border-radius) 0 0 var(--border-radius, $default-border-radius);
|
||||||
|
}
|
||||||
1
styles/v1/index.scss
Normal file
1
styles/v1/index.scss
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
// Styling version 1
|
||||||
35
system.json
Normal file
35
system.json
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"id": "fst",
|
||||||
|
"title": "Foundry System Template",
|
||||||
|
"description": "",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"download": "",
|
||||||
|
"manifest": "",
|
||||||
|
"url": "",
|
||||||
|
"compatibility": {
|
||||||
|
"minimum": 12,
|
||||||
|
"verified": 12,
|
||||||
|
"maximum": 12
|
||||||
|
},
|
||||||
|
"authors": [],
|
||||||
|
"esmodules": [
|
||||||
|
"src/main.mjs"
|
||||||
|
],
|
||||||
|
"styles": [],
|
||||||
|
"packs": [],
|
||||||
|
"documentTypes": {
|
||||||
|
"Actor": {
|
||||||
|
"player": {
|
||||||
|
"htmlFields": [],
|
||||||
|
"filePathFields": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Item": {}
|
||||||
|
},
|
||||||
|
"flags": {
|
||||||
|
"hotReload": {
|
||||||
|
"extensions": ["css", "hbs", "json", "js", "mjs", "svg"],
|
||||||
|
"paths": ["templates", "langs", ".styles", "module", "assets"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
templates/Player/v1/main.hbs
Normal file
3
templates/Player/v1/main.hbs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<form autocomplete="off" class="actor--pc-v2">
|
||||||
|
Hello Foundry
|
||||||
|
</form>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue