feat: initial commit

This commit is contained in:
Johannes Loher 2022-05-16 02:08:27 +02:00
commit ab27f2e5a8
82 changed files with 34255 additions and 0 deletions

13
.editorconfig Normal file
View file

@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: 2021 Johannes Loher
#
# SPDX-License-Identifier: MIT
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true

8
.eslintignore Normal file
View file

@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2021 Johannes Loher
#
# SPDX-License-Identifier: MIT
/dist
/.pnp.cjs
/.pnp.loader.mjs
/.yarn/

31
.eslintrc.cjs Normal file
View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
env: {
browser: true,
},
extends: ['plugin:@typescript-eslint/recommended', 'plugin:jest/recommended', 'plugin:prettier/recommended'],
plugins: ['@typescript-eslint'],
rules: {},
overrides: [
{
files: ['./*.cjs', './*.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off',
},
},
],
};

34
.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: 2021 Johannes Loher
# SPDX-FileCopyrightText: 2021 Oliver Rümpelein
#
# SPDX-License-Identifier: MIT
# IDE
.idea/
.vs/
# Node Modules
node_modules/
npm-debug.log
# Local configuration
foundryconfig.json
# Distribution files
dist
# ESLint
.eslintcache
# Junit results
results.xml
junit.xml
# yarn
.yarn/*
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*

176
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,176 @@
# SPDX-FileCopyrightText: 2021 Johannes Loher
# SPDX-FileCopyrightText: 2021 Oliver Rümpelein
#
# SPDX-License-Identifier: MIT
image: node:lts
variables:
PACKAGE_NAME: tickwerk
PACKAGE_TYPE: module
PACKAGE_REGISTRY_URL: $CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/$PACKAGE_NAME
stages:
- check
- build
- prepare-release
- release
- publish
cache: &global_cache
paths:
- .yarn/cache
lint:
stage: check
before_script:
- yarn install --immutable
script:
- yarn lint
cache:
<<: *global_cache
typecheck:
stage: check
before_script:
- yarn install --immutable
script:
- yarn typecheck
cache:
<<: *global_cache
reuse:
stage: check
image:
name: fsfe/reuse:latest
entrypoint: ['']
script:
- reuse lint
build:
stage: build
before_script:
- yarn install --immutable
- if [[ ! -z ${CI_COMMIT_TAG+x} ]]; then export NODE_ENV=production; fi
script:
- yarn build
cache:
<<: *global_cache
artifacts:
paths:
- dist
expire_in: 1 week
publish-artifacts:
stage: prepare-release
image: alpine:latest
before_script:
- apk update
- apk add zip curl
script: |
cd dist
zip -r ../$PACKAGE_TYPE.zip .
cd ..
curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file $PACKAGE_TYPE.zip "$PACKAGE_REGISTRY_URL/$CI_COMMIT_TAG/$PACKAGE_TYPE.zip"
curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file dist/$PACKAGE_TYPE.json "$PACKAGE_REGISTRY_URL/$CI_COMMIT_TAG/$PACKAGE_TYPE.json"
rules:
- if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+$/'
changelog:
stage: prepare-release
before_script:
- yarn install --immutable
script:
- yarn changelog
cache:
<<: *global_cache
artifacts:
paths:
- CHANGELOG.md
rules:
- if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+$/'
.release-template: &release-template
stage: release
before_script:
- yarn install
- apt update
- apt install --yes jq
- REPOSITORY_URL=$(echo "${CI_REPOSITORY_URL}" | sed -e "s|gitlab-ci-token:.*@|${RELEASE_TOKEN}:${RELEASE_TOKEN_SECRET}@|g")
- git remote set-url origin $REPOSITORY_URL
- git config user.name $GITLAB_USER_LOGIN
- git config user.email $GITLAB_USER_EMAIL
- git branch -D ci-processing || true
- git checkout -b ci-processing
cache:
<<: *global_cache
script: |
yarn bump-version --release=${RELEASE_TYPE}
RELEASE_VERSION=$(jq -r '.version' < package.json)
git add package.json module.json
git --no-pager diff
git commit -m "chore(release): ${RELEASE_VERSION}"
git tag -f ${RELEASE_VERSION}
git push origin ci-processing:${CI_BUILD_REF_NAME} -o ci.skip
git push origin ${RELEASE_VERSION}
only:
- main
when: manual
release-patch:
variables:
RELEASE_TYPE: patch
<<: *release-template
release-minor:
variables:
RELEASE_TYPE: minor
<<: *release-template
release-major:
variables:
RELEASE_TYPE: major
<<: *release-template
release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
script:
- echo 'release job'
rules:
- if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+$/'
release:
tag_name: $CI_COMMIT_TAG
description: './CHANGELOG.md'
assets:
links:
- name: '$PACKAGE_TYPE.zip'
url: '$PACKAGE_REGISTRY_URL/$CI_COMMIT_TAG/$PACKAGE_TYPE.zip'
filepath: /$PACKAGE_TYPE.zip
link_type: package
- name: '$PACKAGE_TYPE.json'
url: '$PACKAGE_REGISTRY_URL/$CI_COMMIT_TAG/$PACKAGE_TYPE.json'
filepath: /$PACKAGE_TYPE.json
link_type: other
publish-latest-manifest:
stage: publish
image: alpine:latest
before_script:
- apk update
- apk add zip curl
script: |
curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file dist/$PACKAGE_TYPE.json "$PACKAGE_REGISTRY_URL/latest/$PACKAGE_TYPE.json"
rules:
- if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+$/'
# publish-to-foundry-admin:
# stage: publish
# image: johannesloher/foundry-publish
# variables:
# FVTT_MANIFEST_PATH: dist/$PACKAGE_TYPE.json
# FVTT_MANIFEST_URL: ${CI_PROJECT_URL}/-/releases/${CI_COMMIT_TAG}/downloads/$PACKAGE_TYPE.json
# FVTT_DELETE_OBSOLETE_VERSIONS: "true"
# script: foundry-publish
# rules:
# - if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+$/'

View file

@ -0,0 +1,48 @@
Your issue may already have been reported! Please search on the issue tracker (https://git.f3l.de/dungeonslayers/tickwerk/-/issues) before submitting a new one.
Thanks for taking the time to fill out this bug report! In order to make it effective, please provide the following information.
# Issue Description
## Expected Behavior
(What is the behavior that you expected?)
## Current Behavior
(What is the current behavior, i.e., what happens actually?)
## Steps to Reproduce
(What are the steps to reproduce the problem?)
1. ...
2. ...
3. ...
## Context
(Please provide any additional context that might be helpful, e.g. log messages,
screenshots, videos, or exports of problematic scenes or worlds.)
# Environment Details
## Version
(Which version(s) of Tickwerk are you seeing the problem on?)
## Foundry VTT Version
(Which version(s) and build of Foundry VTT are you seeing the problem on?)
## Operating System
(Which operating system are you using? (Windows, OS X, Linux (which distro)))
## Browser / App
(Are you using a Browser or the native Electron application?)
## Relevant Modules
(Please list any active modules (including their versions) that you think might be relevant.)

View file

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

View file

@ -0,0 +1,11 @@
Your issue may already have been reported! Please search on the issue tracker (https://git.f3l.de/dungeonslayers/tickwerk/-/issues) before submitting a new one.
In order to submit an effective feature request, please provide the following information.
# Description
(Please describe the proposal in as much detail as you feel is necessary.)
# Context
(Is there anything else you can add about the proposal? You might want to link to related issues here if you haven't already.)

View file

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

1
.husky/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_

View file

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

9
.husky/commit-msg Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2021 Johannes Loher
#
# SPDX-License-Identifier: MIT
. "$(dirname "$0")/_/husky.sh"
yarn run commitlint --edit "$1"

9
.husky/pre-commit Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2021 Johannes Loher
#
# SPDX-License-Identifier: MIT
. "$(dirname "$0")/_/husky.sh"
yarn run lint-staged

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
lts/*

3
.nvmrc.license Normal file
View file

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

10
.prettierignore Normal file
View file

@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2021 Johannes Loher
#
# SPDX-License-Identifier: MIT
/dist
/package-lock.json
/.pnp.cjs
/.pnp.loader.mjs
/.yarn/
/.vscode/

11
.prettierrc.cjs Normal file
View file

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
};

8
.reuse/dep5 Normal file
View file

@ -0,0 +1,8 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: tickwerk
Upstream-Contact: Johannes Loher <johannes.loher@fg4f.de>
Source: https://git.f3l.de/dungeonslayers/tickwerk
Files: .yarn/**
Copyright: Copyright (c) 2016-present, Yarn Contributors. All rights reserved.
License: BSD-2-Clause

7
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"arcanis.vscode-zipfs"
]
}

4
.vscode/extensions.json.license vendored Normal file
View file

@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Oliver Rümpelein
SPDX-License-Identifier: MIT

16
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"runtimeExecutable": "/usr/bin/chromium",
"name": "Launch Chrome against localhost",
"url": "http://localhost:30000/game",
"webRoot": "${workspaceFolder}/dist"
}
]
}

3
.vscode/launch.json.license vendored Normal file
View file

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

16
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,16 @@
{
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
},
"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,
"importSorter.importStringConfiguration.tabSize": 2,
"importSorter.importStringConfiguration.quoteMark": "single",
"importSorter.importStringConfiguration.trailingComma": "multiLine"
}

3
.vscode/settings.json.license vendored Normal file
View file

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

File diff suppressed because one or more lines are too long

786
.yarn/releases/yarn-3.2.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

20
.yarn/sdks/eslint/bin/eslint.js vendored Executable file
View file

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

20
.yarn/sdks/eslint/lib/api.js vendored Normal file
View file

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

6
.yarn/sdks/eslint/package.json vendored Normal file
View file

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

5
.yarn/sdks/integrations.yml vendored Normal file
View file

@ -0,0 +1,5 @@
# This file is automatically generated by @yarnpkg/sdks.
# Manual changes might be lost!
integrations:
- vscode

20
.yarn/sdks/prettier/index.js vendored Executable file
View file

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

6
.yarn/sdks/prettier/package.json vendored Normal file
View file

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

20
.yarn/sdks/typescript/bin/tsc vendored Executable file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(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 Executable file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(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 Normal file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(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`);

196
.yarn/sdks/typescript/lib/tsserver.js vendored Normal file
View file

@ -0,0 +1,196 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(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
//
// Update Oct 8 2021: VSCode changed their format in 1.61.
// 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`: {
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 process.platform === `win32`
? str.replace(/^\^?(zip:|\/zip)\/+/, ``)
: str.replace(/^\^?(zip:|\/zip)\/+/, `/`);
} 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 && process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)) {
hostInfo += ` <1.61`;
}
}
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
return typeof value === 'string' ? fromEditorPath(value) : value;
});
return originalOnMessage.call(
this,
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
);
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
return tsserver;
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserver.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserver.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));

View file

@ -0,0 +1,196 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(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
//
// Update Oct 8 2021: VSCode changed their format in 1.61.
// 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`: {
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 process.platform === `win32`
? str.replace(/^\^?(zip:|\/zip)\/+/, ``)
: str.replace(/^\^?(zip:|\/zip)\/+/, `/`);
} 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 && process.env.VSCODE_IPC_HOOK.match(/Code\/1\.([1-5][0-9]|60)\./)) {
hostInfo += ` <1.61`;
}
}
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 Normal file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.cjs";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(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 Normal file
View file

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

7
.yarnrc.yml Normal file
View file

@ -0,0 +1,7 @@
nodeLinker: pnp
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: '@yarnpkg/plugin-interactive-tools'
yarnPath: .yarn/releases/yarn-3.2.1.cjs

3
.yarnrc.yml.license Normal file
View file

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

19
LICENSE.md Normal file
View file

@ -0,0 +1,19 @@
<!--
SPDX-FileCopyrightText: 2022 Johannes Loher
SPDX-License-Identifier: MIT
-->
# Licensing
This project is being developed under the terms of the
[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT] for Foundry Virtual Tabletop.
The project itself is licensed under multiple licenses. [REUSE] is used to
specify the licenses for the individual files. Most of the licenses are
specified either inside the source file or by an accompanying `.license` file,
but for some files, the licenses are specified in [.reuse/dep5].
[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT]: https://foundryvtt.com/article/license/
[REUSE]: https://reuse.software/
[.reuse/dep5]: .reuse/dep5

View file

@ -0,0 +1,9 @@
Copyright (c) <year> <owner> All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

9
LICENSES/MIT.txt Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

110
README.md Normal file
View file

@ -0,0 +1,110 @@
<!--
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Siegfried Krug
SPDX-License-Identifier: MIT
-->
# Tickwerk
A tick based combat system for [Foundry Virtual Tabletop].
This module adjusts the combat to use ticks instead of rounds for tracking time.
It has special integration with the [Dungeonslayers 4] system, based on the
fanwork “[Tickwerk]”, but is also be adaptable to other systems.
## Installation
To install and use the Tickwerk module for Foundry Virtual Tabletop,
find it in the list in the **Install Module** dialog on the Setup menu of the
application. Alternatively, paste the following Manifest URL in that dialog:
https://git.f3l.de/api/v4/projects/dungeonslayers%2Ftick-based-combat/packages/generic/tickwerk/latest/module.json
## Development
### Prerequisites
In order to build this module, recent versions of `node` and `yarn` are
required. Most likely using `npm` also works but only `yarn` is officially
supported. We recommend using the latest lts version of `node`. If you use `nvm`
to manage your `node` versions, you can simply run
```
nvm install
```
in the project's root directory.
You also need to install the project's dependencies. To do so, run
```
yarn install
```
### Building
You can build the project by running
```
yarn build
```
Alternatively, you can run
```
yarn watch
```
to watch for changes and automatically build as necessary.
### Linking the built package to Foundry VTT
In order to provide a fluent development experience, it is recommended to link
the built package to your local Foundry VTT installation's data folder. In order
to do so, first add a file called `foundryconfig.json` to the project root with
the following content:
```
{
"dataPath": "<path to your home directory>/.local/share/FoundryVTT"
}
```
On platforms other than Linux you need to adjust the path accordingly.
Then run
```
yarn link-package
```
### Running the tests
You can run the tests with the following command:
```
yarn test
```
## Contributing
Code and content contributions are accepted. Please feel free to submit issues
to the issue tracker or submit merge requests for code changes.
## Licensing
This project is being developed under the terms of the
[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT] for Foundry Virtual Tabletop.
The project itself is licensed under multiple licenses. [REUSE] is used to
specify the licenses for the individual files. Most of the licenses are
specified either inside the source file or by an accompanying `.license` file,
but for some files, the licenses are specified in [.reuse/dep5].
[Foundry Virtual Tabletop]: http://foundryvtt.com/
[Dungeonslayers 4]: https://git.f3l.de/dungeonslayers/ds4/
[Tickwerk]: https://dungeonslayers.net/download/kalender2021/released/18_DS_tickwerk_ghost.pdf
[LIMITED LICENSE AGREEMENT FOR MODULE DEVELOPMENT]: https://foundryvtt.com/article/license/
[REUSE]: https://reuse.software/
[.reuse/dep5]: .reuse/dep5

5
commitlint.config.cjs Normal file
View file

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
module.exports = { extends: ['@commitlint/config-conventional'] };

8
lang/de.json Normal file
View file

@ -0,0 +1,8 @@
{
"TICKWERK.AdvanceTicks": "Auf der Tickleiste Vorrücken",
"TICKWERK.StopWaiting": "Abwarten Beenden",
"TICKWERK.Tick": "Tick",
"TICKWERK.Wait": "Abwarten",
"TICKWERK.Waiting": "Abwarten",
"TICKWERK.WarningCannotStartCombat": "Der Kampf kann nur begonnen werden, wenn mindestens ein Kampfteilnehmer einen Tickwert hat."
}

6
lang/de.json.license Normal file
View file

@ -0,0 +1,6 @@
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

8
lang/en.json Normal file
View file

@ -0,0 +1,8 @@
{
"TICKWERK.AdvanceTicks": "Advance on the Tickbar",
"TICKWERK.StopWaiting": "Stop Waiting",
"TICKWERK.Tick": "Tick",
"TICKWERK.Wait": "Wait",
"TICKWERK.Waiting": "Waiting",
"TICKWERK.WarningCannotStartCombat": "In order to start the combat, there needs to be at least one combatant with a tick value."
}

6
lang/en.json.license Normal file
View file

@ -0,0 +1,6 @@
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

38
module.json Normal file
View file

@ -0,0 +1,38 @@
{
"name": "tickwerk",
"title": "Tickwerk",
"description": "Tick based combat for Foundry Virtual Tabletop.",
"authors": [
{
"name": "Johannes Loher",
"email": "johannes.loher@fg4f.de",
"discord": "ghost#2000",
"ko-fi": "ghostfvtt"
}
],
"url": "https://git.f3l.de/dungeonslayers/tickwerk",
"license": "",
"readme": "",
"bugs": "https://git.f3l.de/dungeonslayers/tickwerk/-/issues",
"changelog": "",
"version": "",
"minimumCoreVersion": "9",
"compatibleCoreVersion": "9",
"esmodules": ["tickwerk.js"],
"styles": ["styles/tickwerk.css"],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
},
{
"lang": "de",
"name": "Deutsch",
"path": "lang/de.json"
}
],
"manifest": "https://git.f3l.de/api/v4/projects/dungeonslayers%2Ftick-based-combat/packages/generic/tickwerk/latest/module.json",
"download": "",
"manifestPlusVersion": "1.2.0"
}

3
module.json.license Normal file
View file

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

22317
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

83
package.json Normal file
View file

@ -0,0 +1,83 @@
{
"private": true,
"name": "tickwerk",
"description": "A tick based combat tracker for Foundry Virtual Tabletop",
"version": "1.0.0",
"license": "https://git.f3l.de/dungeonslayers/tickwerk#licensing",
"homepage": "https://git.f3l.de/dungeonslayers/tickwerk",
"repository": {
"type": "git",
"url": "https://git.f3l.de/dungeonslayers/tickwerk"
},
"bugs": {
"url": "https://git.f3l.de/dungeonslayers/tickwerk/-/issues"
},
"contributors": [
{
"name": "Johannes Loher",
"email": "johannes.loher@fg4f.de"
}
],
"type": "module",
"scripts": {
"build": "run-s clean:files build:files",
"build:files": "rollup -c",
"watch": "rollup -c -w",
"link-package": "node ./tools/link-package.js",
"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",
"bump-version": "node ./tools/bump-version.js",
"postinstall": "husky install",
"changelog": "conventional-changelog -p conventionalcommits -o CHANGELOG.md -r 2"
},
"devDependencies": {
"@commitlint/cli": "16.2.4",
"@commitlint/config-conventional": "16.2.4",
"@guanghechen/rollup-plugin-copy": "1.9.4",
"@league-of-foundry-developers/foundry-vtt-types": "9.268.2",
"@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",
"@rollup/plugin-typescript": "8.3.2",
"@seald-io/nedb": "3.0.0",
"@swc/core": "1.2.183",
"@types/fs-extra": "9.0.13",
"@typescript-eslint/eslint-plugin": "5.23.0",
"@typescript-eslint/parser": "5.23.0",
"conventional-changelog-cli": "2.2.2",
"conventional-changelog-conventionalcommits": "4.6.3",
"eslint": "8.15.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.0.0",
"fs-extra": "10.1.0",
"husky": "8.0.1",
"lint-staged": "12.4.1",
"npm-run-all": "4.1.5",
"prettier": "2.6.2",
"rimraf": "3.0.2",
"rollup": "2.73.0",
"rollup-plugin-styles": "4.0.0",
"rollup-plugin-swc3": "0.3.0",
"sass": "1.51.0",
"semver": "7.3.7",
"tslib": "2.4.0",
"typescript": "4.6.4",
"yargs": "17.5.0"
},
"lint-staged": {
"*.(ts|js|cjs|mjs)": "eslint --cache --fix",
"*.(json|scss|yml)": "prettier --write"
},
"packageManager": "yarn@3.2.1"
}

5
package.json.license Normal file
View file

@ -0,0 +1,5 @@
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Oliver RÜmpelein
SPDX-FileCopyrightText: 2021 Siegfried Krug
SPDX-License-Identifier: MIT

10
renovate.json Normal file
View file

@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base", ":automergeAll", ":automergeBranch", ":prHourlyLimitNone", ":prConcurrentLimitNone"],
"packageRules": [
{
"matchPackagePatterns": ["^@pixi"],
"enabled": false
}
]
}

3
renovate.json.license Normal file
View file

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

62
rollup.config.js Normal file
View file

@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import copy from '@guanghechen/rollup-plugin-copy';
import styles from 'rollup-plugin-styles';
import { swc } from 'rollup-plugin-swc3';
import { distDirectory, name, sourceDirectory } from './tools/const.js';
const staticFiles = [
'.reuse',
'lang',
'LICENSE.md',
'LICENSES',
'README.md',
'module.json.license',
'module.json',
'templates',
];
const isProduction = process.env.NODE_ENV === 'production';
/**
* @type {import('rollup').RollupOptions}
*/
const config = {
input: { [name]: `${sourceDirectory}/${name}.ts` },
output: {
dir: distDirectory,
format: 'es',
sourcemap: true,
assetFileNames: '[name].[ext]',
},
plugins: [
swc({
minify: isProduction,
jsc: {
minify: isProduction && {
sourceMap: true,
mangle: {
keepClassNames: true,
keepFnNames: true,
},
},
keepClassNames: true,
},
sourceMaps: true,
}),
styles({
mode: ['extract', `styles/${name}.css`],
url: false,
sourceMap: true,
minimize: isProduction,
}),
copy({
targets: [{ src: staticFiles, dest: distDirectory }],
verbose: true,
}),
],
};
export default config;

View file

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
export const registerCombatTrackerFunctionality = () => {
CONFIG.ui.combat = CombatTrackerMixin(CONFIG.ui.combat as typeof CombatTracker); // TODO: improve upstream types
};
const CombatTrackerMixin = (BaseCombatTracker: typeof CombatTracker) => {
return class TickwerkCombatTracker extends BaseCombatTracker {
static override get defaultOptions(): ApplicationOptions {
return foundry.utils.mergeObject(super.defaultOptions, {
template: 'modules/tickwerk/templates/combat-tracker.hbs',
});
}
override async getData(options?: Partial<ApplicationOptions>): Promise<CombatTracker.Data> {
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 {
super.activateListeners(html);
html.find('.combatant-control[data-control="toggleWaiting"]').on('click', this._onToggleWaiting.bind(this));
}
_onToggleWaiting(event: JQuery.ClickEvent) {
event.preventDefault();
event.stopPropagation();
const button = event.currentTarget;
const li = button.closest('.combatant');
const combat = this.viewed;
const combatant = combat?.combatants.get(li.dataset.combatantId);
combatant?.toggleWaiting();
}
};
};

5
src/constants.ts Normal file
View file

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
export const packageId = 'tickwerk' as const;

View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
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);
};
const onActiveEffectChanged = (activeEffect: ActiveEffect) => {
const game = getGame();
const parent = activeEffect.parent;
const actorId = parent?.id;
if (!(parent instanceof Actor) || actorId === null || actorId === undefined) return;
const statusId = activeEffect.getFlag('core', 'statusId');
if (statusId === CONFIG.Combat.defeatedStatusId) {
const relevantCombats = game.combats?.filter((combat) => combat.getCombatantByActor(actorId) !== undefined) ?? [];
for (const combat of relevantCombats) {
combat.setupTurns();
if (combat === game.combat) {
ui.combat?.render();
}
}
}
};

View file

@ -0,0 +1,139 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { packageId } from '../../constants';
import { getGame } from '../../helpers';
import type { TickwerkCombatant } from './combatant';
export const registerCombatFunctionality = () => {
CONFIG.Combat.documentClass = CombatMixin(CONFIG.Combat.documentClass);
};
const CombatMixin = (BaseCombat: typeof Combat) => {
return class TickwerkCombat extends BaseCombat {
override get combatant() {
return this.turns[0];
}
override get round() {
return this.tickValue;
}
override get started() {
return this.turns.length > 0 && (this.getFlag(packageId, 'started') ?? false);
}
override get turn() {
return 0;
}
override async nextRound(): Promise<never> {
throw new Error('Not implemented!');
}
get tickValue(): number {
const tickValues = this.combatants
.filter((combatant) => !combatant.isDefeated)
.map((combatant) => combatant.initiative)
.filter((tickValue): tickValue is number => tickValue !== null);
const tickValue = Math.min(...tickValues);
return tickValue === Infinity ? 0 : tickValue;
}
override async nextTurn() {
const game = getGame();
const combatant = this.combatant;
if (combatant === undefined || combatant.initiative === null || combatant.id === null) {
return this;
}
const ticks = await Dialog.prompt({
title: game.i18n.localize('TICKWERK.AdvanceTicks'),
content: '<input name="ticks" type="number" value="5" min="0" />',
label: game.i18n.localize('TICKWERK.AdvanceTicks'),
callback: (html) => {
const ticks = html[0]?.querySelector<HTMLInputElement>('input[name="ticks"]')?.value;
return ticks !== undefined ? parseInt(ticks) : undefined;
},
rejectClose: false,
});
if (ticks !== undefined && ticks !== null) {
await combatant.update({ initiative: combatant.initiative + ticks });
const advanceTime = ticks * CONFIG.time.roundTime;
return this.update(undefined, { diff: false, advanceTime } as DocumentModificationContext); // TODO: improve upstream types to allow this without type assertion
}
}
override async resetAll() {
for (const c of this.combatants) {
c.data.update({ initiative: null });
}
return this.update(
{ turn: 0, combatants: this.combatants.toObject(), flags: { [packageId]: { started: false } } },
{ diff: false },
);
}
override setupTurns(): this['turns'] {
const turns = this.combatants.contents.sort(this._sortCombatants);
const c = turns[0];
this.current = {
round: this.round,
turn: 0,
combatantId: c?.id ?? null,
tokenId: c?.data.tokenId ?? null,
};
return (this.turns = turns);
}
override async startCombat(): Promise<this | undefined> {
const hasCombatantWithTickValue = this.combatants.find(
(combatant) => !combatant.isDefeated && combatant.initiative !== null,
);
if (!hasCombatantWithTickValue) {
ui.notifications?.warn('TICKWERK.WarningCannotStartCombat', { localize: true });
return this;
}
return this.setFlag(packageId, 'started', true);
}
protected override _sortCombatants(a: TickwerkCombatant, b: TickwerkCombatant): number {
const da = a.isDefeated ? 1 : 0;
const db = b.isDefeated ? 1 : 0;
const cd = da - db;
if (cd !== 0) return cd;
const wa = a.waiting ? 1 : 0;
const wb = b.waiting ? 1 : 0;
const cw = wa - wb;
if (cw !== 0) return cw;
const ia = a.initiative ?? Infinity;
const ib = b.initiative ?? Infinity;
const ci = ia - ib;
if (ci !== 0) return ci;
const tba = a.getFlag(packageId, 'tieBreaker') ?? 0;
const tbb = b.getFlag(packageId, 'tieBreaker') ?? 0;
const ctb = tba - tbb;
if (ctb !== 0) return ctb;
return (b.id ?? '') > (a.id ?? '') ? 1 : -1;
}
};
};
declare global {
interface FlagConfig {
Combat: {
tickwerk?: {
started?: boolean;
};
};
}
}

View file

@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// 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';
export const registerCombatantFunctionality = () => {
CONFIG.Combatant.documentClass = CombatantMixin(CONFIG.Combatant.documentClass);
};
const CombatantMixin = (BaseCombatant: typeof Combatant) => {
return class TickwerkCombatant extends BaseCombatant {
/**
* An temporary property to make changes to the initiative available to other instances in their `_pre…` methods.
*/
_newInitiative: number | null | undefined;
/**
* An temporary property to make changes to the tieBreaker available to other instances in their `_pre…` methods.
*/
_newTieBreaker: number | undefined;
/***
* Is this combatant currently waiting?
*/
get waiting(): boolean {
return this.getFlag(packageId, 'waiting') ?? false;
}
toggleWaiting(): Promise<this | undefined> {
return this.update({ [`flags.${packageId}.waiting`]: !this.waiting, initiative: this.parent?.round });
}
protected override async _preCreate(...args: Parameters<Combatant['_preCreate']>): Promise<void> {
await super._preCreate(...args);
await this.#updateTieBreakerData(args[0]);
}
protected override async _preUpdate(...args: Parameters<Combatant['_preUpdate']>): Promise<void> {
await super._preUpdate(...args);
await this.#updateTieBreakerData(args[0]);
}
protected override _onCreate(): void {
this._newInitiative = undefined;
this._newTieBreaker = undefined;
}
protected override _onUpdate(): void {
this._newInitiative = undefined;
this._newTieBreaker = undefined;
}
async #updateTieBreakerData(data: DeepPartial<CombatantDataConstructorData>): Promise<void> {
if ('initiative' in data) {
const combatantsWithSameTickValue =
this.parent?.combatants.filter((combatant) => {
const otherInitiative =
combatant._newInitiative !== undefined ? combatant._newInitiative : combatant.initiative;
return otherInitiative === data.initiative;
}) ?? [];
const tieBreaker = await this.#getTieBreaker(combatantsWithSameTickValue);
setProperty(data, `flags.${packageId}.tieBreaker`, tieBreaker);
this._newInitiative = data.initiative;
this._newTieBreaker = tieBreaker;
}
}
async #getTieBreaker(combatants: TickwerkCombatant[]): Promise<number> {
const getTieBreaker = CONFIG.tickwerk?.getTieBreaker ?? defaultGetTieBreaker;
return getTieBreaker(this, combatants);
}
protected override _getInitiativeFormula(): string {
const getInitiativeFormula = CONFIG.tickwerk?.getInitiativeFormula;
if (getInitiativeFormula) return getInitiativeFormula(this);
return super._getInitiativeFormula();
}
};
};
const defaultGetTieBreaker = async (combatant: TickwerkCombatant, combatants: TickwerkCombatant[]): Promise<number> => {
if (combatants.length === 0) return 0;
const tieBreakers = combatants.map((combatant) => {
return (
(combatant._newTieBreaker !== undefined
? combatant._newTieBreaker
: combatant.getFlag(packageId, 'tieBreaker')) ?? 0
);
});
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;
};
}
}

10
src/helpers.ts Normal file
View file

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
export const getGame = (): Game => {
if (!(game instanceof Game)) {
throw new Error('game is not initialized yet.');
}
return game;
};

26
src/logger.ts Normal file
View file

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { packageId } from './constants';
const loggingContext = packageId;
const loggingSeparator = '|';
type LogLevel = 'debug' | 'info' | 'warning' | 'error';
type LoggingFunction = (...data: unknown[]) => void;
const getLoggingFunction = (type: LogLevel = 'info'): LoggingFunction => {
const log = { debug: console.debug, info: console.info, warning: console.warn, error: console.error }[type];
return (...data: unknown[]) => log(loggingContext, loggingSeparator, ...data);
};
const logger = Object.freeze({
debug: getLoggingFunction('debug'),
info: getLoggingFunction('info'),
warn: getLoggingFunction('warning'),
error: getLoggingFunction('error'),
getLoggingFunction,
});
export default logger;

27
src/systems/ds4.ts Normal file
View file

@ -0,0 +1,27 @@
import { packageId } from '../constants';
import type { TickwerkCombatant } from '../data/documents/combatant';
export const registerDS4SpecificFunctionality = () => {
if (CONFIG.tickwerk === undefined) CONFIG.tickwerk = {};
foundry.utils.mergeObject(CONFIG.tickwerk, { getTieBreaker, getInitiativeFormula });
};
const getTieBreaker = async (combatant: TickwerkCombatant, combatants: TickwerkCombatant[]): Promise<number> => {
if (combatants.length === 0) return 0;
const tieBreakers = combatants.map((combatant) => {
return (
(combatant._newTieBreaker !== undefined
? combatant._newTieBreaker
: combatant.getFlag(packageId, 'tieBreaker')) ?? 0
);
});
return Math.max(...tieBreakers) + 1;
};
const getInitiativeFormula = (combatant: TickwerkCombatant) => {
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})`;
};

9
src/systems/index.ts Normal file
View file

@ -0,0 +1,9 @@
import { getGame } from '../helpers';
import { registerDS4SpecificFunctionality } from './ds4';
export const registerSystemSpecificFunctionality = () => {
switch (getGame().system.id) {
case 'ds4':
registerDS4SpecificFunctionality();
}
};

19
src/tickwerk.ts Normal file
View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import '../styles/tickwerk.scss';
import { registerCombatTrackerFunctionality } from './apps/sidebar/combat-tracker';
import { registerActiveEffectFunctionality } from './data/documents/active-effect';
import { registerCombatFunctionality } from './data/documents/combat';
import { registerCombatantFunctionality } from './data/documents/combatant';
import { registerSystemSpecificFunctionality } from './systems';
Hooks.once('init', () => {
registerActiveEffectFunctionality();
registerCombatantFunctionality();
registerCombatFunctionality();
registerCombatTrackerFunctionality();
registerSystemSpecificFunctionality();
});

20
src/utils.ts Normal file
View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
const defaultMessage =
'There has been an unexpected error in the Tickwerk module. For more details, please take a look at the console (F12).';
/**
* Tests if the given `value` is truthy.
*
* If it is not truthy, an {@link Error} is thrown, which depends on the given `message` parameter:
* - If `message` is a string`, it is used to construct a new {@link Error} which then is thrown.
* - If `message` is an instance of {@link Error}, it is thrown.
* - If `message` is `undefined`, an {@link Error} with a default message is thrown.
*/
export const enforce = (value: unknown, message: string | Error = defaultMessage): asserts value => {
if (!value) {
throw message instanceof Error ? message : new Error(message);
}
};

9
styles/tickwerk.scss Normal file
View file

@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2022 Johannes Loher
*
* SPDX-License-Identifier: MIT
*/
#combat li.combatant .tickwerk-token-initiative {
flex: 0 0 60px;
}

225
template.json Normal file
View file

@ -0,0 +1,225 @@
{
"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
}
}
}

6
template.json.license Normal file
View file

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

View file

@ -0,0 +1,118 @@
<section class="tab sidebar-tab directory flexcol" id="combat" data-tab="combat">
<header id="combat-round">
{{#if user.isGM}}
<nav class="encounters flexrow">
<a class="combat-create" title="{{localize '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}}>
<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}}>
<i class="fas fa-caret-right"></i>
</a>
{{/if}}
<a class="combat-control" title="{{localize '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}}">
{{#if user.isGM}}
<a class="combat-control" title="{{localize '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}}>
<i class="fas fa-users-cog"></i>
</a>
{{/if}}
{{#if combatCount}}
{{#if combat.started}}
<h3 class="encounter-title">{{localize 'TICKWERK.Tick'}} {{combat.tickValue}}</h3>
{{else}}
<h3 class="encounter-title">{{localize 'COMBAT.NotStarted'}}</h3>
{{/if}}
{{else}}
<h3 class="encounter-title">{{localize "COMBAT.None"}}</h3>
{{/if}}
{{#if user.isGM}}
<a class="combat-control" title="{{localize 'COMBAT.InitiativeReset'}}" data-control="resetAll"
{{#unless hasCombat}}disabled{{/unless}}>
<i class="fas fa-undo"></i>
</a>
<a class="combat-control" title="{{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">
<i class="fas fa-cog"></i>
</a>
{{/if}}
</nav>
</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}}"/>
<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">
<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>
{{/if}}
<div class="token-effects">
{{#each this.effects}}
<img class="token-effect" src="{{this}}"/>
{{/each}}
</div>
</div>
</div>
{{#if this.hasResource}}
<div class="token-resource">
<span class="resource">{{this.resource}}</span>
</div>
{{/if}}
<div class="token-initiative tickwerk-token-initiative">
{{#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>
{{/if}}
</div>
</li>
{{/each}}
</ol>
<nav id="combat-controls" class="directory-footer flexrow">
{{#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>
{{else}}
<a class="combat-control center" title="{{localize 'COMBAT.Begin'}}" 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>
{{/if}}
{{/if}}
</nav>
</section>

85
tools/bump-version.js Normal file
View file

@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import fs from 'fs-extra';
import semver from 'semver';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
const repository = 'dungeonslayers/tickwerk';
const gitlabURL = 'https://git.f3l.de';
const getLicenseURL = (version) => `${gitlabURL}/${repository}/-/raw/${version}/LICENSE.md`;
const getReadmeURL = (version) => `${gitlabURL}/${repository}/-/raw/${version}/README.md`;
const getChangelogURL = (version) => `${gitlabURL}/${repository}/-/releases/${version}`;
const getDownloadURL = (version) => `${gitlabURL}/${repository}/-/releases/${version}/downloads/module.zip`;
const manifestPath = './module.json';
/**
* Get the contents of the manifest file as object.
* @returns {unknown} An object describing the manifest
*/
function getManifest() {
if (fs.existsSync(manifestPath)) {
return fs.readJSONSync(manifestPath);
}
}
/**
* Get the target version based on on the current version and the argument passed as release.
* @param {string} currentVersion The current version
* @param {semver.ReleaseType | string} release Either a semver release type or a valid semver version
* @returns {string | null} The target version
*/
function getTargetVersion(currentVersion, release) {
if (['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'].includes(release)) {
return semver.inc(currentVersion, release);
} else {
return semver.valid(release);
}
}
/**
* Update version and download URL.
* @param {semver.ReleaseType | string} release Either a semver release type or a valid semver version
*/
function bumpVersion(release) {
if (!release) {
throw new Error('Missing release type');
}
const packageJson = fs.readJSONSync('package.json');
const manifest = getManifest();
if (!manifest) throw new Error('Manifest JSON not found');
const currentVersion = packageJson.version;
const targetVersion = getTargetVersion(currentVersion, release);
if (!targetVersion) {
throw new Error('Incorrect version arguments');
}
if (targetVersion === currentVersion) {
throw new Error('Target version is identical to current version');
}
console.log(`Bumping version number to '${targetVersion}'`);
packageJson.version = targetVersion;
fs.writeJSONSync('package.json', packageJson, { spaces: 4 });
manifest.license = getLicenseURL(targetVersion);
manifest.readme = getReadmeURL(targetVersion);
manifest.changelog = getChangelogURL(targetVersion);
manifest.version = targetVersion;
manifest.download = getDownloadURL(targetVersion);
fs.writeJSONSync(manifestPath, manifest, { spaces: 4 });
}
const argv = yargs(hideBin(process.argv)).usage('Usage: $0').option('release', {
alias: 'r',
type: 'string',
demandOption: true,
description: 'Either a semver release type or a valid semver version',
}).argv;
const release = argv.r;
bumpVersion(release);

9
tools/const.js Normal file
View file

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
export const name = 'tickwerk';
export const sourceDirectory = './src';
export const distDirectory = './dist';
export const destinationDirectory = 'modules';
export const foundryconfigFile = './foundryconfig.json';

55
tools/link-package.js Normal file
View file

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
import fs from 'fs-extra';
import path from 'node:path';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { destinationDirectory, distDirectory, foundryconfigFile, name } from './const.js';
/**
* Get the data path of Foundry VTT based on what is configured in the {@link foundryconfigFile}.
*/
function getDataPath() {
const config = fs.readJSONSync(foundryconfigFile);
if (config?.dataPath) {
if (!fs.existsSync(path.resolve(config.dataPath))) {
throw new Error('User data path invalid, no Data directory found');
}
return path.resolve(config.dataPath);
} else {
throw new Error(`No user data path defined in ${foundryconfigFile}`);
}
}
/**
* Link the built package to the user data folder.
* @param {boolean} clean Whether to remove the link instead of creating it
*/
async function linkPackage(clean) {
if (!fs.existsSync(path.resolve('module.json'))) {
throw new Error('Could not find module.json');
}
const linkDirectory = path.resolve(getDataPath(), 'Data', destinationDirectory, name);
if (clean) {
console.log(`Removing link to built package at ${linkDirectory}.`);
await fs.remove(linkDirectory);
} else if (!fs.existsSync(linkDirectory)) {
console.log(`Linking built package to ${linkDirectory}.`);
await fs.ensureDir(path.resolve(linkDirectory, '..'));
await fs.symlink(path.resolve('.', distDirectory), linkDirectory);
}
}
const argv = yargs(hideBin(process.argv)).usage('Usage: $0').option('clean', {
alias: 'c',
type: 'boolean',
default: false,
description: 'Remove the link instead of creating it',
}).argv;
const clean = argv.c;
await linkPackage(clean);

4
tsconfig.eslint.json Normal file
View file

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

View file

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

16
tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"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"]
}

4
tsconfig.json.license Normal file
View file

@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2021 Johannes Loher
SPDX-FileCopyrightText: 2021 Oliver Rümpelein
SPDX-License-Identifier: MIT

8718
yarn.lock Normal file

File diff suppressed because it is too large Load diff

3
yarn.lock.license Normal file
View file

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