Compare commits
No commits in common. "main" and "v1.1.0" have entirely different histories.
132 changed files with 4580 additions and 5807 deletions
|
|
@ -1,2 +0,0 @@
|
||||||
# The absolute path to the Foundry installation to create symlinks to
|
|
||||||
FOUNDRY_ROOT=""
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
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}}"
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
jobs:
|
|
||||||
release-to-foundry:
|
|
||||||
runs-on: docker
|
|
||||||
steps:
|
|
||||||
- name: retrieve release URLS
|
|
||||||
- name: publish to Foundry
|
|
||||||
58
.github/workflows/draft-release.yaml
vendored
Normal file
58
.github/workflows/draft-release.yaml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Ensure there are specific files to release
|
||||||
|
if: ${{ vars.files_to_release == '' }}
|
||||||
|
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 }}/${{ vars.zip_name }}.zip"' > system.json
|
||||||
|
|
||||||
|
- name: Create the zip
|
||||||
|
run: zip -r ${{ vars.zip_name || 'release' }}.zip ${{ vars.files_to_release }}
|
||||||
|
|
||||||
|
- name: Create the draft release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
tag: "v${{ steps.version.outputs.version }}"
|
||||||
|
commit: ${{ github.ref }}
|
||||||
|
draft: true
|
||||||
|
generateReleaseNotes: true
|
||||||
|
artifacts: "${{vars.zip_name || 'release'}}.zip,system.json"
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,4 +1,2 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
deprecated
|
.styles
|
||||||
.env
|
|
||||||
/foundry
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 705 KiB |
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,17 +2,3 @@
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 900 B |
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="100pt" height="100pt" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="m45.832 75c0 4.582-3.75 8.332-8.332 8.332s-8.332-3.75-8.332-8.332 3.75-8.332 8.332-8.332 8.332 3.75 8.332 8.332zm-8.332-33.332c-4.582 0-8.332 3.75-8.332 8.332s3.75 8.332 8.332 8.332 8.332-3.75 8.332-8.332-3.75-8.332-8.332-8.332zm0-25c-4.582 0-8.332 3.75-8.332 8.332s3.75 8.332 8.332 8.332 8.332-3.75 8.332-8.332-3.75-8.332-8.332-8.332zm25 16.664c4.582 0 8.332-3.75 8.332-8.332s-3.75-8.332-8.332-8.332-8.332 3.75-8.332 8.332 3.75 8.332 8.332 8.332zm0 8.3359c-4.582 0-8.332 3.75-8.332 8.332s3.75 8.332 8.332 8.332 8.332-3.75 8.332-8.332-3.75-8.332-8.332-8.332zm0 25c-4.582 0-8.332 3.75-8.332 8.332s3.75 8.332 8.332 8.332 8.332-3.75 8.332-8.332-3.75-8.332-8.332-8.332z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 831 B |
5
augments.d.ts
vendored
5
augments.d.ts
vendored
|
|
@ -1,8 +1,3 @@
|
||||||
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/`, `foundry/*` ] },
|
{ ignores: [ `scripts/` ] },
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
|
|
@ -16,11 +16,14 @@ export default [
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
CONFIG: `writable`,
|
CONFIG: `writable`,
|
||||||
CONST: `readonly`,
|
|
||||||
game: `readonly`,
|
game: `readonly`,
|
||||||
Handlebars: `readonly`,
|
Handlebars: `readonly`,
|
||||||
Hooks: `readonly`,
|
Hooks: `readonly`,
|
||||||
ui: `readonly`,
|
ui: `readonly`,
|
||||||
|
Actor: `readonly`,
|
||||||
|
Actors: `readonly`,
|
||||||
|
Item: `readonly`,
|
||||||
|
Items: `readonly`,
|
||||||
ActorSheet: `readonly`,
|
ActorSheet: `readonly`,
|
||||||
ItemSheet: `readonly`,
|
ItemSheet: `readonly`,
|
||||||
foundry: `readonly`,
|
foundry: `readonly`,
|
||||||
|
|
@ -28,8 +31,6 @@ export default [
|
||||||
ActiveEffect: `readonly`,
|
ActiveEffect: `readonly`,
|
||||||
Dialog: `readonly`,
|
Dialog: `readonly`,
|
||||||
renderTemplate: `readonly`,
|
renderTemplate: `readonly`,
|
||||||
fromUuid: `readonly`,
|
|
||||||
fromUuidSync: `readonly`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -41,7 +42,6 @@ export default [
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
Logger: `readonly`,
|
Logger: `readonly`,
|
||||||
taf: `readonly`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
|
@ -49,7 +49,6 @@ export default [
|
||||||
"func-names": [`warn`, `as-needed`],
|
"func-names": [`warn`, `as-needed`],
|
||||||
"grouped-accessor-pairs": `error`,
|
"grouped-accessor-pairs": `error`,
|
||||||
"no-alert": `error`,
|
"no-alert": `error`,
|
||||||
"no-empty": [`error`, { allowEmptyCatch: true }],
|
|
||||||
"no-implied-eval": `error`,
|
"no-implied-eval": `error`,
|
||||||
"no-invalid-this": `error`,
|
"no-invalid-this": `error`,
|
||||||
"no-lonely-if": `error`,
|
"no-lonely-if": `error`,
|
||||||
|
|
@ -73,7 +72,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": [`off`],
|
"@stylistic/brace-style": [`warn`, `1tbs`, { "allowSingleLine": true }],
|
||||||
"@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,19 +1,7 @@
|
||||||
{
|
{
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
{
|
|
||||||
"TYPES": {
|
|
||||||
"Actor": {
|
|
||||||
"player": "Player"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"taf": {
|
|
||||||
"settings": {
|
|
||||||
"canPlayersManageAttributes": {
|
|
||||||
"name": "Players Can Manage Attributes",
|
|
||||||
"hint": "This allows players who have edit access to a document to be able to edit what attributes those characters have via the attribute editor"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sheet-names": {
|
|
||||||
"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}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
// Apps
|
|
||||||
import { Ask } from "./apps/Ask.mjs";
|
|
||||||
import { AttributeManager } from "./apps/AttributeManager.mjs";
|
|
||||||
import { PlayerSheet } from "./apps/PlayerSheet.mjs";
|
|
||||||
import { QueryStatus } from "./apps/QueryStatus.mjs";
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { attributeSorter } from "./utils/attributeSort.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";
|
|
||||||
|
|
||||||
const { deepFreeze } = foundry.utils;
|
|
||||||
|
|
||||||
Object.defineProperty(
|
|
||||||
globalThis,
|
|
||||||
`taf`,
|
|
||||||
{
|
|
||||||
value: deepFreeze({
|
|
||||||
DialogManager,
|
|
||||||
QueryManager,
|
|
||||||
Apps: {
|
|
||||||
Ask,
|
|
||||||
AttributeManager,
|
|
||||||
PlayerSheet,
|
|
||||||
QueryStatus,
|
|
||||||
},
|
|
||||||
utils: {
|
|
||||||
attributeSorter,
|
|
||||||
localizer,
|
|
||||||
toID,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
import { __ID__, filePath } from "../consts.mjs";
|
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
|
||||||
|
|
||||||
const validInputTypes = [
|
|
||||||
`checkbox`,
|
|
||||||
`details`,
|
|
||||||
`divider`,
|
|
||||||
`error`,
|
|
||||||
`input`,
|
|
||||||
`select`,
|
|
||||||
];
|
|
||||||
|
|
||||||
export class Ask extends HandlebarsApplicationMixin(ApplicationV2) {
|
|
||||||
// #region Options
|
|
||||||
static DEFAULT_OPTIONS = {
|
|
||||||
tag: `dialog`,
|
|
||||||
classes: [
|
|
||||||
__ID__,
|
|
||||||
`dialog`, // accesses some Foundry-provided styling
|
|
||||||
`Ask`,
|
|
||||||
],
|
|
||||||
position: {
|
|
||||||
width: 330,
|
|
||||||
},
|
|
||||||
window: {
|
|
||||||
title: `Questions`,
|
|
||||||
resizable: true,
|
|
||||||
minimizable: true,
|
|
||||||
contentTag: `form`,
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
closeOnSubmit: true,
|
|
||||||
submitOnChange: false,
|
|
||||||
handler: this.#submit,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
cancel: this.#cancel,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
static PARTS = {
|
|
||||||
inputs: {
|
|
||||||
template: filePath(`templates/Ask/inputs.hbs`),
|
|
||||||
templates: validInputTypes.map(type => filePath(`templates/Ask/inputs/${type}.hbs`)),
|
|
||||||
},
|
|
||||||
controls: {
|
|
||||||
template: filePath(`templates/Ask/controls.hbs`),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// #endregion Options
|
|
||||||
|
|
||||||
// #region Instance
|
|
||||||
_inputs = [];
|
|
||||||
alwaysUseAnswerObject = false;
|
|
||||||
|
|
||||||
/** @type {string | undefined} */
|
|
||||||
_description = undefined;
|
|
||||||
|
|
||||||
/** @type {Function | undefined} */
|
|
||||||
_userOnConfirm;
|
|
||||||
|
|
||||||
/** @type {Function | undefined} */
|
|
||||||
_userOnCancel;
|
|
||||||
|
|
||||||
/** @type {Function | undefined} */
|
|
||||||
_userOnClose;
|
|
||||||
|
|
||||||
constructor({
|
|
||||||
inputs = [],
|
|
||||||
description = undefined,
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
onClose,
|
|
||||||
alwaysUseAnswerObject,
|
|
||||||
...options
|
|
||||||
} = {}) {
|
|
||||||
super(options);
|
|
||||||
this.alwaysUseAnswerObject = alwaysUseAnswerObject;
|
|
||||||
|
|
||||||
for (const input of inputs) {
|
|
||||||
if (!validInputTypes.includes(input.type)) {
|
|
||||||
input.details = `Invalid input type provided: ${input.type}`;
|
|
||||||
input.type = `error`;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
this._inputs = inputs;
|
|
||||||
this._description = description;
|
|
||||||
this._userOnCancel = onCancel;
|
|
||||||
this._userOnConfirm = onConfirm;
|
|
||||||
this._userOnClose = onClose;
|
|
||||||
};
|
|
||||||
// #endregion Instance
|
|
||||||
|
|
||||||
// #region Lifecycle
|
|
||||||
async _onFirstRender() {
|
|
||||||
super._onFirstRender();
|
|
||||||
this.element.show();
|
|
||||||
};
|
|
||||||
|
|
||||||
async _prepareContext() {
|
|
||||||
return {
|
|
||||||
inputs: this._inputs,
|
|
||||||
description: this._description,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
async _onClose() {
|
|
||||||
super._onClose();
|
|
||||||
this._userOnClose?.();
|
|
||||||
};
|
|
||||||
// #endregion Lifecycle
|
|
||||||
|
|
||||||
// #region Actions
|
|
||||||
/** @this {AskDialog} */
|
|
||||||
static async #submit(_event, _element, formData) {
|
|
||||||
const answers = formData.object;
|
|
||||||
const keys = Object.keys(answers);
|
|
||||||
if (keys.length === 1 && !this.alwaysUseAnswerObject) {
|
|
||||||
this._userOnConfirm?.(answers[keys[0]]);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
this._userOnConfirm?.(answers);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @this {AskDialog} */
|
|
||||||
static async #cancel() {
|
|
||||||
this._userOnCancel?.();
|
|
||||||
this.close();
|
|
||||||
};
|
|
||||||
// #endregion Actions
|
|
||||||
};
|
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
import { __ID__, filePath } from "../consts.mjs";
|
|
||||||
import { attributeSorter } from "../utils/attributeSort.mjs";
|
|
||||||
import { toID } from "../utils/toID.mjs";
|
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
|
||||||
const { deepClone, diffObject, mergeObject, performIntegerSort, randomID, setProperty } = foundry.utils;
|
|
||||||
const { DragDrop, TextEditor } = foundry.applications.ux;
|
|
||||||
|
|
||||||
export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) {
|
|
||||||
|
|
||||||
// #region Options
|
|
||||||
static DEFAULT_OPTIONS = {
|
|
||||||
tag: `form`,
|
|
||||||
classes: [
|
|
||||||
__ID__,
|
|
||||||
`AttributeManager`,
|
|
||||||
],
|
|
||||||
position: {
|
|
||||||
width: 400,
|
|
||||||
height: `auto`,
|
|
||||||
},
|
|
||||||
window: {
|
|
||||||
resizable: true,
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
submitOnChange: false,
|
|
||||||
closeOnSubmit: true,
|
|
||||||
handler: this.#onSubmit,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
addNew: this.#addNew,
|
|
||||||
removeAttribute: this.#remove,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
static PARTS = {
|
|
||||||
attributes: { template: filePath(`templates/AttributeManager/attribute-list.hbs`) },
|
|
||||||
controls: { template: filePath(`templates/AttributeManager/controls.hbs`) },
|
|
||||||
};
|
|
||||||
// #endregion Options
|
|
||||||
|
|
||||||
// #region Instance Data
|
|
||||||
/** @type {string | null} */
|
|
||||||
#doc = null;
|
|
||||||
|
|
||||||
#attributes;
|
|
||||||
|
|
||||||
constructor({ document , ...options } = {}) {
|
|
||||||
super(options);
|
|
||||||
this.#doc = document;
|
|
||||||
this.#attributes = deepClone(document.system.attr);
|
|
||||||
};
|
|
||||||
|
|
||||||
get title() {
|
|
||||||
return `Attributes: ${this.#doc.name}`;
|
|
||||||
};
|
|
||||||
// #endregion Instance Data
|
|
||||||
|
|
||||||
// #region Lifecycle
|
|
||||||
async _onRender(context, options) {
|
|
||||||
await super._onRender(context, options);
|
|
||||||
|
|
||||||
const elements = this.element
|
|
||||||
.querySelectorAll(`[data-bind]`);
|
|
||||||
for (const input of elements) {
|
|
||||||
input.addEventListener(`change`, this.#bindListener.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
new DragDrop.implementation({
|
|
||||||
dragSelector: `.draggable`,
|
|
||||||
permissions: {
|
|
||||||
dragstart: this._canDragStart.bind(this),
|
|
||||||
drop: this._canDragDrop.bind(this),
|
|
||||||
},
|
|
||||||
callbacks: {
|
|
||||||
dragstart: this._onDragStart.bind(this),
|
|
||||||
drop: this._onDrop.bind(this),
|
|
||||||
},
|
|
||||||
}).bind(this.element);
|
|
||||||
};
|
|
||||||
// #endregion Lifecycle
|
|
||||||
|
|
||||||
// #region Data Prep
|
|
||||||
async _preparePartContext(partId) {
|
|
||||||
const ctx = {};
|
|
||||||
|
|
||||||
ctx.actor = this.#doc;
|
|
||||||
|
|
||||||
switch (partId) {
|
|
||||||
case `attributes`: {
|
|
||||||
await this._prepareAttributeContext(ctx);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
};
|
|
||||||
|
|
||||||
async _prepareAttributeContext(ctx) {
|
|
||||||
const attrs = [];
|
|
||||||
for (const [id, data] of Object.entries(this.#attributes)) {
|
|
||||||
if (data == null) { continue };
|
|
||||||
attrs.push({
|
|
||||||
id,
|
|
||||||
name: data.name,
|
|
||||||
displayName: data.isNew ? `New Attribute` : data.name,
|
|
||||||
sort: data.sort,
|
|
||||||
isRange: data.isRange,
|
|
||||||
isNew: data.isNew ?? false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
ctx.attrs = attrs.sort(attributeSorter);
|
|
||||||
};
|
|
||||||
// #endregion Data Prep
|
|
||||||
|
|
||||||
// #region Actions
|
|
||||||
/**
|
|
||||||
* @param {Event} event
|
|
||||||
*/
|
|
||||||
async #bindListener(event) {
|
|
||||||
const target = event.target;
|
|
||||||
const data = target.dataset;
|
|
||||||
const binding = data.bind;
|
|
||||||
|
|
||||||
let value = target.value;
|
|
||||||
switch (target.type) {
|
|
||||||
case `checkbox`: {
|
|
||||||
value = target.checked;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
setProperty(this.#attributes, binding, value);
|
|
||||||
await this.render({ parts: [ `attributes` ]});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @this {AttributeManager} */
|
|
||||||
static async #addNew() {
|
|
||||||
const id = randomID();
|
|
||||||
this.#attributes[id] = {
|
|
||||||
name: ``,
|
|
||||||
sort: Number.MAX_SAFE_INTEGER,
|
|
||||||
isRange: false,
|
|
||||||
isNew: true,
|
|
||||||
};
|
|
||||||
await this.render({ parts: [ `attributes` ]});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @this {AttributeManager} */
|
|
||||||
static async #remove($e, element) {
|
|
||||||
const attribute = element.closest(`[data-attribute]`)?.dataset.attribute;
|
|
||||||
if (!attribute) { return };
|
|
||||||
delete this.#attributes[attribute];
|
|
||||||
this.#attributes[`-=${attribute}`] = null;
|
|
||||||
await this.render({ parts: [ `attributes` ] });
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @this {AttributeManager} */
|
|
||||||
static async #onSubmit() {
|
|
||||||
const entries = Object.entries(this.#attributes)
|
|
||||||
.map(([id, attr]) => {
|
|
||||||
if (attr == null) {
|
|
||||||
return [ id, attr ];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (attr.isNew) {
|
|
||||||
delete attr.isNew;
|
|
||||||
return [ toID(attr.name), attr ];
|
|
||||||
};
|
|
||||||
|
|
||||||
return [ id, attr ];
|
|
||||||
});
|
|
||||||
const data = Object.fromEntries(entries);
|
|
||||||
|
|
||||||
const diff = diffObject(
|
|
||||||
this.#doc.system.attr,
|
|
||||||
data,
|
|
||||||
{ inner: false, deletionKeys: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.#doc.update({ "system.attr": diff });
|
|
||||||
};
|
|
||||||
// #endregion Actions
|
|
||||||
|
|
||||||
// #region Drag & Drop
|
|
||||||
_canDragStart() {
|
|
||||||
return this.#doc.isOwner;
|
|
||||||
};
|
|
||||||
|
|
||||||
_canDragDrop() {
|
|
||||||
return this.#doc.isOwner;
|
|
||||||
};
|
|
||||||
|
|
||||||
_onDragStart(event) {
|
|
||||||
const target = event.currentTarget.closest(`[data-attribute]`);
|
|
||||||
if (`link` in event.target.dataset) { return };
|
|
||||||
let dragData;
|
|
||||||
|
|
||||||
if (target.dataset.attribute) {
|
|
||||||
const attributeID = target.dataset.attribute;
|
|
||||||
const attribute = this.#attributes[attributeID];
|
|
||||||
dragData = {
|
|
||||||
_id: attributeID,
|
|
||||||
sort: attribute.sort,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!dragData) { return };
|
|
||||||
event.dataTransfer.setDragImage(target, 16, 23);
|
|
||||||
event.dataTransfer.setData(`text/plain`, JSON.stringify(dragData));
|
|
||||||
};
|
|
||||||
|
|
||||||
_onDrop(event) {
|
|
||||||
const dropped = TextEditor.implementation.getDragEventData(event);
|
|
||||||
|
|
||||||
const dropTarget = event.target.closest(`[data-attribute]`);
|
|
||||||
if (!dropTarget) { return };
|
|
||||||
const targetID = dropTarget.dataset.attribute;
|
|
||||||
let target;
|
|
||||||
|
|
||||||
// Not moving location, ignore drop event
|
|
||||||
if (targetID === dropped._id) { return };
|
|
||||||
|
|
||||||
// Determine all of the siblings and create sort data
|
|
||||||
const siblings = [];
|
|
||||||
for (const element of dropTarget.parentElement.children) {
|
|
||||||
const siblingID = element.dataset.attribute;
|
|
||||||
const attr = this.#attributes[siblingID];
|
|
||||||
const sibling = {
|
|
||||||
_id: siblingID,
|
|
||||||
sort: attr.sort,
|
|
||||||
};
|
|
||||||
if (siblingID && siblingID !== dropped._id) {
|
|
||||||
siblings.push(sibling);
|
|
||||||
};
|
|
||||||
if (siblingID === targetID) {
|
|
||||||
target = sibling;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortUpdates = performIntegerSort(
|
|
||||||
dropped,
|
|
||||||
{
|
|
||||||
target,
|
|
||||||
siblings,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateEntries = sortUpdates.map(({ target, update }) => {
|
|
||||||
return [ `${target._id}.sort`, update.sort ];
|
|
||||||
});
|
|
||||||
const update = Object.fromEntries(updateEntries);
|
|
||||||
|
|
||||||
mergeObject(
|
|
||||||
this.#attributes,
|
|
||||||
update,
|
|
||||||
{
|
|
||||||
insertKeys: false,
|
|
||||||
insertValues: false,
|
|
||||||
inplace: true,
|
|
||||||
performDeletions: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
this.render({ parts: [ `attributes` ] });
|
|
||||||
};
|
|
||||||
// #endregion Drag & Drop
|
|
||||||
};
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
import { __ID__, filePath } from "../consts.mjs";
|
|
||||||
import { AttributeManager } from "./AttributeManager.mjs";
|
|
||||||
import { attributeSorter } from "../utils/attributeSort.mjs";
|
|
||||||
import { TAFDocumentSheetConfig } from "./TAFDocumentSheetConfig.mjs";
|
|
||||||
|
|
||||||
const { HandlebarsApplicationMixin } = foundry.applications.api;
|
|
||||||
const { ActorSheetV2 } = foundry.applications.sheets;
|
|
||||||
const { getProperty } = foundry.utils;
|
|
||||||
|
|
||||||
export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
|
|
||||||
|
|
||||||
// #region Options
|
|
||||||
static DEFAULT_OPTIONS = {
|
|
||||||
classes: [
|
|
||||||
__ID__,
|
|
||||||
`PlayerSheet`,
|
|
||||||
],
|
|
||||||
position: {
|
|
||||||
width: 575,
|
|
||||||
height: 740,
|
|
||||||
},
|
|
||||||
window: {
|
|
||||||
resizable: true,
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
submitOnChange: true,
|
|
||||||
closeOnSubmit: false,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
manageAttributes: this.#manageAttributes,
|
|
||||||
configureSheet: this.#configureSheet,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
static PARTS = {
|
|
||||||
header: { template: filePath(`templates/PlayerSheet/header.hbs`) },
|
|
||||||
attributes: { template: filePath(`templates/PlayerSheet/attributes.hbs`) },
|
|
||||||
content: { template: filePath(`templates/PlayerSheet/content.hbs`) },
|
|
||||||
};
|
|
||||||
// #endregion Options
|
|
||||||
|
|
||||||
// #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() {
|
|
||||||
const controls = super._getHeaderControls();
|
|
||||||
|
|
||||||
controls.push({
|
|
||||||
icon: `fa-solid fa-at`,
|
|
||||||
label: `Manage Attributes`,
|
|
||||||
action: `manageAttributes`,
|
|
||||||
visible: () => {
|
|
||||||
const isGM = game.user.isGM;
|
|
||||||
const allowPlayerEdits = game.settings.get(__ID__, `canPlayersManageAttributes`);
|
|
||||||
const editable = this.isEditable;
|
|
||||||
return isGM || (allowPlayerEdits && editable);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return controls;
|
|
||||||
};
|
|
||||||
|
|
||||||
async close() {
|
|
||||||
this.#attributeManager?.close();
|
|
||||||
this.#attributeManager = null;
|
|
||||||
return super.close();
|
|
||||||
};
|
|
||||||
// #endregion Lifecycle
|
|
||||||
|
|
||||||
// #region Data Prep
|
|
||||||
async _preparePartContext(partID) {
|
|
||||||
let ctx = {
|
|
||||||
actor: this.actor,
|
|
||||||
system: this.actor.system,
|
|
||||||
editable: this.isEditable,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (partID) {
|
|
||||||
case `attributes`: {
|
|
||||||
await this._prepareAttributes(ctx);
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
case `content`: {
|
|
||||||
await this._prepareContent(ctx);
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
};
|
|
||||||
|
|
||||||
async _prepareAttributes(ctx) {
|
|
||||||
ctx.hasAttributes = this.actor.system.hasAttributes;
|
|
||||||
|
|
||||||
const attrs = [];
|
|
||||||
for (const [id, data] of Object.entries(this.actor.system.attr)) {
|
|
||||||
attrs.push({
|
|
||||||
...data,
|
|
||||||
id,
|
|
||||||
path: `system.attr.${id}`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
ctx.attrs = attrs.toSorted(attributeSorter);
|
|
||||||
};
|
|
||||||
|
|
||||||
async _prepareContent(ctx) {
|
|
||||||
const TextEditor = foundry.applications.ux.TextEditor.implementation;
|
|
||||||
ctx.enriched = {
|
|
||||||
system: {
|
|
||||||
content: await TextEditor.enrichHTML(this.actor.system.content),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// #endregion Data Prep
|
|
||||||
|
|
||||||
// #region Actions
|
|
||||||
#attributeManager = null;
|
|
||||||
/** @this {PlayerSheet} */
|
|
||||||
static async #manageAttributes() {
|
|
||||||
this.#attributeManager ??= new AttributeManager({ document: this.actor });
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { TafSVGLoader } from "./svgLoader.mjs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
Attributes:
|
|
||||||
@property {string} name - The name of the icon, takes precedence over the path
|
|
||||||
@property {string} path - The path of the icon file
|
|
||||||
*/
|
|
||||||
export class TafIcon extends TafSVGLoader {
|
|
||||||
static elementName = `taf-icon`;
|
|
||||||
static _stylePath = `icon.css`;
|
|
||||||
};
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { Logger } from "../../utils/Logger.mjs";
|
|
||||||
import { TafIcon } from "./Icon.mjs";
|
|
||||||
import { TafSVGLoader } from "./svgLoader.mjs";
|
|
||||||
|
|
||||||
const components = [
|
|
||||||
TafSVGLoader,
|
|
||||||
TafIcon,
|
|
||||||
];
|
|
||||||
|
|
||||||
export function registerCustomComponents() {
|
|
||||||
(CONFIG.CACHE ??= {}).componentListeners ??= [];
|
|
||||||
for (const component of components) {
|
|
||||||
if (!window.customElements.get(component.elementName)) {
|
|
||||||
Logger.debug(`Registering component "${component.elementName}"`);
|
|
||||||
window.customElements.define(
|
|
||||||
component.elementName,
|
|
||||||
component,
|
|
||||||
);
|
|
||||||
if (component.formAssociated) {
|
|
||||||
CONFIG.CACHE.componentListeners.push(component.elementName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
export const __ID__ = `taf`;
|
|
||||||
|
|
||||||
// MARK: filePath
|
|
||||||
export function filePath(path) {
|
|
||||||
if (path.startsWith(`/`)) {
|
|
||||||
path = path.slice(1);
|
|
||||||
};
|
|
||||||
return `systems/${__ID__}/${path}`;
|
|
||||||
};
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
export class PlayerData extends foundry.abstract.TypeDataModel {
|
|
||||||
static defineSchema() {
|
|
||||||
const fields = foundry.data.fields;
|
|
||||||
return {
|
|
||||||
content: new fields.HTMLField({
|
|
||||||
blank: true,
|
|
||||||
trim: true,
|
|
||||||
initial: ``,
|
|
||||||
}),
|
|
||||||
attr: new fields.TypedObjectField(
|
|
||||||
new fields.SchemaField({
|
|
||||||
name: new fields.StringField({ blank: false, trim: true }),
|
|
||||||
sort: new fields.NumberField({ min: 1, initial: 1, integer: true, nullable: false }),
|
|
||||||
value: new fields.NumberField({ min: 0, initial: 0, integer: true, nullable: false }),
|
|
||||||
max: new fields.NumberField({ min: 0, initial: null, integer: true, nullable: true }),
|
|
||||||
isRange: new fields.BooleanField({ initial: false, nullable: false }),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
initial: {},
|
|
||||||
nullable: false,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
get hasAttributes() {
|
|
||||||
return Object.keys(this.attr).length > 0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
const { Actor } = foundry.documents;
|
|
||||||
|
|
||||||
export class TAFActor extends Actor {
|
|
||||||
async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) {
|
|
||||||
const attr = foundry.utils.getProperty(this.system, attribute);
|
|
||||||
const current = isBar ? attr.value : attr;
|
|
||||||
const update = isDelta ? current + value : value;
|
|
||||||
if ( update === current ) {
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine the updates to make to the actor data
|
|
||||||
let updates;
|
|
||||||
if (isBar) {
|
|
||||||
updates = {[`system.${attribute}.value`]: Math.clamp(update, 0, attr.max)};
|
|
||||||
} else {
|
|
||||||
updates = {[`system.${attribute}`]: update};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Allow a hook to override these changes
|
|
||||||
const allowed = Hooks.call(`modifyTokenAttribute`, {attribute, value, isDelta, isBar}, updates, this);
|
|
||||||
|
|
||||||
return allowed !== false ? this.update(updates) : this;
|
|
||||||
};
|
|
||||||
|
|
||||||
getRollData() {
|
|
||||||
const data = {};
|
|
||||||
|
|
||||||
if (`attr` in this.system) {
|
|
||||||
for (const attrID in this.system.attr) {
|
|
||||||
const attr = this.system.attr[attrID];
|
|
||||||
if (attr.isRange) {
|
|
||||||
data[attrID] = {
|
|
||||||
value: attr.value,
|
|
||||||
max: attr.max,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
data[attrID] = attr.value;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
const { Item } = foundry.documents;
|
|
||||||
|
|
||||||
export class TAFItem extends Item {
|
|
||||||
async _preCreate() {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
const { TokenDocument } = foundry.documents;
|
|
||||||
const { getProperty, getType, hasProperty, isSubclass } = foundry.utils;
|
|
||||||
|
|
||||||
export class TAFTokenDocument extends TokenDocument {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
* This override's purpose is to make it so that Token attributes and bars can
|
|
||||||
* be accessed from the data model's values directly instead of relying on only
|
|
||||||
* the schema, which doesn't account for my TypedObjectField of attributes.
|
|
||||||
*/
|
|
||||||
static getTrackedAttributes(data, _path = []) {
|
|
||||||
|
|
||||||
// Case 1 - Infer attributes from schema structure.
|
|
||||||
if ( (data instanceof foundry.abstract.DataModel) || isSubclass(data, foundry.abstract.DataModel) ) {
|
|
||||||
return this._getTrackedAttributesFromObject(data, _path);
|
|
||||||
}
|
|
||||||
if ( data instanceof foundry.data.fields.SchemaField ) {
|
|
||||||
return this._getTrackedAttributesFromSchema(data, _path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 2 - Infer attributes from object structure.
|
|
||||||
if ( [`Object`, `Array`].includes(getType(data)) ) {
|
|
||||||
return this._getTrackedAttributesFromObject(data, _path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 3 - Retrieve explicitly configured attributes.
|
|
||||||
if ( !data || (typeof data === `string`) ) {
|
|
||||||
const config = this._getConfiguredTrackedAttributes(data);
|
|
||||||
if ( config ) {
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
data = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track the path and record found attributes
|
|
||||||
if ( data !== undefined ) {
|
|
||||||
return {bar: [], value: []};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case 4 - Infer attributes from system template.
|
|
||||||
const bar = new Set();
|
|
||||||
const value = new Set();
|
|
||||||
for ( const [type, model] of Object.entries(game.model.Actor) ) {
|
|
||||||
const dataModel = CONFIG.Actor.dataModels?.[type];
|
|
||||||
const inner = this.getTrackedAttributes(dataModel ?? model, _path);
|
|
||||||
inner.bar.forEach(attr => bar.add(attr.join(`.`)));
|
|
||||||
inner.value.forEach(attr => value.add(attr.join(`.`)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
bar: Array.from(bar).map(attr => attr.split(`.`)),
|
|
||||||
value: Array.from(value).map(attr => attr.split(`.`)),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
getBarAttribute(barName, {alternative} = {}) {
|
|
||||||
const attribute = alternative || this[barName]?.attribute;
|
|
||||||
|
|
||||||
if (!attribute || !this.actor) {
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
const system = this.actor.system;
|
|
||||||
|
|
||||||
// Get the current attribute value
|
|
||||||
const data = getProperty(system, attribute);
|
|
||||||
if (data == null) {
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Number.isNumeric(data)) {
|
|
||||||
let editable = hasProperty(system, attribute);
|
|
||||||
return {
|
|
||||||
type: `value`,
|
|
||||||
attribute,
|
|
||||||
value: Number(data),
|
|
||||||
editable,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (`value` in data && `max` in data) {
|
|
||||||
let editable = hasProperty(system, `${attribute}.value`);
|
|
||||||
const isRange = getProperty(system, `${attribute}.isRange`);
|
|
||||||
if (isRange) {
|
|
||||||
return {
|
|
||||||
type: `bar`,
|
|
||||||
attribute,
|
|
||||||
value: parseInt(data.value || 0),
|
|
||||||
max: parseInt(data.max || 0),
|
|
||||||
editable,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
type: `value`,
|
|
||||||
attribute: `${attribute}.value`,
|
|
||||||
value: Number(data.value),
|
|
||||||
editable,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Otherwise null
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { filePath } from "../consts.mjs";
|
|
||||||
import { options } from "./options.mjs";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
systemFilePath: filePath,
|
|
||||||
"taf-options": options,
|
|
||||||
};
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
// Apps
|
|
||||||
import { PlayerSheet } from "../apps/PlayerSheet.mjs";
|
|
||||||
|
|
||||||
// Data Models
|
|
||||||
import { PlayerData } from "../data/Player.mjs";
|
|
||||||
|
|
||||||
// Documents
|
|
||||||
import { TAFActor } from "../documents/Actor.mjs";
|
|
||||||
import { TAFItem } from "../documents/Item.mjs";
|
|
||||||
import { TAFTokenDocument } from "../documents/Token.mjs";
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
import { registerWorldSettings } from "../settings/world.mjs";
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
import { __ID__ } from "../consts.mjs";
|
|
||||||
import helpers from "../handlebarsHelpers/_index.mjs";
|
|
||||||
import { Logger } from "../utils/Logger.mjs";
|
|
||||||
import { registerCustomComponents } from "../apps/elements/_index.mjs";
|
|
||||||
import { registerSockets } from "../sockets/_index.mjs";
|
|
||||||
|
|
||||||
Hooks.on(`init`, () => {
|
|
||||||
Logger.debug(`Initializing`);
|
|
||||||
|
|
||||||
CONFIG.Token.documentClass = TAFTokenDocument;
|
|
||||||
CONFIG.Actor.documentClass = TAFActor;
|
|
||||||
|
|
||||||
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(
|
|
||||||
__ID__,
|
|
||||||
PlayerSheet,
|
|
||||||
{
|
|
||||||
makeDefault: true,
|
|
||||||
label: `taf.sheet-names.PlayerSheet`,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
registerWorldSettings();
|
|
||||||
|
|
||||||
registerSockets();
|
|
||||||
registerCustomComponents();
|
|
||||||
Handlebars.registerHelper(helpers);
|
|
||||||
});
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { userActivity } from "../utils/QueryManager.mjs";
|
|
||||||
|
|
||||||
Hooks.on(`userConnected`, (user, connected) => {
|
|
||||||
if (user.isSelf) { return };
|
|
||||||
userActivity(user.id, connected);
|
|
||||||
});
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import "./api.mjs";
|
|
||||||
import "./hooks/init.mjs";
|
|
||||||
import "./hooks/userConnected.mjs";
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { __ID__ } from "../consts.mjs";
|
|
||||||
|
|
||||||
export function registerWorldSettings() {
|
|
||||||
game.settings.register(__ID__, `canPlayersManageAttributes`, {
|
|
||||||
name: `taf.settings.canPlayersManageAttributes.name`,
|
|
||||||
hint: `taf.settings.canPlayersManageAttributes.hint`,
|
|
||||||
config: true,
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
scope: `world`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
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,114 +0,0 @@
|
||||||
import { Ask } from "../apps/Ask.mjs";
|
|
||||||
|
|
||||||
/** @type {Map<string, Promise>} */
|
|
||||||
const promises = new Map();
|
|
||||||
|
|
||||||
/** @type {Map<string, ApplicationV2>} */
|
|
||||||
const dialogs = new Map();
|
|
||||||
|
|
||||||
export function close(id) {
|
|
||||||
dialogs.get(id)?.close();
|
|
||||||
dialogs.delete(id);
|
|
||||||
promises.delete(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asks the user to provide a simple piece of information, this is primarily
|
|
||||||
* intended to be used within macros so that it can have better info gathering
|
|
||||||
* 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
|
|
||||||
* without an object wrapper, allowing for easier access.
|
|
||||||
*
|
|
||||||
* @param {AskConfig} data
|
|
||||||
* @param {AskOptions} opts
|
|
||||||
* @returns {AskResult}
|
|
||||||
*/
|
|
||||||
export async function ask(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
onlyOneWaiting = true,
|
|
||||||
alwaysUseAnswerObject = true,
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
if (!data.id) {
|
|
||||||
return {
|
|
||||||
state: `errored`,
|
|
||||||
error: `An ID must be provided`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if (!data.inputs.length) {
|
|
||||||
return {
|
|
||||||
state: `errored`,
|
|
||||||
error: `At least one input must be provided`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const id = data.id;
|
|
||||||
|
|
||||||
// Don't do multi-thread waiting
|
|
||||||
if (dialogs.has(id)) {
|
|
||||||
const app = dialogs.get(id);
|
|
||||||
app.bringToFront();
|
|
||||||
if (onlyOneWaiting) {
|
|
||||||
return { state: `fronted` };
|
|
||||||
} else {
|
|
||||||
return promises.get(id);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let autofocusClaimed = false;
|
|
||||||
for (const i of data.inputs) {
|
|
||||||
i.id ??= foundry.utils.randomID(16);
|
|
||||||
i.key ??= i.label;
|
|
||||||
|
|
||||||
switch (i.type) {
|
|
||||||
case `input`: {
|
|
||||||
i.inputType ??= `text`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only ever allow one input to claim autofocus
|
|
||||||
i.autofocus &&= !autofocusClaimed;
|
|
||||||
autofocusClaimed ||= i.autofocus;
|
|
||||||
|
|
||||||
// Set the value's attribute name if it isn't specified explicitly
|
|
||||||
if (!i.valueAttribute) {
|
|
||||||
switch (i.inputType) {
|
|
||||||
case `checkbox`:
|
|
||||||
i.type = `checkbox`;
|
|
||||||
delete i.valueAttribute;
|
|
||||||
delete i.inputType;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
i.valueAttribute = `value`;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const promise = new Promise((resolve) => {
|
|
||||||
const app = new Ask({
|
|
||||||
...data,
|
|
||||||
alwaysUseAnswerObject,
|
|
||||||
onClose: () => {
|
|
||||||
dialogs.delete(id);
|
|
||||||
promises.delete(id);
|
|
||||||
resolve({ state: `prompted` });
|
|
||||||
},
|
|
||||||
onConfirm: (answers) => resolve({ state: `prompted`, answers }),
|
|
||||||
});
|
|
||||||
app.render({ force: true });
|
|
||||||
dialogs.set(id, app);
|
|
||||||
});
|
|
||||||
|
|
||||||
promises.set(id, promise);
|
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function size() {
|
|
||||||
return dialogs.size;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DialogManager = {
|
|
||||||
close,
|
|
||||||
ask,
|
|
||||||
size,
|
|
||||||
};
|
|
||||||
|
|
@ -1,289 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export function attributeSorter(a, b) {
|
|
||||||
if (a.sort === b.sort) {
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
};
|
|
||||||
return Math.sign(a.sort - b.sort);
|
|
||||||
};
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
/**
|
|
||||||
* A helper method that converts an arbitrary string into a format that can be
|
|
||||||
* used as an object key easily.
|
|
||||||
*
|
|
||||||
* @param {string} text The text to convert
|
|
||||||
* @returns The converted ID
|
|
||||||
*/
|
|
||||||
export function toID(text) {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, `_`)
|
|
||||||
.replace(/\W/g, ``);
|
|
||||||
};
|
|
||||||
5896
package-lock.json
generated
5896
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
|
@ -1,18 +1,16 @@
|
||||||
{
|
{
|
||||||
"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": {
|
||||||
"data:build": "node scripts/buildCompendia.mjs",
|
"css": "sass --watch --embed-source-map --no-error-css styles/:.styles/",
|
||||||
"data:extract": "node scripts/extractCompendia.mjs",
|
"build": "sass --embed-source-map --no-error-css styles/:.styles/",
|
||||||
"link": "node scripts/linkFoundry.mjs",
|
|
||||||
"lint": "eslint --fix",
|
"lint": "eslint --fix",
|
||||||
"lint:nofix": "eslint"
|
"lint:nofix": "eslint"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
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();
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
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);
|
|
||||||
};
|
|
||||||
77
scripts/macros/rollDice.mjs
Normal file
77
scripts/macros/rollDice.mjs
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
async function rollDice() {
|
||||||
|
const sidesOnDice = 6;
|
||||||
|
|
||||||
|
const answers = await DialogManager.ask({
|
||||||
|
id: `eat-the-reich-dice-pool`,
|
||||||
|
question: `Set up your dice pool:`,
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
key: `statBase`,
|
||||||
|
inputType: `number`,
|
||||||
|
defaultValue: 2,
|
||||||
|
label: `Number of Dice`,
|
||||||
|
autofocus: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: `successThreshold`,
|
||||||
|
inputType: `number`,
|
||||||
|
defaultValue: 3,
|
||||||
|
label: `Success Threshold (d${sidesOnDice} > X)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: `critsEnabled`,
|
||||||
|
inputType: `checkbox`,
|
||||||
|
defaultValue: true,
|
||||||
|
label: `Enable Criticals`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const { statBase, successThreshold, critsEnabled } = answers;
|
||||||
|
let rollMode = game.settings.get(`core`, `rollMode`);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let successes = 0;
|
||||||
|
let critsOnly = 0;
|
||||||
|
const results = [];
|
||||||
|
for (let i = statBase; i > 0; i--) {
|
||||||
|
let r = new Roll(`1d${sidesOnDice}`);
|
||||||
|
await r.evaluate();
|
||||||
|
let classes = `roll die d6`;
|
||||||
|
|
||||||
|
// Determine the success count and class modifications for the chat
|
||||||
|
if (r.total > successThreshold) {
|
||||||
|
successes++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
classes += ` failure`
|
||||||
|
}
|
||||||
|
if (r.total === sidesOnDice && critsEnabled) {
|
||||||
|
successes++;
|
||||||
|
critsOnly++;
|
||||||
|
classes += ` success`;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(`<li class="${classes}">${r.total}</li>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = `Rolls:<div class="dice-tooltip"><ol class="dice-rolls">${results.join(``)}</ol></div><hr>Successes: ${successes}<br>Crits: ${critsOnly}`;
|
||||||
|
|
||||||
|
|
||||||
|
if (rollMode === CONST.DICE_ROLL_MODES.BLIND) {
|
||||||
|
ui.notifications.warn(`Cannot make a blind roll from the macro, rolling with mode "Private GM Roll" instead`);
|
||||||
|
rollMode = CONST.DICE_ROLL_MODES.PRIVATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatData = ChatMessage.applyRollMode(
|
||||||
|
{
|
||||||
|
title: `Dice Pool`,
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
rollMode,
|
||||||
|
);
|
||||||
|
|
||||||
|
await ChatMessage.implementation.create(chatData);
|
||||||
|
}
|
||||||
|
|
||||||
|
rollDice()
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
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`);
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
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();
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
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();
|
|
||||||
32
src/components/_index.mjs
Normal file
32
src/components/_index.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { SystemIcon } from "./icon.mjs";
|
||||||
|
import { SystemIncrementer } from "./incrementer.mjs";
|
||||||
|
import { SystemRange } from "./range.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of element classes to register, expects all of them to have a static
|
||||||
|
* property of "elementName" that is the namespaced name that the component will
|
||||||
|
* be registered under. Any elements that are formAssociated have their name added
|
||||||
|
* to the "CONFIG.CACHE.componentListeners" array and should be listened to for
|
||||||
|
* "change" events in sheets.
|
||||||
|
*/
|
||||||
|
const components = [
|
||||||
|
SystemIcon,
|
||||||
|
SystemIncrementer,
|
||||||
|
SystemRange,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function registerCustomComponents() {
|
||||||
|
(CONFIG.CACHE ??= {}).componentListeners ??= [];
|
||||||
|
for (const component of components) {
|
||||||
|
if (!window.customElements.get(component.elementName)) {
|
||||||
|
console.debug(`${game.system.id} | Registering component "${component.elementName}"`);
|
||||||
|
window.customElements.define(
|
||||||
|
component.elementName,
|
||||||
|
component,
|
||||||
|
);
|
||||||
|
if (component.formAssociated) {
|
||||||
|
CONFIG.CACHE.componentListeners.push(component.elementName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
import { filePath } from "../../consts.mjs";
|
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
||||||
import { Logger } from "../../utils/Logger.mjs";
|
|
||||||
import { StyledShadowElement } from "./StyledShadowElement.mjs";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Attributes:
|
Attributes:
|
||||||
@property {string} name - The name of the icon, takes precedence over the path
|
@property {string} name - The name of the icon, takes precedence over the path
|
||||||
@property {string} path - The path of the icon file
|
@property {string} path - The path of the icon file
|
||||||
*/
|
*/
|
||||||
export class TafSVGLoader extends StyledShadowElement(HTMLElement) {
|
export class SystemIcon extends StyledShadowElement(HTMLElement) {
|
||||||
static elementName = `taf-svg`;
|
static elementName = `dd-icon`;
|
||||||
static formAssociated = false;
|
static formAssociated = false;
|
||||||
|
|
||||||
/* Stuff for the mixin to use */
|
/* Stuff for the mixin to use */
|
||||||
static _stylePath = `svg-loader.css`;
|
static _stylePath = ``;
|
||||||
|
|
||||||
|
|
||||||
static _cache = new Map();
|
static _cache = new Map();
|
||||||
|
|
@ -22,8 +20,12 @@ export class TafSVGLoader extends StyledShadowElement(HTMLElement) {
|
||||||
/** @type {null | string} */
|
/** @type {null | string} */
|
||||||
_path;
|
_path;
|
||||||
|
|
||||||
|
/* Stored IDs for all of the hooks that are in this component */
|
||||||
|
#svgHmr;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
// this._shadow = this.attachShadow({ mode: `open`, delegatesFocus: true });
|
||||||
|
|
||||||
this.#container = document.createElement(`div`);
|
this.#container = document.createElement(`div`);
|
||||||
this._shadow.appendChild(this.#container);
|
this._shadow.appendChild(this.#container);
|
||||||
|
|
@ -32,7 +34,7 @@ export class TafSVGLoader extends StyledShadowElement(HTMLElement) {
|
||||||
_mounted = false;
|
_mounted = false;
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
if (this._mounted) { return };
|
if (this._mounted) { return }
|
||||||
|
|
||||||
this._name = this.getAttribute(`name`);
|
this._name = this.getAttribute(`name`);
|
||||||
this._path = this.getAttribute(`path`);
|
this._path = this.getAttribute(`path`);
|
||||||
|
|
@ -54,7 +56,7 @@ export class TafSVGLoader extends StyledShadowElement(HTMLElement) {
|
||||||
*/
|
*/
|
||||||
let content;
|
let content;
|
||||||
if (this._name) {
|
if (this._name) {
|
||||||
content = await this.#getIcon(filePath(`assets/${this._name}.svg`));
|
content = await this.#getIcon(`./systems/dotdungeon/assets/${this._name}.svg`);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this._path && !content) {
|
if (this._path && !content) {
|
||||||
|
|
@ -65,12 +67,28 @@ export class TafSVGLoader extends StyledShadowElement(HTMLElement) {
|
||||||
this.#container.appendChild(content.cloneNode(true));
|
this.#container.appendChild(content.cloneNode(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is so that when we get an HMR event from Foundry we can appropriately
|
||||||
|
handle it using our logic to update the component and the icon cache.
|
||||||
|
*/
|
||||||
|
if (game.settings.get(game.system.id, `devMode`)) {
|
||||||
|
this.#svgHmr = Hooks.on(`${game.system.id}-hmr:svg`, (iconName, data) => {
|
||||||
|
if (this._name === iconName || this._path?.endsWith(data.path)) {
|
||||||
|
const svg = this.#parseSVG(data.content);
|
||||||
|
this.constructor._cache.set(iconName, svg);
|
||||||
|
this.#container.replaceChildren(svg.cloneNode(true));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
this._mounted = true;
|
this._mounted = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
if (!this._mounted) { return };
|
if (!this._mounted) { return }
|
||||||
|
|
||||||
|
Hooks.off(`${game.system.id}-hmr:svg`, this.#svgHmr);
|
||||||
|
|
||||||
this._mounted = false;
|
this._mounted = false;
|
||||||
};
|
};
|
||||||
|
|
@ -78,7 +96,7 @@ export class TafSVGLoader extends StyledShadowElement(HTMLElement) {
|
||||||
async #getIcon(path) {
|
async #getIcon(path) {
|
||||||
// Cache hit!
|
// Cache hit!
|
||||||
if (this.constructor._cache.has(path)) {
|
if (this.constructor._cache.has(path)) {
|
||||||
Logger.debug(`Image ${path} cache hit`);
|
Logger.debug(`Icon ${path} cache hit`);
|
||||||
return this.constructor._cache.get(path);
|
return this.constructor._cache.get(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -92,7 +110,7 @@ export class TafSVGLoader extends StyledShadowElement(HTMLElement) {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
Logger.debug(`Adding image ${path} to the cache`);
|
Logger.debug(`Adding icon ${path} to the cache`);
|
||||||
const svg = this.#parseSVG(await r.text());
|
const svg = this.#parseSVG(await r.text());
|
||||||
this.constructor._cache.set(path, svg);
|
this.constructor._cache.set(path, svg);
|
||||||
return svg;
|
return svg;
|
||||||
153
src/components/incrementer.mjs
Normal file
153
src/components/incrementer.mjs
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
||||||
|
import { SystemIcon } from "./icon.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
Attributes:
|
||||||
|
@property {string} name - The path to the value to update
|
||||||
|
@property {number} value - The actual value of the input
|
||||||
|
@property {number} min - The minimum value of the input
|
||||||
|
@property {number} max - The maximum value of the input
|
||||||
|
@property {number?} smallStep - The step size used for the buttons and arrow keys
|
||||||
|
@property {number?} largeStep - The step size used for the buttons + Ctrl and page up / down
|
||||||
|
|
||||||
|
Styling:
|
||||||
|
- `--height`: Controls the height of the element + the width of the buttons (default: 1.25rem)
|
||||||
|
- `--width`: Controls the width of the number input (default 50px)
|
||||||
|
*/
|
||||||
|
export class SystemIncrementer extends StyledShadowElement(HTMLElement) {
|
||||||
|
static elementName = `dd-incrementer`;
|
||||||
|
static formAssociated = true;
|
||||||
|
|
||||||
|
static _stylePath = `v1/components/incrementer.scss`;
|
||||||
|
|
||||||
|
_internals;
|
||||||
|
#input;
|
||||||
|
|
||||||
|
_min;
|
||||||
|
_max;
|
||||||
|
_smallStep;
|
||||||
|
_largeStep;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Form internals
|
||||||
|
this._internals = this.attachInternals();
|
||||||
|
this._internals.role = `spinbutton`;
|
||||||
|
};
|
||||||
|
|
||||||
|
get form() {
|
||||||
|
return this._internals.form;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.getAttribute(`name`);
|
||||||
|
}
|
||||||
|
set name(value) {
|
||||||
|
this.setAttribute(`name`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.getAttribute(`value`);
|
||||||
|
};
|
||||||
|
set value(value) {
|
||||||
|
this.setAttribute(`value`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return `number`;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.replaceChildren();
|
||||||
|
|
||||||
|
// Attribute parsing / registration
|
||||||
|
const value = this.getAttribute(`value`);
|
||||||
|
this._min = parseInt(this.getAttribute(`min`) ?? 0);
|
||||||
|
this._max = parseInt(this.getAttribute(`max`) ?? 0);
|
||||||
|
this._smallStep = parseInt(this.getAttribute(`smallStep`) ?? 1);
|
||||||
|
this._largeStep = parseInt(this.getAttribute(`largeStep`) ?? 5);
|
||||||
|
|
||||||
|
this._internals.ariaValueMin = this._min;
|
||||||
|
this._internals.ariaValueMax = this._max;
|
||||||
|
|
||||||
|
const container = document.createElement(`div`);
|
||||||
|
|
||||||
|
// The input that the user can see / modify
|
||||||
|
const input = document.createElement(`input`);
|
||||||
|
this.#input = input;
|
||||||
|
input.type = `number`;
|
||||||
|
input.ariaHidden = true;
|
||||||
|
input.min = this.getAttribute(`min`);
|
||||||
|
input.max = this.getAttribute(`max`);
|
||||||
|
input.addEventListener(`change`, this.#updateValue.bind(this));
|
||||||
|
input.value = value;
|
||||||
|
|
||||||
|
// plus button
|
||||||
|
const increment = document.createElement(SystemIcon.elementName);
|
||||||
|
increment.setAttribute(`name`, `ui/plus`);
|
||||||
|
increment.setAttribute(`var:size`, `0.75rem`);
|
||||||
|
increment.setAttribute(`var:fill`, `currentColor`);
|
||||||
|
increment.ariaHidden = true;
|
||||||
|
increment.classList.value = `increment`;
|
||||||
|
increment.addEventListener(`mousedown`, this.#increment.bind(this));
|
||||||
|
|
||||||
|
// minus button
|
||||||
|
const decrement = document.createElement(SystemIcon.elementName);
|
||||||
|
decrement.setAttribute(`name`, `ui/minus`);
|
||||||
|
decrement.setAttribute(`var:size`, `0.75rem`);
|
||||||
|
decrement.setAttribute(`var:fill`, `currentColor`);
|
||||||
|
decrement.ariaHidden = true;
|
||||||
|
decrement.classList.value = `decrement`;
|
||||||
|
decrement.addEventListener(`mousedown`, this.#decrement.bind(this));
|
||||||
|
|
||||||
|
// Construct the DOM
|
||||||
|
container.appendChild(decrement);
|
||||||
|
container.appendChild(input);
|
||||||
|
container.appendChild(increment);
|
||||||
|
this._shadow.appendChild(container);
|
||||||
|
|
||||||
|
/*
|
||||||
|
This converts all of the namespace prefixed properties on the element to
|
||||||
|
CSS variables so that they don't all need to be provided by doing style=""
|
||||||
|
*/
|
||||||
|
for (const attrVar of this.attributes) {
|
||||||
|
if (attrVar.name?.startsWith(`var:`)) {
|
||||||
|
const prop = attrVar.name.replace(`var:`, ``);
|
||||||
|
this.style.setProperty(`--` + prop, attrVar.value);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#updateValue() {
|
||||||
|
let value = parseInt(this.#input.value);
|
||||||
|
if (this.getAttribute(`min`)) {
|
||||||
|
value = Math.max(this._min, value);
|
||||||
|
}
|
||||||
|
if (this.getAttribute(`max`)) {
|
||||||
|
value = Math.min(this._max, value);
|
||||||
|
}
|
||||||
|
this.#input.value = value;
|
||||||
|
this.value = value;
|
||||||
|
this.dispatchEvent(new Event(`change`, { bubbles: true }));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @param {Event} $e */
|
||||||
|
#increment($e) {
|
||||||
|
$e.preventDefault();
|
||||||
|
let value = parseInt(this.#input.value);
|
||||||
|
value += $e.ctrlKey ? this._largeStep : this._smallStep;
|
||||||
|
this.#input.value = value;
|
||||||
|
this.#updateValue();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @param {Event} $e */
|
||||||
|
#decrement($e) {
|
||||||
|
$e.preventDefault();
|
||||||
|
let value = parseInt(this.#input.value);
|
||||||
|
value -= $e.ctrlKey ? this._largeStep : this._smallStep;
|
||||||
|
this.#input.value = value;
|
||||||
|
this.#updateValue();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { filePath } from "../../consts.mjs";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLElement} Base
|
* @param {HTMLElement} Base
|
||||||
*/
|
*/
|
||||||
|
|
@ -13,9 +11,9 @@ export function StyledShadowElement(Base) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The stringified CSS to use
|
* The stringified CSS to use
|
||||||
* @type {Map<string, string>}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
static _styles = new Map();
|
static _styles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTML element of the stylesheet
|
* The HTML element of the stylesheet
|
||||||
|
|
@ -26,6 +24,12 @@ export function StyledShadowElement(Base) {
|
||||||
/** @type {ShadowRoot} */
|
/** @type {ShadowRoot} */
|
||||||
_shadow;
|
_shadow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hook ID for this element's CSS hot reload
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
#cssHmr;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|
@ -36,28 +40,38 @@ export function StyledShadowElement(Base) {
|
||||||
|
|
||||||
#mounted = false;
|
#mounted = false;
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
if (this.#mounted) { return };
|
if (this.#mounted) { return }
|
||||||
|
|
||||||
this._getStyles();
|
this._getStyles();
|
||||||
|
|
||||||
|
if (game.settings.get(`dotdungeon`, `devMode`)) {
|
||||||
|
this.#cssHmr = Hooks.on(`dd-hmr:css`, (data) => {
|
||||||
|
if (data.path.endsWith(this.constructor._stylePath)) {
|
||||||
|
this._style.innerHTML = data.content;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
this.#mounted = true;
|
this.#mounted = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
if (!this.#mounted) { return };
|
if (!this.#mounted) { return }
|
||||||
|
if (this.#cssHmr != null) {
|
||||||
|
Hooks.off(`dd-hmr:css`, this.#cssHmr);
|
||||||
|
this.#cssHmr = null;
|
||||||
|
};
|
||||||
this.#mounted = false;
|
this.#mounted = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
_getStyles() {
|
_getStyles() {
|
||||||
// TODO: Cache the CSS content in a more sane way that doesn't break
|
if (this.constructor._styles) {
|
||||||
const stylePath = this.constructor._stylePath;
|
this._style.innerHTML = this.constructor._styles;
|
||||||
if (this.constructor._styles.has(stylePath)) {
|
|
||||||
this._style.innerHTML = this.constructor._styles.get(stylePath);
|
|
||||||
} else {
|
} else {
|
||||||
fetch(filePath(`styles/components/${stylePath}`))
|
fetch(`./systems/dotdungeon/.styles/${this.constructor._stylePath}`)
|
||||||
.then(r => r.text())
|
.then(r => r.text())
|
||||||
.then(t => {
|
.then(t => {
|
||||||
this.constructor._styles.set(stylePath, t);
|
this.constructor._styles = t;
|
||||||
this._style.innerHTML = t;
|
this._style.innerHTML = t;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
138
src/components/range.mjs
Normal file
138
src/components/range.mjs
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { StyledShadowElement } from "./mixins/Styles.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
Attributes:
|
||||||
|
@property {string} name - The path to the value to update in the datamodel
|
||||||
|
@property {number} value - The actual value of the input
|
||||||
|
@property {number} max - The maximum value that this range has
|
||||||
|
|
||||||
|
@extends {HTMLElement}
|
||||||
|
*/
|
||||||
|
export class SystemRange
|
||||||
|
extends StyledShadowElement(
|
||||||
|
HTMLElement,
|
||||||
|
{ mode: `open`, delegatesFocus: true },
|
||||||
|
) {
|
||||||
|
static elementName = `dd-range`;
|
||||||
|
static formAssociated = true;
|
||||||
|
|
||||||
|
static observedAttributes = [`max`];
|
||||||
|
|
||||||
|
static _stylePath = `v3/components/range.css`;
|
||||||
|
|
||||||
|
_internals;
|
||||||
|
#input;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Form internals
|
||||||
|
this._internals = this.attachInternals();
|
||||||
|
this._internals.role = `spinbutton`;
|
||||||
|
};
|
||||||
|
|
||||||
|
get form() {
|
||||||
|
return this._internals.form;
|
||||||
|
};
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.getAttribute(`name`);
|
||||||
|
};
|
||||||
|
set name(value) {
|
||||||
|
this.setAttribute(`name`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
try {
|
||||||
|
return parseInt(this.getAttribute(`value`));
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Failed to parse attribute: "value" - Make sure it's an integer`);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
set value(value) {
|
||||||
|
this.setAttribute(`value`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
get max() {
|
||||||
|
try {
|
||||||
|
return parseInt(this.getAttribute(`max`));
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Failed to parse attribute: "max" - Make sure it's an integer`);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
set max(value) {
|
||||||
|
this.setAttribute(`max`, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
get type() {
|
||||||
|
return `number`;
|
||||||
|
};
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
// Attribute validation
|
||||||
|
if (!this.hasAttribute(`max`)) {
|
||||||
|
throw new Error(`dotdungeon | Cannot have a range without a maximum value`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard accessible input for the thing
|
||||||
|
this.#input = document.createElement(`input`);
|
||||||
|
this.#input.type = `number`;
|
||||||
|
this.#input.min = 0;
|
||||||
|
this.#input.max = this.max;
|
||||||
|
this.#input.value = this.value;
|
||||||
|
this.#input.addEventListener(`change`, () => {
|
||||||
|
const inputValue = parseInt(this.#input.value);
|
||||||
|
if (inputValue === this.value) { return };
|
||||||
|
this._updateValue.bind(this)(Math.sign(this.value - inputValue));
|
||||||
|
this._updateValue(Math.sign(this.value - inputValue));
|
||||||
|
});
|
||||||
|
this._shadow.appendChild(this.#input);
|
||||||
|
|
||||||
|
// Shadow-DOM construction
|
||||||
|
this._elements = new Array(this.max);
|
||||||
|
const container = document.createElement(`div`);
|
||||||
|
container.classList.add(`container`);
|
||||||
|
|
||||||
|
// Creating the node for filled content
|
||||||
|
const filledContainer = document.createElement(`div`);
|
||||||
|
filledContainer.classList.add(`range-increment`, `filled`);
|
||||||
|
const filledNode = this.querySelector(`[slot="filled"]`);
|
||||||
|
if (filledNode) { filledContainer.appendChild(filledNode) };
|
||||||
|
|
||||||
|
const emptyContainer = document.createElement(`div`);
|
||||||
|
emptyContainer.classList.add(`range-increment`, `empty`);
|
||||||
|
const emptyNode = this.querySelector(`[slot="empty"]`);
|
||||||
|
if (emptyNode) { emptyContainer.appendChild(emptyNode) };
|
||||||
|
|
||||||
|
this._elements.fill(filledContainer, 0, this.value);
|
||||||
|
this._elements.fill(emptyContainer, this.value);
|
||||||
|
container.append(...this._elements.map((slot, i) => {
|
||||||
|
const node = slot.cloneNode(true);
|
||||||
|
node.setAttribute(`data-index`, i + 1);
|
||||||
|
node.addEventListener(`click`, () => {
|
||||||
|
const filled = node.classList.contains(`filled`);
|
||||||
|
this._updateValue(filled ? -1 : 1);
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}));
|
||||||
|
this._shadow.appendChild(container);
|
||||||
|
|
||||||
|
/*
|
||||||
|
This converts all of the namespace prefixed properties on the element to
|
||||||
|
CSS variables so that they don't all need to be provided by doing style=""
|
||||||
|
*/
|
||||||
|
for (const attrVar of this.attributes) {
|
||||||
|
if (attrVar.name?.startsWith(`var:`)) {
|
||||||
|
const prop = attrVar.name.replace(`var:`, ``);
|
||||||
|
this.style.setProperty(`--` + prop, attrVar.value);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
_updateValue(delta) {
|
||||||
|
this.value += delta;
|
||||||
|
this.dispatchEvent(new Event(`change`, { bubbles: true }));
|
||||||
|
};
|
||||||
|
};
|
||||||
3
src/consts.mjs
Normal file
3
src/consts.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const FEATURE_FLAGS = Object.freeze({
|
||||||
|
ROLLMODECONTENT: `Roll Mode Message Content`,
|
||||||
|
});
|
||||||
11
src/documents/ActiveEffect/_proxy.mjs
Normal file
11
src/documents/ActiveEffect/_proxy.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of Foundry-types to in-code Document classes.
|
||||||
|
*/
|
||||||
|
const classes = {};
|
||||||
|
|
||||||
|
/** The class that will be used if no type-specific class is defined */
|
||||||
|
const defaultClass = ActiveEffect;
|
||||||
|
|
||||||
|
export const ActiveEffectProxy = createDocumentProxy(defaultClass, classes);
|
||||||
5
src/documents/Actor/Player/Document.mjs
Normal file
5
src/documents/Actor/Player/Document.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export class Player extends Actor {
|
||||||
|
getRollData() {
|
||||||
|
return this.system;
|
||||||
|
};
|
||||||
|
};
|
||||||
12
src/documents/Actor/Player/Model.mjs
Normal file
12
src/documents/Actor/Player/Model.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export class PlayerData extends foundry.abstract.TypeDataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
return {
|
||||||
|
content: new fields.HTMLField({
|
||||||
|
blank: true,
|
||||||
|
trim: true,
|
||||||
|
initial: ``,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
11
src/documents/Actor/_proxy.mjs
Normal file
11
src/documents/Actor/_proxy.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of Foundry-types to in-code Document classes.
|
||||||
|
*/
|
||||||
|
const classes = {};
|
||||||
|
|
||||||
|
/** The class that will be used if no type-specific class is defined */
|
||||||
|
const defaultClass = Actor;
|
||||||
|
|
||||||
|
export const ActorProxy = createDocumentProxy(defaultClass, classes);
|
||||||
11
src/documents/ChatMessage/_proxy.mjs
Normal file
11
src/documents/ChatMessage/_proxy.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of Foundry-types to in-code Document classes.
|
||||||
|
*/
|
||||||
|
const classes = {};
|
||||||
|
|
||||||
|
/** The class that will be used if no type-specific class is defined */
|
||||||
|
const defaultClass = ChatMessage;
|
||||||
|
|
||||||
|
export const ChatMessageProxy = createDocumentProxy(defaultClass, classes);
|
||||||
11
src/documents/Item/_proxy.mjs
Normal file
11
src/documents/Item/_proxy.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createDocumentProxy } from "../../utils/createDocumentProxy.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of Foundry-types to in-code Document classes.
|
||||||
|
*/
|
||||||
|
const classes = {};
|
||||||
|
|
||||||
|
/** The class that will be used if no type-specific class is defined */
|
||||||
|
const defaultClass = Item;
|
||||||
|
|
||||||
|
export const ItemProxy = createDocumentProxy(defaultClass, classes);
|
||||||
18
src/helpers/_index.mjs
Normal file
18
src/helpers/_index.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { handlebarsLocalizer, localizer } from "../utils/localizer.mjs";
|
||||||
|
import { options } from "./options.mjs";
|
||||||
|
|
||||||
|
export function registerHandlebarsHelpers() {
|
||||||
|
const helperPrefix = game.system.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// MARK: Complex helpers
|
||||||
|
[`${helperPrefix}-i18n`]: handlebarsLocalizer,
|
||||||
|
[`${helperPrefix}-options`]: options,
|
||||||
|
|
||||||
|
// MARK: Simple helpers
|
||||||
|
[`${helperPrefix}-stringify`]: v => JSON.stringify(v, null, ` `),
|
||||||
|
[`${helperPrefix}-empty`]: v => v.length == 0,
|
||||||
|
[`${helperPrefix}-set-has`]: (s, k) => s.has(k),
|
||||||
|
[`${helperPrefix}-empty-state`]: (v) => v ?? localizer(`${game.system.id}.common.empty`),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { localizer } from "../utils/localizer.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} Option
|
* @typedef {object} Option
|
||||||
* @property {string} [label]
|
* @property {string} [label]
|
||||||
|
|
@ -6,9 +8,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string | number} selected The selected value
|
* @param {string | number} selected
|
||||||
* @param {Array<Option | string>} opts The options that are valid
|
* @param {Array<Option | string>} opts
|
||||||
* @param {any} meta The Handlebars meta processing
|
|
||||||
*/
|
*/
|
||||||
export function options(selected, opts, meta) {
|
export function options(selected, opts, meta) {
|
||||||
const { localize = false } = meta.hash;
|
const { localize = false } = meta.hash;
|
||||||
|
|
@ -16,7 +17,7 @@ export function options(selected, opts, meta) {
|
||||||
const htmlOptions = [];
|
const htmlOptions = [];
|
||||||
|
|
||||||
for (let opt of opts) {
|
for (let opt of opts) {
|
||||||
if (typeof opt === `string`) {
|
if (foundry.utils.getType(opt) === `string`) {
|
||||||
opt = { label: opt, value: opt };
|
opt = { label: opt, value: opt };
|
||||||
};
|
};
|
||||||
opt.value = Handlebars.escapeExpression(opt.value);
|
opt.value = Handlebars.escapeExpression(opt.value);
|
||||||
|
|
@ -26,9 +27,9 @@ export function options(selected, opts, meta) {
|
||||||
${selected === opt.value ? `selected` : ``}
|
${selected === opt.value ? `selected` : ``}
|
||||||
${opt.disabled ? `disabled` : ``}
|
${opt.disabled ? `disabled` : ``}
|
||||||
>
|
>
|
||||||
${localize ? game.i18n.format(opt.label) : opt.label}
|
${localize ? localizer(opt.label) : opt.label}
|
||||||
</option>`,
|
</option>`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
return new Handlebars.SafeString(htmlOptions.join(`\n`));
|
return htmlOptions.join(`\n`);
|
||||||
};
|
};
|
||||||
18
src/hooks/hotReload.mjs
Normal file
18
src/hooks/hotReload.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
const loaders = {
|
||||||
|
svg(data) {
|
||||||
|
const iconName = data.path.split(`/`).slice(-1)[0].slice(0, -4);
|
||||||
|
Logger.debug(`hot-reloading icon: ${iconName}`);
|
||||||
|
Hooks.call(`${game.system.id}-hmr:svg`, iconName, data);
|
||||||
|
},
|
||||||
|
js() {window.location.reload()},
|
||||||
|
mjs() {window.location.reload()},
|
||||||
|
css(data) {
|
||||||
|
Logger.debug(`Hot-reloading CSS: ${data.path}`);
|
||||||
|
Hooks.call(`${game.system.id}-hmr:css`, data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Hooks.on(`hotReload`, async (data) => {
|
||||||
|
if (!loaders[data.extension]) {return}
|
||||||
|
return loaders[data.extension](data);
|
||||||
|
});
|
||||||
21
src/hooks/renderChatMessage.mjs
Normal file
21
src/hooks/renderChatMessage.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { FEATURE_FLAGS } from "../consts.mjs";
|
||||||
|
|
||||||
|
Hooks.on(`renderChatMessage`, (msg, html) => {
|
||||||
|
|
||||||
|
// Short-Circuit when the flag isn't set for the message
|
||||||
|
if (msg.getFlag(`taf`, `rollModedContent`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureFlags = game.settings.get(game.system.id, `flags`);
|
||||||
|
const featureFlagEnabled = featureFlags.includes(FEATURE_FLAGS.ROLLMODECONTENT);
|
||||||
|
|
||||||
|
const contentElement = html.find(`.message-content`)[0];
|
||||||
|
let content = contentElement.innerHTML;
|
||||||
|
if (featureFlagEnabled && msg.blind && !game.user.isGM) {
|
||||||
|
content = content.replace(/-=.*?=-/gm, `???`);
|
||||||
|
} else {
|
||||||
|
content = content.replace(/-=|=-/gm, ``);
|
||||||
|
}
|
||||||
|
contentElement.innerHTML = content;
|
||||||
|
});
|
||||||
59
src/main.mjs
Normal file
59
src/main.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// Document Imports
|
||||||
|
import { ActiveEffectProxy } from "./documents/ActiveEffect/_proxy.mjs";
|
||||||
|
import { ActorProxy } from "./documents/Actor/_proxy.mjs";
|
||||||
|
import { ChatMessageProxy } from "./documents/ChatMessage/_proxy.mjs";
|
||||||
|
import { ItemProxy } from "./documents/Item/_proxy.mjs";
|
||||||
|
|
||||||
|
// DataModel Imports
|
||||||
|
import { PlayerData } from "./documents/Actor/Player/Model.mjs";
|
||||||
|
|
||||||
|
// Hook Imports
|
||||||
|
import "./hooks/renderChatMessage.mjs";
|
||||||
|
import "./hooks/hotReload.mjs";
|
||||||
|
|
||||||
|
// Misc Imports
|
||||||
|
import "./utils/globalTaf.mjs";
|
||||||
|
import "./utils/logger.mjs";
|
||||||
|
import "./utils/DialogManager.mjs";
|
||||||
|
import { registerCustomComponents } from "./components/_index.mjs";
|
||||||
|
import { registerHandlebarsHelpers } from "./helpers/_index.mjs";
|
||||||
|
import { registerSettings } from "./settings/_index.mjs";
|
||||||
|
import { registerSheets } from "./sheets/_index.mjs";
|
||||||
|
|
||||||
|
// MARK: init hook
|
||||||
|
Hooks.once(`init`, () => {
|
||||||
|
Logger.info(`Initializing`);
|
||||||
|
CONFIG.ActiveEffect.legacyTransferral = false;
|
||||||
|
|
||||||
|
registerSettings();
|
||||||
|
|
||||||
|
// Data Models
|
||||||
|
CONFIG.Actor.dataModels.player = PlayerData;
|
||||||
|
|
||||||
|
// Update document classes
|
||||||
|
CONFIG.Actor.documentClass = ActorProxy;
|
||||||
|
CONFIG.Item.documentClass = ItemProxy;
|
||||||
|
CONFIG.ActiveEffect.documentClass = ActiveEffectProxy;
|
||||||
|
CONFIG.ChatMessage.documentClass = ChatMessageProxy;
|
||||||
|
registerSheets();
|
||||||
|
|
||||||
|
registerHandlebarsHelpers();
|
||||||
|
|
||||||
|
registerCustomComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: ready hook
|
||||||
|
Hooks.once( `ready`, () => {
|
||||||
|
Logger.info(`Ready`);
|
||||||
|
|
||||||
|
let defaultTab = game.settings.get(game.system.id, `defaultTab`);
|
||||||
|
if (defaultTab) {
|
||||||
|
if (!ui.sidebar?.tabs?.[defaultTab]) {
|
||||||
|
Logger.error(`Couldn't find a sidebar tab with ID:`, defaultTab);
|
||||||
|
} else {
|
||||||
|
Logger.debug(`Switching sidebar tab to:`, defaultTab);
|
||||||
|
ui.sidebar.tabs[defaultTab].activate();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
10
src/settings/_index.mjs
Normal file
10
src/settings/_index.mjs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { registerClientSettings } from "./client_settings.mjs";
|
||||||
|
import { registerDevSettings } from "./dev_settings.mjs";
|
||||||
|
import { registerWorldSettings } from "./world_settings.mjs";
|
||||||
|
|
||||||
|
export function registerSettings() {
|
||||||
|
Logger.debug(`Registering settings`);
|
||||||
|
registerClientSettings();
|
||||||
|
registerWorldSettings();
|
||||||
|
registerDevSettings();
|
||||||
|
};
|
||||||
2
src/settings/client_settings.mjs
Normal file
2
src/settings/client_settings.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export function registerClientSettings() {
|
||||||
|
};
|
||||||
16
src/settings/dev_settings.mjs
Normal file
16
src/settings/dev_settings.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export function registerDevSettings() {
|
||||||
|
game.settings.register(game.system.id, `devMode`, {
|
||||||
|
scope: `client`,
|
||||||
|
type: Boolean,
|
||||||
|
config: false,
|
||||||
|
default: false,
|
||||||
|
requiresReload: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.register(game.system.id, `defaultTab`, {
|
||||||
|
scope: `client`,
|
||||||
|
type: String,
|
||||||
|
config: false,
|
||||||
|
requiresReload: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
24
src/settings/world_settings.mjs
Normal file
24
src/settings/world_settings.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { FEATURE_FLAGS } from "../consts.mjs";
|
||||||
|
|
||||||
|
export function registerWorldSettings() {
|
||||||
|
game.settings.register(game.system.id, `flags`, {
|
||||||
|
name: `Feature Flags`,
|
||||||
|
hint: `World-based feature flags that are used to enable/disable specific behaviours`,
|
||||||
|
scope: `world`,
|
||||||
|
type: new foundry.data.fields.SetField(
|
||||||
|
new foundry.data.fields.StringField(
|
||||||
|
{
|
||||||
|
empty: false,
|
||||||
|
trim: true,
|
||||||
|
options: Object.values(FEATURE_FLAGS),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
initial: new Set(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
config: true,
|
||||||
|
requiresReload: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
26
src/sheets/Player/v1.mjs
Normal file
26
src/sheets/Player/v1.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
export class PlayerSheetv1 extends ActorSheet {
|
||||||
|
static get defaultOptions() {
|
||||||
|
let opts = foundry.utils.mergeObject(
|
||||||
|
super.defaultOptions,
|
||||||
|
{
|
||||||
|
template: `systems/${game.system.id}/templates/Player/v1/main.hbs`,
|
||||||
|
classes: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
opts.classes = [`actor--player`, `style-v1`];
|
||||||
|
return opts;
|
||||||
|
};
|
||||||
|
|
||||||
|
async getData() {
|
||||||
|
const ctx = {};
|
||||||
|
|
||||||
|
ctx.editable = this.isEditable;
|
||||||
|
|
||||||
|
const actor = ctx.actor = this.actor;
|
||||||
|
ctx.system = actor.system;
|
||||||
|
ctx.enriched = { system: {} };
|
||||||
|
ctx.enriched.system.content = await TextEditor.enrichHTML(actor.system.content);
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
}
|
||||||
11
src/sheets/_index.mjs
Normal file
11
src/sheets/_index.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { PlayerSheetv1 } from "./Player/v1.mjs";
|
||||||
|
|
||||||
|
export function registerSheets() {
|
||||||
|
Logger.debug(`Registering sheets`);
|
||||||
|
|
||||||
|
Actors.registerSheet(game.system.id, PlayerSheetv1, {
|
||||||
|
makeDefault: true,
|
||||||
|
types: [`player`],
|
||||||
|
label: `Hello`,
|
||||||
|
});
|
||||||
|
};
|
||||||
178
src/utils/DialogManager.mjs
Normal file
178
src/utils/DialogManager.mjs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { localizer } from "./localizer.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class that allows managing Dialogs that are created for various
|
||||||
|
* purposes such as deleting items, help popups, etc. This is a singleton class
|
||||||
|
* that upon instantiating after the first time will just return the first instance
|
||||||
|
*/
|
||||||
|
export class DialogManager {
|
||||||
|
|
||||||
|
/** @type {Map<string, Dialog>} */
|
||||||
|
static #dialogs = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses a dialog if it already exists, or creates a new one and renders it.
|
||||||
|
*
|
||||||
|
* @param {string} dialogId The ID to associate with the dialog, should be unique
|
||||||
|
* @param {object} data The data to pass to the Dialog constructor
|
||||||
|
* @param {DialogOptions} opts The options to pass to the Dialog constructor
|
||||||
|
* @returns {Dialog} The Dialog instance
|
||||||
|
*/
|
||||||
|
static async createOrFocus(dialogId, data, opts = {}) {
|
||||||
|
if (DialogManager.#dialogs.has(dialogId)) {
|
||||||
|
const dialog = DialogManager.#dialogs.get(dialogId);
|
||||||
|
dialog.bringToTop();
|
||||||
|
return dialog;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
This makes sure that if I provide a close function as a part of the data,
|
||||||
|
that the dialog still gets removed from the set once it's closed, otherwise
|
||||||
|
it could lead to dangling references that I don't care to keep. Or if I don't
|
||||||
|
provide the close function, it just sets the function as there isn't anything
|
||||||
|
extra that's needed to be called.
|
||||||
|
*/
|
||||||
|
if (data?.close) {
|
||||||
|
const provided = data.close;
|
||||||
|
data.close = () => {
|
||||||
|
DialogManager.#dialogs.delete(dialogId);
|
||||||
|
provided();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data.close = () => DialogManager.#dialogs.delete(dialogId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the Dialog with the modified data
|
||||||
|
const dialog = new Dialog(data, opts);
|
||||||
|
DialogManager.#dialogs.set(dialogId, dialog);
|
||||||
|
dialog.render(true);
|
||||||
|
return dialog;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes a dialog if it is rendered
|
||||||
|
*
|
||||||
|
* @param {string} dialogId The ID of the dialog to close
|
||||||
|
*/
|
||||||
|
static async close(dialogId) {
|
||||||
|
const dialog = DialogManager.#dialogs.get(dialogId);
|
||||||
|
dialog?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
static async helpDialog(
|
||||||
|
helpId,
|
||||||
|
helpContent,
|
||||||
|
helpTitle = `dotdungeon.common.help`,
|
||||||
|
localizationData = {},
|
||||||
|
) {
|
||||||
|
DialogManager.createOrFocus(
|
||||||
|
helpId,
|
||||||
|
{
|
||||||
|
title: localizer(helpTitle, localizationData),
|
||||||
|
content: localizer(helpContent, localizationData),
|
||||||
|
buttons: {},
|
||||||
|
},
|
||||||
|
{ resizable: true },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks the user to provide a simple piece of information, this is primarily
|
||||||
|
* intended to be used within macros so that it can have better info gathering
|
||||||
|
* 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
|
||||||
|
* without an object wrapper, allowing for easier access.
|
||||||
|
*/
|
||||||
|
static async ask(data, opts = {}) {
|
||||||
|
if (!data.id) {
|
||||||
|
throw new Error(`Asking the user for input must contain an ID`);
|
||||||
|
}
|
||||||
|
if (!data.inputs.length) {
|
||||||
|
throw new Error(`Must include at least one input specification when prompting the user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let autofocusClaimed = false;
|
||||||
|
for (const i of data.inputs) {
|
||||||
|
i.id ??= foundry.utils.randomID(16);
|
||||||
|
i.inputType ??= `text`;
|
||||||
|
|
||||||
|
// Only ever allow one input to claim autofocus
|
||||||
|
i.autofocus &&= !autofocusClaimed;
|
||||||
|
autofocusClaimed ||= i.autofocus;
|
||||||
|
|
||||||
|
// Set the value's attribute name if it isn't specified explicitly
|
||||||
|
if (!i.valueAttribute) {
|
||||||
|
switch (i.inputType) {
|
||||||
|
case `checkbox`:
|
||||||
|
i.valueAttribute = `checked`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
i.valueAttribute = `value`;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
opts.jQuery = true;
|
||||||
|
data.default ??= `confirm`;
|
||||||
|
data.title ??= `System Question`;
|
||||||
|
|
||||||
|
data.content = await renderTemplate(
|
||||||
|
`systems/${game.system.id}/templates/Dialogs/ask.hbs`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
DialogManager.createOrFocus(
|
||||||
|
data.id,
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
label: `Confirm`,
|
||||||
|
callback: (html) => {
|
||||||
|
const answers = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Retrieve the answer for every input provided using the ID
|
||||||
|
determined during initial data prep, and assign the value
|
||||||
|
to the property of the label in the object.
|
||||||
|
*/
|
||||||
|
for (const i of data.inputs) {
|
||||||
|
const element = html.find(`#${i.id}`)[0];
|
||||||
|
let value = element.value;
|
||||||
|
switch (i.inputType) {
|
||||||
|
case `number`:
|
||||||
|
value = parseFloat(value);
|
||||||
|
break;
|
||||||
|
case `checkbox`:
|
||||||
|
value = element.checked;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Logger.debug(`Ask response: ${value} (type: ${typeof value})`);
|
||||||
|
answers[i.key ?? i.label] = value;
|
||||||
|
if (data.inputs.length === 1) {
|
||||||
|
resolve(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(answers);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: `Cancel`,
|
||||||
|
callback: () => reject(`User cancelled the prompt`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
static get size() {
|
||||||
|
return DialogManager.#dialogs.size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.DialogManager = DialogManager;
|
||||||
39
src/utils/createDocumentProxy.mjs
Normal file
39
src/utils/createDocumentProxy.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
export function createDocumentProxy(defaultClass, classes) {
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
return new Proxy(function () {}, {
|
||||||
|
construct(_target, args) {
|
||||||
|
const [data] = args;
|
||||||
|
|
||||||
|
if (!classes[data.type]) {
|
||||||
|
return new defaultClass(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new classes[data.type](...args);
|
||||||
|
},
|
||||||
|
get(_target, prop, _receiver) {
|
||||||
|
|
||||||
|
if ([`create`, `createDocuments`].includes(prop)) {
|
||||||
|
return (data, options) => {
|
||||||
|
if (data.constructor === Array) {
|
||||||
|
return data.map(i => this.constructor.create(i, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!classes[data.type]) {
|
||||||
|
return defaultClass.create(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes[data.type].create(data, options);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (prop == Symbol.hasInstance) {
|
||||||
|
return (instance) => {
|
||||||
|
if (instance instanceof defaultClass) {return true}
|
||||||
|
return Object.values(classes).some(i => instance instanceof i);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return defaultClass[prop];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
10
src/utils/feature_flags/rollModeMessageContent.mjs
Normal file
10
src/utils/feature_flags/rollModeMessageContent.mjs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { FEATURE_FLAGS } from "../../consts.mjs";
|
||||||
|
|
||||||
|
export function hideMessageText(content) {
|
||||||
|
const featureFlags = game.settings.get(game.system.id, `flags`);
|
||||||
|
const hideContent = featureFlags.includes(FEATURE_FLAGS.ROLLMODECONTENT);
|
||||||
|
if (hideContent) {
|
||||||
|
return `-=${content}=-`;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
};
|
||||||
11
src/utils/globalTaf.mjs
Normal file
11
src/utils/globalTaf.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { FEATURE_FLAGS } from "../consts.mjs";
|
||||||
|
import { hideMessageText } from "./feature_flags/rollModeMessageContent.mjs";
|
||||||
|
|
||||||
|
globalThis.taf = Object.freeze({
|
||||||
|
utils: {
|
||||||
|
hideMessageText,
|
||||||
|
},
|
||||||
|
const: {
|
||||||
|
FEATURE_FLAGS,
|
||||||
|
},
|
||||||
|
});
|
||||||
45
src/utils/localizer.mjs
Normal file
45
src/utils/localizer.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/** A handlebars helper that utilizes the recursive localizer */
|
||||||
|
export function handlebarsLocalizer(key, ...args) {
|
||||||
|
let data = args[0];
|
||||||
|
if (args.length === 1) { data = args[0].hash }
|
||||||
|
if (key instanceof Handlebars.SafeString) {key = key.toString()}
|
||||||
|
const localized = localizer(key, data);
|
||||||
|
return localized;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A localizer that allows recursively localizing strings so that localized strings
|
||||||
|
* that want to use other localized strings can.
|
||||||
|
*
|
||||||
|
* @param {string} key The localization key to retrieve
|
||||||
|
* @param {object?} args The arguments provided to the localizer for replacement
|
||||||
|
* @param {number?} depth The current depth of the localizer
|
||||||
|
* @returns The localized string
|
||||||
|
*/
|
||||||
|
export function localizer(key, args = {}, depth = 0) {
|
||||||
|
/** @type {string} */
|
||||||
|
let localized = game.i18n.format(key, args);
|
||||||
|
const subkeys = localized.matchAll(/@(?<key>[a-zA-Z.]+)/gm);
|
||||||
|
|
||||||
|
// Short-cut to help prevent infinite recursion
|
||||||
|
if (depth > 10) {
|
||||||
|
return localized;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Helps prevent localization 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(
|
||||||
|
/@(?<key>[a-zA-Z.]+)/gm,
|
||||||
|
(_fullMatch, subkey) => {
|
||||||
|
return localizedSubkeys.get(subkey);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -12,10 +12,10 @@ const augmentedProps = new Set([
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** @type {Console} */
|
/** @type {Console} */
|
||||||
export const Logger = new Proxy(console, {
|
globalThis.Logger = new Proxy(console, {
|
||||||
get(target, prop, _receiver) {
|
get(target, prop, _receiver) {
|
||||||
if (augmentedProps.has(prop)) {
|
if (augmentedProps.has(prop)) {
|
||||||
return target[prop].bind(target, game.system.id, `|`);
|
return (...args) => target[prop](game.system.id, `|`, ...args);
|
||||||
};
|
};
|
||||||
return target[prop];
|
return target[prop];
|
||||||
},
|
},
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
.taf.Ask {
|
|
||||||
min-width: 330px;
|
|
||||||
|
|
||||||
.prompt {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 2fr;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-content {
|
|
||||||
gap: 1rem;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
color: var(--color-form-label);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
text-indent: 1em;
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
padding: 6px 8px;
|
|
||||||
box-shadow: 0 0 10px var(--color-shadow-dark);
|
|
||||||
color: var(--color-text-light-1);
|
|
||||||
border-radius: 5px;
|
|
||||||
text-align: center;
|
|
||||||
background: var(--color-level-error-bg);
|
|
||||||
border: 1px solid var(--color-level-error);
|
|
||||||
text-indent: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
align-self: center;
|
|
||||||
justify-self: right;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
.taf.AttributeManager {
|
|
||||||
.attributes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attribute {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: min-content 1fr repeat(3, auto);
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid rebeccapurple;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&.vertical {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Used to style the actual element as dragging */
|
|
||||||
&:has(taf-icon:active) {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
taf-icon {
|
|
||||||
cursor: grab;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
.taf.PlayerSheet {
|
|
||||||
.sheet-header, fieldset, .content {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid rebeccapurple;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sheet-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 4px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-around;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attr-range {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
width: 100px;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
> input {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
--table-row-color-odd: var(--table-header-bg-color);
|
|
||||||
|
|
||||||
&:not(:has(> prose-mirror)) {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prose-mirror {
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
menu {
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
.taf.sheet-config {
|
|
||||||
|
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.tab.active {
|
|
||||||
display: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
.taf {
|
|
||||||
> .window-content {
|
|
||||||
padding: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
:host {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: var(--size, 1rem);
|
|
||||||
height: var(--size, 1rem);
|
|
||||||
fill: var(--fill);
|
|
||||||
}
|
|
||||||
|
|
||||||
path {
|
|
||||||
stroke: var(--stroke);
|
|
||||||
stroke-width: var(--stroke-width);
|
|
||||||
stroke-linejoin: var(--stroke-linejoin);
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
:host {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--fill);
|
|
||||||
stroke: var(--stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
path {
|
|
||||||
stroke: var(--stroke);
|
|
||||||
stroke-width: var(--stroke-width);
|
|
||||||
stroke-linejoin: var(--stroke-linejoin);
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
.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,5 +0,0 @@
|
||||||
.taf > .window-content {
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
.taf > .window-content hr {
|
|
||||||
height: 1px;
|
|
||||||
background: rebeccapurple;
|
|
||||||
border-radius: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
.taf > .window-content input {
|
|
||||||
&.large {
|
|
||||||
--input-height: 2.5rem;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[type="checkbox"] {
|
|
||||||
--checkbox-checked-color: var(--color-warm-1);
|
|
||||||
width: var(--checkbox-size);
|
|
||||||
height: var(--checkbox-size);
|
|
||||||
background: var(--input-background-color);
|
|
||||||
border: 2px solid var(--color-cool-3);
|
|
||||||
position: relative;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&::before, &::after {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid var(--checkbox-checked-color);
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked::after {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
inset: 4px;
|
|
||||||
z-index: 1;
|
|
||||||
content: "";
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--checkbox-checked-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
content: "";
|
|
||||||
background: var(--color-level-error-bg);
|
|
||||||
border-radius: 2px;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
.taf > .window-content p {
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
.taf > .window-content prose-mirror {
|
|
||||||
background: var(--prosemirror-background);
|
|
||||||
gap: 0;
|
|
||||||
|
|
||||||
.editor-content {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableWrapper th,
|
|
||||||
.tableWrapper td {
|
|
||||||
border-color: rebeccapurple;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
.taf > .window-content {
|
|
||||||
.grow { flex-grow: 1; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
@layer resets, themes, elements, components, partials, apps, exceptions;
|
|
||||||
|
|
||||||
/* Resets */
|
|
||||||
@import url("./resets/hr.css") layer(resets);
|
|
||||||
@import url("./resets/inputs.css") layer(resets);
|
|
||||||
@import url("./resets/button.css") layer(resets);
|
|
||||||
|
|
||||||
/* Themes */
|
|
||||||
@import url("./themes/dark.css") layer(themes);
|
|
||||||
@import url("./themes/light.css") layer(themes);
|
|
||||||
|
|
||||||
/* 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/hr.css") layer(elements);
|
|
||||||
@import url("./elements/input.css") layer(elements);
|
|
||||||
@import url("./elements/p.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 */
|
|
||||||
@import url("./Apps/common.css") layer(apps);
|
|
||||||
@import url("./Apps/Ask.css") layer(apps);
|
|
||||||
@import url("./Apps/AttributeManager.css") layer(apps);
|
|
||||||
@import url("./Apps/PlayerSheet.css") layer(apps);
|
|
||||||
@import url("./Apps/QueryStatus.css") layer(apps);
|
|
||||||
@import url("./Apps/TAFDocumentSheetConfig.css") layer(apps);
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
.taf > .window-content button {
|
|
||||||
height: initial;
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
.taf > .window-content hr {
|
|
||||||
all: initial;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue