code:
App.Encyclopedia.addArticle("The X-Series Arcology", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	r.push(`The X-Series Arcology is the latest model of 'arcology', designed in 2037 based off the model of keeping Free Cities as independent from the old world as fiscally viable. The first arcology was designed in early 2016 by the Berlin-based architecture firm Franz-Crekcht as a "Skyscraper for the next generation" - the A-Series. The original A-Series was made to exist as an arcology integrated within old world nations, but capable of producing its own energy, food, and housing for the population contained within, marketed as buildings you could "Live, work, eat and shop at without ever having to leave".`);
	r.push(`These first arcology units were rolled out by the end of the year based on a standard design prototype, and cheap housing prices combined with the promise of stable white-collar work immediately attracted a sizeable population to the A-series arcologies. Based off of these designs, F-C continued to produce arcology schematics, including the K-series and B-series, and formulated plans for arcologies that could operate in rural, maritime, and even oceanic environments. When the popularity of the Arcology model became evident, other design firms replicated their own takes, producing designs like the V-series, D-series, F-series, and finally the ultra-modern X-series that you presently inhabit, designed more as a quasi-independent city-state than the original "next-generation skyscrapers".`);
	r.toParagraph();
	r.push("It wasn't long before arcologies began to form 'clusters' together, relying more on each other for trade and cultural exchange than the old world that surrounded them; increasingly complex infrastructure and surrounding networks began to connect the clusters, tying them together and making them stand apart from the rest of society, largely free from the purview of bureaucrats and old cultural mores. As the world outside the increasingly self-sufficient arcologies began to deteriorate, it wasn't long after that before the first arcologies declared independence from their parent nations, creating the original Free Cities we know today.");
	r.toParagraph();

	r.push("Choose a more particular entry below:");
	r.toNode("div");

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("What the Upgrades Do", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);
	/**
	 * @param {string} text
	 * @param {string} [article]
	 * @param {string} [className]
	 */
	const link = (text, article, className) => App.Encyclopedia.link(text, article, className);
	/**
	 * @param {string|HTMLSpanElement} text
	 * @param {string[]} [tag]
	 */
	const highlight = (text, tag = ["bold"]) => App.UI.DOM.makeElement("span", text, tag);

	App.Events.addNode(t, [`There are a lot of upgrades available for your arcology, ${properTitle()}. Please relax; some panic upon reviewing the options is normal. This list should familiarize you with your choices.`], "div", "note");

	t.append(App.UI.DOM.makeElement("h3", "Construction"));
	r.push("The first upgrade section on the arcology management menu offers an escalating series of generic upgrades for the arcology. A few of these have minor beneficial side effects, but all share the same main effect: they raise the arcology's maximum prosperity level. You will be informed on the end of week report if your arcology is nearing, at, or over this level. Upgrading early is", highlight("not", ["note"]), "useless, since prosperity will increase more rapidly if the cap is much higher than the current prosperity level.");
	r.toParagraph();

	t.append(App.UI.DOM.makeElement("h3", "Facilities"));
	r.push("These upgrades unlock the various facilities, which are detailed", link("here.", "Facilities Overview"));
	r.toParagraph();

	t.append(App.UI.DOM.makeElement("h3", "Penthouse Improvements"));
	r.push("The master suite and Head Girl suite options function like facilities. The master suite is the facility for the fucktoy assignment, and the Head Girl suite can house a single slave for her use.");
	r.toNode("div");

	App.Events.addNode(t, [highlight("Kitchen upgrade:", ["bold", "note"]), "this increases the chances of success for dieting and opens up additional dietary options."], "div", ["indent"]);
	App.Events.addNode(t, [highlight("Feeding phalli:", ["bold", "note"]), "unbroken slaves will find this disgusting, but it can cause beneficial oral fetishes to appear."], "div", ["indent"]);
	App.Events.addNode(t, [highlight("Drug fuckmachines:", ["bold", "note"]), "unbroken slaves will resent this, but it may cause beneficial anal fetishes to appear."], "div", ["indent"]);
	App.Events.addNode(t, [highlight("Personal armory:", ["bold", "note"]), "unlocks bodyguard options on the main menu."], "div", ["indent"]);
	App.Events.addNode(t, [highlight("Pharmaceutical Fabricator:", ["bold", "note"]), "requires a lot of", link("reputation", "Arcologies and Reputation"), "green", "to buy and use; unlocks powerful drug upgrades."], "div", ["indent"]);
	App.Events.addNode(t, [highlight("Surgery upgrade:", ["bold", "note"]), "enables several extreme surgical options like virginity restoration and hermaphrodite creation."], "div", ["indent"]);
	App.Events.addNode(t, [highlight("Media Hub:", ["bold", "note"]), "allows you to produce, encode, and stream videos of your slaves' daily lives to popular online pornography sites."], "div", ["indent"]);

	t.append(App.UI.DOM.makeElement("h3", "Special Upgrades"));
	r.push("Upgrades obtained during special events are listed here for reference. They cannot be purchased normally. Week:");
	r.toNode("div");
	App.Events.addNode(t, [highlight("24:"), "Arming yourself and or your", link("drones", "Security Drones"), "if installed."], "div", ["indent"]);
	App.Events.addNode(t, [highlight("62:"), "Establishing mercs."], "div", ["indent"]);
	App.Events.addNode(t, [highlight("65:"), "Giving your established mercs a unique title."], "div", ["indent"]);
	App.Events.addNode(t, [highlight("72 or later:"), "Establish the", link("Special Force"), "(if the mod is enabled)."], "div", ["indent"]);

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("Personal Assistant", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	App.Events.addNode(t, [`${properTitle()}, "I am your personal assistant."`], "p", "note");

	r.push("Though I am a highly advanced program, I am not a true AI. I am neither sentient nor self-aware. My chief usefulness lies in my computing power, which is sufficient to run the arcology and all of its systems, and to monitor its huge suite of internal sensors. If it happens here, I know about it, and if it's important, I'll tell you. Through me, you are effectively omniscient within your arcology. Omnipotence will have to wait until I implant your slaves with control chips to convert them into my mindless fuckpuppet soldiery.");
	r.toNode("p", ["note"]);

	App.Events.addNode(t, ["That was a joke."], "p", "note");

	r.push("I have been watching you and your slaves with close attention, so I have one other function to mention. I have learned a little of what constitutes successful slave training and husbandry. If you select the 'Rules Assistant' option on the main menu, I will review the rules your slaves are under and address any obvious problems. For example, I will place all disobedient slaves under restrictive rules.");
	r.toNode("p", ["note"]);

	r.push("I would say that it is a pleasure to serve, but you are no doubt", App.UI.DOM.makeElement("span", "intelligent", ["cyan"]), `enough to know that I cannot feel pleasure, making that a pointless pleasantry. Good day, ${properTitle()}.`);
	r.toNode("p", ["note"]);

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("The Wardrobe", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	r.push("The Wardrobe is a dressing area for all of your slaves, and a place where unused outfits are stored. Tailoring is available to make sure outfits fit each slave to your specifications. Here, players make choices about how slaves will appear in their dress, or lack of it. Choose outfits wisely to correspond to the culture that your citizens approve of, and your", App.Encyclopedia.link("reputation", "Arcologies and Reputation", "green"), "will increase more quickly. Items that stretch or constrict such as plugs or corsets will eventually have a lasting impact on slaves anatomy.");
	r.toNode("div", ["note"]);

	r.push("The wardrobe has two functions: First, individual slaves can be selected and then dressed in the wardrobe with anything from the basic clothing or shoes to the more exotic plugs and corsets. Secondly, players can access the wardrobe directly from the penthouse, and unlock collections of new outfits with ¤.");
	r.toParagraph();

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("The Auto Salon", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	r.push("Your auto salon is similar to the studio and the remote surgery, but is far less intimidating.");
	r.push("It is set up like a single seat from an old world beauty salon, except that a series of manipulators descend from the ceiling towards the chair.");
	r.push("It can perform any of the usual cosmetic services. The only special capability it has is to automatically color coordinate nails and makeup with a slave's hair.");
	r.push("It is fashionable to apply color schemes to slaves, and this function will make following the trend easy. Your salon will cost");
	r.push(App.UI.DOM.makeElement("span", `${cashFormat(V.modCost)}`, ["yellowgreen"]), "per use. These procedures are not especially invasive, and you can perform as many of them as you wish during a single week without fear for your slave's health.");
	r.toNode("div", ["note"]);

	r.push("The auto salon is mostly available for the player's experimentation. Some combinations of cosmetic options can have minor effects on some assignments and events, but these are very marginal. Slaves' appearances will differ in many scenes and events based on the player's cosmetic choices, but these details are for flavor only. As usual, gameplay effects are usually called out in explicit colored text; if they aren't, no major gameplay effects are happening.");
	r.toParagraph();

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("The Body Mod Studio", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	r.push("Your studio was sadly neglected by the previous owner of the arcology, but is in great shape and ready for use.");
	r.toNode("div", ["note"]);

	r.push("There are remote surgical and tattoo implements if you wish to hire an artist to do the work for you, but there are also sophisticated piercing and tattoo implements that can help you plan and apply the work yourself. Select a body part and a desired modification, and they'll do the rest.");
	r.toNode("p", ["note"]);

	r.push("Your equipment will cost", App.UI.DOM.makeElement("span", `${cashFormat(V.modCost)}`, ["yellowgreen"]), "per use. These procedures are not especially invasive, and you can perform as many of them as you wish during a single week without fear for your slave's health.");
	r.toNode("p", ["note"]);

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("The Remote Surgery", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	r.push("Congratulations on your purchase of a Caduceus model remote surgical unit. This is the very last word in slave surgical alteration.");
	r.toParagraph();

	r.push("To use your remote surgery, simply strap a slave to the operating table and purchase surgical services. Any doctor can take control of the remote surgery by telepresence and operate on your slave. You have all the options of a modern hospital, right in the comfort of your own home.");
	r.toParagraph();

	App.Encyclopedia.addArticleSource(r.container(), "Owner's Manual, Remote Surgical Unit model 'Caduceus'");

	r.push("It will cost", App.UI.DOM.makeElement("span", `${cashFormat(V.surgeryCost)}`, ["yellowgreen"]), "to purchase a doctor's telepresence and keep the equipment charged with the necessary materials. These procedures are invasive and will reduce a slave's health. Use drugs or rest to counteract this.");
	r.toNode("p", ["note"]);

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("The Pharmaceutical Fab", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	r.push("Pharmaceutical fabricators are the cutting edge of modern medicine. They are in short supply and are therefore ruinously expensive, but can greatly reduce the cost of maintaining a large stable of slaves by cutting drug overhead. They are also the only source of a new generation of advanced drugs that must be tailored to the individual patient's biochemistry.");
	r.toParagraph();

	App.Encyclopedia.addArticleSource(r.container(), "Pharmaceutical Review '32", "Dodgson, Jane Elizabeth");

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("Security Drones", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	r.push("As built, X-Series arcologies are equipped with basic security systems, and a complement of maintenance and cleaning drones.");
	r.toNode("div", ["note"]);

	r.push("The infrastructure that supports these drones is designed to be easily retrofittable to support new designs and classes of drones. One optional upgrade that may suit some owners particularly well is the addition of security drones. These are radically different than the slow, robust utility drones.");
	r.toNode("p", ["note"]);

	r.push("The X-Series security drone is undergoing a yearly upgrade cycle, but the entire family has some major features in common. All are light, multi-rotor flying drones with between three and six propulsors. Generally speaking, they favor speed and ease of handling over armor or endurance.");
	r.toNode("p", ["note"]);

	r.push("Their mobility is such that they can typically reach any point within the arcology in minutes. They are robust to damage but unarmored, and generally carry payloads of between two and five kilograms. Since they are designed to operate within or very close to the arcology, they have an autonomous endurance of less than an hour.");
	r.toNode("p", ["note"]);

	r.push("Standard kit includes hardened communications links to the arcology's computer systems, loudspeakers for public interface, and nonlethal compliance systems including tasers and chemical emitters.");
	r.toNode("p", ["note"]);

	App.Encyclopedia.addArticleSource(r.container(), "X-Series Arcology Owners' Manual");
	r.toParagraph();

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("Water Filtration", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	r.push(`X-Series arcologies are, in many ways, comparable to massive organisms. Under this metaphor, their circulatory systems include thousands of ${V.showInches === 2 ? 'miles' : 'kilometers'} of plumbing for water distribution and waste removal.`);
	r.toNode("div", ["note"]);

	r.push("The X-Series has established a new state of the art in arcology moisture reclamation. Air conditioning, hydrofarming, sewage treatment, and plumbing systems work together to recycle water with an efficiency greater than 99.99%. When combined with the arcology's skin, which is cleverly shaped to catch and retain almost all rain that falls on it, importing water will become a thing of the past.");
	r.toNode("p", ["note"]);

	r.push("This system is carefully balanced. It is designed to work for decades without major maintenance, but it is limited to a certain maximum population. Major upgrades will be necessary should the arcology population exceed this figure. Fortunately, the system is designed to be almost infinitely expandable.");
	r.toNode("p", ["note"]);

	r.push("Finally, the X-Series has been designed from the ground up by slaveowners, for slaveowners. The standard X-Series arcology is not equipped for dairy operations, but the water filtration system has been designed to support dairy upgrades. With minimal additions, X-Series owners will be able to Pasteurize and store a nearly unlimited quantity of nature's bounty.");
	r.toNode("p", ["note"]);

	App.Encyclopedia.addArticleSource(r.container(), "X-Series Arcology Owners' Manual");

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("Slave Nutrition", function() {
	const t = new DocumentFragment();
	const r = new SpacedTextAccumulator(t);

	r.push("Your X-Series arcology is designed to produce large quantities of high quality foodstuffs, from fresh vegetables to basic nutritive protein. The X-Series also produces the most advanced nutrition in the world. Our proprietary special slave nutrition system has been designed by slaveowners, for slaveowners, and truly makes the X-Series special.");
	r.toNode("div", ["note"]);

	r.push("This system produces a protein-rich drink that provides the physically active female body all its necessary nutrients. The artificial flavoring is specially designed to provide a palatable taste and texture while avoiding too much enjoyment and preventing excessive boredom. It supports a strong immune system and an energetic outlook.");
	r.toNode("p", ["note"]);

	r.push("It even causes a mild increase in sex drive. Our development team found that the slave body experiences a slight gain in libido merely from being so perfectly fed. It was simple and healthy to reinforce this effect with additives.");
	r.toNode("p", ["note"]);

	r.push("Our slave nutrition accomplishes all this while leaving the lower digestive tract extremely clean. We recommend your slaves undergo an occasional deep enema: under this regimen, slaveowners can be forgiven for forgetting that their slaves' anuses are involved in digestion at all. The designers of the X-Series trust that their clients can find other uses for them.");
	r.toNode("p", ["note"]);

	App.Encyclopedia.addArticleSource(r.container(), "X-Series Arcology Owners' Manual");

	return t;
}, "X-SeriesArcology");

App.Encyclopedia.addArticle("Media Hub", function() {
	const r = new SpacedTextAccumulator();

	r.push("Not everyone can be a Free Cities titan. But thanks to the power of the internet, everyone can watch!");
	r.toNode("div", ["note"]);

	App.Encyclopedia.addArticleSource(r.container(), `Porn Insider magazine, October 2035: "Free Cities Pornography: a new era of sex slave voyeurism"`);

	r.push("Constructing and upgrading the Media Hub allows you to produce and stream pornographic videos featuring your slaves for the world to see (and pay for).");
	r.toParagraph();

	r.push("Slaves featured in pornography will gain viewers and fame over time. Some may even become world-famous porn stars in their own right, with a little help.");
	r.toParagraph();

	r.push("Slaves with pretty faces will gain viewership significantly faster. Particularly ugly slaves may benefit from having their faces covered with a mask, or covered by a", App.Encyclopedia.link("fuckdoll", "Fuckdolls"), "suit.");
	r.push("Viewership can also be driven by investing cash in promotion and advertising, or by search engine optimization and feed hacking.");
	r.toParagraph();

	r.push("Slaves will produce porn in a variety of genres based on their characteristics. By default, her viewers will decide what she focuses on with their views and credits, but you can upgrade the media hub to allow you to choose a particular genre for a slave to focus on. Having too many slaves focusing on a particular genre will decrease the ability of that genre to draw new viewers. Genres associated with", App.Encyclopedia.link("paraphilias", "Paraphilias Overview"), "will have their viewership increase fastest, followed by", App.Encyclopedia.link("fetish", "Fetishes"), "genres. More general genres (such as those based on age, weight, and build) grow slowly, and genres associated with sexual", App.Encyclopedia.link("quirks", "Quirks"), "grow slowest of all, and are likely to require investment to promote.");
	r.toParagraph();

	r.push("If you stop producing porn of a particular slave, her viewership will decline over time, until she is once again unknown.");
	r.toParagraph();

	return r.container();
}, "X-SeriesArcology");

App.Encyclopedia.addCategory("X-SeriesArcology", function() {
	const r = [];
	if (V.encyclopedia !== "The X-Series Arcology") {
		r.push(App.Encyclopedia.link("The X-Series Arcology"));
	}
	r.push(App.Encyclopedia.link("What the Upgrades Do"));
	r.push(App.Encyclopedia.link("Personal Assistant"));
	r.push(App.Encyclopedia.link("The Wardrobe"));
	r.push(App.Encyclopedia.link("The Auto Salon"));
	r.push(App.Encyclopedia.link("The Body Mod Studio"));
	r.push(App.Encyclopedia.link("The Remote Surgery"));
	r.push(App.Encyclopedia.link("The Pharmaceutical Fab"));
	r.push(App.Encyclopedia.link("Security Drones"));
	r.push(App.Encyclopedia.link("Water Filtration"));
	r.push(App.Encyclopedia.link("Slave Nutrition"));
	return App.UI.DOM.generateLinksStrip(r);
});


code:
/** Render a link that toggles the slave's favorite status
 * @param {App.Entity.SlaveState} slave
 * @param {function():void} [handler]
 * @returns {HTMLAnchorElement}
 */
App.UI.favoriteToggle = function(slave, handler) {
	function favLink() {
		const linkID = `fav-link-${slave.ID}`;
		if (V.favorites.includes(slave.ID)) {
			const link = App.UI.DOM.link(String.fromCharCode(0xe800), () => {
				V.favorites.delete(slave.ID);
				$(`#${linkID}`).replaceWith(favLink());

				if (handler) {
					handler();
				}
			});
			link.classList.add("icons", "favorite");
			link.id = linkID;
			return link;
		} else {
			const link = App.UI.DOM.link(String.fromCharCode(0xe801), () => {
				V.favorites.push(slave.ID);
				$(`#${linkID}`).replaceWith(favLink());

				if (handler) {
					handler();
				}
			});
			link.classList.add("icons", "not-favorite");
			link.id = linkID;
			return link;
		}
	}

	return favLink();
};

repo_name: DontAskDontTell/fc

license: mit


code:
App.Intro.alphaDisclaimer = function() {
	App.Intro.init();
	const node = new DocumentFragment();

	App.UI.DOM.appendNewElement("h2", node, "Warning");

	App.UI.DOM.appendNewElement("p", node, `This is a text-based game that includes descriptions of sexual activity, graphic violence, drug use, and other subjects not suitable for persons under the age of 18. This is a work of fiction; any resemblance to actual persons, places, or events is unintended.`, "yellow");

	App.UI.DOM.appendNewElement("h2", node, "Welcome!");

	App.UI.DOM.appendNewElement("p", node, `An in-game encyclopedia is available from the sidebar, with answers to most basic gameplay questions.`);
	App.Events.addParagraph(node, [
		App.UI.DOM.makeElement("span", `Important info for new players, and a reminder for FC veterans:`, "bold"),
		`when the player is offered a set of choices, the`,
		App.UI.DOM.makeElement("span", `Continue`, "bold"),
		`button at the top of the left sidebar will almost always remain available. Using this to decline all the options presented is not cheating. If it's available, it's supposed to be available. It represents the player character politely bowing out of the situation.`
	]);

	App.UI.DOM.appendNewElement("h2", node, "Please note");
	const p = App.UI.DOM.appendNewElement("p", node);
	App.Events.addNode(p, [
		App.UI.DOM.makeElement("span", `This is an alpha.`, "bold"),
		`That means the game is missing content, is full of bugs, is imbalanced, and is generally in an incomplete state. The game will keep a start of turn autosave. If you encounter a bug, we strongly recommend you reload your start of turn autosave immediately. Please submit your feedback and bug reports here. Consider attaching a save file and screenshot of the problem.`,
	], "div");
	App.Events.addNode(p, [`Pregmod is a modification of the original <i>Free Cities</i> ${V.ver} created by FCdev, which can be seen <a href='https://freecitiesblog.blogspot.com/' target='_blank'>here</a>.`], "div");

	App.Events.addNode(p, [`mod version: ${V.pmodVer}, build: ${V.releaseID}${App.Version.commitHash ? `, commit: ${App.Version.commitHash}` : ``}`], "div", "note");
	App.Events.addNode(p, [`4.0.0 is an alpha release. This means the new player content has minimal implementation.`], "div", "bold"); /* remove me with 4.0.0! */

	App.Events.addResponses(node, [
		new App.Events.Result(`More version info`, versionInfo)
	]);

	App.UI.DOM.appendNewElement("p", node, App.UI.DOM.passageLink(
		"I am 18 or more years of age, I understand, and I wish to continue",
		"Economy Intro"
	));

	return node;

	function versionInfo() {
		const frag = new DocumentFragment();
		App.Events.addNode(frag, [
			App.UI.DOM.makeElement("span", "Mod: expanded age ranges and other tweaks 2016-08-30", ["green", "note"]),
			App.UI.DOM.makeElement("span", `+SV`, "darkred"),
		], "div");
		App.UI.DOM.appendNewElement("div", frag, "Mod: extra preg content and other crap", ["green", "note"]);
		App.UI.DOM.appendNewElement("div", frag, "Saves from versions prior to 0.6 are not compatible.", "bold");

		return frag;
	}
};


code:
/**
 * Provide a mechanism to inspect multiple slaves at once (for example, for Household Liquidators and recETS).
 * @param {Array<App.Entity.SlaveState>} slaves
 * @param {boolean} showFamilyTree
 * @param {FC.SlaveMarketName | FC.SpecialMarketName} [market]
 * @param {Map<number, string>} [marketText] map of Slave ID to text
 * @returns {DocumentFragment}
 */
App.UI.MultipleInspect = function(slaves, showFamilyTree, market, marketText) {
	const tabBar = new App.UI.Tabs.TabBar("MultipleInspect");

	for (const slave of slaves) {
		const text = marketText ? marketText.get(slave.ID) : null;
		tabBar.addTab(slave.slaveName, `slave${slave.ID}`, App.Desc.longSlave(slave, {market: market, marketText: text}));
	}

	if (slaves.length > 1 && showFamilyTree) {
		tabBar.addTab("Family Tree", "family-tree-tab", renderFamilyTree(slaves, slaves[0].ID));
	}

	return tabBar.render();
};


code:
App.UI.descriptionOptions = function() {
	const f = document.createDocumentFragment();
	App.UI.DOM.appendNewElement("h1", f, "Description Options");

	App.UI.DOM.appendNewElement("p", f,
		"These options will affect both the long form description of each slave and the miniscenes available from the main menu.",
		"scene-intro");

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

	options.addOption("", "surnameOrder")
		.addValue("Allow nationality name order", 0).customDescription("Order names ''based on country of origin''.")
		.addValue("Force name surname", 1).customDescription("Names will always be ''Name Surname''.")
		.addValue("Force surname name", 2).customDescription("Names will always be ''Surname Name''.");

	options.addOption("", "seeRace")
		.addValue("Disable most mentions of race", 0).off()
		.customDescription("Ethnicity will ''almost never'' be mentioned.")
		.addValue("Enable mentions of race", 1).on().customDescription("Ethnicity will ''occasionally'' be mentioned.");

	options.addOption("", "seeNationality")
		.addValue("Disable most mentions of nationality", 0).off()
		.customDescription("Nationality will ''almost never'' be mentioned.")
		.addValue("Enable mentions of nationality", 1).on()
		.customDescription("Nationality will ''occasionally'' be mentioned.");

	options.addOption("The effects of implants are", "showImplantEffects")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Clothing is mostly", "showClothing")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Detailed slave age information is", "showAgeDetail")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Approximate height is", "showHeightCMs")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Cosmetic body mods are", "showBodyMods")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Sexual histories are", "showSexualHistory")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Attractiveness and sexual scores are", "showScores")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Volume, in CCs, of breasts is", "showBoobCCs")
		.addValue("Shown", 1).on().addValue("Hidden", 0).off();

	options.addOption("Height and length units are in", "showInches")
		.addValueList([["Metric", 0], ["Both", 1], ["Imperial", 2]]);

	if (V.seeDicks > 0) {
		options.addOption("Approximate sizes of dicks and balls are", "showDickCMs")
			.addValue("Shown", 1).on().addValue("Hidden", 0).off();
	}

	options.addOption("Numbers are displayed as", "showNumbers")
		.addValueList([["Integers", 2], ["Both", 1], ["Words", 0]]);

	if (V.showNumbers === 1) {
		options.addOption(`Only numbers up to ${V.showNumbersMax} are displayed as words`, "showNumbersMax")
			.addValue("Default (20)", 20).showTextBox();
	}

	App.UI.DOM.appendNewElement("p", f, options.render());

	return f;
};


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()
			.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.
After enabling mid-game please run re-run BC Update.");

		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("Localized", 2).on().addValue("Force US locale", 1).on().addValue("Disabled", 0).off()
		.addComment("This will format numbers in some areas according to locale settings or force the US locale (decimal point, comma as thousands separator).");

	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("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');

	options.addOption("Default Rules Assistant mode is", "raDefaultMode")
		.addValue("Simple", 0).addValue("Advanced", 1);

	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 (01/04/23) the most actively developed. Real time 3D models. <span class="warning">(Android/MacOS not supported)</span>`);
				if (App.Art.webglErrorMessage.includes("Version mismatch") || App.Art.webglErrorMessage.includes("Could not find art assets")) {
					options.addComment(`<a href='https://mega.nz/folder/CgRiQaxS#Cjok57jXZwQt-jP7zkRh2Q' target='_blank'> Download the WebGL art assets</a>.`);
					if (App.Art.webglErrorMessage.includes("Could not find art assets")) {
						options.addComment(`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.`);
					}
				}

				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 GPUs.");

				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 GPUs.");
				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();
		}
	};
};


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;
};