feat: update for v10

Unfortunately, ditching TypeScript is needed for that :(
This commit is contained in:
Johannes Loher 2022-11-28 00:52:31 +01:00
parent f96b5cd55e
commit b6411f39dc
34 changed files with 271 additions and 2147 deletions

View File

@ -3,29 +3,36 @@
// SPDX-License-Identifier: MIT
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
env: {
browser: true,
es2022: true,
jquery: true,
},
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
plugins: ['@typescript-eslint'],
globals: {
Hooks: 'readonly',
Actor: 'readonly',
CONFIG: 'readonly',
ui: 'readonly',
Game: 'readonly',
game: 'readonly',
foundry: 'readonly',
Dialog: 'readonly',
twist: 'readonly',
},
rules: {},
overrides: [
{
files: ['./*.cjs', './*.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
},
files: ['./*.cjs', './*.js', './tools/**/*'],
env: { node: true, browser: false, jquery: false },
},
],
};

2
.gitignore vendored
View File

@ -14,6 +14,8 @@ npm-debug.log
# Local configuration
foundryconfig.json
/common
/client
# Distribution files
dist

View File

@ -30,15 +30,6 @@ lint:
cache:
<<: *global_cache
typecheck:
stage: check
before_script:
- yarn install --immutable
script:
- yarn typecheck
cache:
<<: *global_cache
reuse:
stage: check
image:

View File

@ -5,8 +5,6 @@
},
"eslint.nodePath": ".yarn/sdks",
"prettier.prettierPath": ".yarn/sdks/prettier/index.js",
"typescript.tsdk": ".yarn/sdks/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"importSorter.generalConfiguration.sortOnBeforeSave": true,
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.type": "newLineEachExpressionAfterCountLimitExceptIfOnlyOne",
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 120,

View File

@ -1,6 +1,6 @@
{
"name": "eslint",
"version": "8.25.0-sdk",
"version": "8.28.0-sdk",
"main": "./lib/api.js",
"type": "commonjs"
}

View File

@ -1,6 +1,6 @@
{
"name": "prettier",
"version": "2.7.1-sdk",
"version": "2.8.0-sdk",
"main": "./index.js",
"type": "commonjs"
}

View File

@ -1,20 +0,0 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsc
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsc your application uses
module.exports = absRequire(`typescript/bin/tsc`);

View File

@ -1,20 +0,0 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/bin/tsserver
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/bin/tsserver your application uses
module.exports = absRequire(`typescript/bin/tsserver`);

View File

@ -1,20 +0,0 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsc.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsc.js your application uses
module.exports = absRequire(`typescript/lib/tsc.js`);

View File

@ -1,223 +0,0 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
const moduleWrapper = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const isPortal = str => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
// before forwarding it to TS, and to add it back on all returned paths.
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
// file instances instead of the real ones.
//
// We only do this to modules owned by the the dependency tree roots.
// This avoids breaking the resolution when jumping inside a vendor
// with peer dep (otherwise jumping into react-dom would show resolution
// errors on react).
//
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
str = resolved;
}
}
str = normalize(str);
if (str.match(/\.zip\//)) {
switch (hostInfo) {
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
// VSCode only adds it automatically for supported schemes,
// so we have to do it manually for the `zip` scheme.
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
// 2021-10-08: VSCode changed the format in 1.61.
// Before | ^zip:/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
// 2022-04-06: VSCode changed the format in 1.66.
// Before | ^/zip//c:/foo/bar.zip/package.json
// After | ^/zip/c:/foo/bar.zip/package.json
//
// 2022-05-06: VSCode changed the format in 1.68
// Before | ^/zip/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
case `vscode <1.66`: {
str = `^/zip/${str}`;
} break;
case `vscode <1.68`: {
str = `^/zip${str}`;
} break;
case `vscode`: {
str = `^/zip/${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile://${str}`;
} break;
default: {
str = `zip:${str}`;
} break;
}
}
}
return str;
}
function fromEditorPath(str) {
switch (hostInfo) {
case `coc-nvim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// So in order to convert it back, we use .* to match all the thing
// before `zipfile:`
return process.platform === `win32`
? str.replace(/^.*zipfile:\//, ``)
: str.replace(/^.*zipfile:/, ``);
} break;
case `neovim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
return str.replace(/^zipfile:\/\//, ``);
} break;
case `vscode`:
default: {
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
} break;
}
}
// Force enable 'allowLocalPluginLoads'
// TypeScript tries to resolve plugins using a path relative to itself
// which doesn't work when using the global cache
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
// And here is the point where we hijack the VSCode <-> TS communications
// by adding ourselves in the middle. We locate everything that looks
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string | object} */ message) {
const isStringMessage = typeof message === 'string';
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if (
parsedMessage != null &&
typeof parsedMessage === `object` &&
parsedMessage.arguments &&
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
// The RegExp from https://semver.org/ but without the caret at the start
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
) ?? []).map(Number)
if (major === 1) {
if (minor < 61) {
hostInfo += ` <1.61`;
} else if (minor < 66) {
hostInfo += ` <1.66`;
} else if (minor < 68) {
hostInfo += ` <1.68`;
}
}
}
}
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
return typeof value === 'string' ? fromEditorPath(value) : value;
});
return originalOnMessage.call(
this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
);
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserver.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserver.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));

View File

@ -1,223 +0,0 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
const moduleWrapper = tsserver => {
if (!process.versions.pnp) {
return tsserver;
}
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
const isPortal = str => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
// before forwarding it to TS, and to add it back on all returned paths.
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
// file instances instead of the real ones.
//
// We only do this to modules owned by the the dependency tree roots.
// This avoids breaking the resolution when jumping inside a vendor
// with peer dep (otherwise jumping into react-dom would show resolution
// errors on react).
//
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
str = resolved;
}
}
str = normalize(str);
if (str.match(/\.zip\//)) {
switch (hostInfo) {
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
// VSCode only adds it automatically for supported schemes,
// so we have to do it manually for the `zip` scheme.
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
// 2021-10-08: VSCode changed the format in 1.61.
// Before | ^zip:/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
// 2022-04-06: VSCode changed the format in 1.66.
// Before | ^/zip//c:/foo/bar.zip/package.json
// After | ^/zip/c:/foo/bar.zip/package.json
//
// 2022-05-06: VSCode changed the format in 1.68
// Before | ^/zip/c:/foo/bar.zip/package.json
// After | ^/zip//c:/foo/bar.zip/package.json
//
case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
case `vscode <1.66`: {
str = `^/zip/${str}`;
} break;
case `vscode <1.68`: {
str = `^/zip${str}`;
} break;
case `vscode`: {
str = `^/zip/${str}`;
} break;
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
case `coc-nvim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = resolve(`zipfile:${str}`);
} break;
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
// We have to resolve the actual file system path from virtual path,
// everything else is up to neovim
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
str = `zipfile://${str}`;
} break;
default: {
str = `zip:${str}`;
} break;
}
}
}
return str;
}
function fromEditorPath(str) {
switch (hostInfo) {
case `coc-nvim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
// So in order to convert it back, we use .* to match all the thing
// before `zipfile:`
return process.platform === `win32`
? str.replace(/^.*zipfile:\//, ``)
: str.replace(/^.*zipfile:/, ``);
} break;
case `neovim`: {
str = str.replace(/\.zip::/, `.zip/`);
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
return str.replace(/^zipfile:\/\//, ``);
} break;
case `vscode`:
default: {
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
} break;
}
}
// Force enable 'allowLocalPluginLoads'
// TypeScript tries to resolve plugins using a path relative to itself
// which doesn't work when using the global cache
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
// TypeScript already does local loads and if this code is running the user trusts the workspace
// https://github.com/microsoft/vscode/issues/45856
const ConfiguredProject = tsserver.server.ConfiguredProject;
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
this.projectService.allowLocalPluginLoads = true;
return originalEnablePluginsWithOptions.apply(this, arguments);
};
// And here is the point where we hijack the VSCode <-> TS communications
// by adding ourselves in the middle. We locate everything that looks
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
onMessage(/** @type {string | object} */ message) {
const isStringMessage = typeof message === 'string';
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if (
parsedMessage != null &&
typeof parsedMessage === `object` &&
parsedMessage.arguments &&
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
// The RegExp from https://semver.org/ but without the caret at the start
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
) ?? []).map(Number)
if (major === 1) {
if (minor < 61) {
hostInfo += ` <1.61`;
} else if (minor < 66) {
hostInfo += ` <1.66`;
} else if (minor < 68) {
hostInfo += ` <1.68`;
}
}
}
}
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
return typeof value === 'string' ? fromEditorPath(value) : value;
});
return originalOnMessage.call(
this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
);
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));

View File

@ -1,20 +0,0 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = createRequire(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/typescript.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/typescript.js your application uses
module.exports = absRequire(`typescript/lib/typescript.js`);

View File

@ -1,6 +0,0 @@
{
"name": "typescript",
"version": "4.7.4-sdk",
"main": "./lib/typescript.js",
"type": "commonjs"
}

8
jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "es2022",
"target": "ES2022"
},
"exclude": ["node_modules", "dist"],
"include": ["src", "client", "common"]
}

3
jsconfig.json.license Normal file
View File

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2022 Johannes Loher
SPDX-License-Identifier: MIT

View File

@ -1,5 +1,5 @@
{
"name": "tickwerk",
"id": "tickwerk",
"title": "Tickwerk",
"description": "A tick based combat system for Foundry Virtual Tabletop",
"authors": [
@ -16,8 +16,10 @@
"bugs": "https://git.f3l.de/dungeonslayers/tickwerk/-/issues",
"changelog": "https://git.f3l.de/dungeonslayers/tickwerk/-/releases/1.2.1",
"version": "1.2.1",
"minimumCoreVersion": "9",
"compatibleCoreVersion": "9",
"compatibility": {
"minimum": "10.290",
"verified": "10"
},
"esmodules": ["tickwerk.js"],
"styles": ["styles/tickwerk.css"],
"languages": [

View File

@ -27,11 +27,9 @@
"clean": "run-p clean:files clean:link",
"clean:files": "rimraf dist",
"clean:link": "node ./tools/link-package.js --clean",
"lint": "eslint --ext .ts,.js,.cjs,.mjs .",
"lint:fix": "eslint --ext .ts,.js,.cjs,.mjs --fix .",
"format": "prettier --write \"./**/*.(ts|js|cjs|mjs|json|scss|yml)\"",
"typecheck": "tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch",
"lint": "eslint --ext .js,.cjs,.mjs .",
"lint:fix": "eslint --ext .js,.cjs,.mjs --fix .",
"format": "prettier --write \"./**/*.(js|cjs|mjs|json|scss|yml)\"",
"bump-version": "node ./tools/bump-version.js",
"postinstall": "husky install",
"changelog": "conventional-changelog -p conventionalcommits -o CHANGELOG.md -r 2"
@ -40,20 +38,7 @@
"@commitlint/cli": "17.3.0",
"@commitlint/config-conventional": "17.3.0",
"@guanghechen/rollup-plugin-copy": "2.1.4",
"@league-of-foundry-developers/foundry-vtt-types": "9.280.0",
"@pixi/constants": "6.2.1",
"@pixi/core": "6.2.1",
"@pixi/display": "6.2.1",
"@pixi/graphics": "6.2.1",
"@pixi/math": "6.2.1",
"@pixi/runner": "6.2.1",
"@pixi/settings": "6.2.1",
"@pixi/utils": "6.2.1",
"@seald-io/nedb": "3.1.0",
"@swc/core": "1.3.20",
"@types/fs-extra": "9.0.13",
"@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0",
"conventional-changelog-cli": "2.2.2",
"conventional-changelog-conventionalcommits": "5.0.0",
"eslint": "8.28.0",
@ -70,13 +55,10 @@
"rollup-plugin-swc3": "0.7.0",
"sass": "1.56.1",
"semver": "7.3.8",
"ts-node": "10.9.1",
"tslib": "2.4.1",
"typescript": "4.7.4",
"yargs": "17.6.2"
},
"lint-staged": {
"*.(ts|js|cjs|mjs)": "eslint --cache --fix",
"*.(js|cjs|mjs)": "eslint --cache --fix",
"*.(json|scss|yml)": "prettier --write"
},
"packageManager": "yarn@3.2.4"

View File

@ -24,7 +24,7 @@ const isProduction = process.env.NODE_ENV === 'production';
* @type {import('rollup').RollupOptions}
*/
const config = {
input: { [name]: `${sourceDirectory}/${name}.ts` },
input: { [name]: `${sourceDirectory}/${name}.js` },
output: {
dir: distDirectory,
format: 'es',

View File

@ -3,35 +3,43 @@
// SPDX-License-Identifier: MIT
export const registerCombatTrackerFunctionality = () => {
CONFIG.ui.combat = CombatTrackerMixin(CONFIG.ui.combat as typeof CombatTracker); // TODO: improve upstream types
CONFIG.ui.combat = CombatTrackerMixin(CONFIG.ui.combat);
};
const CombatTrackerMixin = (BaseCombatTracker: typeof CombatTracker) => {
/**
* Enhance a combat tracker class with functionality for Tickwerk.
* @param {typeof CombatTracker} BaseCombatTracker The combat tracker class to enhance
* @returns The enhanced combat tracker class
*/
const CombatTrackerMixin = (BaseCombatTracker) => {
return class TickwerkCombatTracker extends BaseCombatTracker {
static override get defaultOptions(): ApplicationOptions {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
template: 'modules/tickwerk/templates/combat-tracker.hbs',
});
}
override async getData(options?: Partial<ApplicationOptions>): Promise<CombatTracker.Data> {
/** @override */
async getData(options) {
const data = await super.getData(options);
return {
...data,
turns: data.turns.map((turn) => ({ ...turn, waiting: this.viewed?.combatants.get(turn.id)?.waiting })),
} as CombatTracker.Data; // TODO: Improve upstream types
};
}
override activateListeners(html: JQuery): void {
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find('.combatant-control[data-control="toggleWaiting"]').on('click', this._onToggleWaiting.bind(this));
}
/**
* Handle clicks on the Combatant waiting control button.
* @param event The originating click event
* @param {JQuery.ClickEvent} event The originating click event
*/
_onToggleWaiting(event: JQuery.ClickEvent) {
_onToggleWaiting(event) {
event.preventDefault();
event.stopPropagation();
const button = event.currentTarget;

View File

@ -2,4 +2,4 @@
//
// SPDX-License-Identifier: MIT
export const packageId = 'tickwerk' as const;
export const packageId = 'tickwerk';

View File

@ -5,12 +5,12 @@
import { getGame } from '../../helpers';
export const registerActiveEffectFunctionality = () => {
Hooks.on<Hooks.CreateDocument<typeof ActiveEffect>>('createActiveEffect', onActiveEffectChanged);
Hooks.on<Hooks.UpdateDocument<typeof ActiveEffect>>('updateActiveEffect', onActiveEffectChanged);
Hooks.on<Hooks.DeleteDocument<typeof ActiveEffect>>('deleteActiveEffect', onActiveEffectChanged);
Hooks.on('createActiveEffect', onActiveEffectChanged);
Hooks.on('updateActiveEffect', onActiveEffectChanged);
Hooks.on('deleteActiveEffect', onActiveEffectChanged);
};
const onActiveEffectChanged = (activeEffect: ActiveEffect) => {
const onActiveEffectChanged = (activeEffect) => {
const game = getGame();
const parent = activeEffect.parent;
const actorId = parent?.id;

View File

@ -4,62 +4,76 @@
import { packageId } from '../../constants';
import type { TickwerkCombatant } from './combatant';
export const registerCombatFunctionality = () => {
CONFIG.Combat.documentClass = CombatMixin(CONFIG.Combat.documentClass);
};
const CombatMixin = (BaseCombat: typeof Combat) => {
/**
* Enhance a combat class with functionality for the Tickwerk.
* @param {typeof Combat} BaseCombat The combat class to enhance
* @returns A combat class, adapted for the Tickwerk
*/
const CombatMixin = (BaseCombat) => {
return class TickwerkCombat extends BaseCombat {
override get combatant() {
return this.turns[0];
/** @override */
get combatant() {
return this.turns?.[0];
}
override get round() {
return this.tickValue;
/** @override */
get nextCombatant() {
return this.turns?.[1];
}
override get started() {
return this.turns.length > 0 && (this.getFlag(packageId, 'started') ?? false);
}
override get turn() {
return 0;
/** @override */
get started() {
return (this.turns?.length ?? 0) > 0 && (this.getFlag(packageId, 'started') ?? false);
}
/**
* The current tick value of the Combat encounter.
* @type {number}
*/
get tickValue(): number {
get tickValue() {
const tickValues = this.combatants
.filter((combatant) => !combatant.isDefeated && !combatant.waiting)
.map((combatant) => combatant.initiative)
.filter((tickValue): tickValue is number => tickValue !== null);
.filter((tickValue) => tickValue !== null);
const tickValue = Math.min(...tickValues);
return tickValue === Infinity ? 0 : tickValue;
}
override async nextRound(): Promise<never> {
/** @override */
prepareDerivedData() {
super.prepareDerivedData();
this.turn = this.started ? 0 : null;
}
/** @override */
async nextRound() {
throw new Error('Not implemented!');
}
override async nextTurn() {
/** @override */
async nextTurn() {
await this.combatant?.advanceTicksDialog();
return this;
}
override previousRound(): Promise<never> {
/** @override */
previousRound() {
throw new Error('Not implemented!');
}
override previousTurn(): Promise<never> {
/** @override */
previousTurn() {
throw new Error('Not implemented!');
}
override async resetAll() {
/** @override */
async resetAll() {
for (const c of this.combatants) {
c.data.update({ initiative: null });
c.updateSource({ initiative: null });
}
return this.update(
{
@ -74,7 +88,8 @@ const CombatMixin = (BaseCombat: typeof Combat) => {
);
}
override setupTurns(): this['turns'] {
/** @override */
setupTurns() {
const turns = this.combatants.contents.sort(this._sortCombatants);
const c = turns[0];
@ -82,12 +97,13 @@ const CombatMixin = (BaseCombat: typeof Combat) => {
round: this.round,
turn: 0,
combatantId: c?.id ?? null,
tokenId: c?.data.tokenId ?? null,
tokenId: c?.tokenId ?? null,
};
return (this.turns = turns);
}
override async startCombat(): Promise<this | undefined> {
/** @override */
async startCombat() {
const hasCombatantWithTickValue = this.combatants.find(
(combatant) => !combatant.isDefeated && combatant.initiative !== null,
);
@ -95,10 +111,14 @@ const CombatMixin = (BaseCombat: typeof Combat) => {
ui.notifications?.warn('TICKWERK.WarningCannotStartCombat', { localize: true });
return this;
}
return this.setFlag(packageId, 'started', true);
this._playCombatSound('startEncounter');
const updateData = { flags: { [packageId]: { started: true } } };
Hooks.callAll('combatStart', this, updateData);
return this.update(updateData);
}
protected override _sortCombatants(a: TickwerkCombatant, b: TickwerkCombatant): number {
/** @override */
_sortCombatants(a, b) {
const da = a.isDefeated ? 1 : 0;
const db = b.isDefeated ? 1 : 0;
const cd = da - db;
@ -121,15 +141,36 @@ const CombatMixin = (BaseCombat: typeof Combat) => {
return (b.id ?? '') > (a.id ?? '') ? 1 : -1;
}
/** @override */
async _preUpdate(changed, options, user) {
delete changed.round;
delete changed.turn;
options.tickwerk = {
combatantBeforeUpdate: this.combatant?.id,
nextCombatantBeforeUpdate: this.nextCombatant?.id,
};
return super._preUpdate(changed, options, user);
}
/** @override */
_onUpdate(data, options, userId) {
super._onUpdate(data, options, userId);
if (game.user.character) {
const { combatantBeforeUpdate, nextCombatantBeforeUpdate } = options.tickwerk ?? {};
if (
combatantBeforeUpdate !== undefined &&
combatantBeforeUpdate !== this.combatant &&
this.combatant?.actorId === game.user.character._id
) {
this._playCombatSound('yourTurn');
} else if (
nextCombatantBeforeUpdate !== undefined &&
nextCombatantBeforeUpdate !== this.nextCombatant &&
this.nextCombatant?.actorId === game.user.character._id
)
this._playCombatSound('nextUp');
}
}
};
};
declare global {
interface FlagConfig {
Combat: {
tickwerk?: {
started?: boolean;
};
};
}
}

View File

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT
import type { CombatantDataConstructorData } from '@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/data/data.mjs/combatantData';
import { packageId } from '../../constants';
import { getGame } from '../../helpers';
@ -10,39 +9,49 @@ export const registerCombatantFunctionality = () => {
CONFIG.Combatant.documentClass = CombatantMixin(CONFIG.Combatant.documentClass);
};
const CombatantMixin = (BaseCombatant: typeof Combatant) => {
/**
* Enhance a co,batant class with functionality for the Tickwerk.
* @param {typeof Combatant} BaseCombatant The combat class to enhance
* @returns A combat class, adapted for the Tickwerk
*/
const CombatantMixin = (BaseCombatant) => {
return class TickwerkCombatant extends BaseCombatant {
/**
* An temporary property to make changes to the initiative available to other instances in their `_pre…` methods.
* @type {number|null|undefined}
*/
_newInitiative: number | null | undefined;
_newInitiative;
/**
* An temporary property to make changes to the tiebreaker available to other instances in their `_pre…` methods.
* @type {number|undefined}
*/
_newTiebreaker: number | undefined;
_newTiebreaker;
/***
* Is this combatant currently waiting?
* @type {boolean}
*/
get waiting(): boolean {
get waiting() {
return this.getFlag(packageId, 'waiting') ?? false;
}
/**
* Toggle the waiting state of this combatant.
* @returns {Promise<this|undefined>} The updated combatant
*/
toggleWaiting(): Promise<this | undefined> {
const update: Record<string, unknown> = { [`flags.${packageId}.waiting`]: !this.waiting };
toggleWaiting() {
const update = { [`flags.${packageId}.waiting`]: !this.waiting };
if (this.parent?.started && this.waiting) update.initiative = this.parent?.round;
return this.update(update);
}
/**
* Advance for the given number of ticks.
* @param ticks The number of ticks to advance for
* @param {number} ticks The number of ticks to advance for
* @returns {Promise<void>} A promise that resolves once when the combatant has advanced
*/
async advanceTicks(ticks: number): Promise<void> {
async advanceTicks(ticks) {
if (this.initiative === null) {
ui.notifications?.warn('TICKWERK.WarningCannotAdvanceWithoutTickValue', { localize: true });
return;
@ -59,11 +68,15 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
await this.update({ initiative: this.initiative + ticks });
const advanceTime = ticks * CONFIG.time.roundTime;
if (advanceTime !== 0) {
await this.combat?.update(undefined, { diff: false, advanceTime } as DocumentModificationContext);
await this.combat?.update(undefined, { diff: false, advanceTime });
}
}
async advanceTicksDialog(): Promise<void> {
/**
* Show a dialog for advancing the combatant a certain number of ticks.
* @returns {Promise<void>} A promise that resolves when the dialog has been confirmed and the combatant has advanced
*/
async advanceTicksDialog() {
const game = getGame();
const id = foundry.utils.randomID();
@ -76,14 +89,14 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
title: game.i18n.localize('TICKWERK.AdvanceTicks'),
content: form,
yes: (html) => {
const ticks = html[0]?.querySelector<HTMLInputElement>('input[name="ticks"]')?.value;
const ticks = html[0]?.querySelector('input[name="ticks"]')?.value;
const parsedTicks = ticks !== undefined ? parseInt(ticks) : undefined;
return Number.isSafeInteger(parsedTicks) ? parsedTicks : undefined;
return Number.isSafeInteger(parsedTicks) ? parsedTicks : NaN;
},
rejectClose: false,
});
if (ticks === undefined) {
if (Number.isNaN(ticks)) {
ui.notifications?.warn('TICKWERK.WarningInvalidNumberOfTicks', { localize: true });
return;
}
@ -95,9 +108,9 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
/**
* Update tiebreaker data for a given creation or update.
* @param data The data of the creation / update
* @param {object} data The data of the creation / update
*/
async #updateTiebreakerData(data: DeepPartial<CombatantDataConstructorData>): Promise<void> {
async #updateTiebreakerData(data) {
if ('initiative' in data) {
const combatantsWithSameTickValue =
this.parent?.combatants.filter((combatant) => {
@ -106,7 +119,7 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
return otherInitiative === data.initiative;
}) ?? [];
const tiebreaker = await this.#getTiebreaker(combatantsWithSameTickValue);
setProperty(data, `flags.${packageId}.tiebreaker`, tiebreaker);
foundry.utils.setProperty(data, `flags.${packageId}.tiebreaker`, tiebreaker);
this._newInitiative = data.initiative;
this._newTiebreaker = tiebreaker;
}
@ -114,46 +127,48 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
/**
* Get a tiebreaker between this combatant and the given other combatants.
* @param combatants The other combatants among which to find a tiebreaker
* @returns A promise that resolves to the tiebreaker
* @param {TickwerkCombatant[]} combatants The other combatants among which to find a tiebreaker
* @returns {Promise<number>} A promise that resolves to the tiebreaker
*/
async #getTiebreaker(combatants: TickwerkCombatant[]): Promise<number> {
async #getTiebreaker(combatants) {
const getTiebreaker = CONFIG.tickwerk?.getTiebreaker ?? defaultGetTiebreaker;
return getTiebreaker(this, combatants);
}
override testUserPermission(
user: foundry.documents.BaseUser,
permission: keyof typeof foundry.CONST.DOCUMENT_PERMISSION_LEVELS | foundry.CONST.DOCUMENT_PERMISSION_LEVELS,
{ exact }: { exact?: boolean | undefined } = {},
): boolean {
/** @override */
testUserPermission(user, permission, { exact } = {}) {
if (user.isGM) return true;
return super.testUserPermission(user, permission, { exact });
}
protected override _getInitiativeFormula(): string {
/** @override */
_getInitiativeFormula() {
const getInitiativeFormula = CONFIG.tickwerk?.getInitiativeFormula;
if (getInitiativeFormula) return getInitiativeFormula(this);
return super._getInitiativeFormula();
}
protected override async _preCreate(...args: Parameters<Combatant['_preCreate']>): Promise<void> {
/** @override */
async _preCreate(...args) {
await super._preCreate(...args);
await this.#updateTiebreakerData(args[0]);
}
protected override async _preUpdate(...args: Parameters<Combatant['_preUpdate']>): Promise<void> {
/** @override */
async _preUpdate(...args) {
await super._preUpdate(...args);
await this.#updateTiebreakerData(args[0]);
}
protected override _onCreate(...args: Parameters<Combatant['_onCreate']>): void {
/** @override */
_onCreate(...args) {
super._onCreate(...args);
this._newInitiative = undefined;
this._newTiebreaker = undefined;
}
protected override _onUpdate(...args: Parameters<Combatant['_onUpdate']>): void {
/** @override */
_onUpdate(...args) {
super._onUpdate(...args);
this._newInitiative = undefined;
this._newTiebreaker = undefined;
@ -161,7 +176,16 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
};
};
const defaultGetTiebreaker = async (combatant: TickwerkCombatant, combatants: TickwerkCombatant[]): Promise<number> => {
/**
* A function to get a tiebreaker for a combatant
* @typedef {(combatant: TickwerkCombatant, combatants: TickwerkCombatant[]) => Promise<number>} GetTiebreaker
*/
/**
* Default implementation to get a tiebreaker for a combatant.
* @type {GetTiebreaker}
*/
const defaultGetTiebreaker = async (combatant, combatants) => {
if (combatants.length === 0) return 0;
const tiebreakers = combatants.map((combatant) => {
return (
@ -172,28 +196,3 @@ const defaultGetTiebreaker = async (combatant: TickwerkCombatant, combatants: Ti
});
return Math.max(...tiebreakers) + 1;
};
export type TickwerkCombatantConstructor = ReturnType<typeof CombatantMixin>;
export type TickwerkCombatant = InstanceType<TickwerkCombatantConstructor>;
declare global {
interface FlagConfig {
Combatant: {
tickwerk: {
tiebreaker?: number | undefined;
waiting?: boolean | undefined;
};
};
}
interface DocumentClassConfig {
Combatant: TickwerkCombatantConstructor;
}
interface CONFIG {
tickwerk?: {
getTiebreaker?: (combatant: TickwerkCombatant, combatants: TickwerkCombatant[]) => Promise<number>;
getInitiativeFormula?: (combatant: TickwerkCombatant) => string;
};
}
}

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
export const getGame = (): Game => {
export const getGame = () => {
if (!(game instanceof Game)) {
throw new Error('game is not initialized yet.');
}

View File

@ -5,7 +5,6 @@
import { packageId } from '../constants';
import { getGame } from '../helpers';
import type { TickwerkCombatant } from '../data/documents/combatant';
export const registerDS4SpecificFunctionality = () => {
if (CONFIG.tickwerk === undefined) CONFIG.tickwerk = {};
foundry.utils.mergeObject(CONFIG.tickwerk, { getTiebreaker, getInitiativeFormula });
@ -14,20 +13,22 @@ export const registerDS4SpecificFunctionality = () => {
Hooks.on('ds4.rollItem', onRollItem);
};
const getTiebreaker = async (combatant: TickwerkCombatant, combatants: TickwerkCombatant[]): Promise<number> => {
const ds4combatant = combatant as DS4TickwerkCombatant;
const ds4combatants = combatants as DS4TickwerkCombatant[];
/** @type {import("../data/documents/combatant").GetTiebreaker} */
const getTiebreaker = async (combatant, combatants) => {
if (combatants.length === 0) return 0;
const lowerBounds: number[] = [];
const upperBounds: number[] = [];
const equals: number[] = [];
/** @type {number[]} */
const lowerBounds = [];
/** @type {number[]} */
const upperBounds = [];
/** @type {number[]} */
const equals = [];
for (const combatant of ds4combatants) {
const tiebreaker = combatant._newTiebreaker ?? combatant.getFlag(packageId, 'tiebreaker') ?? 0;
if (getInitiative(combatant) > getInitiative(ds4combatant)) {
for (const other of combatants) {
const tiebreaker = other._newTiebreaker ?? other.getFlag(packageId, 'tiebreaker') ?? 0;
if (getInitiative(other) > getInitiative(combatant)) {
lowerBounds.push(tiebreaker);
} else if (getInitiative(combatant) < getInitiative(ds4combatant)) {
} else if (getInitiative(other) < getInitiative(combatant)) {
upperBounds.push(tiebreaker);
} else {
equals.push(tiebreaker);
@ -62,37 +63,32 @@ const getTiebreaker = async (combatant: TickwerkCombatant, combatants: TickwerkC
}
};
const getInitiativeFormula = (combatant: TickwerkCombatant) => {
/**
* Get the initiative formula for a combatant.
* @param {TickwerkCombatant} combatant The combatant for which to get the initiative formula
* @returns {string} The initiative formula
*/
const getInitiativeFormula = (combatant) => {
const started = combatant.combat?.started ?? false;
if (!started) return '-@combatValues.initiative.total';
const tickValue = combatant.combat?.round ?? 0;
return `max(${tickValue} + 10 - @combatValues.initiative.total, ${tickValue})`;
};
type DS4TickwerkCombatant = TickwerkCombatant & { actor: (Actor & { data: { data: ActorData } }) | null };
const getInitiative = (combatant: DS4TickwerkCombatant): number => {
return combatant.actor?.data.data.combatValues.initiative.total ?? -Infinity;
/**
* Get the initiative for a combatant.
* @param {TickwerkCombatant} combatant The combatant for which to get the initiative
* @returns {number}
*/
const getInitiative = (combatant) => {
return combatant.actor?.system.combatValues.initiative.total ?? -Infinity;
};
interface ActorData {
combatValues: {
initiative: {
total: number;
};
};
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Hooks {
interface StaticCallbacks {
'ds4.rollItem': (item: Item) => void;
}
}
}
const onRollItem = (item: Item) => {
/**
* React to an item roll by prompting the user to advance ticks.
* @param {Item} item The item that has been rolled
*/
const onRollItem = (item) => {
const game = getGame();
if (game.settings.get(packageId, 'ds4.reactToRollItemHook')) {
if (['weapon', 'spell'].includes(item.type) && item.actor?.id) {

View File

@ -1,225 +0,0 @@
{
"Actor": {
"types": ["character", "creature"],
"templates": {
"base": {
"attributes": {
"body": {
"base": 0,
"mod": 0
},
"mobility": {
"base": 0,
"mod": 0
},
"mind": {
"base": 0,
"mod": 0
}
},
"traits": {
"strength": {
"base": 0,
"mod": 0
},
"constitution": {
"base": 0,
"mod": 0
},
"agility": {
"base": 0,
"mod": 0
},
"dexterity": {
"base": 0,
"mod": 0
},
"intellect": {
"base": 0,
"mod": 0
},
"aura": {
"base": 0,
"mod": 0
}
},
"combatValues": {
"hitPoints": {
"mod": 0,
"value": 0
},
"defense": {
"mod": 0
},
"initiative": {
"mod": 0
},
"movement": {
"mod": 0
},
"meleeAttack": {
"mod": 0
},
"rangedAttack": {
"mod": 0
},
"spellcasting": {
"mod": 0
},
"targetedSpellcasting": {
"mod": 0
}
}
}
},
"creature": {
"templates": ["base"],
"baseInfo": {
"loot": "",
"foeFactor": 1,
"creatureType": "humanoid",
"sizeCategory": "normal",
"experiencePoints": 0,
"description": ""
}
},
"character": {
"templates": ["base"],
"baseInfo": {
"race": "",
"class": "",
"heroClass": "",
"culture": ""
},
"progression": {
"level": 0,
"experiencePoints": 0,
"talentPoints": {
"total": 0,
"used": 0
},
"progressPoints": {
"total": 0,
"used": 0
}
},
"profile": {
"biography": "",
"gender": "",
"birthday": "",
"birthplace": "",
"age": 0,
"height": 0,
"hairColor": "",
"weight": 0,
"eyeColor": "",
"specialCharacteristics": ""
},
"currency": {
"gold": 0,
"silver": 0,
"copper": 0
},
"slayerPoints": {
"value": 0
}
}
},
"Item": {
"types": [
"weapon",
"armor",
"shield",
"spell",
"equipment",
"loot",
"talent",
"racialAbility",
"language",
"alphabet",
"specialCreatureAbility"
],
"templates": {
"base": {
"description": ""
},
"physical": {
"quantity": 1,
"price": 0,
"availability": "unset",
"storageLocation": "-"
},
"equipable": {
"equipped": false
},
"protective": {
"armorValue": 0
}
},
"weapon": {
"templates": ["base", "physical", "equipable"],
"attackType": "melee",
"weaponBonus": 0,
"opponentDefense": 0
},
"armor": {
"templates": ["base", "physical", "equipable", "protective"],
"armorMaterialType": "cloth",
"armorType": "body"
},
"shield": {
"templates": ["base", "physical", "equipable", "protective"]
},
"spell": {
"templates": ["base", "equipable"],
"spellType": "spellcasting",
"bonus": "",
"spellCategory": "unset",
"maxDistance": {
"value": "",
"unit": "meter"
},
"effectRadius": {
"value": "",
"unit": "meter"
},
"duration": {
"value": "",
"unit": "custom"
},
"cooldownDuration": "0r",
"minimumLevels": {
"healer": null,
"wizard": null,
"sorcerer": null
}
},
"equipment": {
"templates": ["base", "physical", "equipable"]
},
"loot": {
"templates": ["base", "physical"]
},
"talent": {
"templates": ["base"],
"rank": {
"base": 0,
"max": 0,
"mod": 0
}
},
"racialAbility": {
"templates": ["base"]
},
"language": {
"templates": ["base"]
},
"alphabet": {
"templates": ["base"]
},
"specialCreatureAbility": {
"templates": ["base"],
"experiencePoints": 0
}
}
}

View File

@ -1,6 +0,0 @@
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Oliver Rümpelein
SPDX-FileCopyrightText: 2021 Gesina Schwalbe
SPDX-FileCopyrightText: 2021 Siegfried Krug
SPDX-License-Identifier: MIT

View File

@ -5,82 +5,89 @@ SPDX-FileCopyrightText: 2022 Johannes Loher
SPDX-License-Identifier: MIT
--}}
<section class="tab sidebar-tab directory flexcol" id="combat" data-tab="combat">
<header id="combat-round">
<section class="{{cssClass}} directory flexcol" id="{{cssId}}" data-tab="{{tabName}}">
<header class="combat-tracker-header">
{{#if user.isGM}}
<nav class="encounters flexrow">
<a class="combat-create" title="{{localize 'COMBAT.Create'}}">
<nav class="encounters flexrow" aria-label="COMBAT.NavLabel">
<a class="combat-button combat-create" data-tooltip="COMBAT.Create">
<i class="fas fa-plus"></i>
</a>
{{#if combatCount}}
<a class="combat-cycle" title="{{localize 'COMBAT.EncounterPrevious'}}"
{{#if previousId}}data-combat-id="{{previousId}}"{{else}}disabled{{/if}}>
<a class="combat-button combat-cycle" data-tooltip="COMBAT.EncounterPrevious"
{{#if previousId}}data-document-id="{{previousId}}"{{else}}disabled{{/if}}>
<i class="fas fa-caret-left"></i>
</a>
<h4 class="encounter">{{localize "COMBAT.Encounter"}} {{currentIndex}} / {{combatCount}}</h4>
<a class="combat-cycle" title="{{localize 'COMBAT.EncounterNext'}}"
{{#if nextId}}data-combat-id="{{nextId}}"{{else}}disabled{{/if}}>
<a class="combat-button combat-cycle" data-tooltip="COMBAT.EncounterNext"
{{#if nextId}}data-document-id="{{nextId}}"{{else}}disabled{{/if}}>
<i class="fas fa-caret-right"></i>
</a>
{{/if}}
<a class="combat-control" title="{{localize 'COMBAT.Delete'}}" data-control="endCombat" {{#unless combatCount}}disabled{{/unless}}>
<a class="combat-button combat-control" data-tooltip="COMBAT.Delete" data-control="endCombat" {{#unless combatCount}}disabled{{/unless}}>
<i class="fas fa-trash"></i>
</a>
</nav>
{{/if}}
<nav class="encounters flexrow {{#if hasCombat}}combat{{/if}}">
<div class="encounter-controls flexrow {{#if hasCombat}}combat{{/if}}">
{{#if user.isGM}}
<a class="combat-control" title="{{localize 'COMBAT.RollAll'}}" data-control="rollAll" {{#unless turns}}disabled{{/unless}}>
<a class="combat-button combat-control" data-tooltip="COMBAT.RollAll" data-control="rollAll" {{#unless turns}}disabled{{/unless}}>
<i class="fas fa-users"></i>
</a>
<a class="combat-control" title="{{localize 'COMBAT.RollNPC'}}" data-control="rollNPC" {{#unless turns}}disabled{{/unless}}>
<a class="combat-button combat-control" data-tooltip="COMBAT.RollNPC" data-control="rollNPC" {{#unless turns}}disabled{{/unless}}>
<i class="fas fa-users-cog"></i>
</a>
{{/if}}
{{#if combatCount}}
{{#if combat.started}}
<h3 class="encounter-title">{{localize 'TICKWERK.Tick'}} {{combat.tickValue}}</h3>
<h3 class="encounter-title noborder">{{localize 'TICKWERK.Tick'}} {{combat.tickValue}}</h3>
{{else}}
<h3 class="encounter-title">{{localize 'COMBAT.NotStarted'}}</h3>
<h3 class="encounter-title noborder">{{localize 'COMBAT.NotStarted'}}</h3>
{{/if}}
{{else}}
<h3 class="encounter-title">{{localize "COMBAT.None"}}</h3>
<h3 class="encounter-title noborder">{{localize "COMBAT.None"}}</h3>
{{/if}}
{{#if user.isGM}}
<a class="combat-control" title="{{localize 'COMBAT.InitiativeReset'}}" data-control="resetAll"
<a class="combat-button combat-control" data-tooltip="COMBAT.InitiativeReset" data-control="resetAll"
{{#unless hasCombat}}disabled{{/unless}}>
<i class="fas fa-undo"></i>
</a>
<a class="combat-control" title="{{labels.scope}}"
<a class="combat-button combat-control" data-tooltip="{{labels.scope}}"
data-control="toggleSceneLink" {{#unless hasCombat}}disabled{{/unless}}>
<i class="fas fa-{{#unless linked}}un{{/unless}}link"></i>
</a>
<a class="combat-settings" title="{{localize 'COMBAT.Settings'}}" data-control="trackerSettings">
<a class="combat-button combat-settings" data-tooltip="COMBAT.Settings" data-control="trackerSettings">
<i class="fas fa-cog"></i>
</a>
{{/if}}
</nav>
</div>
</header>
<ol id="combat-tracker" class="directory-list">
{{#each turns}}
<li class="combatant actor directory-item flexrow {{this.css}}" data-combatant-id="{{this.id}}">
<img class="token-image" data-src="{{this.img}}" title="{{this.name}}"/>
<img class="token-image" data-src="{{this.img}}" alt="{{this.name}}"/>
<div class="token-name flexcol">
<h4>{{this.name}}</h4>
<div class="combatant-controls flexrow">
{{#if (or ../user.isGM (and ../control this.owner))}}
<a class="combatant-control" title="{{#if this.waiting}}{{localize 'TICKWERK.StopWaiting'}}{{else}}{{localize 'TICKWERK.Wait'}}{{/if}}" data-control="toggleWaiting">
<a class="combatant-control" data-tooltip="{{#if this.waiting}}TICKWERK.StopWaiting{{else}}TICKWERK.Wait{{/if}}" data-control="toggleWaiting">
<i class="fas {{#if this.waiting}}fa-play-circle{{else}}fa-pause-circle{{/if}}"></i></a>
{{/if}}
{{#if ../user.isGM}}
<a class="combatant-control {{#if this.hidden}}active{{/if}}" title="{{localize 'COMBAT.ToggleVis'}}" data-control="toggleHidden">
<i class="fas fa-eye-slash"></i></a>
<a class="combatant-control {{#if this.defeated}}active{{/if}}" title="{{localize 'COMBAT.ToggleDead'}}" data-control="toggleDefeated">
<i class="fas fa-skull"></i></a>
<a class="combatant-control {{#if this.hidden}}active{{/if}}" data-tooltip="COMBAT.ToggleVis" data-control="toggleHidden">
<i class="fas fa-eye-slash"></i>
</a>
<a class="combatant-control {{#if this.defeated}}active{{/if}}" data-tooltip="COMBAT.ToggleDead" data-control="toggleDefeated">
<i class="fas fa-skull"></i>
</a>
{{/if}}
{{#if this.canPing}}
<a class="combatant-control" data-tooltip="COMBAT.PingCombatant" data-control="pingCombatant">
<i class="fa-solid fa-bullseye-arrow"></i>
</a>
{{/if}}
<div class="token-effects">
{{#each this.effects}}
@ -100,25 +107,25 @@ SPDX-License-Identifier: MIT
{{#if this.hasRolled}}
<span class="initiative">{{#if this.waiting}}{{localize "TICKWERK.Waiting"}}{{else}}{{this.initiative}}{{/if}}</span>
{{else if this.owner}}
<a class="combatant-control roll" title="{{localize 'COMBAT.InitiativeRoll'}}" data-control="rollInitiative"></a>
<a class="combatant-control roll" data-tooltip="COMBAT.InitiativeRoll" data-control="rollInitiative"></a>
{{/if}}
</div>
</li>
{{/each}}
</ol>
<nav id="combat-controls" class="directory-footer flexrow">
<nav id="combat-controls" class="directory-footer flexrow" data-tooltip-direction="UP">
{{#if hasCombat}}
{{#if user.isGM}}
{{#if combat.started}}
<a class="combat-control center" title="{{localize 'COMBAT.End'}}" data-control="endCombat">{{localize 'COMBAT.End'}}</a>
<a class="combat-control" title="{{localize 'COMBAT.TurnNext'}}" data-control="nextTurn"><i class="fas fa-arrow-right"></i></a>
<a class="combat-control center" data-control="endCombat">{{localize 'COMBAT.End'}}</a>
<a class="combat-control" data-tooltip="COMBAT.TurnNext" data-control="nextTurn"><i class="fas fa-arrow-right"></i></a>
{{else}}
<a class="combat-control center" title="{{localize 'COMBAT.Begin'}}" data-control="startCombat">{{localize 'COMBAT.Begin'}}</a>
<a class="combat-control center" data-control="startCombat">{{localize 'COMBAT.Begin'}}</a>
{{/if}}
{{else if (and control combat.started)}}
<a class="combat-control center" title="{{localize 'COMBAT.TurnEnd'}}" data-control="nextTurn">{{localize 'COMBAT.TurnEnd'}}</a>
<a class="combat-control" title="{{localize 'COMBAT.TurnNext'}}" data-control="nextTurn"><i class="fas fa-arrow-right"></i></a>
<a class="combat-control center" data-control="nextTurn">{{localize 'COMBAT.TurnEnd'}}</a>
<a class="combat-control" data-tooltip="COMBAT.TurnNext" data-control="nextTurn"><i class="fas fa-arrow-right"></i></a>
{{/if}}
{{/if}}
</nav>

View File

@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"include": ["src", "*.js"]
}

View File

@ -1,3 +0,0 @@
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-License-Identifier: MIT

View File

@ -1,16 +0,0 @@
{
"compilerOptions": {
"target": "ES2021",
"lib": ["ES2021", "DOM"],
"types": ["@league-of-foundry-developers/foundry-vtt-types"],
"esModuleInterop": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"resolveJsonModule": true,
"importsNotUsedAsValues": "error"
},
"include": ["src"]
}

1148
yarn.lock

File diff suppressed because it is too large Load Diff