Merge branch 'master' into 003-add-character-profile

This commit is contained in:
Johannes Loher 2021-01-06 14:33:46 +01:00
commit daeb9fc703
3 changed files with 487 additions and 448 deletions

View file

@ -3,50 +3,89 @@
An implementation of the Dungeonslayers 4 game system for [Foundry Virtual
Tabletop](http://foundryvtt.com).
## Prerequisites
This system provides character sheet support for Actors and Items and mechanical
support for dice and rules necessary to
play games of Dungeponslayers 4.
In order to build this system, a recent version of `npm` is required.
## Installation
## Building
To install and use the Dungeonslayers 4 system for Foundry Virtual Tabletop,
simply paste the following URL into the **Install System** dialog on the Setup
menu of the application.
To build the system, first install all required dependencies:
https://git.f3l.de/dungeonslayers/ds4/-/raw/master/src/system.json?inline=false
## Development
### Prerequisits
In order to build this system, recent versions of `node` and `npm` are required.
We recommend using the latest lts version of `node`, which is `v14.15.4` at the
time of writing. If you use `nvm` to manage your `node` versions, you can simply
run
```
nvm install
```
in the project's root directory.
You also need to install the the project's dependencies. To do so, run
```
npm install
```
Then build the project by running
### Building
You can build the project by running
```
npm run build
```
If you'd like the built system to be automatically linked to your local Foundry
VTT installation's data folder, add a file called `foundryconfig.json` to the
project root with the following contents:
Alternatively, you can run
```
npm run build:watch
```
to watch for changes and automatically build as necessary.
### Linking the built system to Foundry VTT
In order to provide a fluent development experience, it is recommended to link
the built system to your local Foundry VTT installation's data folder. In order
to do so, first add a file called `foundryconfig.json` to the project root with
the following content:
```
{
"dataPath": "/<absolute path to your home>/.local/share/FoundryVTT",
"repository": "",
"rawURL": ""
"dataPath": "<path to your home directory>/.local/share/FoundryVTT"
}
```
On platforms other than Linux you need to adjust the path accordingly.
Then run
```
npm run link
```
If you want the system to be continuously build upon every saved change, just
run
### Running the tests
You can run the tests with the following command:
```
npm run build:watch
npm test
```
# Licensing
## Contributing
Code and content contributions are accepted. Please feel free to submit issues
to the issue tracker or submit merge requests for code changes.
## Licensing
Dungeonslayers (© Christian Kennig) is licensed under [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/de/deed.en).
@ -56,5 +95,5 @@ CC BY-NC-SA 3.0. Hence the modified icons are also published under this
license. A copy of this license can be found under
[src/assets/official/LICENSE](src/assets/official/LICENSE).
The rest of this project is licensed under the MIT License, a copy of which can
be found under [LICENSE](LICENSE).
The software component of this project is licensed under the MIT License, a copy
of which can be found under [LICENSE](LICENSE).

View file

@ -1,429 +1,429 @@
const gulp = require("gulp");
const fs = require("fs-extra");
const path = require("path");
const chalk = require("chalk");
const archiver = require("archiver");
const stringify = require("json-stringify-pretty-compact");
const typescript = require("typescript");
const ts = require("gulp-typescript");
const less = require("gulp-less");
const sass = require("gulp-sass");
const git = require("gulp-git");
const argv = require("yargs").argv;
sass.compiler = require("sass");
function getConfig() {
const configPath = path.resolve(process.cwd(), "foundryconfig.json");
let config;
if (fs.existsSync(configPath)) {
config = fs.readJSONSync(configPath);
return config;
} else {
return;
}
}
function getManifest() {
const json = {};
if (fs.existsSync("src")) {
json.root = "src";
} else {
json.root = "dist";
}
const modulePath = path.join(json.root, "module.json");
const systemPath = path.join(json.root, "system.json");
if (fs.existsSync(modulePath)) {
json.file = fs.readJSONSync(modulePath);
json.name = "module.json";
} else if (fs.existsSync(systemPath)) {
json.file = fs.readJSONSync(systemPath);
json.name = "system.json";
} else {
return;
}
return json;
}
/**
* TypeScript transformers
* @returns {typescript.TransformerFactory<typescript.SourceFile>}
*/
function createTransformer() {
/**
* @param {typescript.Node} node
*/
function shouldMutateModuleSpecifier(node) {
if (!typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node)) return false;
if (node.moduleSpecifier === undefined) return false;
if (!typescript.isStringLiteral(node.moduleSpecifier)) return false;
if (!node.moduleSpecifier.text.startsWith("./") && !node.moduleSpecifier.text.startsWith("../")) return false;
if (path.extname(node.moduleSpecifier.text) !== "") return false;
return true;
}
/**
* Transforms import/export declarations to append `.js` extension
* @param {typescript.TransformationContext} context
*/
function importTransformer(context) {
return (node) => {
/**
* @param {typescript.Node} node
*/
function visitor(node) {
if (shouldMutateModuleSpecifier(node)) {
if (typescript.isImportDeclaration(node)) {
const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
return typescript.updateImportDeclaration(
node,
node.decorators,
node.modifiers,
node.importClause,
newModuleSpecifier
);
} else if (typescript.isExportDeclaration(node)) {
const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
return typescript.updateExportDeclaration(
node,
node.decorators,
node.modifiers,
node.exportClause,
newModuleSpecifier
);
}
}
return typescript.visitEachChild(node, visitor, context);
}
return typescript.visitNode(node, visitor);
};
}
return importTransformer;
}
const tsConfig = ts.createProject("tsconfig.json", {
getCustomTransformers: (_program) => ({
after: [createTransformer()],
}),
});
/********************/
/* BUILD */
/********************/
/**
* Build TypeScript
*/
function buildTS() {
return gulp.src("src/**/*.ts").pipe(tsConfig()).pipe(gulp.dest("dist"));
}
/**
* Build Less
*/
function buildLess() {
return gulp.src("src/*.less").pipe(less()).pipe(gulp.dest("dist"));
}
/**
* Build SASS
*/
function buildSASS() {
return gulp.src("src/*.scss").pipe(sass().on("error", sass.logError)).pipe(gulp.dest("dist"));
}
/**
* Copy static files
*/
async function copyFiles() {
const statics = ["lang", "fonts", "assets", "templates", "module.json", "system.json", "template.json"];
try {
for (const file of statics) {
if (fs.existsSync(path.join("src", file))) {
await fs.copy(path.join("src", file), path.join("dist", file));
}
}
return Promise.resolve();
} catch (err) {
Promise.reject(err);
}
}
/**
* Watch for changes for each build step
*/
function buildWatch() {
gulp.watch("src/**/*.ts", { ignoreInitial: false }, buildTS);
gulp.watch("src/**/*.less", { ignoreInitial: false }, buildLess);
gulp.watch("src/**/*.scss", { ignoreInitial: false }, buildSASS);
gulp.watch(["src/fonts", "src/lang", "src/templates", "src/*.json"], { ignoreInitial: false }, copyFiles);
}
/********************/
/* CLEAN */
/********************/
/**
* Remove built files from `dist` folder
* while ignoring source files
*/
async function clean() {
const name = path.basename(path.resolve("."));
const files = [];
// If the project uses TypeScript
if (fs.existsSync(path.join("src", `${name}.ts`))) {
files.push(
"lang",
"templates",
"assets",
"module",
`${name}.js`,
"module.json",
"system.json",
"template.json"
);
}
// If the project uses Less or SASS
if (fs.existsSync(path.join("src", `${name}.less`)) || fs.existsSync(path.join("src", `${name}.scss`))) {
files.push("fonts", `${name}.css`);
}
console.log(" ", chalk.yellow("Files to clean:"));
console.log(" ", chalk.blueBright(files.join("\n ")));
// Attempt to remove the files
try {
for (const filePath of files) {
await fs.remove(path.join("dist", filePath));
}
return Promise.resolve();
} catch (err) {
Promise.reject(err);
}
}
/********************/
/* LINK */
/********************/
/**
* Link build to User Data folder
*/
async function linkUserData() {
const name = path.basename(path.resolve("."));
const config = fs.readJSONSync("foundryconfig.json");
let destDir;
try {
if (
fs.existsSync(path.resolve(".", "dist", "module.json")) ||
fs.existsSync(path.resolve(".", "src", "module.json"))
) {
destDir = "modules";
} else if (
fs.existsSync(path.resolve(".", "dist", "system.json")) ||
fs.existsSync(path.resolve(".", "src", "system.json"))
) {
destDir = "systems";
} else {
throw Error(`Could not find ${chalk.blueBright("module.json")} or ${chalk.blueBright("system.json")}`);
}
let linkDir;
if (config.dataPath) {
if (!fs.existsSync(path.join(config.dataPath, "Data")))
throw Error("User Data path invalid, no Data directory found");
linkDir = path.join(config.dataPath, "Data", destDir, name);
} else {
throw Error("No User Data path defined in foundryconfig.json");
}
if (argv.clean || argv.c) {
console.log(chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`));
await fs.remove(linkDir);
} else if (!fs.existsSync(linkDir)) {
console.log(chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`));
await fs.symlink(path.resolve("./dist"), linkDir);
}
return Promise.resolve();
} catch (err) {
Promise.reject(err);
}
}
/*********************/
/* PACKAGE */
/*********************/
/**
* Package build
*/
async function packageBuild() {
const manifest = getManifest();
return new Promise((resolve, reject) => {
try {
// Remove the package dir without doing anything else
if (argv.clean || argv.c) {
console.log(chalk.yellow("Removing all packaged files"));
fs.removeSync("package");
return;
}
// Ensure there is a directory to hold all the packaged versions
fs.ensureDirSync("package");
// Initialize the zip file
const zipName = `${manifest.file.name}-v${manifest.file.version}.zip`;
const zipFile = fs.createWriteStream(path.join("package", zipName));
const zip = archiver("zip", { zlib: { level: 9 } });
zipFile.on("close", () => {
console.log(chalk.green(zip.pointer() + " total bytes"));
console.log(chalk.green(`Zip file ${zipName} has been written`));
return resolve();
});
zip.on("error", (err) => {
throw err;
});
zip.pipe(zipFile);
// Add the directory with the final code
zip.directory("dist/", manifest.file.name);
zip.finalize();
} catch (err) {
return reject(err);
}
});
}
/*********************/
/* PACKAGE */
/*********************/
/**
* Update version and URLs in the manifest JSON
*/
function updateManifest(cb) {
const packageJson = fs.readJSONSync("package.json");
const config = getConfig(),
manifest = getManifest(),
rawURL = config.rawURL,
repoURL = config.repository,
manifestRoot = manifest.root;
if (!config) cb(Error(chalk.red("foundryconfig.json not found")));
if (!manifest) cb(Error(chalk.red("Manifest JSON not found")));
if (!rawURL || !repoURL) cb(Error(chalk.red("Repository URLs not configured in foundryconfig.json")));
try {
const version = argv.update || argv.u;
/* Update version */
const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/;
const currentVersion = manifest.file.version;
let targetVersion = "";
if (!version) {
cb(Error("Missing version number"));
}
if (versionMatch.test(version)) {
targetVersion = version;
} else {
targetVersion = currentVersion.replace(versionMatch, (substring, major, minor, patch) => {
console.log(substring, Number(major) + 1, Number(minor) + 1, Number(patch) + 1);
if (version === "major") {
return `${Number(major) + 1}.0.0`;
} else if (version === "minor") {
return `${major}.${Number(minor) + 1}.0`;
} else if (version === "patch") {
return `${major}.${minor}.${Number(patch) + 1}`;
} else {
return "";
}
});
}
if (targetVersion === "") {
return cb(Error(chalk.red("Error: Incorrect version arguments.")));
}
if (targetVersion === currentVersion) {
return cb(Error(chalk.red("Error: Target version is identical to current version.")));
}
console.log(`Updating version number to '${targetVersion}'`);
packageJson.version = targetVersion;
manifest.file.version = targetVersion;
/* Update URLs */
const result = `${rawURL}/v${manifest.file.version}/package/${manifest.file.name}-v${manifest.file.version}.zip`;
manifest.file.url = repoURL;
manifest.file.manifest = `${rawURL}/master/${manifestRoot}/${manifest.name}`;
manifest.file.download = result;
const prettyProjectJson = stringify(manifest.file, {
maxLength: 35,
indent: "\t",
});
fs.writeJSONSync("package.json", packageJson, { spaces: "\t" });
fs.writeFileSync(path.join(manifest.root, manifest.name), prettyProjectJson, "utf8");
return cb();
} catch (err) {
cb(err);
}
}
function gitAdd() {
return gulp.src("package").pipe(git.add({ args: "--no-all" }));
}
function gitCommit() {
return gulp.src("./*").pipe(
git.commit(`v${getManifest().file.version}`, {
args: "-a",
disableAppendPaths: true,
})
);
}
function gitTag() {
const manifest = getManifest();
return git.tag(`v${manifest.file.version}`, `Updated to ${manifest.file.version}`, (err) => {
if (err) throw err;
});
}
const execGit = gulp.series(gitAdd, gitCommit, gitTag);
const execBuild = gulp.parallel(buildTS, buildLess, buildSASS, copyFiles);
exports.build = gulp.series(clean, execBuild);
exports.watch = buildWatch;
exports.clean = clean;
exports.link = linkUserData;
exports.package = packageBuild;
exports.update = updateManifest;
exports.publish = gulp.series(clean, updateManifest, execBuild, packageBuild, execGit);
const gulp = require("gulp");
const fs = require("fs-extra");
const path = require("path");
const chalk = require("chalk");
const archiver = require("archiver");
const stringify = require("json-stringify-pretty-compact");
const typescript = require("typescript");
const ts = require("gulp-typescript");
const less = require("gulp-less");
const sass = require("gulp-sass");
const git = require("gulp-git");
const argv = require("yargs").argv;
sass.compiler = require("sass");
function getConfig() {
const configPath = path.resolve(process.cwd(), "foundryconfig.json");
let config;
if (fs.existsSync(configPath)) {
config = fs.readJSONSync(configPath);
return config;
} else {
return;
}
}
function getManifest() {
const json = {};
if (fs.existsSync("src")) {
json.root = "src";
} else {
json.root = "dist";
}
const modulePath = path.join(json.root, "module.json");
const systemPath = path.join(json.root, "system.json");
if (fs.existsSync(modulePath)) {
json.file = fs.readJSONSync(modulePath);
json.name = "module.json";
} else if (fs.existsSync(systemPath)) {
json.file = fs.readJSONSync(systemPath);
json.name = "system.json";
} else {
return;
}
return json;
}
/**
* TypeScript transformers
* @returns {typescript.TransformerFactory<typescript.SourceFile>}
*/
function createTransformer() {
/**
* @param {typescript.Node} node
*/
function shouldMutateModuleSpecifier(node) {
if (!typescript.isImportDeclaration(node) && !typescript.isExportDeclaration(node)) return false;
if (node.moduleSpecifier === undefined) return false;
if (!typescript.isStringLiteral(node.moduleSpecifier)) return false;
if (!node.moduleSpecifier.text.startsWith("./") && !node.moduleSpecifier.text.startsWith("../")) return false;
if (path.extname(node.moduleSpecifier.text) !== "") return false;
return true;
}
/**
* Transforms import/export declarations to append `.js` extension
* @param {typescript.TransformationContext} context
*/
function importTransformer(context) {
return (node) => {
/**
* @param {typescript.Node} node
*/
function visitor(node) {
if (shouldMutateModuleSpecifier(node)) {
if (typescript.isImportDeclaration(node)) {
const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
return typescript.updateImportDeclaration(
node,
node.decorators,
node.modifiers,
node.importClause,
newModuleSpecifier,
);
} else if (typescript.isExportDeclaration(node)) {
const newModuleSpecifier = typescript.createLiteral(`${node.moduleSpecifier.text}.js`);
return typescript.updateExportDeclaration(
node,
node.decorators,
node.modifiers,
node.exportClause,
newModuleSpecifier,
);
}
}
return typescript.visitEachChild(node, visitor, context);
}
return typescript.visitNode(node, visitor);
};
}
return importTransformer;
}
const tsConfig = ts.createProject("tsconfig.json", {
getCustomTransformers: (_program) => ({
after: [createTransformer()],
}),
});
/********************/
/* BUILD */
/********************/
/**
* Build TypeScript
*/
function buildTS() {
return gulp.src("src/**/*.ts").pipe(tsConfig()).pipe(gulp.dest("dist"));
}
/**
* Build Less
*/
function buildLess() {
return gulp.src("src/*.less").pipe(less()).pipe(gulp.dest("dist"));
}
/**
* Build SASS
*/
function buildSASS() {
return gulp.src("src/*.scss").pipe(sass().on("error", sass.logError)).pipe(gulp.dest("dist"));
}
/**
* Copy static files
*/
async function copyFiles() {
const statics = ["lang", "fonts", "assets", "templates", "module.json", "system.json", "template.json"];
try {
for (const file of statics) {
if (fs.existsSync(path.join("src", file))) {
await fs.copy(path.join("src", file), path.join("dist", file));
}
}
return Promise.resolve();
} catch (err) {
Promise.reject(err);
}
}
/**
* Watch for changes for each build step
*/
function buildWatch() {
gulp.watch("src/**/*.ts", { ignoreInitial: false }, buildTS);
gulp.watch("src/**/*.less", { ignoreInitial: false }, buildLess);
gulp.watch("src/**/*.scss", { ignoreInitial: false }, buildSASS);
gulp.watch(["src/fonts", "src/lang", "src/templates", "src/*.json"], { ignoreInitial: false }, copyFiles);
}
/********************/
/* CLEAN */
/********************/
/**
* Remove built files from `dist` folder
* while ignoring source files
*/
async function clean() {
const name = path.basename(path.resolve("."));
const files = [];
// If the project uses TypeScript
if (fs.existsSync(path.join("src", `${name}.ts`))) {
files.push(
"lang",
"templates",
"assets",
"module",
`${name}.js`,
"module.json",
"system.json",
"template.json",
);
}
// If the project uses Less or SASS
if (fs.existsSync(path.join("src", `${name}.less`)) || fs.existsSync(path.join("src", `${name}.scss`))) {
files.push("fonts", `${name}.css`);
}
console.log(" ", chalk.yellow("Files to clean:"));
console.log(" ", chalk.blueBright(files.join("\n ")));
// Attempt to remove the files
try {
for (const filePath of files) {
await fs.remove(path.join("dist", filePath));
}
return Promise.resolve();
} catch (err) {
Promise.reject(err);
}
}
/********************/
/* LINK */
/********************/
/**
* Link build to User Data folder
*/
async function linkUserData() {
const name = path.basename(path.resolve("."));
const config = fs.readJSONSync("foundryconfig.json");
let destDir;
try {
if (
fs.existsSync(path.resolve(".", "dist", "module.json")) ||
fs.existsSync(path.resolve(".", "src", "module.json"))
) {
destDir = "modules";
} else if (
fs.existsSync(path.resolve(".", "dist", "system.json")) ||
fs.existsSync(path.resolve(".", "src", "system.json"))
) {
destDir = "systems";
} else {
throw Error(`Could not find ${chalk.blueBright("module.json")} or ${chalk.blueBright("system.json")}`);
}
let linkDir;
if (config.dataPath) {
if (!fs.existsSync(path.join(config.dataPath, "Data")))
throw Error("User Data path invalid, no Data directory found");
linkDir = path.join(config.dataPath, "Data", destDir, name);
} else {
throw Error("No User Data path defined in foundryconfig.json");
}
if (argv.clean || argv.c) {
console.log(chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`));
await fs.remove(linkDir);
} else if (!fs.existsSync(linkDir)) {
console.log(chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`));
await fs.symlink(path.resolve("./dist"), linkDir);
}
return Promise.resolve();
} catch (err) {
Promise.reject(err);
}
}
/*********************/
/* PACKAGE */
/*********************/
/**
* Package build
*/
async function packageBuild() {
const manifest = getManifest();
return new Promise((resolve, reject) => {
try {
// Remove the package dir without doing anything else
if (argv.clean || argv.c) {
console.log(chalk.yellow("Removing all packaged files"));
fs.removeSync("package");
return;
}
// Ensure there is a directory to hold all the packaged versions
fs.ensureDirSync("package");
// Initialize the zip file
const zipName = `${manifest.file.name}-v${manifest.file.version}.zip`;
const zipFile = fs.createWriteStream(path.join("package", zipName));
const zip = archiver("zip", { zlib: { level: 9 } });
zipFile.on("close", () => {
console.log(chalk.green(zip.pointer() + " total bytes"));
console.log(chalk.green(`Zip file ${zipName} has been written`));
return resolve();
});
zip.on("error", (err) => {
throw err;
});
zip.pipe(zipFile);
// Add the directory with the final code
zip.directory("dist/", manifest.file.name);
zip.finalize();
} catch (err) {
return reject(err);
}
});
}
/*********************/
/* PACKAGE */
/*********************/
/**
* Update version and URLs in the manifest JSON
*/
function updateManifest(cb) {
const packageJson = fs.readJSONSync("package.json");
const config = getConfig(),
manifest = getManifest(),
rawURL = config.rawURL,
repoURL = config.repository,
manifestRoot = manifest.root;
if (!config) cb(Error(chalk.red("foundryconfig.json not found")));
if (!manifest) cb(Error(chalk.red("Manifest JSON not found")));
if (!rawURL || !repoURL) cb(Error(chalk.red("Repository URLs not configured in foundryconfig.json")));
try {
const version = argv.update || argv.u;
/* Update version */
const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/;
const currentVersion = manifest.file.version;
let targetVersion = "";
if (!version) {
cb(Error("Missing version number"));
}
if (versionMatch.test(version)) {
targetVersion = version;
} else {
targetVersion = currentVersion.replace(versionMatch, (substring, major, minor, patch) => {
console.log(substring, Number(major) + 1, Number(minor) + 1, Number(patch) + 1);
if (version === "major") {
return `${Number(major) + 1}.0.0`;
} else if (version === "minor") {
return `${major}.${Number(minor) + 1}.0`;
} else if (version === "patch") {
return `${major}.${minor}.${Number(patch) + 1}`;
} else {
return "";
}
});
}
if (targetVersion === "") {
return cb(Error(chalk.red("Error: Incorrect version arguments.")));
}
if (targetVersion === currentVersion) {
return cb(Error(chalk.red("Error: Target version is identical to current version.")));
}
console.log(`Updating version number to '${targetVersion}'`);
packageJson.version = targetVersion;
manifest.file.version = targetVersion;
/* Update URLs */
const result = `${rawURL}/v${manifest.file.version}/package/${manifest.file.name}-v${manifest.file.version}.zip`;
manifest.file.url = repoURL;
manifest.file.manifest = `${rawURL}/master/${manifestRoot}/${manifest.name}`;
manifest.file.download = result;
const prettyProjectJson = stringify(manifest.file, {
maxLength: 35,
indent: "\t",
});
fs.writeJSONSync("package.json", packageJson, { spaces: "\t" });
fs.writeFileSync(path.join(manifest.root, manifest.name), prettyProjectJson, "utf8");
return cb();
} catch (err) {
cb(err);
}
}
function gitAdd() {
return gulp.src("package").pipe(git.add({ args: "--no-all" }));
}
function gitCommit() {
return gulp.src("./*").pipe(
git.commit(`v${getManifest().file.version}`, {
args: "-a",
disableAppendPaths: true,
}),
);
}
function gitTag() {
const manifest = getManifest();
return git.tag(`v${manifest.file.version}`, `Updated to ${manifest.file.version}`, (err) => {
if (err) throw err;
});
}
const execGit = gulp.series(gitAdd, gitCommit, gitTag);
const execBuild = gulp.parallel(buildTS, buildLess, buildSASS, copyFiles);
exports.build = gulp.series(clean, execBuild);
exports.watch = buildWatch;
exports.clean = clean;
exports.link = linkUserData;
exports.package = packageBuild;
exports.update = updateManifest;
exports.publish = gulp.series(clean, updateManifest, execBuild, packageBuild, execGit);

View file

@ -22,7 +22,7 @@
"gridUnits": "m",
"primaryTokenAttribute": "combatValues.hitPoints.current",
"url": "https://git.f3l.de/dungeonslayers/ds4",
"manifest": "https://git.f3l.de/dungeonslayers/ds4/-/raw/latest/src/system.json?inline=false",
"download": "https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/latest/download?job=build",
"manifest": "https://git.f3l.de/dungeonslayers/ds4/-/raw/master/src/system.json?inline=false",
"download": "https://git.f3l.de/dungeonslayers/ds4/-/jobs/artifacts/0.1.0/download?job=build",
"license": "MIT"
}