code:
/**
 * @file Makes sure that node dependencies are installed
 */

// @ts-ignore
import jetpack from "fs-jetpack";
import {ask} from "./yesno.js";
import {execSync} from "child_process";

// We need to load package.json to detect kinds of dependencies
// Don't want to install a dev dependency as a normal dependency and vise versa
/** @type {object} */
const packageContents = jetpack.read("package.json", "json");
/** @type {object} */
const devDependencies = packageContents.devDependencies;
/** @type {object} */
const dependencies = packageContents.dependencies;

// default settings
const settings = {
	/** @type {-1|0|1} -1 = do not ask, 0 = ask, 1 = auto install */
	manageNodePackages: 0
};

// create settings.json if it doesn't exist
if (jetpack.exists("settings.json") !== "file") {
	jetpack.write("settings.json", settings);
}

// load settings from settings.json
for (let [key, value] of Object.entries(jetpack.read("settings.json", "json"))) {
	if (!(key in settings) || settings[key] !== value) {
		settings[key] = value;
		jetpack.write("settings.json", settings, {atomic: true});
	}
}

/**
 * Adds any packages with the wrong version to problems
 * @param {string} npmList results of `npm ls` command
 * @param {string[]} problems
 */
async function parseWrongVersion(npmList, problems) {
	npmList.split("\n").forEach(line => {
		// npm ERR! invalid: eslint@7.0.0 C:\Users\dev\Documents\Projects\fc pregmod\node_modules\eslint
		if (line.trim().startsWith("npm ERR! invalid: ")) {
			line = line.replace("npm ERR! invalid: ", "");
			let nodePackage = line.match(/^[^ ]*/)[0];
			problems.push(nodePackage.split("@")[0]);
		}
	});
	return problems;
}

/**
 * Adds any missing packages to problems
 * @param {string} npmList results of `npm ls` command
 * @param {string[]} problems
 */
async function parseMissing(npmList, problems) {
	npmList.split("\n").forEach(line => {
		// npm ERR! missing: eslint@^8.0.0, required by free-cities@1.0.0
		if (line.trim().startsWith("npm ERR! missing: ")) {
			line = line.replace("npm ERR! missing: ", "");
			let nodePackage = line.match(/^[^,]*/)[0];
			problems.push(nodePackage.split("@")[0]);
		}
	});
	return problems;
}

/**
 * Checks for missing and outdated node packages.
 */
async function main() {
	let npmList = "";
	try {
		npmList = execSync('npm ls', {maxBuffer: 1024 * 1024 * 1024, stdio: ['pipe']}, ).toString();
	} catch (e) {
		e.output.forEach(out => {
			if (out === null) { return; }
			npmList += "\n" + out.toString();
		});
	}

	/** @type {string[]} */
	let problems = [];

	if (npmList.includes("npm ERR! invalid: ")) {
		problems = await parseWrongVersion(npmList, problems);
	}

	if (npmList.includes("npm ERR! missing: ")) {
		problems = await parseMissing(npmList, problems);
	}

	let devDependencyCommand = "npm install --save-dev";
	let dependencyCommand = "npm install --save";

	problems.forEach(problem => {
		let matched = false;
		for (let [key, value] of Object.entries(devDependencies)) {
			if (matched === true) { continue; }
			if (key === problem) {
				matched = true;
				devDependencyCommand += ` ${key}@${value}`;
			}
		}
		if (matched === true) { return; }
		for (let [key, value] of Object.entries(dependencies)) {
			if (matched === true) { continue; }
			if (key === problem) {
				matched = true;
				dependencyCommand += ` ${key}@${value}`;
			}
		}
		if (matched === false) {
			console.log(`Unknown dependency type for ${problem}`);
		}
	});

	if (problems.length === 0) { return; }

	console.log("The Node packages below are missing or are the wrong version.");
	console.log("");
	problems.forEach(problem => console.log(problem));
	console.log("");
	console.log("The command(s) that need ran to fix this problem are:");
	console.log("");
	if (devDependencyCommand !== "npm install --save-dev") {
		console.log(devDependencyCommand);
	}
	if (dependencyCommand !== "npm install --save") {
		console.log(dependencyCommand);
	}
	console.log("");
	if (settings.manageNodePackages === 0) {
		// @ts-ignore
		const answer = await ask({
			question: "Would you like us to run the above commands for you? [Y/N]",
		});
		if (answer === true) {
			if (devDependencyCommand !== "npm install --save-dev") {
				execSync(devDependencyCommand, {stdio:[0, 1, 2]});
			}
			if (dependencyCommand !== "npm install --save") {
				execSync(dependencyCommand, {stdio:[0, 1, 2]});
			}
		} else {
			console.log("If you wish not to be asked this question again, change 'manageNodePackages' in settings.json to -1.");
			// TODO:@franklygeorge when settings menu exists add instructions for that here too
		}
	} else if (settings.manageNodePackages === 1) {
		if (devDependencyCommand !== "npm install --save-dev") {
			execSync(devDependencyCommand, {stdio:[0, 1, 2]});
		}
		if (dependencyCommand !== "npm install --save") {
			execSync(dependencyCommand, {stdio:[0, 1, 2]});
		}
	}
}

// @ts-ignore
await main();


code:
#!/bin/bash

# Checks dependencies and prompts the user to install them if missing.
# Returns exit code 69 :) if dependencies still don't exist at end of execution
# checks for git and node

# If you make a change to the functionality of this script you should also make sure to add the same functionality to 'dependencyCheck.bat'

aptInstall="sudo apt-get install git nodejs"
pacmanInstall="sudo pacman -S git nodejs"
dnfInstall="sudo dnf install git nodejs"
yumInstall="sudo yum install git nodejs"
brewInstall="brew install git nodejs"

function modulesCheck() {
    # if node_modules doesn't exist
    if [[ ! -d node_modules ]]; then
        # ask user if we can install modules
        echo ""
        echo "All optional dependencies are install, but the Node modules are not installed."
        echo ""
        echo "These packages should take up less than 500MB of disk space. ~120 MB at time of writing."
        echo "They will be stored in the 'node_modules' directory inside of the project directory."
        echo ""
        read -n 1 -p "Would you like us to run 'npm install' to install them for you? [Y/N]?" choice
        case "$choice" in
        y|Y )
            echo ""
            npm install
            # Check for missing node dependencies
            node ./devTools/scripts/dependencyCheck.js
            echo ""
            echo ""
            ;;
        * )
            echo ""
            echo ""
            exit 69
            ;;
        esac
    else
    # Check for missing node dependencies
	node ./devTools/scripts/dependencyCheck.js
    fi
}

function ensureDependenciesAndExit() {
    # wait for user input
    read -n 1 -s -r -p "Press any key to continue..."
    echo ""

    # check for dependencies
    if command -v git &> /dev/null; then
        git="true"
    fi

    if command -v node &> /dev/null; then
        node="true"
    fi

    # If no missing dependencies, exit with code 0, else exit with code 69
    if [[ "$git" && "$node" ]]; then
        modulesCheck
        echo ""
        echo ""
        exit 0
    else
        echo ""
        echo ""
        exit 69
    fi
}

function manualInstall() {
    # prints out the info for manual installation
    # waits for user input
    # checks for dependencies
    # and then exits
    echo ""
    echo ""
    echo "To install these packages run the command below that applies to your system."
    echo ""

    echo "Ubuntu/Debian and derivatives:"
    echo "  $aptInstall"

    echo "Arch and derivatives:"
    echo "  $pacmanInstall"

    echo "Fedora/RHEL/CentOS and derivatives:"
    # dnf
    echo "  $dnfInstall"
    # yum
    echo "  or"
    echo "  $yumInstall"

    echo "Mac OS X:"
    echo "  $brewInstall"

    echo ""
    echo "You should install these packages now if you wish to do so."
    ensureDependenciesAndExit
}

function automaticInstall() {
    # Install using $packageManager
    echo ""
    echo ""
    if [[ "$packageManager" == "apt" ]]; then
        echo "Executing '$aptInstall'"
        $aptInstall
    elif [[ "$packageManager" == "pacman" ]]; then
        echo "Executing '$pacmanInstall'"
        $pacmanInstall
    elif [[ "$packageManager" == "dnf" ]]; then
        echo "Executing '$dnfInstall'"
        $dnfInstall
    elif [[ "$packageManager" == "yum" ]]; then
        echo "Executing '$yumInstall'"
        $yumInstall
    elif [[ "$packageManager" == "brew" ]]; then
        echo "Executing '$brewInstall'"
        $brewInstall
    fi
    ensureDependenciesAndExit
}

# Check for dependencies
if command -v git &> /dev/null; then
    git="true"
fi

if command -v node &> /dev/null; then
    node="true"
fi

# If no missing dependencies, exit
if [[ "$git" && "$node" ]]; then
    modulesCheck
    exit 0
fi

# Otherwise prompt the user
echo "This project has some optional dependencies that enable features that we believe will make the development process easier and more fruitful."
echo "You are seeing this because some or all of these dependencies are missing."
echo "If you wish to no longer see this, please use one the legacy compiler. 'compile-legacy.sh'"
echo ""
echo "Here is a list of the packages that are missing:"

if [[ ! "$git" ]]; then
    echo "  git, https://git-scm.com/, needed to interact with the .git folder in this project."
    echo "    Allows for things like:"
    echo "      Keeping multiple compiled versions of FC based of the commit they were compiled with."
    echo "      The legacy sanity checks have this as a hard dependency."
fi

if [[ ! "$node" ]]; then
    echo "  Node.js, https://nodejs.org/, enables all of the new sanity checks and the new compiler."
    echo "    Allows for things like:"
    echo "      Source maps for easier debugging: https://dzone.com/articles/what-are-source-maps-and-how-to-properly-use-them"
    echo "      Javascript linting to catch bugs early using ESLint, https://eslint.org/"
    # TODO: @franklygeorge: update as we add the rest of the features
fi

# checks for apt, brew, dnf, yum, and pacman in that order
# meaning that we need to check for them in reverse order
if command -v pacman &> /dev/null; then
    packageManager="pacman"
fi

if command -v yum &> /dev/null; then
    packageManager="yum"
fi

if command -v dnf &> /dev/null; then
    packageManager="dnf"
fi

if command -v brew &> /dev/null; then
    packageManager="brew"
fi

if command -v apt-get &> /dev/null; then
    packageManager="apt"
fi

# if no package manager
if [[ ! "$packageManager" ]]; then
    manualInstall
fi

echo ""
echo "Should the packages listed above be installed automatically using the ${packageManager} package manager?"
echo "You will likely be asked for your root/sudo password as it is often required to install new packages."
echo "If you do not want us to install these dependencies automatically, decline and the required commands will be printed out to do so manually."

# ask for confirmation
read -n 1 -p "Continue with the automatic installation? [Y/N]?" choice
case "$choice" in
  y|Y ) automaticInstall;;
  n|N ) manualInstall;;
  * ) manualInstall;;
esac

repo_name: Arkerthan/fc

license: mit


code:
/**
 * @file returns changes to the project's files. can return files changed or which lines have been changed.
 *
 * Can be called from the command line
 *
 * node devTools/scripts/detectChanges.js
 * node devTools/scripts/detectChanges.js --lines
 *
 * Or imported as shown below
 *
 * import ChangeParser from './detectChanges.js';
 * const changedFiles = ChangeParser.changedFiles();
 * const changedLines = ChangeParser.changedLines();
 */

import {execSync} from 'child_process';
// @ts-ignore
import jetpack from 'fs-jetpack';
import yargs from "yargs";
import {hideBin} from "yargs/helpers";
import {resolve} from 'path';
import {fileURLToPath} from 'url';

const args = yargs(hideBin(process.argv))
	.showHelpOnFail(true)
	.option('lines', {
		type: 'boolean',
		description: 'Returns list of lines changed',
		default: false,
	})
	.parse();

/**
 * // an object that contains file paths as keys and a list of numbers as values
 * @typedef {Object.<string, Array<number>>} ChangedLines
 */

class ChangeParser {
	constructor() {
		this.fetchOrigin();
	}

	/**
	 * Updates origin/pregmod-master
	 */
	fetchOrigin() {
		const command = "git fetch --quiet origin pregmod-master";
		execSync(command);
	}

	/**
	 * Returns the number of lines in the file.
	 * @param {string} filePath path to the file to count lines
	 * @returns {number} number of lines in the file
	 */
	countFileLines(filePath){
		return jetpack.read(filePath, "utf8").split('\n').length;
	}

	/**
	 * Returns a list of files that are not tracked by git and are not in .gitignore
	 * @returns {Array<string>}
	 */
	getUntrackedFiles() {
		const command = "git ls-files -o --exclude-standard .";
		let result = execSync(command).toString().trim().split('\n');

		if (result !== undefined) {
			return result.filter(file => file!== "");
		} else {
			return [];
		}
	}

	/**
	 * Returns a list of files that have been changed.
	 * @returns {Array<string>}
	 */
	changedFiles() {
		let command = "git merge-base origin/pregmod-master HEAD";
		const hash = execSync(command).toString().trim();

		command = `git diff --name-only --diff-filter=d ${hash}`;
		let result = execSync(command).toString().trim().split('\n');

		// add result and this.getUntrackedFiles() together and return
		return result.concat(this.getUntrackedFiles());
	}

	/**
	 * Returns a json object of files with their changed lines.
	 * @returns {ChangedLines}
	 */
	changedLines() {
		/** @type {ChangedLines} */
		let changed = {};

		const untracked = this.getUntrackedFiles();

		for (const file of this.changedFiles()) {
			changed[file] = [];
			if (untracked.includes(file)) {
				// add all lines to changed[file]
				for (let i = 1; i <= this.countFileLines(file); i++) {
					changed[file].push(i);
				}
			} else {
				const command = `git diff -U${this.countFileLines(file)} origin/pregmod-master -- ${file}`;
				/** @type {Array<string>} */
				let result = execSync(command).toString().trim().split('\n');
				// remove first two lines
				result = result.slice(2);
				// remove all lines starting with ---, +++, or @@
				result = result.filter(line => !line.startsWith("---") && !line.startsWith("+++") && !line.startsWith("@@"));
				// remove all lines starting with -
				result = result.filter(line => !line.startsWith("-"));
				let lineNo = 0;
				// for each line
				result.forEach(line => {
					lineNo += 1;
					// if line starts with + add line number to changed lines
					if (line.startsWith("+")) {
						changed[file].push(lineNo);
					}
				});
			}
		}

		return changed;
	}
}

const parser = new ChangeParser();

// @ts-ignore
const pathToThisFile = resolve(fileURLToPath(import.meta.url));
const pathPassedToNode = resolve(process.argv[1]);

if (pathToThisFile.includes(pathPassedToNode)) {
	// called via console
	if (args.lines === true) {
		// get changed lines
		const changed = parser.changedLines();
		// for each file in changed
		for (const file in changed) {
			// for each line in the file
			for (const line of changed[file]) {
				console.log(`${file}, line ${line}`);
			}
		}
	} else {
		// get changed files and print them
		console.log(parser.changedFiles().join("\n"));
	}
}

export default parser;


code:
/**
 * @file Lints files using ESLint
 *
 * Can be called from the command line
 *
 * node devTools/scripts/eslintChecks.js
 * node devTools/scripts/eslintChecks.js --changed
 *
 * Or imported as shown below
 *
 * import eslintChecks from './eslintChecks.js';
 * const changed = false;
 * /** @type{object[]} *\/
 * const report = await eslintChecks(changed);
 */

import detectChanges from './detectChanges.js';

import yargs from "yargs";
import {hideBin} from "yargs/helpers";
import {resolve} from 'path';
import {fileURLToPath} from 'url';
import {ESLint} from "eslint";
// @ts-ignore
import path from "path";

const args = yargs(hideBin(process.argv))
	.showHelpOnFail(true)
	.option('changed', {
		type: 'boolean',
		description: 'Only check changed files',
		default: false,
	})
	.parse();

const eslint = new ESLint({
	cache: true,
	cacheStrategy: "content"
});

/**
 * Runs ESLint on files
 * @param {boolean} changed If true, only check changed files. If false, check all files.
 * @returns {Promise<object[]>}
 */
async function eslintChecks(changed = false) {
	let results = [];

	if (args.changed === false) {
		console.log("If this is the first time you have ran ESLint on all files, then this will take a while...");
	}

	try {
		let filesToLint = ["**/*.js"];
		if (args.changed === true) {
			filesToLint = detectChanges.changedFiles();
		}
		const ESLintResults = await eslint.lintFiles(filesToLint);

		const formatter = await eslint.loadFormatter("json");
		results = JSON.parse(await formatter.format(ESLintResults));
	} catch (e) {
		console.error(e);
		process.exit(1);
	}

	// If changed filter out line numbers that were not edited
	if (args.changed === true) {
		const changedLines = detectChanges.changedLines();

		const originalResults = JSON.parse(JSON.stringify(results));

		results = [];

		Object.keys(originalResults).forEach(resultKey => {
			/** @type {object} */
			let result = originalResults[resultKey];
			const filePath = path.relative(process.cwd(), result.filePath).replace(/\\/g, "/");
			if (Object.keys(changedLines).includes(filePath)) {
				const changedLineNumbers = changedLines[filePath];
				const originalMessages = JSON.parse(JSON.stringify(result.messages));
				result.messages = [];
				originalMessages.forEach(message => {
					// if changedLineNumbers is not between message.line and message.endLine
					changedLineNumbers.forEach(lineNo => {
						if (
							lineNo >= message.line &&
							lineNo <= message.endLine
						) {
							// Keep this one
							message.path = filePath;
							result.messages.push(message);
						}
					});
				});
				results.push(result);
			}
		});
	}

	return results;
}

// @ts-ignore
const pathToThisFile = resolve(fileURLToPath(import.meta.url));
const pathPassedToNode = resolve(process.argv[1]);

if (pathToThisFile.includes(pathPassedToNode)) {
	// @ts-ignore
	const report = await eslintChecks(args.changed);
	// parse report using eslint
	// @ts-ignore
	const formatter = await eslint.loadFormatter("stylish");
	const reportText = formatter.format(report);
	console.log(reportText);
}

export default eslintChecks;


code:
/**
 * @file This ports the non twine sanity checks from sanityCheck.sh into javascript.
 * Only checks .js and .md files
 *
 * Can be called from the command line
 *
 * node devTools/scripts/extraChecks.js
 * node devTools/scripts/extraChecks.js --changed
 *
 * Or imported as shown below
 *
 * import extraChecks from './extraChecks.js';
 * const changed = false;
 * /** @type{Array<string>} *\/
 * const report = extraChecks(changed);
 */

// This file uses Regex for a lot of its checks.
// A great resource for Regex is here: https://regex101.com/

import detectChanges from './detectChanges.js';

// @ts-ignore
import jetpack from 'fs-jetpack';
import yargs from "yargs";
import {hideBin} from "yargs/helpers";
import {resolve} from 'path';
import {fileURLToPath} from 'url';
import indefinite from "indefinite";
// @ts-ignore
import path from "path";

const customArticles = [
	// list of words with their articles. If the article used doesn't match this list, return an error
	// unlisted words are sent to indefinite (https://www.npmjs.com/package/indefinite) for checking
	// a word can be listed with both articles
	// all entries will be matched by exact case (except for the first letter)
	// "A house" and "a house" are the same; but "a House" and "a house" are different
	"a FCTV",
	"a MILF",
	"a SHIT",
	"a MUCH",
	// TODO:@franklygeorge move this to settings.json
]
	.map((entry) => entry.slice(0, 1).toLowerCase() + entry.slice(1));

/**
 * @typedef {object} LineObject
 * @property {string} line
 * @property {number} lineNumber
 */

const args = yargs(hideBin(process.argv))
	.showHelpOnFail(true)
	.option('changed', {
		type: 'boolean',
		description: 'Only check changed files',
		default: false,
	})
	.parse();

/**
 * Runs extra sanity checks of files
 * @param {boolean} changed If true, only check changed files. If false, check all files.
 * @returns {Array<string>}
 */
function extraChecks(changed = false) {
	/** @type {Array<string>} */
	let errors = [];

	/** @type {Array<string>} */
	let files = [];

	if (args.changed === true) {
		// get list of changed files
		files = detectChanges.changedFiles().filter(file => (
			file.endsWith(".js") ||
			file.endsWith(".md")
		));
	} else {
		// get list of all javascript and markdown files
		files = jetpack.find(".", {matching: "*.{js,md}"});
	}

	// filter out any files not in js/ or src/
	files = files.filter(file => file.startsWith("js") || file.startsWith("src"));

	files.forEach(file => {
		/** @type {string} */
		const content = jetpack.read(file, "utf8");

		if (content === undefined) {
			// skip file
			return;
		}

		content.split("\n").forEach((line, index) => {
			const lineNumber = index +1;
			// TODO:@franklygeorge ignoring lines based off comments
			// TODO:@franklygeorge prettier/more readable error messages

			// Raises an error if @@color: is found in the line
			if (line.includes("@@color:") && !line.includes("@@color:rgb(")) {
				errors.push(
					`"Invalid color code. Should be @@.[color], @@.red for example." at .${path.sep}${file}:${lineNumber}`
				);
			}

			// Raises an error if there is punctuation following a </span>
			if (line.match(/<\/span>(\.|,|;|:)/) !== null) {
				errors.push(
					`"</span> should have punctuation in front of it not behind it" at .${path.sep}${file}:${lineNumber}`
				);
			}

			// Raises an error if there is a class="something" without any quotation marks
			// invalid: class=something || class=something"
			// valid: class="something"
			if (line.match(/<.+class=[^\\"']/) !== null) {
				errors.push(
					`"Missing quotation marks around class selector" at .${path.sep}${file}:${lineNumber}`
				);
			}

			// Raises an error if a word exists twice in a row
			// invalid: word word || a a || you you || on on
			// valid: word words || let t || you you're || strap-on on
			let duplicateWordList = line.match(/(^|\s)([a-zA-Z]+)\s\2(\s|$)/g);

			// remove duplicates from list
			// @ts-expect-error invalid typescript error message
			duplicateWordList = [...new Set(duplicateWordList)];

			if (duplicateWordList !== null) {
				duplicateWordList.forEach((group) => {
					if (group === undefined || group === null || group === "") { return; }

					const groupLower = group.trim().toLowerCase();

					if (groupLower !== "in in") {
						errors.push(`"Duplicate words" at .${path.sep}${file}:${lineNumber}`);
					}
				});
			}

			// Checks for a letter that repeats twice, with some exceptions
			// invalid: aa || ee || ll
			// valid: ann || cc || mm || xx || II
			// skips any line that starts with "for ", "if ", "else ", and "let "
			const doubleLetterList = line.match(/(\s|^)([a-zA-Z])\2\b(\s|\n|$)/g);

			if (doubleLetterList !== null) {
				doubleLetterList.forEach((group) => {
					const groupLower = group.trim().toLowerCase();

					if (
						groupLower.length === 2 &&
						groupLower !== "cc" && groupLower !== "mm" && groupLower !== "xx" &&
						group.trim() !== "II" &&
						!line.startsWith("for ") && !line.startsWith("if ") &&
						!line.startsWith("else ") && !line.startsWith("let ")
					) {
						errors.push(`"Double letters" at .${path.sep}${file}:${lineNumber}`);
					}
				});
			}

			// Raises an error if "a an" or "an a" exists in the line
			if (line.toLowerCase().match(/(^|\s|\n)(a an|an a)($|\s|\n)/g) !== null) {
				errors.push(`"Double articles" at .${path.sep}${file}:${lineNumber}`);
			}

			// Raises an error if the article is potentially incorrect for the word following it
			// Uses customArticles and indefinite (https://www.npmjs.com/package/indefinite) for checking
			const articleWords = line.match(/(^|\s)(a|an) \w+/g);

			if (articleWords!== null) {
				articleWords.forEach((wordWithAAn) => {
					wordWithAAn = wordWithAAn.trim();

					const wordWithAAnLowerFirst = wordWithAAn.slice(0, 1).toLowerCase() + wordWithAAn.slice(1);
					const word = wordWithAAn.replace(/(^|\s)(a|an) /, "").trim();
					const aAn = wordWithAAn.replace(word, "").trim();

					if (
						aAn === "" || word === null || word === undefined || word.length < 2 ||
						customArticles.indexOf(wordWithAAnLowerFirst) !== -1
					) {
						// do nothing
					} else if (wordWithAAnLowerFirst !== indefinite(word)) {
						errors.push(`"Potentially incorrect article" at .${path.sep}${file}:${lineNumber}`);
					}
				});
			}

			// Raises an error if the line has a word that has a dollar sign in it
			// invalid: b$dding
			// valid: $bidding || bidding$
			if (line.match(/\w\$\w/) !== null) {
				errors.push(`"Mid-word $" at .${path.sep}${file}:${lineNumber}`);
			}
		});
	});

	// remove duplicates from errors
	errors = [...new Set(errors)];

	if (args.changed === true) {
		const changedLines = detectChanges.changedLines();
		errors.filter(error => {
			// filter out errors for unchanged lines
			// file path is between ` at .${path.sep}` and ":"
			let file = error.match(/\.(\\|\/)\S+?:/)[0].replace("." + path.sep, "").replace(":", "");
			if (file in changedLines) {
				// line is from ":" to the end of the line and is only digits
				let lineNumber = error.match(/\d+?$/)[0];
				if (lineNumber in changedLines[file]) {
					return true;
				}
			}
			return false;
		});
	}

	return errors;
}

// @ts-ignore
const pathToThisFile = resolve(fileURLToPath(import.meta.url));
const pathPassedToNode = resolve(process.argv[1]);

if (pathToThisFile.includes(pathPassedToNode)) {
	const report = extraChecks(args.changed);
	if (report.length > 0) {
		console.log("");
		console.log(report.join("\n"));
		console.log("");
		console.log(`Extra sanity checks found ${report.length} issues.`);
	} else {
		console.log("");
		console.log("No extra sanity check issues found.");
	}
}

export default extraChecks;


code:
/**
 * @file Spell checks files using cspell
 * Only checks .js and .md files
 *
 * Can be called from the command line
 *
 * node devTools/scripts/spellingChecks.js
 * node devTools/scripts/spellingChecks.js --changed
 *
 * Or imported as shown below
 *
 * import spellingChecks from './spellingChecks.js';
 * const changed = false;
 * /** @type{Array<string>} *\/
 * const report = spellingChecks(changed);
 */

import detectChanges from './detectChanges.js';

import {execSync} from 'child_process';
import yargs from "yargs";
import {hideBin} from "yargs/helpers";
import {resolve} from 'path';
import {fileURLToPath} from 'url';
// @ts-ignore
import jetpack from 'fs-jetpack';
import stripAnsi from "strip-ansi";

const args = yargs(hideBin(process.argv))
	.showHelpOnFail(true)
	.option('changed', {
		type: 'boolean',
		description: 'Only check changed files',
		default: false,
	})
	.parse();

/**
 * Runs spelling checks of files
 * @param {boolean} changed If true, only check changed files. If false, check all files.
 * @returns {Array<string>}
 */
function spellingChecks(changed = false) {
	const cspellPath = "cspell.json";

	// sort words in cspell.json by alphabetical order
	// also makes them lowercase
	if (jetpack.exists(cspellPath) === "file") {
		const cspell = jetpack.read(cspellPath, "json");

		// if cspell doesn't have a words property create it
		if (!cspell.hasOwnProperty("words")) {
			cspell.words = [];
		}

		// if cspell doesn't have a ignoreWords property create it
		if (!cspell.hasOwnProperty("ignoreWords")) {
			cspell.ignoreWords = [];
		}

		// if cspell doesn't have a flagWords property create it
		if (!cspell.hasOwnProperty("flagWords")) {
			cspell.flagWords = [];
		}

		// cspell.json to lower case
		cspell.words = cspell.words.map(word => word.toLowerCase());
		cspell.ignoreWords = cspell.ignoreWords.map(word => word.toLowerCase());
		cspell.flagWords = cspell.flagWords.map(word => word.toLowerCase());

		// sort cspell.json by alphabetically order
		cspell.words.sort();
		cspell.ignoreWords.sort();
		cspell.flagWords.sort();

		// save cspell.json
		jetpack.write(cspellPath, cspell, {jsonIndent: 4, atomic: true});
	}

	/** @type {Array<string>} */
	let errors = [];

	let command = `npx cspell --show-context --show-suggestions --gitignore --color "**/*.{js,md}"`;

	if (args.changed === true) {
		// only change files
		let files = detectChanges.changedFiles();
		// filter out any files not in js/ or src/
		files = files.filter(file => file.startsWith("js") || file.startsWith("src"));
		// filter out any files that are not javascript or markdown files
		files = files.filter(file => file.endsWith(".js") || file.endsWith(".md"));
		// if no files return errors
		if (files.length === 0) {
			return errors;
		}
		command = `npx cspell --show-context --show-suggestions --gitignore --color "${files.join('" "')}"`;
	}

	// cSpell will error if there are any misspelled words
	let result = "";
	try {
		result = execSync(command, {maxBuffer: 1024 * 1024 * 1024}).toString();
	} catch (e) {
		e.output.forEach(out => {
			if (out === null) { return; }
			result += "\n" + out.toString();
		});
	}

	errors = result.split("\n").filter(
		line => stripAnsi(line.trim()).startsWith(".")
	);

	// remove duplicates from errors
	errors = [...new Set(errors)];

	if (args.changed === true) {
		const changedLines = detectChanges.changedLines();
		errors.filter(error => {
			let cleanError = stripAnsi(error);
			// filter out errors for unchanged lines
			// file path is from the start of the line to :
			let file = cleanError.match(/^[^:]+/)[0].slice(2);
			if (file in changedLines) {
				// line is from first : to second :
				let lineNumber = cleanError.split(":")[1];
				if (lineNumber in changedLines[file]) {
					return true;
				}
			}
			return false;
		});
	}

	return errors;
}

// @ts-ignore
const pathToThisFile = resolve(fileURLToPath(import.meta.url));
const pathPassedToNode = resolve(process.argv[1]);

if (pathToThisFile.includes(pathPassedToNode)) {
	const report = spellingChecks(args.changed);
	if (report.length > 0) {
		console.log("");
		console.log(report.join("\n"));
		console.log("");
		console.log(`cSpell found ${report.length} issues.`);
	} else {
		console.log("");
		console.log("cSpell found no issues.");
	}
}

export default spellingChecks;


code:
'use strict';

// Copied from https://github.com/tcql/node-yesno/blob/master/yesno.js
// as to not have it as a node dependency

// const readline = require('readline');
import readline from "readline";

const options = {
    yes: [ 'yes', 'y' ],
    no:  [ 'no', 'n' ]
};

function defaultInvalidHandler ({ question, defaultValue, yesValues, noValues }) {
    var yValues = (yesValues || options.yes);
    var nValues  = (noValues || options.no);

    process.stdout.write('\nInvalid Response.\n');
    process.stdout.write('Answer either yes : (' + yValues.join(', ') + ') \n');
    process.stdout.write('Or no: (' + nValues.join(', ') + ') \n\n');
}

async function ask ({ question, defaultValue, yesValues, noValues, invalid }) {
    if (!invalid || typeof invalid !== 'function')
        invalid = defaultInvalidHandler;

    var yValues = (yesValues || options.yes).map((v) => v.toLowerCase());
    var nValues  = (noValues || options.no).map((v) => v.toLowerCase());

    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });

    return new Promise(function (resolve, reject) {
        rl.question(question + ' ', async function (answer) {
            rl.close();

            const cleaned = answer.trim().toLowerCase();

            if (cleaned == '' && defaultValue != null)
                return resolve(defaultValue);

            if (yValues.indexOf(cleaned) >= 0)
                return resolve(true);

            if (nValues.indexOf(cleaned) >= 0)
                return resolve(false);

            invalid({ question, defaultValue, yesValues, noValues });
            const result = await ask({ question, defaultValue, yesValues, noValues, invalid });
            resolve(result);
        });
    });
}

// module.exports = ask;
export {ask}


code:
#!/bin/bash
# git push --set-upstream origin origin/pregmod-master / git checkout -b pregmod-master --track origin/pregmod-master
mainBranch="pregmod-master"
previousBranch="Not found"

function displayHelp() {
cat <<HelpText
Usage: $0 [OPTION]...
Always pushes commit(s) / the current branch
Order of operation: new (if enabled) -> push (if not closed after creating a new branch) -> sync (if enabled) -> change branch (if enabled)
 -> delete previous branch (if enabled)

Options:
 -s, --sync      : Sync upstream/$mainBranch
 -d, --delete    : Delete previous branch
 -m, --move      : Change current branch to either;
  supplied / previous, the choice currently
  follows the flow as written i.e supplied -> previous
 -b, --branch X  : Set supplied branch to X
 -c, --create Y  : Create the new branch Y
 -h, --help      : Show this help text

 e.g.
 Before:
  git status
  On branch XYZ
  Your branch is ahead of 'origin/XYZ' by 5 commits.

  git branch --list
  * XYZ
  $mainBranch
  .....

 During:
  $0 -b $mainBranch -c -d
  Branches: Supplied ($mainBranch) / previous (XYZ)
  Sync Master? Y/N: N

 After:
  git branch --list
  * $mainBranch
  .....

  All five new commits for branch XYZ were pushed prior to the switch over and
   then afterwards the local copy of branch XYZ was deleted.
HelpText
}

if [[ $(git rev-parse --quiet --abbrev-ref @{-1} 2>/dev/null) && $? -eq 0 ]]; then
	previousBranch=$(git rev-parse --abbrev-ref @{-1})
fi

if [[ "$#" -eq 0 ]]; then echo "For more options see $0 -h."; fi
while [[ "$1" ]]; do
	case $1 in
	-s | --sync)
		sync="true"
		;;
	-d | --delete)
		delete="true"
		;;
	-m | --move)
		change="true"
		;;
	-b | --branch)
		tgtBranch=$2
		shift
		;;
	-c | --create)
		newBranch=$2
		shift
		;;
	-h | --help)
		displayHelp
		exit 0
		;;
	*)
		echo "Unknown argument $1."
		displayHelp
		exit 1
		;;
	esac
	shift
done

if [[ "$newBranch" ]]; then
	if [[ $(git stash show 2>/dev/null | echo $?) -gt 0 ]]; then git stash save *; fi
	git checkout -qb $newBranch
	if [[ $(git stash show 2>/dev/null | echo $?) -gt 0 ]];then
		git stash show
		read -p "Pop the latest stash? (y/n): " stash
		[ "$stash" == "y" ] && git stash pop;
	fi
	read -p "Exit? (y/n): " exit
	[ "$exit" == "y" ] && exit;
fi

git push -q 2>/dev/null || git push -qu "origin" HEAD 1>/dev/null
echo "Branches: Supplied ($tgtBranch) / previous ($previousBranch)"

if [[ "$sync" ]]; then
	if [[ $(git stash show 2>/dev/null | echo $?) -gt 0 ]]; then git stash save *; fi
	git checkout -q origin/$mainBranch && git reset --hard upstream/$mainBranch -q && git push -q && git checkout -q @{-1}
	if [[ $(git stash show 2>/dev/null | echo $?) -gt 0 ]]; then git stash pop; fi
fi
if [[ "$change" ]]; then
	if [[ "$tgtBranch" ]]; then git checkout -q origin/$tgtBranch; else git checkout -q @{-1}; fi
fi
if [[ "$delete" ]]; then git branch -qD @{-1}; fi


code:
declare namespace FC {
	namespace RA {

		interface GenericNumericTarget<T> {
			cond: "==" | ">=" | "<=" | ">" | "<";
			val: T;
		}
		type NumericTarget = GenericNumericTarget<number>;
		type ExpressiveNumericTarget = GenericNumericTarget<number | string>;

		interface RuleConditionEditorArguments {
			activation: PostFixRule;
			advancedMode: boolean;
		}

		interface RuleConditions extends RuleConditionEditorArguments{
			selectedSlaves: number[];
			excludedSlaves: number[];
			applyRuleOnce: boolean;
		}

		interface RuleSurgerySettings {
			voice: number;
			eyes: number;
			hears: number;
			smells: number;
			tastes: number;
			lactation: number;
			prostate: number;
			cosmetic: number;
			accent: number;
			shoulders: number;
			shouldersImplant: number;
			boobs: NumericRange;
			boobsImplantTypes: SizingImplantType[];
			boobsImplantAllowReplacing: boolean;
			hips: number;
			hipsImplant: number;
			butt: NumericRange;
			buttImplantTypes: SizingImplantType[];
			buttImplantAllowReplacing: boolean;
			faceShape: FaceShape;
			lips: NumericRange;
			holes: number;
			hair: number;
			bodyhair: number;
			vasectomy: boolean;
			bellyImplant: string;
			tummy: number;
			earShape: number;
			horn: number;
		}

		interface RuleGrowthSetters {
			boobs: ExpressiveNumericTarget;
			butt: ExpressiveNumericTarget;
			lips: ExpressiveNumericTarget;
			dick: ExpressiveNumericTarget;
			balls: ExpressiveNumericTarget;
			intensity: number;
		}

		interface RuleReleaseSetters {
			masturbation: number;
			partner: number;
			facilityLeader: number;
			family: number;
			slaves: number;
			master: number;
		}

		//type Piercings = ;

		interface RuleSetters {
			releaseRules: RuleReleaseSetters;
			lactationRules: WithNone<"induce" | "maintain"> | null;
			mobilityRules: Rules.Mobility;
			restRules: FC.Rules.Rest;
			toyHole: FC.ToyHole;
			clitSetting: SmartPiercingSetting;
			clitSettingXY: number;
			clitSettingXX: number;
			clitSettingEnergy: number;
			speechRules: Rules.Speech;
			clothes: FC.Clothes;
			collar: FC.Collar;
			faceAccessory: string;
			mouthAccessory: WithNone<MouthAccessory>;
			shoes: WithNone<Shoes>;
			armAccessory: string;
			legAccessory: string;
			chastityVagina: number;
			chastityAnus: number;
			chastityPenis: number;
			virginAccessory: string;
			aVirginAccessory: string;
			vaginalAccessory: string;
			aVirginDickAccessory: string;
			dickAccessory: string;
			bellyAccessory: FC.BellyAccessory;
			buttplug: string;
			aVirginButtplug: string;
			vaginalAttachment: string;
			buttplugAttachment: string;
			iris: string;
			pupil: string;
			sclera: string;
			makeup: number;
			nails: number;
			hColor: string;
			hLength: number;
			haircuts: number;
			hStyle: FC.HairStyle;
			eyebrowHColor: string;
			eyebrowHStyle: string;
			eyebrowFullness: FC.EyebrowThickness;
			markings: "remove beauty marks" | "remove birthmarks" | "remove both";
			pubicHColor: string;
			pubicHStyle: string;
			piercing: InstanceType<typeof App.Entity.completePiercingStateRA>;
			vaginaLube: number;
			boobsTat: string | number;
			buttTat: string | number;
			vaginaTat: string | number;
			dickTat: string | number;
			lipsTat: string | number;
			anusTat: string | number;
			shouldersTat: string | number;
			armsTat: string | number;
			legsTat: string | number;
			backTat: string | number;
			stampTat: string | number;
			birthsTat: string | number;
			abortionTat: string | number;
			pitRules: number;
			arenaRules: number;
			curatives: number;
			livingRules: Rules.Living;
			relationshipRules: Rules.Relationship;
			standardPunishment: Rules.Punishment;
			standardReward: Rules.Reward;
			weight: NumericRange;
			diet: string;
			dietCum: 0 | 1 | 2;
			dietMilk: FC.dietMilkType;
			onDiet: number;
			muscles: NumericTarget;
			XY: number;
			XX: number;
			gelding: number;
			preg: boolean;
			abortion: string[];
			growth: RuleGrowthSetters;
			hyper_drugs: number;
			aphrodisiacs: number;
			autoBrand: number;
			pornFeed: number;
			pornFameSpending: number;
			dietGrowthSupport: number;
			eyewear: string;
			earwear: string;
			setAssignment: Assignment;
			facilityRemove: boolean;
			removalAssignment: Assignment;
			surgery: RuleSurgerySettings;
			underArmHColor: string;
			underArmHStyle: string;
			drug: FC.Drug;
			eyes: string;
			pregSpeed: string;
			bellyImplantVol: number;
			teeth: string;
			label: string;
			removeLabel: string;
			skinColor: string;
			inflationType: FC.InflationLiquid;
			brandTarget: string;
			brandDesign: string;
			scarTarget: string;
			scarDesign: string;
			hornColor: string;
			labelTagsClear: boolean;
			choosesOwnClothes: 0 | 1;
			pronoun: number;
			posePrompt: string;
			positivePrompt: string;
			negativePrompt: string;
			overridePrompts: boolean;
		}

		interface Rule {
			ID: string;
			name: string;
			condition: RuleConditions;
			set: RuleSetters;
		}

		type PostFixRule = Array<string | number | boolean>
	}
}


code:
declare namespace FC {

    type ColorHex = {
        amaranth: "#E52B50";
        amethyst: "#9966CC";
        aquamarine: "#7FFFD4";
        azure: "#007FFF";
        citrine: "#e4d00a";
        crimson: "#DC143C";
        emerald: "#50C878";
        gold: "#ffd700";
        ivory: "#fffff0";
        jade: "#00a86b";
        platinum: "#e5e4e2";
        onyx: "#0f0f0f";
        ruby: "#cc1057";
        sapphire: "#0f52ba";
        silver: "#c0c0c0";
   }

    type PatternColor = "black" | "blue" | "brown" | "green" | "grey" | "orange" | "pink" | "purple" | "red" | "white" | "yellow"
    type PatternedEars = "leopard" | "tiger" | "jaguar"
    type PatternedTails = "leopard" | "tiger" | "jaguar" | "tanuki" | "raccoon" | "gazelle"
    type PatternedAppendages = "moth"

}


code:
declare namespace FC {
	namespace SecExp {

		interface UnitData {
			troops: number,
			maxTroops: number,
			equip: number
		}

		interface PlayerUnitData extends UnitData {
			active: number,
			ID: number,
		}

		interface PlayerHumanUnitData extends PlayerUnitData {
			platoonName: string,
			training: number,
			loyalty: number,
			cyber: number,
			medics: number,
			SF: number,
			commissars: number,
			battlesFought: number,
		}

		type PlayerHumanUnitType = "bots" | "slaves" | "militia" | "mercs" | "SF";
		type PlayerHumanUnitTypeMod = "bots" | "slaves" | "militia" | "mercs";
		type PlayerHumanUnitTypeModHuman = "slaves" | "militia" | "mercs";
		type EnemyUnitType = "raiders" | "free city" | "old world" | "freedom fighters";
	}
}


code:
declare namespace FC {
	namespace UI {
		namespace DOM {
			namespace Widgets {}
		}
		namespace Hotkeys {}
		namespace View {}
		namespace SlaveSummary {
			type AppendRenderer = (slave: FC.SlaveState, parentNode: Node) => void;

			interface AbbreviationState {
				clothes: number;
				devotion: number;
				diet: number;
				drugs: number;
				genitalia: number;
				health: number;
				hormoneBalance: number;
				mental: number;
				nationality: number;
				origins: number;
				physicals: number;
				race: number;
				rules: number;
				rulesets: number;
				skills: number;
			}

			interface State {
				abbreviation: AbbreviationState;
			}
		}
	}
}

declare type PassageLinkMap = Pick<HTMLElementTagNameMap, 'a' | 'audio' | 'img' | 'source' | 'video'>


code:
declare namespace FC {
	/**
	 * This interface defines all (almost, see ArcologyStateFSPeculiarities below) the FS-related
	 * arcology properties.
	 * * `research`: FS provides -Research property [Bool], otherwise has to be set to `undefined`
	 * * `SMR`: FS provides SMR policy, [Bool], otherwise has to be set to `undefined`
	 * * `policy`: string union of boolean laws and other policies, otherwise has to be set to `undefined`
	 * * `choice`: string union of numeric laws and choices, that may have other values except `0` and `1`.
	 */
	interface FutureSocietyIdMap {
		FSSupremacist: {
			noun: "Racial Supremacism", adj: "Supremacist", deco: "Supremacist",
			research: undefined, SMR: true, policy: "LawME", choice: undefined
		};
		FSSubjugationist: {
			noun: "Racial Subjugationism", adj: "Subjugationist", deco: "Subjugationist",
			research: undefined, SMR: true, policy: "LawME", choice: undefined
		};
		FSGenderRadicalist: {
			noun: "Gender Radicalism", adj: "Gender Radicalist", deco: "Gender Radicalist",
			research: true, SMR: undefined, policy: "LawBeauty", choice: "LawFuta"
		};
		FSGenderFundamentalist: {
			noun: "Gender Fundamentalism", adj: "Gender Fundamentalist", deco: "Gender Fundamentalist",
			research: true, SMR: true, policy: "LawBimbo" | "LawBeauty", choice: undefined
		};
		FSDegradationist: {
			noun: "Degradationism", adj: "Degradationist", deco: "Degradationist",
			research: true, SMR: true, policy: "Law", choice: undefined
		};
		FSPaternalist: {
			noun: "Paternalism", adj: "Paternalist", deco: "Paternalist",
			research: true, SMR: true, policy: "Law", choice: undefined
		};
		FSBodyPurist: {
			noun: "Body Purism", adj: "Body Purist", deco: "Body Purist",
			research: true, SMR: true, policy: "Law" | "CatLaw", choice: undefined
		};
		FSTransformationFetishist: {
			noun: "Transformation Fetishism", adj: "Transformation Fetishist", deco: "Transformation Fetishist",
			research: true, SMR: true, policy: undefined, choice: undefined
		};
		FSYouthPreferentialist: {
			noun: "Youth Preferentialism", adj: "Youth Preferentialist", deco: "Youth Preferentialist",
			research: true, SMR: true, policy: "Law", choice: undefined
		};
		FSMaturityPreferentialist: {
			noun: "Maturity Preferentialism", adj: "Maturity Preferentialist", deco: "Maturity Preferentialist",
			research: true, SMR: true, policy: "Law", choice: undefined
		};
		FSSlimnessEnthusiast: {
			noun: "Slimness Enthusiasm", adj: "Slimness Enthusiast", deco: "Slimness Enthusiast",
			research: true, SMR: true, policy: "Law" | "FoodLaw", choice: undefined
		};
		FSAssetExpansionist: {
			noun: "Asset Expansionism", adj: "Asset Expansionist", deco: "Asset Expansionist",
			research: true, SMR: true, policy: undefined, choice: undefined
		};
		FSPastoralist: {
			noun: "Pastoralism", adj: "Pastoralist", deco: "Pastoralist",
			research: true, SMR: true, policy: "Law", choice: undefined
		};
		FSCummunism: {
			noun: "Cummunism", adj: "Cummunist", deco: undefined,
			research: true, SMR: undefined, policy: undefined, choice: undefined
		};
		FSPhysicalIdealist: {
			noun: "Physical Idealism", adj: "Physical Idealist", deco: "Physical Idealist",
			research: true, SMR: true, policy:	"Law" | "StrongFat", choice: undefined
		};
		FSHedonisticDecadence: {
			noun: "Decadent Hedonism", adj: "Decadent Hedonist", deco: "Hedonistic",
			research: "" | "Diet", SMR: true, policy: "Law" | "Law2", choice: "StrongFat"
		};
		FSChattelReligionist: {
			noun: "Chattel Religionism", adj: "Chattel Religionist", deco: "Chattel Religionist",
			research: undefined, SMR: true, policy: "Law" | "Law2", choice: undefined
		};
		FSNull: {
			noun: "Multiculturalism", adj: "Multiculturalist", deco: undefined,
			research: undefined, SMR: undefined, policy: undefined, choice: undefined
		};
		FSIncestFetishist: {
			noun: "Incest Fetishism", adj: "Incest Fetishist", deco: undefined,
			research: true, SMR: undefined, policy: undefined, choice: undefined
		};
		FSRomanRevivalist: {
			noun: "Roman Revivalism", adj: "Roman Revivalist", deco: "Roman Revivalist",
			research: undefined, SMR: true, policy: "Law", choice: undefined
		};
		FSNeoImperialist: {
			noun: "Neo-Imperialism", adj: "Neo-Imperialist", deco: "Neo-Imperialist",
			research: undefined, SMR: true, policy: "Law1" | "Law2", choice: undefined
		};
		FSEgyptianRevivalist: {
			noun: "Egyptian Revivalism", adj: "Egyptian Revivalist", deco: "Egyptian Revivalist",
			research: undefined, SMR: true, policy: "Law" | "IncestPolicy", choice: undefined
		};
		FSEdoRevivalist: {
			noun: "Edo Revivalism", adj: "Edo Revivalist", deco: "Edo Revivalist",
			research: undefined, SMR: true, policy: "Law", choice: undefined
		};
		FSArabianRevivalist: {
			noun: "Arabian Revivalism", adj: "Arabian Revivalist", deco: "Arabian Revivalist",
			research: undefined, SMR: true, policy: "Law", choice: undefined
		};
		FSChineseRevivalist: {
			noun: "Chinese Revivalism", adj: "Chinese Revivalist", deco: "Chinese Revivalist",
			research: undefined, SMR: true, policy: "Law", choice: undefined
		};
		FSAztecRevivalist: {
			noun: "Aztec Revivalism", adj: "Aztec Revivalist", deco: "Aztec Revivalist",
			research: undefined, SMR: true, policy: "Law", choice: undefined
		};
		FSAntebellumRevivalist: {
			noun: "Antebellum Revivalism", adj: "Antebellum Revivalist", deco: "Antebellum Revivalist",
			research: undefined, SMR: true, policy: "Law1" | "Law2", choice: undefined
		};
		FSRepopulationFocus: {
			noun: "Repopulation Focus", adj: "Repopulationist", deco: "Repopulationist",
			research: true, SMR: true, policy: "Law", choice: undefined
		};
		FSRestart: {
			noun: "Eugenics", adj: "Eugenics", deco: "Eugenics",
			research: true, SMR: true, policy: "Law", choice: undefined
		};
		FSIntellectualDependency: {
			noun: "Intellectual Dependency", adj: "Intellectual Dependency", deco: "Intellectual Dependency",
			research: true, SMR: true, policy: "Law" | "LawBeauty", choice: undefined
		};
		FSSlaveProfessionalism: {
			noun: "Slave Professionalism", adj: "Slave Professional", deco: "Slave Professionalism",
			research: true, SMR: true, policy: "Law", choice: undefined
		};
		FSPetiteAdmiration: {
			noun: "Petite Admiration", adj: "Petite Admiration", deco: "Petite Admiration",
			research: true, SMR: true, policy: "Law" | "Law2", choice: undefined
		};
		FSStatuesqueGlorification: {
			noun: "Statuesque Glorification", adj: "Statuesque Glorification", deco: "Statuesque Glorification",
			research: true, SMR: true, policy: "Law" | "Law2", choice: undefined
		}
	}

	type FutureSociety = keyof FutureSocietyIdMap;
	type FutureSocietyNoun = FutureSocietyIdMap[keyof FutureSocietyIdMap]["noun"];
	type FutureSocietyAdj = FutureSocietyIdMap[keyof FutureSocietyIdMap]["adj"];
	type FutureSocietyDeco = FutureSocietyIdMap[keyof FutureSocietyIdMap]["deco"] | "standard";

	type FutureSocietyWithResearchMap = {
		[K in keyof FutureSocietyIdMap]: FutureSocietyIdMap[K]["research"] extends true ? `${K}Research`
		: FutureSocietyIdMap[K]["research"] extends string ? `${K}${FutureSocietyIdMap[K]["research"]}Research`
		: never;
	}

	type FutureSocietyPolicy = {
		[K in keyof FutureSocietyIdMap]: FutureSocietyIdMap[K]["policy"] extends string ?
		`${K}${FutureSocietyIdMap[K]["policy"]}` : never;
	}

	type FutureSocietyChoice = {
		[K in keyof FutureSocietyIdMap]: FutureSocietyIdMap[K]["choice"] extends string ?
		`${K}${FutureSocietyIdMap[K]["choice"]}` : never;
	}

	type FutureSocietyHasResearchAny = FutureSocietyWithResearchMap[FutureSociety];
	type FutureSocietyPolicyAny = FutureSocietyPolicy[FutureSociety];
	type FutureSocietyChoiceAny = FutureSocietyChoice[FutureSociety];

	type FSDecoName<T extends FutureSociety> = FutureSocietyIdMap[T]["deco"] extends undefined ? never : `${T}Decoration`
	type FSSMRName<T extends FutureSociety> = FutureSocietyIdMap[T]["SMR"] extends undefined ? never : `${T}SMR`

	type FSPolicyValue = number | null;

	type FSName<T extends string> = T extends `FS${infer Name}` ? Name : never;

	// direction with respect to the player's arcology
	type ArcologyDirection = 0 /* player */ | "east" | "north" | "northeast" | "northwest" | "south" | "southeast" | "southwest" | "west";

	type ArcologyStateBase = {
		name: string;
		direction: ArcologyDirection;
		government: string;
		leaderID: number;
		honeymoon: number;
		prosperity: number;
		ownership: number;
		minority: number;
		PCminority: number;
		demandFactor: number;
		embargo: number;
		embargoTarget: -1|ArcologyDirection;
		influenceTarget: -1|ArcologyDirection;
		influenceBonus: number;
		CyberEconomic: number;
		CyberEconomicTarget: -1|ArcologyDirection;
		CyberReputation: number;
		CyberReputationTarget: -1|ArcologyDirection;
		rival: number;
		childhoodFertilityInducedNCSResearch: Bool;
		weeks: number;
	}

	type ArcologyStateFSPeculiarities = {
		FSSupremacistRace: Zeroable<Race>;
		FSSubjugationistRace: Zeroable<Race>;

		FSEgyptianRevivalistInterest: number;

		FSRepopulationFocusPregPolicy: number;
		FSRepopulationFocusMilfPolicy: number;
		FSRepopulationFocusInterest: number;

		FSEugenicsChastityPolicy: number;
		FSEugenicsSterilizationPolicy: number;
		FSEugenicsInterest: number;

		FSChattelReligionistCreed: number;
	}

	type ArcologyStateFS = ArcologyStateFSPeculiarities &
		Record<FutureSociety, FSPolicyValue> &
		Record<FutureSocietyHasResearchAny, Bool> &
		Record<FSDecoName<FutureSociety>, number> &
		Record<FSSMRName<FutureSociety>, Bool> &
		Record<FutureSocietyPolicyAny, number> &
		Record<FutureSocietyChoiceAny, number>;

	type ArcologyState = ArcologyStateBase & ArcologyStateFS;

	const enum RevivalSocietyFreezeValue {
		Antebellum,
		Arabian,
		Aztec,
		Chinese,
		Edo,
		Egyptian,
		NeoImperial,
		Roman,
	}
	interface RevivalSocietyFreeze extends Record<string, RevivalSocietyFreezeValue> {
		ANTEBELLUM: RevivalSocietyFreezeValue.Antebellum,
		ARABIAN: RevivalSocietyFreezeValue.Arabian,
		AZTEC: RevivalSocietyFreezeValue.Aztec,
		CHINESE: RevivalSocietyFreezeValue.Chinese,
		EDO: RevivalSocietyFreezeValue.Edo,
		EGYPTIAN: RevivalSocietyFreezeValue.Egyptian,
		NEO_IMPERIAL: RevivalSocietyFreezeValue.NeoImperial,
		ROMAN: RevivalSocietyFreezeValue.Roman,
	}

	const enum FSHumanDevelopmentVector {
		/** Positive for mature preference, negative for young */
		Age,
		/** Positive for statuesque preference, negative for petite */
		Height,
		/** Positive for decadence preference negative for physical idealist */
		Weight,
		/** Positive for transformation preference, negative for body purism */
		Modifications,
		/** Positive for expansionist preference, negative for slim */
		Assets,
		/** Positive for professionalism, negative for intellectual dependency */
		Intelligence,
		/** Positive for radicalism, negative for fundamentalism */
		Gender,
		/** Positive for repopulation, negative for eugenics */
		Breeding,
		/** Positive for paternalism, negative for degradationism */
		LifeQuality,
	}

	interface FSHumanDevelopmentVectorFreeze extends Record<string, FSHumanDevelopmentVector> {
		AGE: FSHumanDevelopmentVector.Age,
		HEIGHT: FSHumanDevelopmentVector.Height,
		WEIGHT: FSHumanDevelopmentVector.Weight,
		MODIFICATIONS: FSHumanDevelopmentVector.Modifications,
		ASSETS: FSHumanDevelopmentVector.Assets,
		INTELLIGENCE: FSHumanDevelopmentVector.Intelligence,
		GENDER: FSHumanDevelopmentVector.Gender,
		BREEDING: FSHumanDevelopmentVector.Breeding,
		LIFE_QUALITY: FSHumanDevelopmentVector.LifeQuality,
	}

	interface SlaveSocialEffect {
		/** FS */
		FS: FutureSocietyDeco | "";
		/** positive or negative value (a small integer, or a fraction between -1 and 1) */
		magnitude: number;
		/** for compact/list mode (plain text string) */
		shortDesc: string;
		/** for expanded mode (HTML string) */
		longDesc: string;
		/** true if the effect is only due to the facility she's assigned to */
		creditFacility?: boolean;
	}
}


code:
declare namespace FC {
	type WithNone<T> = T | "none";

	const enum Bool {
		False = 0,
		True = 1
	}

	const enum NoObject {
		Value = 0
	}

	type Zeroable<T> = T | NoObject;
}

type WidenLiterals<T> =
  T extends boolean ? boolean :
  T extends string ? string :
  T extends number ? number :
  T;


code:
declare namespace FC {
	namespace Data {

		namespace Wardrobe {
			interface ClothingOptionBase {
				name: string;
				fs?: string; // FSPolicy;
			}
			interface ClothingOption extends ClothingOptionBase {
				value: string;
			}

			interface UpdatingClothingOption extends ClothingOptionBase {
				updateSlave: DeepPartialSlaveState;
			}
		}
		namespace Pronouns {
			interface Definition {
				pronoun: string;
				possessive: string;
				possessivePronoun: string;
				object: string;
				objectReflexive: string;
				noun: string;
			}
		}

		interface JobDesc {
			position: string;
			assignment: Assignment;
			publicSexUse: boolean;
			fuckdollAccepted: boolean;
			broodmotherAccepted?: boolean;
			/** workers can take part time jobs in addition to their main (full-time) one */
			partTime?: boolean;
		}

		interface ManagerJobDesc extends JobDesc {
			shouldWalk: boolean;
			shouldHold: boolean;
			shouldSee: boolean;
			shouldHear: boolean;
			shouldTalk: boolean;
			shouldThink: boolean;
			requiredDevotion: number;
			/** Applicable careers */
			careers: string[];
			/** Applicable skill name */
			skill: string;
			positionAbbreviation?: string;
		}

		interface FacilityDesc {
			/** Base name for state variables */
			baseName: string;
			/** Generic name for UI (Brothel, Club, etc.)
			 * If null, baseName is used instead
			 */
			genericName: string | null;
			jobs: Record<string, JobDesc>;
			defaultJob: string;
			manager: ManagerJobDesc | null;
			decorated: boolean;
		}

		namespace SlaveSummary {
			interface SmartVibrator {
				setting: {
					off: string,
					submissive: string,
					lesbian: string,
					oral: string,
					humiliation: string,
					anal: string,
					boobs: string,
					sadist: string,
					masochist: string,
					dom: string,
					pregnancy: string,
					vanilla: string,
					all: string,
					none: string,
					monitoring: string,
					men: string,
					women: string,
					"anti-men": string,
					"anti-women": string,
				},
				system: {
					"piercing": string,
					"bullet": string,
					"vibe": string
				}
			}
		}
	}
}


code:
declare namespace FC {
	namespace Desc {
		interface LongSlaveOptions {
			descType?: DescType;
			market?: Zeroable<SlaveMarketName | SpecialMarketName | "starting">;
			marketText?: string; /* extra text to be appended to the first line of the description, dependent on the original market */
			noArt?: boolean;
			/** Allow linking to other passages, for example to other slaves */
			links?:boolean;
		}
	}
}


code:
declare namespace FC {
	export type Upgrade = InstanceType<typeof App.Upgrade>;

	interface IUpgrade {
		/** The variable name of the upgrade. */
		property: string;
		/** Properties pertaining to any tiers available. */
		tiers: IUpgradeTier[];
		/** Any object the upgrade property is part of, if not the default `V`. */
		object?: Object;
	}

	interface IUpgradeTier {
		/** The value `property` must be set to in order to display this tier. */
		value: any;
		/** The value to set `property` to upon purchase of the upgrade, if any. */
		upgraded?: any;
		/** The link text. */
		link?: string;
		/** The text to display for the current tier of the upgrade. */
		text: string;
		/**
		 * How much the upgrade costs.
		 *
		 * If one is not provided, the upgrade will be free.
		 */
		cost?: number;
		/** Any handler to run upon purchase. */
		handler?: () => void;
		/** Any additional information to display upon hover on the link. */
		notes?: string[];
		/**
		 * Any prerequisites that must be met before the upgrade is available.
		 *
		 * If none are given, the upgrade will always be available.
		 */
		prereqs?: boolean[];
		/** Any additional nodes to attach. */
		nodes?: Array<string|HTMLElement|DocumentFragment>;
	}

	interface IFacilityAssignment {
		/** The name of the assignment. */
		position: string;
		/** The assignment type. */
		assignment: Assignment;
		/** Whether the assigned slaves engage in sex with the public. */
		publicSexUse: boolean;
		/** Whether fuckdolls can work here. */
		fuckdollAccepted: boolean;
		/** Whether slaves assigned here can work at another assignment at the same time. */
		partTime?: boolean;
	}

	interface IManagerAssignment {
		/** The name of the assignment. */
		position: string;
		/** The assignment name's abbreviated form, if any. */
		positionAbbreviation?: string;
		/** The assignment type. */
		assignment: Assignment;
		/** A list of careers that count towards past experience. */
		careers: string[];
		/** Which of a slave's manager skills is affected. */
		skill: string;
		/** Whether the assigned slaves engage in sex with the public. */
		publicSexUse: boolean;
		/** Whether fuckdolls can work here. */
		fuckdollAccepted: boolean;
		/** Whether broodmothers can work here. */
		broodmotherAccepted: boolean;
		/** If slaves assigned here must be able to walk. */
		shouldWalk: boolean;
		/** If slaves assigned here must be able to hold items. */
		shouldHold: boolean;
		/** If slaves assigned here must be able to see. */
		shouldSee: boolean;
		/** If slaves assigned here must be able to hear. */
		shouldHear: boolean;
		/** If slaves assigned here must be able to talk. */
		shouldTalk: boolean;
		/** If slaves assigned here must have cognitive function. */
		shouldThink: boolean;
		/** The minimum required devotion level to be assigned here. */
		requiredDevotion: number;
	}

	interface FacilityFramework {
		/** The variable name of the facility. */
		baseName: string;
		/** The generic form of the facility. */
		genericName: string|null;
		/** An object listing all the available positions in the facility. */
		jobs: Record<string, IFacilityAssignment>;
		/** The default job in the facility. */
		defaultJob: string;
		/** Properties pertaining to the facility's manager. */
		manager: IManagerAssignment;
		/** Whether the facility can use FS decorations. */
		decorated: boolean;
	}

	namespace Facilities {
		export type Facility = InstanceType<typeof App.Facilities.Facility>;
		export type Animal = InstanceType<typeof App.Entity.Animal>;

		interface Decoration extends Record<FC.FutureSocietyDeco, string> {}

		interface Rule {
			/** The variable name of the rule. */
			property: string;
			/** Any prerequisites that must be met for the rule to be displayed. */
			prereqs: boolean[];
			/** Properties pertaining to any options available. */
			options: Array<{
				/** The text displayed when the rule is active. */
				text: string;
				/** The link text to set the rule to active. */
				link: string;
				/** The value to set `property` to when the rule is active. */
				value: any;
				/** Any handler to run upon setting the value. */
				handler?: (value: any) => void;
				/** Any additional information to display with on the link. */
				note?: string;
				/** Any prerequisites that must be met for the option to be displayed. */
				prereqs?: boolean[];
			}>;
			/** Any additional nodes to attach. */
			nodes?: Array<string|HTMLElement|DocumentFragment>;
			/** Any object the rule property is part of, if not the default `V`. */
			object?: Object;
		}

		interface Expand {
			/**
			 * The number of spots to add per expansion.
			 *
			 * Defaults to 5.
			 */
			amount?: number;
			/**
			 * How much expanding the facility costs.
			 *
			 * Defaults to 1000 times the number of slaves the facility can currently support.
			 */
			cost?: number;
			/**
			 * A short description of how many slaves are in the facility out of how many possible.
			 *
			 * Defaults to `"${facilityName} can support ${possible} slaves. It currently has ${current} slaves."`.
			 */
			desc?: string;
			/** The maximum number of times the facility can be expanded, if any. */
			maximum?: number;
			/** Any link to remove all slaves from the facility, if not the default. */
			removeAll?: HTMLDivElement;
			/**
			 * The assignment to assign the facility's manager to upon decommission.
			 *
			 * Defaults to `"rest"`.
			 */
			removeManager?: FC.Assignment;
			/**
			 * The assignment to assign the facility's employees to upon decommission.
			 *
			 * Defaults to `"rest"`.
			 */
			removeSlave?: FC.Assignment;
			/**
			 * Whether the facility cannot be expanded.
			 *
			 * Defaults to `false`.
			 */
			unexpandable?: boolean;
		}

		interface Pit {
			/** Defaults to "the Pit" if not otherwise set. */
			name: string;
			// Arena section
			trainingIDs: number[];

			// Pit section
			/**
			 * If there is a fight event at the end of the week.
			 */
			active: boolean
			/** The type of audience the Pit has. */
			audience: "none" | "free" | "paid";
			/** The type of decoration the Pit is using. */
			decoration: FC.FutureSocietyDeco;
			/** An array of the IDs of slaves assigned to the Pit. */
			fighterIDs: number[];
			/** Whether or not a fight has taken place during the week. */
			fought: boolean;
			/**
			 * Base for the max number of fights that can be held each week.
			 * fightNum = 3 + 2 * fightsBase
			 */
			fightsBase: 0 | 1 | 2
			/**
			 * How many can watch the fight. Influences rep/cash generation
			 */
			seats: 0 | 1 | 2
			/**
			 * * 0: no lethal fights allowed
			 * * 1: lethal and nonlethal fights allowed
			 * * 2: only lethal fights allowed
			 */
			lethal: 0 | 1 | 2
			/**
			 * Which virginities will be respected
			 */
			virginities: "none" | "vaginal" | "anal" | "all"
			/**
			 * Whether slaves fighting must be in somewhat fair health and generally able to do combat.
			 */
			minimumHealth: boolean
			/**
			 * Schedule a slave to fight the BG
			 */
			slaveFightingBodyguard: number
			/**
			 * Schedule two slaves to fight each other
			 */
			slavesFighting: [number, number]
		}
	}
}


code:
declare namespace FC {
    interface Loan {
		/** The lender's name. */
		name: 'bank' | 'shark';
		/** The amount they've lent. */
		principal: number;
		/** The week on which repayment is due. */
		deadline: number;
		/** The number of installments remaining until the full amount is paid. */
		installments: number;
		/** The Annual Percentage Rate. */
		apr: number;
		/** The amount of interest the loan will accumulate. */
		interest: number;
		/** The full amount that will be paid. */
		full: number;
	}

	interface SlaveStatisticData {
		/** ID of relevant slave */
		ID: number,
		slaveName: string,
		customLabel: string,
		income: number,
		adsIncome: number,
		rep: number,
		food: number,
		cost: number,
		customers: number,

		milk?:number,
		cum?:number,
		fluid?:number,
	}
}


code:
declare namespace FC {
	/**@deprecated */
	type SlaveStateOrZero = Zeroable<SlaveState>;
	/**@deprecated */
	type HumanStateOrZero = Zeroable<HumanState>;

	type DefaultGameStateVariables = typeof App.Data.defaultGameStateVariables;
	type ResetOnNGPVariables = typeof App.Data.resetOnNGPlus;

	interface Enunciation {
		title: string;
		say: string;
	}

	interface Rival {
		/**
		- 0: Init
		- 1: Not established
		- 2: Established
		- 3: Captured
		- 4: Killed / Accepted offer
		- 5: Enslaved
		 */
		state: number;
		duration: number;
		prosperity: number;
		power: number;
		race?: FC.Race;
		/**
		- 1: Female
		- 2: Male
		 */
		gender?: number;
		ID?: number;
		FS: {
			name: string;
			race?: FC.Race;
			adopted?: number;
		}
		/**
			- 0: Unknown / Gone
			- 1: Announced
			- 2: Rescued
			- 3: Given in. Currently unused, is checked but commented out within src/endWeek/saDevotion.js
			*/
		hostageState: number,
	}

	interface Peacekeepers {
		/**
		- 0: Gone
		- 1: Not established
		- 2: Established
		- 3: Independent
		 */
		state: number;
		generalName?: string;
		strength?: number;
		attitude?: number;
		undermining?: number;
		influenceAnnounced?: number;
		tastes?: string;
	}

	export type RecruiterTarget = "desperate whores" | "young migrants" | "recent divorcees" |
		"expectant mothers" | "dissolute sissies" | "reassignment candidates" | "other arcologies";

	interface DeprecatedGameVariables {
		/** @deprecated */
		surgeryType: string;

		relationLinks?: Record<number, {father: number, mother: number}>;

		spire: number;
		customPronouns?: Record<number, Data.Pronouns.Definition>;
	}

	export type HeadGirlTraining = "health" | "paraphilia" | "soften" | "flaw" | "obedience" |
		"entertain skill" | "oral skill" | "fuck skill" | "penetrative skill" | "anal skill" | "whore skill";

	export interface HeadGirlTrainee {
		ID: number;
		training: HeadGirlTraining;
	}

	export interface ReminderEntry {
		message: string | Node;
		week: number;
		category: string;
		slaveID?: number;
	}

	export interface TrinketData {
		name: string,
		id?: number | null
		extra?: string,
		napkinShape?: string
	}

	/**
	 * These variables shall not be in the game state and there is a hope they will be exterminated in the future
	 */
	interface TemporaryVariablesInTheGameState {
		gameover?: string;
		/** @deprecated */
		returnTo: string;

		slavesToImportMax?: number;

		activeArcologyIdx?: number;

		showAllEntries: {
			costsBudget: number;
			repBudget: number;
		};

		brothelSpots?: number;
		clubSpots?: number;
		dairySpots?: number;
		servantsQuartersSpots?: number;
		brothelSlavesGettingHelp?: number;
		clubSlavesGettingHelp?: number;

		arcadeDemandDegResult?: 1 | 2 | 3 | 4 | 5;

		FarmerDevotionThreshold?: number;
		FarmerDevotionBonus?: number;
		FarmerTrustThreshold?: number;
		FarmerTrustBonus?: number;
		FarmerHealthBonus?: number;

		milkmaidDevotionThreshold?: number;
		milkmaidDevotionBonus?: number;
		milkmaidTrustThreshold?: number;
		milkmaidTrustBonus?: number;
		milkmaidHealthBonus?: number;

		activeSlave: Zeroable<SlaveState>;
		readySlave: Zeroable<SlaveState>;
		newSlavePool: Array<FC.SlaveState>
		event?: InstanceType<typeof App.Events.BaseEvent>;
		AS: number;
		seed?: string;
		applyCareerBonus?: Bool;
		careerBonusNeeded?: number[];
		prostheticsConfig?: string;

		oldLimbs: any;

		heroSlaves: SlaveTemplate[];
		limitedCheatStart?: Bool
		lastCashTransaction?: number;
	}

	export interface GameVariables extends DefaultGameStateVariables, ResetOnNGPVariables,
		DeprecatedGameVariables, TemporaryVariablesInTheGameState {}
}


code:
declare global {
	// extensions
	interface Array<T> {
		includes(needle: any): boolean; // because we use silly unions with 0
	}

	interface Window {
		storyProxy?: object;
	}

	const V: FC.GameVariables;
	function createReadonlyProxy<T>(target: T): Readonly<T>;
	function createCheatProxy<T>(target: T): T;
}

export {}


code:
import {DeepPartial} from "ts-essentials";
declare global {
	export namespace FC {
		export type SlaveState = InstanceType<typeof App.Entity.SlaveState>;
		export type PlayerState = InstanceType<typeof App.Entity.PlayerState>;
		export type AnimalState = InstanceType<typeof App.Entity.Animal>;

		export type DeepPartialSlaveState = DeepPartial<SlaveState>;

		type SlaveStateRequiredAttributes = "ID" | "slaveName";
		export interface SlaveTemplate extends DeepPartial<Omit<SlaveState, SlaveStateRequiredAttributes>>,
			Pick<SlaveState, SlaveStateRequiredAttributes> {
			removedLimbs?: number[];
		}

		export interface GingeredSlave extends SlaveState {
			// note that all members are optional...GingeredSlave and SlaveState are bidirectionally interchangeable
			gingering?: InstanceType<typeof App.Entity.GingeringParameters>;
			beforeGingering?: SlaveState;
		}

		export interface ReportSlave extends SlaveState {
			paraphiliaSatisfied: number;
			pornFameBonus: number;
			inappropriateLactation: number;
			fetishChanged: number;
			slaveUsedRest: number;
		}

		export type SlaveUpdate = DeepPartialSlaveState;

		//#region SlaveState types
		namespace Rules {
			type Living = "spare" | "normal" | "luxurious";
			interface LivingFreezed extends Record<string, Living> {
				LUXURIOUS: 'luxurious';
				NORMAL: 'normal';
				SPARE: 'spare';
			}

			type Rest = "none" | "cruel" | "restrictive" | "permissive" | "mandatory";
			interface RestFreezed extends Record<string, Rest> {
				NONE: "none";
				MIN: "cruel";
				MID: "restrictive";
				MAX: "permissive";
				MANDATORY: "mandatory";
			}

			type Mobility = "restrictive" | "permissive";
			type Speech = "restrictive" | "permissive" | "accent elimination" | "language lessons";
			type Relationship = "restrictive" | "just friends" | "permissive";
			type Lactation = "none" | "induce" | "maintain";
			type Punishment = "confinement" | "whipping" | "chastity" | "situational";
			type Reward = "relaxation" | "drugs" | "orgasm" | "situational" | "confinement";
		}

		type Assignment =
			// Penthouse Assignments
			'rest' | 'please you' | 'take classes' | 'be a servant' | 'whore' | 'serve the public' | 'be a subordinate slave' |
			'get milked' | 'work a glory hole' | 'stay confined' |
			// Leadership Assignments
			'guard you' | 'be your Head Girl' | 'recruit girls' | 'be your agent' | 'live with your agent' |
			// Facility Assignments
			'be confined in the arcade' | 'be the Madam' | 'work in the brothel' | 'be the Wardeness' | 'be confined in the cellblock' |
			'be the DJ' | 'serve in the club' | 'be the Nurse' | 'get treatment in the clinic' | 'be the Milkmaid' | 'work in the dairy' |
			'be the Farmer' | 'work as a farmhand' | 'live with your Head Girl' | 'be your Concubine' | 'serve in the master suite' |
			'be the Matron' | 'work as a nanny' | 'be the Schoolteacher' | 'learn in the schoolroom' | 'be the Stewardess' |
			'work as a servant' | 'be the Attendant' | 'rest in the spa' |
			// Does this one exist?
			'labor in the production line' |
			// Other
			'choose her own job' |
			// Pseudo-jobs
			'@Lurcher' | '@Pit' | "@Arena" | '@be imported' | '@lay in tank';
		interface AssignmentFreeze extends Record<string, Assignment> {
			// Penthouse Assignments
			REST: 'rest';
			FUCKTOY: 'please you';
			CLASSES: 'take classes';
			HOUSE: 'be a servant';
			WHORE: 'whore';
			PUBLIC: 'serve the public';
			SUBORDINATE: 'be a subordinate slave';
			MILKED: 'get milked';
			GLORYHOLE: 'work a glory hole';
			CONFINEMENT: 'stay confined';
			// Leadership Assignments
			BODYGUARD: 'guard you';
			HEADGIRL: 'be your Head Girl';
			RECRUITER: 'recruit girls';
			AGENT: 'be your agent';
			AGENTPARTNER: 'live with your agent';
			// Facility Assignments
			ARCADE: 'be confined in the arcade';
			MADAM: 'be the Madam';
			BROTHEL: 'work in the brothel';
			WARDEN: 'be the Wardeness';
			CELLBLOCK: 'be confined in the cellblock';
			DJ: 'be the DJ';
			CLUB: 'serve in the club';
			NURSE: 'be the Nurse';
			CLINIC: 'get treatment in the clinic';
			MILKMAID: 'be the Milkmaid';
			DAIRY: 'work in the dairy';
			FARMER: 'be the Farmer';
			FARMYARD: 'work as a farmhand';
			HEADGIRLSUITE: 'live with your Head Girl';
			CONCUBINE: 'be your Concubine';
			MASTERSUITE: 'serve in the master suite';
			MATRON: 'be the Matron';
			NURSERY: 'work as a nanny';
			TEACHER: 'be the Schoolteacher';
			SCHOOL: 'learn in the schoolroom';
			STEWARD: 'be the Stewardess';
			QUARTER: 'work as a servant';
			ATTENDANT: 'be the Attendant';
			SPA: 'rest in the spa';
			// Does this one exist?
			BABY_FACTORY: 'labor in the production line';
			// Other
			CHOICE: 'choose her own job';
			// Pseudo-jobs
			LURCHER: '@Lurcher';
			PIT: '@Pit';
			ARENA: "@Arena";
			IMPORTED: '@be imported';
			TANK: '@lay in tank';
		}

		type Fetish = WithNone<"mindbroken" | "submissive" | "cumslut" | "humiliation" | "buttslut" | "boobs" | "sadist" |
			"masochist" | "dom" | "pregnancy" | "bestiality">;
		interface FetishFreeze extends Record<string, Fetish> {
			NONE: "none";
			MINDBROKEN: "mindbroken";
			SUBMISSIVE: "submissive";
			CUMSLUT: "cumslut";
			HUMILIATION: "humiliation";
			BUTTSLUT: "buttslut";
			BOOBS: "boobs";
			SADIST: "sadist";
			MASOCHIST: "masochist";
			DOM: "dom";
			PREGNANCY: "pregnancy";
			BESTIALITY: "bestiality";
		}

		type BehavioralFlaw = WithNone<
			| "arrogant" // clings to her dignity, thinks slavery is beneath her
			| "bitchy" // : can 't keep her opinions to herself
			| "odd" // says and does odd things
			| "hates men" // hates men
			/** hates women */
			| "hates women"
			| "gluttonous" // likes eating, gains weight
			| "anorexic" // dislikes eating and being forced to eat, loses weight
			| "devout" // resistance through religious faith
			| "liberated" // believes slavery is wrong
		>;
		interface BehavioralFlawFreeze extends Record<string, BehavioralFlaw> {
			NONE: "none";
			ARROGANT: "arrogant";
			BITCHY: "bitchy";
			ODD: "odd";
			HATESMEN: "hates men";
			HATESWOMEN: "hates women";
			GLUTTONOUS: "gluttonous";
			ANOREXIC: "anorexic";
			DEVOUT: "devout";
			LIBERATED: "liberated";
		}

		type BehavioralQuirk = WithNone<
			/** believes she has value as a slave */
			| "confident"
			/** often has as witty or cunning remark ready, knows when to say it */
			| "cutting"
			/** is funny */
			| "funny"
			/** loves working out */
			| "fitness"
			/** likes spending time with women */
			| "adores women"
			/** likes spending time with men */
			| "adores men"
			/** defines herself on the thoughts of others */
			| "insecure"
			/** breaks cultural norms */
			| "sinful"
			/** advocates slavery */
			| "advocate">;
		interface BehavioralQuirkFreeze extends Record<string, BehavioralQuirk> {
			NONE: "none";
			CONFIDENT: "confident";
			CUTTING: "cutting";
			FUNNY: "funny";
			FITNESS: "fitness";
			ADORESWOMEN: "adores women";
			ADORESMEN: "adores men";
			INSECURE: "insecure";
			SINFUL: "sinful";
			ADVOCATE: "advocate";
		}

		type SexualFlaw = WithNone<
			/** hates oral sex */
			| "hates oral"
			/** hates anal sex */
			| "hates anal"
			/** dislikes penetrative sex */
			| "hates penetration"
			/** nervous when naked */
			| "shamefast"
			/** believes sex should be based on love and consent */
			| "idealistic"
			/** dislikes sex */
			| "repressed"
			/** inert during sex */
			| "apathetic"
			/** sexually crude and has little sense of what partners find disgusting during sex */
			| "crude"
			/** sexually judgemental and often judges her sexual partners' performance */
			| "judgemental"
			/** disregards herself in sex */
			| "neglectful"
			/** addicted to cum */
			| "cum addict"
			/** addicted to anal */
			| "anal addict"
			/** addicted to being the center of attention */
			| "attention whore"
			/** addicted to her own breasts */
			| "breast growth"
			/** sexually abusive */
			| "abusive"
			/** loves causing pain and suffering */
			| "malicious"
			/** hates herself */
			| "self hating"
			/** addicted to being pregnant */
			| "breeder"
			/** addicted to fucking animals */
			| "animal lover">;
		interface SexualFlawFreeze extends Record<string, SexualFlaw> {
			NONE: "none";
			HATESORAL: "hates oral";
			HATESANAL: "hates anal";
			HATESPEN: "hates penetration";
			SHAMEFAST: "shamefast";
			IDEAL: "idealistic";
			REPRESSED: "repressed";
			APATHETIC: "apathetic";
			CRUDE: "crude";
			JUDGEMENT: "judgemental";
			NEGLECT: "neglectful";
			CUMADDICT: "cum addict";
			ANALADDICT: "anal addict";
			ATTENTION: "attention whore";
			BREASTEXP: "breast growth";
			ABUSIVE: "abusive";
			MALICIOUS: "malicious";
			SELFHATING: "self hating";
			BREEDER: "breeder";
			ANIMALLOVER: "animal lover";
		}

		type SexualQuirk = WithNone<
			/** can take a facefucking */
			"gagfuck queen"
			/** knows how far she can go without getting hurt */
			| "painal queen"
			/** knows how much resistance her partners want */
			| "strugglefuck queen"
			/** is a tease */
			| "tease"
			/** enjoys the closeness of sex */
			| "romantic"
			/** enjoys breaking sexual boundaries */
			| "perverted"
			/** enjoys bring her partners to orgasm */
			| "caring"
			/** willing to do anything */
			| "unflinching"
			/** prefers big cocks */
			| "size queen">;
		interface SexualQuirkFreeze extends Record<string, SexualQuirk> {
			NONE: "none";
			GAGFUCK: "gagfuck queen";
			PAINAL: "painal queen";
			STRUGGLE: "strugglefuck queen";
			TEASE: "tease";
			ROMANTIC: "romantic";
			PERVERT: "perverted";
			CARING: "caring";
			UNFLINCHING: "unflinching";
			SIZEQUEEN: "size queen";
		}

		type BreastShape = "normal" | "perky" | "saggy" | "torpedo-shaped" | "downward-facing" | "wide-set" | "spherical";
		interface BreastShapeFreeze extends Record<string, BreastShape> {
			NORMAL: "normal";
			PERKY: "perky";
			SAGGY: "saggy";
			TORPEDO: "torpedo-shaped";
			DOWNWARD: "downward-facing";
			WIDE: "wide-set";
			SPHERICAL: "spherical";
		}

		type Clothes = "a ball gown" | "a bimbo outfit" | "a biyelgee costume" | "a bra" | "a bunny outfit" | "a burkini" |
			"a burqa" | "a button-up shirt" | "a button-up shirt and panties" | "a chattel habit" | "a cheerleader outfit" | "a comfortable bodysuit" |
			"a courtesan dress" | "a cybersuit" | "a dirndl" | "a fallen nuns habit" | "a Fuckdoll suit" | "a gothic lolita dress" | "a halter top dress" |
			"a hanbok" | "a hijab and abaya" | "a hijab and blouse" | "a huipil" | "a kimono" | "a klan robe" | "a latex catsuit" | "a leotard" |
			"a long qipao" | "a maternity dress" | "a military uniform" | "a mini dress" | "a monokini" | "a mounty outfit" | "a nice maid outfit" |
			"a nice nurse outfit" | "a nice pony outfit" | "a niqab and abaya" | "a one-piece swimsuit" | "a penitent nuns habit" | "a police uniform" |
			"a red army uniform" | "a Santa dress" | "a scalemail bikini" | "a schoolgirl outfit" | "a schutzstaffel uniform" | "a skimpy loincloth" |
			"a slave gown" | "a slutty klan robe" | "a slutty maid outfit" | "a slutty nurse outfit" | "a slutty outfit" | "a slutty pony outfit" |
			"a slutty qipao" | "a slutty schutzstaffel uniform" | "a sports bra" | "a string bikini" | "a striped bra" | "a succubus outfit" |
			"a sweater" | "a sweater and cutoffs" | "a sweater and panties" | "a t-shirt" | "a t-shirt and jeans" | "a t-shirt and panties" |
			"a t-shirt and thong" | "a tank-top" | "a tank-top and panties" | "a thong" | "a toga" | "a tube top" | "a tube top and thong" |
			"an apron" | "an oversized t-shirt" | "an oversized t-shirt and boyshorts" | "attractive lingerie" | "attractive lingerie for a pregnant woman" |
			"battlearmor" | "Imperial Plate" | "a tight Imperial bodysuit" | "battledress" | "body oil" | "boyshorts" | "chains" | "choosing her own clothes" |
			"clubslut netting" | "conservative clothing" | "cutoffs" | "cutoffs and a t-shirt" | "harem gauze" | "jeans" | "kitty lingerie" |
			"leather pants" | "leather pants and a tube top" | "leather pants and pasties" | "lederhosen" | "nice business attire" | "no clothing" |
			"overalls" | "panties" | "panties and pasties" | "pasties" | "restrictive latex" | "shibari ropes" | "slutty business attire" |
			"slutty jewelry" | "spats and a tank top" | "sport shorts" | "sport shorts and a sports bra" | "sport shorts and a t-shirt" |
			"stretch pants and a crop-top" | "striped panties" | "striped underwear" | "uncomfortable straps" |	"Western clothing" |
			"a confederate army uniform" | "an evening dress";
		type BellyAccessory = WithNone<"a small empathy belly" | "a medium empathy belly" | "a large empathy belly" | "a huge empathy belly" |
			"a corset" | "an extreme corset" | "a support band">;
		type Collar = WithNone<"ancient Egyptian" | "cruel retirement counter" | "uncomfortable leather" | "tight steel" | "shock punishment" |
			"heavy gold" | "pretty jewelry" | "nice retirement counter" | "bell collar" | "leather with cowbell" | "bowtie" | "neck tie" |
			"neck corset" | "stylish leather" | "satin choker" | "preg biometrics" | "silk ribbon">;
		type HairStyle = "afro"| "braided" | "cornrows" | "curled" | "dreadlocks" | "eary" | "bun" | "messy bun" | "ponytail" | "tails" |
			"drills" | "luxurious" | "messy" | "neat" | "permed" | "bangs" | "hime" | "strip" | "up" | "shaved" | "trimmed" | "buzzcut" | "bald" | "undercut" |
			"double buns" | "chignon" | "french twist" | "crown braid" | "dutch braid" | "double dutch braid" | "pixie cut" | "bob cut";
		type Shoes = "heels" | "pumps" | "extreme heels" | "boots" | "flats" | "platform heels" | "extreme heels" | "extreme platform heels" | "platform shoes";
		type MouthAccessory = "dildo gag" | "massive dildo gag" | "ball gag" | "bit gag" | "ring gag";

		type Diet = "healthy" | "restricted" | "corrective" | "muscle building" | "fattening" | "slimming" | "XX" | "XY" | "XXY" |
			"cum production" | "cleansing" | "fertility" | "high caloric";
		interface DietFreeze extends Record<string, Diet> {
			HEALTHY: "healthy";
			RESTRICTED: "restricted";
			CORRECTIVE: "corrective";
			MUSCLE: "muscle building";
			FATTEN: "fattening";
			SLIM: "slimming";
			FEMALE: "XX";
			MALE: "XY";
			FUTA: "XXY";
			CUM: "cum production";
			CLEANSE: "cleansing";
			FERTILITY: "fertility";
			CALORIC: "high caloric";
		}

		type PCDiet = Diet | "exotic" | "medicinal" | "weaning";
		interface PCDietFreeze extends Record<string, PCDiet> {
			HEALTHY: "healthy";
			RESTRICTED: "restricted";
			CORRECTIVE: "corrective";
			MUSCLE: "muscle building";
			FATTEN: "fattening";
			SLIM: "slimming";
			FEMALE: "XX";
			MALE: "XY";
			FUTA: "XXY";
			CUM: "cum production";
			CLEANSE: "cleansing";
			FERTILITY: "fertility";
			CALORIC: "high caloric";
			EXOTIC: "exotic";
			MEDICINAL: "medicinal";
			WEANING: "weaning";
		}

		type Drug = "no drugs" |
			"breast injections" | "butt injections" | "lip injections" | "nipple enhancers" | "penis enhancement" | "testicle enhancement" |
			"intensive breast injections" | "intensive butt injections" | "intensive penis enhancement" | "intensive testicle enhancement" |
			"fertility drugs" | "super fertility drugs" |
			"psychosuppressants" | "psychostimulants" | "steroids" |
			"hyper breast injections" | "hyper butt injections" | "hyper penis enhancement" | "hyper testicle enhancement" |
			"female hormone injections" | "male hormone injections" | "priapism agents" |
			"anti-aging cream" | "appetite suppressors" | "hormone enhancers" | "hormone blockers" |
			"penis atrophiers" | "testicle atrophiers" | "clitoris atrophiers" | "labia atrophiers" | "nipple atrophiers" | "lip atrophiers" |
			"breast redistributors" | "butt redistributors" | "sag-B-gone" | "growth stimulants" | "stimulants";
		interface DrugFreeze extends Record<string, Drug> {
			NONE: "no drugs";
			GROWBREAST: "breast injections";
			GROWBUTT: "butt injections";
			GROWLIP: "lip injections";
			GROWNIPPLE: "nipple enhancers";
			GROWPENIS: "penis enhancement";
			GROWTESTICLE: "testicle enhancement";
			INTENSIVEBREAST: "intensive breast injections";
			INTENSIVEBUTT: "intensive butt injections";
			INTENSIVEPENIS: "intensive penis enhancement";
			INTENSIVETESTICLE: "intensive testicle enhancement";
			FERTILITY: "fertility drugs";
			SUPERFERTILITY: "super fertility drugs";
			PSYCHOSUPP: "psychosuppressants";
			PSYCHOSTIM: "psychostimulants";
			STEROID: "steroids";
			HYPERBREAST: "hyper breast injections";
			HYPERBUTT: "hyper butt injections";
			HYPERPENIS: "hyper penis enhancement";
			HYPERTESTICLE: "hyper testicle enhancement";
			HORMONEFEMALE: "female hormone injections";
			HORMONEMALE: "male hormone injections";
			PRIAPISM: "priapism agents";
			ANTIAGE: "anti-aging cream";
			APPETITESUPP: "appetite suppressors";
			HORMONEENHANCE: "hormone enhancers";
			HORMONEBLOCK: "hormone blockers";
			ATROPHYPENIS: "penis atrophiers";
			ATROPHYTESTICLE: "testicle atrophiers";
			ATROPHYCLIT: "clitoris atrophiers";
			ATROPHYLABIA: "labia atrophiers";
			ATROPHYNIPPLE: "nipple atrophiers";
			ATROPHYLIP: "lip atrophiers";
			REDISTBREAST: "breast redistributors";
			REDISTBUTT: "butt redistributors";
			SAGBGONE: "sag-B-gone";
			GROWTHSTIM: "growth stimulants";
			STIM: "stimulants";
		}

		type EarWear = WithNone<"hearing aids" | "muffling ear plugs" | "deafening ear plugs">;
		type EarShape = WithNone<"damaged" | "normal" | "pointy" | "elven" | "cow" | "robot" | "orcish" | "sheep" | "deer" | "gazelle" | "bird" | "dragon">;
		type EarTopType = WithNone<"normal" | "cat" | "leopard" | "tiger" | "jaguar" | "lion" | "dog" | "wolf" | "jackal" | "fox" | "raccoon" | "rabbit" | "squirrel"| "horse">;
		type EyebrowStyle = "bald" | "curved" | "elongated" | "high-arched" | "natural" | "rounded" | "shaved" | "shortened" |
			"slanted inwards" | "slanted outwards" | "straight";
		type EyebrowThickness = "pencil-thin" | "thin" | "threaded" | "natural" | "tapered" | "thick" | "bushy";
		type EyeWear = WithNone<"glasses" | "blurring glasses" | "corrective glasses" | "blurring contacts" | "corrective contacts">;

		type FaceShape = "masculine" | "androgynous" | "normal" | "cute" | "sensual" | "exotic";
		interface FaceShapeFreeze extends Record<string, FaceShape> {
			MASC: "masculine";
			ANDRO: "androgynous";
			NORMAL: "normal";
			CUTE: "cute";
			SENSUAL: "sensual";
			EXOTIC: "exotic";
		}

		type GenderGenes =
			/** female */
			"XX"
			/** Triple X syndrome female */
			| "XXX"
			/** male */
			| "XY"
			/** Klinefelter syndrome male */
			| "XXY"
			/** XYY syndrome male */
			| "XYY"
			| "X0"
			| "X"
			| "YY";
		interface GenderGenesFreeze extends Record<string, GenderGenes> {
			FEMALE: "XX";
			MALE: "XY";
			INVIABLE: "YY";
		}

		type GestationDrug = "slow gestation" | "speed up" | "labor suppressors";
		interface GestationDrugFreeze extends Record<string, GestationDrug> {
			SLOW: "slow gestation";
			FAST: "speed up";
			LABOR: "labor suppressors";
		}

		type HornType = WithNone<"curved succubus horns" | "backswept horns" | "cow horns" | "one long oni horn" |
			"two long oni horns" | "small horns">;

		type InflationLiquid = WithNone<"water" | "cum" | "milk" | "food" | "aphrodisiac" | "curative" | "tightener" | "urine" | "stimulant">;
		interface InflationLiquidFreeze extends Record<string, InflationLiquid> {
			NONE: "none";
			WATER: "water";
			CUM: "cum";
			MILK: "milk";
			FOOD: "food";
			APHRO: "aphrodisiac";
			CURATIVE: "curative";
			TIGHTEN: "tightener";
			URINE: "urine";
			STIM: "stimulant";
		}

		type TailType = WithNone<"mod" | "combat" | "sex"| "stinger">;
		type AppendagesType = WithNone<"mod" | "flight" | "sex" | "falcon" | "arachnid" | "kraken">;
		type Markings = WithNone<"beauty mark" | "birthmark" | "freckles" | "heavily freckled">;
		type TailShape = WithNone<"cat" | "leopard" | "tiger" | "jaguar" | "lion" | "dog" | "wolf" | "jackal" | "fox" | "kitsune" | "tanuki" | "raccoon" | "rabbit" | "squirrel" | "horse" | "bird" | "phoenix" | "peacock" | "raven" | "swan" | "sheep" | "cow" | "gazelle" | "deer" | "succubus" | "dragon" >;
		type WingsShape = WithNone<"angel" | "seraph" | "demon"| "dragon" | "phoenix" | "bird"| "fairy" | "butterfly" | "moth" | "insect" | "evil" >;

		type ToyHole = "all her holes" | "mouth" | "boobs" | "pussy" | "ass" | "dick";
		interface ToyHoleFreeze extends Record<string, ToyHole> {
			ALL: "all her holes";
			MOUTH: "mouth";
			BOOBS: "boobs";
			PUSSY: "pussy";
			ASS: "ass";
			DICK: "dick";
		}

		type OvaryImplantType = 0 | "fertility" | "sympathy" | "asexual";
		interface OvaryImplantTypeFreeze extends Record<string, OvaryImplantType> {
			NONE: 0;
			FERTILITY: "fertility";
			SYMPATHY: "sympathy";
			ASEXUAL: "asexual";
		}

		type NippleShape = "huge" | "puffy" | "inverted" | "tiny" | "cute" | "partially inverted" | "fuckable" | "flat";
		interface NippleShapeFreeze extends Record<string, NippleShape> {
			HUGE: "huge";
			PUFFY: "puffy";
			INVERTED: "inverted";
			TINY: "tiny";
			CUTE: "cute";
			PARTIAL: "partially inverted";
			FUCKABLE: "fuckable";
			FLAT: "flat";
		}

		type LactationType = 0 | 1 | 2;
		type VaginaLubeType = 0 | 1 | 2;
		type WombImplantType = WithNone<"restraint">;
		type LabiaType = 0 | 1 | 2 | 3;
		type ClitType = 0 | 1 | 2 | 3 | 4 | 5;
		type AnusType = 0 | 1 | 2 | 3 | 4;
		type ProstateType = 0 | 1 | 2 | 3;
		type dietCumType = 0 | 1 | 2;
		type dietMilkType = 0 | 1 | 2;
		/**
		 * 0: no; 1: yes; 2: heavy
		 */
		type PiercingType = 0 | 1 | 2;
		type Piercing = "ear" | "nose" | "eyebrow" | "lips" | "tongue" | "nipple" | "areola" | "navel" | "corset" | "genitals" | "vagina" | "dick" | "anus";
		type Race = "amerindian" | "asian" | "black" | "indo-aryan" | "latina" | "malay" | "middle eastern" | "mixed race" |
			"pacific islander" | "catgirl" | "semitic" | "southern european" | "white";

		type SizingImplantType = "normal" | "string" | "fillable" | "advanced fillable" | "hyper fillable";
		interface SizingImplantTypeFreeze extends Record<string, SizingImplantType> {
			NORMAL: "normal";
			STRING: "string";
			FILLABLE: "fillable";
			ADVANCED: "advanced fillable";
			HYPER: "hyper fillable";
		}

		type LipsImplantType = "normal";
		type InstalledSizingImplantType = WithNone<SizingImplantType>;
		type InstalledLipsImplantType = WithNone<LipsImplantType>;
		type SizableBodyPart = "lips" | "boobs" | "butt" | "dick" | "balls";
		type SizingImplantTarget = "boobs" | "butt" | "lips";

		interface BodyPartImplantTypeMap {
			lips: LipsImplantType;
			boobs: SizingImplantType;
			butt: SizingImplantType;
		}

		type BodyPartInstalledImplantType<Target extends SizingImplantTarget> = WithNone<BodyPartImplantTypeMap[Target]>;

		type SmartPiercingSetting = WithNone<"off" | "all" | "no default setting" | "random" | "women" | "men" | "vanilla" | "oral" | "anal" |
			"boobs" | "submissive" | "humiliation" | "pregnancy" | "dom" | "masochist" | "sadist" | "anti-women" | "anti-men">;
		interface SmartPiercingSettingFreeze extends Record<string, SmartPiercingSetting> {
			NONE: "none";
			OFF: "off";
			ALL: "all";
			NODEFAULT: "no default setting";
			RANDOM: "random";
			WOMEN: "women";
			MEN: "men";
			VANILLA: "vanilla";
			ORAL: "oral";
			ANAL: "anal";
			BOOBS: "boobs";
			SUBMISSIVE: "submissive";
			HUMILIATION: "humiliation";
			PREGNANCY: "pregnancy";
			DOM: "dom";
			MASOCHIST: "masochist";
			SADIST: "sadist";
			ANTIWOMEN: "anti-women";
			ANTIMEN: "anti-men";
		}

		type TeethType = "normal" | "crooked" | "gapped" | "straightening braces" | "cosmetic braces" | "removable" | "pointy" |
			"fangs" | "fang" | "baby" | "mixed";
		type MinorInjury = Zeroable<"black eye" | "bad bruise" | "split lip" | "sore ass">;

		type RelationShipKind =
			/** married to you */
			-3
			/** emotionally bound to you */
			| -2
			/** emotional slut */
			| -1
			| 0
			/** friends with relationshipTarget */
			| 1
			/** best friends with relationshipTarget */
			| 2
			/** friends with benefits with relationshipTarget */
			| 3
			/** lover with relationshipTarget */
			| 4
			/** relationshipTarget 's slave wife */
			| 5;
		type RivalryType =
			/** None */
			0
			/** dislikes rivalryTarget */
			| 1
			/** rival of rivalryTarget */
			| 2
			/** bitterly hates rivalryTarget */
			| 3;
		type IndentureType =
			/** complete protection */
			2
			/** some protection */
			| 1
			/** no protection */
			| 0;

		type HeightImplant = -1 | 0 | 1;
		type Hearing = -2 | -1 | 0;

		type AnimalType = "human" | "dog" | "pig" | "horse" | "cow";
		type SpermType = AnimalType | "sterile";

		type GeneticQuirk = 0 | 1 | 2;
		interface GeneticQuirks {
			/** Oversized breasts. Increased growth rate, reduced shrink rate. Breasts try to return to oversized state if reduced. */
			macromastia: GeneticQuirk | 3;
			/** Greatly oversized breasts. Increased growth rate, reduced shrink rate. Breasts try to return to oversized state if reduced.
			 *
			 * **macromastia + gigantomastia** - Breasts never stop growing. Increased growth rate, no shrink rate. */
			gigantomastia: GeneticQuirk | 3;
			/** sperm is much more likely to knock someone up */
			potent: GeneticQuirk;
			/** is prone to having twins, shorter pregnancy recovery rate */
			fertility: GeneticQuirk;
			/** is prone to having multiples, even shorter pregnancy recovery rate
			 *
			 * **fertility + hyperFertility** - will have multiples, even shorter pregnancy recovery rate */
			hyperFertility: GeneticQuirk;
			/** pregnancy does not block ovulation, slave can become pregnant even while pregnant */
			superfetation: GeneticQuirk;
			/** Pleasurable pregnancy and orgasmic birth. Wider hips, looser and wetter vagina. High pregadaptation and low birth damage. */
			uterineHypersensitivity: GeneticQuirk;
			/** is abnormally tall. gigantism + dwarfism - is very average*/
			gigantism: GeneticQuirk;
			/** is abnormally short. gigantism + dwarfism - is very average*/
			dwarfism: GeneticQuirk;
			/** has a flawless face. pFace + uFace - Depends on carrier status, may swing between average and above/below depending on it */
			pFace: GeneticQuirk;
			/** has a hideous face. pFace + uFace - Depends on carrier status, may swing between average and above/below depending on it */
			uFace: GeneticQuirk;
			/** has pale skin, white hair and red eyes */
			albinism: GeneticQuirk;
			/** may have mismatched eyes, the eye color stored here is always the left eye */
			heterochromia: GeneticQuirk | string;
			/** ass never stops growing. Increased growth rate, reduced shrink rate. */
			rearLipedema: GeneticQuirk;
			/** has (or will have) a huge dong */
			wellHung: GeneticQuirk;
			/** constantly gains weight unless dieting, easier to gain weight. wGain + wLoss - weight gain/loss fluctuates randomly */
			wGain: GeneticQuirk;
			/** constantly loses weight unless gaining, easier to lose weight. wGain + wLoss - weight gain/loss fluctuates randomly */
			wLoss: GeneticQuirk;
			/** body attempts to normalize to an androgynous state */
			androgyny: GeneticQuirk;
			/** constantly gains muscle mass, easier to gain muscle. mGain + mLoss - muscle gain/loss amplified, passively lose muscle unless building */
			mGain: GeneticQuirk;
			/** constantly loses muscle mass, easier to gain muscle. mGain + mLoss - muscle gain/loss amplified, passively lose muscle unless building */
			mLoss: GeneticQuirk;
			/** ova will split if room is available
			 *  only affects fetuses */
			twinning: GeneticQuirk;
			/** slave can only ever birth girls */
			girlsOnly: GeneticQuirk;
			/** abnormal production of amniotic fluid
			 * only affects fetuses */
			polyhydramnios: GeneticQuirk;
			/** inappropriate lactation*/
			galactorrhea: GeneticQuirk | 3;
			/** retains childlike characteristics*/
			neoteny: GeneticQuirk | 3;
			/** rapid aging
			 *
			 * **neoteny + progeria** - progeria wins, not that she'll make it to the point that neoteny really kicks in */
			progeria: GeneticQuirk | 3,
		}
		interface FetusGenetics {
			gender: GenderGenes;
			name: string;
			surname: Zeroable<string>;
			mother: number;
			motherName: string;
			father: number;
			fatherName: string;
			nationality: string;
			race: Race;
			intelligence: number;
			face: number;
			faceShape: FaceShape;
			eyeColor: string
			hColor: string;
			skin: string;
			markings: Markings;
			behavioralFlaw: BehavioralFlaw;
			sexualFlaw: SexualFlaw;
			pubicHStyle: string;
			underArmHStyle: string;
			clone: Zeroable<string>;
			cloneID: number;
			geneticQuirks: Partial<GeneticQuirks>;
			fetish: Fetish;
			spermY: number;
			inbreedingCoeff?: number;
			adultHeight: number;
			boobPotential: number;
			artSeed: number;
		}
		//#endregion

		type LimbType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10

		type ArmState = InstanceType<typeof App.Entity.ArmState>;
		type LegState = InstanceType<typeof App.Entity.LegState>;

		interface LimbsState {
			arm: {
				left: ArmState;
				right: ArmState;
			};
			leg: {
				left: LegState,
				right: LegState;
			};
			PLimb: 0 | 1 | 2 | 3;
		}

		interface PregnancyData {
			type: string;
			normalOvaMin: number;
			normalOvaMax: number;
			normalBirth: number;
			minLiveBirth: number;
			drugsEffect: number;
			fetusWeek: number[];
			fetusSize: number[];
			fetusRate: number[];
			sizeType: number;
		}

		type HumanState = SlaveState | PlayerState;

		type ImageFormat = "png"|"jpg"|"gif"|"webp"|"webm"|"mp4";
		export interface CustomImage {
			filename?: string;
			format?: ImageFormat;
		}
	}
}

export {}


code:
declare namespace FC {
	namespace UseSlave {
		interface Option {
			/** The link text. */
			link: string;
			/** The text that appears when the option is selected. */
			desc: string;
			/** The link tooltip. */
			tooltip: string;
			/** Any prerequisites required for the option to appear. */
			prereq: () => boolean;
			/** Any effects the option has. */
			effect: () => void;
			/** The partner's reaction to the action. */
			reaction: string;
		}
	}
}