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
|
||||
|
||||
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
2
.gitignore
vendored
|
@ -14,6 +14,8 @@ npm-debug.log
|
|||
|
||||
# Local configuration
|
||||
foundryconfig.json
|
||||
/common
|
||||
/client
|
||||
|
||||
# Distribution files
|
||||
dist
|
||||
|
|
|
@ -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:
|
||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -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,
|
||||
|
|
2
.yarn/sdks/eslint/package.json
vendored
2
.yarn/sdks/eslint/package.json
vendored
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "eslint",
|
||||
"version": "8.25.0-sdk",
|
||||
"version": "8.28.0-sdk",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs"
|
||||
}
|
||||
|
|
2
.yarn/sdks/prettier/package.json
vendored
2
.yarn/sdks/prettier/package.json
vendored
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "prettier",
|
||||
"version": "2.7.1-sdk",
|
||||
"version": "2.8.0-sdk",
|
||||
"main": "./index.js",
|
||||
"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",
|
||||
"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": [
|
||||
|
|
26
package.json
26
package.json
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
|
@ -2,4 +2,4 @@
|
|||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export const packageId = 'tickwerk' as const;
|
||||
export const packageId = 'tickwerk';
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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.');
|
||||
}
|
|
@ -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) {
|
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
|
||||
--}}
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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