Compare commits

..

No commits in common. "main" and "v2.3.0" have entirely different histories.
main ... v2.3.0

57 changed files with 3585 additions and 4286 deletions

View file

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

View file

@ -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}}"

View file

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

55
.github/workflows/draft-release.yaml vendored Normal file
View file

@ -0,0 +1,55 @@
name: Create Draft Release
on: [workflow_dispatch]
jobs:
draft:
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: Update the manifest with the relevant properties
id: manifest-update
uses: microsoft/variable-substitution@v1
with:
files: "system.json"
env:
download: "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/release.zip"
- name: Create the zip
run: zip -r release.zip langs module styles templates system.json README.md assets
- name: Create the draft release
uses: ncipollo/release-action@v1
with:
tag: "v${{ steps.version.outputs.version }}"
commit: ${{ github.ref }}
draft: true
body: <img aria-hidden="true" src="https://img.shields.io/github/downloads/${{ github.repository }}/v${{ steps.version.outputs.version }}/release.zip?style=flat-square&color=%2300aa00">
generateReleaseNotes: true
artifacts: "release.zip,system.json"

2
.gitignore vendored
View file

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

@ -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

5
augments.d.ts vendored
View file

@ -1,8 +1,3 @@
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/`, `foundry/*` ] },
{ ignores: [ `scripts/` ] },
{
languageOptions: {
globals: globals.browser,
@ -16,7 +16,6 @@ export default [
languageOptions: {
globals: {
CONFIG: `writable`,
CONST: `readonly`,
game: `readonly`,
Handlebars: `readonly`,
Hooks: `readonly`,
@ -73,7 +72,7 @@ export default [
"@stylistic/eol-last": `warn`,
"@stylistic/operator-linebreak": [`warn`, `before`],
"@stylistic/indent": [`warn`, `tab`],
"@stylistic/brace-style": [`off`],
"@stylistic/brace-style": [`warn`, `1tbs`, { "allowSingleLine": 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-style": [`warn`, `last`],

View file

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

View file

@ -2,13 +2,10 @@
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;
@ -19,16 +16,13 @@ Object.defineProperty(
{
value: deepFreeze({
DialogManager,
QueryManager,
Apps: {
Ask,
AttributeManager,
PlayerSheet,
QueryStatus,
},
utils: {
attributeSorter,
localizer,
toID,
},
}),

View file

@ -12,7 +12,6 @@ const validInputTypes = [
];
export class Ask extends HandlebarsApplicationMixin(ApplicationV2) {
// #region Options
static DEFAULT_OPTIONS = {
tag: `dialog`,
classes: [
@ -48,9 +47,7 @@ export class Ask extends HandlebarsApplicationMixin(ApplicationV2) {
template: filePath(`templates/Ask/controls.hbs`),
},
};
// #endregion Options
// #region Instance
_inputs = [];
alwaysUseAnswerObject = false;
@ -91,7 +88,6 @@ export class Ask extends HandlebarsApplicationMixin(ApplicationV2) {
this._userOnConfirm = onConfirm;
this._userOnClose = onClose;
};
// #endregion Instance
// #region Lifecycle
async _onFirstRender() {

View file

@ -1,7 +1,7 @@
import { __ID__, filePath } from "../consts.mjs";
import { AttributeManager } from "./AttributeManager.mjs";
import { attributeSorter } from "../utils/attributeSort.mjs";
import { TAFDocumentSheetConfig } from "./TAFDocumentSheetConfig.mjs";
import { ResizeControlManager } from "./ResizeControlManager.mjs";
const { HandlebarsApplicationMixin } = foundry.applications.api;
const { ActorSheetV2 } = foundry.applications.sheets;
@ -28,7 +28,7 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
},
actions: {
manageAttributes: this.#manageAttributes,
configureSheet: this.#configureSheet,
sizeSettings: this.#configureSizeSettings,
},
};
@ -78,6 +78,15 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
return isGM || (allowPlayerEdits && editable);
},
});
controls.push({
icon: `fa-solid fa-crop-simple`,
label: `Configure Size`,
action: `sizeSettings`,
visible: () => {
const isGM = game.user.isGM;
return isGM;
},
});
return controls;
};
@ -147,18 +156,15 @@ export class PlayerSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
};
};
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 });
#sizeSettings = null;
/** @this {PlayerSheet} */
static async #configureSizeSettings() {
this.#sizeSettings ??= new ResizeControlManager({ document: this.actor });
if (this.#sizeSettings.rendered) {
await this.#sizeSettings.bringToFront();
} else {
await this.#sizeSettings.render({ force: true });
};
};
// #endregion Actions
};

View file

@ -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
};

View file

@ -0,0 +1,61 @@
import { __ID__, filePath } from "../consts.mjs";
const { HandlebarsApplicationMixin, DocumentSheetV2 } = foundry.applications.api;
const { getProperty } = foundry.utils;
export class ResizeControlManager extends HandlebarsApplicationMixin(DocumentSheetV2) {
// #region Options
static DEFAULT_OPTIONS = {
classes: [
__ID__,
`ResizeControlManager`,
],
position: {
width: 400,
height: `auto`,
},
window: {
resizable: true,
},
form: {
submitOnChange: false,
closeOnSubmit: true,
},
actions: {},
};
static PARTS = {
settings: { template: filePath(`templates/ResizeControlManager/settings.hbs`) },
controls: { template: filePath(`templates/ResizeControlManager/controls.hbs`) },
};
// #endregion Options
// #region Instance Data
get title() {
return `Sizing Settings For : ${this.document.name}`;
};
// #endregion Instance Data
// #region Data Prep
async _prepareContext() {
const sizing = getProperty(this.document, `flags.${__ID__}.PlayerSheet.size`) ?? {};
const ctx = {
meta: {
idp: this.id,
},
width: sizing.width,
height: sizing.height,
resizable: sizing.resizable,
resizeOptions: [
{ label: `Default`, value: `` },
{ label: `Resizable`, value: `true` },
{ label: `No Resizing`, value: `false` },
],
};
return ctx;
};
// #endregion Data Prep
};

View file

@ -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
};

View file

@ -17,7 +17,6 @@ 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`);
@ -42,7 +41,6 @@ Hooks.on(`init`, () => {
registerWorldSettings();
registerSockets();
registerCustomComponents();
Handlebars.registerHelper(helpers);
});

View file

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

View file

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

View file

@ -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);
});
};

View file

@ -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);
};

View file

@ -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);
};

View file

@ -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,
},
});
};
};

View file

@ -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);
};

View file

@ -1,18 +1,17 @@
import { Ask } from "../apps/Ask.mjs";
/** @type {Map<string, Promise>} */
const promises = new Map();
export class DialogManager {
/** @type {Map<string, Promise>} */
static #promises = new Map();
static #dialogs = new Map();
/** @type {Map<string, ApplicationV2>} */
const dialogs = new Map();
static async close(id) {
this.#dialogs.get(id)?.close();
this.#dialogs.delete(id);
this.#promises.delete(id);
};
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
@ -23,13 +22,13 @@ export function close(id) {
* @param {AskOptions} opts
* @returns {AskResult}
*/
export async function ask(
static async ask(
data,
{
onlyOneWaiting = true,
alwaysUseAnswerObject = true,
} = {},
) {
) {
if (!data.id) {
return {
state: `errored`,
@ -45,13 +44,13 @@ export async function ask(
const id = data.id;
// Don't do multi-thread waiting
if (dialogs.has(id)) {
const app = dialogs.get(id);
if (this.#dialogs.has(id)) {
const app = this.#dialogs.get(id);
app.bringToFront();
if (onlyOneWaiting) {
return { state: `fronted` };
} else {
return promises.get(id);
return this.#promises.get(id);
};
};
@ -89,26 +88,21 @@ export async function ask(
...data,
alwaysUseAnswerObject,
onClose: () => {
dialogs.delete(id);
promises.delete(id);
this.#dialogs.delete(id);
this.#promises.delete(id);
resolve({ state: `prompted` });
},
onConfirm: (answers) => resolve({ state: `prompted`, answers }),
});
app.render({ force: true });
dialogs.set(id, app);
this.#dialogs.set(id, app);
});
promises.set(id, promise);
this.#promises.set(id, promise);
return promise;
};
};
export function size() {
return dialogs.size;
};
export const DialogManager = {
close,
ask,
size,
static get size() {
return this.#dialogs.size;
};
};

View file

@ -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,
};

View file

@ -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;
};

View file

@ -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);
},
);
};

5894
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,16 @@
{
"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"
"globals": "^15.9.0",
"sass": "^1.77.8"
},
"scripts": {
"data:build": "node scripts/buildCompendia.mjs",
"data:extract": "node scripts/extractCompendia.mjs",
"link": "node scripts/linkFoundry.mjs",
"css": "sass --watch --embed-source-map --no-error-css styles/:.styles/",
"build": "sass --embed-source-map --no-error-css styles/:.styles/",
"lint": "eslint --fix",
"lint:nofix": "eslint"
}

View file

@ -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();

View file

@ -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);
};

View file

@ -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`);

View file

@ -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();

View file

@ -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();

View file

@ -23,6 +23,10 @@
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
button {
flex-grow: 1;
}
}
label {

View file

@ -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;
}
}

View file

@ -0,0 +1,20 @@
.taf.ResizeControlManager {
fieldset {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
align-items: center;
gap: 8px;
border: 1px solid rebeccapurple;
border-radius: 4px;
}
.controls {
display: flex;
flex-direction: row;
gap: 8px;
button {
flex-grow: 1;
}
}
}

View file

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

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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%;
}
}
}

View file

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

View file

@ -3,27 +3,21 @@
/* 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);
@import url("./Apps/ResizeControlManager.css") layer(apps);

View file

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

View file

@ -1,13 +1,3 @@
.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,13 +1,3 @@
.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,10 +2,10 @@
"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.4.0",
"download": "",
"manifest": "",
"url": "https://git.varify.ca/Foundry/taf",
"version": "2.3.0",
"download": "#{DOWNLOAD}#",
"manifest": "https://github.com/Eldritch-Oliver/Text-Actors-Foundry/releases/latest/download/system.json",
"url": "https://github.com/Eldritch-Oliver/Text-Actors-Foundry",
"compatibility": {
"minimum": 13,
"verified": 13,
@ -41,11 +41,10 @@
},
"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

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

@ -1,46 +0,0 @@
<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,7 @@
<div class="controls">
<button
type="submit"
>
Save and Close
</button>
</div>

View file

@ -0,0 +1,29 @@
<div class="settings">
<p>
Changes to these settings will only take effect after a reload of Foundry.
</p>
<fieldset>
<legend>Sizing</legend>
<label for="{{ meta.idp }}-width">Width</label>
<input
type="number"
id="{{ meta.idp }}-width"
value="{{ width }}"
name="flags.taf.PlayerSheet.size.width"
>
<label for="{{ meta.idp }}-height">Height</label>
<input
type="number"
id="{{ meta.idp }}-height"
value="{{ height }}"
name="flags.taf.PlayerSheet.size.height"
>
<label for="{{ meta.idp }}-resizable">Resizable?</label>
<select
id="{{ meta.idp }}-resizable"
name="flags.taf.PlayerSheet.size.resizable"
>
{{ taf-options resizable resizeOptions }}
</select>
</fieldset>
</div>

View file

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

View file

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

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