code:
App.UI.optionsPassage = function() {
	const el = new DocumentFragment();
	App.UI.DOM.appendNewElement("h1", el, `Game Options`);
	V.passageSwitchHandler = App.EventHandlers.optionsChanged;
	el.append(intro());

	// Results
	const results = App.UI.DOM.appendNewElement("div", el);

	try {
		const tabBar = new App.UI.Tabs.TabBar("Options");
		tabBar.addTab("Display", "display", App.Intro.display(false));
		tabBar.addTab("Content & flavour", "content-flavor", App.Intro.contentAndFlavor(false));
		tabBar.addTab("Mods", "mods", mods());
		tabBar.addTab("Debug & cheating", "debug-cheating", debugCheating());
		if (V.cheatMode) {
			tabBar.addTab("Cheats", "cheat", cheat());
			tabBar.addTab("Cheat edit schools", "cheat-school", cheatSchools());
		}
		tabBar.addTab("Experimental", "experimental", experimental());
		el.append(tabBar.render());
	} catch (e) {
		App.UI.DOM.appendNewElement("p", el,
			"Options cannot be displayed. Try running Backwards Compatibility and see if the problem persists.",
			["bold", "error"]);
		el.append(App.UI.DOM.formatException(e));
	}
	return el;

	function intro() {
		let links;
		let options;
		const el = new DocumentFragment();

		options = new App.UI.OptionsGroup();
		options.addOption("End of week autosaving is currently", "autosave")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
		el.append(options.render());

		const table = App.UI.DOM.appendNewElement("table", el);
		let row;
		fillRow([
			`You are currently playing:`,
			`FC version: ${App.Version.base},`,
			`mod version: ${App.Version.pmod},`,
			`build: ${App.Version.release}${App.Version.commitHash ? `, commit: ${App.Version.commitHash}` : ``}`
		]);
		fillRow([
			`This save was created using:`,
			App.UI.DOM.makeElement("span", `FC version: ${V.ver},`, (V.ver !== App.Version.base) ? "yellow" : null),
			``,
			App.UI.DOM.makeElement("span", `build: ${V.releaseID}.`, (V.releaseID !== App.Version.release) ? "red" : null)
		]);

		function fillRow(contents) {
			row = App.UI.DOM.appendNewElement("tr", table);
			for (const content of contents) {
				App.UI.DOM.appendNewElement("td", row, content);
			}
		}

		links = [];
		links.push(App.UI.DOM.passageLink("Apply Backwards Compatibility Update", "Backwards Compatibility"));

		links.push(
			App.UI.DOM.link(
				`Reset extended family mode controllers`,
				() => {
					resetFamilyCounters();
					const span = document.createElement("span");
					span.classList.add("note");
					App.UI.DOM.appendNewElement("span", span, "Done: ", "lightgreen");
					span.append("all family relations flushed and rebuilt.");
					jQuery(results).empty().append(span);
				},
				[],
				"",
				"Clears and rebuilds .sister and .daughter tracking."
			)
		);

		if (isNaN(V.rep)) {
			links.push(
				App.UI.DOM.link(
					`Reset Reputation (${V.rep})`,
					() => {
						V.rep = 0;
						jQuery(results).empty().append(`Reputation reset to ${V.rep}`);
					},
					[],
					"Options"
				)
			);
		}

		if (isNaN(V.rep)) {
			links.push(
				App.UI.DOM.link(
					`Reset Money (${V.cash})`,
					() => {
						V.cash = 500;
						jQuery(results).empty().append(`Cash reset to ${V.cash}`);
					},
					[],
					"Options"
				)
			);
		}

		if (V.releaseID === 1057) {
			links.push(
				App.UI.DOM.link(
					`Free male anatomy removal due to accidentally flawed updater`,
					() => {
						V.PC.dick = 0;
						V.PC.balls = 0;
						V.PC.prostate = 0;
						V.PC.scrotum = 0;
						V.PC.ballsImplant = 0;
						jQuery(results).empty().append(`Cash reset to ${V.cash}`);
					},
					[],
					"Options",
					"Use this if your female PC picked up a few extra parts during the conversion process.",
				)
			);
		}

		App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(links));

		if ((V.releaseID >= 1000) || V.ver.startsWith("0.9") || V.ver.startsWith("0.8") || V.ver.startsWith("0.7") || V.ver.startsWith("0.6")) {
			App.UI.DOM.appendNewElement("h3", el, `NEW GAME PLUS`);
			App.UI.DOM.appendNewElement("div", el, `You can begin a new game with up to five (or more) of your current slaves, although starting resources other than these slaves will be reduced.`);
			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.link(
				"Activate New Game Plus",
				() => {
					V.ui = "start";
				},
				[],
				"New Game Plus"
			), ["indent"]);
			App.UI.DOM.appendNewElement("div", el, App.Arcology.purchase());
		} else {
			App.UI.DOM.appendNewElement("div", el, `New Game Plus is not available because this game was not started with a compatible version.`, ["note"]);
		}
		return el;
	}

	function mods() {
		const el = new DocumentFragment();
		let options;
		let option;

		options = new App.UI.OptionsGroup();

		options.addOption("The Special Force Mod is currently", "Toggle", V.SF)
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
			.addComment("
This mod is triggered after week 72. It is non-canon where it conflicts with canonical updates to the base game.
");

		options.addOption("The Security Expansion mod is", "secExpEnabled")
			.addValue("Enabled", 1).on().addCallback(() => App.Mods.SecExp.Init())
			.addValue("Disabled", 0).off()
			.addComment("
The mod can be activated in any moment, but it may result in unbalanced gameplay if activated very late in the game.
");

		option = options.addOption("Catmod is currently", "seeCats")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
		if (V.seeCats) {
			option.addComment(`Disabling Catmod will not murder existing catgirls, you have to do that yourself. <span style="font-style:normal">😿</span>`);
		}

		options.addOption("The food system is", "enabled", V.mods.food)
			.addValue("Enabled", true).on().addValue("Disabled", false).off()
			.addComment("This will enable the food supply and demand system.");

		el.append(options.render());

		if (V.secExpEnabled > 0) {
			App.UI.DOM.appendNewElement("h2", el, `Security Expansion mod options`);
			options = new App.UI.OptionsGroup();

			if (V.SecExp.settings.battle.enabled > 0 || V.SecExp.settings.rebellion.enabled > 0) {
				options.addOption("Detailed battle statistics are", "showStats", V.SecExp.settings)
					.addValue("Shown", 1).on().addValue("Hidden", 0).off()
					.addComment("Visibility of detailed statistics and battle turns.");

				options.addOption("Difficulty is", "difficulty", V.SecExp.settings)
					.addValueList([["Extremely hard", 2], ["Very hard", 1.5], ["Hard", 1.25], ["Normal", 1], ["Easy", 0.75], ["Very easy", 0.5]]);

				options.addOption("Unit descriptions are", "unitDescriptions", V.SecExp.settings)
					.addValue("Abbreviated", 1).addValue("Summarized", 0);
			}

			options.addOption("Battles are", "enabled", V.SecExp.settings.battle)
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

			options.addOption("Rebellions are", "enabled", V.SecExp.settings.rebellion)
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

			if (V.SecExp.settings.battle.enabled > 0) {
				options.addOption("Battle frequency", "frequency", V.SecExp.settings.battle)
					.addValueList([["Extremely high", 2], ["Very high", 1.5], ["High", 1.25], ["Normal", 1], ["Low", 0.75], ["Very low", 0.5]]);
			}

			if (V.SecExp.settings.rebellion.enabled > 0) {
				options.addOption("Rebellion buildup", "speed", V.SecExp.settings.rebellion)
					.addValueList([["Extremely fast", 2], ["Very fast", 1.5], ["Fast", 1.25], ["Normal", 1], ["Slow", 0.75], ["Very slow", 0.5]]);
			}

			if (V.SecExp.settings.battle.enabled > 0) {
				options.addOption("Commanders gain a prestige rank every 10 victories", "allowSlavePrestige", V.SecExp.settings.battle)
					.addValue("Yes", 1).on().addValue("No", 0).off();
			}

			if (V.SecExp.settings.battle.enabled > 0) {
				options.addOption("Force battles", "force", V.SecExp.settings.battle)
					.addValue("Yes", 1).on().addValue("No", 0).off();
			}
			if (V.SecExp.settings.rebellion.enabled > 0) {
				options.addOption("Force rebellions", "force", V.SecExp.settings.rebellion)
					.addValue("Yes", 1).on().addValue("No", 0).off()
					.addComment("Rebellions take precedence over Battles.");
			}

			if (V.SecExp.settings.battle.enabled > 0) {
				options.addOption("Late game major battles are", "enabled", V.SecExp.settings.battle.major)
					.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
					.addComment("More likely after week 120");
			}

			if (V.SecExp.settings.battle.enabled > 0 && V.SecExp.settings.battle.major.enabled > 0) {
				options.addOption("Multiplier is", "mult", V.SecExp.settings.battle.major)
					.addValueList([["Extremely high", 2], ["Very high", 1.5], ["High", 1.25], ["Normal", 1], ["Low", 0.75], ["Very low", 0.5]]);

				options.addOption("This week a major battle is", "force", V.SecExp.settings.battle.major)
					.addValue("Guaranteed", 1).on().addValue("Not guaranteed", 0).off();
			}

			if (V.SecExp.settings.battle.enabled > 0 && V.SecExp.settings.battle.major.enabled > 0) {
				options.addOption("Gameover on battle loss", "gameOver", V.SecExp.settings.battle.major)
					.addValue("Yes", 1).on().addValue("No", 0).off();
			}

			if (V.SecExp.settings.rebellion.enabled > 0) {
				options.addOption("Gameover on rebellion loss", "gameOver", V.SecExp.settings.rebellion)
					.addValue("Yes", 1).on().addValue("No", 0).off();
			}

			el.append(options.render());

			const subHeading = document.createElement("div");
			subHeading.classList.add("subHeading");

			if (V.debugMode || V.cheatMode || V.cheatModeM) {
				App.UI.DOM.appendNewElement("div", subHeading, "Debug/cheat", "bold");
				let td;
				let links;
				const table = document.createElement("table");
				table.classList.add("invisible");
				el.append(table);

				let tr = document.createElement("tr");
				tr.style.textAlign = "center";

				td = createTd();
				links = [];
				links.push(
					App.UI.DOM.link(
						"Set loyalty high",
						() => {
							changeLoyalty("high");
						},
						[],
						"Options"
					)
				);
				links.push(
					App.UI.DOM.link(
						"Set loyalty average",
						() => {
							changeLoyalty("average");
						},
						[],
						"Options"
					)
				);
				links.push(
					App.UI.DOM.link(
						"Set loyalty low",
						() => {
							changeLoyalty("low");
						},
						[],
						"Options"
					)
				);
				links.push(
					App.UI.DOM.link(
						"Randomize loyalty",
						() => {
							changeLoyalty("random");
						},
						[],
						"Options"
					)
				);

				td.append(App.UI.DOM.generateLinksStrip(links));
				tr.append(td);
				table.append(tr);

				tr = document.createElement("tr");
				tr.style.textAlign = "center";
				td = createTd();
				links = [];
				links.push(App.UI.DOM.link(
					"Give Authority",
					() => {
						App.Mods.SecExp.authorityX(1000);
					},
					[],
					"Options"
				));
				links.push(App.UI.DOM.link(
					"Remove Authority",
					() => {
						App.Mods.SecExp.authorityX(-1000);
					},
					[],
					"Options"
				));
				td.append(App.UI.DOM.generateLinksStrip(links));
				tr.append(td);
				table.append(tr);

				tr = document.createElement("tr");
				td = document.createElement("td");
				td.style.textAlign = "right";
				links = [];
				links.push(App.UI.DOM.link(
					"Raise security",
					() => {
						V.SecExp.core.security = Math.clamp(V.SecExp.core.security + 5, 0, 100);
					},
					[],
					"Options"
				));
				links.push(App.UI.DOM.link(
					"Lower security",
					() => {
						V.SecExp.core.security = Math.clamp(V.SecExp.core.security - 5, 0, 100);
					},
					[],
					"Options"
				));
				tr.append(App.UI.DOM.generateLinksStrip(links));
				tr.append(td);

				td = document.createElement("td");
				td.style.textAlign = "left";
				links = [];
				links.push(App.UI.DOM.link(
					"Raise crime",
					() => {
						V.SecExp.core.crimeLow = Math.clamp(V.SecExp.core.crimeLow + 5, 0, 100);
					},
					[],
					"Options"
				));
				links.push(App.UI.DOM.link(
					"Lower crime",
					() => {
						V.SecExp.core.crimeLow = Math.clamp(V.SecExp.core.crimeLow - 5, 0, 100);
					},
					[],
					"Options"
				));
				tr.append(App.UI.DOM.generateLinksStrip(links));
				tr.append(td);
				table.append(tr);

				tr = document.createElement("tr");
				td = document.createElement("td");
				td.style.textAlign = "right";
				links = [];
				links.push(App.UI.DOM.link(
					"Give militia manpower",
					() => {
						V.SecExp.units.militia.free += 30;
					},
					[],
					"Options"
				));
				links.push(App.UI.DOM.link(
					"Remove militia manpower",
					() => {
						V.SecExp.units.militia.free = Math.max(V.SecExp.units.militia.free - 30, 0);
					},
					[],
					"Options"
				));
				tr.append(App.UI.DOM.generateLinksStrip(links));
				tr.append(td);

				td = document.createElement("td");
				td.style.textAlign = "left";
				links = [];
				links.push(App.UI.DOM.link(
					"Give mercs manpower",
					() => {
						V.SecExp.units.mercs.free += 30;
					},
					[],
					"Options"
				));
				links.push(App.UI.DOM.link(
					"Remove mercs manpower",
					() => {
						V.SecExp.units.mercs.free = Math.max(V.SecExp.units.mercs.free - 30, 0);
					},
					[],
					"Options"
				));
				tr.append(App.UI.DOM.generateLinksStrip(links));
				tr.append(td);
				table.append(tr);
				subHeading.append(table);
				el.append(subHeading);
			}	/* closes cheatmode check */
		} /* closes SecExp check*/
		return el;

		function createTd() {
			const td = document.createElement("td");
			td.style.columnSpan = "2";
			return td;
		}

		/**
		 *
		 * @param {"high"|"average"|"low"|"random"} level
		 */
		function changeLoyalty(level) {
			const numberMap = new Map([
				["high", [80, 100]],
				["average", [40, 60]],
				["low", [20]],
				["random", [100]],
			]);

			App.Mods.SecExp.unit.squads("human").forEach(u => u.loyalty = numberGenerator());
			function numberGenerator() {
				const range = numberMap.get(level);
				if (range[1]) {
					return random(range[0], range[1]);
				} else {
					return random(range[0]);
				}
			}
		}
	}

	function cheat() {
		const el = new DocumentFragment();
		const options = new App.UI.OptionsGroup();
		options.addOption("Arcology sex slaves", "NPCSlaves").showTextBox();
		options.addOption("Arcology prosperity cap", "AProsperityCap").showTextBox();
		options.addOption("Shelter Abuse Counter", "shelterAbuse").showTextBox();
		el.append(options.render());

		el.append(App.Intro.economy(false));
		return el;
	}

	function cheatSchools() {
		const el = new DocumentFragment();
		const options = new App.UI.OptionsGroup();
		options.addCustom(App.UI.DOM.makeElement("h2", "Schools"));
		for (const [key, obj] of App.Data.misc.schools) {
			if (typeof obj.requirements === "function" && !obj.requirements()) {
				continue;
			}
			options.addCustom(App.UI.DOM.makeElement("h3", obj.title));
			options.addOption("Students Bought", "studentsBought", V[key]).showTextBox();
			options.addOption("Upgrades", "schoolUpgrade", V[key])
				.addValueList([
					["None", 0],
					["Partial", 1],
					["Full", 2],
				]);
			options.addOption("Moved to arcology", "schoolPresent", V[key])
				.addValue("No", 0).off()
				.addValue("Yes", 1).on();
			options.addOption("Prosperity", "schoolProsperity", V[key]).showTextBox();
			options.addOption("Failed", "schoolAnnexed", V[key])
				.addValue("No", 0).off()
				.addValue("Yes", 1).on();
		}
		el.append(options.render());
		return el;
	}

	function debugCheating() {
		const el = new DocumentFragment();
		let options;
		let option;
		let links;
		let r;
		const popCap = menialPopCap();

		App.UI.DOM.appendNewElement("h2", el, `Debug`);

		options = new App.UI.OptionsGroup();

		options.addOption("DebugMode is", "debugMode")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
			.addComment("This will add passages to Display Variables and create Bug Reports to the sidebar.");

		if (V.debugMode > 0) {
			options.addOption("The custom function part of debug mode is", "debugModeCustomFunction")
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
			options.addOption("Event selection is", "debugModeEventSelection")
				.addValue("Manual", 1).on().addValue("Automatic", 0).off();
		}

		option = options.addCustomOption("Genetics array");
		if (V.cheatMode === 1) {
			option.addButton("Edit Genetics", () => { }, "Edit Genetics");
		} else {
			option.addComment("Enable cheat mode to edit genetics.");
		}

		options.addCustomOption("Rules Assistant").addButton("Reset Rules", () => { initRules(); }, "Rules Assistant");

		options.addOption("Passage Profiler is", "profiler")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
			.addComment("Outputs performance data at the bottom of every passage.");

		el.append(options.render());

		App.UI.DOM.appendNewElement("h2", el, `Cheating`);

		options = new App.UI.OptionsGroup();

		options.addOption("CheatMode is", "cheatMode")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
			.addComment("This will unlock some options that would usually be restricted by progress.");

		if (V.cheatMode === 0) {
			el.append(options.render());
		} else {
			options.addOption("Sidebar Cheats are currently", "cheatModeM")
				.addValue("Shown", 1).on().addValue("Hidden", 0).off();

			options.addOption("Slave aging", "seeAge")
				.addValue("Enabled", 1).on().addValue("Celebrate birthdays, but don't age.", 2).addValue("Disabled", 0).off();

			el.append(options.render());

			links = [];

			links.push(
				App.UI.DOM.link(
					`Add ${commaNum(100000)} money`,
					() => {
						V.cheater = 1;
						cashX(100000, "cheating");
					},
					[],
					"Options"
				)
			);

			links.push(
				App.UI.DOM.link(
					`Add ${commaNum(10000)} rep`,
					() => {
						V.cheater = 1;
						repX(10000, "cheating");
					},
					[],
					"Options"
				)
			);

			r = [];
			r.push(App.UI.DOM.generateLinksStrip(links));
			r.push(App.UI.DOM.makeElement("span", "Cheating will be flagged in your save", "note"));
			App.Events.addNode(el, r, "div", "scLink2");

			SectorCounts();

			links = [];
			links.push(
				App.UI.DOM.link(
					"Raise prosperity cap",
					() => {
						V.AProsperityCapModified += 10;
					},
					[],
					"Options"
				)
			);

			links.push(
				App.UI.DOM.link(
					"Lower prosperity cap",
					() => {
						V.AProsperityCapModified -= 10;
					},
					[],
					"Options"
				)
			);
			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(links), "scLink2");

			links = [];
			links.push(
				App.UI.DOM.link(
					"Raise prosperity",
					() => {
						V.arcologies[0].prosperity = Math.clamp(V.arcologies[0].prosperity + 10, 0, V.AProsperityCap);
					},
					[],
					"Options"
				)
			);

			links.push(
				App.UI.DOM.link(
					"Lower prosperity",
					() => {
						V.arcologies[0].prosperity = Math.clamp(V.arcologies[0].prosperity - 10, 0, V.AProsperityCap);
					},
					[],
					"Options"
				)
			);
			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(links), "scLink2");

			links = [];
			links.push(
				App.UI.DOM.link(
					"Give menial slaves",
					() => {
						V.menials = Math.clamp(V.menials + 30, 0, popCap.value);
					},
					[],
					"Options"
				)
			);

			links.push(
				App.UI.DOM.link(
					"Remove menial slaves",
					() => {
						V.menials = Math.clamp(V.menials - 30, 0, popCap.value);
					},
					[],
					"Options"
				)
			);
			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(links), "scLink2");

			links = [];

			// Will no longer work as intended due to population changes
			links.push(
				App.UI.DOM.link(
					"Add citizens",
					() => {
						V.lowerClass = Math.max(V.lowerClass + 200, 0);
					},
					[],
					"Options"
				)
			);

			// also no longer properly functional
			links.push(
				App.UI.DOM.link(
					"Remove citizens",
					() => {
						V.lowerClass = Math.max(V.lowerClass - 200, 0);
					},
					[],
					"Options"
				)
			);
			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(links), "scLink2");

			links = [];
			// Will work to a limited degree, minimums and maximums for slaves are set through population
			links.push(
				App.UI.DOM.link(
					"Add slaves",
					() => {
						V.NPCSlaves = Math.max(V.NPCSlaves + 200, 0);
					},
					[],
					"Options"
				)
			);

			// Will work to a limited degree
			links.push(
				App.UI.DOM.link(
					"Remove slaves",
					() => {
						V.NPCSlaves = Math.max(V.NPCSlaves - 200, 0);
					},
					[],
					"Options"
				)
			);
			if (V.corp.Announced !== 1) {
				links = [];
				links.push(
					App.UI.DOM.link(
						"Enable Corporation",
						() => {
							V.corp.Announced = 1;
						},
						[],
						"Options"
					)
				);
			}
			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(links), "scLink2");
		}
		return (el);
	}

	function experimental() {
		const el = new DocumentFragment();
		let options;
		let r;

		r = [];
		r.push(`Experimental means just that: experimental. Options below are likely to be in an`);
		r.push(App.UI.DOM.makeElement("span", `even more incomplete or broken state than usual.`, "yellow"));
		r.push(App.UI.DOM.makeElement("span", `THEY MAY NOT WORK AT ALL.`, "red"));
		r.push(`Make sure you back up your save before enabling any of these, and if you are that interested, consider helping to improve them.`);
		App.Events.addNode(el, r, "div", "bold");

		options = new App.UI.OptionsGroup();

		if (V.seePreg !== 0) {
			options.addOption("Nursery is", "nursery", V.experimental)
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
				.addComment("This will enable the experimental nursery, which allows players to interact with growing slave children. An alternative to the incubator.");
		}

		if (V.seeExtreme === 1 && V.seeBestiality === 1) {
			options.addOption("Animal Ovaries are", "animalOvaries", V.experimental)
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
				.addComment("This will allow slaves to be impregnated by animals. Not currently implemented.");
		}

		if (V.seeExtreme === 1) {
			options.addOption("Dinner party", "dinnerParty", V.experimental)
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
				.addComment("This will enable a controversial but very broken event. Warning: Snuff, cannibalism.");
		}

		if (V.debugMode && V.imageChoice === 1) {
			options.addOption("If art for Deepmurk's clothing has not yet been reported, log it in console", "reportMissingClothing", V.experimental)
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
		}

		options.addOption("Rules Assistant target growth expressions", "raGrowthExpr", V.experimental)
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
			.addComment("This will allow javascript expressions to be used in the Rules Assistant growth target setters.");

		options.addOption("New event", "tempEventToggle")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

		options.addOption("Sex overhaul", "sexOverhaul", V.experimental)
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
			.addComment("This will enable a new way to interact with slaves. Currently working but missing flavor text.");

		el.append(options.render());

		el.append(App.UI.playerMods());
		return el;
	}
};
/**
 *
 * @param {boolean} isIntro
 * @returns {DocumentFragment}
 */
App.Intro.display = function(isIntro) {
	const el = new DocumentFragment();
	let options;
	let r;

	App.UI.DOM.appendNewElement("h2", el, "Reports");

	options = new App.UI.OptionsGroup();

	options.addOption("End week report descriptive details are", "showEWD")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("End week report performance details are", "showEWM")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Master Suite report details such as slave changes are", "verboseDescriptions")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("End week societal effects from slaves are", "compressSocialEffects", V.UI)
		.addValue("Expanded", 0).on().addValue("Compacted", 1).off();

	options.addOption("Accordion on week end defaults to", "useAccordion")
		.addValue("Open", 0).on().addValue("Collapsed", 1).off();

	options.addOption("Economic Tabs on weekly reports are", "useTabs")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Economic detail sheets for facilities are", "showEconomicDetails")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Economic report neighbor details such as trade impacts on culture are", "showNeighborDetails")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Numeric formatting is currently", "formatNumbers")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment("This will comma-format numbers in some areas.");

	el.append(options.render());

	App.UI.DOM.appendNewElement("h2", el, "General");

	options = new App.UI.OptionsGroup();

	options.addOption("Main menu leadership controls displayed", "positionMainLinks")
		.addValueList([["Above", 1], ["Above and below", 0], ["Below", -1]]);

	options.addOption("New Model UI", "newModelUI")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Penthouse Facility Display", "verticalizeArcologyLinks")
		.addValueList([["Triple column", 3], ["Double Column", 2], ["Single Column", 1], ["Collapsed", 0], ["Hidden", -1]]);

	options.addOption("Main menu arcology description", "seeArcology")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Main menu desk description", "seeDesk")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Main menu newsfeed", "seeFCNN")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Tips from the Encyclopedia are", "showTipsFromEncy")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Help tooltips are", "tooltipsEnabled")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment(`This is mostly for new players. <span class='exampleTooltip noteworthy'>Colored text</span> can have tooltips.`)
		.addCallbackToEach(App.UI.GlobalTooltips.update);

	options.addOption("Main menu slave tabs are", "useSlaveSummaryTabs")
		.addValue("Enabled", 1).on().addValue("CardStyle", 2).on().addValue("Disabled", 0).off();

	options.addOption("The slave Quick list in-page scroll-to is", "useSlaveListInPageJSNavigation")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Condense special slaves into their own tab", "useSlaveSummaryOverviewTab")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Interactions with your fucktoys are", "fucktoyInteractionsPosition")
		.addValueList([["next to them", 1], ["at page bottom", 0]]);

	options.addOption("Hide tabs in Slave Interact", "slaveInteractLongForm")
		.addValue("Enabled", true).on().addValue("Disabled", false).off();

	options.addOption("Line separations are", "lineSeparations")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Warning on end week saving", "endweekSaveWarning")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Purchase options are", "purchaseStyle")
		.addValue("Links", 'link').addValue("Buttons", 'button');

	el.append(options.render());

	r = [];
	r.push(`UI theme selector. Allows to select a single CSS file to be loaded.`);
	r.push(App.UI.DOM.makeElement("span", `The file has to be located in the same directory as the HTML file otherwise it will simply not load at all.`, "red"));
	r.push(App.UI.Theme.selector());
	App.Events.addParagraph(el, r);

	App.UI.DOM.appendNewElement("h2", el, "Sidebar");

	options = new App.UI.OptionsGroup();

	options.addOption("Style is", "Style", V.sideBarOptions)
		.addValue("Compact", 'compact').addValue("Expanded", 'expanded');

	options.addOption("Cash is", "Cash", V.sideBarOptions)
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Upkeep is", "Upkeep", V.sideBarOptions)
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Sex slave count is", "SexSlaveCount", V.sideBarOptions)
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Room population is", "roomPop", V.sideBarOptions)
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("GSP is", "GSP", V.sideBarOptions)
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Rep is", "Rep", V.sideBarOptions)
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Confirmation before ending a week is", "confirmWeekEnd", V.sideBarOptions)
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment("Enabling this will open a dialog box to confirm you meant to end a week.");

	if (V.secExpEnabled > 0) {
		options.addOption("Authority is", "Authority", V.sideBarOptions)
			.addValue("Shown", 1).on().addValue("Hidden", 0).off();

		options.addOption("Security is", "Security", V.sideBarOptions)
			.addValue("Shown", 1).on().addValue("Hidden", 0).off();

		options.addOption("Crime is", "Crime", V.sideBarOptions)
			.addValue("Shown", 1).on().addValue("Hidden", 0).off();
	}

	el.append(options.render());

	App.UI.DOM.appendNewElement("h2", el, "Images");
	el.append(App.UI.artOptions());

	return el;
};

/**
 *
 * @param {boolean} isIntro
 * @returns {DocumentFragment}
 */
App.Intro.contentAndFlavor = function(isIntro) {
	const el = new DocumentFragment();
	let r;
	let options;
	let option;

	App.UI.DOM.appendNewElement("h2", el, "Content");

	const descOptions = App.UI.DOM.makeElement("div");
	r = [];
	r.push("More granular control of what appears is in");
	r.push(App.UI.DOM.link("Description Options", () => {
		jQuery(descOptions).empty().append(App.UI.descriptionOptions());
	}));

	App.Events.addNode(descOptions, r, "div");
	el.append(descOptions);

	options = new App.UI.OptionsGroup();

	options.addOption("The difficulty setting is currently set to", "baseDifficulty")
		.addValueList([["Very easy", 1], ["Easy", 2], ["Default", 3], ["Hard", 4], ["Very hard", 5]]);

	if (!isIntro) {
		options.addOption("Maximum random slave events per week", "RIEPerWeek")
			.addValueList([1, 2, 3]);
	}

	options.addOption("Slaves falling ill is currently", "seeIllness")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment("Will not affect existing ill slaves already in-game.");

	options.addOption("Extreme content like amputation is currently", "seeExtreme")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment("Will not affect extreme surgeries already applied in-game.");

	options.addOption("Permanent stretching of holes is currently", "seeStretching")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment("Will not affect holes already stretched.");

	options.addOption("Bestiality related content is currently", "seeBestiality")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Watersports related content is currently", "seePee")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Incest content is currently", "seeIncest")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Proportion of slave girls with dicks", "seeDicks")
		.addValueList([
			["None (0%)", 0],
			["Nearly none (1%)", 1],
			["A few (10%)", 10],
			["Some (25%)", 25],
			["Half (50%)", 50],
			["Lots (75%)", 75],
			["Most (90%)", 90],
			["Almost all (99%)", 99],
			["All (100%)", 100]
		]);

	if (V.seeDicks === 0) {
		options.addOption("Surgical attachment of dicks is", "makeDicks")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
	}

	if (V.seeDicks !== 0 || V.makeDicks !== 0) {
		options.addOption("Circumcision is", "seeCircumcision")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
	}

	const existingPregnancies = isIntro ? `` : ` Will not affect existing pregnancies already in-game.`;
	options.addOption("Pregnancy related content is currently", "seePreg")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment(existingPregnancies);

	if (V.seePreg) {
		options.addOption("Child gender is generated based on", "seeDicksAffectsPregnancy")
			.addValue(`Proportion of slaves with dicks (${V.seeDicks}% male)`, 1).on().addValue("Genetics", 0).off()
			.addComment(existingPregnancies);

		if (V.seeDicksAffectsPregnancy === 0) {
			options.addOption("XX slaves only father daughters", "adamPrinciple")
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
				.addComment(existingPregnancies);
		}

		options.addOption("Extreme pregnancy content like broodmothers is currently", "seeHyperPreg")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
			.addComment(isIntro ? `` : "Will not affect existing hyperpregnancies already in-game.");

		options.addOption("Pregnancy complications due to multiples and body size are currently", "dangerousPregnancy")
			.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
	}

	option = options.addOption(`Precocious puberty (pregnancy younger than ${V.fertilityAge})`, "precociousPuberty")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
	if (isIntro) {
		option.addComment(`Under certain conditions slaves can become pregnant or impregnate others younger than normal age (${V.fertilityAge}); they may also experience delayed puberty.`);
	} else {
		option.addComment("Will not affect existing precocious puberty cases already in-game.");
	}

	options.addOption("Slaves with fat lips or heavy oral piercings may lisp", "disableLisping")
		.addValue("Yes", 0).on().addValue("No", 1).off();

	options.addOption("(Temp option) The long-term damage mechanic is currently", "disableLongDamage")
		.addValue("Enabled", 0).on().addValue("Disabled", 1).off();

	options.addOption("Experimental male pronouns are currently", "diversePronouns")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment("Apply Backwards Compatibility after changing to update slave's pronouns. Not all scenes support male pronouns and this is not yet incorporated into the lore or mechanics.");

	options.addOption("Male slave names are currently", "allowMaleSlaveNames")
		.addValue("Enabled", true).on().addValue("Disabled", false).off()
		.addComment("This only affects slave generation and not your ability to name your slaves.");

	options.addOption("Missing slave names are currently", "showMissingSlaves")
		.addValue("Enabled", true).on().addValue("Disabled", false).off();

	el.append(options.render());

	App.UI.DOM.appendNewElement("h2", el, `Intersecting mechanics`);

	options = new App.UI.OptionsGroup();

	options.addOption("Slave assets affected by weight is", "weightAffectsAssets")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment("Diet will still affect asset size.");

	options.addOption("Chem damage from drugs is", "curativeSideEffects") // Yes, this variable controls all chem damage. The vestigial "curatives" name isn't worth changing.
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment("If enabled, many drugs will cause harmful, though curable, carcinogen buildup and genomic damage over time.");

	el.append(options.render());

	App.UI.DOM.appendNewElement("h2", el, `Flavour`);

	options = new App.UI.OptionsGroup();

	options.addOption("Slave reactions to facility assignments are", "showAssignToScenes")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Post sex clean up", "postSexCleanUp")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Appraisal miniscenes on slave sale are", "showAppraisal")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Assignment performance vignettes on the end week report are", "showVignettes")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Slaves can have alternate titles", "newDescriptions")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Family titles for relatives", "allowFamilyTitles")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	options.addOption("Limit family growth", "limitFamilies")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
		.addComment("Restricts acquisition of additional relatives, by means other than birth, for slaves with families.");

	options.addOption("Distant relatives such as aunts, nieces and cousins are", "showDistantRelatives")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	el.append(options.render());
	return el;
};

App.UI.artOptions = function() {
	const el = new DocumentFragment();
	let options = new App.UI.OptionsGroup();

	if (V.seeImages && !V.seeCustomImagesOnly) {
		const art = App.UI.DOM.appendNewElement("div", el, App.Art.SlaveArtElement(BaseSlave(), 2, 0), ["imageRef", "medImg"]);
		art.style.float = "none";
		art.style.display = "block";
		art.style.marginLeft = "auto";
		art.style.marginRight = "auto";
	}

	options.addOption("Images are", "seeImages")
		.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

	if (V.seeImages > 0) {
		options.addOption("Show custom images only", "seeCustomImagesOnly")
			.addValue("Enabled", 1).addValue("Disabled", 0);

		if (!V.seeCustomImagesOnly) {
			/* REMOVE THIS WARNING ONCE ART DEVELOPMENT RESUMES */
			options.addComment('All image packs are currently incomplete; some outfits will not be displayed.');
			const option = options.addOption("Image style is", "imageChoice")
				.addValueList([ // Keeping the most up to date options as the first in line
					["Elohiem's interactive WebGL", 4],
					["NoX/Deepmurk's vector art", 1],
					["Revamped embedded vector art", 3],
					["Non-embedded vector art", 2],
					["Shokushu's rendered image pack", 5],
				]);
			if (V.imageChoice === 1) {
				options.addComment("The only 2D art in somewhat recent development. Contains many outfits.");
				options.addOption("Face artwork is", "seeFaces")
					.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

				options.addOption("Highlights on shiny clothing are", "seeVectorArtHighlights")
					.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

				options.addOption("Height scaling", "seeHeight")
					.addValue("All images", 2).on().addValue("Small images", 1).addValue("Disabled", 0).off();

				options.addOption("Clothing erection bulges are", "showClothingErection")
					.addValue("Enabled", true).on().addValue("Disabled", false).off();
			} else if (V.imageChoice === 5) {
				options.addComment("Dead since long before the end of vanilla. You need to" +
					" download the image" +
					" pack and put the 'renders' folder into the resources/ folder where this html file is.");

				options.addOption("Slave summary fetish images are", "seeMainFetishes")
					.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
			} else if (V.imageChoice === 3) {
				options.addComment("This art development is dead.");
				options.addOption("Clothing erection bulges are", "showClothingErection")
					.addValue("Enabled", true).on().addValue("Disabled", false).off();
			} else if (V.imageChoice === 4) {
				options.addComment(`This art is currently (06/12/22) the most actively developed. Real time 3D models.  Download the WebGL art assets and place the 'webgl' folder into the resources/ folder where this HTML file is.
				Then refresh the page.
				Create the resources folder if it does not exist. (Android/MacOS not supported)`);

				let option = options.addOption("Supersampling (SSAA)", "setSuperSampling");
				for (const value of [0.25, 0.5, 1, 2, 4]) {
					option.addValue(`${value}`, value);
				}
				option.addComment("Multiplies the resolution of the render. Use a smaller factor for low-end GPU's.");

				option = options.addOption("Texture resolution", "setTextureResolution");
				for (const value of [512, 1024, 2048, 4096]) {
					option.addValue(`${value}`, value);
				}
				option.addComment("Refresh the page to take affect.");

				options.addOption("Face culling", "setFaceCulling")
					.addValue("Enabled", true).off().addValue("Disabled", false).on()
					.addComment("Whether to draw the backside of the model, affects transparent surfaces such as hair. Enabling is recommended for low-end GPU's.");
				options.addOption("Pan speed", "setPanSpeed")
					.addValue("0.25", 0.25).off().addValue("0.5", 0.5).off().addValue("1", 1).on().addValue("2", 2).off().addValue("4", 4).off();
				options.addOption("Rotation speed", "setRotationSpeed")
					.addValue("0.25", 0.25).off().addValue("0.5", 0.5).off().addValue("1", 1).on().addValue("2", 2).off().addValue("4", 4).off();
				options.addOption("Zoom speed", "setZoomSpeed")
					.addValue("0.25", 0.25).off().addValue("0.5", 0.5).off().addValue("1", 1).on().addValue("2", 2).off().addValue("4", 4).off();

				options.addOption("Auto-frame", "setAutoFrame")
					.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
				if (V.setAutoFrame === 1) {
					options.addOption("Default view", "setDefaultView").showTextBox()
						.addComment("Choose a value between 0 and 1.");
				}
				options.addOption("Large Image size", "setImageSize").showTextBox()
					.addComment("Recommended to be between 0.5 and 1.5.");
				options.addOption("Three-Quarter View", "set3QView")
					.addValue("Enabled", true).off().addValue("Disabled", false).on()
					.addComment("Toggle between frontal and three-quarter view on reset");
				options.addOption("Ambient Occlusion", "setSSAO")
					.addValue("Enabled", true).on().addValue("Disabled", false).off();
				options.addOption("SubSurface Scattering", "setSSS")
					.addValue("Enabled", true).on().addValue("Disabled", false).off();
				options.addOption("Shadowmapping", "setShadowMapping")
					.addValue("Enabled", true).on().addValue("Disabled", false).off();
				options.addOption("Idle Animations", "seeAnimation")
					.addValue("Enabled", true).on().addValue("Disabled", false).off()
					.addComment("Experimental idle animations, only visible in slave inspect screen. Will cause slowdown on some systems.");
				options.addOption("Animation FPS", "animFPS")
					.addValue("6", 6).off().addValue("12", 12).on().addValue("24", 24).off().addValue("32", 32).off();
			} else if (V.imageChoice === 2) {
				option.addComment("This art development is dead since vanilla. Since it is not embedded, requires a separate art pack to be downloaded.");
			}

			options.addOption("PA avatar art is", "seeAvatar")
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

			options.addOption("Slave images in lists are", "seeSummaryImages")
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off();

			options.addOption("Slave images in the weekly report are", "seeReportImages")
				.addValue("Enabled", 1).on().addValue("Disabled", 0).off();
		}
	}

	el.append(options.render());
	return el;
};


code:
App.UI.OptionsGroup = (function() {
	class Row {
		/**
		 * @param {HTMLDivElement} container
		 * @param {function():void} refresh
		 */
		render(container, refresh) {} // jshint ignore:line
	}

	/**
	 * @typedef value
	 * @property {*} value
	 * @property {string} [name]
	 * @property {string} mode
	 * @property {number} [compareValue]
	 * @property {string} [descAppend] can be SC markup
	 * @property {boolean} [on]
	 * @property {boolean} [off]
	 * @property {boolean} [neutral]
	 * @property {Function} [callback]
	 */

	class OptionButtonRow extends Row {
		/**
		 * @param {string|HTMLElement} description string can be HTML markup
		 * @param {string} property
		 * @param {object} [object=V]
		 */
		constructor(description, property, object = V) {
			super();
			this.description = description;
			this.property = property;
			this.object = object;
			/**
			 * @type {Array<value>}
			 */
			this.valuePairs = [];
		}

		/**
		 * @template {any} T
		 * @param {string} name
		 * @param {T|string} [value=name]
		 * @param {function(T|string):void} [callback]
		 * @returns {OptionButtonRow}
		 */
		addValue(name, value = name, callback = undefined) {
			this.valuePairs.push({
				name: name, value: value, mode: "=", callback: callback
			});
			return this;
		}

		/**
		 * @param {Array<*|Array>} values
		 * @returns {OptionButtonRow}
		 */
		addValueList(values) {
			for (const value of values) {
				if (Array.isArray(value)) {
					this.addValue(value[0], value[1], value[2]);
				} else {
					this.addValue(value);
				}
			}
			return this;
		}

		/**
		 * @param {Map} values
		 * @returns {OptionButtonRow}
		 */
		addValueMap(values) {
			for (const [key, value] of values) {
				this.addValue(key, value);
			}
			return this;
		}

		/**
		 * @param {*} value
		 * @param {number} compareValue
		 * @param {string} mode on of: "<", "<=", ">", ">="
		 * @param {string} [name=value]
		 */
		addRange(value, compareValue, mode, name = value) {
			this.valuePairs.push({
				name: name, value: value, mode: mode, compareValue: compareValue
			});
			return this;
		}

		/**
		 * @param {Object} [params]
		 * @param {string} [params.unit]
		 * @param {boolean} [params.large=false]
		 * @param {boolean} [params.forceString=false]
		 * @returns {OptionButtonRow}
		 */
		showTextBox({unit, large = false, forceString = false} = {}) {
			this.textbox = {unit: unit, large: large, forceString: forceString};
			return this;
		}

		/**
		 * @param {string|DocumentFragment|HTMLElement} comment string can be HTML
		 * @returns {OptionButtonRow}
		 */
		addComment(comment) {
			this.comment = comment;
			return this;
		}

		/**
		 * Adds a button that executes the callback when clicked AND reloads the passage
		 *
		 * @param {string} name
		 * @param {function():void} callback
		 * @param {string} passage
		 */
		customButton(name, callback, passage) {
			this.valuePairs.push({
				name: name, value: passage, callback: callback, mode: "custom"
			});
			return this;
		}

		/**
		 * @param {Node} node
		 * @returns {OptionButtonRow}
		 */
		addCustomDOM(node) {
			this.valuePairs.push({
				value: node, mode: "DOM"
			});
			return this;
		}

		/* modify last added option */

		/**
		 * Added to the description if last added value is selected.
		 * example use: addValue(...).customDescription(...).addValue(...).customDescription(...)
		 * @param {string} description can be SC markup
		 */
		customDescription(description) {
			this.valuePairs.last().descAppend = description;
			return this;
		}

		/**
		 * @param {function(any):void} callback gets executed on every button click. Selected value is given as argument.
		 */
		addCallback(callback) {
			this.valuePairs.last().callback = callback;
			return this;
		}

		/**
		 * TODO: Replace with a global callback
		 *
		 * @param {function(any):void} callback gets executed on every button click. Selected value is given as argument.
		 */
		addCallbackToEach(callback) {
			this.valuePairs.forEach(pair => pair.callback = callback);
			return this;
		}

		/**
		 * Mark option as on to style differently.
		 * @returns {OptionButtonRow}
		 */
		on() {
			this.valuePairs.last().on = true;
			return this;
		}

		/**
		 * Mark option as off to style differently.
		 * @returns {OptionButtonRow}
		 */
		off() {
			this.valuePairs.last().off = true;
			return this;
		}

		/**
		 * Puts the options in side a pulldown if there are at least 6.
		 * Not counting text boxes or comments.
		 * @returns {OptionButtonRow}
		 */
		pulldown() {
			this.enablePulldown = true;
			return this;
		}

		/**
		 * @param {HTMLDivElement} container
		 * @param {function():void} refresh
		 */
		render(container, refresh) {
			/* left side */
			const desc = document.createElement("div");
			desc.className = "description";
			$(desc).append(this.description);
			container.append(desc);

			/* right side */
			const currentValue = this.object[this.property];
			if (currentValue === undefined) {
				throw new TypeError(`Current value is undefined for option row "${this.description}"`);
			}
			let anySelected = false;

			const buttonGroup = document.createElement("div");
			buttonGroup.classList.add("button-group");
			if (!this.enablePulldown || this.valuePairs.length < 6) {
				for (const value of this.valuePairs) {
					if (value.mode === "DOM") {
						/* insert DOM and go to next element */
						buttonGroup.append(value.value);
						continue;
					}
					const button = document.createElement("button");
					button.append(value.name);
					if (value.on) {
						button.classList.add("on");
					} else if (value.off) {
						button.classList.add("off");
					}
					if (value.mode === "custom") {
						button.onclick = () => {
							value.callback();
							if (value.value) {
								Engine.play(value.value);
							} else {
								refresh();
							}
						};
					} else {
						if (value.mode === "=" && _.isEqual(currentValue, value.value)) {
							button.classList.add("selected", "disabled");
							anySelected = true;
							if (value.descAppend !== undefined) {
								desc.append(" ");
								$(desc).wiki(value.descAppend);
							}
						} else if (!anySelected && inRange(value.mode, value.compareValue, currentValue)) {
							button.classList.add("selected");
							// disable the button if clicking it won't change the variable value
							if (_.isEqual(currentValue, value.value)) {
								button.classList.add("disabled");
							}
							anySelected = true;
							if (value.descAppend !== undefined) {
								desc.append(" ");
								$(desc).wiki(value.descAppend);
							}
						}
						button.onclick = () => {
							this.object[this.property] = value.value;
							if (value.callback) {
								value.callback(value.value);
							}
							refresh();
						};
					}
					buttonGroup.append(button);
				}
			} else {
				const options = this.valuePairs.map(value => {
					return {key: value.value, name: value.name};
				});
				buttonGroup.append(App.UI.DOM.makeSelect(options, this.object[this.property], value => {
					if (!isNaN(Number(value))) {
						// @ts-ignore
						value = Number(value);
					}
					this.object[this.property] = value;
					const originalObj = this.valuePairs.find(obj => obj.value === value);
					if (originalObj && typeof originalObj.callback === "function") {
						originalObj.callback(originalObj.value);
					}
					refresh();
				}));
			}

			if (this.textbox) {
				const onlyNumber = !this.textbox.forceString && typeof currentValue === "number";
				const textbox = App.UI.DOM.makeTextBox(currentValue, input => {
					this.object[this.property] = input;
					refresh();
				}, onlyNumber);
				if (onlyNumber) {
					textbox.classList.add("number");
				}
				if (this.textbox.large) {
					textbox.classList.add("full-width");
				}
				buttonGroup.append(textbox);
				if (this.textbox.unit) {
					buttonGroup.append(" ", this.textbox.unit);
				}
			}

			if (this.comment) {
				const comment = document.createElement("span");
				comment.classList.add("comment");
				$(comment).append(this.comment);
				buttonGroup.append(comment);
			}
			container.append(buttonGroup);

			function inRange(mode, compareValue, value) {
				if (mode === "<") {
					return value < compareValue;
				} else if (mode === "<=") {
					return value <= compareValue;
				} else if (mode === ">") {
					return value > compareValue;
				} else if (mode === ">=") {
					return value >= compareValue;
				}
				return false;
			}
		}
	}

	class CustomButtonRow extends Row {
		/**
		 * @typedef CustomButton
		 * @property {string} name
		 * @property {function():void} callback
		 * @property {string} [passage] if undefined reloads current passage
		 */

		/**
		 * @param {string} description may be HTML markup
		 */
		constructor(description) {
			super();
			/**
			 * @private
			 */
			this._description = description;
			/**
			 * @type {string}
			 * @private
			 */
			this._comment = undefined;
			/**
			 * @type {Array<CustomButton>}
			 * @private
			 */
			this._buttons = [];
		}

		/**
		 * @param {string} name
		 * @param {function():void} callback
		 * @param {string} [passage] if undefined reloads current passage
		 * @returns {CustomButtonRow}
		 */
		addButton(name, callback, passage) {
			this._buttons.push({
				name: name,
				callback: callback,
				passage: passage
			});
			return this;
		}

		/**
		 * @param {string} comment may be HTML markup
		 * @returns {CustomButtonRow}
		 */
		addComment(comment) {
			this._comment = comment;
			return this;
		}

		/**
		 * @param {HTMLDivElement} container
		 * @param {function():void} refresh
		 */
		render(container, refresh) {
			/* left side */
			const desc = document.createElement("div");
			desc.className = "description";
			$(desc).append(this._description);
			container.append(desc);

			/* right side */
			const buttonGroup = document.createElement("div");
			buttonGroup.classList.add("button-group");

			for (const button of this._buttons) {
				const buttonElement = document.createElement("button");
				buttonElement.append(button.name);
				buttonElement.onclick = () => {
					button.callback();
					if (button.passage) {
						Engine.play(button.passage);
					} else {
						refresh();
					}
				};
				buttonGroup.append(buttonElement);
			}

			if (this._comment) {
				const comment = document.createElement("span");
				comment.classList.add("comment");
				$(comment).append(this._comment);
				buttonGroup.append(comment);
			}

			container.append(buttonGroup);
		}
	}

	class Comment extends Row {
		/**
		 * @param {string} comment can be SC markup
		 */
		constructor(comment) {
			super();
			this.comment = comment;
			this.long = false;
		}

		/**
		 * @param {HTMLDivElement} container
		 * @param {function():void} refresh
		 */
		render(container, refresh) {
			/* left */
			container.append(document.createElement("div"));

			/* right */
			const comment = document.createElement("div");
			comment.classList.add("comment");
			$(comment).wiki(this.comment);
			container.append(comment);
		}
	}

	class CustomRow extends Row {
		/**
		 * @param {HTMLElement|string|DocumentFragment} element
		 */
		constructor(element) {
			super();
			this.element = element;
		}

		/**
		 * @param {HTMLDivElement} container
		 */
		render(container) {
			/** @type {HTMLDivElement} */
			const div = App.UI.DOM.makeElement("div", this.element, "custom-row");
			container.append(div);
		}
	}

	return class OptionsGroup {
		constructor() {
			/**
			 * @type {Array<Row>}
			 */
			this.rows = [];
			this.doubleColumn = false;
			this.refresh = App.UI.reload;
		}

		/**
		 * @returns {OptionsGroup}
		 */
		enableDoubleColumn() {
			this.doubleColumn = true;
			return this;
		}

		/**
		 * Adds a custom function to be executed when changing an option. By default App.UI.reload() is executed.
		 * Can be overwritten by some options, the behaviour is the same as with the default function.
		 *
		 * @param {function():void} refresh
		 * @returns {OptionsGroup}
		 */
		customRefresh(refresh) {
			this.refresh = refresh;
			return this;
		}

		/**
		 * @template {Row} T
		 * @param {T} row
		 * @returns {T}
		 * @private
		 */
		_addRow(row) {
			this.rows.push(row);
			return row;
		}

		/**
		 * @param {string|HTMLElement} name
		 * @param {string} property
		 * @param {object} [object=V]
		 * @returns {OptionButtonRow}
		 */
		addOption(name, property, object = V) {
			const option = new OptionButtonRow(name, property, object);
			return this._addRow(option);
		}

		/**
		 * @param {string} name
		 * @returns {CustomButtonRow}
		 */
		addCustomOption(name) {
			return this._addRow(new CustomButtonRow(name));
		}

		/**
		 * @param {string} comment may contain SC markup
		 * @returns {Comment}
		 */
		addComment(comment) {
			const c = new Comment(comment);
			return this._addRow(c);
		}

		/**
		 * Adds a custom element taking up both rows
		 *
		 * @param {HTMLElement|string|DocumentFragment} element
		 * @returns {CustomRow}
		 */
		addCustom(element) {
			return this._addRow(new CustomRow(element));
		}

		/**
		 * @returns {HTMLDivElement}
		 */
		render() {
			const container = document.createElement("div");
			container.className = "options-group";
			if (this.doubleColumn) {
				container.classList.add("double");
			}

			for (const row of this.rows) {
				row.render(container, this.refresh);
			}

			return container;
		}
	};
})();

/** A wrapper for option handlers that shows a dialog with the results of setting the option.
 * @template T
 * @param {function(T): string|HTMLElement|DocumentFragment} contentGenerator
 * @param {string} [caption]
 * @returns {function(T): void}
 */
App.UI.DialogHandler = function(contentGenerator, caption) {
	return (arg) => {
		const dialogContent = contentGenerator(arg);
		if (dialogContent) {
			if (Dialog.isOpen()) {
				Dialog.close();
			}
			if (caption) {
				Dialog.setup(caption);
			}
			$(Dialog.body()).empty().append(dialogContent);
			Dialog.open();
		}
	};
};

repo_name: Fanatey/fc

license: mit


code:
App.UI.summaryOptions = function() {
	const el = new DocumentFragment();
	let options;

	App.UI.DOM.appendNewElement("h1", el, "Summary Options");

	App.UI.DOM.appendNewElement("p", el, `These options will affect the short slave summaries that appear on the main menu and the facility management screens.`, "scene-intro");

	App.UI.DOM.appendNewElement("h2", el, "Main menu features");

	options = new App.UI.OptionsGroup();

	options.addOption("Rules Assistant visibility", "rulesAssistantMain")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Facilities in the sidebar are", "abbreviateSidebar")
		.addValueList([["Summarized", 2], ["Abbreviated", 1]]);

	options.addOption("Sorting main menu options are", "sortSlavesMain")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	if (V.sortSlavesMain > 0) {
		options.addOption("Sorting direction", "sortSlavesOrder")
			.addValueList([["Ascending", "ascending"], ["Descending", "descending"]]);

		options.addOption("Slaves are sorted by", "sortSlavesBy")
			.addValueList([
				["Devotion", "devotion"],
				["Name", "name"],
				["Date purchased", "seniority"],
				["Age", "actualAge"],
				["How old they look", "visualAge"],
				["Age of their body", "physicalAge"],
				["Assignment", "assignment"],
				["Weekly Income", "weeklyIncome"]
			]);
	}

	el.append(options.render());

	App.UI.DOM.appendNewElement("h2", el, "Individual panels");
	App.UI.DOM.appendNewElement("div", el, "Sample summary:");
	el.append(App.UI.SlaveList.render([V.slaves.random().ID], [], App.UI.SlaveList.SlaveInteract.stdInteract));

	options = (new App.UI.OptionsGroup()).enableDoubleColumn();

	options.addOption("Panel style is", "slavePanelStyle")
		.addValueList([
			["None", 0],
			["Line Separator", 1],
			["Card", 2]
		]);

	App.UI.SlaveSummary.addOptions(options);

	options.addOption("Granular slave stat numbers are", "summaryStats")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Main menu assignment shortcuts are", "displayAssignments")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	if (V.showMissingSlaves) {
		options.addOption("Missing slave parents are", "showMissingSlavesSD")
			.addValue("Shown", true).on().addValue("Hidden", false).off();
	}

	options.addCustomOption("FC Dev's preferred options")
		.addButton(
			"Apply",
			() => {
				V.seeDesk = 0;
				V.seeFCNN = 0;
				V.sortSlavesBy = "devotion";
				V.sortSlavesOrder = "descending";
				V.sortSlavesMain = 0;
				V.rulesAssistantMain = 1;
				Object.assign(
					V.UI.slaveSummary.abbreviation,
					{
						devotion: 1,
						mental: 1,
						rules: 1,
						health: 1,
						diet: 1,
						drugs: 1,
						hormoneBalance: 1,
						race: 1,
						genitalia: 1,
						physicals: 1,
						skills: 1,
						nationality: 1,
						rulesets: 1,
						clothes: 0,
						origins: 0
					}
				);
				V.abbreviateSidebar = 1;
			}
		);

	el.append(options.render());

	return el;
};


code:
/**
 * Generate a list of links the player can safely jump to.
 *
 * Tags are:
 * jump-to-safe: Allow to jump to this passage from anywhere
 * jump-from-safe: Allow to jump from this passage to anywhere
 * hidden: Allows to jump to via history, but does not show up in the quick links menu; use together with jump-to-safe
 * no-history: Do not record history for this passage, act as if was never opened; mutually exclusive with the jump tags
 *
 * @returns {DocumentFragment}
 */
App.UI.quickMenu = (function() {
	// setup passage lists
	const jumpFrom = Story.lookup("tags", "jump-from-safe").map(passage => passage.title);
	const jumpTo = Story.lookup("tags", "jump-to-safe").map(passage => passage.title);
	const hidden = Story.lookup("tags", "jump-hidden").map(passage => passage.title);
	const noHistory = Story.lookup("tags", "no-history").map(passage => passage.title);

	// if property name is a passage name, then it's a link, otherwise only text.
	// category titles are never links to passages
	// Only two values are allowed: true or an object following the same rules
	const layout = addOtherCategory({
		Main: true,
		Manage: {
			"Manage Penthouse": true,
			"Manage Arcology": true,
			"Manage Personal Affairs": true,
			"Manage Corporation": true,
			"Personal assistant options": true,
			"Firebase": true,
			"propagandaHub": true,
			"securityHQ": true,
			"secBarracks": true,
			"riotControlCenter": true,
		},
		Social: {
			"Future Society": true,
			"Policies": true,
			"edicts": true,
			"Universal Rules": true,
			"Neighbor Interact": true,
		},
		Facilities: {
			"Head Girl Suite": true,
			"BG Select": true,
			"Brothel": true,
			"Club": true,
			"Arcade": true,
			"Dairy": true,
			"Farmyard": true,
			"Servants' Quarters": true,
			"Master Suite": true,
			"Schoolroom": true,
			"Spa": true,
			"Nursery": true,
			"Clinic": true,
			"Cellblock": true,
			"Incubator": true,
			"Pit": true,
		},
		Locations: {
			"Gene Lab": true,
			"Dispensary": true,
			"Organ Farm": true,
			"Implant Manufactory": true,
			"Prosthetic Lab": true,
			"Wardrobe": true,
			"Toy Shop": true,
			"The Black Market": true,
		},
		Tools: {
			"Find Slave": true,
			"Underperforming Slaves": true,
			"Rules Assistant": true,
			"Personal Attention Select": true,
		},
		Options: {
			"Options": true,
			"Summary Options": true,
			"Description Options": true,
			"Hotkey Settings": true,
		}
	});

	// true means hidden
	const hiddenPassages = cleanPassageMapping({
		"Arcade": () => !V.arcade,
		"BG Select": () => !V.dojo,
		"Brothel": () => !V.brothel,
		"Cellblock": () => !V.cellblock,
		"Clinic": () => !V.clinic,
		"Club": () => !V.club,
		"Dairy": () => !V.dairy,
		"Dispensary": () => !V.dispensary,
		"edicts": () => V.secExpEnabled <= 0,
		"Farmyard": () => !V.farmyard,
		"Firebase": () => !V.SF.Toggle || V.SF.Toggle < 1 || V.SF.Active < 1,
		"Future Society": () => !V.FSAnnounced,
		"Gene Lab": () => !V.geneticMappingUpgrade,
		"Toy Shop": () => !V.toyShop,
		"Head Girl Suite": () => !V.HGSuite,
		"Implant Manufactory": () => !V.ImplantProductionUpgrade,
		"Incubator": () => V.incubator.capacity === 0,
		"Manage Corporation": () => V.corp.Announced !== 1,
		"Master Suite": () => !V.masterSuite,
		"Neighbor Interact": () => V.arcologies.length === 0,
		"Nursery": () => !V.nursery,
		"Organ Farm": () => !V.organFarmUpgrade,
		"Pit": () => !V.pit,
		"propagandaHub": () => V.secExpEnabled === 0 || !V.SecExp.buildings.propHub,
		"Prosthetic Lab": () => V.researchLab.level === 0,
		"riotControlCenter": () => V.secExpEnabled === 0 || !V.SecExp.buildings.riotCenter,
		"Schoolroom": () => !V.schoolroom,
		"secBarracks": () => V.secExpEnabled === 0 || !V.SecExp.buildings.barracks,
		"securityHQ": () => V.secExpEnabled === 0 || !V.SecExp.buildings.secHub,
		"Servants' Quarters": () => !V.servantsQuarters,
		"Spa": () => !V.spa,
		"The Black Market": () => V.rep < 10000,
		"Weapons Manufacturing": () => true,
	});

	// show different names than the actual passage name, can be a function
	const uiNames = cleanPassageMapping({
		"Arcade": () => App.Entity.facilities.arcade.UIName,
		"BG Select": "Armory",
		"Brothel": () => App.Entity.facilities.brothel.UIName,
		"Cellblock": () => App.Entity.facilities.cellblock.UIName,
		"Clinic": () => App.Entity.facilities.clinic.UIName,
		"Club": () => App.Entity.facilities.club.UIName,
		"Dairy": () => App.Entity.facilities.dairy.UIName,
		"Dispensary": "Pharmaceutical Fabricator",
		"edicts": "Edicts",
		"Farmyard": () => App.Entity.facilities.farmyard.UIName,
		"Find Slave": "Locate Slave",
		"Firebase": () => `${capFirstChar(V.SF.Lower || "No one")}'s Firebase`,
		"Future Society": "Future Societies",
		"Head Girl Suite": () => App.Entity.facilities.headGirlSuite.UIName,
		"Incubator": () => App.Entity.facilities.incubator.UIName,
		"Master Suite": () => App.Entity.facilities.masterSuite.UIName,
		"Neighbor Interact": "Diplomacy",
		"Nursery": () => App.Entity.facilities.nursery.UIName,
		"Options": "Game Options",
		"Personal assistant options": "Personal Assistant",
		"Personal Attention Select": "Personal Attention",
		"Pit": () => App.Entity.facilities.pit.UIName,
		"propagandaHub": "Manage PR",
		"riotControlCenter": "Manage Rebels",
		"Schoolroom": () => App.Entity.facilities.schoolroom.UIName,
		"secBarracks": "Manage Military",
		"securityHQ": "Manage Security",
		"Servants' Quarters": () => App.Entity.facilities.servantsQuarters.UIName,
		"Spa": () => App.Entity.facilities.spa.UIName,
		"The Black Market": "Black Market",
	});

	// extra information behind the link, is a function
	const f = App.Entity.facilities;
	const extraInfo = cleanPassageMapping({
		"Arcade": () => occupancy(f.arcade),
		"BG Select": () => occupancy(f.armory),
		"Brothel": () => occupancy(f.brothel),
		"Cellblock": () => occupancy(f.cellblock),
		"Clinic": () => occupancy(f.clinic),
		"Club": () => occupancy(f.club),
		"Dairy": () => occupancy(f.dairy),
		"Farmyard": () => occupancy(f.farmyard),
		"Head Girl Suite": () => occupancy(f.headGirlSuite),
		"Incubator": () => occupancy(f.incubator),
		"Master Suite": () => occupancy(f.masterSuite),
		"Nursery": () => occupancy(f.nursery),
		"Pit": () => occupancy(f.pit),
		"Schoolroom": () => occupancy(f.schoolroom),
		"Servants' Quarters": () => occupancy(f.servantsQuarters),
		"Spa": () => occupancy(f.spa),
	});

	// true shows a notification symbol
	const notifications = cleanPassageMapping({
		"Future Society": () => FutureSocieties.availCredits() > 0 || V.FSReminder,
		"Incubator": () => V.incubator.capacity > 0 && V.incubator.readySlaves > 0,
		"Manage Corporation": () => V.corp.SpecToken > 0 && V.corp.SpecTimer === 0,
	});

	/**
	 * The DOM element of name of the currently played passage or any of it's parents. Used during generation to
	 * uncollapse the category with the current passage.
	 * @type {HTMLElement}
	 */
	let currentPassage;
	/**
	 * DOM elements that have a notification. Used to traverse upwards to give the categories notifications too.
	 * @type {Array<HTMLElement>}
	 */
	let notificationPassages;

	let hotkeysEnabled = false;

	// register hotkeys
	// this is in it's own scope as we can forget the hotkeys object immediately afterwards
	{
		// setup hotkeys list, upper/lower case is important!
		const hotkeys = cleanPassageMapping({
			"BG Select": "b",
			"Buy Slaves": "s",
			"edicts": "shift+e",
			"Firebase": "z",
			"Future Society": "f",
			"Main": "m",
			"Manage Arcology": "c",
			"Manage Corporation": "shift+c",
			"Manage Penthouse": "p",
			"Manage Personal Affairs": "x",
			"Neighbor Interact": "d",
			"Options": "o",
			"Personal assistant options": "t",
			"Personal Attention Select": "a",
			"Policies": "y",
			"propagandaHub": "shift+h",
			"Recruiter Select": "u",
			"riotControlCenter": "shift+r",
			"Rules Assistant": "r",
			"secBarracks": "shift+a",
			"securityHQ": "shift+s",
			"Universal Rules": "v",
			// Facilities
			"Brothel": "1",
			"Club": "2",
			"Arcade": "3",
			"Dairy": "4",
			"Farmyard": "5",
			"Servants' Quarters": "6",
			"Master Suite": "7",
			"Schoolroom": "8",
			"Spa": "9",
			"Nursery": "0",
			"Clinic": "shift+1",
			"Cellblock": "shift+2",
			"Incubator": "shift+3",
			"Pit": "shift+4",
		});

		// register
		for (const passage of jumpTo) {
			if (!hidden.includes(passage) ||
				// some passages may be hidden, but still accessible through hotkeys
				hotkeys.hasOwnProperty(passage)) {
				const action = {
					callback: () => {
						if (hotkeysEnabled &&
							// we are not already on the passage
							State.passage !== passage &&
							// the passage is accessible
							!(hiddenPassages[passage] && hiddenPassages[passage]())) {
							Engine.play(passage);
						}
					},
					combinations: [],
				};
				// add hotkey if there is one
				if (hotkeys[passage]) {
					action.combinations.push(hotkeys[passage]);
				}
				// custom ui text
				if (uiNames[passage]) {
					action.uiName = uiNames[passage];
				}
				App.UI.Hotkeys.add(passage, action);
			}
		}
	}

	// setup history
	let history = [];
	let historyNavigation = false;
	$(document).on(':passageinit', event => {
		// if navigated here normally, add passage to history, otherwise remove last entry from history
		if (!historyNavigation) {
			if (State.passage === event.passage.title || // reloaded
				noHistory.includes(State.passage) || // came from a no-history passage
				noHistory.includes(event.passage.title) // no history for this passage
			) {
				return;
			}
			// if last passage can be jumped to add passage to history, otherwise clear history
			if (jumpTo.includes(State.passage)) {
				history.unshift(State.passage);
			} else {
				history = [];
			}
		} else {
			historyNavigation = false;
			history.shift();
		}
	});
	App.UI.Hotkeys.add("historyBack", {callback: goBack, combinations: ["backspace"], uiName: "Back in history"});

	/**
	 * Goes back in history if possible.
	 */
	function goBack() {
		if (history.length > 0 && jumpFrom.includes(State.passage)) {
			historyNavigation = true;
			Engine.play(history[0]);
		}
	}

	/**
	 * Add a "Return" link at second position if there is a history.
	 * @param {Array<HTMLElement>} linkList
	 */
	function addBackLink(linkList) {
		if (history.length > 0) {
			const div = document.createElement("div");
			div.classList.add("menu-link");
			const a = document.createElement("a");
			a.append("Return");
			a.onclick = goBack;
			const hotkey = App.UI.Hotkeys.hotkeys("historyBack");
			if (hotkey !== "") {
				div.append(a, " ", App.UI.DOM.makeElement("span", hotkey, "hotkey"));
			}
			// insert at second position
			linkList.splice(1, 0, div);
		}
	}

	/**
	 * Generate a quick menu
	 * @returns {string|HTMLDivElement}
	 */
	function generateMenu() {
		if (!jumpFrom.includes(State.passage)) {
			hotkeysEnabled = false;
			return "";
		}
		hotkeysEnabled = true;
		currentPassage = null;
		notificationPassages = [];

		const div = document.createElement("div");
		div.classList.add("quick-links");

		try {
			// quick menu
			const links = generateLinkList(layout);

			if (V.debugMode) {
				addCategory(links, "Hidden", hidden.reduce((acc, cur) => {
					acc[cur] = true;
					return acc;
				}, {}));
			}

			addBackLink(links);
			div.append(...links);

			// traverse from current passage up to uncollapse.
			if (currentPassage !== null) {
				while (!currentPassage.classList.contains("quick-links")) {
					currentPassage.classList.remove("collapsed");
					currentPassage = currentPassage.parentElement;
				}
			}

			// traverse from notifications up to add icon
			for (let i = 0; i < notificationPassages.length; i++) {
				while (!notificationPassages[i].classList.contains("quick-links")) {
					if (notificationPassages[i].classList.contains("category")) {
						notificationPassages[i].classList.add("notification");
					}
					notificationPassages[i] = notificationPassages[i].parentElement;
				}
			}
		} catch (e) {
			console.error(e);
			div.append("Quick Links generation failed. Please run ", App.UI.DOM.passageLink("Backwards Compatibility", "Backwards Compatibility"));
		}

		return div;
	}

	/**
	 * Create a list of links and/or categories based on the given category
	 *
	 * @param {object} category
	 * @returns {Array<HTMLElement>}
	 */
	function generateLinkList(category) {
		/** @type {Array<HTMLElement>} */
		const links = [];
		for (const passage in category) {
			if (category[passage] === true) {
				addMenuLink(links, passage);
			} else {
				addCategory(links, passage, category[passage]);
			}
		}
		return links;
	}

	/**
	 * Add a category to a link list
	 *
	 * @param {Array<HTMLElement>} outerLinkList
	 * @param {string} name
	 * @param {object} category
	 */
	function addCategory(outerLinkList, name, category) {
		const innerLinkList = generateLinkList(category);
		if (innerLinkList.length === 0) {
			// category is empty
			return;
		}
		const toggle = document.createElement("div");
		toggle.classList.add("toggle");
		toggle.append(name);
		const content = document.createElement("div");
		content.classList.add("content");
		content.append(...innerLinkList);

		// wrap everything in one div, so we can control collapsing by changing only one class
		const wrapper = document.createElement("div");
		wrapper.classList.add("category", "collapsed");
		toggle.onclick = () => {
			wrapper.classList.toggle("collapsed");
		};
		wrapper.append(toggle, content);
		outerLinkList.push(wrapper);
	}

	/**
	 * Create a link to the given passage and add it to the link list if link is accessible.
	 *
	 * @param {Array<HTMLElement>} linkList
	 * @param {string} passage
	 */
	function addMenuLink(linkList, passage) {
		// passage is hidden
		if (hiddenPassages.hasOwnProperty(passage) && hiddenPassages[passage]()) {
			return;
		}
		// we show some kind of text/link
		const div = document.createElement("div");
		linkList.push(div);
		div.classList.add("menu-link");
		// this passage has a notification
		if (notifications[passage] && notifications[passage]()) {
			notificationPassages.push(div);
			div.classList.add("notification");
		}
		// there is additional non-link info
		if (extraInfo[passage]) {
			div.append(" ", extraInfo[passage]());
		}
		// we are already on this passage
		if (State.passage === passage) {
			div.prepend(uiName(passage));
			currentPassage = div;
			return;
		}
		// That is not a passage we can safely jump to, it's a deco element
		if (!jumpTo.includes(passage)) {
			div.prepend(uiName(passage));
			return;
		}
		// Create link
		const a = document.createElement("a");
		a.append(uiName(passage));
		a.onclick = () => {
			Engine.play(passage);
		};
		div.prepend(a);
		const hotkeyString = App.UI.Hotkeys.hotkeys(passage);
		if (hotkeyString !== "") {
			div.append(" ", App.UI.DOM.makeElement("span", hotkeyString, "hotkey"));
		}
	}

	/**
	 * Gives back the proper name for the passage or the passage title itself if there is none
	 *
	 * @param {string} passage
	 * @returns {string}
	 */
	function uiName(passage) {
		if (uiNames[passage]) {
			if (typeof uiNames[passage] === "function") {
				return uiNames[passage]();
			}
			return uiNames[passage];
		}
		return passage;
	}

	/**
	 * Adds an "Other" category to a given layout consisting of passages that can be jumped to not sorted into the
	 * existing layout.
	 *
	 * @param {object} layout
	 * @returns {object}
	 */
	function addOtherCategory(layout) {
		const passages = jumpTo.slice();

		// filter out passages used in layout
		filterPassages(passages, layout);

		// filter out hidden passages
		for (let i = 0; i < hidden.length; i++) {
			const index = passages.indexOf(hidden[i]);
			if (index > -1) {
				passages.splice(index, 1);
			}
		}

		// Other category
		if (passages.length > 0) {
			const other = {};
			for (let i = 0; i < passages.length; i++) {
				other[passages[i]] = true;
			}
			layout.Other = other;
		}
		return layout;
	}

	/**
	 * Remove all entries from a given list that exist as keys in a given layout.
	 *
	 * @param {Array<string>} passages
	 * @param {object} layout
	 */
	function filterPassages(passages, layout) {
		for (const category in layout) {
			if (layout[category] === true) {
				const index = passages.indexOf(category);
				if (index > -1) {
					passages.splice(index, 1);
				}
			} else {
				filterPassages(passages, layout[category]);
			}
		}
	}

	/**
	 * @param {App.Entity.Facilities.Facility} facility
	 * @returns {string}
	 */
	function occupancy(facility) {
		const desc = facility.occupancyReport(V.abbreviateSidebar === 2);
		if (desc === "") {
			return "";
		}
		return `(${desc})`;
	}

	/**
	 * Cleans out all mappings that are not contained in jumpTo and therefore not considered safe.
	 *
	 * @param {object} keys
	 * @returns {object}
	 */
	function cleanPassageMapping(keys) {
		const result = {};
		for (const key in keys) {
			if (jumpTo.includes(key)) {
				result[key] = keys[key];
			} else {
				console.log("Found mapping to unsafe passage: " + key + " to " + keys[key]);
			}
		}
		return result;
	}

	return generateMenu;
})();


code:
/**
 * Notify the game that the sidebar needs to be refreshed as soon as possible, but do not do it immediately.
 * This allows us to call this function repeatedly without impacting performance (for example, from repX() and cashX()).
 * The game will redraw the sidebar exactly once, as soon as all the scripts have finished executing.
 */
App.Utils.scheduleSidebarRefresh = (function() {
	let refresh = false;

	function updateSidebar() {
		refresh = false;
		UIBar.update();
	}

	function schedule() {
		if (!refresh) {
			refresh = true;
			setTimeout(updateSidebar, 0);
		}
	}

	return schedule;
})();

App.Utils.userButton = function(nextButton = V.nextButton, nextLink = V.nextLink) {
	const el = document.createElement("span");
	el.id = "next-button-wrapper"; // We must always have a named element so we have something to refresh even if the button is hidden for a scene
	const pass = passage();
	let link;

	if (pass !== "End Week") {
		if (pass === "Main") {
			link = App.UI.DOM.link(
				"END WEEK",
				() => endWeek()
			);
			link.style.fontWeight = "bold";
			link.id = "endWeekButton";
			el.append(link);
			el.append(" ");
			App.UI.DOM.appendNewElement("span", el, App.UI.Hotkeys.hotkeys("endWeek"), ["hotkey"]);
		} else {
			if (nextButton !== " ") {
				link = App.UI.DOM.passageLink(
					nextButton,
					nextLink
				);
				link.style.fontWeight = "bold";
				link.id = "nextButton";
				el.append(link);
				el.append(" ");
				App.UI.DOM.appendNewElement("span", el, App.UI.Hotkeys.hotkeys("nextLink"), ["hotkey"]);
			}
		}
	}
	return el;
};

App.Utils.updateUserButton = function() {
	return jQuery("#next-button-wrapper").empty().append(App.Utils.userButton());
};


code:
App.UI.storyCaption = function() {
	const pass = passage();
	const fragment = new DocumentFragment();

	if (V.ui !== "start") {
		App.UI.DOM.appendNewElement("p", fragment, App.Utils.userButton());

		if (V.cheatMode || V.debugMode) {
			App.UI.DOM.appendNewElement("div", fragment, pass);
			if (V.event instanceof App.Events.BaseEvent) {
				App.UI.DOM.appendNewElement("div", fragment, V.event.eventName);
			}
		}

		fragment.append(week());
		fragment.append(weather());

		if (V.sideBarOptions.Cash > 0) {
			fragment.append(cash());
		}

		if (V.mods.food.enabled && V.mods.food.market) {
			fragment.append(food());
		}

		if (V.sideBarOptions.Upkeep > 0) {
			fragment.append(upkeep());
		}

		if (V.sideBarOptions.SexSlaveCount > 0) {
			fragment.append(slaves());
		}

		if (V.sideBarOptions.roomPop > 0 && V.sideBarOptions.Style === 'expanded') {
			fragment.append(rooms());
		}

		if (pass !== "Manage Penthouse") {
			if (V.sideBarOptions.GSP > 0 && pass !== "Neighbor Interact") {
				fragment.append(economy());
			}

			if (V.sideBarOptions.Rep > 0) {
				fragment.append(reputation());
			}

			if (V.secExpEnabled > 0) {
				if (V.sideBarOptions.Authority > 0) {
					fragment.append(authority());
				}

				if (V.sideBarOptions.Security > 0) {
					fragment.append(security());
				}

				if (V.sideBarOptions.Crime > 0) {
					fragment.append(crime());
				}
			}
		}

		App.UI.DOM.appendNewElement("p", fragment, App.UI.quickMenu());

		if (V.nextButton !== "Continue" && V.nextButton !== " ") {
			if (V.debugMode > 0) {
				fragment.append(debug());
			}
		}

		const reminders = App.UI.DOM.appendNewElement("div", fragment, App.UI.DOM.link("Reminders", () => App.Reminders.dialog()));
		if (V.reminders.find(r => r.week <= V.week)) {
			reminders.append(" ", App.UI.DOM.makeElement("span", String.fromCharCode(0xe80c), ["icons", "noteworthy"]));
		}
	} else if (pass === "Starting Girls") {
		fragment.append(startingGirls());
	}

	const p = document.createElement("p");
	p.append(App.UI.DOM.makeElement("span", "FCE", ["bold"]), ": ",
		App.Encyclopedia.Dialog.linkDOM(V.encyclopedia, V.encyclopedia));
	fragment.append(p);

	return fragment;

	function week() {
		const fragment = new DocumentFragment();
		const div = document.createElement("div");
		const tooltip = document.createElement("div");
		App.UI.DOM.appendNewElement("div", tooltip, capFirstChar(years(V.week)));
		if (V.arcologies[0].weeks !== V.week) {
			App.UI.DOM.appendNewElement("div", tooltip, `${capFirstChar(years(V.arcologies[0].weeks))} in ${V.arcologies[0].name}`);
		}
		App.UI.DOM.appendNewElement("span", fragment, App.UI.DOM.spanWithTooltip(`Week ${V.week}`, tooltip, ['bold']));
		fragment.append(div);
		App.UI.DOM.appendNewElement("div", fragment, `Week of ${asDateString(V.week)}`);

		if (showCheats()) {
			fragment.append(App.UI.DOM.makeTextBox(V.week, week => {
				week = Math.trunc(week);
				V.week = week > 0 ? week : V.week;
				App.Utils.scheduleSidebarRefresh();
			}, true));
		}
		return fragment;
	}

	function weather() {
		const div = App.UI.DOM.appendNewElement("div", fragment, V.weatherToday.name, ["note"]);
		if (V.weatherToday.severity === 1) {
			div.classList.add("cyan");
		} else if (V.weatherToday.severity === 2) {
			div.classList.add("yellow");
		} else if (V.weatherToday.severity === 3) {
			div.classList.add("orange");
		} else {
			div.classList.add("red");
		}
		return div;
	}

	function cash() {
		const fragment = new DocumentFragment();
		V.cash = Math.trunc(V.cash);
		const css = ["cash"];
		if (V.cash <= 0) {
			css.push("dec");
		}
		const cashDiv = document.createElement("div");
		const cashDiff = V.cash - V.cashLastWeek;
		const classes = cashDiff > 0 ? 'cash' : 'cash dec';

		App.UI.DOM.appendNewElement("span", cashDiv, "Cash", css);
		cashDiv.append(" | ");
		App.UI.DOM.appendNewElement("span", cashDiv, cashFormat(V.cash), css);
		fragment.append(cashDiv);

		if (V.sideBarOptions.Style === 'compact') {
			tippy(cashDiv, {
				content: `<span class="${classes}">${cashFormat(cashDiff)}</span> since last week`,
				allowHTML: true,
			});
		} else {
			if (pass === "Main") {
				const div = document.createElement("div");
				div.append("(");
				let cashDiff = V.cash - V.cashLastWeek;
				if (cashDiff > 0) {
					App.UI.DOM.appendNewElement("span", div, cashFormat(cashDiff), ["green"]);
				} else {
					App.UI.DOM.appendNewElement("span", div, cashFormat(cashDiff), ["cash", "dec"]);
				}
				div.append(" since last week)");
				fragment.append(div);
			}
		}

		if (V.lastCashTransaction) {
			const transaction = document.createElement("div");
			css.push('note');
			App.UI.DOM.appendNewElement("span", transaction, `Last transaction`, css);
			transaction.append(" | ");
			if (V.lastCashTransaction > 0) {
				App.UI.DOM.appendNewElement("span", transaction, cashFormat(V.lastCashTransaction), css);
			} else {
				App.UI.DOM.appendNewElement("span", transaction, cashFormat(V.lastCashTransaction), ["cash", "dec"]);
			}
			fragment.append(transaction);
		}

		if (showCheats()) {
			fragment.append(App.UI.DOM.makeTextBox(V.cash, cash => {
				V.cash = Math.trunc(cash);
				V.cheater = 1;
				App.Utils.scheduleSidebarRefresh();
			}, true));
		}
		return fragment;
	}

	function food() {
		const fragment = new DocumentFragment();
		const foodDiv = document.createElement("div");

		const css = ["food"];
		if (V.mods.food.amount < App.Facilities.Farmyard.foodConsumption()) {
			css.push("dec");
		}
		const foodDiff = V.mods.food.amount - V.mods.food.lastWeek;
		const classes = foodDiff > 0 ? 'food' : 'food dec';

		/* check if there is enough food for the next week */
		App.UI.DOM.appendNewElement("span", foodDiv, "Food", css);
		if (V.mods.food.amount < 0) {
			V.mods.food.amount = 0;
		}
		foodDiv.append(" | ");
		App.UI.DOM.appendNewElement("span", foodDiv, massFormat(V.mods.food.amount), css);
		fragment.append(foodDiv);

		if (V.sideBarOptions.Style === 'compact') {
			tippy(foodDiv, {
				content: `<span class="${classes}">${massFormat(foodDiff)}</span> since last week`,
				allowHTML: true,
			});
		} else {
			if (pass === "Main") {
				const innerDiv = document.createElement("div");
				innerDiv.append("(");
				const foodDiff = (V.mods.food.amount - V.mods.food.lastWeek);
				App.UI.DOM.appendNewElement("span", innerDiv, massFormat(foodDiff), foodDiff < 0 ? ["red"] : ["chocolate"]);
				innerDiv.append(" since last week)");
				fragment.append(innerDiv);
			}
		}

		if (showCheats()) {
			fragment.append(App.UI.DOM.makeTextBox(V.mods.food.amount, food => {
				food = Math.trunc(food);
				V.mods.food.amount = food > 0 ? food : 0;
				App.Utils.scheduleSidebarRefresh();
			}, true));
		}
		return fragment;
	}

	function upkeep() {
		const div = document.createElement("div");
		if (pass === "Main") {
			div.append(App.UI.DOM.passageLink("Upkeep", "Costs Budget"), " | ");
		} else {
			div.append(" Upkeep | ");
		}
		App.UI.DOM.appendNewElement("span", div, cashFormat(V.costs), ["cash", "dec"]);
		return div;
	}

	function slaves() {
		const div = document.createElement("div");
		const dormsPop = V.dormitoryPopulation;
		const roomsPop = V.roomsPopulation;
		const dormsCap = V.dormitory;
		const roomsCap = V.rooms;
		const css = [];

		if ((dormsPop > dormsCap || roomsPop > roomsCap) && V.sideBarOptions.Style === 'compact') {
			css.push("warning");
		} else {
			css.push("pink");
		}

		App.UI.DOM.appendNewElement("span", div, "Total Sex Slaves", css);
		div.append(` | ${V.slaves.length}`);
		if (V.sideBarOptions.roomPop > 0 && V.sideBarOptions.Style === 'compact') {
			tippy(div, {
				content: rooms(),
				allowHTML: true,
			});
		}
		return div;
	}

	function rooms() {
		const fragment = new DocumentFragment();

		fragment.append(roomRow("Penthouse Beds", V.dormitoryPopulation + V.roomsPopulation, V.dormitory + V.rooms));
		fragment.append(roomRow("Dormitory Beds", V.dormitoryPopulation, V.dormitory));
		fragment.append(roomRow("Luxury Rooms", V.roomsPopulation, V.rooms));

		return fragment;
	}

	/**
	 * @param {string} name
	 * @param {number} pops
	 * @param {number} cap
	 * @returns {HTMLDivElement}
	 */
	function roomRow(name, pops, cap) {
		const div = document.createElement("div");
		App.UI.DOM.appendNewElement("span", div, name, ["pink"]);
		div.append(" | ");
		if (pops > cap) {
			App.UI.DOM.appendNewElement("span", div, String(pops), ["warning"]);
		} else {
			div.append(String(pops));
		}
		div.append(`/${cap}`);
		return div;
	}

	function economy() {
		const div = document.createElement("div");
		App.UI.DOM.appendNewElement("span", div, "GSP", ["yellowgreen"]);
		div.append(` | ${num(Math.trunc(0.1 * V.arcologies[0].prosperity))}`);
		if (V.showNumbers === 2 || (V.showNumbers === 1 && Math.trunc(0.1 * V.arcologies[0].prosperity) > V.showNumbersMax)) {
			div.append("m");
		} else {
			div.append(" million");
		}
		div.append(" ", ownershipReport(true));
		return div;
	}

	function reputation() {
		const div = document.createElement("div");
		if (pass === "Main") {
			div.append(App.UI.DOM.passageLink("Rep", "Rep Budget"));
		} else {
			App.UI.DOM.appendNewElement("span", div, "Rep", ["reputation", "inc"]);
		}
		div.append(" | ");

		if (V.rep > 19000) {
			App.UI.DOM.appendNewElement("span", div, "worshipped").style.color = "rgb(0,145,0)";
		} else if (V.rep > 18000) {
			App.UI.DOM.appendNewElement("span", div, "great").style.color = "rgb(0,150,0)";
		} else if (V.rep > 17000) {
			App.UI.DOM.appendNewElement("span", div, "exalted").style.color = "rgb(0,155,0)";
		} else if (V.rep > 16000) {
			App.UI.DOM.appendNewElement("span", div, "illustrious").style.color = "rgb(0,160,0)";
		} else if (V.rep > 15000) {
			App.UI.DOM.appendNewElement("span", div, "prestigious").style.color = "rgb(0,165,0)";
		} else if (V.rep > 14000) {
			App.UI.DOM.appendNewElement("span", div, "renowned").style.color = "rgb(0,170,0)";
		} else if (V.rep > 13000) {
			App.UI.DOM.appendNewElement("span", div, "famed").style.color = "rgb(0,175,0)";
		} else if (V.rep > 12000) {
			App.UI.DOM.appendNewElement("span", div, "celebrated").style.color = "rgb(0,180,0)";
		} else if (V.rep > 11000) {
			App.UI.DOM.appendNewElement("span", div, "honored").style.color = "rgb(0,185,0)";
		} else if (V.rep > 10000) {
			App.UI.DOM.appendNewElement("span", div, "acclaimed").style.color = "rgb(0,190,0)";
		} else if (V.rep > 9000) {
			App.UI.DOM.appendNewElement("span", div, "eminent").style.color = "rgb(0,195,0)";
		} else if (V.rep > 8250) {
			App.UI.DOM.appendNewElement("span", div, "prominent").style.color = "rgb(0,200,0)";
		} else if (V.rep > 7500) {
			App.UI.DOM.appendNewElement("span", div, "distinguished").style.color = "rgb(0,205,0)";
		} else if (V.rep > 6750) {
			App.UI.DOM.appendNewElement("span", div, "admired").style.color = "rgb(0,210,0)";
		} else if (V.rep > 6000) {
			App.UI.DOM.appendNewElement("span", div, "esteemed").style.color = "rgb(0,215,0)";
		} else if (V.rep > 5250) {
			App.UI.DOM.appendNewElement("span", div, "respected").style.color = "rgb(0,220,0)";
		} else if (V.rep > 4500) {
			App.UI.DOM.appendNewElement("span", div, "known").style.color = "rgb(0,225,0)";
		} else if (V.rep > 3750) {
			App.UI.DOM.appendNewElement("span", div, "recognized").style.color = "rgb(0,230,0)";
		} else if (V.rep > 3000) {
			App.UI.DOM.appendNewElement("span", div, "rumored").style.color = "rgb(0,235,0)";
		} else if (V.rep > 2250) {
			App.UI.DOM.appendNewElement("span", div, "envied").style.color = "rgb(0,240,0)";
		} else if (V.rep > 1500) {
			App.UI.DOM.appendNewElement("span", div, "resented").style.color = "rgb(0,245,0)";
		} else if (V.rep > 750) {
			App.UI.DOM.appendNewElement("span", div, "disliked").style.color = "rgb(0,250,0)";
		} else {
			App.UI.DOM.appendNewElement("span", div, "unknown").style.color = "rgb(0,255,0)";
		}
		div.append(` (${num(V.rep)})`);

		if (showCheats()) {
			div.append(App.UI.DOM.makeTextBox(V.rep, rep => {
				V.rep = Math.clamp(Math.trunc(rep), 0, 20000);
				V.cheater = 1;
				App.Utils.scheduleSidebarRefresh();
			}, true));
		}
		return div;
	}

	function authority() {
		const div = document.createElement("div");
		App.UI.DOM.appendNewElement("span", div, "Auth", ["darkviolet"]);

		div.append(" | ");
		const span = document.createElement("span");
		span.className = "darkviolet";
		App.Mods.SecExp.authorityX();
		if (V.SecExp.core.authority > 19500) {
			span.append("divine will");
		} else if (V.SecExp.core.authority > 19000) {
			span.append("sovereign");
		} else if (V.SecExp.core.authority > 18000) {
			span.append("monarch");
		} else if (V.SecExp.core.authority > 17000) {
			span.append("tyrant");
		} else if (V.SecExp.core.authority > 15000) {
			span.append("dictator");
		} else if (V.SecExp.core.authority > 14000) {
			span.append("prince");
		} else if (V.SecExp.core.authority > 13000) {
			span.append("master");
		} else if (V.SecExp.core.authority > 12000) {
			span.append("leader");
		} else if (V.SecExp.core.authority > 11000) {
			span.append("director");
		} else if (V.SecExp.core.authority > 10000) {
			span.append("overseer");
		} else if (V.SecExp.core.authority > 9000) {
			span.append("chief");
		} else if (V.SecExp.core.authority > 8000) {
			span.append("manager");
		} else if (V.SecExp.core.authority > 7000) {
			span.append("principal");
		} else if (V.SecExp.core.authority > 6000) {
			span.append("auxiliary");
		} else if (V.SecExp.core.authority > 5000) {
			span.append("subordinate");
		} else if (V.SecExp.core.authority > 4000) {
			span.append("follower");
		} else if (V.SecExp.core.authority > 3000) {
			span.append("powerless");
		} else if (V.SecExp.core.authority > 2000) {
			span.append("toothless");
		} else if (V.SecExp.core.authority > 1000) {
			span.append("mostly harmless");
		} else {
			span.append("harmless");
		}

		div.append(span, ` (${num(Math.trunc(V.SecExp.core.authority))})`);
		if (showCheats()) {
			div.append(App.UI.DOM.makeTextBox(V.SecExp.core.authority, auth => {
				V.SecExp.core.authority = Math.clamp(Math.trunc(auth), 0, 20000);
				V.cheater = 1;
				App.Utils.scheduleSidebarRefresh();
			}));
		}
		return div;
	}

	function security() {
		const div = document.createElement("div");
		App.UI.DOM.appendNewElement("span", div, "Security", ["deepskyblue"]);
		div.append(" | ");

		App.UI.DOM.appendNewElement("span", div, `${Math.trunc(V.SecExp.core.security)}%`, ["deepskyblue"]);
		if (showCheats()) {
			div.append(App.UI.DOM.makeTextBox(V.SecExp.core.security, sec => {
				V.SecExp.core.security = Math.clamp(Math.trunc(sec), 0, 100);
				V.cheater = 1;
				App.Utils.scheduleSidebarRefresh();
			}, true));
		}
		return div;
	}

	function crime() {
		V.SecExp.core.crimeLow = Math.clamp(V.SecExp.core.crimeLow, 0, 100);
		const div = document.createElement("div");
		App.UI.DOM.appendNewElement("span", div, "Crime", ["orangered"]);
		div.append(" | ");
		App.UI.DOM.appendNewElement("span", div, `${Math.trunc(V.SecExp.core.crimeLow)}%`, ["orangered"]);

		if (showCheats()) {
			div.append(App.UI.DOM.makeTextBox(V.SecExp.core.crimeLow, crime => {
				V.SecExp.core.crimeLow = Math.clamp(Math.trunc(crime), 0, 100);
				V.cheater = 1;
				App.Utils.scheduleSidebarRefresh();
			}, true));
		}
		return div;
	}

	function showCheats() {
		return pass === "Main" && V.cheatMode && V.cheatModeM;
	}

	function debug() {
		const p = document.createElement("p");
		p.append("Debugging Tools");

		App.UI.DOM.appendNewElement("div", p, App.UI.DOM.link("Display Variables", App.checkVars));

		App.UI.DOM.appendNewElement("div", p, App.UI.DOM.link("Display Changed Variables", () => {
			/* makes sure we store the current state so we can return to it */
			Config.history.maxStates = 2;
			Engine.play("Variable Difference");
		}));

		if (V.debugModeCustomFunction > 0) {
			const textarea = document.createElement("textarea");
			textarea.id = "cheat-function";
			p.append(textarea);

			p.append(App.UI.DOM.link("Run Custom Function", () => {
				let value = textarea.value;
				if (value.charAt(0) !== "(") {
					value = `() => {${value}}`;
				}
				if (typeof eval(value) === "function") {
					(eval(value))();
					Engine.play(pass);
				} else {
					Dialog.append("Not a valid function").open();
				}
			}));
		}

		App.UI.DOM.appendNewElement("div", p, App.UI.DOM.link("Dump Game State", App.Debug.dumpGameState));

		return p;
	}

	function startingGirls() {
		// @ts-ignore // In starting girls we know that there is always an active slave
		let cost = startingSlaveCost(V.activeSlave);
		const p = document.createElement("p");

		if (cost > V.cash) {
			const div = document.createElement("div");
			div.classList.add("cash", "dec");
			div.append("This slave will cost ",
				App.UI.DOM.makeElement("span", cashFormat(cost), ["bold"]), ".",
				App.UI.DOM.makeElement("div", `You only have: ${cashFormat(V.cash)}.`));
			p.append(div);
		} else {
			const div = document.createElement("div");
			div.append("This slave will cost ",
				App.UI.DOM.makeElement("span", cashFormat(cost), ["cash"]), ".",
				App.UI.DOM.makeElement("div", `You have ${cashFormat(V.cash)}.`));
			p.append(div);
		}
		return p;
	}
};