Compare commits
126 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 703dc83681 | |||
| 41034854eb | |||
| 382ca50bb5 | |||
| e99016e433 | |||
| 361a2004d8 | |||
| 44977c95cc | |||
| c6f14b3c21 | |||
| 022b6c5b31 | |||
| cb334f41de | |||
| cab29d9cd6 | |||
| 6997c736dc | |||
| b428eb3bf6 | |||
| c014e17da2 | |||
| 9ea417ddc1 | |||
| 804c4b3984 | |||
| c113c326c6 | |||
| 6a2cc1170d | |||
| d60448640f | |||
| 860c8b619a | |||
| df0c69c731 | |||
| bb095a9b4e | |||
| 1bf6cbbd45 | |||
| c6598ac5fa | |||
| fd10ba402d | |||
| f500152ba7 | |||
| bf579a3451 | |||
| 5c1985c4ab | |||
| 213996ab0a | |||
| 53f35562ad | |||
| d3f9c4c376 | |||
| 9e3bc775b4 | |||
| 7245e89c62 | |||
| a4ae2aefca | |||
| 6e5422e08b | |||
| cd3b5998dd | |||
| 871c820f94 | |||
| 2ddcda676e | |||
| 45de56650a | |||
| d018aea4f1 | |||
| d7db9cb2df | |||
| 41d3541c4b | |||
| 5e0028cdd6 | |||
| a63f0e02d9 | |||
| bb5e27af87 | |||
| ea57941472 | |||
| 39d122a882 | |||
| aa7c231e58 | |||
| a06934538e | |||
| 4b121c1f0f | |||
| 351300651b | |||
| 031fdb4a40 | |||
| 760009c9ba | |||
| 07a55e9064 | |||
| a4355c608a | |||
| 4275909dc8 | |||
| 7841e04dfc | |||
| 2146d51fde | |||
| bb616dbec2 | |||
| 92ad2607cd | |||
| 72a612d8a9 | |||
| 14e53455c6 | |||
| 49784448e8 | |||
| eac6a02c04 | |||
| 03b647cac1 | |||
| 1b986da6d4 | |||
| 5c030c680d | |||
| f1521992a2 | |||
| cf89b53b3b | |||
| 9b5b4bb9d1 | |||
| caf6dfa4a3 | |||
| 6c150b9b0e | |||
| 0179121c87 | |||
| 5b63688834 | |||
| 2eeeae9eef | |||
| 57f9c347ff | |||
| 9723ea8bdc | |||
| 7cb5e49d6d | |||
| 786bd68c35 | |||
| 088b8c6f5d | |||
| 00692431cd | |||
| 834f169a80 | |||
| 9057cbd682 | |||
| 01e046f916 | |||
| 6df0780676 | |||
| 58893f46db | |||
| 0362342419 | |||
| 5770abb7e8 | |||
| a242101b5b | |||
| 47b68621c1 | |||
| e79bd4d505 | |||
| db4f57fc90 | |||
| 723bcf8735 | |||
| bd301d69fb | |||
| b6ab0a229a | |||
|
|
bfd408ef0b | ||
|
|
36811b268c | ||
|
|
ce6ac8a93b | ||
|
|
8632054e63 | ||
| f7fee99b44 | |||
|
|
a7f91babf7 | ||
|
|
696f9e8261 | ||
|
|
72ebc0354d | ||
|
|
4e304f7d22 | ||
|
|
6081b8f9e8 | ||
|
|
c7c0deaec7 | ||
|
|
48e40538dc | ||
|
|
6866bea131 | ||
|
|
65cc95c35c | ||
|
|
cb266b3c1e | ||
| a50d0e8609 | |||
|
|
b683e8b5a0 | ||
|
|
baaafcccfc | ||
|
|
c29fa3e017 | ||
|
|
dff8a46ebb | ||
|
|
c50e88e483 | ||
|
|
a98af33477 | ||
|
|
171a133563 | ||
|
|
ca267868a1 | ||
|
|
df9a63073a | ||
|
|
cd3f076b7d | ||
| da57b12800 | |||
|
|
92e7ec1c72 | ||
| f3a3a65be1 | |||
|
|
865bf87b25 | ||
| 0bc3594672 | |||
|
|
5cc94f9185 |
58 changed files with 4397 additions and 3509 deletions
2
.env.template
Normal file
2
.env.template
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# The absolute path to the Foundry installation to create symlinks to
|
||||||
|
FOUNDRY_ROOT=""
|
||||||
96
.forgejo/workflows/draft-release.yaml
Normal file
96
.forgejo/workflows/draft-release.yaml
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
on: [ workflow_dispatch ]
|
||||||
|
jobs:
|
||||||
|
create-artifacts:
|
||||||
|
name: "Create artifacts"
|
||||||
|
runs-on: act
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm clean-install
|
||||||
|
|
||||||
|
- id: version
|
||||||
|
run: cat system.json | echo version=`jq -r ".version"` >> "$FORGEJO_OUTPUT"
|
||||||
|
|
||||||
|
- name: Assert that the tag doesn't exist
|
||||||
|
run: node scripts/tagExists.mjs
|
||||||
|
env:
|
||||||
|
TAG_NAME: "v${{steps.version.outputs.version}}"
|
||||||
|
|
||||||
|
# Compendia steps
|
||||||
|
- name: Build compendia
|
||||||
|
run: "npm run data:build"
|
||||||
|
- name: Remove compendia source
|
||||||
|
run: "rm -rf packs/**/_source"
|
||||||
|
|
||||||
|
- name: Compress files
|
||||||
|
run: zip -r release.zip langs module styles templates README.md assets
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
system.json
|
||||||
|
release.zip
|
||||||
|
scripts/*.mjs
|
||||||
|
package-lock.json
|
||||||
|
package.json
|
||||||
|
retention-days: 7
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
|
forgejo-release:
|
||||||
|
name: "Create Forgejo release"
|
||||||
|
runs-on: act
|
||||||
|
needs:
|
||||||
|
- create-artifacts
|
||||||
|
if: vars.RELEASE_TO_FORGEJO == 'yes'
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: https://data.forgejo.org/forgejo/download-artifact@v4
|
||||||
|
with:
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm i
|
||||||
|
|
||||||
|
- id: version
|
||||||
|
run: cat system.json | echo version=`jq -r ".version"` >> "$FORGEJO_OUTPUT"
|
||||||
|
|
||||||
|
- name: Update manifest
|
||||||
|
run: node scripts/prepareManifest.mjs
|
||||||
|
env:
|
||||||
|
DOWNLOAD_URL: "${{forgejo.server_url}}/${{forgejo.repository}}/releases/download/v${{steps.version.outputs.version}}/release.zip"
|
||||||
|
LATEST_URL: "${{forgejo.server_url}}/${{forgejo.repository}}/releases/download/latest/system.json"
|
||||||
|
|
||||||
|
- name: Add manifest into release archive
|
||||||
|
run: zip release.zip --update system.json
|
||||||
|
|
||||||
|
- name: Upload archive to s3
|
||||||
|
run: node scripts/uploadToS3.mjs
|
||||||
|
env:
|
||||||
|
TAG: "v${{steps.version.outputs.version}}"
|
||||||
|
FILE: "release.zip"
|
||||||
|
S3_BUCKET: "${{vars.S3_BUCKET}}"
|
||||||
|
S3_REGION: "${{vars.S3_REGION}}"
|
||||||
|
S3_KEY: "${{secrets.S3_KEY}}"
|
||||||
|
S3_SECRET: "${{secrets.S3_SECRET}}"
|
||||||
|
S3_ENDPOINT: "${{vars.S3_ENDPOINT}}"
|
||||||
|
|
||||||
|
- name: Upload manifest to s3
|
||||||
|
run: node scripts/uploadToS3.mjs
|
||||||
|
env:
|
||||||
|
TAG: "v${{steps.version.outputs.version}}"
|
||||||
|
FILE: "system.json"
|
||||||
|
S3_BUCKET: "${{vars.S3_BUCKET}}"
|
||||||
|
S3_REGION: "${{vars.S3_REGION}}"
|
||||||
|
S3_KEY: "${{secrets.S3_KEY}}"
|
||||||
|
S3_SECRET: "${{secrets.S3_SECRET}}"
|
||||||
|
S3_ENDPOINT: "${{vars.S3_ENDPOINT}}"
|
||||||
|
|
||||||
|
- name: Create draft release
|
||||||
|
run: node scripts/createForgejoRelease.mjs
|
||||||
|
env:
|
||||||
|
TAG: "v${{steps.version.outputs.version}}"
|
||||||
|
CDN_URL: "${{vars.CDN_URL}}"
|
||||||
9
.forgejo/workflows/publish-release.yaml
Normal file
9
.forgejo/workflows/publish-release.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
jobs:
|
||||||
|
release-to-foundry:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- name: retrieve release URLS
|
||||||
|
- name: publish to Foundry
|
||||||
55
.github/workflows/draft-release.yaml
vendored
55
.github/workflows/draft-release.yaml
vendored
|
|
@ -1,55 +0,0 @@
|
||||||
name: Create Draft Release
|
|
||||||
on: [workflow_dispatch]
|
|
||||||
jobs:
|
|
||||||
everything:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Checkout the repository
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Install node and NPM
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
|
|
||||||
# Install required packages
|
|
||||||
- run: npm install
|
|
||||||
|
|
||||||
- name: Reading the system.json for the version
|
|
||||||
id: "version"
|
|
||||||
run: cat system.json | echo version=`jq -r ".version"` >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Check that tag doesn't exist
|
|
||||||
- uses: mukunku/tag-exists-action@v1.5.0
|
|
||||||
id: check-tag
|
|
||||||
with:
|
|
||||||
tag: "v${{ steps.version.outputs.version }}"
|
|
||||||
|
|
||||||
- name: "Ensure that the tag doesn't exist"
|
|
||||||
if: ${{ steps.check-tag.outputs.exists == 'true' }}
|
|
||||||
run: exit 1
|
|
||||||
|
|
||||||
# Compile the stuff that needs to be compiled
|
|
||||||
- run: npm run build
|
|
||||||
- run: node scripts/buildCompendia.mjs
|
|
||||||
|
|
||||||
- name: Move system.json to a temp file
|
|
||||||
id: manifest-move
|
|
||||||
run: mv system.json system.temp.json
|
|
||||||
|
|
||||||
- name: Update the download property in the manifest
|
|
||||||
id: manifest-update
|
|
||||||
run: cat system.temp.json | jq -r --tab '.download = "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/release.zip"' > system.json
|
|
||||||
|
|
||||||
- name: Create the zip
|
|
||||||
run: zip -r release.zip langs module styles templates system.json README.md assets
|
|
||||||
|
|
||||||
- name: Create the draft release
|
|
||||||
uses: ncipollo/release-action@v1
|
|
||||||
with:
|
|
||||||
tag: "v${{ steps.version.outputs.version }}"
|
|
||||||
commit: ${{ github.ref }}
|
|
||||||
draft: true
|
|
||||||
body: <img aria-hidden="true" src="https://img.shields.io/github/downloads/${{ github.repository }}/v${{ steps.version.outputs.version }}/release.zip?style=flat-square&color=%2300aa00">
|
|
||||||
generateReleaseNotes: true
|
|
||||||
artifacts: "release.zip,system.json"
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
deprecated
|
deprecated
|
||||||
|
.env
|
||||||
|
/foundry
|
||||||
|
|
|
||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -8,9 +8,9 @@
|
||||||
"git.branchProtection": [],
|
"git.branchProtection": [],
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"*.lock": true,
|
"*.lock": true,
|
||||||
".styles": false,
|
|
||||||
"node_modules": true,
|
"node_modules": true,
|
||||||
"packs": true,
|
"packs": true,
|
||||||
|
"foundry": true
|
||||||
},
|
},
|
||||||
"html.customData": [
|
"html.customData": [
|
||||||
"./.vscode/components.html-data.json"
|
"./.vscode/components.html-data.json"
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -2,3 +2,17 @@
|
||||||
This is an intentionally bare-bones system that features a text-only character
|
This is an intentionally bare-bones system that features a text-only character
|
||||||
sheet, allowing the playing of games that may not otherwise have a Foundry system
|
sheet, allowing the playing of games that may not otherwise have a Foundry system
|
||||||
implementation.
|
implementation.
|
||||||
|
|
||||||
|
## Unlisted Releases
|
||||||
|
Some of the versions of Text-Based Actors are not available in the [Releases list](https://git.varify.ca/Foundry/taf/releases),
|
||||||
|
these versions are installable manually by using the appropriate manifest link
|
||||||
|
below:
|
||||||
|
|
||||||
|
| Version | Manifest URL
|
||||||
|
| ------- | ------------
|
||||||
|
| v2.2.1 | https://cdn.varify.ca/Foundry/taf/v2.2.1/system.json
|
||||||
|
| v2.2.0 | https://cdn.varify.ca/Foundry/taf/v2.2.0/system.json
|
||||||
|
| v2.1.0 | https://cdn.varify.ca/Foundry/taf/v2.1.0/system.json
|
||||||
|
| v2.0.0 | https://cdn.varify.ca/Foundry/taf/v2.0.0/system.json
|
||||||
|
| v1.1.0 | https://cdn.varify.ca/Foundry/taf/v1.1.0/system.json
|
||||||
|
| v1.0.0 | https://cdn.varify.ca/Foundry/taf/v1.0.0/system.json
|
||||||
|
|
|
||||||
3
assets/icons/disconnected.svg
Normal file
3
assets/icons/disconnected.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m91.562 48.438c0.9375-2.8125 1.5625-5.625 1.5625-8.4375 0-13.438-10.938-24.375-24.062-24.375-6.875 0-13.75 3.125-18.125 8.125-3.4375-2.8125-7.8125-4.375-12.5-4.375-11.25 0-20.312 9.0625-20.312 20.312 0 1.25 0 2.5 0.3125 3.75-9.6875 1.5625-16.562 10-16.562 20 0 11.25 9.0625 20.312 20.312 20.312h56.25c11.25 0 20.312-9.0625 20.312-20.312-0.3125-5.3125-2.8125-10.938-7.1875-15zm-28.125 13.125c0.9375 0.9375 0.9375 2.5 0 3.4375-0.3125 0.3125-0.9375 0.625-1.5625 0.625s-1.25-0.3125-1.5625-0.625l-7.8125-7.8125-7.8125 7.8125c-0.3125 0.3125-0.9375 0.625-1.5625 0.625s-1.25-0.3125-1.5625-0.625c-0.9375-0.9375-0.9375-2.5 0-3.4375l7.8125-7.8125-7.8125-7.8125c-0.9375-0.9375-0.9375-2.5 0-3.4375s2.5-0.9375 3.4375 0l7.8125 7.8125 7.8125-7.8125c0.9375-0.9375 2.5-0.9375 3.4375 0s0.9375 2.5 0 3.4375l-7.8125 7.8125z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 900 B |
5
augments.d.ts
vendored
5
augments.d.ts
vendored
|
|
@ -1,3 +1,8 @@
|
||||||
|
declare global {
|
||||||
|
class Hooks extends foundry.helpers.Hooks {};
|
||||||
|
const fromUuid = foundry.utils.fromUuid;
|
||||||
|
}
|
||||||
|
|
||||||
interface Actor {
|
interface Actor {
|
||||||
/** The system-specific data */
|
/** The system-specific data */
|
||||||
system: any;
|
system: any;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import stylistic from "@stylistic/eslint-plugin";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
// Tell eslint to ignore files that I don't mind being formatted slightly differently
|
// Tell eslint to ignore files that I don't mind being formatted slightly differently
|
||||||
{ ignores: [ `scripts/` ] },
|
{ ignores: [ `scripts/`, `foundry/*` ] },
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
|
|
@ -16,6 +16,7 @@ export default [
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
CONFIG: `writable`,
|
CONFIG: `writable`,
|
||||||
|
CONST: `readonly`,
|
||||||
game: `readonly`,
|
game: `readonly`,
|
||||||
Handlebars: `readonly`,
|
Handlebars: `readonly`,
|
||||||
Hooks: `readonly`,
|
Hooks: `readonly`,
|
||||||
|
|
@ -72,7 +73,7 @@ export default [
|
||||||
"@stylistic/eol-last": `warn`,
|
"@stylistic/eol-last": `warn`,
|
||||||
"@stylistic/operator-linebreak": [`warn`, `before`],
|
"@stylistic/operator-linebreak": [`warn`, `before`],
|
||||||
"@stylistic/indent": [`warn`, `tab`],
|
"@stylistic/indent": [`warn`, `tab`],
|
||||||
"@stylistic/brace-style": [`warn`, `1tbs`, { "allowSingleLine": true }],
|
"@stylistic/brace-style": [`off`],
|
||||||
"@stylistic/quotes": [`warn`, `backtick`, { "avoidEscape": true }],
|
"@stylistic/quotes": [`warn`, `backtick`, { "avoidEscape": true }],
|
||||||
"@stylistic/comma-dangle": [`warn`, { arrays: `always-multiline`, objects: `always-multiline`, imports: `always-multiline`, exports: `always-multiline`, functions: `always-multiline` }],
|
"@stylistic/comma-dangle": [`warn`, { arrays: `always-multiline`, objects: `always-multiline`, imports: `always-multiline`, exports: `always-multiline`, functions: `always-multiline` }],
|
||||||
"@stylistic/comma-style": [`warn`, `last`],
|
"@stylistic/comma-style": [`warn`, `last`],
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,19 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"module": "es2022",
|
||||||
|
"target": "es2022",
|
||||||
"types": [
|
"types": [
|
||||||
"./augments.d.ts"
|
"./augments.d.ts"
|
||||||
]
|
],
|
||||||
}
|
"paths": {
|
||||||
|
"@client/*": ["./foundry/client/*"],
|
||||||
|
"@common/*": ["./foundry/common/*"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"module/**/*",
|
||||||
|
"foundry/client/client.mjs",
|
||||||
|
"foundry/client/global.d.mts",
|
||||||
|
"foundry/common/primitives/global.d.mts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,48 @@
|
||||||
},
|
},
|
||||||
"sheet-names": {
|
"sheet-names": {
|
||||||
"PlayerSheet": "Player Sheet"
|
"PlayerSheet": "Player Sheet"
|
||||||
|
},
|
||||||
|
"misc": {
|
||||||
|
"Key": "Key",
|
||||||
|
"Value": "Value",
|
||||||
|
"no-data-submitted": "No data submitted",
|
||||||
|
"data-query-notif-header": "Data Query Notification"
|
||||||
|
},
|
||||||
|
"Apps": {
|
||||||
|
"QueryStatus": {
|
||||||
|
"title": "Information Request Status",
|
||||||
|
"user-disconnected-tooltip": "This user is not logged in to Foundry",
|
||||||
|
"cancel-request": "Cancel Request",
|
||||||
|
"finish-early": "Finish Request Early",
|
||||||
|
"send-request": "Send Request"
|
||||||
|
},
|
||||||
|
"TAFDocumentSheetConfig": {
|
||||||
|
"Sizing": "Sizing",
|
||||||
|
"Width": {
|
||||||
|
"label": "Width"
|
||||||
|
},
|
||||||
|
"Height": {
|
||||||
|
"label": "Height"
|
||||||
|
},
|
||||||
|
"Resizable": {
|
||||||
|
"label": "Resizable"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"foundry": "Foundry",
|
||||||
|
"system": "Text-Based Actors"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sockets": {
|
||||||
|
"user-list-required": "A list fo users must be provided"
|
||||||
|
},
|
||||||
|
"notifs": {
|
||||||
|
"error": {
|
||||||
|
"missing-id": "An ID must be provided",
|
||||||
|
"invalid-socket": "Invalid socket data received, this means a module or system bug is present.",
|
||||||
|
"unknown-socket-event": "An unknown socket event was received: {event}",
|
||||||
|
"malformed-socket-payload": "Socket event \"{event}\" received with malformed payload. Details: {details}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,13 @@
|
||||||
import { Ask } from "./apps/Ask.mjs";
|
import { Ask } from "./apps/Ask.mjs";
|
||||||
import { AttributeManager } from "./apps/AttributeManager.mjs";
|
import { AttributeManager } from "./apps/AttributeManager.mjs";
|
||||||
import { PlayerSheet } from "./apps/PlayerSheet.mjs";
|
import { PlayerSheet } from "./apps/PlayerSheet.mjs";
|
||||||
|
import { QueryStatus } from "./apps/QueryStatus.mjs";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { attributeSorter } from "./utils/attributeSort.mjs";
|
import { attributeSorter } from "./utils/attributeSort.mjs";
|
||||||
import { DialogManager } from "./utils/DialogManager.mjs";
|
import { DialogManager } from "./utils/DialogManager.mjs";
|
||||||
|
import { localizer } from "./utils/localizer.mjs";
|
||||||
|
import { QueryManager } from "./utils/QueryManager.mjs";
|
||||||
import { toID } from "./utils/toID.mjs";
|
import { toID } from "./utils/toID.mjs";
|
||||||
|
|
||||||
const { deepFreeze } = foundry.utils;
|
const { deepFreeze } = foundry.utils;
|
||||||
|
|
@ -16,13 +19,16 @@ Object.defineProperty(
|
||||||
{
|
{
|
||||||
value: deepFreeze({
|
value: deepFreeze({
|
||||||
DialogManager,
|
DialogManager,
|
||||||
|
QueryManager,
|
||||||
Apps: {
|
Apps: {
|
||||||
Ask,
|
Ask,
|
||||||
AttributeManager,
|
AttributeManager,
|
||||||
PlayerSheet,
|
PlayerSheet,
|
||||||
|
QueryStatus,
|
||||||
},
|
},
|
||||||
utils: {
|
utils: {
|
||||||
attributeSorter,
|
attributeSorter,
|
||||||
|
localizer,
|
||||||
toID,
|
toID,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const validInputTypes = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export class Ask extends HandlebarsApplicationMixin(ApplicationV2) {
|
export class Ask extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
// #region Options
|
||||||
static DEFAULT_OPTIONS = {
|
static DEFAULT_OPTIONS = {
|
||||||
tag: `dialog`,
|
tag: `dialog`,
|
||||||
classes: [
|
classes: [
|
||||||
|
|
@ -47,7 +48,9 @@ export class Ask extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
template: filePath(`templates/Ask/controls.hbs`),
|
template: filePath(`templates/Ask/controls.hbs`),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// #endregion Options
|
||||||
|
|
||||||
|
// #region Instance
|
||||||
_inputs = [];
|
_inputs = [];
|
||||||
alwaysUseAnswerObject = false;
|
alwaysUseAnswerObject = false;
|
||||||
|
|
||||||
|
|
@ -88,6 +91,7 @@ export class Ask extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
this._userOnConfirm = onConfirm;
|
this._userOnConfirm = onConfirm;
|
||||||
this._userOnClose = onClose;
|
this._userOnClose = onClose;
|
||||||
};
|
};
|
||||||
|
// #endregion Instance
|
||||||
|
|
||||||
// #region Lifecycle
|
// #region Lifecycle
|
||||||
async _onFirstRender() {
|
async _onFirstRender() {
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,8 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
|
||||||
};
|
};
|
||||||
|
|
||||||
static PARTS = {
|
static PARTS = {
|
||||||
attributes: {
|
attributes: { template: filePath(`templates/AttributeManager/attribute-list.hbs`) },
|
||||||
template: filePath(`templates/AttributeManager/attribute-list.hbs`),
|
controls: { template: filePath(`templates/AttributeManager/controls.hbs`) },
|
||||||
},
|
|
||||||
controls: {
|
|
||||||
template: filePath(`templates/AttributeManager/controls.hbs`),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
// #endregion Options
|
// #endregion Options
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { __ID__, filePath } from "../consts.mjs";
|
import { __ID__, filePath } from "../consts.mjs";
|
||||||
import { AttributeManager } from "./AttributeManager.mjs";
|
import { AttributeManager } from "./AttributeManager.mjs";
|
||||||
import { attributeSorter } from "../utils/attributeSort.mjs";
|
import { attributeSorter } from "../utils/attributeSort.mjs";
|
||||||
|
import { TAFDocumentSheetConfig } from "./TAFDocumentSheetConfig.mjs";
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
const { ActorSheetV2 } = foundry.applications.sheets;
|
const { ActorSheetV2 } = foundry.applications.sheets;
|
||||||
|
const { getProperty } = foundry.utils;
|
||||||
|
|
||||||
export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||||
|
|
||||||
|
|
@ -26,6 +28,7 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
manageAttributes: this.#manageAttributes,
|
manageAttributes: this.#manageAttributes,
|
||||||
|
configureSheet: this.#configureSheet,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -37,6 +40,30 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||||
// #endregion Options
|
// #endregion Options
|
||||||
|
|
||||||
// #region Lifecycle
|
// #region Lifecycle
|
||||||
|
_initializeApplicationOptions(options) {
|
||||||
|
const sizing = getProperty(options.document, `flags.${__ID__}.PlayerSheet.size`) ?? {};
|
||||||
|
|
||||||
|
options.window ??= {};
|
||||||
|
switch (sizing.resizable) {
|
||||||
|
case `false`:
|
||||||
|
options.window.resizable ??= false;
|
||||||
|
break;
|
||||||
|
case `true`:
|
||||||
|
options.window.resizable ??= true;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
options.position ??= {};
|
||||||
|
if (sizing.width) {
|
||||||
|
options.position.width ??= sizing.width;
|
||||||
|
};
|
||||||
|
if (sizing.height) {
|
||||||
|
options.position.height ??= sizing.height;
|
||||||
|
};
|
||||||
|
|
||||||
|
return super._initializeApplicationOptions(options);
|
||||||
|
};
|
||||||
|
|
||||||
_getHeaderControls() {
|
_getHeaderControls() {
|
||||||
const controls = super._getHeaderControls();
|
const controls = super._getHeaderControls();
|
||||||
|
|
||||||
|
|
@ -54,6 +81,12 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||||
|
|
||||||
return controls;
|
return controls;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
this.#attributeManager?.close();
|
||||||
|
this.#attributeManager = null;
|
||||||
|
return super.close();
|
||||||
|
};
|
||||||
// #endregion Lifecycle
|
// #endregion Lifecycle
|
||||||
|
|
||||||
// #region Data Prep
|
// #region Data Prep
|
||||||
|
|
@ -103,10 +136,29 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
||||||
// #endregion Data Prep
|
// #endregion Data Prep
|
||||||
|
|
||||||
// #region Actions
|
// #region Actions
|
||||||
|
#attributeManager = null;
|
||||||
/** @this {PlayerSheet} */
|
/** @this {PlayerSheet} */
|
||||||
static async #manageAttributes() {
|
static async #manageAttributes() {
|
||||||
const app = new AttributeManager({ document: this.actor });
|
this.#attributeManager ??= new AttributeManager({ document: this.actor });
|
||||||
await app.render({ force: true });
|
if (this.#attributeManager.rendered) {
|
||||||
|
await this.#attributeManager.bringToFront();
|
||||||
|
} else {
|
||||||
|
await this.#attributeManager.render({ force: true });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static async #configureSheet(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
if ( event.detail > 1 ) { return }
|
||||||
|
|
||||||
|
// const docSheetConfigWidth = TAFDocumentSheetConfig.DEFAULT_OPTIONS.position.width;
|
||||||
|
new TAFDocumentSheetConfig({
|
||||||
|
document: this.document,
|
||||||
|
position: {
|
||||||
|
top: this.position.top + 40,
|
||||||
|
left: this.position.left + ((this.position.width - 60) / 2),
|
||||||
|
},
|
||||||
|
}).render({ force: true });
|
||||||
};
|
};
|
||||||
// #endregion Actions
|
// #endregion Actions
|
||||||
};
|
};
|
||||||
|
|
|
||||||
111
module/apps/QueryStatus.mjs
Normal file
111
module/apps/QueryStatus.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { __ID__, filePath } from "../consts.mjs";
|
||||||
|
import { cancel, finish, get as getQuery, requery } from "../utils/QueryManager.mjs";
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
export class QueryStatus extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: [
|
||||||
|
__ID__,
|
||||||
|
`QueryStatus`,
|
||||||
|
],
|
||||||
|
position: {
|
||||||
|
width: 300,
|
||||||
|
height: `auto`,
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
title: `taf.Apps.QueryStatus.title`,
|
||||||
|
resizable: true,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
promptUser: this.promptUser,
|
||||||
|
finishEarly: this.finishEarly,
|
||||||
|
cancelRequest: this.cancelRequest,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
users: {
|
||||||
|
template: filePath(`templates/QueryStatus/users.hbs`),
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
template: filePath(`templates/QueryStatus/controls.hbs`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// #endregion Options
|
||||||
|
|
||||||
|
// #region Instance
|
||||||
|
/** @type {string} */
|
||||||
|
#requestID;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
requestID,
|
||||||
|
...opts
|
||||||
|
}) {
|
||||||
|
if (!requestID) {
|
||||||
|
Logger.error(`A requestID must be provided for QueryStatus applications`);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
super(opts);
|
||||||
|
this.#requestID = requestID;
|
||||||
|
};
|
||||||
|
|
||||||
|
get requestID() {
|
||||||
|
return this.#requestID;
|
||||||
|
};
|
||||||
|
// #endregion Instance
|
||||||
|
|
||||||
|
// #region Lifecycle
|
||||||
|
async _preparePartContext(partID) {
|
||||||
|
const ctx = {};
|
||||||
|
|
||||||
|
switch (partID) {
|
||||||
|
case `users`: {
|
||||||
|
this._prepareUsers(ctx);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareUsers(ctx) {
|
||||||
|
const query = getQuery(this.#requestID);
|
||||||
|
if (!query) { return };
|
||||||
|
|
||||||
|
const users = [];
|
||||||
|
for (const userID of query.users) {
|
||||||
|
const user = game.users.get(userID);
|
||||||
|
users.push({
|
||||||
|
id: userID,
|
||||||
|
name: user.name,
|
||||||
|
active: user.active,
|
||||||
|
answers: query.responses[userID] ?? null,
|
||||||
|
status: query.status[userID],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
ctx.users = users;
|
||||||
|
};
|
||||||
|
// #endregion Lifecycle
|
||||||
|
|
||||||
|
// #region Actions
|
||||||
|
/** @this {QueryStatus} */
|
||||||
|
static async promptUser($e, element) {
|
||||||
|
const userID = element.closest(`[data-user-id]`)?.dataset.userId;
|
||||||
|
if (!userID) { return };
|
||||||
|
requery(this.#requestID, [ userID ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {QueryStatus} */
|
||||||
|
static async cancelRequest() {
|
||||||
|
cancel(this.#requestID);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {QueryStatus} */
|
||||||
|
static async finishEarly() {
|
||||||
|
finish(this.#requestID);
|
||||||
|
};
|
||||||
|
// #endregion Actions
|
||||||
|
};
|
||||||
171
module/apps/TAFDocumentSheetConfig.mjs
Normal file
171
module/apps/TAFDocumentSheetConfig.mjs
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
import { __ID__, filePath } from "../consts.mjs";
|
||||||
|
import { getDefaultSizing } from "../utils/getSizing.mjs";
|
||||||
|
|
||||||
|
const { diffObject, expandObject, flattenObject } = foundry.utils;
|
||||||
|
const { DocumentSheetConfig } = foundry.applications.apps;
|
||||||
|
const { CONST } = foundry;
|
||||||
|
|
||||||
|
export class TAFDocumentSheetConfig extends DocumentSheetConfig {
|
||||||
|
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: [`taf`],
|
||||||
|
form: {
|
||||||
|
handler: this.#onSubmit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static get PARTS() {
|
||||||
|
const { form, footer } = super.PARTS;
|
||||||
|
return {
|
||||||
|
tabs: { template: `templates/generic/tab-navigation.hbs` },
|
||||||
|
foundryTab: {
|
||||||
|
...form,
|
||||||
|
template: filePath(`templates/TAFDocumentSheetConfig/foundry.hbs`),
|
||||||
|
templates: [ `templates/sheets/document-sheet-config.hbs` ],
|
||||||
|
},
|
||||||
|
systemTab: {
|
||||||
|
template: filePath(`templates/TAFDocumentSheetConfig/system.hbs`),
|
||||||
|
classes: [`standard-form`],
|
||||||
|
},
|
||||||
|
footer,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static TABS = {
|
||||||
|
main: {
|
||||||
|
initial: `system`,
|
||||||
|
labelPrefix: `taf.Apps.TAFDocumentSheetConfig.tabs`,
|
||||||
|
tabs: [
|
||||||
|
{ id: `system` },
|
||||||
|
{ id: `foundry` },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// #endregion Options
|
||||||
|
|
||||||
|
// #region Data Prep
|
||||||
|
async _preparePartContext(partID, context, options) {
|
||||||
|
this._prepareTabs(`main`);
|
||||||
|
|
||||||
|
context.meta = {
|
||||||
|
idp: this.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (partID) {
|
||||||
|
case `foundryTab`: {
|
||||||
|
await this._prepareFormContext(context, options);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
case `systemTab`: {
|
||||||
|
await this._prepareSystemSettingsContext(context, options);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
case `footer`: {
|
||||||
|
await this._prepareFooterContext(context, options);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
async _prepareSystemSettingsContext(context, _options) {
|
||||||
|
// Inherited values for placeholders
|
||||||
|
const defaults = getDefaultSizing();
|
||||||
|
context.placeholders = {
|
||||||
|
...defaults,
|
||||||
|
resizable: defaults.resizable ? `Resizable` : `Not Resizable`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom values from document itself
|
||||||
|
const sheetConfig = this.document.getFlag(__ID__, `PlayerSheet`) ?? {};
|
||||||
|
const sizing = sheetConfig.size ?? {};
|
||||||
|
context.values = {
|
||||||
|
width: sizing.width,
|
||||||
|
height: sizing.height,
|
||||||
|
resizable: sizing.resizable ?? ``,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static prep
|
||||||
|
context.resizeOptions = [
|
||||||
|
{ label: `Default (${context.placeholders.resizable})`, value: `` },
|
||||||
|
{ label: `Resizable`, value: `true` },
|
||||||
|
{ label: `No Resizing`, value: `false` },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
// #endregion Data Prep
|
||||||
|
|
||||||
|
// #region Actions
|
||||||
|
/** @this {TAFDocumentSheetConfig} */
|
||||||
|
static async #onSubmit(event, form, formData) {
|
||||||
|
const foundryReopen = await TAFDocumentSheetConfig.#submitFoundry.call(this, event, form, formData);
|
||||||
|
const systemReopen = await TAFDocumentSheetConfig.#submitSystem.call(this, event, form, formData);
|
||||||
|
if (foundryReopen || systemReopen) {
|
||||||
|
this.document._onSheetChange({ sheetOpen: true });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is mostly the form submission handler that foundry uses in
|
||||||
|
* DocumentSheetConfig, however because we clobber that in order to save our
|
||||||
|
* own config stuff as well, we need to duplicate Foundry's handling and tweak
|
||||||
|
* it a bit to make it work nicely with our custom saving.
|
||||||
|
*
|
||||||
|
* @this {TAFDocumentSheetConfig}
|
||||||
|
*/
|
||||||
|
static async #submitFoundry(_event, _form, formData) {
|
||||||
|
const { object } = formData;
|
||||||
|
const { documentName, type = CONST.BASE_DOCUMENT_TYPE } = this.document;
|
||||||
|
|
||||||
|
// Update themes.
|
||||||
|
const themes = game.settings.get(`core`, `sheetThemes`);
|
||||||
|
const defaultTheme = foundry.utils.getProperty(themes, `defaults.${documentName}.${type}`);
|
||||||
|
const documentTheme = themes.documents?.[this.document.uuid];
|
||||||
|
const themeChanged = (object.defaultTheme !== defaultTheme) || (object.theme !== documentTheme);
|
||||||
|
if (themeChanged) {
|
||||||
|
foundry.utils.setProperty(themes, `defaults.${documentName}.${type}`, object.defaultTheme);
|
||||||
|
themes.documents ??= {};
|
||||||
|
themes.documents[this.document.uuid] = object.theme;
|
||||||
|
await game.settings.set(`core`, `sheetThemes`, themes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sheets.
|
||||||
|
const { defaultClass } = this.constructor.getSheetClassesForSubType(documentName, type);
|
||||||
|
const sheetClass = this.document.getFlag(`core`, `sheetClass`) ?? ``;
|
||||||
|
const defaultSheetChanged = object.defaultClass !== defaultClass;
|
||||||
|
const documentSheetChanged = object.sheetClass !== sheetClass;
|
||||||
|
|
||||||
|
if (themeChanged || (game.user.isGM && defaultSheetChanged)) {
|
||||||
|
if (game.user.isGM && defaultSheetChanged) {
|
||||||
|
const setting = game.settings.get(`core`, `sheetClasses`);
|
||||||
|
foundry.utils.setProperty(setting, `${documentName}.${type}`, object.defaultClass);
|
||||||
|
await game.settings.set(`core`, `sheetClasses`, setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This causes us to manually rerender the sheet due to the theme or default
|
||||||
|
// sheet class changing resulting in no update making it to the client-document's
|
||||||
|
// _onUpdate handling
|
||||||
|
if (!documentSheetChanged) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the document-specific override.
|
||||||
|
if (documentSheetChanged) {
|
||||||
|
this.document.setFlag(`core`, `sheetClass`, object.sheetClass);
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @this {TAFDocumentSheetConfig} */
|
||||||
|
static async #submitSystem(_event, _form, formData) {
|
||||||
|
const { FLAGS: flags } = expandObject(formData.object);
|
||||||
|
const diff = flattenObject(diffObject(this.document.flags, flags));
|
||||||
|
const hasChanges = Object.keys(diff).length > 0;
|
||||||
|
if (hasChanges) {
|
||||||
|
await this.document.update({ flags });
|
||||||
|
};
|
||||||
|
return hasChanges;
|
||||||
|
};
|
||||||
|
// #endregion Actions
|
||||||
|
};
|
||||||
7
module/documents/Item.mjs
Normal file
7
module/documents/Item.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
const { Item } = foundry.documents;
|
||||||
|
|
||||||
|
export class TAFItem extends Item {
|
||||||
|
async _preCreate() {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -6,6 +6,7 @@ import { PlayerData } from "../data/Player.mjs";
|
||||||
|
|
||||||
// Documents
|
// Documents
|
||||||
import { TAFActor } from "../documents/Actor.mjs";
|
import { TAFActor } from "../documents/Actor.mjs";
|
||||||
|
import { TAFItem } from "../documents/Item.mjs";
|
||||||
import { TAFTokenDocument } from "../documents/Token.mjs";
|
import { TAFTokenDocument } from "../documents/Token.mjs";
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|
@ -16,6 +17,7 @@ import { __ID__ } from "../consts.mjs";
|
||||||
import helpers from "../handlebarsHelpers/_index.mjs";
|
import helpers from "../handlebarsHelpers/_index.mjs";
|
||||||
import { Logger } from "../utils/Logger.mjs";
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
import { registerCustomComponents } from "../apps/elements/_index.mjs";
|
import { registerCustomComponents } from "../apps/elements/_index.mjs";
|
||||||
|
import { registerSockets } from "../sockets/_index.mjs";
|
||||||
|
|
||||||
Hooks.on(`init`, () => {
|
Hooks.on(`init`, () => {
|
||||||
Logger.debug(`Initializing`);
|
Logger.debug(`Initializing`);
|
||||||
|
|
@ -25,6 +27,10 @@ Hooks.on(`init`, () => {
|
||||||
|
|
||||||
CONFIG.Actor.dataModels.player = PlayerData;
|
CONFIG.Actor.dataModels.player = PlayerData;
|
||||||
|
|
||||||
|
// We disable items in the system for now
|
||||||
|
CONFIG.Item.documentClass = TAFItem;
|
||||||
|
delete CONFIG.ui.sidebar.TABS.items;
|
||||||
|
|
||||||
foundry.documents.collections.Actors.registerSheet(
|
foundry.documents.collections.Actors.registerSheet(
|
||||||
__ID__,
|
__ID__,
|
||||||
PlayerSheet,
|
PlayerSheet,
|
||||||
|
|
@ -36,6 +42,7 @@ Hooks.on(`init`, () => {
|
||||||
|
|
||||||
registerWorldSettings();
|
registerWorldSettings();
|
||||||
|
|
||||||
|
registerSockets();
|
||||||
registerCustomComponents();
|
registerCustomComponents();
|
||||||
Handlebars.registerHelper(helpers);
|
Handlebars.registerHelper(helpers);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
6
module/hooks/userConnected.mjs
Normal file
6
module/hooks/userConnected.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { userActivity } from "../utils/QueryManager.mjs";
|
||||||
|
|
||||||
|
Hooks.on(`userConnected`, (user, connected) => {
|
||||||
|
if (user.isSelf) { return };
|
||||||
|
userActivity(user.id, connected);
|
||||||
|
});
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
import "./api.mjs";
|
import "./api.mjs";
|
||||||
import "./hooks/init.mjs";
|
import "./hooks/init.mjs";
|
||||||
|
import "./hooks/userConnected.mjs";
|
||||||
|
|
|
||||||
33
module/sockets/_index.mjs
Normal file
33
module/sockets/_index.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
import { queryCancel } from "./query/cancel.mjs";
|
||||||
|
import { queryNotify } from "./query/notify.mjs";
|
||||||
|
import { queryPrompt } from "./query/prompt.mjs";
|
||||||
|
import { querySubmit } from "./query/submit.mjs";
|
||||||
|
|
||||||
|
const events = {
|
||||||
|
// Data Request sockets
|
||||||
|
"query.cancel": queryCancel,
|
||||||
|
"query.notify": queryNotify,
|
||||||
|
"query.prompt": queryPrompt,
|
||||||
|
"query.submit": querySubmit,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function registerSockets() {
|
||||||
|
Logger.info(`Setting up socket listener`);
|
||||||
|
|
||||||
|
game.socket.on(`system.taf`, (data, userID) => {
|
||||||
|
const { event, payload } = data ?? {};
|
||||||
|
if (event == null || payload === undefined) {
|
||||||
|
ui.notifications.error(game.i18n.format(`taf.notifs.error.invalid-socket`));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (events[event] == null) {
|
||||||
|
ui.notifications.error(game.i18n.format(`taf.notifs.error.unknown-socket-event`, { event }));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = game.users.get(userID);
|
||||||
|
events[event](payload, user);
|
||||||
|
});
|
||||||
|
};
|
||||||
19
module/sockets/query/cancel.mjs
Normal file
19
module/sockets/query/cancel.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { DialogManager } from "../../utils/DialogManager.mjs";
|
||||||
|
import { localizer } from "../../utils/localizer.mjs";
|
||||||
|
|
||||||
|
export async function queryCancel(payload) {
|
||||||
|
const { id } = payload;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
ui.notifications.error(localizer(
|
||||||
|
`taf.notifs.error.malformed-socket-payload`,
|
||||||
|
{
|
||||||
|
event: `query.cancel`,
|
||||||
|
details: `taf.notifs.error.missing-id`,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
await DialogManager.close(id);
|
||||||
|
};
|
||||||
25
module/sockets/query/notify.mjs
Normal file
25
module/sockets/query/notify.mjs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { localizer } from "../../utils/localizer.mjs";
|
||||||
|
import { respondedToQueries } from "../../utils/QueryManager.mjs";
|
||||||
|
|
||||||
|
export function queryNotify(payload) {
|
||||||
|
const { id, userID, content, includeGM } = payload;
|
||||||
|
|
||||||
|
if (userID !== game.user.id) { return };
|
||||||
|
|
||||||
|
// Ensure that each user can only get one notification about a query
|
||||||
|
if (!respondedToQueries.has(id)) { return };
|
||||||
|
|
||||||
|
let whisper = [game.user.id];
|
||||||
|
if (includeGM) {
|
||||||
|
whisper = game.users.filter(u => u.isGM).map(u => u.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
ChatMessage.implementation.create({
|
||||||
|
flavor: localizer(`taf.misc.data-query-notif-header`),
|
||||||
|
content,
|
||||||
|
whisper,
|
||||||
|
style: CONST.CHAT_MESSAGE_STYLES.OOC,
|
||||||
|
});
|
||||||
|
|
||||||
|
respondedToQueries.delete(id);
|
||||||
|
};
|
||||||
54
module/sockets/query/prompt.mjs
Normal file
54
module/sockets/query/prompt.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { DialogManager } from "../../utils/DialogManager.mjs";
|
||||||
|
import { localizer } from "../../utils/localizer.mjs";
|
||||||
|
import { respondedToQueries } from "../../utils/QueryManager.mjs";
|
||||||
|
|
||||||
|
export async function queryPrompt(payload) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
users,
|
||||||
|
config,
|
||||||
|
request,
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
ui.notifications.error(localizer(
|
||||||
|
`taf.notifs.error.malformed-socket-payload`,
|
||||||
|
{
|
||||||
|
event: `query.cancel`,
|
||||||
|
details: `taf.notifs.error.missing-id`,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// null/undefined is a special case for "all users but me" by default
|
||||||
|
if (users != null && !Array.isArray(users)) {
|
||||||
|
ui.notifications.error(localizer(
|
||||||
|
`taf.notifs.error.malformed-socket-payload`,
|
||||||
|
{
|
||||||
|
event: `query.cancel`,
|
||||||
|
details: `taf.sockets.user-list-required`,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (users != null && !users.includes(game.user.id)) { return };
|
||||||
|
|
||||||
|
request.id = id;
|
||||||
|
const result = await DialogManager.ask(request, config);
|
||||||
|
if (result.state === `fronted`) {
|
||||||
|
return;
|
||||||
|
} else if (result.state === `errored`) {
|
||||||
|
ui.notifications.error(result.error);
|
||||||
|
} else if (result.state === `prompted`) {
|
||||||
|
respondedToQueries.add(request.id);
|
||||||
|
game.socket.emit(`system.taf`, {
|
||||||
|
event: `query.submit`,
|
||||||
|
payload: {
|
||||||
|
id: request.id,
|
||||||
|
answers: result.answers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
23
module/sockets/query/submit.mjs
Normal file
23
module/sockets/query/submit.mjs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { addResponse, has as hasQuery } from "../../utils/QueryManager.mjs";
|
||||||
|
import { localizer } from "../../utils/localizer.mjs";
|
||||||
|
|
||||||
|
export function querySubmit(payload, user) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
answers,
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
ui.notifications.error(localizer(
|
||||||
|
`taf.notifs.error.malformed-socket-payload`,
|
||||||
|
{
|
||||||
|
event: `query.cancel`,
|
||||||
|
details: `taf.notifs.error.missing-id`,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasQuery(id)) { return };
|
||||||
|
addResponse(id, user.id, answers);
|
||||||
|
};
|
||||||
|
|
@ -1,106 +1,114 @@
|
||||||
import { Ask } from "../apps/Ask.mjs";
|
import { Ask } from "../apps/Ask.mjs";
|
||||||
|
|
||||||
export class DialogManager {
|
/** @type {Map<string, Promise>} */
|
||||||
/** @type {Map<string, Promise>} */
|
const promises = new Map();
|
||||||
static #promises = new Map();
|
|
||||||
static #dialogs = new Map();
|
|
||||||
|
|
||||||
static async close(id) {
|
/** @type {Map<string, ApplicationV2>} */
|
||||||
this.#dialogs.get(id)?.close();
|
const dialogs = new Map();
|
||||||
this.#dialogs.delete(id);
|
|
||||||
this.#promises.delete(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
export function close(id) {
|
||||||
* Asks the user to provide a simple piece of information, this is primarily
|
dialogs.get(id)?.close();
|
||||||
* intended to be used within macros so that it can have better info gathering
|
dialogs.delete(id);
|
||||||
* as needed. This returns an object of input keys/labels to the value the user
|
promises.delete(id);
|
||||||
* input for that label, if there is only one input, this will return the value
|
};
|
||||||
* without an object wrapper, allowing for easier access.
|
|
||||||
*
|
/**
|
||||||
* @param {AskConfig} data
|
* Asks the user to provide a simple piece of information, this is primarily
|
||||||
* @param {AskOptions} opts
|
* intended to be used within macros so that it can have better info gathering
|
||||||
* @returns {AskResult}
|
* as needed. This returns an object of input keys/labels to the value the user
|
||||||
*/
|
* input for that label, if there is only one input, this will return the value
|
||||||
static async ask(
|
* without an object wrapper, allowing for easier access.
|
||||||
data,
|
*
|
||||||
{
|
* @param {AskConfig} data
|
||||||
onlyOneWaiting = true,
|
* @param {AskOptions} opts
|
||||||
alwaysUseAnswerObject = true,
|
* @returns {AskResult}
|
||||||
} = {},
|
*/
|
||||||
) {
|
export async function ask(
|
||||||
if (!data.id) {
|
data,
|
||||||
return {
|
{
|
||||||
state: `errored`,
|
onlyOneWaiting = true,
|
||||||
error: `An ID must be provided`,
|
alwaysUseAnswerObject = true,
|
||||||
};
|
} = {},
|
||||||
};
|
) {
|
||||||
if (!data.inputs.length) {
|
if (!data.id) {
|
||||||
return {
|
return {
|
||||||
state: `errored`,
|
state: `errored`,
|
||||||
error: `At least one input must be provided`,
|
error: `An ID must be provided`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const id = data.id;
|
if (!data.inputs.length) {
|
||||||
|
return {
|
||||||
// Don't do multi-thread waiting
|
state: `errored`,
|
||||||
if (this.#dialogs.has(id)) {
|
error: `At least one input must be provided`,
|
||||||
const app = this.#dialogs.get(id);
|
};
|
||||||
app.bringToFront();
|
};
|
||||||
if (onlyOneWaiting) {
|
const id = data.id;
|
||||||
return { state: `fronted` };
|
|
||||||
} else {
|
// Don't do multi-thread waiting
|
||||||
return this.#promises.get(id);
|
if (dialogs.has(id)) {
|
||||||
};
|
const app = dialogs.get(id);
|
||||||
};
|
app.bringToFront();
|
||||||
|
if (onlyOneWaiting) {
|
||||||
let autofocusClaimed = false;
|
return { state: `fronted` };
|
||||||
for (const i of data.inputs) {
|
} else {
|
||||||
i.id ??= foundry.utils.randomID(16);
|
return promises.get(id);
|
||||||
i.key ??= i.label;
|
};
|
||||||
|
};
|
||||||
switch (i.type) {
|
|
||||||
case `input`: {
|
let autofocusClaimed = false;
|
||||||
i.inputType ??= `text`;
|
for (const i of data.inputs) {
|
||||||
}
|
i.id ??= foundry.utils.randomID(16);
|
||||||
}
|
i.key ??= i.label;
|
||||||
|
|
||||||
// Only ever allow one input to claim autofocus
|
switch (i.type) {
|
||||||
i.autofocus &&= !autofocusClaimed;
|
case `input`: {
|
||||||
autofocusClaimed ||= i.autofocus;
|
i.inputType ??= `text`;
|
||||||
|
}
|
||||||
// Set the value's attribute name if it isn't specified explicitly
|
}
|
||||||
if (!i.valueAttribute) {
|
|
||||||
switch (i.inputType) {
|
// Only ever allow one input to claim autofocus
|
||||||
case `checkbox`:
|
i.autofocus &&= !autofocusClaimed;
|
||||||
i.valueAttribute = `checked`;
|
autofocusClaimed ||= i.autofocus;
|
||||||
break;
|
|
||||||
default:
|
// Set the value's attribute name if it isn't specified explicitly
|
||||||
i.valueAttribute = `value`;
|
if (!i.valueAttribute) {
|
||||||
};
|
switch (i.inputType) {
|
||||||
};
|
case `checkbox`:
|
||||||
};
|
i.type = `checkbox`;
|
||||||
|
delete i.valueAttribute;
|
||||||
const promise = new Promise((resolve) => {
|
delete i.inputType;
|
||||||
const app = new Ask({
|
break;
|
||||||
...data,
|
default:
|
||||||
alwaysUseAnswerObject,
|
i.valueAttribute = `value`;
|
||||||
onClose: () => {
|
};
|
||||||
this.#dialogs.delete(id);
|
};
|
||||||
this.#promises.delete(id);
|
};
|
||||||
resolve({ state: `prompted` });
|
|
||||||
},
|
const promise = new Promise((resolve) => {
|
||||||
onConfirm: (answers) => resolve({ state: `prompted`, answers }),
|
const app = new Ask({
|
||||||
});
|
...data,
|
||||||
app.render({ force: true });
|
alwaysUseAnswerObject,
|
||||||
this.#dialogs.set(id, app);
|
onClose: () => {
|
||||||
});
|
dialogs.delete(id);
|
||||||
|
promises.delete(id);
|
||||||
this.#promises.set(id, promise);
|
resolve({ state: `prompted` });
|
||||||
return promise;
|
},
|
||||||
};
|
onConfirm: (answers) => resolve({ state: `prompted`, answers }),
|
||||||
|
});
|
||||||
static get size() {
|
app.render({ force: true });
|
||||||
return this.#dialogs.size;
|
dialogs.set(id, app);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
promises.set(id, promise);
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function size() {
|
||||||
|
return dialogs.size;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DialogManager = {
|
||||||
|
close,
|
||||||
|
ask,
|
||||||
|
size,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
289
module/utils/QueryManager.mjs
Normal file
289
module/utils/QueryManager.mjs
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
import { filePath } from "../consts.mjs";
|
||||||
|
import { Logger } from "./Logger.mjs";
|
||||||
|
import { QueryStatus } from "../apps/QueryStatus.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing information about the current status for all
|
||||||
|
* users involved with the data request.
|
||||||
|
* @typedef {Record<
|
||||||
|
* string,
|
||||||
|
* "finished" | "waiting" | "disconnected" | "unprompted"
|
||||||
|
* >} UserStatus
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef QueryData
|
||||||
|
* @property {string[]} users
|
||||||
|
* @property {Function} resolve
|
||||||
|
* @property {Record<string, object>} responses
|
||||||
|
* @property {(() => Promise<void>)|null} onSubmit
|
||||||
|
* @property {QueryStatus|null} app
|
||||||
|
* @property {UserStatus} status
|
||||||
|
* @property {object} request The data used to form the initial request
|
||||||
|
* @property {object} config The data used to create the initial config
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This internal API is used in order to prevent the query.notify event
|
||||||
|
* from being fired off in situations where the user hasn't responded,
|
||||||
|
* wasn't part of the query, or has already been notified.
|
||||||
|
* @type {Set<string>}
|
||||||
|
*/
|
||||||
|
export const respondedToQueries = new Set();
|
||||||
|
|
||||||
|
/** @type {Map<string, QueryData>} */
|
||||||
|
const queries = new Map();
|
||||||
|
|
||||||
|
/** @type {Map<string, Promise>} */
|
||||||
|
const promises = new Map();
|
||||||
|
|
||||||
|
async function sendBasicNotification(requestID, userID, answers) {
|
||||||
|
const content = await foundry.applications.handlebars.renderTemplate(
|
||||||
|
filePath(`templates/query-response.hbs`),
|
||||||
|
{ answers },
|
||||||
|
);
|
||||||
|
|
||||||
|
await notify(requestID, userID, content, { includeGM: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
export function has(requestID) {
|
||||||
|
return queries.has(requestID);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @returns {Omit<QueryData, "resolve"|"onSubmit"|"app">} */
|
||||||
|
export function get(requestID) {
|
||||||
|
if (!queries.has(requestID)) { return null };
|
||||||
|
const query = queries.get(requestID);
|
||||||
|
const cloned = foundry.utils.deepClone(query);
|
||||||
|
|
||||||
|
delete cloned.onSubmit;
|
||||||
|
delete cloned.resolve;
|
||||||
|
delete cloned.app;
|
||||||
|
|
||||||
|
return foundry.utils.deepFreeze(cloned);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function query(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
onSubmit = sendBasicNotification,
|
||||||
|
users = null,
|
||||||
|
showStatusApp = true,
|
||||||
|
...config
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
if (!request.id) {
|
||||||
|
ui.notifications.error(game.i18n.localize(`taf.notifs.error.missing-id`));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
game.socket.emit(`system.taf`, {
|
||||||
|
event: `query.prompt`,
|
||||||
|
payload: {
|
||||||
|
id: request.id,
|
||||||
|
users,
|
||||||
|
request,
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (promises.has(request.id)) {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
users ??= game.users
|
||||||
|
.filter(u => u.id !== game.user.id)
|
||||||
|
.map(u => u.id);
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
|
||||||
|
/** @type {UserStatus} */
|
||||||
|
const status = {};
|
||||||
|
for (const user of users) {
|
||||||
|
status[user] = game.users.get(user).active ? `waiting` : `disconnected`;
|
||||||
|
};
|
||||||
|
|
||||||
|
queries.set(
|
||||||
|
request.id,
|
||||||
|
{
|
||||||
|
users,
|
||||||
|
request,
|
||||||
|
config,
|
||||||
|
responses: {},
|
||||||
|
resolve,
|
||||||
|
onSubmit,
|
||||||
|
app: null,
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (showStatusApp) {
|
||||||
|
const app = new QueryStatus({ requestID: request.id });
|
||||||
|
app.render({ force: true });
|
||||||
|
queries.get(request.id).app = app;
|
||||||
|
};
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function requery(requestID, users) {
|
||||||
|
const query = queries.get(requestID);
|
||||||
|
if (!query) { return };
|
||||||
|
|
||||||
|
game.socket.emit(`system.taf`, {
|
||||||
|
event: `query.prompt`,
|
||||||
|
payload: {
|
||||||
|
id: requestID,
|
||||||
|
users,
|
||||||
|
request: query.request,
|
||||||
|
config: query.config,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
query.status[user] = `waiting`;
|
||||||
|
};
|
||||||
|
query.app?.render({ parts: [ `users` ] });
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function addResponse(requestID, userID, answers) {
|
||||||
|
if (!queries.has(requestID)) { return };
|
||||||
|
const query = queries.get(requestID);
|
||||||
|
|
||||||
|
// User closed the popup manually
|
||||||
|
if (answers == null) {
|
||||||
|
query.status[userID] = `unprompted`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User submitted the answers as expected
|
||||||
|
else {
|
||||||
|
query.responses[userID] = answers;
|
||||||
|
query.status[userID] = `finished`;
|
||||||
|
await query.onSubmit?.(requestID, userID, answers);
|
||||||
|
};
|
||||||
|
|
||||||
|
await maybeResolve(requestID);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function maybeResolve(requestID) {
|
||||||
|
const query = queries.get(requestID);
|
||||||
|
|
||||||
|
// Determine how many users are considered "finished"
|
||||||
|
let finishedUserCount = 0;
|
||||||
|
for (const user of query.users) {
|
||||||
|
const hasApp = query.app != null;
|
||||||
|
|
||||||
|
switch (query.status[user]) {
|
||||||
|
case `finished`: {
|
||||||
|
finishedUserCount++;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
case `cancelled`:
|
||||||
|
case `disconnected`:
|
||||||
|
case `unprompted`: {
|
||||||
|
if (!hasApp) {
|
||||||
|
finishedUserCount++;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure that we have a finished response from everyone prompted
|
||||||
|
if (query.users.length === finishedUserCount) {
|
||||||
|
query.app?.close();
|
||||||
|
query.resolve(query.responses);
|
||||||
|
queries.delete(requestID);
|
||||||
|
promises.delete(requestID);
|
||||||
|
} else {
|
||||||
|
query.app?.render({ parts: [ `users` ] });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function notify(requestID, userID, content, { includeGM = false } = {}) {
|
||||||
|
// Prevent sending notifications for not-your queries
|
||||||
|
if (!queries.has(requestID)) { return };
|
||||||
|
|
||||||
|
game.socket.emit(`system.taf`, {
|
||||||
|
event: `query.notify`,
|
||||||
|
payload: {
|
||||||
|
id: requestID,
|
||||||
|
userID,
|
||||||
|
content,
|
||||||
|
includeGM,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function finish(requestID) {
|
||||||
|
// prevent finishing other people's queries
|
||||||
|
if (!queries.has(requestID)) { return };
|
||||||
|
|
||||||
|
const query = queries.get(requestID);
|
||||||
|
query.app?.close();
|
||||||
|
query.resolve(query.responses);
|
||||||
|
queries.delete(requestID);
|
||||||
|
promises.delete(requestID);
|
||||||
|
|
||||||
|
game.socket.emit(`system.taf`, {
|
||||||
|
event: `query.cancel`,
|
||||||
|
payload: { id: requestID },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function cancel(requestID) {
|
||||||
|
// prevent cancelling other people's queries
|
||||||
|
if (!queries.has(requestID)) { return };
|
||||||
|
|
||||||
|
const query = queries.get(requestID);
|
||||||
|
query.app?.close();
|
||||||
|
query.resolve(null);
|
||||||
|
queries.delete(requestID);
|
||||||
|
promises.delete(requestID);
|
||||||
|
|
||||||
|
game.socket.emit(`system.taf`, {
|
||||||
|
event: `query.cancel`,
|
||||||
|
payload: { id: requestID },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function setApplication(requestID, app) {
|
||||||
|
if (!queries.has(requestID)) { return };
|
||||||
|
if (!(app instanceof QueryStatus)) { return };
|
||||||
|
const query = queries.get(requestID);
|
||||||
|
if (query.app) {
|
||||||
|
Logger.error(`Cannot set an application for a query that has one already`);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
query.app = app;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function userActivity(userID, connected) {
|
||||||
|
for (const [id, query] of queries.entries()) {
|
||||||
|
if (query.users.includes(userID)) {
|
||||||
|
|
||||||
|
// Update the user's status to allow for the app to re-prompt them
|
||||||
|
if (query.status[userID] !== `finished`) {
|
||||||
|
if (connected) {
|
||||||
|
query.status[userID] = `unprompted`;
|
||||||
|
} else {
|
||||||
|
query.status[userID] = `disconnected`;
|
||||||
|
};
|
||||||
|
maybeResolve(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
query.app?.render({ parts: [ `users` ] });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QueryManager = {
|
||||||
|
has, get,
|
||||||
|
query, requery,
|
||||||
|
addResponse,
|
||||||
|
notify,
|
||||||
|
finish, cancel,
|
||||||
|
setApplication,
|
||||||
|
userActivity,
|
||||||
|
};
|
||||||
32
module/utils/getSizing.mjs
Normal file
32
module/utils/getSizing.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { PlayerSheet } from "../apps/PlayerSheet.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef SheetSizing
|
||||||
|
* @property {number} width The initial width of the application
|
||||||
|
* @property {number} height The initial height of the application
|
||||||
|
* @property {boolean} resizable Whether or not the application
|
||||||
|
* is able to be resized with a drag handle.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the computed default sizing data based on world settings
|
||||||
|
* and the sheet class' DEFAULT_OPTIONS
|
||||||
|
* @returns {SheetSizing}
|
||||||
|
*/
|
||||||
|
export function getDefaultSizing() {
|
||||||
|
/** @type {SheetSizing} */
|
||||||
|
const sizing = {
|
||||||
|
width: undefined,
|
||||||
|
height: undefined,
|
||||||
|
resizable: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: defaults from world settings
|
||||||
|
|
||||||
|
// Defaults from the sheet class itself
|
||||||
|
sizing.height ||= PlayerSheet.DEFAULT_OPTIONS.position.height;
|
||||||
|
sizing.width ||= PlayerSheet.DEFAULT_OPTIONS.position.width;
|
||||||
|
sizing.resizable ||= PlayerSheet.DEFAULT_OPTIONS.window.resizable;
|
||||||
|
|
||||||
|
return sizing;
|
||||||
|
};
|
||||||
32
module/utils/localizer.mjs
Normal file
32
module/utils/localizer.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
const config = Object.preventExtensions({
|
||||||
|
subKeyPattern: /@(?<key>[a-zA-Z.]+)/gm,
|
||||||
|
maxDepth: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function localizer(key, args = {}, depth = 0) {
|
||||||
|
/** @type {string} */
|
||||||
|
let localized = game.i18n.format(key, args);
|
||||||
|
const subkeys = localized.matchAll(config.subKeyPattern);
|
||||||
|
|
||||||
|
// Short-cut to help prevent infinite recursion
|
||||||
|
if (depth > config.maxDepth) {
|
||||||
|
return localized;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Helps prevent recursion on the same key so that we aren't doing excess work.
|
||||||
|
*/
|
||||||
|
const localizedSubkeys = new Map();
|
||||||
|
for (const match of subkeys) {
|
||||||
|
const subkey = match.groups.key;
|
||||||
|
if (localizedSubkeys.has(subkey)) { continue };
|
||||||
|
localizedSubkeys.set(subkey, localizer(subkey, args, depth + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
return localized.replace(
|
||||||
|
config.subKeyPattern,
|
||||||
|
(_fullMatch, subkey) => {
|
||||||
|
return localizedSubkeys.get(subkey);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
5988
package-lock.json
generated
5988
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
|
@ -1,16 +1,18 @@
|
||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.934.0",
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^9.8.0",
|
||||||
"@foundryvtt/foundryvtt-cli": "^1.0.3",
|
"@foundryvtt/foundryvtt-cli": "^1.0.3",
|
||||||
"@league-of-foundry-developers/foundry-vtt-types": "^9.280.0",
|
|
||||||
"@stylistic/eslint-plugin": "^2.6.1",
|
"@stylistic/eslint-plugin": "^2.6.1",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"dotenv": "^17.2.2",
|
||||||
"eslint": "^9.8.0",
|
"eslint": "^9.8.0",
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0"
|
||||||
"sass": "^1.77.8"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"css": "sass --watch --embed-source-map --no-error-css styles/:.styles/",
|
"data:build": "node scripts/buildCompendia.mjs",
|
||||||
"build": "sass --embed-source-map --no-error-css styles/:.styles/",
|
"data:extract": "node scripts/extractCompendia.mjs",
|
||||||
|
"link": "node scripts/linkFoundry.mjs",
|
||||||
"lint": "eslint --fix",
|
"lint": "eslint --fix",
|
||||||
"lint:nofix": "eslint"
|
"lint:nofix": "eslint"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
52
scripts/createForgejoRelease.mjs
Normal file
52
scripts/createForgejoRelease.mjs
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const {
|
||||||
|
TAG,
|
||||||
|
FORGEJO_API_URL: API,
|
||||||
|
FORGEJO_REPOSITORY: REPO,
|
||||||
|
FORGEJO_TOKEN: TOKEN,
|
||||||
|
CDN_URL,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
async function addReleaseAsset(releaseID, name) {
|
||||||
|
return axios.post(
|
||||||
|
`${API}/repos/${REPO}/releases/${releaseID}/assets`,
|
||||||
|
{ external_url: `${CDN_URL}/${REPO}/${TAG}/${name}`, },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${TOKEN}`,
|
||||||
|
"Content-Type": `multipart/form-data`,
|
||||||
|
},
|
||||||
|
params: { name },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
|
||||||
|
// Initial Release Data
|
||||||
|
const release = await axios.post(
|
||||||
|
`${API}/repos/${REPO}/releases`,
|
||||||
|
{
|
||||||
|
name: TAG,
|
||||||
|
tag_name: TAG,
|
||||||
|
draft: true,
|
||||||
|
hide_archive_links: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: { Authorization: `token ${TOKEN}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addReleaseAsset(release.data.id, `release.zip`);
|
||||||
|
await addReleaseAsset(release.data.id, `system.json`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to add assets to the release`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Release created`);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
47
scripts/linkFoundry.mjs
Normal file
47
scripts/linkFoundry.mjs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { symlink, unlink } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
|
||||||
|
config({ quiet: true });
|
||||||
|
|
||||||
|
const root = process.env.FOUNDRY_ROOT;
|
||||||
|
|
||||||
|
// Early exit
|
||||||
|
if (!root) {
|
||||||
|
console.error(`Must provide a FOUNDRY_ROOT environment variable`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert Foundry exists
|
||||||
|
if (!existsSync(root)) {
|
||||||
|
console.error(`Foundry root not found.`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Removing existing symlink
|
||||||
|
if (existsSync(`foundry`)) {
|
||||||
|
console.log(`Attempting to unlink foundry instance`);
|
||||||
|
try {
|
||||||
|
await unlink(`foundry`);
|
||||||
|
} catch {
|
||||||
|
console.error(`Failed to unlink foundry folder.`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Account for if the root is pointing at an Electron install
|
||||||
|
let targetRoot = root;
|
||||||
|
if (existsSync(join(root, `resources`, `app`))) {
|
||||||
|
console.log(`Switching to use the "${root}/resources/app" directory`);
|
||||||
|
targetRoot = join(root, `resources`, `app`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create symlink
|
||||||
|
console.log(`Linking foundry source into folder`)
|
||||||
|
try {
|
||||||
|
await symlink(targetRoot, `foundry`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
45
scripts/prepareManifest.mjs
Normal file
45
scripts/prepareManifest.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
The intent of this script is to do all of the modifications of the
|
||||||
|
manifest file that we need to do in order to release the system.
|
||||||
|
This can include removing dev-only fields/attributes that end
|
||||||
|
users will never, and should never, care about nor need.
|
||||||
|
*/
|
||||||
|
import { readFile, writeFile } from "fs/promises";
|
||||||
|
|
||||||
|
const MANIFEST_PATH = `system.json`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
DOWNLOAD_URL,
|
||||||
|
LATEST_URL,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
let manifest;
|
||||||
|
try {
|
||||||
|
manifest = JSON.parse(await readFile(MANIFEST_PATH, `utf-8`));
|
||||||
|
console.log(`Manifest loaded from disk`);
|
||||||
|
} catch {
|
||||||
|
console.error(`Failed to parse manifest file.`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Updating download/manifest URLs`)
|
||||||
|
manifest.download = DOWNLOAD_URL;
|
||||||
|
manifest.manifest = LATEST_URL;
|
||||||
|
|
||||||
|
// Filter out dev-only resources
|
||||||
|
if (manifest.esmodules) {
|
||||||
|
console.log(`Removing dev-only esmodules`);
|
||||||
|
manifest.esmodules = manifest.esmodules.filter(
|
||||||
|
filepath => !filepath.startsWith(`dev/`)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove dev flags
|
||||||
|
console.log(`Cleaning up flags`);
|
||||||
|
delete manifest.flags?.hotReload;
|
||||||
|
if (Object.keys(manifest.flags).length === 0) {
|
||||||
|
delete manifest.flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
await writeFile(MANIFEST_PATH, JSON.stringify(manifest, undefined, `\t`));
|
||||||
|
console.log(`Manifest written back to disk`);
|
||||||
38
scripts/tagExists.mjs
Normal file
38
scripts/tagExists.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const {
|
||||||
|
TAG_NAME,
|
||||||
|
FORGEJO_API_URL: API_URL,
|
||||||
|
FORGEJO_REPOSITORY: REPO,
|
||||||
|
FORGEJO_TOKEN: TOKEN,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
|
||||||
|
if (!TAG_NAME) {
|
||||||
|
console.log(`Tag name must not be blank`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestURL = `${API_URL}/repos/${REPO}/tags/${TAG_NAME}`;
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
requestURL,
|
||||||
|
{
|
||||||
|
headers: { Authorization: `token ${TOKEN}` },
|
||||||
|
validateStatus: () => true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// We actually *want* an error when the tag exists, instead of when
|
||||||
|
// it doesn't
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log(`Tag with name "${TAG_NAME}" already exists`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Tag with name "${TAG_NAME}" not found, proceeding`);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
65
scripts/uploadToS3.mjs
Normal file
65
scripts/uploadToS3.mjs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||||
|
import { createReadStream } from "fs";
|
||||||
|
|
||||||
|
const requiredEnvVariables = [
|
||||||
|
`TAG`, `FILE`,
|
||||||
|
`FORGEJO_REPOSITORY`,
|
||||||
|
`S3_BUCKET`, `S3_REGION`, `S3_KEY`, `S3_SECRET`, `S3_ENDPOINT`,
|
||||||
|
];
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
|
||||||
|
// Assert all of the required env variables are present
|
||||||
|
const missing = [];
|
||||||
|
for (const envVar of requiredEnvVariables) {
|
||||||
|
if (!(envVar in process.env)) {
|
||||||
|
missing.push(envVar);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.error(`Missing the following required environment variables: ${missing.join(`, `)}`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
TAG,
|
||||||
|
S3_ENDPOINT,
|
||||||
|
S3_REGION,
|
||||||
|
S3_KEY,
|
||||||
|
S3_SECRET,
|
||||||
|
S3_BUCKET,
|
||||||
|
FILE,
|
||||||
|
FORGEJO_REPOSITORY: REPO,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
const s3Client = new S3Client({
|
||||||
|
endpoint: S3_ENDPOINT,
|
||||||
|
forcePathStyle: false,
|
||||||
|
region: S3_REGION,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: S3_KEY,
|
||||||
|
secretAccessKey: S3_SECRET
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = FILE.split(`/`).at(-1);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
Bucket: S3_BUCKET,
|
||||||
|
Key: `${REPO}/${TAG}/${name}`,
|
||||||
|
Body: createReadStream(FILE),
|
||||||
|
ACL: "public-read",
|
||||||
|
METADATA: {
|
||||||
|
"x-repo-version": TAG,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await s3Client.send(new PutObjectCommand(params));
|
||||||
|
console.log("Upload successful");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Upload to s3 failed");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
@ -23,10 +23,6 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
button {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
|
|
||||||
33
styles/Apps/QueryStatus.css
Normal file
33
styles/Apps/QueryStatus.css
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
.taf.QueryStatus {
|
||||||
|
.user-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid rebeccapurple;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
|
||||||
|
> .user-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
/* Same height as the icons used for loading/disconnected */
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
styles/Apps/TAFDocumentSheetConfig.css
Normal file
15
styles/Apps/TAFDocumentSheetConfig.css
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
.taf.sheet-config {
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.tab.active {
|
||||||
|
display: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
styles/elements/div.css
Normal file
20
styles/elements/div.css
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
.taf > .window-content div {
|
||||||
|
&.chip {
|
||||||
|
display: inline flex;
|
||||||
|
color: var(--chip-color);
|
||||||
|
background: var(--chip-background);
|
||||||
|
border: 1px solid var(--chip-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.key {
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
color: var(--chip-value-color);
|
||||||
|
background: var(--chip-value-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
.taf > .window-content prose-mirror {
|
.taf > .window-content prose-mirror {
|
||||||
background: var(--prosemirror-background);
|
background: var(--prosemirror-background);
|
||||||
|
gap: 0;
|
||||||
|
|
||||||
.editor-content {
|
.editor-content {
|
||||||
padding: 0 8px 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableWrapper th,
|
.tableWrapper th,
|
||||||
|
|
|
||||||
45
styles/elements/span.css
Normal file
45
styles/elements/span.css
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
@keyframes rotate {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
50% { transform: rotate(360deg); }
|
||||||
|
100% { transform: rotate(720deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes prixClipFix {
|
||||||
|
0%, 100% {
|
||||||
|
clip-path: polygon(50% 50%,0 0,0 0,0 0,0 0,0 0);
|
||||||
|
}
|
||||||
|
25%, 63% {
|
||||||
|
clip-path: polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0);
|
||||||
|
}
|
||||||
|
37%, 50% {
|
||||||
|
clip-path: polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.taf > .window-content span {
|
||||||
|
&.loader {
|
||||||
|
--size: 35px;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
animation: rotate 2s linear infinite;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
content: "";
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 5px solid var(--spinner-outer-colour, #fff);
|
||||||
|
animation: prixClipFix 4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after{
|
||||||
|
inset: 8px;
|
||||||
|
transform: rotate3d(90, 90, 0, 180deg );
|
||||||
|
border-color: var(--spinner-inner-colour, #ff3d00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
styles/elements/table.css
Normal file
21
styles/elements/table.css
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
This styling is unscoped in order to make it so that it still applies
|
||||||
|
to the chat messages which are not within a scope I control.
|
||||||
|
*/
|
||||||
|
table.taf-query-summary {
|
||||||
|
margin: 0px;
|
||||||
|
|
||||||
|
tr:hover > td {
|
||||||
|
background-color: var(--table-header-border-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--table-header-border-color);
|
||||||
|
width: 40%;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
styles/elements/utils.css
Normal file
3
styles/elements/utils.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.taf > .window-content {
|
||||||
|
.grow { flex-grow: 1; }
|
||||||
|
}
|
||||||
|
|
@ -3,20 +3,27 @@
|
||||||
/* Resets */
|
/* Resets */
|
||||||
@import url("./resets/hr.css") layer(resets);
|
@import url("./resets/hr.css") layer(resets);
|
||||||
@import url("./resets/inputs.css") layer(resets);
|
@import url("./resets/inputs.css") layer(resets);
|
||||||
|
@import url("./resets/button.css") layer(resets);
|
||||||
|
|
||||||
/* Themes */
|
/* Themes */
|
||||||
@import url("./themes/dark.css") layer(themes);
|
@import url("./themes/dark.css") layer(themes);
|
||||||
@import url("./themes/light.css") layer(themes);
|
@import url("./themes/light.css") layer(themes);
|
||||||
|
|
||||||
/* Elements */
|
/* Elements */
|
||||||
|
@import url("./elements/utils.css") layer(elements);
|
||||||
|
@import url("./elements/div.css") layer(elements);
|
||||||
@import url("./elements/headers.css") layer(elements);
|
@import url("./elements/headers.css") layer(elements);
|
||||||
@import url("./elements/hr.css") layer(elements);
|
@import url("./elements/hr.css") layer(elements);
|
||||||
@import url("./elements/input.css") layer(elements);
|
@import url("./elements/input.css") layer(elements);
|
||||||
@import url("./elements/p.css") layer(elements);
|
@import url("./elements/p.css") layer(elements);
|
||||||
@import url("./elements/prose-mirror.css") layer(elements);
|
@import url("./elements/prose-mirror.css") layer(elements);
|
||||||
|
@import url("./elements/span.css") layer(elements);
|
||||||
|
@import url("./elements/table.css") layer(elements);
|
||||||
|
|
||||||
/* Apps */
|
/* Apps */
|
||||||
@import url("./Apps/common.css") layer(apps);
|
@import url("./Apps/common.css") layer(apps);
|
||||||
@import url("./Apps/Ask.css") layer(apps);
|
@import url("./Apps/Ask.css") layer(apps);
|
||||||
@import url("./Apps/AttributeManager.css") layer(apps);
|
@import url("./Apps/AttributeManager.css") layer(apps);
|
||||||
@import url("./Apps/PlayerSheet.css") layer(apps);
|
@import url("./Apps/PlayerSheet.css") layer(apps);
|
||||||
|
@import url("./Apps/QueryStatus.css") layer(apps);
|
||||||
|
@import url("./Apps/TAFDocumentSheetConfig.css") layer(apps);
|
||||||
|
|
|
||||||
3
styles/resets/button.css
Normal file
3
styles/resets/button.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.taf > .window-content button {
|
||||||
|
height: initial;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
.theme-dark {
|
.theme-dark {
|
||||||
--prosemirror-background: var(--color-cool-5);
|
--prosemirror-background: var(--color-cool-5);
|
||||||
|
|
||||||
|
--spinner-outer-colour: white;
|
||||||
|
--spinner-inner-colour: #FF3D00;
|
||||||
|
|
||||||
|
/* Chip Variables */
|
||||||
|
--chip-color: #fff7ed;
|
||||||
|
--chip-background: #2b3642;
|
||||||
|
--chip-value-color: #fff7ed;
|
||||||
|
--chip-value-background: #10161d;
|
||||||
|
--chip-border-color: var(--chip-value-background);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
.theme-light {
|
.theme-light {
|
||||||
--prosemirror-background: white;
|
--prosemirror-background: white;
|
||||||
|
|
||||||
|
--spinner-outer-colour: black;
|
||||||
|
--spinner-inner-colour: #FF3D00;
|
||||||
|
|
||||||
|
/* Chip Variables */
|
||||||
|
--chip-color: #18181b;
|
||||||
|
--chip-background: #fafafa;
|
||||||
|
--chip-value-color: #18181b;
|
||||||
|
--chip-value-background: #d4d4d8aa;
|
||||||
|
--chip-border-color: var(--chip-value-background);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
system.json
16
system.json
|
|
@ -2,20 +2,17 @@
|
||||||
"id": "taf",
|
"id": "taf",
|
||||||
"title": "Text-Based Actors",
|
"title": "Text-Based Actors",
|
||||||
"description": "An intentionally minimalist system that enables you to play rules-light games without getting in your way!",
|
"description": "An intentionally minimalist system that enables you to play rules-light games without getting in your way!",
|
||||||
"version": "2.2.1",
|
"version": "2.4.0",
|
||||||
"download": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/release.zip",
|
"download": "",
|
||||||
"manifest": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/system.json",
|
"manifest": "",
|
||||||
"url": "https://github.com/Oliver-Akins/Text-Actors-Foundry",
|
"url": "https://git.varify.ca/Foundry/taf",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": 13,
|
"minimum": 13,
|
||||||
"verified": 13,
|
"verified": 13,
|
||||||
"maximum": 13
|
"maximum": 13
|
||||||
},
|
},
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{ "name": "Oliver" }
|
||||||
"name": "Oliver Akins",
|
|
||||||
"url": "https://oliver.akins.me"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"esmodules": [
|
"esmodules": [
|
||||||
"./module/main.mjs"
|
"./module/main.mjs"
|
||||||
|
|
@ -44,10 +41,11 @@
|
||||||
},
|
},
|
||||||
"Item": {}
|
"Item": {}
|
||||||
},
|
},
|
||||||
|
"socket": true,
|
||||||
"flags": {
|
"flags": {
|
||||||
"hotReload": {
|
"hotReload": {
|
||||||
"extensions": ["css", "hbs", "json", "js", "mjs", "svg"],
|
"extensions": ["css", "hbs", "json", "js", "mjs", "svg"],
|
||||||
"paths": ["templates", "langs", ".styles", "module", "assets"]
|
"paths": ["templates", "langs", "styles", "module", "assets"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
name="{{attr.path}}.value"
|
name="{{attr.path}}.value"
|
||||||
value="{{attr.value}}"
|
value="{{attr.value}}"
|
||||||
aria-label="Current value"
|
aria-label="Current value"
|
||||||
|
data-tooltip="@{{ attr.id }}{{#if attr.isRange}}.value{{/if}}"
|
||||||
>
|
>
|
||||||
{{#if attr.isRange}}
|
{{#if attr.isRange}}
|
||||||
<span aria-hidden="true">/</span>
|
<span aria-hidden="true">/</span>
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
name="{{attr.path}}.max"
|
name="{{attr.path}}.max"
|
||||||
value="{{attr.max}}"
|
value="{{attr.max}}"
|
||||||
aria-label="Maximum value"
|
aria-label="Maximum value"
|
||||||
|
data-tooltip="@{{ attr.id }}.max"
|
||||||
>
|
>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
value="{{system.content}}"
|
value="{{system.content}}"
|
||||||
collaborate="true"
|
collaborate="true"
|
||||||
data-document-uuid="{{actor.uuid}}"
|
data-document-uuid="{{actor.uuid}}"
|
||||||
|
toggled="true"
|
||||||
>
|
>
|
||||||
{{{ enriched.system.content }}}
|
{{{ enriched.system.content }}}
|
||||||
</prose-mirror>
|
</prose-mirror>
|
||||||
|
|
|
||||||
8
templates/QueryStatus/controls.hbs
Normal file
8
templates/QueryStatus/controls.hbs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="control-row">
|
||||||
|
<button data-action="cancelRequest">
|
||||||
|
{{ localize "taf.Apps.QueryStatus.cancel-request" }}
|
||||||
|
</button>
|
||||||
|
<button data-action="finishEarly">
|
||||||
|
{{ localize "taf.Apps.QueryStatus.finish-early" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
46
templates/QueryStatus/users.hbs
Normal file
46
templates/QueryStatus/users.hbs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<ul class="user-list">
|
||||||
|
{{#each users as | user |}}
|
||||||
|
<li
|
||||||
|
style="--spinner-inner-colour: var(--user-color-{{user.id}})"
|
||||||
|
data-user-id="{{ user.id }}"
|
||||||
|
>
|
||||||
|
<div class="user-summary">
|
||||||
|
<div class="grow">
|
||||||
|
{{ user.name }}
|
||||||
|
</div>
|
||||||
|
{{#if (eq user.status "waiting")}}
|
||||||
|
<span class="loader"></span>
|
||||||
|
{{else if (eq user.status "disconnected")}}
|
||||||
|
<taf-icon
|
||||||
|
data-tooltip="taf.Apps.QueryStatus.user-disconnected-tooltip"
|
||||||
|
name="icons/disconnected"
|
||||||
|
var:size="35px"
|
||||||
|
var:stroke="currentColor"
|
||||||
|
var:fill="currentColor"
|
||||||
|
></taf-icon>
|
||||||
|
{{else if (eq user.status "unprompted")}}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-action="promptUser"
|
||||||
|
>
|
||||||
|
{{ localize "taf.Apps.QueryStatus.send-request" }}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#if (eq user.status "finished")}}
|
||||||
|
<div class="chip-list">
|
||||||
|
{{#each user.answers as | answer |}}
|
||||||
|
<div class="chip">
|
||||||
|
<span class="key">
|
||||||
|
{{ @key }}
|
||||||
|
</span>
|
||||||
|
<span class="value">
|
||||||
|
{{ answer }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
3
templates/TAFDocumentSheetConfig/foundry.hbs
Normal file
3
templates/TAFDocumentSheetConfig/foundry.hbs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div class="foundry tab {{tabs.foundry.cssClass}}" data-group="main" data-tab="foundry">
|
||||||
|
{{> "templates/sheets/document-sheet-config.hbs" }}
|
||||||
|
</div>
|
||||||
48
templates/TAFDocumentSheetConfig/system.hbs
Normal file
48
templates/TAFDocumentSheetConfig/system.hbs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<div class="system tab {{tabs.system.cssClass}}" data-group="main" data-tab="system">
|
||||||
|
<fieldset>
|
||||||
|
<legend>
|
||||||
|
{{ localize "taf.Apps.TAFDocumentSheetConfig.Sizing" }}
|
||||||
|
</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{meta.idp}}-width">
|
||||||
|
{{ localize "taf.Apps.TAFDocumentSheetConfig.Width.label" }}
|
||||||
|
</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="FLAGS.taf.PlayerSheet.size.width"
|
||||||
|
id="{{meta.idp}}-width"
|
||||||
|
value="{{values.width}}"
|
||||||
|
placeholder="{{placeholders.width}}"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{meta.idp}}-height">
|
||||||
|
{{ localize "taf.Apps.TAFDocumentSheetConfig.Height.label" }}
|
||||||
|
</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="FLAGS.taf.PlayerSheet.size.height"
|
||||||
|
id="{{meta.idp}}-height"
|
||||||
|
value="{{values.height}}"
|
||||||
|
placeholder="{{placeholders.height}}"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{meta.idp}}-resize">
|
||||||
|
{{ localize "taf.Apps.TAFDocumentSheetConfig.Resizable.label" }}
|
||||||
|
</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<select
|
||||||
|
name="FLAGS.taf.PlayerSheet.size.resizable"
|
||||||
|
id="{{meta.idp}}-resize"
|
||||||
|
>
|
||||||
|
{{ taf-options values.resizable resizeOptions }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
16
templates/query-response.hbs
Normal file
16
templates/query-response.hbs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{{#if answers}}
|
||||||
|
<table class="taf-query-summary">
|
||||||
|
<tr>
|
||||||
|
<td>{{ localize "taf.misc.Key" }}</td>
|
||||||
|
<td>{{ localize "taf.misc.Value" }}</td>
|
||||||
|
</tr>
|
||||||
|
{{#each answers as | answer |}}
|
||||||
|
<tr>
|
||||||
|
<td>{{ @key }}</td>
|
||||||
|
<td>{{ answer }}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
{{ localize "taf.misc.no-data-submitted" }}
|
||||||
|
{{/if}}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue