Add some utils and update the memory database to "store" rows

This commit is contained in:
Oliver-Akins 2025-04-27 21:32:35 -06:00
parent 9694d72dd0
commit 6b35c10106
4 changed files with 220 additions and 60 deletions

View file

@ -1,6 +1,11 @@
// Apps
import { StatsViewer } from "./Apps/StatsViewer.mjs";
import { TestApp } from "./Apps/TestApp.mjs";
// Utils
import { filterPrivateRows } from "./utils/filterPrivateRows.mjs";
import { validateValue } from "./utils/validateValue.mjs";
const { deepFreeze } = foundry.utils;
Object.defineProperty(
@ -12,6 +17,10 @@ Object.defineProperty(
TestApp,
StatsViewer,
},
utils: {
filterPrivateRows,
validateValue,
},
}),
writable: false,
},

View file

@ -1,82 +1,150 @@
/* eslint-disable no-unused-vars */
import { Table } from "./model.mjs";
import { filterPrivateRows } from "../filterPrivateRows.mjs";
const { randomID } = foundry.utils;
function generateRow(value, isPrivate = false) {
return {
id: randomID(),
timestamp: Date.now(),
value,
isPrivate,
};
};
function getNormalDistributionHeight(x, a, b) {
const maxHeight = b;
return Math.round(Math.exp(-( ((x - a) ** 2) / b )) * maxHeight);
};
const { randomID, mergeObject } = foundry.utils;
export class MemoryDatabase {
static getTables() {
/** @type {Array<{ name: string; }>} */
return [
{ name: `Dice/d4` },
{ name: `Dice/d6` },
{ name: `Dice/d8` },
{ name: `Dice/d10` },
{ name: `Dice/d12` },
{ name: `Dice/d20` },
{ name: `Dice/d100` },
{ name: `Count of Successes` },
];
static #tables = {
"Dice/d10": {
name: `Dice/d10`,
graphType: `bar`,
buckets: {
type: `range`,
min: 1,
max: 10,
step: 1,
},
config: {
stacked: true,
},
},
"Dice/d20": {
name: `Dice/d20`,
graphType: `bar`,
buckets: {
type: `range`,
min: 1,
max: 20,
step: 1,
},
config: {
stacked: true,
},
},
"Dice/d100": {
name: `Dice/d100`,
graphType: `bar`,
buckets: {
type: `range`,
min: 1,
max: 100,
step: 1,
},
config: {
stacked: true,
},
},
"Count of Successes": {
name: `Count of Successes`,
graphType: `bar`,
buckets: {
type: `number`,
min: 0,
step: 1,
},
config: {
stacked: true,
},
},
"Type of Result": {
name: `Type of Result`,
graphType: `bar`,
buckets: {
type: `string`,
trim: true, // forced true
blank: false, // forced false
textSearch: false, // forced false
choices: [`Normal`, `Popped Off`, `Downed`],
},
config: {
stacked: true,
},
},
};
static createRow(table, user, row) {};
static #rows = {};
static #cache = {};
static getTables() {
/** @type {Array<{ name: string; }>} */
return Object.values(this.#tables);
};
static getRows(tableID, users) {
if (users.length === 0) {
static getTable(tableID) {
return this.#tables[tableID];
};
static createRow(table, userID, row) {
if (!this.#tables[table]) { return };
this.#rows[userID] ??= {};
this.#rows[userID][table] ??= [];
// data format assertions
row._id ||= randomID();
this.#rows[userID][table].push(row);
this.render();
};
static getRows(tableID, userIDs, privacy = `none`) {
if (userIDs.length === 0) {
return {};
};
const datasets = {};
for (const user of users) {
if (this.#cache[user]?.[tableID]) {
datasets[user] = this.#cache[user][tableID];
} else {
const [table, subtable] = tableID.split(`/`);
if (!subtable) {
continue;
}
const size = Number.parseInt(subtable.slice(1));
const rows = [];
for (let i = 1; i <= size; i++) {
const count = getNormalDistributionHeight(i, size / 2, size);
const temp = new Array(count)
.fill(null)
.map(() => generateRow(
game.user.id == user ? i : Math.ceil(Math.random() * size),
));
rows.push(...temp);
for (const userID of userIDs) {
if (this.#rows[userID]?.[tableID]) {
datasets[userID] = filterPrivateRows(
this.#rows[userID][tableID] ?? [],
userID,
privacy,
);
};
this.#cache[user] ??= {};
datasets[user] = this.#cache[user][tableID] = rows;
}
}
return datasets;
};
static updateRow(table, user, rowId, changes) {};
static updateRow(table, userID, rowID, changes) {
if (!this.#tables[table] || !this.#rows[userID]?.[table]) { return };
let row = this.#rows[userID][table].find(row => row._id === rowID);
if (!row) { return };
mergeObject(row, changes);
this.render();
};
static deleteRow(table, user, rowId) {};
static deleteRow(table, userID, rowID) {
if (!this.#tables[table] || !this.#rows[userID]?.[table]) { return };
let rowIndex = this.#rows[userID][table].findIndex(row => row._id === rowID);
if (rowIndex === -1) { return };
this.#rows[userID][table].splice(rowIndex, 1);
this.render();
};
static apps = {};
static render() {
for (const app of Object.values(this.apps)) {
app.render();
};
};
/**
* Used to listen for changes from other clients and refresh the local DB as
* required, so that the StatsTracker stays up to date.
*/
static registerListeners() {};
static unregisterListeners() {};
};
/* eslint-enable no-unused-vars */

View file

@ -0,0 +1,30 @@
/**
* Filters an array of database rows based on if the current user would
* be able to see them based on the privacy level.
*
* @param {Array<any>} rows The rows to filter
* @param {string} userID The user's ID who the rows belong to
* @param {"all"|"me"|"none"} privacy The privacy level we're filtering for
* @returns The filtered rows
*/
export function filterPrivateRows(rows, userID, privacy) {
console.log(rows, userID, privacy);
const filtered = [];
const isMe = userID === game.user.id;
// TODO: make this use a permission rather than just isGM
const canSeeAll = game.user.isGM;
for (const row of rows) {
let allowed = !row.isPrivate;
allowed ||= privacy === `all` && canSeeAll;
allowed ||= privacy === `my` && isMe;
if (allowed) {
filtered.push(row);
};
};
return filtered;
};

View file

@ -0,0 +1,53 @@
import { Logger } from "./Logger.mjs";
const { deepClone } = foundry.utils;
const { StringField, NumberField } = foundry.data.fields;
export function validateValue(value, options) {
let opts = deepClone(options);
if (validatorTypes[opts.type] == null) {
Logger.error(`Failed to find type validator for: ${opts.type}`);
return false;
};
const validator = validatorTypes[opts.type];
validator.transformOptions(opts);
const field = new validator.field(opts);
const error = field.validate(value);
// DataFields return a class instance on error, or void when valid.
return !error;
};
const validatorTypes = {
string: {
field: StringField,
transformOptions: (opts) => {
delete opts.type;
opts.nullable = false;
opts.trim = true;
opts.blank = false;
if (typeof opts.choices === `function`) {
Logger.error(`Choices cannot be a function in a table's buckets configuraion`);
delete opts.choices;
};
},
},
number: {
field: NumberField,
transformOptions: (opts) => {
delete opts.type;
opts.nullable = false;
opts.integer = true;
},
},
range: {
field: NumberField,
transformOptions: (opts) => {
delete opts.type;
opts.nullable = false;
opts.integer = true;
},
},
};