Merge branch 'v10' into 'main'
feat: update for v10 See merge request dungeonslayers/tickwerk!15
This commit is contained in:
commit
4ef42f4fa8
34 changed files with 271 additions and 2147 deletions
|
@ -3,29 +3,36 @@
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
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: {},
|
rules: {},
|
||||||
|
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['./*.cjs', './*.js'],
|
files: ['./*.cjs', './*.js', './tools/**/*'],
|
||||||
rules: {
|
env: { node: true, browser: false, jquery: false },
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -14,6 +14,8 @@ npm-debug.log
|
||||||
|
|
||||||
# Local configuration
|
# Local configuration
|
||||||
foundryconfig.json
|
foundryconfig.json
|
||||||
|
/common
|
||||||
|
/client
|
||||||
|
|
||||||
# Distribution files
|
# Distribution files
|
||||||
dist
|
dist
|
||||||
|
|
|
@ -30,15 +30,6 @@ lint:
|
||||||
cache:
|
cache:
|
||||||
<<: *global_cache
|
<<: *global_cache
|
||||||
|
|
||||||
typecheck:
|
|
||||||
stage: check
|
|
||||||
before_script:
|
|
||||||
- yarn install --immutable
|
|
||||||
script:
|
|
||||||
- yarn typecheck
|
|
||||||
cache:
|
|
||||||
<<: *global_cache
|
|
||||||
|
|
||||||
reuse:
|
reuse:
|
||||||
stage: check
|
stage: check
|
||||||
image:
|
image:
|
||||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -5,8 +5,6 @@
|
||||||
},
|
},
|
||||||
"eslint.nodePath": ".yarn/sdks",
|
"eslint.nodePath": ".yarn/sdks",
|
||||||
"prettier.prettierPath": ".yarn/sdks/prettier/index.js",
|
"prettier.prettierPath": ".yarn/sdks/prettier/index.js",
|
||||||
"typescript.tsdk": ".yarn/sdks/typescript/lib",
|
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
|
||||||
"importSorter.generalConfiguration.sortOnBeforeSave": true,
|
"importSorter.generalConfiguration.sortOnBeforeSave": true,
|
||||||
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.type": "newLineEachExpressionAfterCountLimitExceptIfOnlyOne",
|
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.type": "newLineEachExpressionAfterCountLimitExceptIfOnlyOne",
|
||||||
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 120,
|
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 120,
|
||||||
|
|
2
.yarn/sdks/eslint/package.json
vendored
2
.yarn/sdks/eslint/package.json
vendored
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "eslint",
|
"name": "eslint",
|
||||||
"version": "8.25.0-sdk",
|
"version": "8.28.0-sdk",
|
||||||
"main": "./lib/api.js",
|
"main": "./lib/api.js",
|
||||||
"type": "commonjs"
|
"type": "commonjs"
|
||||||
}
|
}
|
||||||
|
|
2
.yarn/sdks/prettier/package.json
vendored
2
.yarn/sdks/prettier/package.json
vendored
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "prettier",
|
"name": "prettier",
|
||||||
"version": "2.7.1-sdk",
|
"version": "2.8.0-sdk",
|
||||||
"main": "./index.js",
|
"main": "./index.js",
|
||||||
"type": "commonjs"
|
"type": "commonjs"
|
||||||
}
|
}
|
||||||
|
|
20
.yarn/sdks/typescript/bin/tsc
vendored
20
.yarn/sdks/typescript/bin/tsc
vendored
|
@ -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`);
|
|
20
.yarn/sdks/typescript/bin/tsserver
vendored
20
.yarn/sdks/typescript/bin/tsserver
vendored
|
@ -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`);
|
|
20
.yarn/sdks/typescript/lib/tsc.js
vendored
20
.yarn/sdks/typescript/lib/tsc.js
vendored
|
@ -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`);
|
|
223
.yarn/sdks/typescript/lib/tsserver.js
vendored
223
.yarn/sdks/typescript/lib/tsserver.js
vendored
|
@ -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`));
|
|
223
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
223
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
|
@ -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`));
|
|
20
.yarn/sdks/typescript/lib/typescript.js
vendored
20
.yarn/sdks/typescript/lib/typescript.js
vendored
|
@ -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`);
|
|
6
.yarn/sdks/typescript/package.json
vendored
6
.yarn/sdks/typescript/package.json
vendored
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "typescript",
|
|
||||||
"version": "4.7.4-sdk",
|
|
||||||
"main": "./lib/typescript.js",
|
|
||||||
"type": "commonjs"
|
|
||||||
}
|
|
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "es2022",
|
||||||
|
"target": "ES2022"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"include": ["src", "client", "common"]
|
||||||
|
}
|
3
jsconfig.json.license
Normal file
3
jsconfig.json.license
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||||
|
|
||||||
|
SPDX-License-Identifier: MIT
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "tickwerk",
|
"id": "tickwerk",
|
||||||
"title": "Tickwerk",
|
"title": "Tickwerk",
|
||||||
"description": "A tick based combat system for Foundry Virtual Tabletop",
|
"description": "A tick based combat system for Foundry Virtual Tabletop",
|
||||||
"authors": [
|
"authors": [
|
||||||
|
@ -16,8 +16,10 @@
|
||||||
"bugs": "https://git.f3l.de/dungeonslayers/tickwerk/-/issues",
|
"bugs": "https://git.f3l.de/dungeonslayers/tickwerk/-/issues",
|
||||||
"changelog": "https://git.f3l.de/dungeonslayers/tickwerk/-/releases/1.2.1",
|
"changelog": "https://git.f3l.de/dungeonslayers/tickwerk/-/releases/1.2.1",
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"minimumCoreVersion": "9",
|
"compatibility": {
|
||||||
"compatibleCoreVersion": "9",
|
"minimum": "10.290",
|
||||||
|
"verified": "10"
|
||||||
|
},
|
||||||
"esmodules": ["tickwerk.js"],
|
"esmodules": ["tickwerk.js"],
|
||||||
"styles": ["styles/tickwerk.css"],
|
"styles": ["styles/tickwerk.css"],
|
||||||
"languages": [
|
"languages": [
|
||||||
|
|
26
package.json
26
package.json
|
@ -27,11 +27,9 @@
|
||||||
"clean": "run-p clean:files clean:link",
|
"clean": "run-p clean:files clean:link",
|
||||||
"clean:files": "rimraf dist",
|
"clean:files": "rimraf dist",
|
||||||
"clean:link": "node ./tools/link-package.js --clean",
|
"clean:link": "node ./tools/link-package.js --clean",
|
||||||
"lint": "eslint --ext .ts,.js,.cjs,.mjs .",
|
"lint": "eslint --ext .js,.cjs,.mjs .",
|
||||||
"lint:fix": "eslint --ext .ts,.js,.cjs,.mjs --fix .",
|
"lint:fix": "eslint --ext .js,.cjs,.mjs --fix .",
|
||||||
"format": "prettier --write \"./**/*.(ts|js|cjs|mjs|json|scss|yml)\"",
|
"format": "prettier --write \"./**/*.(js|cjs|mjs|json|scss|yml)\"",
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"typecheck:watch": "tsc --noEmit --watch",
|
|
||||||
"bump-version": "node ./tools/bump-version.js",
|
"bump-version": "node ./tools/bump-version.js",
|
||||||
"postinstall": "husky install",
|
"postinstall": "husky install",
|
||||||
"changelog": "conventional-changelog -p conventionalcommits -o CHANGELOG.md -r 2"
|
"changelog": "conventional-changelog -p conventionalcommits -o CHANGELOG.md -r 2"
|
||||||
|
@ -40,20 +38,7 @@
|
||||||
"@commitlint/cli": "17.3.0",
|
"@commitlint/cli": "17.3.0",
|
||||||
"@commitlint/config-conventional": "17.3.0",
|
"@commitlint/config-conventional": "17.3.0",
|
||||||
"@guanghechen/rollup-plugin-copy": "2.1.4",
|
"@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",
|
"@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-cli": "2.2.2",
|
||||||
"conventional-changelog-conventionalcommits": "5.0.0",
|
"conventional-changelog-conventionalcommits": "5.0.0",
|
||||||
"eslint": "8.28.0",
|
"eslint": "8.28.0",
|
||||||
|
@ -70,13 +55,10 @@
|
||||||
"rollup-plugin-swc3": "0.7.0",
|
"rollup-plugin-swc3": "0.7.0",
|
||||||
"sass": "1.56.1",
|
"sass": "1.56.1",
|
||||||
"semver": "7.3.8",
|
"semver": "7.3.8",
|
||||||
"ts-node": "10.9.1",
|
|
||||||
"tslib": "2.4.1",
|
|
||||||
"typescript": "4.7.4",
|
|
||||||
"yargs": "17.6.2"
|
"yargs": "17.6.2"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.(ts|js|cjs|mjs)": "eslint --cache --fix",
|
"*.(js|cjs|mjs)": "eslint --cache --fix",
|
||||||
"*.(json|scss|yml)": "prettier --write"
|
"*.(json|scss|yml)": "prettier --write"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.2.4"
|
"packageManager": "yarn@3.2.4"
|
||||||
|
|
|
@ -24,7 +24,7 @@ const isProduction = process.env.NODE_ENV === 'production';
|
||||||
* @type {import('rollup').RollupOptions}
|
* @type {import('rollup').RollupOptions}
|
||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
input: { [name]: `${sourceDirectory}/${name}.ts` },
|
input: { [name]: `${sourceDirectory}/${name}.js` },
|
||||||
output: {
|
output: {
|
||||||
dir: distDirectory,
|
dir: distDirectory,
|
||||||
format: 'es',
|
format: 'es',
|
||||||
|
|
|
@ -3,35 +3,43 @@
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
export const registerCombatTrackerFunctionality = () => {
|
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 {
|
return class TickwerkCombatTracker extends BaseCombatTracker {
|
||||||
static override get defaultOptions(): ApplicationOptions {
|
/** @override */
|
||||||
|
static get defaultOptions() {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
template: 'modules/tickwerk/templates/combat-tracker.hbs',
|
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);
|
const data = await super.getData(options);
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
turns: data.turns.map((turn) => ({ ...turn, waiting: this.viewed?.combatants.get(turn.id)?.waiting })),
|
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);
|
super.activateListeners(html);
|
||||||
html.find('.combatant-control[data-control="toggleWaiting"]').on('click', this._onToggleWaiting.bind(this));
|
html.find('.combatant-control[data-control="toggleWaiting"]').on('click', this._onToggleWaiting.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle clicks on the Combatant waiting control button.
|
* 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.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const button = event.currentTarget;
|
const button = event.currentTarget;
|
|
@ -2,4 +2,4 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
export const packageId = 'tickwerk' as const;
|
export const packageId = 'tickwerk';
|
|
@ -5,12 +5,12 @@
|
||||||
import { getGame } from '../../helpers';
|
import { getGame } from '../../helpers';
|
||||||
|
|
||||||
export const registerActiveEffectFunctionality = () => {
|
export const registerActiveEffectFunctionality = () => {
|
||||||
Hooks.on<Hooks.CreateDocument<typeof ActiveEffect>>('createActiveEffect', onActiveEffectChanged);
|
Hooks.on('createActiveEffect', onActiveEffectChanged);
|
||||||
Hooks.on<Hooks.UpdateDocument<typeof ActiveEffect>>('updateActiveEffect', onActiveEffectChanged);
|
Hooks.on('updateActiveEffect', onActiveEffectChanged);
|
||||||
Hooks.on<Hooks.DeleteDocument<typeof ActiveEffect>>('deleteActiveEffect', onActiveEffectChanged);
|
Hooks.on('deleteActiveEffect', onActiveEffectChanged);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onActiveEffectChanged = (activeEffect: ActiveEffect) => {
|
const onActiveEffectChanged = (activeEffect) => {
|
||||||
const game = getGame();
|
const game = getGame();
|
||||||
const parent = activeEffect.parent;
|
const parent = activeEffect.parent;
|
||||||
const actorId = parent?.id;
|
const actorId = parent?.id;
|
|
@ -4,62 +4,76 @@
|
||||||
|
|
||||||
import { packageId } from '../../constants';
|
import { packageId } from '../../constants';
|
||||||
|
|
||||||
import type { TickwerkCombatant } from './combatant';
|
|
||||||
|
|
||||||
export const registerCombatFunctionality = () => {
|
export const registerCombatFunctionality = () => {
|
||||||
CONFIG.Combat.documentClass = CombatMixin(CONFIG.Combat.documentClass);
|
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 {
|
return class TickwerkCombat extends BaseCombat {
|
||||||
override get combatant() {
|
/** @override */
|
||||||
return this.turns[0];
|
get combatant() {
|
||||||
|
return this.turns?.[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
override get round() {
|
/** @override */
|
||||||
return this.tickValue;
|
get nextCombatant() {
|
||||||
|
return this.turns?.[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
override get started() {
|
/** @override */
|
||||||
return this.turns.length > 0 && (this.getFlag(packageId, 'started') ?? false);
|
get started() {
|
||||||
}
|
return (this.turns?.length ?? 0) > 0 && (this.getFlag(packageId, 'started') ?? false);
|
||||||
|
|
||||||
override get turn() {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current tick value of the Combat encounter.
|
* The current tick value of the Combat encounter.
|
||||||
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
get tickValue(): number {
|
get tickValue() {
|
||||||
const tickValues = this.combatants
|
const tickValues = this.combatants
|
||||||
.filter((combatant) => !combatant.isDefeated && !combatant.waiting)
|
.filter((combatant) => !combatant.isDefeated && !combatant.waiting)
|
||||||
.map((combatant) => combatant.initiative)
|
.map((combatant) => combatant.initiative)
|
||||||
.filter((tickValue): tickValue is number => tickValue !== null);
|
.filter((tickValue) => tickValue !== null);
|
||||||
const tickValue = Math.min(...tickValues);
|
const tickValue = Math.min(...tickValues);
|
||||||
return tickValue === Infinity ? 0 : tickValue;
|
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!');
|
throw new Error('Not implemented!');
|
||||||
}
|
}
|
||||||
|
|
||||||
override async nextTurn() {
|
/** @override */
|
||||||
|
async nextTurn() {
|
||||||
await this.combatant?.advanceTicksDialog();
|
await this.combatant?.advanceTicksDialog();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
override previousRound(): Promise<never> {
|
/** @override */
|
||||||
|
previousRound() {
|
||||||
throw new Error('Not implemented!');
|
throw new Error('Not implemented!');
|
||||||
}
|
}
|
||||||
|
|
||||||
override previousTurn(): Promise<never> {
|
/** @override */
|
||||||
|
previousTurn() {
|
||||||
throw new Error('Not implemented!');
|
throw new Error('Not implemented!');
|
||||||
}
|
}
|
||||||
|
|
||||||
override async resetAll() {
|
/** @override */
|
||||||
|
async resetAll() {
|
||||||
for (const c of this.combatants) {
|
for (const c of this.combatants) {
|
||||||
c.data.update({ initiative: null });
|
c.updateSource({ initiative: null });
|
||||||
}
|
}
|
||||||
return this.update(
|
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 turns = this.combatants.contents.sort(this._sortCombatants);
|
||||||
|
|
||||||
const c = turns[0];
|
const c = turns[0];
|
||||||
|
@ -82,12 +97,13 @@ const CombatMixin = (BaseCombat: typeof Combat) => {
|
||||||
round: this.round,
|
round: this.round,
|
||||||
turn: 0,
|
turn: 0,
|
||||||
combatantId: c?.id ?? null,
|
combatantId: c?.id ?? null,
|
||||||
tokenId: c?.data.tokenId ?? null,
|
tokenId: c?.tokenId ?? null,
|
||||||
};
|
};
|
||||||
return (this.turns = turns);
|
return (this.turns = turns);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async startCombat(): Promise<this | undefined> {
|
/** @override */
|
||||||
|
async startCombat() {
|
||||||
const hasCombatantWithTickValue = this.combatants.find(
|
const hasCombatantWithTickValue = this.combatants.find(
|
||||||
(combatant) => !combatant.isDefeated && combatant.initiative !== null,
|
(combatant) => !combatant.isDefeated && combatant.initiative !== null,
|
||||||
);
|
);
|
||||||
|
@ -95,10 +111,14 @@ const CombatMixin = (BaseCombat: typeof Combat) => {
|
||||||
ui.notifications?.warn('TICKWERK.WarningCannotStartCombat', { localize: true });
|
ui.notifications?.warn('TICKWERK.WarningCannotStartCombat', { localize: true });
|
||||||
return this;
|
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 da = a.isDefeated ? 1 : 0;
|
||||||
const db = b.isDefeated ? 1 : 0;
|
const db = b.isDefeated ? 1 : 0;
|
||||||
const cd = da - db;
|
const cd = da - db;
|
||||||
|
@ -121,15 +141,36 @@ const CombatMixin = (BaseCombat: typeof Combat) => {
|
||||||
|
|
||||||
return (b.id ?? '') > (a.id ?? '') ? 1 : -1;
|
return (b.id ?? '') > (a.id ?? '') ? 1 : -1;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
/** @override */
|
||||||
interface FlagConfig {
|
async _preUpdate(changed, options, user) {
|
||||||
Combat: {
|
delete changed.round;
|
||||||
tickwerk?: {
|
delete changed.turn;
|
||||||
started?: boolean;
|
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -2,7 +2,6 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// 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 { packageId } from '../../constants';
|
||||||
import { getGame } from '../../helpers';
|
import { getGame } from '../../helpers';
|
||||||
|
|
||||||
|
@ -10,39 +9,49 @@ export const registerCombatantFunctionality = () => {
|
||||||
CONFIG.Combatant.documentClass = CombatantMixin(CONFIG.Combatant.documentClass);
|
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 {
|
return class TickwerkCombatant extends BaseCombatant {
|
||||||
/**
|
/**
|
||||||
* An temporary property to make changes to the initiative available to other instances in their `_pre…` methods.
|
* 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.
|
* 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?
|
* Is this combatant currently waiting?
|
||||||
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get waiting(): boolean {
|
get waiting() {
|
||||||
return this.getFlag(packageId, 'waiting') ?? false;
|
return this.getFlag(packageId, 'waiting') ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the waiting state of this combatant.
|
* Toggle the waiting state of this combatant.
|
||||||
|
* @returns {Promise<this|undefined>} The updated combatant
|
||||||
*/
|
*/
|
||||||
toggleWaiting(): Promise<this | undefined> {
|
toggleWaiting() {
|
||||||
const update: Record<string, unknown> = { [`flags.${packageId}.waiting`]: !this.waiting };
|
const update = { [`flags.${packageId}.waiting`]: !this.waiting };
|
||||||
if (this.parent?.started && this.waiting) update.initiative = this.parent?.round;
|
if (this.parent?.started && this.waiting) update.initiative = this.parent?.round;
|
||||||
return this.update(update);
|
return this.update(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Advance for the given number of ticks.
|
* 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) {
|
if (this.initiative === null) {
|
||||||
ui.notifications?.warn('TICKWERK.WarningCannotAdvanceWithoutTickValue', { localize: true });
|
ui.notifications?.warn('TICKWERK.WarningCannotAdvanceWithoutTickValue', { localize: true });
|
||||||
return;
|
return;
|
||||||
|
@ -59,11 +68,15 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
|
||||||
await this.update({ initiative: this.initiative + ticks });
|
await this.update({ initiative: this.initiative + ticks });
|
||||||
const advanceTime = ticks * CONFIG.time.roundTime;
|
const advanceTime = ticks * CONFIG.time.roundTime;
|
||||||
if (advanceTime !== 0) {
|
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 game = getGame();
|
||||||
const id = foundry.utils.randomID();
|
const id = foundry.utils.randomID();
|
||||||
|
|
||||||
|
@ -76,14 +89,14 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
|
||||||
title: game.i18n.localize('TICKWERK.AdvanceTicks'),
|
title: game.i18n.localize('TICKWERK.AdvanceTicks'),
|
||||||
content: form,
|
content: form,
|
||||||
yes: (html) => {
|
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;
|
const parsedTicks = ticks !== undefined ? parseInt(ticks) : undefined;
|
||||||
return Number.isSafeInteger(parsedTicks) ? parsedTicks : undefined;
|
return Number.isSafeInteger(parsedTicks) ? parsedTicks : NaN;
|
||||||
},
|
},
|
||||||
rejectClose: false,
|
rejectClose: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ticks === undefined) {
|
if (Number.isNaN(ticks)) {
|
||||||
ui.notifications?.warn('TICKWERK.WarningInvalidNumberOfTicks', { localize: true });
|
ui.notifications?.warn('TICKWERK.WarningInvalidNumberOfTicks', { localize: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -95,9 +108,9 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update tiebreaker data for a given creation or update.
|
* 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) {
|
if ('initiative' in data) {
|
||||||
const combatantsWithSameTickValue =
|
const combatantsWithSameTickValue =
|
||||||
this.parent?.combatants.filter((combatant) => {
|
this.parent?.combatants.filter((combatant) => {
|
||||||
|
@ -106,7 +119,7 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
|
||||||
return otherInitiative === data.initiative;
|
return otherInitiative === data.initiative;
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
const tiebreaker = await this.#getTiebreaker(combatantsWithSameTickValue);
|
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._newInitiative = data.initiative;
|
||||||
this._newTiebreaker = tiebreaker;
|
this._newTiebreaker = tiebreaker;
|
||||||
}
|
}
|
||||||
|
@ -114,46 +127,48 @@ const CombatantMixin = (BaseCombatant: typeof Combatant) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a tiebreaker between this combatant and the given other combatants.
|
* Get a tiebreaker between this combatant and the given other combatants.
|
||||||
* @param combatants The other combatants among which to find a tiebreaker
|
* @param {TickwerkCombatant[]} combatants The other combatants among which to find a tiebreaker
|
||||||
* @returns A promise that resolves to the 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;
|
const getTiebreaker = CONFIG.tickwerk?.getTiebreaker ?? defaultGetTiebreaker;
|
||||||
return getTiebreaker(this, combatants);
|
return getTiebreaker(this, combatants);
|
||||||
}
|
}
|
||||||
|
|
||||||
override testUserPermission(
|
/** @override */
|
||||||
user: foundry.documents.BaseUser,
|
testUserPermission(user, permission, { exact } = {}) {
|
||||||
permission: keyof typeof foundry.CONST.DOCUMENT_PERMISSION_LEVELS | foundry.CONST.DOCUMENT_PERMISSION_LEVELS,
|
|
||||||
{ exact }: { exact?: boolean | undefined } = {},
|
|
||||||
): boolean {
|
|
||||||
if (user.isGM) return true;
|
if (user.isGM) return true;
|
||||||
return super.testUserPermission(user, permission, { exact });
|
return super.testUserPermission(user, permission, { exact });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override _getInitiativeFormula(): string {
|
/** @override */
|
||||||
|
_getInitiativeFormula() {
|
||||||
const getInitiativeFormula = CONFIG.tickwerk?.getInitiativeFormula;
|
const getInitiativeFormula = CONFIG.tickwerk?.getInitiativeFormula;
|
||||||
if (getInitiativeFormula) return getInitiativeFormula(this);
|
if (getInitiativeFormula) return getInitiativeFormula(this);
|
||||||
return super._getInitiativeFormula();
|
return super._getInitiativeFormula();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async _preCreate(...args: Parameters<Combatant['_preCreate']>): Promise<void> {
|
/** @override */
|
||||||
|
async _preCreate(...args) {
|
||||||
await super._preCreate(...args);
|
await super._preCreate(...args);
|
||||||
await this.#updateTiebreakerData(args[0]);
|
await this.#updateTiebreakerData(args[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async _preUpdate(...args: Parameters<Combatant['_preUpdate']>): Promise<void> {
|
/** @override */
|
||||||
|
async _preUpdate(...args) {
|
||||||
await super._preUpdate(...args);
|
await super._preUpdate(...args);
|
||||||
await this.#updateTiebreakerData(args[0]);
|
await this.#updateTiebreakerData(args[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override _onCreate(...args: Parameters<Combatant['_onCreate']>): void {
|
/** @override */
|
||||||
|
_onCreate(...args) {
|
||||||
super._onCreate(...args);
|
super._onCreate(...args);
|
||||||
this._newInitiative = undefined;
|
this._newInitiative = undefined;
|
||||||
this._newTiebreaker = undefined;
|
this._newTiebreaker = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override _onUpdate(...args: Parameters<Combatant['_onUpdate']>): void {
|
/** @override */
|
||||||
|
_onUpdate(...args) {
|
||||||
super._onUpdate(...args);
|
super._onUpdate(...args);
|
||||||
this._newInitiative = undefined;
|
this._newInitiative = undefined;
|
||||||
this._newTiebreaker = 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;
|
if (combatants.length === 0) return 0;
|
||||||
const tiebreakers = combatants.map((combatant) => {
|
const tiebreakers = combatants.map((combatant) => {
|
||||||
return (
|
return (
|
||||||
|
@ -172,28 +196,3 @@ const defaultGetTiebreaker = async (combatant: TickwerkCombatant, combatants: Ti
|
||||||
});
|
});
|
||||||
return Math.max(...tiebreakers) + 1;
|
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
export const getGame = (): Game => {
|
export const getGame = () => {
|
||||||
if (!(game instanceof Game)) {
|
if (!(game instanceof Game)) {
|
||||||
throw new Error('game is not initialized yet.');
|
throw new Error('game is not initialized yet.');
|
||||||
}
|
}
|
|
@ -5,7 +5,6 @@
|
||||||
import { packageId } from '../constants';
|
import { packageId } from '../constants';
|
||||||
import { getGame } from '../helpers';
|
import { getGame } from '../helpers';
|
||||||
|
|
||||||
import type { TickwerkCombatant } from '../data/documents/combatant';
|
|
||||||
export const registerDS4SpecificFunctionality = () => {
|
export const registerDS4SpecificFunctionality = () => {
|
||||||
if (CONFIG.tickwerk === undefined) CONFIG.tickwerk = {};
|
if (CONFIG.tickwerk === undefined) CONFIG.tickwerk = {};
|
||||||
foundry.utils.mergeObject(CONFIG.tickwerk, { getTiebreaker, getInitiativeFormula });
|
foundry.utils.mergeObject(CONFIG.tickwerk, { getTiebreaker, getInitiativeFormula });
|
||||||
|
@ -14,20 +13,22 @@ export const registerDS4SpecificFunctionality = () => {
|
||||||
Hooks.on('ds4.rollItem', onRollItem);
|
Hooks.on('ds4.rollItem', onRollItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTiebreaker = async (combatant: TickwerkCombatant, combatants: TickwerkCombatant[]): Promise<number> => {
|
/** @type {import("../data/documents/combatant").GetTiebreaker} */
|
||||||
const ds4combatant = combatant as DS4TickwerkCombatant;
|
const getTiebreaker = async (combatant, combatants) => {
|
||||||
const ds4combatants = combatants as DS4TickwerkCombatant[];
|
|
||||||
if (combatants.length === 0) return 0;
|
if (combatants.length === 0) return 0;
|
||||||
|
|
||||||
const lowerBounds: number[] = [];
|
/** @type {number[]} */
|
||||||
const upperBounds: number[] = [];
|
const lowerBounds = [];
|
||||||
const equals: number[] = [];
|
/** @type {number[]} */
|
||||||
|
const upperBounds = [];
|
||||||
|
/** @type {number[]} */
|
||||||
|
const equals = [];
|
||||||
|
|
||||||
for (const combatant of ds4combatants) {
|
for (const other of combatants) {
|
||||||
const tiebreaker = combatant._newTiebreaker ?? combatant.getFlag(packageId, 'tiebreaker') ?? 0;
|
const tiebreaker = other._newTiebreaker ?? other.getFlag(packageId, 'tiebreaker') ?? 0;
|
||||||
if (getInitiative(combatant) > getInitiative(ds4combatant)) {
|
if (getInitiative(other) > getInitiative(combatant)) {
|
||||||
lowerBounds.push(tiebreaker);
|
lowerBounds.push(tiebreaker);
|
||||||
} else if (getInitiative(combatant) < getInitiative(ds4combatant)) {
|
} else if (getInitiative(other) < getInitiative(combatant)) {
|
||||||
upperBounds.push(tiebreaker);
|
upperBounds.push(tiebreaker);
|
||||||
} else {
|
} else {
|
||||||
equals.push(tiebreaker);
|
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;
|
const started = combatant.combat?.started ?? false;
|
||||||
if (!started) return '-@combatValues.initiative.total';
|
if (!started) return '-@combatValues.initiative.total';
|
||||||
const tickValue = combatant.combat?.round ?? 0;
|
const tickValue = combatant.combat?.round ?? 0;
|
||||||
return `max(${tickValue} + 10 - @combatValues.initiative.total, ${tickValue})`;
|
return `max(${tickValue} + 10 - @combatValues.initiative.total, ${tickValue})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DS4TickwerkCombatant = TickwerkCombatant & { actor: (Actor & { data: { data: ActorData } }) | null };
|
/**
|
||||||
|
* Get the initiative for a combatant.
|
||||||
const getInitiative = (combatant: DS4TickwerkCombatant): number => {
|
* @param {TickwerkCombatant} combatant The combatant for which to get the initiative
|
||||||
return combatant.actor?.data.data.combatValues.initiative.total ?? -Infinity;
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const getInitiative = (combatant) => {
|
||||||
|
return combatant.actor?.system.combatValues.initiative.total ?? -Infinity;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ActorData {
|
/**
|
||||||
combatValues: {
|
* React to an item roll by prompting the user to advance ticks.
|
||||||
initiative: {
|
* @param {Item} item The item that has been rolled
|
||||||
total: number;
|
*/
|
||||||
};
|
const onRollItem = (item) => {
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
||||||
namespace Hooks {
|
|
||||||
interface StaticCallbacks {
|
|
||||||
'ds4.rollItem': (item: Item) => void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRollItem = (item: Item) => {
|
|
||||||
const game = getGame();
|
const game = getGame();
|
||||||
if (game.settings.get(packageId, 'ds4.reactToRollItemHook')) {
|
if (game.settings.get(packageId, 'ds4.reactToRollItemHook')) {
|
||||||
if (['weapon', 'spell'].includes(item.type) && item.actor?.id) {
|
if (['weapon', 'spell'].includes(item.type) && item.actor?.id) {
|
225
template.json
225
template.json
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -5,82 +5,89 @@ SPDX-FileCopyrightText: 2022 Johannes Loher
|
||||||
SPDX-License-Identifier: MIT
|
SPDX-License-Identifier: MIT
|
||||||
--}}
|
--}}
|
||||||
|
|
||||||
<section class="tab sidebar-tab directory flexcol" id="combat" data-tab="combat">
|
<section class="{{cssClass}} directory flexcol" id="{{cssId}}" data-tab="{{tabName}}">
|
||||||
<header id="combat-round">
|
<header class="combat-tracker-header">
|
||||||
{{#if user.isGM}}
|
{{#if user.isGM}}
|
||||||
<nav class="encounters flexrow">
|
<nav class="encounters flexrow" aria-label="COMBAT.NavLabel">
|
||||||
<a class="combat-create" title="{{localize 'COMBAT.Create'}}">
|
<a class="combat-button combat-create" data-tooltip="COMBAT.Create">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
</a>
|
</a>
|
||||||
{{#if combatCount}}
|
{{#if combatCount}}
|
||||||
<a class="combat-cycle" title="{{localize 'COMBAT.EncounterPrevious'}}"
|
<a class="combat-button combat-cycle" data-tooltip="COMBAT.EncounterPrevious"
|
||||||
{{#if previousId}}data-combat-id="{{previousId}}"{{else}}disabled{{/if}}>
|
{{#if previousId}}data-document-id="{{previousId}}"{{else}}disabled{{/if}}>
|
||||||
<i class="fas fa-caret-left"></i>
|
<i class="fas fa-caret-left"></i>
|
||||||
</a>
|
</a>
|
||||||
<h4 class="encounter">{{localize "COMBAT.Encounter"}} {{currentIndex}} / {{combatCount}}</h4>
|
<h4 class="encounter">{{localize "COMBAT.Encounter"}} {{currentIndex}} / {{combatCount}}</h4>
|
||||||
<a class="combat-cycle" title="{{localize 'COMBAT.EncounterNext'}}"
|
<a class="combat-button combat-cycle" data-tooltip="COMBAT.EncounterNext"
|
||||||
{{#if nextId}}data-combat-id="{{nextId}}"{{else}}disabled{{/if}}>
|
{{#if nextId}}data-document-id="{{nextId}}"{{else}}disabled{{/if}}>
|
||||||
<i class="fas fa-caret-right"></i>
|
<i class="fas fa-caret-right"></i>
|
||||||
</a>
|
</a>
|
||||||
{{/if}}
|
{{/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>
|
<i class="fas fa-trash"></i>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<nav class="encounters flexrow {{#if hasCombat}}combat{{/if}}">
|
<div class="encounter-controls flexrow {{#if hasCombat}}combat{{/if}}">
|
||||||
{{#if user.isGM}}
|
{{#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>
|
<i class="fas fa-users"></i>
|
||||||
</a>
|
</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>
|
<i class="fas fa-users-cog"></i>
|
||||||
</a>
|
</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if combatCount}}
|
{{#if combatCount}}
|
||||||
{{#if combat.started}}
|
{{#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}}
|
{{else}}
|
||||||
<h3 class="encounter-title">{{localize 'COMBAT.NotStarted'}}</h3>
|
<h3 class="encounter-title noborder">{{localize 'COMBAT.NotStarted'}}</h3>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<h3 class="encounter-title">{{localize "COMBAT.None"}}</h3>
|
<h3 class="encounter-title noborder">{{localize "COMBAT.None"}}</h3>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if user.isGM}}
|
{{#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}}>
|
{{#unless hasCombat}}disabled{{/unless}}>
|
||||||
<i class="fas fa-undo"></i>
|
<i class="fas fa-undo"></i>
|
||||||
</a>
|
</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}}>
|
data-control="toggleSceneLink" {{#unless hasCombat}}disabled{{/unless}}>
|
||||||
<i class="fas fa-{{#unless linked}}un{{/unless}}link"></i>
|
<i class="fas fa-{{#unless linked}}un{{/unless}}link"></i>
|
||||||
</a>
|
</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>
|
<i class="fas fa-cog"></i>
|
||||||
</a>
|
</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</nav>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ol id="combat-tracker" class="directory-list">
|
<ol id="combat-tracker" class="directory-list">
|
||||||
{{#each turns}}
|
{{#each turns}}
|
||||||
<li class="combatant actor directory-item flexrow {{this.css}}" data-combatant-id="{{this.id}}">
|
<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">
|
<div class="token-name flexcol">
|
||||||
<h4>{{this.name}}</h4>
|
<h4>{{this.name}}</h4>
|
||||||
<div class="combatant-controls flexrow">
|
<div class="combatant-controls flexrow">
|
||||||
{{#if (or ../user.isGM (and ../control this.owner))}}
|
{{#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>
|
<i class="fas {{#if this.waiting}}fa-play-circle{{else}}fa-pause-circle{{/if}}"></i></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if ../user.isGM}}
|
{{#if ../user.isGM}}
|
||||||
<a class="combatant-control {{#if this.hidden}}active{{/if}}" title="{{localize 'COMBAT.ToggleVis'}}" data-control="toggleHidden">
|
<a class="combatant-control {{#if this.hidden}}active{{/if}}" data-tooltip="COMBAT.ToggleVis" data-control="toggleHidden">
|
||||||
<i class="fas fa-eye-slash"></i></a>
|
<i class="fas fa-eye-slash"></i>
|
||||||
<a class="combatant-control {{#if this.defeated}}active{{/if}}" title="{{localize 'COMBAT.ToggleDead'}}" data-control="toggleDefeated">
|
</a>
|
||||||
<i class="fas fa-skull"></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}}
|
{{/if}}
|
||||||
<div class="token-effects">
|
<div class="token-effects">
|
||||||
{{#each this.effects}}
|
{{#each this.effects}}
|
||||||
|
@ -100,25 +107,25 @@ SPDX-License-Identifier: MIT
|
||||||
{{#if this.hasRolled}}
|
{{#if this.hasRolled}}
|
||||||
<span class="initiative">{{#if this.waiting}}{{localize "TICKWERK.Waiting"}}{{else}}{{this.initiative}}{{/if}}</span>
|
<span class="initiative">{{#if this.waiting}}{{localize "TICKWERK.Waiting"}}{{else}}{{this.initiative}}{{/if}}</span>
|
||||||
{{else if this.owner}}
|
{{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}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<nav id="combat-controls" class="directory-footer flexrow">
|
<nav id="combat-controls" class="directory-footer flexrow" data-tooltip-direction="UP">
|
||||||
{{#if hasCombat}}
|
{{#if hasCombat}}
|
||||||
{{#if user.isGM}}
|
{{#if user.isGM}}
|
||||||
{{#if combat.started}}
|
{{#if combat.started}}
|
||||||
<a class="combat-control center" title="{{localize 'COMBAT.End'}}" data-control="endCombat">{{localize 'COMBAT.End'}}</a>
|
<a class="combat-control center" 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" data-tooltip="COMBAT.TurnNext" data-control="nextTurn"><i class="fas fa-arrow-right"></i></a>
|
||||||
{{else}}
|
{{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}}
|
{{/if}}
|
||||||
{{else if (and control combat.started)}}
|
{{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 center" 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" data-tooltip="COMBAT.TurnNext" data-control="nextTurn"><i class="fas fa-arrow-right"></i></a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": ["src", "*.js"]
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
SPDX-FileCopyrightText: 2021 Johannes Loher
|
|
||||||
|
|
||||||
SPDX-License-Identifier: MIT
|
|
|
@ -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"]
|
|
||||||
}
|
|
Loading…
Reference in a new issue