DEV: Add a script for dumping and comparing all asset layer pose mappings

This commit is contained in:
bananarama92 2023-11-15 21:02:17 +01:00
parent d21c199975
commit b973cb1ce3
No known key found for this signature in database
GPG key ID: ECBC951D6255A50F
9 changed files with 14532 additions and 148 deletions

1
.gitattributes vendored
View file

@ -1,3 +1,4 @@
# Auto detect text files and perform LF normalization
* text=auto
BondageClub/Tools/Node/package-lock.json linguist-generated -diff
BondageClub/Tools/Node/PoseMappingJSON/* linguist-generated

View file

@ -1,148 +1,10 @@
"use strict";
const vm = require("vm");
const fs = require("fs");
const BASE_PATH = "../../";
// Files needed to check the Female3DCG assets
const NEEDED_FILES = [
"Scripts/Common.js",
"Scripts/Dialog.js",
"Scripts/Asset.js",
"Scripts/ExtendedItem.js",
"Scripts/ModularItem.js",
"Scripts/TypedItem.js",
"Scripts/VariableHeight.js",
"Scripts/VibratorMode.js",
"Scripts/Property.js",
"Scripts/TextItem.js",
"Screens/Inventory/Futuristic/Futuristic.js",
"Screens/Inventory/ItemTorso/FuturisticHarness/FuturisticHarness.js",
"Screens/Inventory/ItemNeckAccessories/CollarNameTag/CollarNameTag.js",
"Screens/Inventory/ItemArms/FullLatexSuit/FullLatexSuit.js",
"Screens/Inventory/ItemButt/InflVibeButtPlug/InflVibeButtPlug.js",
"Screens/Inventory/ItemDevices/VacBedDeluxe/VacBedDeluxe.js",
"Screens/Inventory/ItemDevices/WoodenBox/WoodenBox.js",
"Screens/Inventory/ItemPelvis/SciFiPleasurePanties/SciFiPleasurePanties.js",
"Screens/Inventory/ItemNeckAccessories/CollarShockUnit/CollarShockUnit.js",
"Screens/Inventory/ItemVulva/ClitAndDildoVibratorbelt/ClitAndDildoVibratorbelt.js",
"Screens/Inventory/ItemBreast/FuturisticBra/FuturisticBra.js",
"Screens/Inventory/ItemArms/TransportJacket/TransportJacket.js",
"Screens/Inventory/ItemMouth/FuturisticPanelGag/FuturisticPanelGag.js",
"Screens/Inventory/ItemNeckAccessories/CollarAutoShockUnit/CollarAutoShockUnit.js",
"Screens/Inventory/ItemArms/PrisonLockdownSuit/PrisonLockdownSuit.js",
"Screens/Inventory/ItemPelvis/LoveChastityBelt/LoveChastityBelt.js",
"Screens/Inventory/ItemVulva/LoversVibrator/LoversVibrator.js",
"Screens/Inventory/ItemButt/AnalBeads2/AnalBeads2.js",
"Screens/Inventory/ItemDevices/LuckyWheel/LuckyWheel.js",
"Screens/Inventory/ItemDevices/FuturisticCrate/FuturisticCrate.js",
"Screens/Inventory/Cloth/CheerleaderTop/CheerleaderTop.js",
"Screens/Inventory/ClothAccessory/Bib/Bib.js",
"Screens/Inventory/ItemDevices/DollBox/DollBox.js",
"Screens/Inventory/ItemDevices/PetBowl/PetBowl.js",
"Screens/Inventory/ItemHead/DroneMask/DroneMask.js",
"Screens/Inventory/ItemMisc/WoodenSign/WoodenSign.js",
"Screens/Inventory/ItemHood/CanvasHood/CanvasHood.js",
"Screens/Inventory/ItemPelvis/ObedienceBelt/ObedienceBelt.js",
"Screens/Inventory/ItemNeckAccessories/CustomCollarTag/CustomCollarTag.js",
"Screens/Inventory/ItemNeckAccessories/ElectronicTag/ElectronicTag.js",
"Screens/Inventory/ItemNeckRestraints/PetPost/PetPost.js",
"Screens/Inventory/ItemVulva/FuturisticVibrator/FuturisticVibrator.js",
"Screens/Inventory/ItemPelvis/FuturisticTrainingBelt/FuturisticTrainingBelt.js",
"Screens/Inventory/ItemDevices/KabeshiriWall/KabeshiriWall.js",
"Screens/Inventory/ItemDevices/FuckMachine/FuckMachine.js",
"Screens/Inventory/ItemBreast/ForbiddenChastityBra/ForbiddenChastityBra.js",
"Screens/Inventory/Suit/LatexCatsuit/LatexCatsuit.js",
"Assets/Female3DCG/Female3DCG.js",
"Assets/Female3DCG/Female3DCGExtended.js",
"Scripts/Translation.js",
"Scripts/Text.js",
"Screens/Character/ItemColor/ItemColor.js",
"Scripts/Testing.js",
];
let localError = false;
let globalError = false;
/**
* Logs the error to console and sets erroneous exit code
* @param {string} text The error
*/
function error(text) {
console.log("ERROR:", text);
process.exitCode = 1;
localError = true;
globalError = true;
}
/**
* Parse a CSV file content into an array
* @param {string} str - Content of the CSV
* @returns {string[][]} Array representing each line of the parsed content, each line itself is split by commands and stored within an array.
*/
function CommonParseCSV(str) {
/** @type {string[][]} */
let arr = [];
let quote = false; // true means we're inside a quoted field
let c;
let col;
// We remove whitespace on start and end
str = str.trim() + "\n";
// iterate over each character, keep track of current row and column (of the returned array)
for (let row = (col = c = 0); c < str.length; c++) {
var cc = str[c],
nc = str[c + 1]; // current character, next character
arr[row] = arr[row] || []; // create a new row if necessary
arr[row][col] = arr[row][col] || ""; // create a new column (start with empty string) if necessary
// If the current character is a quotation mark, and we're inside a
// quoted field, and the next character is also a quotation mark,
// add a quotation mark to the current column and skip the next character
if (cc == '"' && quote && nc == '"') {
arr[row][col] += cc;
++c;
continue;
}
// If it's just one quotation mark, begin/end quoted field
if (cc == '"') {
quote = !quote;
continue;
}
// If it's a comma and we're not in a quoted field, move on to the next column
if (cc == "," && !quote) {
++col;
continue;
}
// If it's a newline and we're not in a quoted field, move on to the next
// row and move to column 0 of that new row
if (cc == "\n" && !quote) {
++row;
col = 0;
continue;
}
// Otherwise, append the current character to the current column
arr[row][col] += cc;
}
return arr;
}
/**
* Loads a CSV file and verifies correct column widths
* @param {string} path Path to file, relative to BondageClub directory
* @param {number} expectedWidth Expected number of columns
*/
function loadCSV(path, expectedWidth) {
const data = CommonParseCSV(fs.readFileSync(BASE_PATH + path, { encoding: "utf-8" }));
for (let line = 0; line < data.length; line++) {
if (data[line].length !== expectedWidth) {
error(`Bad width of line ${line + 1} (${data[line].length} vs ${expectedWidth}) in ${path}`);
}
}
return data;
}
const { NEEDED_FILES, BASE_PATH, error, loadCSV } = require("./Common.js");
const common = require("./Common.js");
/**
* Checks for {@link AssetDefinition.DynamicGroupName}
@ -1258,7 +1120,7 @@ function sanitizeVMOutput(input) {
const dialogArray = loadCSV("Screens/Character/Player/Dialog_Player.csv", 6);
// No further checks if initial data load failed
if (localError) {
if (common.localError) {
return;
}
@ -1270,7 +1132,7 @@ function sanitizeVMOutput(input) {
// Check all groups
for (const Group of AssetFemale3DCG) {
localError = false;
common.localError = false;
Groups.push(Group);
/** @type {AssetDefinition[]} */
@ -1284,7 +1146,7 @@ function sanitizeVMOutput(input) {
});
continue;
}
localError = false;
common.localError = false;
// Check any extended item config
if (Asset.Extended) {
@ -1319,13 +1181,13 @@ function sanitizeVMOutput(input) {
}
}
if (!localError) {
if (!common.localError) {
GroupAssets.push(Asset);
}
}
}
if (globalError) {
if (common.globalError) {
console.log("WARNING: Type errors detected, skipping other checks");
return;
}

View file

@ -0,0 +1,180 @@
/** Common utility scripts for the test suit. */
"use strict";
const fs = require("fs");
/** Files needed to check the Female3DCG assets. */
const NEEDED_FILES = [
"Scripts/Common.js",
"Scripts/Dialog.js",
"Scripts/Asset.js",
"Scripts/ExtendedItem.js",
"Scripts/ModularItem.js",
"Scripts/TypedItem.js",
"Scripts/VariableHeight.js",
"Scripts/VibratorMode.js",
"Scripts/Property.js",
"Scripts/TextItem.js",
"Screens/Inventory/Futuristic/Futuristic.js",
"Screens/Inventory/ItemTorso/FuturisticHarness/FuturisticHarness.js",
"Screens/Inventory/ItemNeckAccessories/CollarNameTag/CollarNameTag.js",
"Screens/Inventory/ItemArms/FullLatexSuit/FullLatexSuit.js",
"Screens/Inventory/ItemButt/InflVibeButtPlug/InflVibeButtPlug.js",
"Screens/Inventory/ItemDevices/VacBedDeluxe/VacBedDeluxe.js",
"Screens/Inventory/ItemDevices/WoodenBox/WoodenBox.js",
"Screens/Inventory/ItemPelvis/SciFiPleasurePanties/SciFiPleasurePanties.js",
"Screens/Inventory/ItemNeckAccessories/CollarShockUnit/CollarShockUnit.js",
"Screens/Inventory/ItemVulva/ClitAndDildoVibratorbelt/ClitAndDildoVibratorbelt.js",
"Screens/Inventory/ItemBreast/FuturisticBra/FuturisticBra.js",
"Screens/Inventory/ItemArms/TransportJacket/TransportJacket.js",
"Screens/Inventory/ItemMouth/FuturisticPanelGag/FuturisticPanelGag.js",
"Screens/Inventory/ItemNeckAccessories/CollarAutoShockUnit/CollarAutoShockUnit.js",
"Screens/Inventory/ItemArms/PrisonLockdownSuit/PrisonLockdownSuit.js",
"Screens/Inventory/ItemPelvis/LoveChastityBelt/LoveChastityBelt.js",
"Screens/Inventory/ItemVulva/LoversVibrator/LoversVibrator.js",
"Screens/Inventory/ItemButt/AnalBeads2/AnalBeads2.js",
"Screens/Inventory/ItemDevices/LuckyWheel/LuckyWheel.js",
"Screens/Inventory/ItemDevices/FuturisticCrate/FuturisticCrate.js",
"Screens/Inventory/Cloth/CheerleaderTop/CheerleaderTop.js",
"Screens/Inventory/ClothAccessory/Bib/Bib.js",
"Screens/Inventory/ItemDevices/DollBox/DollBox.js",
"Screens/Inventory/ItemDevices/PetBowl/PetBowl.js",
"Screens/Inventory/ItemHead/DroneMask/DroneMask.js",
"Screens/Inventory/ItemMisc/WoodenSign/WoodenSign.js",
"Screens/Inventory/ItemHood/CanvasHood/CanvasHood.js",
"Screens/Inventory/ItemPelvis/ObedienceBelt/ObedienceBelt.js",
"Screens/Inventory/ItemNeckAccessories/CustomCollarTag/CustomCollarTag.js",
"Screens/Inventory/ItemNeckAccessories/ElectronicTag/ElectronicTag.js",
"Screens/Inventory/ItemNeckRestraints/PetPost/PetPost.js",
"Screens/Inventory/ItemVulva/FuturisticVibrator/FuturisticVibrator.js",
"Screens/Inventory/ItemPelvis/FuturisticTrainingBelt/FuturisticTrainingBelt.js",
"Screens/Inventory/ItemDevices/KabeshiriWall/KabeshiriWall.js",
"Screens/Inventory/ItemDevices/FuckMachine/FuckMachine.js",
"Screens/Inventory/ItemBreast/ForbiddenChastityBra/ForbiddenChastityBra.js",
"Screens/Inventory/Suit/LatexCatsuit/LatexCatsuit.js",
"Assets/Female3DCG/Female3DCG.js",
"Assets/Female3DCG/Female3DCGExtended.js",
"Scripts/Translation.js",
"Scripts/Text.js",
"Screens/Character/ItemColor/ItemColor.js",
"Scripts/Testing.js",
];
/** The base path for any BC asset/script lookup. */
const BASE_PATH = "../../";
let localError = false;
let globalError = false;
/**
* Logs the error to console and sets erroneous exit code
* @param {string} text The error
*/
function error(text) {
console.log("ERROR:", text);
process.exitCode = 1;
localError = true;
globalError = true;
}
/** @see {@link Object.entries} */
const entries = /** @type {<KT extends String, VT>(record: Partial<Record<KT, VT>>) => [key: KT, value: VT][]} */(Object.entries);
/** @see {@link Object.keys} */
const keys = /** @type {<KT extends String>(record: Partial<Record<KT, unknown>>) => KT[]} */(Object.keys);
/** @see {@link Object.fromEntries} */
const fromEntries = /** @type {<KT extends String, VT>(list: [key: KT, value: VT][]) => Record<KT, VT>} */(Object.fromEntries);
/**
* Return whether the passed object is a record/interface.
* @type {(obj: unknown) => obj is Record<string, unknown>}
*/
function isObject(obj) {
return obj !== null && typeof obj === "object" && !Array.isArray(obj);
}
/**
* Parse a CSV file content into an array
* @param {string} str - Content of the CSV
* @returns {string[][]} Array representing each line of the parsed content, each line itself is split by commands and stored within an array.
*/
function parseCSV(str) {
/** @type {string[][]} */
let arr = [];
let quote = false; // true means we're inside a quoted field
let c;
let col;
// We remove whitespace on start and end
str = str.trim() + "\n";
// iterate over each character, keep track of current row and column (of the returned array)
for (let row = (col = c = 0); c < str.length; c++) {
var cc = str[c],
nc = str[c + 1]; // current character, next character
arr[row] = arr[row] || []; // create a new row if necessary
arr[row][col] = arr[row][col] || ""; // create a new column (start with empty string) if necessary
// If the current character is a quotation mark, and we're inside a
// quoted field, and the next character is also a quotation mark,
// add a quotation mark to the current column and skip the next character
if (cc == '"' && quote && nc == '"') {
arr[row][col] += cc;
++c;
continue;
}
// If it's just one quotation mark, begin/end quoted field
if (cc == '"') {
quote = !quote;
continue;
}
// If it's a comma and we're not in a quoted field, move on to the next column
if (cc == "," && !quote) {
++col;
continue;
}
// If it's a newline and we're not in a quoted field, move on to the next
// row and move to column 0 of that new row
if (cc == "\n" && !quote) {
++row;
col = 0;
continue;
}
// Otherwise, append the current character to the current column
arr[row][col] += cc;
}
return arr;
}
/**
* Loads a CSV file and verifies correct column widths
* @param {string} path Path to file, relative to BondageClub directory
* @param {number} expectedWidth Expected number of columns
*/
function loadCSV(path, expectedWidth) {
const data = parseCSV(fs.readFileSync(BASE_PATH + path, { encoding: "utf-8" }));
for (let line = 0; line < data.length; line++) {
if (data[line].length !== expectedWidth) {
error(`Bad width of line ${line + 1} (${data[line].length} vs ${expectedWidth}) in ${path}`);
}
}
return data;
}
module.exports = {
NEEDED_FILES,
BASE_PATH,
localError,
globalError,
error,
entries,
keys,
fromEntries,
isObject,
loadCSV,
};

View file

@ -0,0 +1,227 @@
"use strict";
const vm = require("vm");
const fs = require("fs");
const process = require("process");
const minimist = require("minimist");
const util = require('util');
const { NEEDED_FILES, BASE_PATH, loadCSV, entries, fromEntries, keys, isObject } = require("./Common.js");
const HELP = `\
Script for dumping and comparing pose mappings.
Usage:
node PoseMapping.js [options]
Options:
-h, --help Show help
--dump <path> Write the current pose mappings to the specified JSON file
--compare <path> Compare the current pose mappings with those in the passed JSON file
`;
/** @typedef {string} AssetName */
/** @typedef {string} LayerName */
/**
* @template T
* @typedef {Record<AssetGroupName, Record<AssetName, Record<LayerName, T>>>} LayerMapping
*/
/** @typedef {LayerMapping<AssetPoseMapping>} AllPoseMappings */
/** @typedef {Partial<Record<AssetPoseName, Record<"-" | "+", AssetPoseName | PoseType>>>} DiffRecord */
/** @typedef {[key: AssetPoseName, value: Record<"-" | "+", AssetPoseName | PoseType>][]} DiffList */
/** @typedef {Partial<LayerMapping<DiffRecord>>} AllDiffMappings */
/**
* @param {minimist.ParsedArgs} argv
* @returns {{ help?: boolean, dump?: string, compare?: string }}
*/
function validateArgv(argv) {
const { _, ...kwargs } = argv;
delete kwargs.h;
/** @type {string[]} */
const invalidArguments = [];
invalidArguments.push(..._);
/** @type {ReturnType<typeof validateArgv>} */
const ret = {};
const validKeys = new Set(["help", "dump", "compare"]);
for (const [k, v] of Object.entries(kwargs)) {
if (validKeys.has(k)) {
ret[k] = v;
} else {
invalidArguments.push(k);
}
}
if (invalidArguments.length > 0) {
throw new Error(`Found ${invalidArguments.length} unknown arguments: ${invalidArguments.join()}`);
} else if (keys(ret).length === 0) {
throw new Error(`Expects at least one argument`);
} else if ("dump" in kwargs && !kwargs.dump) {
throw new Error("Dump file path must be a non-empty string");
} else if ("compare" in kwargs && !kwargs.compare) {
throw new Error("Compare file path must be a non-empty string");
}
return ret;
}
/**
* Dump the pose mappings, extracted from the passed assets, to the output json file specified in `outputPath`
* @param {readonly Asset[]} assets
* @returns {AllPoseMappings}
*/
function gatherPoseMappings(assets) {
const layers = [...assets].sort((a, b) => {
const groupPrio = a.Group.Name.localeCompare(b.Group.Name);
const assetPrio = a.Name.localeCompare(b.Name);
return groupPrio || assetPrio;
}).flatMap(a => a.Layer);
const ret = /** @type {AllPoseMappings} */({});
for (const layer of layers) {
const layerName = layer.Name ?? "";
const assetName = layer.Asset.Name;
const groupName = layer.Asset.Group.Name;
if (!ret[groupName]) {
ret[groupName] = {};
}
if (!ret[groupName][assetName]) {
ret[groupName][assetName] = {};
}
const posesSorted = fromEntries(entries(layer.PoseMapping).sort((i, j) => i[0].localeCompare(j[0])).filter(i => i[1]));
ret[groupName][assetName][layerName] = posesSorted;
}
return ret;
}
/**
* Construct the difference between the pose mappings in `newData` and `oldData`.
* Assets and groups absent in `oldData` are ignored.
* @param {AllPoseMappings} newData
* @param {Partial<AllPoseMappings>} oldData
* @param {readonly Pose[]} poses
* @returns {AllDiffMappings | null}
*/
function getPoseMappingsDiff(newData, oldData, poses) {
const poseNames = poses.map(p => p.Name).sort();
/** @type {AllDiffMappings} */
const diffMapping = {};
for (const [groupName, assetNew] of entries(newData)) {
const assetOld = oldData[groupName];
if (!isObject(assetOld)) {
continue;
}
for (const [assetName, layerNew] of entries(assetNew)) {
const layerOld = assetOld[assetName];
if (!isObject(layerOld)) {
continue;
}
for (const [layerName, posesNew] of entries(layerNew)) {
const posesOld = layerOld[layerName];
if (!isObject(posesOld)) {
continue;
}
const diff = /** @type {DiffList} */(poseNames.map(k => {
const poseNew = posesNew[k] ?? "";
const poseOld = posesOld[k] ?? "";
if (poseNew === poseOld) {
return null;
} else {
return [k, { "+": poseNew, "-": poseOld }];
}
}).filter(i => i !== null));
if (diff.length === 0) {
continue;
}
let groupDiff = diffMapping[groupName];
if (!groupDiff) groupDiff = diffMapping[groupName] = {};
let assetDiff = groupDiff[assetName];
if (!assetDiff) assetDiff = groupDiff[assetName] = {};
assetDiff[layerName] = fromEntries(diff);
}
}
}
return keys(diffMapping).length === 0 ? null : diffMapping;
}
/**
* @returns {{ poseMapping: AllPoseMappings, poses: Pose[] }}
*/
function runVM() {
const [commonFile, ...neededFiles] = NEEDED_FILES;
/** @type {vm.Context & { Asset?: Asset[], Pose?: Pose[] }} */
const context = vm.createContext({
OuterArray: Array,
Object: Object,
TestingColorLayers: new Set(loadCSV("Assets/Female3DCG/LayerNames.csv", 2).map(i => i[0])),
TestingColorGroups: new Set(loadCSV("Assets/Female3DCG/ColorGroups.csv", 2).map(i => i[0])),
});
vm.runInContext(fs.readFileSync(BASE_PATH + commonFile, { encoding: "utf-8" }), context, {
filename: commonFile,
});
// Only patch `CommonGet` after loading `Common`, lest our monkey patch will be overriden again
context.CommonGet = (file, callback) => {
const data = fs.readFileSync(`../../${file}`, "utf8");
const obj = {
status: 200,
responseText: data,
};
callback.bind(obj)(obj);
};
for (const file of neededFiles) {
vm.runInContext(fs.readFileSync(BASE_PATH + file, { encoding: "utf-8" }), context, {
filename: file,
});
}
const { Asset, Pose } = context;
if (!Asset || !Pose) {
throw new Error("Failed to generate the `Asset` and/or `Pose` arrays");
}
return { poseMapping: gatherPoseMappings(Asset), poses: Pose };
}
(function () {
const kwargs = validateArgv(minimist(
process.argv.slice(2),
{ string: ["compare", "dump"], alias: { "h": "help" } },
));
if (kwargs.help) {
console.log(HELP);
return;
}
const { poseMapping, poses } = runVM();
if (kwargs.dump !== undefined) {
fs.writeFileSync(kwargs.dump, JSON.stringify(poseMapping, undefined, 4));
console.log(`Succesfully written pose mapping to "${kwargs.dump}"`);
}
if (kwargs.compare !== undefined) {
/** @type {null | Record<string, any> | any[]} */
const poseMappingOld = JSON.parse(fs.readFileSync(kwargs.compare, { encoding: "utf8" }));
if (!isObject(poseMappingOld)) {
throw new Error(`Invalid "${kwargs.compare}" JSON data expected a nested record`);
}
const diff = getPoseMappingsDiff(poseMapping, poseMappingOld, poses);
if (diff !== null) {
console.error(util.inspect(diff, { depth: null, colors: true }));
process.exit(1);
} else {
console.log(`Succesfully compared "${kwargs.compare}" pose mappings without conflict`);
}
}
})();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
Various utility scripts used for running the CI and other dev-related tasks.
AssetCheck.js
-------------
Tests for various asset-related properties.
PoseMapping.js
--------------
Scripts for dumping and comparing asset layer pose mappings. JSON dumps from older BC versions can be stored in `PoseMappingJSON/`.
GenerateChangelog.js
--------------------
Helper script for generating the BC changelog.

View file

@ -10,6 +10,7 @@
"include": [
"../../Assets/Female3DCG/Female3DCG_Types.d.ts",
"../../Scripts/Typedef.d.ts",
"AssetCheck.js"
"AssetCheck.js",
"PoseMapping.js"
]
}

View file

@ -32,6 +32,7 @@
"gulp-size": "^4.0.1",
"imagemin-jpegtran": "^7.0.0",
"marked": "^4.0.10",
"minimist": "^1.2.6",
"node-fetch": "^2.6.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.0.3",

View file

@ -12,6 +12,7 @@
"assets:lint:fix": "prettier-eslint --write Assets/Female3DCG/Female3DCG.js Assets/Female3DCG/Female3DCGExtended.js",
"assets:check": "cd Tools/Node && node --unhandled-rejections=strict AssetCheck",
"assets:typecheck": "cd ../ && tsc -p BondageClub/Tools/Node/tsconfig-assetcheck.json",
"assets:posemapping": "cd Tools/Node && node --unhandled-rejections=strict PoseMapping",
"scripts:lint": "eslint \"Scripts/**/*.js\" \"Screens/**/*.js\" \"Tools/**/*.js\" \"Backgrounds/Backgrounds.js\"",
"scripts:lint:fix": "eslint --fix \"Scripts/**/*.js\" \"Screens/**/*.js\" \"Tools/**/*.js\" \"Backgrounds/Backgrounds.js\"",
"scripts:typecheck": "cd ../ && tsc -p BondageClub/jsconfig.json",
@ -61,7 +62,8 @@
"through2": "^4.0.2",
"typescript": "^5.2.2",
"@types/css-font-loading-module": "^0.0.9",
"@types/node": "^20.8.7"
"@types/node": "^20.8.7",
"minimist": "^1.2.6"
},
"dependencies": {
"pixi-filters": "^5.2.1",