Compare commits

..

156 commits
v2.1.0 ... main

Author SHA1 Message Date
703dc83681 Merge pull request 'Make the DialogManager more ESM-y' (#15) from chore/esmify-DialogManager into main
Reviewed-on: #15
2025-11-22 03:56:58 +00:00
41034854eb Make the DialogManager more ESM-y 2025-11-21 19:56:07 -07:00
382ca50bb5 Merge pull request 'Data Request API helper' (#10) from feat/data-requests into main
Reviewed-on: #10
2025-11-22 02:51:15 +00:00
e99016e433 Add a title to the QueryStatus app 2025-11-21 19:50:36 -07:00
361a2004d8 Update loading spinner to use CSS variables depending on theme 2025-11-21 19:49:02 -07:00
44977c95cc Make it so requesting data from offline users shows disconnected initially 2025-11-21 19:48:43 -07:00
c6f14b3c21 Remove unused status value 2025-11-21 19:30:31 -07:00
022b6c5b31 Remove / tweak error messages 2025-11-21 19:30:01 -07:00
cb334f41de Add safety check to ensure the response we got was for one of our requests. 2025-11-21 19:28:33 -07:00
cab29d9cd6 Add localization for a bunch of stuff that I initially missed 2025-11-21 19:25:58 -07:00
6997c736dc Implement the localizer helper API 2025-11-21 19:23:58 -07:00
b428eb3bf6 Finish styling the QueryStatus application 2025-11-20 23:37:46 -07:00
c014e17da2 Implement the request cancellation 2025-11-20 22:43:14 -07:00
9ea417ddc1 Add region comments 2025-11-20 22:42:56 -07:00
804c4b3984 Disable the eslint brace-style rule because sometimes inconsistency is better than consistency 2025-11-20 22:42:42 -07:00
c113c326c6 Move the query event handlers into a subfolder and name them in a consistent way 2025-11-20 22:14:43 -07:00
6a2cc1170d Add actions for finishing early and cancelling the request entirely 2025-11-20 17:12:55 -07:00
d60448640f Add methods to handle the finishing and cancellation of requests to the API 2025-11-20 17:12:03 -07:00
860c8b619a Make requestID readonly on the application 2025-11-20 17:11:31 -07:00
df0c69c731 Update the way the QueryManager exports are structured to be more esm-y rather than Java-y 2025-11-19 21:18:42 -07:00
bb095a9b4e Get user re-querying working when they disconnect and improve the user status 2025-11-19 21:02:49 -07:00
1bf6cbbd45 Remove github workflow entirely 2025-11-19 19:36:42 -07:00
c6598ac5fa Add the unlisted releases to the README for manual installation 2025-11-19 19:19:38 -07:00
fd10ba402d Remove github step since we aren't releasing to Github at all 2025-11-18 23:54:23 -07:00
f500152ba7 Remove pnpm lock 2025-11-18 23:48:46 -07:00
bf579a3451 Give the CDN URL to the proper step of the action 2025-11-18 23:29:45 -07:00
5c1985c4ab Re-add the error printing 2025-11-18 23:28:02 -07:00
213996ab0a Correct error with CDN URL creation 2025-11-18 23:25:41 -07:00
53f35562ad Update error handling 2025-11-18 23:22:01 -07:00
d3f9c4c376 Actually commit the correct file this time 2025-11-18 23:18:39 -07:00
9e3bc775b4 Ensure the uploadScript gets the environment variables it requires and clean up asset adding error 2025-11-18 23:16:56 -07:00
7245e89c62 Update package.json for the AWS dependency I forgot to include 2025-11-18 23:06:28 -07:00
a4ae2aefca Remove the Github release shenanigans in favour of uploading to s3 2025-11-18 23:03:01 -07:00
6e5422e08b Try using the stream as the request payload 2025-11-18 17:06:11 -07:00
cd3b5998dd Apparently Github has a different URI for asset uploads 2025-11-18 01:12:09 -07:00
871c820f94 Make the response conform to Github's specification 2025-11-18 01:05:12 -07:00
2ddcda676e Begin work on testing the Github release portion of the action 2025-11-18 00:58:50 -07:00
45de56650a Add missing semicolon 2025-11-17 22:08:37 -07:00
d018aea4f1 Add some informational logs to the manifest preparation 2025-11-17 22:08:14 -07:00
d7db9cb2df Add success log 2025-11-17 22:06:25 -07:00
41d3541c4b Try providing the serialization as an attachment name 2025-11-17 21:59:50 -07:00
5e0028cdd6 Try providing the Content-Type for Axios 2025-11-17 21:52:19 -07:00
a63f0e02d9 Move uploading into a helper function and improve error handling 2025-11-17 21:43:57 -07:00
bb5e27af87 Set release name properly 2025-11-17 21:31:17 -07:00
ea57941472 Remove FormData shenanigans 2025-11-17 21:30:22 -07:00
39d122a882 Use set on the formData instead of append 2025-11-17 21:25:43 -07:00
aa7c231e58 Use a readStream instead of openAsBlob 2025-11-17 21:24:06 -07:00
a06934538e Try a different import method for openAsBlob 2025-11-17 20:58:39 -07:00
4b121c1f0f Remove uses when using a script 2025-11-16 14:49:09 -07:00
351300651b Add script for creating the forgejo release 2025-11-16 14:47:58 -07:00
031fdb4a40 Add token to see if that fixes the issue 2025-11-16 14:05:49 -07:00
760009c9ba Remove name from version extraction 2025-11-16 14:05:38 -07:00
07a55e9064 Add a log for visibility 2025-11-16 14:04:41 -07:00
a4355c608a Create release directory 2025-11-16 13:56:08 -07:00
4275909dc8 Try making the release step work 2025-11-16 13:54:39 -07:00
7841e04dfc Fix yaml syntax 2025-11-16 12:54:24 -07:00
2146d51fde Add an artifact for checking the end result 2025-11-16 12:53:12 -07:00
bb616dbec2 Correct URL template 2025-11-16 12:43:28 -07:00
92ad2607cd Add the scripts and dependency install to the release jobs 2025-11-16 12:33:51 -07:00
72a612d8a9 Run the correct script 2025-11-16 12:28:20 -07:00
14e53455c6 Use the correct artifact download action 2025-11-16 12:25:33 -07:00
49784448e8 Pluralize outputs 2025-11-16 12:24:13 -07:00
eac6a02c04 Apparently only single quotes work as string delimiters 2025-11-16 12:18:44 -07:00
03b647cac1 Prevent boolean casting that breaks the checks 2025-11-16 12:07:45 -07:00
1b986da6d4 Update the forgejo variable reference to be correct 2025-11-16 12:00:12 -07:00
5c030c680d Add names and make the variable reference be correct 2025-11-16 11:57:07 -07:00
f1521992a2 Switch to using the act tag instead of Docker 2025-11-16 11:53:05 -07:00
cf89b53b3b Version bump 2025-11-16 11:25:19 -07:00
9b5b4bb9d1 Add rest of steps back to action 2025-11-16 11:22:33 -07:00
caf6dfa4a3 Remove logs and add error when the tag name is empty 2025-11-16 11:10:09 -07:00
6c150b9b0e Pluralize outputs 2025-11-16 11:08:28 -07:00
0179121c87 Try using a variable directly instead of environment variable 2025-11-16 11:05:42 -07:00
5b63688834 Try using a different output method 2025-11-16 11:01:08 -07:00
2eeeae9eef Try to figure out why the version is getting empty string 2025-11-16 10:49:22 -07:00
57f9c347ff Add functionality back in 2025-11-16 10:43:57 -07:00
9723ea8bdc Comment out functionality to try and figure out what's breaking 2025-11-16 10:39:34 -07:00
7cb5e49d6d Install node dependencies during build 2025-11-16 10:28:51 -07:00
786bd68c35 Update the URL pointers for the manifest creation 2025-11-16 10:27:05 -07:00
088b8c6f5d Remove Github URLs from the manifest 2025-11-16 10:12:26 -07:00
00692431cd Begin work on the manifest preparation step for actions 2025-11-16 02:09:35 -07:00
834f169a80 Tweak the npm scripts 2025-11-16 01:55:07 -07:00
9057cbd682 Add axios as a devDependency for the tagExists script 2025-11-16 01:49:22 -07:00
01e046f916 Remove unneeded file hiding 2025-11-16 01:48:58 -07:00
6df0780676 Finish implementing the tagExists action helper script 2025-11-16 01:48:22 -07:00
58893f46db Begin implementing a script to check if a tag exists via forgejo API for my release workflow 2025-11-16 01:01:26 -07:00
0362342419 Get the QueryStatus application displaying the status more appropriately 2025-11-12 00:09:52 -07:00
5770abb7e8 Add a disconnected icon for the purposes of the QueryStatus app 2025-11-12 00:08:49 -07:00
a242101b5b Begin working on the QueryStatus application for the requestor to monitor user responses 2025-11-09 00:31:04 -07:00
47b68621c1 Add CONST as a readonly global 2025-11-08 19:12:30 -07:00
e79bd4d505 Add the template and styles for the chat notification 2025-11-08 19:12:19 -07:00
db4f57fc90 Add the special case for prompting all but the requesting user 2025-11-08 19:11:52 -07:00
723bcf8735 Add notification and submission tracking for the QueryManager 2025-11-08 19:07:01 -07:00
bd301d69fb Implement the chat notification for the player 2025-11-08 19:06:19 -07:00
b6ab0a229a Rename the notification event name 2025-11-08 19:06:01 -07:00
Eldritch-Oliver
bfd408ef0b Begin implementing the socket event handlers 2025-11-08 00:40:48 -07:00
Eldritch-Oliver
36811b268c Add a QueryManager helper class 2025-11-08 00:40:33 -07:00
Eldritch-Oliver
ce6ac8a93b Add foundations for the data request sockets 2025-11-05 22:41:53 -07:00
Eldritch-Oliver
8632054e63 Switch to using pnpm 2025-10-15 16:40:53 -06:00
f7fee99b44
Merge pull request #48 from Eldritch-Oliver/feature/tabbed-doc-sheet-config
Custom Document Sheet Config
2025-10-05 11:59:21 -06:00
Eldritch-Oliver
a7f91babf7 Remove log 2025-10-05 11:58:58 -06:00
Eldritch-Oliver
696f9e8261 Remove context menu action that is deprecated 2025-10-04 19:42:27 -06:00
Eldritch-Oliver
72ebc0354d Remove old application that is no longer used by anything 2025-10-04 19:41:53 -06:00
Eldritch-Oliver
4e304f7d22 Override the default configureSheet action in order to open the custom DocumentSheetConfig 2025-10-04 19:41:24 -06:00
Eldritch-Oliver
6081b8f9e8 Add a custom DocumentSheetConfig that supports tab-based configuration for my system-specific stuff 2025-10-04 19:40:51 -06:00
Eldritch-Oliver
c7c0deaec7 Make the hotReload target the correct styles directory 2025-10-01 20:52:42 -06:00
Eldritch-Oliver
48e40538dc Restore intellisense for the module code 2025-10-01 00:38:17 -06:00
Eldritch-Oliver
6866bea131 Update scripts to allow auto-linking of Foundry source for intellisense 2025-09-28 00:34:49 -06:00
Eldritch-Oliver
65cc95c35c Begin working on a symlink script to make intellisense better 2025-09-27 11:48:59 -06:00
Eldritch-Oliver
cb266b3c1e Version bump for release 2025-09-17 19:23:34 -06:00
a50d0e8609
Merge pull request #42 from Eldritch-Oliver/feature/resize-controls
Add the ability to tell the system the starting width/height and whether or not a sheet should be resizable
2025-09-17 19:22:00 -06:00
Eldritch-Oliver
b683e8b5a0 Remove unneeded _onRender method 2025-09-17 19:20:49 -06:00
Eldritch-Oliver
baaafcccfc Make the styling of the save button more consistent 2025-09-17 19:13:22 -06:00
Eldritch-Oliver
c29fa3e017 Update the size settings to apply without using the constructor 2025-09-17 19:12:23 -06:00
Eldritch-Oliver
dff8a46ebb Make the size settings apply to the application when it is constructed 2025-09-16 00:45:48 -06:00
Eldritch-Oliver
c50e88e483 Add an application for controlling the size settings. 2025-09-16 00:45:08 -06:00
Eldritch-Oliver
a98af33477 Tweak indentation to be more consistent with other applications 2025-09-16 00:35:05 -06:00
Eldritch-Oliver
171a133563 Update manifest data 2025-09-15 21:57:09 -06:00
Eldritch-Oliver
ca267868a1 Update the draft release procedure to use a different text replacement method (closes #41) 2025-09-06 18:59:07 -06:00
Eldritch-Oliver
df9a63073a Ensure AttributeManager is a singleton per actor 2025-09-04 20:21:06 -06:00
Eldritch-Oliver
cd3f076b7d Improve handling of checkbox inputs within the DialogManager.ask 2025-09-04 19:37:18 -06:00
da57b12800
Merge pull request #40 from Eldritch-Oliver/feature/prevent-item-creation
Prevent item creation and hide the item tab
2025-09-04 19:36:05 -06:00
Eldritch-Oliver
92e7ec1c72 Prevent item creation and hide the item tab 2025-09-04 19:35:11 -06:00
f3a3a65be1
Merge pull request #39 from Eldritch-Oliver/GH-38
Add tooltips for the attribute names on the player sheet
2025-09-04 18:49:25 -06:00
Eldritch-Oliver
865bf87b25 Add tooltips for the attribute names on the player sheet 2025-09-04 18:48:38 -06:00
0bc3594672
Merge pull request #37 from Eldritch-Oliver/GH-34
Allow read-mode on Actor sheets instead of forced edit-only mode
2025-09-01 22:54:38 -06:00
Eldritch-Oliver
5cc94f9185 Allow read-mode on Actor sheets 2025-09-01 22:46:43 -06:00
Oliver-Akins
d66402f5cf Version bump 2025-07-26 22:45:14 -06:00
4b0ddbfc0c
Merge pull request #32 from Oliver-Akins/GH-16
Ensure attribute values remain centered when the attribute name is long
2025-07-26 22:44:42 -06:00
Oliver-Akins
c8d1259e6f Ensure attribute values remain centered when the attribute name is long 2025-07-26 22:44:14 -06:00
ab14729e91
Merge pull request #30 from Oliver-Akins/GH-19
Ask Application Block Improvements
2025-07-26 22:37:35 -06:00
Oliver-Akins
daa88fb272 Add better styling for checkboxes to make them more distinct 2025-07-26 22:36:37 -06:00
Oliver-Akins
08fb6768ad Add a divider block type 2025-07-26 11:06:05 -06:00
Oliver-Akins
fe022b2d43 Remove unused CSS reset 2025-07-26 11:03:17 -06:00
Oliver-Akins
1cdf03fe36 Add the handlebars for the select block 2025-07-26 10:54:06 -06:00
Oliver-Akins
ce65e3a516 Add an error block and validation to help ensure users know when they messed up the block type 2025-07-26 10:53:37 -06:00
Oliver-Akins
7796c82962 Move the prompt wrapper into each block partial so that those which don't need it can leave it out 2025-07-26 10:52:55 -06:00
Oliver-Akins
40820988da Add my typical options helper for ease of use 2025-07-26 10:51:55 -06:00
61f41d610e
Merge pull request #29 from Oliver-Akins/GH-17
Make the checkbox inputs right-aligned in the Ask application
2025-07-25 22:46:34 -06:00
387f9464b8
Merge pull request #27 from Oliver-Akins/GH-21
Make checkbox inputs can default to unchecked
2025-07-25 22:46:01 -06:00
75a09ef8e5
Merge pull request #26 from Oliver-Akins/GH-18
Make sure the details block types span the full width of the ask modal
2025-07-25 22:45:39 -06:00
Oliver-Akins
392923a497 Make the checkbox inputs right-aligned 2025-07-25 22:36:27 -06:00
Oliver-Akins
27dbc2db47 Make checkbox inputs can default to unchecked 2025-07-25 22:28:31 -06:00
Oliver-Akins
d7efa2c4de Make sure the details block types span the full width of the ask modal 2025-07-25 22:21:56 -06:00
312202191d
Merge pull request #25 from Oliver-Akins/fix/#22
Ensure newly created attributes save properly without needing to reorder them first
2025-07-25 22:17:11 -06:00
9ce0e71b21
Merge pull request #24 from Oliver-Akins/fix/#14
Make attribute drag and drop require using the drag handle
2025-07-25 22:16:44 -06:00
a8506be4ef
Merge pull request #23 from Oliver-Akins/fix/missing-icon
Make the drag handle icon for the Attribute Manager actually show up
2025-07-25 22:15:48 -06:00
Oliver-Akins
196da71c1d Ensure newly created attributes save properly without needing to reorder them first 2025-07-25 22:14:31 -06:00
Oliver-Akins
9266257e18 Improve the styling of the dragged element a lil bit to make it retain a background 2025-07-25 22:01:16 -06:00
Oliver-Akins
7d3e6d3653 Tweak what element is required to be used in order to drag an attribute 2025-07-25 22:00:56 -06:00
Oliver-Akins
08a90776ae Add an auto-generated badge to the release content 2025-07-25 21:11:44 -06:00
Oliver-Akins
fa8dc7d037 Fix the build step to include the assets folder 2025-07-25 21:11:30 -06:00
Oliver-Akins
47e5d5168b Version bump 2025-07-24 20:44:55 -06:00
f67efa7ffa
Merge pull request #13 from Oliver-Akins/feature/improved-ask-modal
Improved Ask Modal
2025-07-24 20:43:53 -06:00
Oliver-Akins
44a88cc7b5 Add DialogManager to a global API and create the Ask dialog for the user 2025-07-24 20:34:17 -06:00
Oliver-Akins
02b49687cf Cleanup logger statements 2025-07-24 20:33:07 -06:00
Oliver-Akins
db3cad909e Add a systemFilePath handlebars helper for better file path referencing 2025-07-24 20:23:42 -06:00
77 changed files with 4816 additions and 3417 deletions

2
.env.template Normal file
View file

@ -0,0 +1,2 @@
# The absolute path to the Foundry installation to create symlinks to
FOUNDRY_ROOT=""

View file

@ -0,0 +1,96 @@
on: [ workflow_dispatch ]
jobs:
create-artifacts:
name: "Create artifacts"
runs-on: act
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: npm clean-install
- id: version
run: cat system.json | echo version=`jq -r ".version"` >> "$FORGEJO_OUTPUT"
- name: Assert that the tag doesn't exist
run: node scripts/tagExists.mjs
env:
TAG_NAME: "v${{steps.version.outputs.version}}"
# Compendia steps
- name: Build compendia
run: "npm run data:build"
- name: Remove compendia source
run: "rm -rf packs/**/_source"
- name: Compress files
run: zip -r release.zip langs module styles templates README.md assets
- name: Upload artifacts
uses: https://data.forgejo.org/forgejo/upload-artifact@v4
with:
path: |
system.json
release.zip
scripts/*.mjs
package-lock.json
package.json
retention-days: 7
if-no-files-found: error
forgejo-release:
name: "Create Forgejo release"
runs-on: act
needs:
- create-artifacts
if: vars.RELEASE_TO_FORGEJO == 'yes'
steps:
- name: Download artifacts
uses: https://data.forgejo.org/forgejo/download-artifact@v4
with:
merge-multiple: true
- name: Install dependencies
run: npm i
- id: version
run: cat system.json | echo version=`jq -r ".version"` >> "$FORGEJO_OUTPUT"
- name: Update manifest
run: node scripts/prepareManifest.mjs
env:
DOWNLOAD_URL: "${{forgejo.server_url}}/${{forgejo.repository}}/releases/download/v${{steps.version.outputs.version}}/release.zip"
LATEST_URL: "${{forgejo.server_url}}/${{forgejo.repository}}/releases/download/latest/system.json"
- name: Add manifest into release archive
run: zip release.zip --update system.json
- name: Upload archive to s3
run: node scripts/uploadToS3.mjs
env:
TAG: "v${{steps.version.outputs.version}}"
FILE: "release.zip"
S3_BUCKET: "${{vars.S3_BUCKET}}"
S3_REGION: "${{vars.S3_REGION}}"
S3_KEY: "${{secrets.S3_KEY}}"
S3_SECRET: "${{secrets.S3_SECRET}}"
S3_ENDPOINT: "${{vars.S3_ENDPOINT}}"
- name: Upload manifest to s3
run: node scripts/uploadToS3.mjs
env:
TAG: "v${{steps.version.outputs.version}}"
FILE: "system.json"
S3_BUCKET: "${{vars.S3_BUCKET}}"
S3_REGION: "${{vars.S3_REGION}}"
S3_KEY: "${{secrets.S3_KEY}}"
S3_SECRET: "${{secrets.S3_SECRET}}"
S3_ENDPOINT: "${{vars.S3_ENDPOINT}}"
- name: Create draft release
run: node scripts/createForgejoRelease.mjs
env:
TAG: "v${{steps.version.outputs.version}}"
CDN_URL: "${{vars.CDN_URL}}"

View file

@ -0,0 +1,9 @@
on:
release:
types: [published]
jobs:
release-to-foundry:
runs-on: docker
steps:
- name: retrieve release URLS
- name: publish to Foundry

View file

@ -1,54 +0,0 @@
name: Create Draft Release
on: [workflow_dispatch]
jobs:
everything:
runs-on: ubuntu-latest
steps:
# Checkout the repository
- uses: actions/checkout@v4
# Install node and NPM
- uses: actions/setup-node@v4
with:
node-version: "20"
# Install required packages
- run: npm install
- name: Reading the system.json for the version
id: "version"
run: cat system.json | echo version=`jq -r ".version"` >> "$GITHUB_OUTPUT"
# Check that tag doesn't exist
- uses: mukunku/tag-exists-action@v1.5.0
id: check-tag
with:
tag: "v${{ steps.version.outputs.version }}"
- name: "Ensure that the tag doesn't exist"
if: ${{ steps.check-tag.outputs.exists == 'true' }}
run: exit 1
# Compile the stuff that needs to be compiled
- run: npm run build
- run: node scripts/buildCompendia.mjs
- name: Move system.json to a temp file
id: manifest-move
run: mv system.json system.temp.json
- name: Update the download property in the manifest
id: manifest-update
run: cat system.temp.json | jq -r --tab '.download = "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/release.zip"' > system.json
- name: Create the zip
run: zip -r release.zip langs module styles templates system.json README.md
- 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: "release.zip,system.json"

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
node_modules/
deprecated
.env
/foundry

View file

@ -8,9 +8,9 @@
"git.branchProtection": [],
"files.exclude": {
"*.lock": true,
".styles": false,
"node_modules": true,
"packs": true,
"foundry": true
},
"html.customData": [
"./.vscode/components.html-data.json"

View file

@ -2,3 +2,17 @@
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
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

View file

@ -0,0 +1,3 @@
<svg version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path d="m91.562 48.438c0.9375-2.8125 1.5625-5.625 1.5625-8.4375 0-13.438-10.938-24.375-24.062-24.375-6.875 0-13.75 3.125-18.125 8.125-3.4375-2.8125-7.8125-4.375-12.5-4.375-11.25 0-20.312 9.0625-20.312 20.312 0 1.25 0 2.5 0.3125 3.75-9.6875 1.5625-16.562 10-16.562 20 0 11.25 9.0625 20.312 20.312 20.312h56.25c11.25 0 20.312-9.0625 20.312-20.312-0.3125-5.3125-2.8125-10.938-7.1875-15zm-28.125 13.125c0.9375 0.9375 0.9375 2.5 0 3.4375-0.3125 0.3125-0.9375 0.625-1.5625 0.625s-1.25-0.3125-1.5625-0.625l-7.8125-7.8125-7.8125 7.8125c-0.3125 0.3125-0.9375 0.625-1.5625 0.625s-1.25-0.3125-1.5625-0.625c-0.9375-0.9375-0.9375-2.5 0-3.4375l7.8125-7.8125-7.8125-7.8125c-0.9375-0.9375-0.9375-2.5 0-3.4375s2.5-0.9375 3.4375 0l7.8125 7.8125 7.8125-7.8125c0.9375-0.9375 2.5-0.9375 3.4375 0s0.9375 2.5 0 3.4375l-7.8125 7.8125z"/>
</svg>

After

Width:  |  Height:  |  Size: 900 B

5
augments.d.ts vendored
View file

@ -1,3 +1,8 @@
declare global {
class Hooks extends foundry.helpers.Hooks {};
const fromUuid = foundry.utils.fromUuid;
}
interface Actor {
/** The system-specific data */
system: any;

View file

@ -4,7 +4,7 @@ import stylistic from "@stylistic/eslint-plugin";
export default [
// Tell eslint to ignore files that I don't mind being formatted slightly differently
{ ignores: [ `scripts/` ] },
{ ignores: [ `scripts/`, `foundry/*` ] },
{
languageOptions: {
globals: globals.browser,
@ -16,6 +16,7 @@ export default [
languageOptions: {
globals: {
CONFIG: `writable`,
CONST: `readonly`,
game: `readonly`,
Handlebars: `readonly`,
Hooks: `readonly`,
@ -72,7 +73,7 @@ export default [
"@stylistic/eol-last": `warn`,
"@stylistic/operator-linebreak": [`warn`, `before`],
"@stylistic/indent": [`warn`, `tab`],
"@stylistic/brace-style": [`warn`, `1tbs`, { "allowSingleLine": true }],
"@stylistic/brace-style": [`off`],
"@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-style": [`warn`, `last`],

View file

@ -1,7 +1,19 @@
{
"compilerOptions": {
"module": "es2022",
"target": "es2022",
"types": [
"./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"
]
}
}

View file

@ -13,6 +13,48 @@
},
"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}"
}
}
}
}

36
module/api.mjs Normal file
View file

@ -0,0 +1,36 @@
// 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,
},
}),
},
);

133
module/apps/Ask.mjs Normal file
View file

@ -0,0 +1,133 @@
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
};

View file

@ -1,6 +1,5 @@
import { __ID__, filePath } from "../consts.mjs";
import { attributeSorter } from "../utils/attributeSort.mjs";
import { Logger } from "../utils/Logger.mjs";
import { toID } from "../utils/toID.mjs";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -35,12 +34,8 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
};
static PARTS = {
attributes: {
template: filePath(`templates/AttributeManager/attribute-list.hbs`),
},
controls: {
template: filePath(`templates/AttributeManager/controls.hbs`),
},
attributes: { template: filePath(`templates/AttributeManager/attribute-list.hbs`) },
controls: { template: filePath(`templates/AttributeManager/controls.hbs`) },
};
// #endregion Options
@ -72,7 +67,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
};
new DragDrop.implementation({
dragSelector: `[data-attribute]`,
dragSelector: `.draggable`,
permissions: {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this),
@ -133,7 +128,6 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
};
};
Logger.debug(`Updating ${binding} value to ${value}`);
setProperty(this.#attributes, binding, value);
await this.render({ parts: [ `attributes` ]});
};
@ -143,7 +137,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
const id = randomID();
this.#attributes[id] = {
name: ``,
sort: Number.POSITIVE_INFINITY,
sort: Number.MAX_SAFE_INTEGER,
isRange: false,
isNew: true,
};
@ -196,7 +190,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
};
_onDragStart(event) {
const target = event.currentTarget;
const target = event.currentTarget.closest(`[data-attribute]`);
if (`link` in event.target.dataset) { return };
let dragData;
@ -210,6 +204,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2)
};
if (!dragData) { return };
event.dataTransfer.setDragImage(target, 16, 23);
event.dataTransfer.setData(`text/plain`, JSON.stringify(dragData));
};

View file

@ -1,9 +1,11 @@
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) {
@ -26,6 +28,7 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
},
actions: {
manageAttributes: this.#manageAttributes,
configureSheet: this.#configureSheet,
},
};
@ -37,6 +40,30 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
// #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();
@ -54,6 +81,12 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
return controls;
};
async close() {
this.#attributeManager?.close();
this.#attributeManager = null;
return super.close();
};
// #endregion Lifecycle
// #region Data Prep
@ -103,10 +136,29 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
// #endregion Data Prep
// #region Actions
#attributeManager = null;
/** @this {PlayerSheet} */
static async #manageAttributes() {
const app = new AttributeManager({ document: this.actor });
await app.render({ force: true });
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
};

111
module/apps/QueryStatus.mjs Normal file
View file

@ -0,0 +1,111 @@
import { __ID__, filePath } from "../consts.mjs";
import { cancel, finish, get as getQuery, requery } from "../utils/QueryManager.mjs";
import { Logger } from "../utils/Logger.mjs";
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export class QueryStatus extends HandlebarsApplicationMixin(ApplicationV2) {
// #region Options
static DEFAULT_OPTIONS = {
classes: [
__ID__,
`QueryStatus`,
],
position: {
width: 300,
height: `auto`,
},
window: {
title: `taf.Apps.QueryStatus.title`,
resizable: true,
},
actions: {
promptUser: this.promptUser,
finishEarly: this.finishEarly,
cancelRequest: this.cancelRequest,
},
};
static PARTS = {
users: {
template: filePath(`templates/QueryStatus/users.hbs`),
},
controls: {
template: filePath(`templates/QueryStatus/controls.hbs`),
},
};
// #endregion Options
// #region Instance
/** @type {string} */
#requestID;
constructor({
requestID,
...opts
}) {
if (!requestID) {
Logger.error(`A requestID must be provided for QueryStatus applications`);
return null;
};
super(opts);
this.#requestID = requestID;
};
get requestID() {
return this.#requestID;
};
// #endregion Instance
// #region Lifecycle
async _preparePartContext(partID) {
const ctx = {};
switch (partID) {
case `users`: {
this._prepareUsers(ctx);
break;
};
};
return ctx;
};
async _prepareUsers(ctx) {
const query = getQuery(this.#requestID);
if (!query) { return };
const users = [];
for (const userID of query.users) {
const user = game.users.get(userID);
users.push({
id: userID,
name: user.name,
active: user.active,
answers: query.responses[userID] ?? null,
status: query.status[userID],
});
};
ctx.users = users;
};
// #endregion Lifecycle
// #region Actions
/** @this {QueryStatus} */
static async promptUser($e, element) {
const userID = element.closest(`[data-user-id]`)?.dataset.userId;
if (!userID) { return };
requery(this.#requestID, [ userID ]);
};
/** @this {QueryStatus} */
static async cancelRequest() {
cancel(this.#requestID);
};
/** @this {QueryStatus} */
static async finishEarly() {
finish(this.#requestID);
};
// #endregion Actions
};

View file

@ -0,0 +1,171 @@
import { __ID__, filePath } from "../consts.mjs";
import { getDefaultSizing } from "../utils/getSizing.mjs";
const { diffObject, expandObject, flattenObject } = foundry.utils;
const { DocumentSheetConfig } = foundry.applications.apps;
const { CONST } = foundry;
export class TAFDocumentSheetConfig extends DocumentSheetConfig {
// #region Options
static DEFAULT_OPTIONS = {
classes: [`taf`],
form: {
handler: this.#onSubmit,
},
};
static get PARTS() {
const { form, footer } = super.PARTS;
return {
tabs: { template: `templates/generic/tab-navigation.hbs` },
foundryTab: {
...form,
template: filePath(`templates/TAFDocumentSheetConfig/foundry.hbs`),
templates: [ `templates/sheets/document-sheet-config.hbs` ],
},
systemTab: {
template: filePath(`templates/TAFDocumentSheetConfig/system.hbs`),
classes: [`standard-form`],
},
footer,
};
};
static TABS = {
main: {
initial: `system`,
labelPrefix: `taf.Apps.TAFDocumentSheetConfig.tabs`,
tabs: [
{ id: `system` },
{ id: `foundry` },
],
},
};
// #endregion Options
// #region Data Prep
async _preparePartContext(partID, context, options) {
this._prepareTabs(`main`);
context.meta = {
idp: this.id,
};
switch (partID) {
case `foundryTab`: {
await this._prepareFormContext(context, options);
break;
};
case `systemTab`: {
await this._prepareSystemSettingsContext(context, options);
break;
};
case `footer`: {
await this._prepareFooterContext(context, options);
break;
};
};
return context;
};
async _prepareSystemSettingsContext(context, _options) {
// Inherited values for placeholders
const defaults = getDefaultSizing();
context.placeholders = {
...defaults,
resizable: defaults.resizable ? `Resizable` : `Not Resizable`,
};
// Custom values from document itself
const sheetConfig = this.document.getFlag(__ID__, `PlayerSheet`) ?? {};
const sizing = sheetConfig.size ?? {};
context.values = {
width: sizing.width,
height: sizing.height,
resizable: sizing.resizable ?? ``,
};
// Static prep
context.resizeOptions = [
{ label: `Default (${context.placeholders.resizable})`, value: `` },
{ label: `Resizable`, value: `true` },
{ label: `No Resizing`, value: `false` },
];
};
// #endregion Data Prep
// #region Actions
/** @this {TAFDocumentSheetConfig} */
static async #onSubmit(event, form, formData) {
const foundryReopen = await TAFDocumentSheetConfig.#submitFoundry.call(this, event, form, formData);
const systemReopen = await TAFDocumentSheetConfig.#submitSystem.call(this, event, form, formData);
if (foundryReopen || systemReopen) {
this.document._onSheetChange({ sheetOpen: true });
};
};
/**
* This method is mostly the form submission handler that foundry uses in
* DocumentSheetConfig, however because we clobber that in order to save our
* own config stuff as well, we need to duplicate Foundry's handling and tweak
* it a bit to make it work nicely with our custom saving.
*
* @this {TAFDocumentSheetConfig}
*/
static async #submitFoundry(_event, _form, formData) {
const { object } = formData;
const { documentName, type = CONST.BASE_DOCUMENT_TYPE } = this.document;
// Update themes.
const themes = game.settings.get(`core`, `sheetThemes`);
const defaultTheme = foundry.utils.getProperty(themes, `defaults.${documentName}.${type}`);
const documentTheme = themes.documents?.[this.document.uuid];
const themeChanged = (object.defaultTheme !== defaultTheme) || (object.theme !== documentTheme);
if (themeChanged) {
foundry.utils.setProperty(themes, `defaults.${documentName}.${type}`, object.defaultTheme);
themes.documents ??= {};
themes.documents[this.document.uuid] = object.theme;
await game.settings.set(`core`, `sheetThemes`, themes);
}
// Update sheets.
const { defaultClass } = this.constructor.getSheetClassesForSubType(documentName, type);
const sheetClass = this.document.getFlag(`core`, `sheetClass`) ?? ``;
const defaultSheetChanged = object.defaultClass !== defaultClass;
const documentSheetChanged = object.sheetClass !== sheetClass;
if (themeChanged || (game.user.isGM && defaultSheetChanged)) {
if (game.user.isGM && defaultSheetChanged) {
const setting = game.settings.get(`core`, `sheetClasses`);
foundry.utils.setProperty(setting, `${documentName}.${type}`, object.defaultClass);
await game.settings.set(`core`, `sheetClasses`, setting);
}
// This causes us to manually rerender the sheet due to the theme or default
// sheet class changing resulting in no update making it to the client-document's
// _onUpdate handling
if (!documentSheetChanged) {
return true;
}
}
// Update the document-specific override.
if (documentSheetChanged) {
this.document.setFlag(`core`, `sheetClass`, object.sheetClass);
};
return false;
};
/** @this {TAFDocumentSheetConfig} */
static async #submitSystem(_event, _form, formData) {
const { FLAGS: flags } = expandObject(formData.object);
const diff = flattenObject(diffObject(this.document.flags, flags));
const hasChanges = Object.keys(diff).length > 0;
if (hasChanges) {
await this.document.update({ flags });
};
return hasChanges;
};
// #endregion Actions
};

View file

@ -1,10 +1,7 @@
import { Logger } from "../utils/Logger.mjs";
const { Actor } = foundry.documents;
export class TAFActor extends Actor {
async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) {
Logger.table({ attribute, value, isDelta, isBar });
const attr = foundry.utils.getProperty(this.system, attribute);
const current = isBar ? attr.value : attr;
const update = isDelta ? current + value : value;

View file

@ -0,0 +1,7 @@
const { Item } = foundry.documents;
export class TAFItem extends Item {
async _preCreate() {
return false;
};
};

View file

@ -1,5 +1,3 @@
import { Logger } from "../utils/Logger.mjs";
const { TokenDocument } = foundry.documents;
const { getProperty, getType, hasProperty, isSubclass } = foundry.utils;
@ -61,7 +59,7 @@ export class TAFTokenDocument extends TokenDocument {
*/
getBarAttribute(barName, {alternative} = {}) {
const attribute = alternative || this[barName]?.attribute;
Logger.log(barName, attribute);
if (!attribute || !this.actor) {
return null;
};

View file

@ -0,0 +1,7 @@
import { filePath } from "../consts.mjs";
import { options } from "./options.mjs";
export default {
systemFilePath: filePath,
"taf-options": options,
};

View file

@ -0,0 +1,34 @@
/**
* @typedef {object} Option
* @property {string} [label]
* @property {string|number} value
* @property {boolean} [disabled]
*/
/**
* @param {string | number} selected The selected value
* @param {Array<Option | string>} opts The options that are valid
* @param {any} meta The Handlebars meta processing
*/
export function options(selected, opts, meta) {
const { localize = false } = meta.hash;
selected = Handlebars.escapeExpression(selected);
const htmlOptions = [];
for (let opt of opts) {
if (typeof opt === `string`) {
opt = { label: opt, value: opt };
};
opt.value = Handlebars.escapeExpression(opt.value);
htmlOptions.push(
`<option
value="${opt.value}"
${selected === opt.value ? `selected` : ``}
${opt.disabled ? `disabled` : ``}
>
${localize ? game.i18n.format(opt.label) : opt.label}
</option>`,
);
};
return new Handlebars.SafeString(htmlOptions.join(`\n`));
};

View file

@ -6,6 +6,7 @@ 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
@ -13,8 +14,10 @@ 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`);
@ -24,6 +27,10 @@ Hooks.on(`init`, () => {
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,
@ -33,6 +40,9 @@ Hooks.on(`init`, () => {
},
);
registerCustomComponents();
registerWorldSettings();
registerSockets();
registerCustomComponents();
Handlebars.registerHelper(helpers);
});

View file

@ -0,0 +1,6 @@
import { userActivity } from "../utils/QueryManager.mjs";
Hooks.on(`userConnected`, (user, connected) => {
if (user.isSelf) { return };
userActivity(user.id, connected);
});

View file

@ -1 +1,3 @@
import "./api.mjs";
import "./hooks/init.mjs";
import "./hooks/userConnected.mjs";

33
module/sockets/_index.mjs Normal file
View file

@ -0,0 +1,33 @@
import { Logger } from "../utils/Logger.mjs";
import { queryCancel } from "./query/cancel.mjs";
import { queryNotify } from "./query/notify.mjs";
import { queryPrompt } from "./query/prompt.mjs";
import { querySubmit } from "./query/submit.mjs";
const events = {
// Data Request sockets
"query.cancel": queryCancel,
"query.notify": queryNotify,
"query.prompt": queryPrompt,
"query.submit": querySubmit,
};
export function registerSockets() {
Logger.info(`Setting up socket listener`);
game.socket.on(`system.taf`, (data, userID) => {
const { event, payload } = data ?? {};
if (event == null || payload === undefined) {
ui.notifications.error(game.i18n.format(`taf.notifs.error.invalid-socket`));
return;
};
if (events[event] == null) {
ui.notifications.error(game.i18n.format(`taf.notifs.error.unknown-socket-event`, { event }));
return;
};
const user = game.users.get(userID);
events[event](payload, user);
});
};

View file

@ -0,0 +1,19 @@
import { DialogManager } from "../../utils/DialogManager.mjs";
import { localizer } from "../../utils/localizer.mjs";
export async function queryCancel(payload) {
const { id } = payload;
if (!id) {
ui.notifications.error(localizer(
`taf.notifs.error.malformed-socket-payload`,
{
event: `query.cancel`,
details: `taf.notifs.error.missing-id`,
},
));
return;
};
await DialogManager.close(id);
};

View file

@ -0,0 +1,25 @@
import { localizer } from "../../utils/localizer.mjs";
import { respondedToQueries } from "../../utils/QueryManager.mjs";
export function queryNotify(payload) {
const { id, userID, content, includeGM } = payload;
if (userID !== game.user.id) { return };
// Ensure that each user can only get one notification about a query
if (!respondedToQueries.has(id)) { return };
let whisper = [game.user.id];
if (includeGM) {
whisper = game.users.filter(u => u.isGM).map(u => u.id);
};
ChatMessage.implementation.create({
flavor: localizer(`taf.misc.data-query-notif-header`),
content,
whisper,
style: CONST.CHAT_MESSAGE_STYLES.OOC,
});
respondedToQueries.delete(id);
};

View file

@ -0,0 +1,54 @@
import { DialogManager } from "../../utils/DialogManager.mjs";
import { localizer } from "../../utils/localizer.mjs";
import { respondedToQueries } from "../../utils/QueryManager.mjs";
export async function queryPrompt(payload) {
const {
id,
users,
config,
request,
} = payload;
if (!id) {
ui.notifications.error(localizer(
`taf.notifs.error.malformed-socket-payload`,
{
event: `query.cancel`,
details: `taf.notifs.error.missing-id`,
},
));
return;
};
// null/undefined is a special case for "all users but me" by default
if (users != null && !Array.isArray(users)) {
ui.notifications.error(localizer(
`taf.notifs.error.malformed-socket-payload`,
{
event: `query.cancel`,
details: `taf.sockets.user-list-required`,
},
));
return;
};
if (users != null && !users.includes(game.user.id)) { return };
request.id = id;
const result = await DialogManager.ask(request, config);
if (result.state === `fronted`) {
return;
} else if (result.state === `errored`) {
ui.notifications.error(result.error);
} else if (result.state === `prompted`) {
respondedToQueries.add(request.id);
game.socket.emit(`system.taf`, {
event: `query.submit`,
payload: {
id: request.id,
answers: result.answers,
},
});
};
};

View file

@ -0,0 +1,23 @@
import { addResponse, has as hasQuery } from "../../utils/QueryManager.mjs";
import { localizer } from "../../utils/localizer.mjs";
export function querySubmit(payload, user) {
const {
id,
answers,
} = payload;
if (!id) {
ui.notifications.error(localizer(
`taf.notifs.error.malformed-socket-payload`,
{
event: `query.cancel`,
details: `taf.notifs.error.missing-id`,
},
));
return;
};
if (!hasQuery(id)) { return };
addResponse(id, user.id, answers);
};

View file

@ -0,0 +1,114 @@
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,
};

View file

@ -0,0 +1,289 @@
import { filePath } from "../consts.mjs";
import { Logger } from "./Logger.mjs";
import { QueryStatus } from "../apps/QueryStatus.mjs";
/**
* An object containing information about the current status for all
* users involved with the data request.
* @typedef {Record<
* string,
* "finished" | "waiting" | "disconnected" | "unprompted"
* >} UserStatus
*/
/**
* @typedef QueryData
* @property {string[]} users
* @property {Function} resolve
* @property {Record<string, object>} responses
* @property {(() => Promise<void>)|null} onSubmit
* @property {QueryStatus|null} app
* @property {UserStatus} status
* @property {object} request The data used to form the initial request
* @property {object} config The data used to create the initial config
*/
/**
* This internal API is used in order to prevent the query.notify event
* from being fired off in situations where the user hasn't responded,
* wasn't part of the query, or has already been notified.
* @type {Set<string>}
*/
export const respondedToQueries = new Set();
/** @type {Map<string, QueryData>} */
const queries = new Map();
/** @type {Map<string, Promise>} */
const promises = new Map();
async function sendBasicNotification(requestID, userID, answers) {
const content = await foundry.applications.handlebars.renderTemplate(
filePath(`templates/query-response.hbs`),
{ answers },
);
await notify(requestID, userID, content, { includeGM: false });
};
export function has(requestID) {
return queries.has(requestID);
};
/** @returns {Omit<QueryData, "resolve"|"onSubmit"|"app">} */
export function get(requestID) {
if (!queries.has(requestID)) { return null };
const query = queries.get(requestID);
const cloned = foundry.utils.deepClone(query);
delete cloned.onSubmit;
delete cloned.resolve;
delete cloned.app;
return foundry.utils.deepFreeze(cloned);
};
export async function query(
request,
{
onSubmit = sendBasicNotification,
users = null,
showStatusApp = true,
...config
} = {},
) {
if (!request.id) {
ui.notifications.error(game.i18n.localize(`taf.notifs.error.missing-id`));
return;
};
game.socket.emit(`system.taf`, {
event: `query.prompt`,
payload: {
id: request.id,
users,
request,
config,
},
});
if (promises.has(request.id)) {
return null;
};
users ??= game.users
.filter(u => u.id !== game.user.id)
.map(u => u.id);
const promise = new Promise((resolve) => {
/** @type {UserStatus} */
const status = {};
for (const user of users) {
status[user] = game.users.get(user).active ? `waiting` : `disconnected`;
};
queries.set(
request.id,
{
users,
request,
config,
responses: {},
resolve,
onSubmit,
app: null,
status,
},
);
});
if (showStatusApp) {
const app = new QueryStatus({ requestID: request.id });
app.render({ force: true });
queries.get(request.id).app = app;
};
return promise;
};
export async function requery(requestID, users) {
const query = queries.get(requestID);
if (!query) { return };
game.socket.emit(`system.taf`, {
event: `query.prompt`,
payload: {
id: requestID,
users,
request: query.request,
config: query.config,
},
});
for (const user of users) {
query.status[user] = `waiting`;
};
query.app?.render({ parts: [ `users` ] });
};
export async function addResponse(requestID, userID, answers) {
if (!queries.has(requestID)) { return };
const query = queries.get(requestID);
// User closed the popup manually
if (answers == null) {
query.status[userID] = `unprompted`;
}
// User submitted the answers as expected
else {
query.responses[userID] = answers;
query.status[userID] = `finished`;
await query.onSubmit?.(requestID, userID, answers);
};
await maybeResolve(requestID);
};
async function maybeResolve(requestID) {
const query = queries.get(requestID);
// Determine how many users are considered "finished"
let finishedUserCount = 0;
for (const user of query.users) {
const hasApp = query.app != null;
switch (query.status[user]) {
case `finished`: {
finishedUserCount++;
break;
};
case `cancelled`:
case `disconnected`:
case `unprompted`: {
if (!hasApp) {
finishedUserCount++;
};
break;
};
};
};
// Ensure that we have a finished response from everyone prompted
if (query.users.length === finishedUserCount) {
query.app?.close();
query.resolve(query.responses);
queries.delete(requestID);
promises.delete(requestID);
} else {
query.app?.render({ parts: [ `users` ] });
};
};
export async function notify(requestID, userID, content, { includeGM = false } = {}) {
// Prevent sending notifications for not-your queries
if (!queries.has(requestID)) { return };
game.socket.emit(`system.taf`, {
event: `query.notify`,
payload: {
id: requestID,
userID,
content,
includeGM,
},
});
};
export async function finish(requestID) {
// prevent finishing other people's queries
if (!queries.has(requestID)) { return };
const query = queries.get(requestID);
query.app?.close();
query.resolve(query.responses);
queries.delete(requestID);
promises.delete(requestID);
game.socket.emit(`system.taf`, {
event: `query.cancel`,
payload: { id: requestID },
});
};
export async function cancel(requestID) {
// prevent cancelling other people's queries
if (!queries.has(requestID)) { return };
const query = queries.get(requestID);
query.app?.close();
query.resolve(null);
queries.delete(requestID);
promises.delete(requestID);
game.socket.emit(`system.taf`, {
event: `query.cancel`,
payload: { id: requestID },
});
};
export async function setApplication(requestID, app) {
if (!queries.has(requestID)) { return };
if (!(app instanceof QueryStatus)) { return };
const query = queries.get(requestID);
if (query.app) {
Logger.error(`Cannot set an application for a query that has one already`);
return;
};
query.app = app;
};
export async function userActivity(userID, connected) {
for (const [id, query] of queries.entries()) {
if (query.users.includes(userID)) {
// Update the user's status to allow for the app to re-prompt them
if (query.status[userID] !== `finished`) {
if (connected) {
query.status[userID] = `unprompted`;
} else {
query.status[userID] = `disconnected`;
};
maybeResolve(id);
};
query.app?.render({ parts: [ `users` ] });
};
};
};
export const QueryManager = {
has, get,
query, requery,
addResponse,
notify,
finish, cancel,
setApplication,
userActivity,
};

View file

@ -0,0 +1,32 @@
import { PlayerSheet } from "../apps/PlayerSheet.mjs";
/**
* @typedef SheetSizing
* @property {number} width The initial width of the application
* @property {number} height The initial height of the application
* @property {boolean} resizable Whether or not the application
* is able to be resized with a drag handle.
*/
/**
* Retrieves the computed default sizing data based on world settings
* and the sheet class' DEFAULT_OPTIONS
* @returns {SheetSizing}
*/
export function getDefaultSizing() {
/** @type {SheetSizing} */
const sizing = {
width: undefined,
height: undefined,
resizable: undefined,
};
// TODO: defaults from world settings
// Defaults from the sheet class itself
sizing.height ||= PlayerSheet.DEFAULT_OPTIONS.position.height;
sizing.width ||= PlayerSheet.DEFAULT_OPTIONS.position.width;
sizing.resizable ||= PlayerSheet.DEFAULT_OPTIONS.window.resizable;
return sizing;
};

View file

@ -0,0 +1,32 @@
const config = Object.preventExtensions({
subKeyPattern: /@(?<key>[a-zA-Z.]+)/gm,
maxDepth: 10,
});
export function localizer(key, args = {}, depth = 0) {
/** @type {string} */
let localized = game.i18n.format(key, args);
const subkeys = localized.matchAll(config.subKeyPattern);
// Short-cut to help prevent infinite recursion
if (depth > config.maxDepth) {
return localized;
};
/*
Helps prevent recursion on the same key so that we aren't doing excess work.
*/
const localizedSubkeys = new Map();
for (const match of subkeys) {
const subkey = match.groups.key;
if (localizedSubkeys.has(subkey)) { continue };
localizedSubkeys.set(subkey, localizer(subkey, args, depth + 1));
};
return localized.replace(
config.subKeyPattern,
(_fullMatch, subkey) => {
return localizedSubkeys.get(subkey);
},
);
};

5988
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,18 @@
{
"devDependencies": {
"@aws-sdk/client-s3": "^3.934.0",
"@eslint/js": "^9.8.0",
"@foundryvtt/foundryvtt-cli": "^1.0.3",
"@league-of-foundry-developers/foundry-vtt-types": "^9.280.0",
"@stylistic/eslint-plugin": "^2.6.1",
"axios": "^1.13.2",
"dotenv": "^17.2.2",
"eslint": "^9.8.0",
"globals": "^15.9.0",
"sass": "^1.77.8"
"globals": "^15.9.0"
},
"scripts": {
"css": "sass --watch --embed-source-map --no-error-css styles/:.styles/",
"build": "sass --embed-source-map --no-error-css styles/:.styles/",
"data:build": "node scripts/buildCompendia.mjs",
"data:extract": "node scripts/extractCompendia.mjs",
"link": "node scripts/linkFoundry.mjs",
"lint": "eslint --fix",
"lint:nofix": "eslint"
}

View file

@ -0,0 +1,52 @@
import axios from "axios";
const {
TAG,
FORGEJO_API_URL: API,
FORGEJO_REPOSITORY: REPO,
FORGEJO_TOKEN: TOKEN,
CDN_URL,
} = process.env;
async function addReleaseAsset(releaseID, name) {
return axios.post(
`${API}/repos/${REPO}/releases/${releaseID}/assets`,
{ external_url: `${CDN_URL}/${REPO}/${TAG}/${name}`, },
{
headers: {
Authorization: `token ${TOKEN}`,
"Content-Type": `multipart/form-data`,
},
params: { name },
}
);
};
async function main() {
// Initial Release Data
const release = await axios.post(
`${API}/repos/${REPO}/releases`,
{
name: TAG,
tag_name: TAG,
draft: true,
hide_archive_links: true,
},
{
headers: { Authorization: `token ${TOKEN}` },
}
);
try {
await addReleaseAsset(release.data.id, `release.zip`);
await addReleaseAsset(release.data.id, `system.json`);
} catch (e) {
console.error(`Failed to add assets to the release`);
process.exit(1);
};
console.log(`Release created`);
};
main();

47
scripts/linkFoundry.mjs Normal file
View file

@ -0,0 +1,47 @@
import { existsSync } from "fs";
import { symlink, unlink } from "fs/promises";
import { join } from "path";
import { config } from "dotenv";
config({ quiet: true });
const root = process.env.FOUNDRY_ROOT;
// Early exit
if (!root) {
console.error(`Must provide a FOUNDRY_ROOT environment variable`);
process.exit(1);
};
// Assert Foundry exists
if (!existsSync(root)) {
console.error(`Foundry root not found.`);
process.exit(1);
};
// Removing existing symlink
if (existsSync(`foundry`)) {
console.log(`Attempting to unlink foundry instance`);
try {
await unlink(`foundry`);
} catch {
console.error(`Failed to unlink foundry folder.`);
process.exit(1);
};
};
// Account for if the root is pointing at an Electron install
let targetRoot = root;
if (existsSync(join(root, `resources`, `app`))) {
console.log(`Switching to use the "${root}/resources/app" directory`);
targetRoot = join(root, `resources`, `app`);
};
// Create symlink
console.log(`Linking foundry source into folder`)
try {
await symlink(targetRoot, `foundry`);
} catch (e) {
console.error(e);
process.exit(1);
};

View file

@ -0,0 +1,45 @@
/*
The intent of this script is to do all of the modifications of the
manifest file that we need to do in order to release the system.
This can include removing dev-only fields/attributes that end
users will never, and should never, care about nor need.
*/
import { readFile, writeFile } from "fs/promises";
const MANIFEST_PATH = `system.json`;
const {
DOWNLOAD_URL,
LATEST_URL,
} = process.env;
let manifest;
try {
manifest = JSON.parse(await readFile(MANIFEST_PATH, `utf-8`));
console.log(`Manifest loaded from disk`);
} catch {
console.error(`Failed to parse manifest file.`);
process.exit(1);
};
console.log(`Updating download/manifest URLs`)
manifest.download = DOWNLOAD_URL;
manifest.manifest = LATEST_URL;
// Filter out dev-only resources
if (manifest.esmodules) {
console.log(`Removing dev-only esmodules`);
manifest.esmodules = manifest.esmodules.filter(
filepath => !filepath.startsWith(`dev/`)
);
};
// Remove dev flags
console.log(`Cleaning up flags`);
delete manifest.flags?.hotReload;
if (Object.keys(manifest.flags).length === 0) {
delete manifest.flags;
};
await writeFile(MANIFEST_PATH, JSON.stringify(manifest, undefined, `\t`));
console.log(`Manifest written back to disk`);

38
scripts/tagExists.mjs Normal file
View file

@ -0,0 +1,38 @@
import axios from "axios";
const {
TAG_NAME,
FORGEJO_API_URL: API_URL,
FORGEJO_REPOSITORY: REPO,
FORGEJO_TOKEN: TOKEN,
} = process.env;
async function main() {
if (!TAG_NAME) {
console.log(`Tag name must not be blank`);
process.exit(1);
};
const requestURL = `${API_URL}/repos/${REPO}/tags/${TAG_NAME}`;
const response = await axios.get(
requestURL,
{
headers: { Authorization: `token ${TOKEN}` },
validateStatus: () => true,
},
);
// We actually *want* an error when the tag exists, instead of when
// it doesn't
if (response.status === 200) {
console.log(`Tag with name "${TAG_NAME}" already exists`);
process.exit(1);
};
console.log(`Tag with name "${TAG_NAME}" not found, proceeding`);
};
main();

65
scripts/uploadToS3.mjs Normal file
View file

@ -0,0 +1,65 @@
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { createReadStream } from "fs";
const requiredEnvVariables = [
`TAG`, `FILE`,
`FORGEJO_REPOSITORY`,
`S3_BUCKET`, `S3_REGION`, `S3_KEY`, `S3_SECRET`, `S3_ENDPOINT`,
];
async function main() {
// Assert all of the required env variables are present
const missing = [];
for (const envVar of requiredEnvVariables) {
if (!(envVar in process.env)) {
missing.push(envVar);
};
};
if (missing.length > 0) {
console.error(`Missing the following required environment variables: ${missing.join(`, `)}`);
process.exit(1);
};
const {
TAG,
S3_ENDPOINT,
S3_REGION,
S3_KEY,
S3_SECRET,
S3_BUCKET,
FILE,
FORGEJO_REPOSITORY: REPO,
} = process.env;
const s3Client = new S3Client({
endpoint: S3_ENDPOINT,
forcePathStyle: false,
region: S3_REGION,
credentials: {
accessKeyId: S3_KEY,
secretAccessKey: S3_SECRET
},
});
const name = FILE.split(`/`).at(-1);
const params = {
Bucket: S3_BUCKET,
Key: `${REPO}/${TAG}/${name}`,
Body: createReadStream(FILE),
ACL: "public-read",
METADATA: {
"x-repo-version": TAG,
},
};
try {
const response = await s3Client.send(new PutObjectCommand(params));
console.log("Upload successful");
} catch (err) {
console.error("Upload to s3 failed");
};
};
main();

55
styles/Apps/Ask.css Normal file
View file

@ -0,0 +1,55 @@
.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;
}
}

View file

@ -23,6 +23,11 @@
flex-direction: column;
}
}
/* Used to style the actual element as dragging */
&:has(taf-icon:active) {
background: var(--background);
}
}
taf-icon {

View file

@ -30,6 +30,7 @@
align-items: center;
gap: 4px;
width: 100px;
margin: 0 auto;
> input {
text-align: center;

View file

@ -0,0 +1,33 @@
.taf.QueryStatus {
.user-list {
display: flex;
flex-direction: column;
gap: 4px;
list-style-type: none;
margin: 0;
padding: 0;
li {
display: flex;
flex-direction: column;
margin: 0;
border: 1px solid rebeccapurple;
border-radius: 4px;
padding: 4px 8px;
> .user-summary {
display: flex;
flex-direction: row;
align-items: center;
/* Same height as the icons used for loading/disconnected */
height: 35px;
}
}
}
.control-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
}

View file

@ -0,0 +1,15 @@
.taf.sheet-config {
section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.tab {
display: none;
}
.tab.active {
display: unset;
}
}

20
styles/elements/div.css Normal file
View file

@ -0,0 +1,20 @@
.taf > .window-content div {
&.chip {
display: inline flex;
color: var(--chip-color);
background: var(--chip-background);
border: 1px solid var(--chip-border-color);
border-radius: 4px;
.key {
padding: 2px 4px;
}
.value {
padding: 2px 4px;
border-radius: 0 4px 4px 0;
color: var(--chip-value-color);
background: var(--chip-value-background);
}
}
}

7
styles/elements/hr.css Normal file
View file

@ -0,0 +1,7 @@
.taf > .window-content hr {
height: 1px;
background: rebeccapurple;
border-radius: 0;
margin: 0;
padding: 0;
}

View file

@ -3,4 +3,54 @@
--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;
}
}
}
}

View file

@ -1,8 +1,9 @@
.taf > .window-content prose-mirror {
background: var(--prosemirror-background);
gap: 0;
.editor-content {
padding: 0 8px 8px;
padding: 8px;
}
.tableWrapper th,

45
styles/elements/span.css Normal file
View file

@ -0,0 +1,45 @@
@keyframes rotate {
0% { transform: rotate(0deg); }
50% { transform: rotate(360deg); }
100% { transform: rotate(720deg); }
}
@keyframes prixClipFix {
0%, 100% {
clip-path: polygon(50% 50%,0 0,0 0,0 0,0 0,0 0);
}
25%, 63% {
clip-path: polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0);
}
37%, 50% {
clip-path: polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%);
}
}
.taf > .window-content span {
&.loader {
--size: 35px;
width: var(--size);
height: var(--size);
border-radius: 50%;
position: relative;
animation: rotate 2s linear infinite;
display: block;
&::before, &::after {
content: "";
box-sizing: border-box;
position: absolute;
inset: 0px;
border-radius: 50%;
border: 5px solid var(--spinner-outer-colour, #fff);
animation: prixClipFix 4s linear infinite;
}
&::after{
inset: 8px;
transform: rotate3d(90, 90, 0, 180deg );
border-color: var(--spinner-inner-colour, #ff3d00);
}
}
}

21
styles/elements/table.css Normal file
View file

@ -0,0 +1,21 @@
/*
This styling is unscoped in order to make it so that it still applies
to the chat messages which are not within a scope I control.
*/
table.taf-query-summary {
margin: 0px;
tr:hover > td {
background-color: var(--table-header-border-highlight);
}
td {
padding: 4px 8px;
border: 1px solid var(--table-header-border-color);
width: 40%;
&:first-of-type {
width: 60%;
}
}
}

View file

@ -0,0 +1,3 @@
.taf > .window-content {
.grow { flex-grow: 1; }
}

View file

@ -1,16 +1,29 @@
@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/PlayerSheet.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);

3
styles/resets/button.css Normal file
View file

@ -0,0 +1,3 @@
.taf > .window-content button {
height: initial;
}

3
styles/resets/hr.css Normal file
View file

@ -0,0 +1,3 @@
.taf > .window-content hr {
all: initial;
}

View file

@ -1,5 +1,8 @@
.taf > .window-content {
button, input {
input[type="checkbox"] {
all: initial;
&::after, &::before {
all: initial;
}
}
}

View file

@ -1,3 +1,13 @@
.theme-dark {
--prosemirror-background: var(--color-cool-5);
--spinner-outer-colour: white;
--spinner-inner-colour: #FF3D00;
/* Chip Variables */
--chip-color: #fff7ed;
--chip-background: #2b3642;
--chip-value-color: #fff7ed;
--chip-value-background: #10161d;
--chip-border-color: var(--chip-value-background);
}

View file

@ -1,3 +1,13 @@
.theme-light {
--prosemirror-background: white;
--spinner-outer-colour: black;
--spinner-inner-colour: #FF3D00;
/* Chip Variables */
--chip-color: #18181b;
--chip-background: #fafafa;
--chip-value-color: #18181b;
--chip-value-background: #d4d4d8aa;
--chip-border-color: var(--chip-value-background);
}

View file

@ -2,20 +2,17 @@
"id": "taf",
"title": "Text-Based Actors",
"description": "An intentionally minimalist system that enables you to play rules-light games without getting in your way!",
"version": "2.1.0",
"download": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/release.zip",
"manifest": "https://github.com/Oliver-Akins/Text-Actors-Foundry/releases/latest/download/system.json",
"url": "https://github.com/Oliver-Akins/Text-Actors-Foundry",
"version": "2.4.0",
"download": "",
"manifest": "",
"url": "https://git.varify.ca/Foundry/taf",
"compatibility": {
"minimum": 13,
"verified": 13,
"maximum": 13
},
"authors": [
{
"name": "Oliver Akins",
"url": "https://oliver.akins.me"
}
{ "name": "Oliver" }
],
"esmodules": [
"./module/main.mjs"
@ -44,10 +41,11 @@
},
"Item": {}
},
"socket": true,
"flags": {
"hotReload": {
"extensions": ["css", "hbs", "json", "js", "mjs", "svg"],
"paths": ["templates", "langs", ".styles", "module", "assets"]
"paths": ["templates", "langs", "styles", "module", "assets"]
}
}
}

View file

@ -0,0 +1,13 @@
<div class="control-row">
<button
type="button"
data-action="cancel"
>
Cancel
</button>
<button
type="submit"
>
Confirm and Close
</button>
</div>

10
templates/Ask/inputs.hbs Normal file
View file

@ -0,0 +1,10 @@
<div class="dialog-content">
{{#if description}}
<p>
{{ description }}
</p>
{{/if}}
{{#each inputs as | i |}}
{{> (concat (systemFilePath "templates/Ask/inputs/" ) i.type ".hbs") i}}
{{/each}}
</div>

View file

@ -0,0 +1,14 @@
<div class="prompt">
<label
for="{{id}}"
>
{{ label }}
</label>
<input
type="checkbox"
id="{{ id }}"
name="{{ key }}"
{{ checked defaultValue }}
{{#if autofocus}}autofocus{{/if}}
>
</div>

View file

@ -0,0 +1,3 @@
<p>
{{{ details }}}
</p>

View file

@ -0,0 +1 @@
<hr>

View file

@ -0,0 +1,3 @@
<p class="error">
{{ details }}
</p>

View file

@ -0,0 +1,14 @@
<div class="prompt">
<label
for="{{id}}"
>
{{ label }}
</label>
<input
type="{{ inputType }}"
id="{{ id }}"
name="{{ key }}"
{{ valueAttribute }}="{{ defaultValue }}"
{{#if autofocus}}autofocus{{/if}}
>
</div>

View file

@ -0,0 +1,13 @@
<div class="prompt">
<label
for="{{id}}"
>
{{ label }}
</label>
<select
id="{{ id }}"
name="{{ key }}"
>
{{ taf-options defaultValue options }}
</select>
</div>

View file

@ -8,6 +8,7 @@
name="icons/drag-handle"
var:stroke="currentColor"
var:fill="currentColor"
class="draggable"
></taf-icon>
{{#if attr.isNew}}
<input

View file

@ -12,6 +12,7 @@
name="{{attr.path}}.value"
value="{{attr.value}}"
aria-label="Current value"
data-tooltip="@{{ attr.id }}{{#if attr.isRange}}.value{{/if}}"
>
{{#if attr.isRange}}
<span aria-hidden="true">/</span>
@ -21,6 +22,7 @@
name="{{attr.path}}.max"
value="{{attr.max}}"
aria-label="Maximum value"
data-tooltip="@{{ attr.id }}.max"
>
{{/if}}
</div>

View file

@ -6,6 +6,7 @@
value="{{system.content}}"
collaborate="true"
data-document-uuid="{{actor.uuid}}"
toggled="true"
>
{{{ enriched.system.content }}}
</prose-mirror>

View file

@ -0,0 +1,8 @@
<div class="control-row">
<button data-action="cancelRequest">
{{ localize "taf.Apps.QueryStatus.cancel-request" }}
</button>
<button data-action="finishEarly">
{{ localize "taf.Apps.QueryStatus.finish-early" }}
</button>
</div>

View file

@ -0,0 +1,46 @@
<ul class="user-list">
{{#each users as | user |}}
<li
style="--spinner-inner-colour: var(--user-color-{{user.id}})"
data-user-id="{{ user.id }}"
>
<div class="user-summary">
<div class="grow">
{{ user.name }}
</div>
{{#if (eq user.status "waiting")}}
<span class="loader"></span>
{{else if (eq user.status "disconnected")}}
<taf-icon
data-tooltip="taf.Apps.QueryStatus.user-disconnected-tooltip"
name="icons/disconnected"
var:size="35px"
var:stroke="currentColor"
var:fill="currentColor"
></taf-icon>
{{else if (eq user.status "unprompted")}}
<button
type="button"
data-action="promptUser"
>
{{ localize "taf.Apps.QueryStatus.send-request" }}
</button>
{{/if}}
</div>
{{#if (eq user.status "finished")}}
<div class="chip-list">
{{#each user.answers as | answer |}}
<div class="chip">
<span class="key">
{{ @key }}
</span>
<span class="value">
{{ answer }}
</span>
</div>
{{/each}}
</div>
{{/if}}
</li>
{{/each}}
</ul>

View file

@ -0,0 +1,3 @@
<div class="foundry tab {{tabs.foundry.cssClass}}" data-group="main" data-tab="foundry">
{{> "templates/sheets/document-sheet-config.hbs" }}
</div>

View file

@ -0,0 +1,48 @@
<div class="system tab {{tabs.system.cssClass}}" data-group="main" data-tab="system">
<fieldset>
<legend>
{{ localize "taf.Apps.TAFDocumentSheetConfig.Sizing" }}
</legend>
<div class="form-group">
<label for="{{meta.idp}}-width">
{{ localize "taf.Apps.TAFDocumentSheetConfig.Width.label" }}
</label>
<div class="form-fields">
<input
type="number"
name="FLAGS.taf.PlayerSheet.size.width"
id="{{meta.idp}}-width"
value="{{values.width}}"
placeholder="{{placeholders.width}}"
>
</div>
</div>
<div class="form-group">
<label for="{{meta.idp}}-height">
{{ localize "taf.Apps.TAFDocumentSheetConfig.Height.label" }}
</label>
<div class="form-fields">
<input
type="number"
name="FLAGS.taf.PlayerSheet.size.height"
id="{{meta.idp}}-height"
value="{{values.height}}"
placeholder="{{placeholders.height}}"
>
</div>
</div>
<div class="form-group">
<label for="{{meta.idp}}-resize">
{{ localize "taf.Apps.TAFDocumentSheetConfig.Resizable.label" }}
</label>
<div class="form-fields">
<select
name="FLAGS.taf.PlayerSheet.size.resizable"
id="{{meta.idp}}-resize"
>
{{ taf-options values.resizable resizeOptions }}
</select>
</div>
</div>
</fieldset>
</div>

View file

@ -0,0 +1,16 @@
{{#if answers}}
<table class="taf-query-summary">
<tr>
<td>{{ localize "taf.misc.Key" }}</td>
<td>{{ localize "taf.misc.Value" }}</td>
</tr>
{{#each answers as | answer |}}
<tr>
<td>{{ @key }}</td>
<td>{{ answer }}</td>
</tr>
{{/each}}
</table>
{{else}}
{{ localize "taf.misc.no-data-submitted" }}
{{/if}}