diff --git a/.env.template b/.env.template
deleted file mode 100644
index 180dbd6..0000000
--- a/.env.template
+++ /dev/null
@@ -1,2 +0,0 @@
-# The absolute path to the Foundry installation to create symlinks to
-FOUNDRY_ROOT=""
diff --git a/.github/workflows/draft-release.yaml b/.github/workflows/draft-release.yaml
index d38d6ab..de3b830 100644
--- a/.github/workflows/draft-release.yaml
+++ b/.github/workflows/draft-release.yaml
@@ -50,13 +50,5 @@ jobs:
tag: "v${{ steps.version.outputs.version }}"
commit: ${{ github.ref }}
draft: true
- body: >
- |
- |
- | ### Changes:
- | -
- |
- | This version can be installed using this manifest URL: https://github.com/Oliver-Akins/Foundry-Stat-Tracker/releases/download/v${{ steps.version.outputs.version }}/module.json
generateReleaseNotes: true
artifacts: "prod.dist/release.zip,prod.dist/module.json"
- artifactsErrorsFailBuild: true
diff --git a/.gitignore b/.gitignore
index ff8974d..8878028 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,8 +17,6 @@ lerna-debug.log*
node_modules
/*.dist
*.local
-.env
-/foundry
# Editor directories and files
.vscode/*
diff --git a/.promo/imgs/example-dice-stat.png b/.promo/imgs/example-dice-stat.png
deleted file mode 100644
index 63cb502..0000000
Binary files a/.promo/imgs/example-dice-stat.png and /dev/null differ
diff --git a/.promo/imgs/example-string-stat.png b/.promo/imgs/example-string-stat.png
deleted file mode 100644
index fa884dc..0000000
Binary files a/.promo/imgs/example-string-stat.png and /dev/null differ
diff --git a/.promo/imgs/example-table-config.png b/.promo/imgs/example-table-config.png
deleted file mode 100644
index a86cdf5..0000000
Binary files a/.promo/imgs/example-table-config.png and /dev/null differ
diff --git a/README.md b/README.md
index 61e924e..ccf8f72 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,2 @@
-# Stats Tracker
-This FoundryVTT module aims to provide a clean way of keeping track of any data
-points you want within Foundry, whether that be dice rolls, or other things like
-how many natural 1s to natural 20s you get.
-
-I was inspired by the dicestats module, however it only allows tracking dice
-statistics, which is something I found myself needing to work around and struggle
-against, so I decided to make this module to fill that gap while improving upon
-the graph rendering.
-
-For more information on how to use this module, check out the "Documentation"
-compendium within your world!
-
-## Installation
-
-

-

-
-
-You can find a history of all releases on the [Foundry package listing](https://foundryvtt.com/packages/stat-tracker)
-or in the [GitHub releases tab](https://github.com/Oliver-Akins/Foundry-Stat-Tracker/releases).
-Prereleases will only be released to the GitHub page, so if you want to check
-out those releases, you'll need to use those manifest links directly.
-
-## Bugs or Feature Requests
-Bugs and Feature Requests can be submitted via the [GitHub issues](https://github.com/Oliver-Akins/Foundry-Stat-Tracker/issues/new/choose).
-Planned features can also be seen in the GitHub issues list.
-
-## Contribution
-Contribution guidelines / requirements coming soon.
+# Foundry-Stat-Tracker
+A Foundry module that allows tracking arbitrary stats.
diff --git a/augments.d.ts b/augments.d.ts
deleted file mode 100644
index a08bb60..0000000
--- a/augments.d.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-declare global {
- class Hooks extends foundry.helpers.Hooks {};
- const fromUuid = foundry.utils.fromUuid;
-}
-
-interface Actor {
- /** The system-specific data */
- system: any;
-};
-
-interface Item {
- /** The system-specific data */
- system: any;
-};
diff --git a/jsconfig.json b/jsconfig.json
index 27f6e7c..8b97154 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -1,22 +1,10 @@
{
"compilerOptions": {
"module": "ES2020",
- "target": "ES2020",
- "types": [
- "./augments.d.ts"
- ],
- "paths": {
- "@client/*": ["./foundry/client/*"],
- "@common/*": ["./foundry/common/*"],
- }
+ "target": "ES2020"
},
"exclude": ["node_modules", "**/node_modules/*"],
- "include": [
- "module/**/*",
- "foundry/client/client.mjs",
- "foundry/client/global.d.mts",
- "foundry/common/primitives/global.d.mts"
- ],
+ "include": ["module/**/*"],
"typeAcquisition": {
"include": ["joi"]
}
diff --git a/module/Apps/StatsViewer.mjs b/module/Apps/StatsViewer.mjs
index 152be1c..57a1cd8 100644
--- a/module/Apps/StatsViewer.mjs
+++ b/module/Apps/StatsViewer.mjs
@@ -21,10 +21,10 @@ export class StatsViewer extends HandlebarsApplicationMixin(ApplicationV2) {
resizable: true,
minimizable: true,
controls: [
- // {
- // label: `Add All Users To Graph`,
- // action: `addAllUsers`,
- // },
+ {
+ label: `Add All Users To Graph`,
+ action: `addAllUsers`,
+ },
],
},
position: {
diff --git a/module/Apps/TableCreator.mjs b/module/Apps/TableCreator.mjs
index 1422fbf..5d9f511 100644
--- a/module/Apps/TableCreator.mjs
+++ b/module/Apps/TableCreator.mjs
@@ -75,7 +75,7 @@ export class TableCreator extends HandlebarsApplicationMixin(ApplicationV2) {
if (this._name.startsWith(`Dice`)) {
ctx.createButtonDisabled = !this._name.match(diceNamespacePattern);
ctx.typeDisabled = true;
- ctx.type = BucketTypes.NUMBER;
+ ctx.type = BucketTypes.RANGE;
this.#diceNamespaceAlert ??= ui.notifications.info(
`Tables in the "Dice" namespace must be formatted as "Dice/dX" where X is the number of sides on the die and are restricted to be ranges 1 to X.`,
{ permanent: true },
@@ -104,11 +104,11 @@ export class TableCreator extends HandlebarsApplicationMixin(ApplicationV2) {
return;
};
+ Logger.log(`updating ${binding} value to ${target.value}`);
this[binding] = target.value;
this.render();
};
- /** @this {TableCreator} */
static async #createTable() {
/** @type {string} */
const name = this._name;
@@ -122,38 +122,25 @@ export class TableCreator extends HandlebarsApplicationMixin(ApplicationV2) {
return;
};
- let created = false;
if (name.startsWith(`Dice`)) {
if (!name.match(diceNamespacePattern)) {
ui.notifications.error(`Table name doesn't conform to the "Dice/dX" format required by the Dice namespace.`);
return;
};
const size = Number(name.replace(`Dice/d`, ``));
- created = await CONFIG.stats.db.createTable(createDiceTable(size));
- if (created) {
- this.close();
- ui.notifications.remove(this.#diceNamespaceAlert);
- this.#diceNamespaceAlert = null;
- };
- } else {
- created = await CONFIG.stats.db.createTable({
- name,
- buckets: {
- type: this._type,
- },
- graph: {
- type: `bar`,
- stacked: true,
- },
- });
- }
-
- if (created) {
- this.close();
- if (this.#diceNamespaceAlert) {
- ui.notifications.remove(this.#diceNamespaceAlert);
- this.#diceNamespaceAlert = null;
- };
+ await CONFIG.stats.db.createTable(createDiceTable(size));
+ return;
};
+
+ await CONFIG.stats.db.createTable({
+ name,
+ buckets: {
+ type: this._type,
+ },
+ graph: {
+ type: `bar`,
+ stacked: true,
+ },
+ });
};
};
diff --git a/module/Apps/TableManager.mjs b/module/Apps/TableManager.mjs
index f413805..2bd2e82 100644
--- a/module/Apps/TableManager.mjs
+++ b/module/Apps/TableManager.mjs
@@ -210,7 +210,7 @@ export class TableManager extends HandlebarsApplicationMixin(ApplicationV2) {
};
async _prepareStringContext(ctx, table) {
- ctx.buckets.choices = [...(table.buckets.choices ?? [])];
+ ctx.buckets.choices = [...table.buckets.choices];
};
// #endregion Data Prep
@@ -253,6 +253,7 @@ export class TableManager extends HandlebarsApplicationMixin(ApplicationV2) {
*/
static async #deleteTable() {
const table = await CONFIG.stats.db.getTable(this.activeTableID);
+ Logger.debug({ table });
if (!table) {
ui.notifications.error(
`You must select a table before you can delete it`,
diff --git a/module/__tests__/README.md b/module/__tests__/README.md
deleted file mode 100644
index e90289f..0000000
--- a/module/__tests__/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-## Testing
-
-The stat-tracker module utilizes [quench](https://foundryvtt.com/packages/quench)
-for it's end-to-end tests and unit tests, enabling us to be sure that the module
-is as stable as possible and detect when there are breaking changes.
diff --git a/module/__tests__/registration.mjs b/module/__tests__/registration.mjs
deleted file mode 100644
index 0d45207..0000000
--- a/module/__tests__/registration.mjs
+++ /dev/null
@@ -1,13 +0,0 @@
-import { barGraphTests } from "./schemas/barGraph.test.mjs";
-import { numberBucketTests } from "./schemas/numberBucket.test.mjs";
-import { rowTests } from "./schemas/row.test.mjs";
-import { stringBucketTests } from "./schemas/stringBucket.test.mjs";
-import { tableTests } from "./schemas/table.test.mjs";
-
-Hooks.on(`quenchReady`, (quench) => {
- numberBucketTests(quench);
- stringBucketTests(quench);
- barGraphTests(quench);
- tableTests(quench);
- rowTests(quench);
-});
diff --git a/module/__tests__/schemas/barGraph.test.mjs b/module/__tests__/schemas/barGraph.test.mjs
deleted file mode 100644
index a2b9fea..0000000
--- a/module/__tests__/schemas/barGraph.test.mjs
+++ /dev/null
@@ -1,51 +0,0 @@
-import { api } from "../../api.mjs";
-
-export function barGraphTests(quench) {
- quench.registerBatch(
- `${__ID__}.barGraphSchema`,
- (ctx) => {
- const { describe, it, expect } = ctx;
-
- describe(`the bar graph schema`, () => {
- it(`should default any additional properties left out`, () => {
- const { value, error } = api.schemas.graphs.bar.validate(
- { type: `bar` },
- );
- expect(value).to.have.keys(`type`, `stacked`, `showEmptyBuckets`);
- expect(error).to.be.undefined;
- });
-
- it(`should allow stacked to be provided specifically`, () => {
- const { value, error } = api.schemas.graphs.bar.validate(
- { type: `bar`, stacked: true },
- );
- expect(value).to.have.keys(`type`, `stacked`, `showEmptyBuckets`);
- expect(error).to.be.undefined;
- });
-
- it(`should allow showEmptyBuckets to be provided specifically`, () => {
- const { value, error } = api.schemas.graphs.bar.validate(
- { type: `bar`, showEmptyBuckets: true },
- );
- expect(value).to.have.keys(`type`, `stacked`, `showEmptyBuckets`);
- expect(error).to.be.undefined;
- });
-
- it(`should only allow showEmptyBuckets to be a boolean`, () => {
- const { value, error } = api.schemas.graphs.bar.validate(
- { type: `bar`, showEmptyBuckets: `a potato` },
- );
- expect(value).to.have.keys(`type`, `stacked`, `showEmptyBuckets`);
- expect(error).not.to.be.undefined;
- });
-
- it(`should only allow stacked to be a boolean`, () => {
- const { error } = api.schemas.graphs.bar.validate(
- { type: `bar`, stacked: `a potato` },
- );
- expect(error).not.to.be.undefined;
- });
- });
- },
- );
-};
diff --git a/module/__tests__/schemas/numberBucket.test.mjs b/module/__tests__/schemas/numberBucket.test.mjs
deleted file mode 100644
index 2ebba9a..0000000
--- a/module/__tests__/schemas/numberBucket.test.mjs
+++ /dev/null
@@ -1,61 +0,0 @@
-import { api } from "../../api.mjs";
-
-export function numberBucketTests(quench) {
- quench.registerBatch(
- `${__ID__}.numberBucketSchema`,
- (ctx) => {
- const { describe, it, expect } = ctx;
-
- describe(`the number bucket schema`, () => {
- it(`should allow all additional properties to be left out`, () => {
- const { error } = api.schemas.buckets.number.validate(
- { type: `number` },
- );
- expect(error).to.be.undefined;
- });
-
- it(`should allow the min additional property if only it is provided with the type`, () => {
- const { error } = api.schemas.buckets.number.validate(
- { type: `number`, min: 0 },
- );
- expect(error).to.be.undefined;
- });
-
- it(`should allow the max additional property if only it is provided with the type`, () => {
- const { error } = api.schemas.buckets.number.validate(
- { type: `number`, max: 10 },
- );
- expect(error).to.be.undefined;
- });
-
- it(`should not allow the step additional property if only it is provided with the type`, () => {
- const { error } = api.schemas.buckets.number.validate(
- { type: `number`, step: 1 },
- );
- expect(error).not.to.be.undefined;
- });
-
- it(`should not allow max to be less than min`, () => {
- const { error } = api.schemas.buckets.number.validate(
- { type: `number`, min: 10, max: 5 },
- );
- expect(error).not.to.be.undefined;
- });
-
- it(`should not allow max to be less than min`, () => {
- const { error } = api.schemas.buckets.number.validate(
- { type: `number`, min: 10, max: 15 },
- );
- expect(error).to.be.undefined;
- });
-
- it(`should allow step when min is also provided`, () => {
- const { error } = api.schemas.buckets.number.validate(
- { type: `number`, min: 10, step: 5 },
- );
- expect(error).to.be.undefined;
- });
- });
- },
- );
-};
diff --git a/module/__tests__/schemas/row.test.mjs b/module/__tests__/schemas/row.test.mjs
deleted file mode 100644
index 394e42e..0000000
--- a/module/__tests__/schemas/row.test.mjs
+++ /dev/null
@@ -1,96 +0,0 @@
-import { api } from "../../api.mjs";
-import { PrivacyMode } from "../../utils/privacy.mjs";
-
-export function rowTests(quench) {
- quench.registerBatch(
- `${__ID__}.rowSchema`,
- (ctx) => {
- const { describe, it, expect } = ctx;
-
- describe(`the row schema`, () => {
- it(`should allow number-based values`, () => {
- const { error } = api.schemas.row.validate(
- {
- _id: `1`,
- timestamp: (new Date()).toISOString(),
- value: 1,
- privacy: PrivacyMode.PUBLIC,
- },
- );
- expect(error).to.be.undefined;
- });
-
- it(`should allow string-based values`, () => {
- const { error } = api.schemas.row.validate(
- {
- _id: `1`,
- timestamp: (new Date()).toISOString(),
- value: `apple`,
- privacy: PrivacyMode.PUBLIC,
- },
- );
- expect(error).to.be.undefined;
- });
-
- it(`shouldn't allow invalid privacy modes`, () => {
- const { error } = api.schemas.row.validate(
- {
- _id: `1`,
- timestamp: (new Date()).toISOString(),
- value: 1,
- privacy: `yahaha`,
- },
- );
- expect(error).not.to.be.undefined;
- });
-
- it(`shouldn't allow invalid value modes`, () => {
- const { error } = api.schemas.row.validate(
- {
- _id: `1`,
- timestamp: (new Date()).toISOString(),
- value: true,
- privacy: PrivacyMode.PUBLIC,
- },
- );
- expect(error).not.to.be.undefined;
- });
-
- it(`shouldn't allow non-ISO date formats`, () => {
- const { error } = api.schemas.row.validate(
- {
- _id: `1`,
- timestamp: (new Date()).toDateString(),
- value: 1,
- privacy: PrivacyMode.PUBLIC,
- },
- );
- expect(error).not.to.be.undefined;
- });
-
- it(`should require an ID to be present`, () => {
- const { error } = api.schemas.row.validate(
- {
- timestamp: (new Date()).toISOString(),
- value: true,
- privacy: PrivacyMode.PUBLIC,
- },
- );
- expect(error).not.to.be.undefined;
- });
-
- it(`shouldn't allow empty string as a value`, () => {
- const { error } = api.schemas.row.validate(
- {
- _id: `1`,
- timestamp: (new Date()).toISOString(),
- value: ``,
- privacy: PrivacyMode.PUBLIC,
- },
- );
- expect(error).not.to.be.undefined;
- });
- });
- },
- );
-};
diff --git a/module/__tests__/schemas/stringBucket.test.mjs b/module/__tests__/schemas/stringBucket.test.mjs
deleted file mode 100644
index d992d63..0000000
--- a/module/__tests__/schemas/stringBucket.test.mjs
+++ /dev/null
@@ -1,40 +0,0 @@
-import { api } from "../../api.mjs";
-
-export function stringBucketTests(quench) {
- quench.registerBatch(
- `${__ID__}.stringBucketSchema`,
- (ctx) => {
- const { describe, it, expect } = ctx;
-
- describe(`the string bucket schema`, () => {
- it(`should allow all additional properties to be left out`, () => {
- const { error } = api.schemas.buckets.string.validate(
- { type: `string` },
- );
- expect(error).to.be.undefined;
- });
-
- it(`should allow specific choices to be provided`, () => {
- const { error } = api.schemas.buckets.string.validate(
- { type: `string`, choices: [`choice 1`, `choice 2`] },
- );
- expect(error).to.be.undefined;
- });
-
- it(`shouldn't allow specific choices to be empty`, () => {
- const { error } = api.schemas.buckets.string.validate(
- { type: `string`, choices: [] },
- );
- expect(error).not.to.be.undefined;
- });
-
- it(`should only allow specific choices to be strings`, () => {
- const { error } = api.schemas.buckets.string.validate(
- { type: `string`, choices: [`choice 1`, 5] },
- );
- expect(error).not.to.be.undefined;
- });
- });
- },
- );
-};
diff --git a/module/__tests__/schemas/table.test.mjs b/module/__tests__/schemas/table.test.mjs
deleted file mode 100644
index c3d430e..0000000
--- a/module/__tests__/schemas/table.test.mjs
+++ /dev/null
@@ -1,43 +0,0 @@
-import { api } from "../../api.mjs";
-
-const graph = { type: `bar` };
-const buckets = { type: `string` };
-
-export function tableTests(quench) {
- quench.registerBatch(
- `${__ID__}.tableSchema`,
- (ctx) => {
- const { describe, it, expect } = ctx;
-
- describe(`the table schema`, () => {
- it(`should require that name be a non-empty string`, () => {
- const { error } = api.schemas.table.validate(
- { name: ``, graph, buckets },
- );
- expect(error).not.to.be.undefined;
- });
-
- it(`should require that name only contain alphanumeric characters`, () => {
- const { error } = api.schemas.table.validate(
- { name: `:(`, graph, buckets },
- );
- expect(error).not.to.be.undefined;
- });
-
- it(`should allow the name to contain spaces`, () => {
- const { error } = api.schemas.table.validate(
- { name: `a name with spaces`, graph, buckets },
- );
- expect(error).to.be.undefined;
- });
-
- it(`should allow a single forward slash for subtables`, () => {
- const { error } = api.schemas.table.validate(
- { name: `Table/subtable`, graph, buckets },
- );
- expect(error).to.be.undefined;
- });
- });
- },
- );
-};
diff --git a/module/api.mjs b/module/api.mjs
index 274f255..0dbaf9d 100644
--- a/module/api.mjs
+++ b/module/api.mjs
@@ -11,9 +11,8 @@ import { UserFlagDatabase } from "./utils/databases/UserFlag.mjs";
// Utils
import { barGraphSchema, numberBucketSchema, rowSchema, stringBucketSchema, tableSchema } from "./utils/databases/model.mjs";
-import { determinePrivacyFromRollMode, filterPrivateRows, PrivacyMode } from "./utils/privacy.mjs";
+import { filterPrivateRows, PrivacyMode } from "./utils/privacy.mjs";
import { validateBucketConfig, validateValue } from "./utils/buckets.mjs";
-import { inferRollMode } from "./utils/inferRollMode.mjs";
const { deepFreeze } = foundry.utils;
@@ -25,8 +24,6 @@ export const api = deepFreeze({
TableManager,
},
utils: {
- determinePrivacyFromRollMode,
- inferRollMode,
filterPrivateRows,
validateValue,
validateBucketConfig,
@@ -41,6 +38,7 @@ export const api = deepFreeze({
},
schemas: {
buckets: {
+ range: numberBucketSchema,
number: numberBucketSchema,
string: stringBucketSchema,
},
diff --git a/module/hooks/createChatMessage.mjs b/module/hooks/createChatMessage.mjs
deleted file mode 100644
index e686c70..0000000
--- a/module/hooks/createChatMessage.mjs
+++ /dev/null
@@ -1,40 +0,0 @@
-import { determinePrivacyFromRollMode } from "../utils/privacy.mjs";
-import { inferRollMode } from "../utils/inferRollMode.mjs";
-
-Hooks.on(`createChatMessage`, (message, options, author) => {
- const isSelf = author === game.user.id;
- const isNew = options.action === `create`;
- const hasRolls = message.rolls?.length > 0;
- const autoTracking = game.settings.get(__ID__, `autoTrackRolls`);
- if (!isSelf || !isNew || !hasRolls || !autoTracking) { return };
-
- /** An object of dice denomination to database rows */
- const rows = {};
-
- const privacy = determinePrivacyFromRollMode(options.rollMode ?? inferRollMode(message));
-
- /*
- Goes through all of the dice within the message and grabs their result in order
- to batch-save them all to the database handler.
- */
- for (const roll of message.rolls) {
- for (const die of roll.dice) {
- const size = die.denomination;
- rows[size] ??= [];
- for (const result of die.results) {
- rows[size].push({ privacy, value: result.result });
- };
- };
- };
-
- // save all the rows, then rerender once we're properly done
- for (const denomination in rows) {
- CONFIG.stats.db.createRows(
- `Dice/${denomination}`,
- author,
- rows[denomination],
- { rerender: false },
- );
- };
- CONFIG.stats.db.render({ userUpdated: author });
-});
diff --git a/module/hooks/preCreateChatMessage.mjs b/module/hooks/preCreateChatMessage.mjs
new file mode 100644
index 0000000..05a4d41
--- /dev/null
+++ b/module/hooks/preCreateChatMessage.mjs
@@ -0,0 +1,30 @@
+import { determinePrivacyFromRollMode } from "../utils/privacy.mjs";
+
+Hooks.on(`preCreateChatMessage`, (_message, context, options, author) => {
+ const isNew = options.action === `create`;
+ const hasRolls = context.rolls?.length > 0;
+ const autoTracking = game.settings.get(__ID__, `autoTrackRolls`);
+ if (!isNew || !hasRolls || !autoTracking) { return };
+
+ /** An object of dice denomination to rows to add */
+ const rows = {};
+
+ const privacy = determinePrivacyFromRollMode(options.rollMode);
+ for (const roll of context.rolls) {
+ for (const die of roll.dice) {
+ const size = die.denomination;
+ rows[size] ??= [];
+ for (const result of die.results) {
+ rows[size].push({ privacy, value: result.result });
+ };
+ };
+ };
+
+ for (const denomination in rows) {
+ CONFIG.stats.db.createRows(
+ `Dice/${denomination}`,
+ author,
+ rows[denomination],
+ );
+ };
+});
diff --git a/module/hooks/ready.mjs b/module/hooks/ready.mjs
index 025ed1b..33fe483 100644
--- a/module/hooks/ready.mjs
+++ b/module/hooks/ready.mjs
@@ -29,10 +29,10 @@ Hooks.on(`ready`, () => {
);
// Fire and forget
- CONFIG.stats.db.migrateData(lastVersion, notif)
+ CONFIG.stats.db.migrateData(notif)
.then(() => {
game.settings.set(__ID__, `lastVersion`, __VERSION__);
- setTimeout(() => ui.notifications.remove(notif), 5_000);
+ setTimeout(() => ui.notifications.remove(notif), 500);
});
} else {
ui.notifications.error(
diff --git a/module/main.mjs b/module/main.mjs
index 4a90d3f..b3a76f4 100644
--- a/module/main.mjs
+++ b/module/main.mjs
@@ -5,9 +5,4 @@ import "./hooks/init.mjs";
import "./hooks/ready.mjs";
// Document Hooks
-import "./hooks/createChatMessage.mjs";
-
-// Dev Only imports
-if (import.meta.env.DEV) {
- import(`./__tests__/registration.mjs`);
-};
+import "./hooks/preCreateChatMessage.mjs";
diff --git a/module/utils/buckets.mjs b/module/utils/buckets.mjs
index 53c78f6..c689445 100644
--- a/module/utils/buckets.mjs
+++ b/module/utils/buckets.mjs
@@ -6,6 +6,7 @@ const { StringField, NumberField } = foundry.data.fields;
export const BucketTypes = {
STRING: `string`,
NUMBER: `number`,
+ RANGE: `range`,
};
/**
@@ -51,7 +52,8 @@ export function validateBucketConfig(config) {
const validator = validators[conf.type];
if (validator == null) {
- throw new Error(`Failed to find type validator for: ${conf.type}`);
+ Logger.error(`Failed to find type validator for: ${conf.type}`);
+ return false;
};
// Disallow function choices if present
@@ -60,7 +62,7 @@ export function validateBucketConfig(config) {
delete conf.choices;
};
- validator.validateConfig?.(conf);
+ validator.validateConfig(conf);
return conf;
};
@@ -73,7 +75,7 @@ const validators = {
opts.trim = true;
opts.blank = false;
},
- transformConfig: (config) => {
+ validateConfig: (config) => {
if (config.choices.length === 0) {
delete config.choices;
config[`-=choices`] = null;
@@ -83,6 +85,35 @@ const validators = {
[BucketTypes.NUMBER]: {
field: NumberField,
transformOptions: transformNumberFieldOptions,
+ validateConfig: (config) => {
+ if (config.step != null && config.min == null) {
+ delete config.step;
+ config[`-=step`] = null;
+ };
+ if (
+ config.min != null
+ && config.max != null
+ && config.min > config.max
+ ) {
+ throw new Error(`"min" must be less than "max"`);
+ }
+ },
+ },
+ [BucketTypes.RANGE]: {
+ field: NumberField,
+ transformOptions: transformNumberFieldOptions,
+ validateConfig: (config) => {
+ if (config.min == null) {
+ throw new Error(`"min" must be defined for range buckets`);
+ };
+ if (config.max == null) {
+ throw new Error(`"max" must be defined for range buckets`);
+ };
+ if (config.min > config.max) {
+ throw new Error(`"min" must be less than "max"`);
+ }
+ config.step ??= 1;
+ },
},
};
diff --git a/module/utils/databases/Database.mjs b/module/utils/databases/Database.mjs
index 9cd4be1..1a55f62 100644
--- a/module/utils/databases/Database.mjs
+++ b/module/utils/databases/Database.mjs
@@ -28,7 +28,7 @@ Default Subtables:
tables that are parents to other tables.
*/
-const { deleteProperty, diffObject, expandObject, isNewerVersion, mergeObject } = foundry.utils;
+const { deleteProperty, diffObject, expandObject, mergeObject } = foundry.utils;
/**
* The generic Database implementation, any subclasses should implement all of
@@ -131,8 +131,8 @@ export class Database {
return false;
};
- const table = await this.getTable(tableID);
- if (!table) {
+ const table = this.getTable(tableID);
+ if (!tables[tableID]) {
ui.notifications.error(`Cannot update table that doesn't exist`);
return false;
};
@@ -144,7 +144,7 @@ export class Database {
const diff = diffObject(
table,
expandObject(changes),
- { deletionKeys: true },
+ { inner: true, deletionKeys: true },
);
if (Object.keys(diff).length === 0) { return false };
@@ -275,7 +275,7 @@ export class Database {
* @returns {boolean}
*/
static requiresMigrationFrom(lastVersion) {
- return isNewerVersion(__VERSION__, lastVersion);
+ return foundry.utils.isNewerVersion(__VERSION__, lastVersion);
};
/**
@@ -289,27 +289,7 @@ export class Database {
* @param {Notification} notif The progress bar notification used for
* user feedback while performing migrations.
*/
- static async migrateData(lastVersion, notif) {
- const totalSteps = 1;
-
- /*
- This migration is for going up to 1.0.3, getting rid of any tables that have
- a bucket type of range, since those were not supported within the initial
- release, but could still accidentally be created by users.
- */
- if (isNewerVersion(`1.0.3`, lastVersion)) {
- Logger.log(`Migrating up to the v1.0.3 data structure`);
- const tables = game.settings.get(__ID__, `tables`);
- for (const table of Object.values(tables)) {
- if (table.buckets.type !== `range`) { continue };
- table.buckets.type = BucketTypes.NUMBER;
- table.buckets.showEmptyBuckets = true;
- };
- await game.settings.set(__ID__, `tables`, tables);
- notif.update({ pct: notif.pct + (1 / totalSteps) });
- };
-
- };
+ static async migrateData(lastVersion, notif) {};
};
/* eslint-enable no-unused-vars */
diff --git a/module/utils/databases/Memory.mjs b/module/utils/databases/Memory.mjs
index b32555a..8dba510 100644
--- a/module/utils/databases/Memory.mjs
+++ b/module/utils/databases/Memory.mjs
@@ -1,6 +1,7 @@
import { filterPrivateRows, PrivacyMode } from "../privacy.mjs";
import { createDiceTable } from "./utils.mjs";
import { Database } from "./Database.mjs";
+import { Logger } from "../Logger.mjs";
import { validateBucketConfig } from "../buckets.mjs";
const { deleteProperty, diffObject, expandObject, mergeObject, randomID } = foundry.utils;
@@ -129,6 +130,7 @@ export class MemoryDatabase extends Database {
row._id ||= randomID();
row.timestamp = new Date().toISOString();
+ Logger.debug(`Adding row:`, row);
this.#rows[userID][table].push(row);
if (rerender) {
this.render({ userUpdated: userID });
diff --git a/module/utils/databases/UserFlag.mjs b/module/utils/databases/UserFlag.mjs
index c5292cd..7072c5c 100644
--- a/module/utils/databases/UserFlag.mjs
+++ b/module/utils/databases/UserFlag.mjs
@@ -2,7 +2,6 @@ import { filterPrivateRows, PrivacyMode } from "../privacy.mjs";
import { Database } from "./Database.mjs";
import { Logger } from "../Logger.mjs";
import { rowSchema } from "./model.mjs";
-import { validateValue } from "../buckets.mjs";
const { hasProperty, mergeObject, randomID } = foundry.utils;
@@ -28,7 +27,7 @@ export class UserFlagDatabase extends Database {
return false;
};
- const userData = user.getFlag(__ID__, dataFlag) ?? {};
+ const userData = user.getFlag(__ID__, dataFlag);
userData[tableID] ??= [];
userData[tableID].push(corrected);
await user.setFlag(__ID__, dataFlag, userData);
@@ -47,9 +46,6 @@ export class UserFlagDatabase extends Database {
const userData = user.getFlag(__ID__, dataFlag) ?? {};
userData[tableID] ??= [];
- let valueErrorPosted = false;
- let validationErrorPosted = false;
-
for (const row of rows) {
row._id = randomID();
row.timestamp = new Date().toISOString();
@@ -59,31 +55,12 @@ export class UserFlagDatabase extends Database {
{ abortEarly: false, convert: true, dateFormat: `iso`, render: false },
);
if (error) {
- if (!validationErrorPosted) {
- ui.notifications.error(
- `One or more rows being created did not conform to required schema, skipping row and see console for more information.`,
- { console: false },
- );
- validationErrorPosted = true;
- };
+ ui.notifications.error(`A row being created did not conform to required schema, see console for more information.`, { console: false });
Logger.error(`Failing row:`, row);
Logger.error(error);
continue;
};
- const validValue = validateValue(corrected.value, table.buckets);
- if (!validValue) {
- if (!valueErrorPosted) {
- ui.notifications.warn(
- `One or more rows being created did not contain a valid value, skipping row and see console for more information.`,
- { console: false },
- );
- valueErrorPosted = true;
- };
- Logger.warn(`Row with invalid value:`, row);
- continue;
- };
-
userData[tableID].push(corrected);
};
@@ -123,36 +100,22 @@ export class UserFlagDatabase extends Database {
const table = await this.getTable(tableID);
if (!table) {
Logger.error(`Cannot find the table with ID "${tableID}"`);
- return false;
+ return;
};
const user = game.users.get(userID);
if (!user) {
Logger.error(`Can't find the user with ID "${tableID}"`);
- return false;
+ return;
};
const userData = user.getFlag(__ID__, dataFlag) ?? {};
let row = userData[tableID]?.find(row => row._id === rowID);
- if (!row) { return false };
-
- if (hasProperty(changes, `value`)) {
- const validValue = validateValue(changes.value, table.buckets);
- if (!validValue) {
- ui.notifications.warn(
- `One or more rows being created did not contain a valid value, skipping row and see console for more information.`,
- { console: false },
- );
- Logger.warn(`Row with invalid value:`, row);
- return false;
- };
- };
-
+ if (!row) { return };
mergeObject(row, changes);
await user.setFlag(__ID__, dataFlag, userData);
this.render({ userUpdated: userID });
this.triggerListeners();
- return true;
};
static async deleteRow(tableID, userID, rowID) {
@@ -183,6 +146,7 @@ export class UserFlagDatabase extends Database {
if (this.#listener !== null) { return };
this.#listener = Hooks.on(`updateUser`, (doc, diff, options, userID) => {
+ Logger.debug({ diff, userID, doc });
// Shortcircuit when on the client that triggered the update
if (userID === game.user.id) { return };
if (!hasProperty(diff, `flags.${__ID__}.${dataFlag}`)) { return };
diff --git a/module/utils/databases/model.mjs b/module/utils/databases/model.mjs
index ed83e23..0461055 100644
--- a/module/utils/databases/model.mjs
+++ b/module/utils/databases/model.mjs
@@ -3,25 +3,31 @@ import { PrivacyMode } from "../privacy.mjs";
// MARK: Buckets
export const numberBucketSchema = Joi.object({
- type: Joi.string().valid(`number`).required(),
+ type: Joi.string().valid(`number`, `range`).required(),
min: Joi
.number()
.integer()
- .when(`step`, {
- is: Joi.exist(),
+ .when(`type`, {
+ is: Joi.string().valid(`range`),
then: Joi.required(),
+ otherwise: Joi.optional(),
}),
max: Joi
.number()
.integer()
- .when(`min`, {
- is: Joi.exist(),
- then: Joi.number().greater(Joi.ref(`min`)),
+ .when(`type`, {
+ is: Joi.string().valid(`range`),
+ then: Joi.required(),
+ otherwise: Joi.optional(),
}),
step: Joi
.number()
.integer()
- .min(1),
+ .when(`type`, {
+ is: Joi.string().valid(`range`),
+ then: Joi.required(),
+ otherwise: Joi.optional(),
+ }),
});
export const stringBucketSchema = Joi.object({
@@ -31,15 +37,16 @@ export const stringBucketSchema = Joi.object({
.items(
Joi.string().trim().invalid(``),
)
- .min(1)
.optional(),
});
// MARK: Graphs
export const barGraphSchema = Joi.object({
type: Joi.string().valid(`bar`).required(),
- stacked: Joi.boolean().optional().default(true),
- showEmptyBuckets: Joi.boolean().optional().default(false),
+ stacked: Joi
+ .boolean()
+ .default(true)
+ .optional(),
});
// MARK: Table
@@ -52,34 +59,18 @@ export const tableSchema = Joi.object({
.pattern(/^[0-9a-z \-_]+(\/[0-9a-z \-_]+)?$/i),
buckets: Joi
.alternatives()
- .conditional(
- `/buckets.type`,
- {
- switch: [
- {
- is: `number`,
- then: numberBucketSchema,
- },
- {
- is: `string`,
- then: stringBucketSchema,
- },
- ],
- otherwise: Joi.forbidden(),
- },
+ .try(
+ numberBucketSchema,
+ stringBucketSchema,
)
+ .match(`one`)
.required(),
graph: Joi
.alternatives()
- .conditional(
- `/graph.type`,
- {
- switch: [
- { is: `bar`, then: barGraphSchema },
- ],
- otherwise: Joi.forbidden(),
- },
+ .try(
+ barGraphSchema,
)
+ .match(`one`)
.required(),
});
diff --git a/module/utils/databases/utils.mjs b/module/utils/databases/utils.mjs
index 58c3907..ba4cf69 100644
--- a/module/utils/databases/utils.mjs
+++ b/module/utils/databases/utils.mjs
@@ -2,7 +2,7 @@ export function createDiceTable(size) {
return {
name: `Dice/d${size}`,
buckets: {
- type: `number`,
+ type: `range`,
min: 1,
max: size,
step: 1,
@@ -10,7 +10,6 @@ export function createDiceTable(size) {
graph: {
type: `bar`,
stacked: true,
- showEmptyBuckets: true,
},
};
};
diff --git a/module/utils/inferRollMode.mjs b/module/utils/inferRollMode.mjs
deleted file mode 100644
index 4880c21..0000000
--- a/module/utils/inferRollMode.mjs
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * A helper function to try and infer what roll mode was used when creating a
- * chat message in case the roll mode was not provided during the createChatMessage
- * hook for whatever reason.
- *
- * **Disclaimer**: This inference is not totally correct. Particularly when inferring
- * a GM's message, as it won't be able to distinguish between a self-roll and a
- * private GM roll when it's
- *
- * @param {ChatMessage} message The ChatMessage document to infer from
- * @returns The Foundry-specified roll mode
- */
-export function inferRollMode(message) {
- const whisperCount = message.whisper.length;
- if (whisperCount === 0) {
- return CONST.DICE_ROLL_MODES.PUBLIC;
- };
-
- if (whisperCount === 1 && message.whisper[0] === game.user.id) {
- return CONST.DICE_ROLL_MODES.SELF;
- };
-
- let allGMs = true;
- for (const userID of message.whisper) {
- const user = game.users.get(userID);
- if (!user) { continue };
- allGMs &&= user.isGM;
- };
-
- if (!allGMs) {
- return CONST.DICE_ROLL_MODES.PUBLIC;
- };
- return message.blind
- ? CONST.DICE_ROLL_MODES.BLIND
- : CONST.DICE_ROLL_MODES.PRIVATE;
-};
diff --git a/module/utils/privacy.mjs b/module/utils/privacy.mjs
index b437880..20ed3ff 100644
--- a/module/utils/privacy.mjs
+++ b/module/utils/privacy.mjs
@@ -11,8 +11,6 @@ export const PrivacyMode = Object.freeze({
export function determinePrivacyFromRollMode(rollMode) {
switch (rollMode) {
- case CONST.DICE_ROLL_MODES.PUBLIC:
- return PrivacyMode.PUBLIC;
case CONST.DICE_ROLL_MODES.BLIND:
return PrivacyMode.GM;
case CONST.DICE_ROLL_MODES.PRIVATE:
@@ -20,7 +18,7 @@ export function determinePrivacyFromRollMode(rollMode) {
case CONST.DICE_ROLL_MODES.SELF:
return PrivacyMode.SELF;
}
- return PrivacyMode.SELF;
+ return PrivacyMode.PUBLIC;
};
/**
@@ -33,6 +31,7 @@ export function determinePrivacyFromRollMode(rollMode) {
* @returns The filtered rows
*/
export function filterPrivateRows(rows, userID, privacies) {
+ console.log({rows, userID, privacies});
const filtered = [];
const isMe = userID === game.user.id;
diff --git a/package-lock.json b/package-lock.json
index 6594a26..7664dfa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,11 +13,10 @@
"devDependencies": {
"@foundryvtt/foundryvtt-cli": "^1.1.0",
"@stylistic/eslint-plugin": "^4.2.0",
- "dotenv": "^17.2.2",
"eslint": "^9.25.0",
"glob": "^11.0.1",
"terser": "^5.39.0",
- "vite": "^6.3.4"
+ "vite": "^6.3.1"
}
},
"node_modules/@esbuild/aix-ppc64": {
@@ -1681,19 +1680,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/dotenv": {
- "version": "17.2.2",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
- "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2045,14 +2031,10 @@
}
},
"node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
+ "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
"dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -2970,11 +2952,10 @@
"dev": true
},
"node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=12"
},
@@ -3393,14 +3374,13 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
+ "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3"
+ "fdir": "^6.4.3",
+ "picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
@@ -3482,18 +3462,17 @@
}
},
"node_modules/vite": {
- "version": "6.3.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
- "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz",
+ "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
- "fdir": "^6.4.4",
+ "fdir": "^6.4.3",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
- "tinyglobby": "^0.2.13"
+ "tinyglobby": "^0.2.12"
},
"bin": {
"vite": "bin/vite.js"
diff --git a/package.json b/package.json
index ad4b381..441ebfc 100644
--- a/package.json
+++ b/package.json
@@ -7,17 +7,17 @@
"lint:nofix": "eslint",
"dev": "NODE_ENV=development vite build --mode dev --watch",
"dev:once": "NODE_ENV=development vite build --mode dev",
- "staging": "NODE_ENV=staging vite build --mode staging",
+ "staging": "NODE_ENV=staging vite build --mode staging --watch",
+ "staging:once": "NODE_ENV=staging vite build --mode staging",
"build": "NODE_ENV=production vite build --mode prod"
},
"devDependencies": {
"@foundryvtt/foundryvtt-cli": "^1.1.0",
"@stylistic/eslint-plugin": "^4.2.0",
- "dotenv": "^17.2.2",
"eslint": "^9.25.0",
"glob": "^11.0.1",
"terser": "^5.39.0",
- "vite": "^6.3.4"
+ "vite": "^6.3.1"
},
"dependencies": {
"chart.js": "^4.4.9",
diff --git a/packs/docs/_source/English_pBOyeBDuTeowuDOE.json b/packs/docs/_source/English_pBOyeBDuTeowuDOE.json
index f6e18d1..cba21ab 100644
--- a/packs/docs/_source/English_pBOyeBDuTeowuDOE.json
+++ b/packs/docs/_source/English_pBOyeBDuTeowuDOE.json
@@ -133,7 +133,7 @@
"image": {},
"text": {
"format": 1,
- "content": "A bucket is the term used to identify a group of allowed values within a @UUID[Compendium.stat-tracker.docs.JournalEntry.pBOyeBDuTeowuDOE.JournalEntryPage.ugzCCxQskUSYMZR4]{table}, each bucket must have a type, and a number of additional settings depending on what type it is.
String Buckets
This is the most simple type of bucket, it allows a string to be added as the row's value. The only additional configuration for this type of bucket is restricting what strings can be added be added.
e.g. you can limit each row to only have a value of \"Critical Success\", or \"Critical Failure\" and if someone tries to add \"Apple Sauce\" into the table, it will reject that row.
Number Buckets
This type of bucket is likely the one you will utilize the most, it allows storing any number. It accepts an set of additional options described below, all of which are optional.
Setting | Description |
Minimum | The minimum allowed value. Required when Step is provided. |
Maximum | The maximum allowed value, must be greater than Minimum |
Step | When a step is set it requires each number to be a \"step\" away from the lower one. So if you have a minimum of 2 and a step of 4, the allowed values are: 2, 6, 10, 14, 18, etc. |
"
+ "content": "A bucket is the term used to identify a group of allowed values within a @UUID[Compendium.stat-tracker.docs.JournalEntry.pBOyeBDuTeowuDOE.JournalEntryPage.ugzCCxQskUSYMZR4]{table}, each bucket must have a type, and a number of additional settings depending on what type it is.
String Buckets
This is the most simple type of bucket, it allows a string to be added as the row's value. The only additional configuration for this type of bucket is restricting what strings can be added be added.
e.g. you can limit each row to only have a value of \"Critical Success\", or \"Critical Failure\" and if someone tries to add \"Apple Sauce\" into the table, it will reject that row.
Number Buckets
This type of bucket is likely the one you will utilize the most, it allows storing any number. It accepts an set of additional options described below, all of which are optional.
Setting | Description |
Minimum | The minimum allowed value |
Maximum | The maximum allowed value, must be greater than Minimum |
Step | Requires Minimum When a step is set it requires each number to be a \"step\" away from the lower one. So if you have a minimum of 2 and a step of 4, the allowed values are: 2, 6, 10, 14, 18, etc. |
Range Buckets
The range bucket is what is used by the auto-generated dice tables, these bucket types use the same options as the @UUID[Compendium.stat-tracker.docs.JournalEntry.pBOyeBDuTeowuDOE.JournalEntryPage.e9FYKidbfFnnTspO#number-buckets]{Number Buckets} but the Minimum/Maximum/Step options are required when the type is range.
"
},
"video": {
"controls": true,
@@ -153,7 +153,7 @@
"systemId": "empty-system",
"systemVersion": "0.0.0",
"createdTime": 1748329573212,
- "modifiedTime": 1748803873692,
+ "modifiedTime": 1748499573438,
"lastModifiedBy": "t2sWGWEYSMFrfBu3"
},
"_key": "!journal.pages!pBOyeBDuTeowuDOE.e9FYKidbfFnnTspO"
@@ -250,7 +250,7 @@
"image": {},
"text": {
"format": 1,
- "content": "The module provides a multitude of utility functions through it's API for usage however desired. This will go over them and describe their purpose.
filterPrivateRows
This method is intended to take @UUID[Compendium.stat-tracker.docs.JournalEntry.pBOyeBDuTeowuDOE.JournalEntryPage.S7Z6mZ0JablJVQJu]{rows} provided by the database and filter out any that the user would not be able to see normally. This is usually called by the database adapters so there's unlikely to be any reason to use it externally.
Available under <api>.utils.filterPrivateRows.
inferRollMode
This utility is intended to try and determine what roll mode was used to create a chat message. The inference is not entirely accurate because it struggles to differentiate between a GM rolling with a Private GM Roll and a Self Roll when there is only one GM present in the world.
Available under <api>.utils.inferRollMode
validateValue
Available under <api>.utils.validateValue.
validateBucketConfig
Available under <api>.utils.validateBucketConfig.
"
+ "content": "The module provides a multitude of utility functions through it's API for usage however desired. This will go over them and describe their purpose.
filterPrivateRows
This method is intended to take @UUID[Compendium.stat-tracker.docs.JournalEntry.pBOyeBDuTeowuDOE.JournalEntryPage.S7Z6mZ0JablJVQJu]{rows} provided by the database and filter out any that the user would not be able to see normally. This is usually called by the database adapters so there's unlikely to be any reason to use it externally.
Available under <api>.utils.filterPrivateRows.
validateValue
Available under <api>.utils.validateValue.
validateBucketConfig
Available under <api>.utils.validateBucketConfig.
"
},
"video": {
"controls": true,
@@ -266,11 +266,11 @@
"compendiumSource": null,
"duplicateSource": null,
"exportSource": null,
- "coreVersion": "13.345",
+ "coreVersion": "13.344",
"systemId": "empty-system",
"systemVersion": "0.0.0",
"createdTime": 1748330904988,
- "modifiedTime": 1749864406851,
+ "modifiedTime": 1748394635911,
"lastModifiedBy": "t2sWGWEYSMFrfBu3"
},
"_key": "!journal.pages!pBOyeBDuTeowuDOE.TQzWrVTEz4oQhLPD"
@@ -348,7 +348,7 @@
"systemId": "empty-system",
"systemVersion": "0.0.0",
"createdTime": 1748393806045,
- "modifiedTime": 1748803890586,
+ "modifiedTime": 1748499640505,
"lastModifiedBy": "t2sWGWEYSMFrfBu3"
},
"_key": "!journal.pages!pBOyeBDuTeowuDOE.IXZpEBEJsvOpY3OL"
@@ -406,7 +406,7 @@
"image": {},
"text": {
"format": 1,
- "content": "All of these enums are available within <api>.enums, they are read-only and cannot be modified by other plugins.
Privacy Modes
This enum is used by the module to specify all of the privacy levels that it uses.
The valid values are:
GM - Representing that only gamemasters and assistant gamemasters will be able to see this data entry. This mode is similar to the \"Blind GM Roll\" roll mode.
PRIVATE - Indicating that this is a piece of private data, that only gamemasters, assistant gamemasters, and the user who owns the piece of data will be able to see it.
SELF - Similar to the \"GM\" level, but instead of gamemasters, it's only the user who owns the piece of data that's able to see it.
PUBLIC - Everyone can see it.
"
+ "content": "All of these enums are available within <api>.enums, they are read-only and cannot be modified by other plugins.
Privacy Modes
This enum is used by the module to specify all of the privacy levels that it uses.
The valid values are:
GM - Representing that only gamemasters and assistant gamemasters will be able to see this data entry. This mode is similar to the \"Blind GM Roll\" roll mode.
PRIVATE - Indicating that this is a piece of private data, that only gamemasters, assistant gamemasters, and the user who owns the piece of data will be able to see it.
SELF - Similar to the \"GM\" level, but instead of gamemasters, it's only the user who owns the piece of data that's able to see it.
PUBLIC - Everyone can see it.
"
},
"video": {
"controls": true,
@@ -426,7 +426,7 @@
"systemId": "empty-system",
"systemVersion": "0.0.0",
"createdTime": 1748394110971,
- "modifiedTime": 1748803937108,
+ "modifiedTime": 1748657380240,
"lastModifiedBy": "t2sWGWEYSMFrfBu3"
},
"_key": "!journal.pages!pBOyeBDuTeowuDOE.WYaZPgSRDx8L7Zmq"
diff --git a/public/module.json b/public/module.json
index 431b68d..50eecbf 100644
--- a/public/module.json
+++ b/public/module.json
@@ -1,8 +1,7 @@
{
"id": "stat-tracker",
"title": "Stats Tracker",
- "description": "A user-first approach to stat tracking. Designed from the ground up with the intent of being able to track whatever statistics you want.
",
- "version": "1.0.3",
+ "version": "1.0.0",
"compatibility": {
"maximum": 13,
"verified": 13,
diff --git a/public/templates/Apps/TableManager/buckets/number.hbs b/public/templates/Apps/TableManager/buckets/number.hbs
index cebbfaa..91ce2ec 100644
--- a/public/templates/Apps/TableManager/buckets/number.hbs
+++ b/public/templates/Apps/TableManager/buckets/number.hbs
@@ -1,4 +1,13 @@
-
+
+ {{#if buckets.locked}}
+
+ This bucket configuration has been locked, preventing editing
+ of the settings.
+
+ {{/if}}