code:
/**
 * UI for the Salon. Refreshes without refreshing the passage.
 * @param {App.Entity.SlaveState} slave
 * @param {boolean} [cheat=false] if true, will hide scenes and keep the player from being billed for changes.
 * @param {boolean} [startingGirls=false] change systems for starting girls
 */
App.UI.salon = function(slave, cheat = false, startingGirls = false) {
	const container = document.createElement("span");
	container.id = "salon";
	const {
		He, His,
		he, his, him
	} = getPronouns(slave);

	container.append(createPage());
	return container;

	function createPage() {
		const el = new DocumentFragment();
		if (!cheat) {
			App.Events.drawEventArt(el, slave);
			el.append(intro());
		}
		if (!startingGirls) {
			// Starting girls has its own, more powerful, eye modifier which interferes with this one.
			// TODO: unify them
			el.append(eyewear());
		}
		if (
			(["leopard", "tiger", "jaguar"].includes(slave.earT) && slave.earTColor !== "hairless")
			|| ["leopard", "tiger", "jaguar", "gazelle", "tanuki", "raccoon"].includes(slave.tailShape)
			|| slave.wingsShape === "moth"
		) {
			el.append(pattern());
		}
		el.append(ears());
		if (slave.horn !== "none") {
			el.append(horns());
		}
		el.append(hair());
		el.append(makeup());
		el.append(nails());
		el.append(skin());
		el.append(bodyHair());
		if (slave.tail !== "none") {
			el.append(tail());
		}
		if (slave.appendages !== "none") {
			el.append(appendages());
		}
		return el;
	}

	function intro() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h1", el, "The Auto Salon");
		App.UI.DOM.appendNewElement("div", el, `${SlaveFullName(slave)} is seated in the auto salon. ${He} is awaiting your artistic pleasure.`, "scene-intro");
		return el;
	}

	function eyewear() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Eyewear");
		const r = [];
		const options = new App.UI.OptionsGroup();

		if (getBestVision(slave) === 0) {
			r.push(`${He} is blind`);
		} else if (anyVisionEquals(slave, 1)) {
			r.push(`${He} is nearsighted`);
		} else {
			r.push(`${His} vision is normal`);
		}

		const option = options.addOption(r.join(" "), "eyewear", slave)
			.addValue("None", "none")
			.addValue("Cosmetic glasses", "glasses", billMod);
		if (getBestVision(slave) !== 0 && anyVisionEquals(slave, 1)) {
			option.addValue("Corrective glasses", "corrective glasses", billMod);
			if (hasAnyEyes(slave)) {
				option.addValue("Corrective contacts", "corrective contacts", billMod);
			}
		} else {
			option.addValue("Blurring glasses", "blurring glasses", billMod);
			if (hasAnyEyes(slave)) {
				option.addValue("Blurring contacts", "blurring contacts", billMod);
			}
			if (getBestVision(slave) !== 0) {
				option.addComment("Blurring options are annoying and impede performance on some assignments.");
			}
		}
		el.append(options.render());

		el.append(App.Medicine.Modification.eyeSelector(slave, cheat));
		return el;
	}

	function pattern() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Pattern");

		el.append(App.Medicine.Salon.pattern(slave, cheat));

		return el;
	}

	function ears() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Ears");
		const r = [];
		const options = new App.UI.OptionsGroup();

		if (slave.earImplant === 1) {
			r.push(`${He} has artificial inner ear implants`);
		} else if (slave.hears < -1) {
			r.push(`${He} is deaf`);
		} else if (slave.hears > -1) {
			r.push(`${His} hearing is normal`);
		} else {
			r.push(`${He} is hearing impaired`);
		}
		const option = options.addOption(r.join(" "), "earwear", slave)
			.addValue("None", "none");
		// Hard of hearing
		if (slave.hears === -1 && slave.earImplant !== 1) {
			option.addValue("Hearing aids", "hearing aids", billMod);
		}
		// Is not deaf
		if (slave.hears > -2 || slave.earImplant === 1) {
			option.addValue("Muffling ear plugs", "muffling ear plugs", billMod);
			option.addValue("Deafening ear plugs", "deafening ear plugs", billMod);
		}

		// Top ear Color
		if (slave.earT !== "none" && slave.earTColor !== "hairless") {
			let title;
			let option;
			let showChoices = true;
			const filtered = App.Medicine.Modification.Color.Primary.filter(c => ![slave.earTColor].includes(c.value));

			title = `${His} ears are ${slave.earTColor}${slave.earTEffect === "none" ? `.` : ` and have ${slave.earTEffect}.`}`;
			App.UI.DOM.appendNewElement("div", el, title);

			if (showChoices) {
				if (cheat) {
					if (slave.tail !== "none" && (slave.earTColor !== slave.tailColor || slave.earTEffect !== slave.tailEffect)) {
						options.addCustomOption("")
							.addButton("Match tail", () => {
								slave.earTColor = slave.tailColor;
								slave.earTEffect = slave.tailEffect;
								App.UI.reload();
							});
					} else if (slave.earTColor !== slave.hColor) {
						options.addCustomOption("")
							.addButton("Match current hair", () =>{
								slave.earTColor = slave.hColor;
								App.UI.reload();
							});
					}
				}

				if (slave.earTEffect !== "none" || slave.earTEffectColor !== "none") {
					options.addCustomOption("")
						.addButton("Remove effect", () => {
							slave.earTEffect = "none";
							slave.earTEffectColor = "none";
							App.UI.reload();
						});
				}

				option = options.addOption("Fur Color", "earTColor", slave);
				for (const color of App.Medicine.Modification.Color.Primary) {
					option.addValue(capFirstChar(color.value), color.value, billMod);
				}
				option.pulldown();

				option = options.addOption("Effect color", "earTEffectColor", slave);
				for (const color of filtered) {
					option.addValue(capFirstChar(color.value), color.value, billMod);
				}
				option.pulldown();

				option = options.addOption("Effect", "earTEffect", slave);
				for (const color of App.Medicine.Modification.Color.Effect) {
					option.addValue(capFirstChar(color.value), `${slave.earTEffectColor} ${color.value}`, billMod);
				}
				option.pulldown();
			}
		}
		el.append(options.render());

		return el;
	}

	function horns() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Horns");
		const options = new App.UI.OptionsGroup();

		const option = options.addOption(`Set the color of ${his} ${slave.horn}`, "hornColor", slave);
		for (const hornColor of App.Medicine.Modification.hornColor) {
			option.addValue(capFirstChar(hornColor), hornColor, billMod);
		}

		el.append(options.render());

		return el;
	}

	function makeup() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Makeup");
		const options = new App.UI.OptionsGroup();

		options.addOption(App.Desc.makeup(slave), "makeup", slave)
			.addValue("Makeup free", 0)
			.addValue("Nice", 1, billMod)
			.addValue("Gorgeous", 2, billMod)
			.addValue("Slutty", 4, billMod)
			.addValue("Color-coordinate with hair", 3, billMod);

		options.addOption("", "makeup", slave)
			.addValue("Neon", 5, billMod)
			.addValue("Neon, color-coordinate with hair", 6, billMod);

		options.addOption("", "makeup", slave)
			.addValue("Metallic", 7, billMod)
			.addValue("Metallic, color-coordinate with hair", 8, billMod);

		el.append(options.render());

		return el;
	}

	function nails() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Nails");
		const options = new App.UI.OptionsGroup();

		options.addOption(App.Desc.nails(slave), "nails", slave)
			.addValue("Neatly clipped", 0, billMod)
			.addValue("Long and elegant", 1, billMod)
			.addValue("Sharp and claw-like", 3, billMod)
			.addValue("Bright and glittery", 4, billMod)
			.addValue("Very long and garish", 5, billMod)
			.addValue("Color-coordinate with hair", 2, billMod);

		options.addOption("", "nails", slave)
			.addValue("Neon", 6, billMod)
			.addValue("Neon, color-coordinate with hair", 7, billMod);

		options.addOption("", "nails", slave)
			.addValue("Metallic", 8, billMod)
			.addValue("Metallic, color-coordinate with hair", 9, billMod);

		el.append(options.render());

		return el;
	}

	function hair() {
		const el = new DocumentFragment();
		let option;
		App.UI.DOM.appendNewElement("h3", el, "Hair");
		const options = new App.UI.OptionsGroup();
		let title;
		let showChoices = true;
		const hasWig = (slave.bald === 1 && slave.hStyle !== "bald");
		const filtered = App.Medicine.Modification.Color.Primary.filter(c => ![slave.hColor].includes(c.value));

		if (slave.bald === 1) {
			if (slave.hStyle === "bald") {
				title = `${He} is completely bald.`;
				showChoices = false;
			} else {
				title = `${His} wig is ${slave.hColor}${slave.hEffect === "none" ? `` : ` with ${slave.hEffect}`}.`;
			}
		} else {
			title = `${His} hair is ${slave.hColor}${slave.hEffect === "none" ? `` : ` with ${slave.hEffect}`}.`;
		}
		App.UI.DOM.appendNewElement("div", el, title);

		if (slave.bald === 1) {
			options.addOption(`Use a wig`, "hStyle", slave)
				.addValue("Enable", "neat").on()
				.addValue("Disable", "bald").off();
		}

		if (showChoices || cheat) {
			if (slave.hLength > 1) {
				// Original color
				if (cheat) {
					option = options.addOption("Natural color", "origHColor", slave)
						.showTextBox();
					for (const color of App.Medicine.Modification.Color.Primary) {
						option.addValue(capFirstChar(color.value), color.value, billMod);
					}
					option.pulldown();

					options.addCustomOption("")
						.addButton("Sync body hair color", () => {
							slave.eyebrowHColor = slave.origHColor;
							slave.pubicHColor = slave.origHColor;
							slave.underArmHColor = slave.origHColor;
							App.UI.reload();
						});
				}
				// Color
				if (slave.hEffectColor !== "none" || slave.hEffect !== "none") {
					options.addCustomOption("")
						.addButton("Remove effects", () => {
							slave.hEffectColor = "none";
							slave.hEffect = "none";
							App.UI.reload();
						});
				}
				option = options.addOption("Primary color", "hColor", slave);
				if (slave.origHColor !== slave.hColor) {
					option.addValue("Restore natural color", slave.origHColor, billMod);
				}
				for (const color of App.Medicine.Modification.Color.Primary) {
					option.addValue(capFirstChar(color.value), color.value, billMod);
				}
				option.pulldown();

				option = options.addOption("Effect color", "hEffectColor", slave);
				for (const color of filtered) {
					option.addValue(capFirstChar(color.value), color.value, billMod);
				}
				option.pulldown();

				option = options.addOption("Effect", "hEffect", slave);
				for (const color of App.Medicine.Modification.Color.Effect) {
					option.addValue(capFirstChar(color.value), `${slave.hEffectColor} ${color.value}`, billMod);
				}
				option.pulldown();
			}
			// Style
			if (slave.hLength > 1) {
				title = `Style ${hasWig ? "wig" : "hair"} `;
			} else {
				title = `${His} ${hasWig ? "wig" : "hair"} is too short to style meaningfully`;
			}
			option = options.addOption(title, "hStyle", slave);
			if (slave.hLength > 1) {
				for (const style of App.Medicine.Modification.hairStyles.Normal) {
					option.addValue(style.title, style.value, billMod);
				}
				option.pulldown();
			}

			// Style + Cut
			if (slave.hLength > 1) {
				option = options.addOption(`${hasWig ? "Change wig style and length" : "Cut and style hair"}`, "hStyle", slave);
				if (slave.hLength > 1) {
					for (const style of App.Medicine.Modification.hairStyles.Cut) {
						option.addValue(
							style.title,
							style.value,
							() => {
								slave.hLength = style.hLength;
								billMod();
							});
					}
				}
			}

			// Length
			option = options.addOption(`${hasWig ? "Find longer or shorter wig" : "Cut or lengthen hair"}`, "hLength", slave);
			if (slave.hLength > 0) {
				for (const style of App.Medicine.Modification.hLength) {
					if (
						(style.hasOwnProperty("requirements") && !style.requirements(slave)) ||
						(style.hLength && style.hLength > slave.hLength)
					) {
						continue;
					}
					option.addValue(style.title, style.hLength, billMod);
				}
				if (!slave.bald && slave.hLength < 150) {
					option.addValue("Apply extensions", slave.hLength + 10, billMod);
				}
			} else {
				option.addValue("Apply hair growth stimulating treatment", 1);
			}

			option.showTextBox();

			// Maintain
			if (!hasWig) {
				options.addOption(`Maintain this length`, "haircuts", slave)
					.addValue("Enable", 1).on()
					.addValue("Disable", 0).off();
			}
		}

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

	function skin() {
		const el = new DocumentFragment();
		let option;
		App.UI.DOM.appendNewElement("h3", el, "Skin");
		const options = new App.UI.OptionsGroup();
		let comment = [];

		if (cheat) {
			option = options.addOption(`${His} natural skin color is`, "origSkin", slave).showTextBox().pulldown();
			const naturalSkins = slave.race === "catgirl" ? App.Medicine.Modification.catgirlNaturalSkins : App.Medicine.Modification.naturalSkins;
			for (const skin of naturalSkins) {
				option.addValue(capFirstChar(skin), skin, () => slave.skin = slave.origSkin);
			}
		}

		option = options.addOption(`${His} skin is ${slave.skin}.`, "skin", slave);
		if (App.Medicine.Modification.dyedSkins.includes(slave.skin)) {
			option.addValue("Remove coloring", slave.origSkin, billMod);
		} else if ((slave.skin === "sun tanned") || (slave.skin === "spray tanned")) {
			option.addValue("Remove tanning", slave.origSkin, billMod);
		}

		if (!App.Medicine.Modification.dyedSkins.includes(slave.skin)) {
			if (slave.skin === "sun tanned" || slave.skin === "spray tanned") {
				comment.push(`${His} skin tanning must be removed before any advanced procedure to change ${his} skin color.`);
			} else {
				if (skinToneLevel(slave.skin) > 1) {
					option.addValue("Bleach", changeSkinTone(slave.skin, -2), billMod);
				}
				if (skinToneLevel(slave.skin) > 8) {
					option.addValue("Lighten", changeSkinTone(slave.skin, -1), billMod);
				}
				if (skinToneLevel(slave.skin) < 18) {
					option.addValue("Darken", changeSkinTone(slave.skin, 1), billMod);
				}
				if (skinToneLevel(slave.skin) < 25) {
					option.addValue("Blacken", changeSkinTone(slave.skin, 2), billMod);
				}
			}
			if (slave.skin !== "sun tanned") {
				if (skinToneLevel(slave.skin) < 6) {
					comment.push(`${His} skin is so light in color that any attempt at natural tanning is more likely to damage ${his} skin.`);
				} else if ((skinToneLevel(slave.skin) > 20)) {
					comment.push(`${His} skin is so dark in color that any attempt at natural tanning is not likely to appear on ${his} skin.`);
				} else {
					option.addValue("Sun tan", "sun tanned", billMod);
				}
			}
			if (slave.skin !== "spray tanned") {
				option.addValue("Spray tan", "sun tanned", billMod);
			}
			option.addComment(comment.join(" "));
		}

		option = options.addOption(`Dye or paint`, "skin", slave).showTextBox();
		for (const dye of App.Medicine.Modification.dyedSkins) {
			option.addValue(capFirstChar(dye), dye, billMod);
		}

		if (cheat) {
			options.addOption(`${His} skin's marks are`, "markings", slave)
				.addValueList([
					["None", "none"],
					["Freckles", "freckles"],
					["Heavily freckled", "heavily freckled"],
					["Beauty mark", "beauty mark"],
					["Birthmark", "birthmark"],
				]);
		}
		option.pulldown();

		if (slave.markings === "beauty mark") {
			option = options.addOption(`${He} has a prominent mole on ${his} face`, "markings", slave)
				.addValue("Remove it", "none", billMod);
			if (slave.face > 40) {
				option.addComment(`The mole qualifies as a beauty mark and enhances ${his} attractiveness due to ${his} facial beauty.`);
			} else if (slave.face < -10) {
				option.addComment(`The mole makes ${him} even less attractive.`);
			} else {
				option.addComment(`The mole qualifies as a beauty mark since ${he}'s pretty, having no significant impact on ${his} beauty.`);
			}
		}
		if (slave.markings === "birthmark") {
			option = options.addOption(`${He} has a large birthmark`, "markings", slave)
				.addValue("Bleach it", "none", billMod);
			if (slave.prestige > 0 || slave.porn.prestige > 1) {
				option.addComment(`The birthmark enhances ${his} attractiveness due to ${his} prestige.`);
			} else {
				option.addComment(`The birthmark detracts from ${his} attractiveness.`);
			}
		}

		el.append(options.render());

		return el;
	}

	function bodyHair() {
		const el = new DocumentFragment();
		const options = new App.UI.OptionsGroup();
		let option;
		let r = [];
		App.UI.DOM.appendNewElement("h3", el, "Body hair");

		// Eyebrows
		if (slave.eyebrowHStyle !== "bald" || cheat) {
			// Describe and change color
			r.push(`${His} eyebrows`);
			if (slave.eyebrowHStyle === "shaved" || slave.eyebrowHStyle === "bald") {
				r.push(`would be ${slave.eyebrowHColor} if present.`);
			} else {
				r.push(`are ${slave.eyebrowHColor}.`);
			}

			option = options.addOption(r.join(" "), "eyebrowHColor", slave);
			if (slave.eyebrowHColor !== slave.hColor) {
				option.addValue("Match the hair", slave.hColor);
			}
			option.addValueList(makeAList(App.Medicine.Modification.Color.Primary.map(color => color.value)));
			option.addCallbackToEach(billMod);
			option.pulldown();

			// Style
			option = options.addOption(`Style ${his} eyebrow hair`, "eyebrowHStyle", slave).showTextBox();
			for (const fullness of App.Medicine.Modification.eyebrowStyles) {
				option.addValue(capFirstChar(fullness), fullness, billMod);
			}
			if (cheat) {
				option.addValue("Bald", "bald");
			}
			option.pulldown();

			// Fullness
			option = options.addOption(`Fullness of ${his} eyebrow hair`, "eyebrowFullness", slave);
			for (const fullness of App.Medicine.Modification.eyebrowFullness) {
				option.addValue(capFirstChar(fullness), fullness, billMod);
			}
			option.pulldown();
		} else {
			options.addComment(`${His} eyebrows are completely hairless.`);
		}

		// Pubic hair
		const pubertyAge = Math.min(slave.pubertyAgeXX, slave.pubertyAgeXY);
		r = [];
		const hasPubes = (slave.pubicHStyle !== "bald" && slave.pubicHStyle !== "hairless" && slave.physicalAge >= pubertyAge - 1);
		if (hasPubes) {
			r.push(`${His}`);
			if (slave.physicalAge < pubertyAge) {
				r.push(`wispy pubic hair, which is just starting to grow in,`);
			} else if (slave.pubicHStyle === "in a strip") {
				r.push(`pubic hair, which is shaved into a strip,`);
			} else {
				r.push(`${slave.pubicHStyle} pubic hair`);
			}
			if (slave.pubicHStyle === "waxed") {
				r.push(`would be ${slave.pubicHColor} if present.`);
			} else {
				r.push(`is ${slave.pubicHColor}.`);
			}
		} else {
			r.push(`${His} groin is completely hairless.`);
		}
		option = options.addOption(r.join(" "), "pubicHColor", slave);
		if (hasPubes) {
			if (slave.pubicHColor !== slave.hColor) {
				option.addValue("Match the curtains", slave.hColor);
			}
			option.addValueList(makeAList(App.Medicine.Modification.Color.Primary.map(color => color.value)))
				.addCallbackToEach(billMod)
				.pulldown();
		}
		if (hasPubes || cheat) {
			// Style
			option = options.addOption(`Style ${his} pubic hair`, "pubicHStyle", slave);
			for (const fullness of App.Medicine.Modification.pubicStyles) {
				option.addValue(capFirstChar(fullness), fullness, billMod);
			}
			if (cheat) {
				option.addValue("Bald", "bald");
			}
			option.pulldown();
		}

		// Armpit hair
		r = [];
		const hasPitHair = (slave.underArmHStyle !== "bald" && slave.underArmHStyle !== "hairless" && slave.physicalAge >= pubertyAge - 1);
		if (hasPitHair) {
			r.push(`${His}`);
			if (slave.physicalAge < pubertyAge) {
				r.push(`wispy underarm hair`);
			} else {
				r.push(`${slave.underArmHStyle} underarm hair`);
			}
			if (slave.underArmHStyle === "waxed") {
				r.push(`would be ${slave.underArmHColor} if present.`);
			} else {
				r.push(`is ${slave.underArmHColor}.`);
			}
		} else {
			r.push(`${His} underarms are completely hairless.`);
		}
		option = options.addOption(r.join(" "), "underArmHColor", slave);
		if (hasPitHair) {
			if (slave.underArmHColor !== slave.hColor) {
				option.addValue("Match the hair", slave.hColor);
			}
			option.addValueList(makeAList(App.Medicine.Modification.Color.Primary.map(color => color.value)))
				.addCallbackToEach(billMod)
				.pulldown();
		}
		if (hasPitHair || cheat) {
			// Style
			option = options.addOption(`Style ${his} armpit hair`, "underArmHStyle", slave);
			for (const fullness of App.Medicine.Modification.armpitStyles) {
				option.addValue(capFirstChar(fullness), fullness, billMod);
			}
			if (cheat) {
				option.addValue("Bald", "bald");
			}
			option.pulldown();
		}

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

	function tail() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Tail");
		const options = new App.UI.OptionsGroup();
		let title;
		let option;
		let showChoices = true;
		const filtered = App.Medicine.Modification.Color.Primary.filter(c => ![slave.tailColor].includes(c.value));

		title = `${He} has a tail that is ${slave.tailColor}${slave.tailEffect === "none" ? `` : ` and has ${slave.tailEffect}`}.`;
		App.UI.DOM.appendNewElement("div", el, title);

		if (showChoices) {
			if (cheat) {
				if (slave.earT !== "none" && (slave.earTColor !== slave.tailColor || slave.earTEffect !== slave.tailEffect)) {
					options.addCustomOption("")
						.addButton("Match ears", () => {
							slave.tailColor = slave.earTColor;
							slave.tailEffect = slave.earTEffect;
							App.UI.reload();
						});
				} else if (slave.tailColor !== slave.hColor) {
					options.addCustomOption("")
						.addButton("Match current hair", () => {
							slave.tailColor = slave.hColor;
							slave.tailEffect = slave.hEffect;
							App.UI.reload();
						});
				}
			}

			if (slave.tailEffect !== "none" || slave.tailEffectColor !== "none") {
				options.addCustomOption("")
					.addButton("Remove effect", () => {
						slave.tailEffect = "none";
						slave.tailEffectColor = "none";
						App.UI.reload();
					});
			}

			option = options.addOption("Fur Color", "tailColor", slave);
			for (const color of App.Medicine.Modification.Color.Primary) {
				option.addValue(capFirstChar(color.value), color.value, billMod);
			}
			option.pulldown();

			option = options.addOption("Effect color", "tailEffectColor", slave);
			for (const color of filtered) {
				option.addValue(capFirstChar(color.value),  color.value, billMod);
			}
			option.pulldown();

			option = options.addOption("Effect", "tailEffect", slave);
			for (const color of App.Medicine.Modification.Color.Effect) {
				option.addValue(capFirstChar(color.value), `${slave.tailEffectColor} ${color.value}`, billMod);
			}
			option.pulldown();
		}
		el.append(options.render());

		return el;
	}

	function appendages() {
		const el = new DocumentFragment();
		App.UI.DOM.appendNewElement("h3", el, "Appendages");
		const options = new App.UI.OptionsGroup();
		let title;
		let option;
		let showChoices = true;
		const filtered = App.Medicine.Modification.Color.Primary.filter(c => ![slave.appendagesColor].includes(c.value));

		title = `${He} has ${slave.appendagesColor} appendages${slave.appendagesEffect === "none" ? `` : ` that have ${slave.appendagesEffect}`}.`;
		App.UI.DOM.appendNewElement("div", el, title);

		if (showChoices) {
			if (slave.appendagesEffect !== "none" || slave.appendagesEffectColor !== "none") {
				options.addCustomOption("")
					.addButton("Remove effect", () => {
						slave.appendagesEffect = "none";
						slave.appendagesEffectColor = "none";
						App.UI.reload();
					});
			}

			option = options.addOption("Fur Color", "appendagesColor", slave);
			for (const color of App.Medicine.Modification.Color.Primary) {
				option.addValue(capFirstChar(color.value), color.value, billMod);
			}
			option.pulldown();

			option = options.addOption("Effect color", "appendagesEffectColor", slave);
			for (const color of filtered) {
				option.addValue(capFirstChar(color.value), color.value, billMod);
			}
			option.pulldown();

			option = options.addOption("Effect", "appendagesEffect", slave);
			for (const color of App.Medicine.Modification.Color.Effect) {
				option.addValue(capFirstChar(color.value), `${slave.appendagesEffectColor} ${color.value}`, billMod);
			}
			option.pulldown();
		}
		el.append(options.render());

		return el;
	}

	function billMod() {
		if (!cheat) {
			cashX(forceNeg(V.modCost), "slaveMod", slave);
		}
	}

	function makeAList(iterable) {
		return Array.from(iterable, (k => [capFirstChar(k), k]));
	}
};


code:
App.Facilities.SchoolTeacherSelect = function() {
	const f = document.createDocumentFragment();

	if (S.Schoolteacher) {
		f.append(`${SlaveFullName(S.Schoolteacher)} is working as your Schoolteacher, helping to teach your slaves. `);
		f.append(App.UI.DOM.link("Remove Schoolteacher", () => {
			removeJob(S.Schoolteacher, Job.TEACHER);
		},
		[], "Schoolroom"
		));
	} else {
		f.append(`No Schoolteacher assigned, appoint one from your devoted slaves.`);
	}

	f.append(App.UI.SlaveList.facilityManagerSelection(App.Entity.facilities.schoolroom, "Schoolroom"));
	return f;
};

repo_name: Fekenol/fc

license: mit


code:
App.Facilities.Schoolroom.schoolroom = class Schoolroom extends App.Facilities.Facility {
	constructor() {
		const schoolroom = App.Entity.facilities.schoolroom;
		const decommissionHandler = () => {
			V.schoolroom = 0;
			V.schoolroomDecoration = "standard";
			V.schoolroomUpgradeSkills = 0;
			V.schoolroomUpgradeLanguage = 0;
			V.schoolroomUpgradeRemedial = 0;
			V.schoolroomRemodelBimbo = 0;
		};

		super(
			schoolroom,
			decommissionHandler,
		);

		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.encyclopedia = "Schoolroom";
	}

	/** @returns {string} */
	get intro() {
		const text = [];

		text.push(`${this.facility.nameCaps} is well-equipped, with wallscreens to display lessons. These are currently`, this.decorations);

		if (this.facility.hostedSlaves > 2) {
			text.push(`${this.facility.nameCaps} is busy with slaves, repeating their lessons out loud to drill the instruction home. A few students are maintaining uncomfortable positions in the corner as punishment for poor work.`);
		} else if (this.facility.hostedSlaves > 0) {
			text.push(`${this.facility.nameCaps} is sparsely populated, the few students repeating their lessons out loud to drill the instruction home. One slave is maintaining an uncomfortable position in the corner as punishment for poor work.`);
		} else if (S.Schoolteacher) {
			text.push(`${S.Schoolteacher.slaveName} is alone in ${V.schoolroomName}, and has nothing to do but work on $his lesson plans.`);
		} else {
			text.push(`None of your slaves are learning in ${V.schoolroomName}.`);
		}

		return text.join(' ');
	}

	/** @returns {string} */
	get decorations() {
		/** @type {FC.Facilities.Decoration} */
		const FS = {
			"Roman Revivalist": `showing the story of a famous Roman slave who sacrificed her life for the life of her ${properMaster()}.`,
			"Neo-Imperialist": `showing the work of many serfs building a great monument to their Imperial ${properMaster()}, and emphasizing the beauty of the society they built together.`,
			"Aztec Revivalist": `showing the reenactment of a legendary story of a slave who ascended by offering her blood to the gods, and was granted eternal life.`,
			"Egyptian Revivalist": `showing an interpretation of Egyptian history that attributes many great monuments to the enlightened use slave labor.`,
			"Edo Revivalist": `showing an interpretation of Japanese cultural history that emphasizes a serf's duty to her social superiors.`,
			"Arabian Revivalist": `showing an interpretation of Arabian cultural history that focuses on thriving slave markets and vibrant harems.`,
			"Chinese Revivalist": `showing an interpretation of Chinese cultural history that focuses on concubinage, palace etiquette, and social order.`,
			"Chattel Religionist": `displaying a passage from the holy book that supports slavery.`,
			"Degradationist": `displaying a rote recitation of a slave's proper acceptance of her subhuman status.`,
			"Repopulationist": `reviewing a number of sexual positions to accommodate a heavily pregnant girl.`,
			"Eugenics": `reviewing ways to better your owner's standing intermixed with exaggerated pregnancy horror stories focused on slave pregnancy.`,
			"Asset Expansionist": `reviewing techniques that allow two slaves with huge breasts to inspect and moisturize each other's hard to reach areas.`,
			"Transformation Fetishist": `offering a brief primer on surgical recovery, with practical techniques to make it quicker.`,
			"Gender Radicalist": `going over how to keep one's asspussy ready for intercourse at any time, including how to schedule regular enemata and pre-lubrication.`,
			"Gender Fundamentalist": `going over the trifecta that is the standard approach of sex slaves: a blowjob, followed by vaginal, finished with anal.`,
			"Physical Idealist": `offering a brief primer on the arcology's nutritional system which will allow slaves to double check their own protein intake.`,
			"Supremacist": `reviewing the scientific evidence for ${V.arcologies[0].FSSupremacistRace} superiority.`,
			"Subjugationist": `reviewing the scientific evidence for ${V.arcologies[0].FSSubjugationistRace} inferiority.`,
			"Paternalist": `reviewing a lesson on time-management skills, and the students are taking notes on their own tablets.`,
			"Pastoralist": `reviewing how to help fellow slaves with huge, lactating breasts.`,
			"Maturity Preferentialist": `going over daily sets of exercises designed to keep mature slaves' holes as tight as possible.`,
			"Youth Preferentialist": `reviewing social cues that young slaves who have been enslaved through their entire adulthood might not understand.`,
			"Body Purist": `offering a brief primer on the arcology's nutritional system which will allow slaves to double check their own caloric intake.`,
			"Slimness Enthusiast": `offering a brief primer on the arcology's nutritional system which will allow slaves to double check their own caloric intake.`,
			"Hedonistic": `exposing slaves to new, fascinating forms of pleasure; both for others and for themselves.`,
			"Intellectual Dependency": `reviewing various sexual positions, and the students are practicing with each other.`,
			"Slave Professionalism": `reviewing a complex lesson on social cues and when to act on them to better shift things in ${properTitle()}'s favor.`,
			"Petite Admiration": `exploring methods to pleasure a partner far taller than oneself.`,
			"Statuesque Glorification": `reviewing the reasons why tall individuals are widely considered more attractive.`,
			"standard": `reviewing the often complex subject of how to address citizens other that one's owner.`,
		};

		const res = FS[V.schoolroomDecoration];

		if (!res) {
			throw new Error(`Unknown V.schoolroomDecoration value of '${V.schoolroomDecoration}' found in decorations().`);
		}

		return res;
	}

	/** @returns {FC.Facilities.Expand} */
	get expand() {
		return {
			desc: `${this.facility.nameCaps} has room to house ${numberWithPluralOne(V.schoolroom, "slave")} while they learn. There ${this.facility.hostedSlaves === 1 ? `is ${num(this.facility.hostedSlaves)} slave` : `are ${num(this.facility.hostedSlaves)} slaves`} learning in ${V.schoolroomName}.`,
			removeSlave: "take classes",
		};
	}

	/** @returns {FC.IUpgrade[]} */
	get upgrades() {
		return [
			{
				property: "schoolroomUpgradeSkills",
				tiers: [
					{
						value: 0,
						upgraded: 1,
						text: `${this.facility.nameCaps} inculcates the basic skills necessary to a sex slave.`,
						link: `Upgrade the curriculum to cover some intermediate skills`,
						cost: 10000 * V.upgradeMultiplierArcology,
						notes: [`increases the effectiveness of ${V.schoolroomName}`],
					},
					{
						value: 1,
						text: `${this.facility.nameCaps} provides slaves with some intermediate skills, including a solid foundation in sex, efficient and safe prostitution, and the rudiments of courtesanship.`,
					},
				],
			},
			{
				property: "schoolroomUpgradeLanguage",
				tiers: [
					{
						value: 0,
						upgraded: 1,
						text: `${this.facility.nameCaps} includes only basic language classes in its curriculum.`,
						link: `Install advanced linguistic interfaces to efficiently teach the arcology's lingua franca`,
						cost: 5000 * V.upgradeMultiplierArcology * V.HackingSkillMultiplier,
						notes: [`increases the effectiveness of ${V.schoolroomName}`],
					},
					{
						value: 1,
						text: `${this.facility.nameCaps} boasts state of the art linguistic interfaces that allow it to teach the basics of the arcology's lingua franca with increased success.`,
					},
				],
			},
			{
				property: "schoolroomRemodelBimbo",
				tiers: [
					{
						value: 0,
						upgraded: 1,
						text: `${this.facility.nameCaps} is designed with intelligent slaves in mind and seeks to smarten slaves by providing them with an education.`,
						link: `Redesign the curriculum to undo pesky educations and retard slaves while benefiting the most simple of minds`,
						cost: 7500 * V.upgradeMultiplierArcology * V.HackingSkillMultiplier,
						handler: () => {
							V.schoolroomUpgradeRemedial = 0;

							App.UI.reload();
						},
						prereqs: [
							V.arcologies[0].FSIntellectualDependency > 80,
						],
					},
					{
						value: 1,
						upgraded: 0,
						text: `${this.facility.nameCaps} is designed with moronic slaves in mind and seeks to dumb down slaves by providing them a confusing, contradictory education that retards decision making skills and undoes existing schooling.`,
						link: `Restore the curriculum to the standard`,
						cost: 7500 * V.upgradeMultiplierArcology * V.HackingSkillMultiplier,
						handler: () => {
							V.schoolroomUpgradeRemedial = 0;

							App.UI.reload();
						},
					},
				],
			},
			{
				property: "schoolroomUpgradeRemedial",
				tiers: [
					{
						value: 0,
						upgraded: 1,
						text: `${this.facility.nameCaps} teaches woefully smart slaves using its modified methods.`,
						link: `Purchase specialized materials to help smart slaves get on the right track`,
						cost: 5000 * V.upgradeMultiplierArcology * V.HackingSkillMultiplier,
						notes: [`increases the effectiveness of ${V.schoolroomName}`],
						prereqs: [
							V.schoolroomRemodelBimbo > 0,
						],
					},
					{
						value: 1,
						text: `${this.facility.nameCaps} has been upgraded with advanced teaching tools to help even the smartest slave learn at an acceptable pace. Dumb slaves won't learn much faster as a result, but smarties will benefit a great deal.`,
						prereqs: [
							V.schoolroomRemodelBimbo > 0,
						],
					},
					{
						value: 0,
						upgraded: 1,
						text: `${this.facility.nameCaps} teaches idiots using standard methods.`,
						link: `Purchase specialized materials to help stupid slaves learn good`,
						cost: 5000 * V.upgradeMultiplierArcology * V.HackingSkillMultiplier,
						handler: () => V.schoolroomRemodelBimbo = 0,
						notes: [`increases the effectiveness of ${V.schoolroomName}`],
						prereqs: [
							V.schoolroomRemodelBimbo === 0,
						],
					},
					{
						value: 1,
						text: `${this.facility.nameCaps} has been upgraded with advanced teaching tools to help even the stupidest slave learn at an acceptable pace. Intelligent slaves won't learn much faster as a result, but idiots will benefit a great deal.`,
						prereqs: [
							V.schoolroomRemodelBimbo === 0,
						],
					},
				],
			},
		];
	}
};


code:
/** @type {FC.FacilityFramework} */
App.Data.Facilities.schoolroom = {
	baseName: "schoolroom",
	genericName: null,
	jobs: {
		student: {
			position: "student",
			assignment: Job.SCHOOL,
			publicSexUse: false,
			fuckdollAccepted: false
		}
	},
	defaultJob: "student",
	manager: {
		position: "schoolteacher",
		assignment: Job.TEACHER,
		careers: App.Data.Careers.Leader.schoolteacher,
		skill: "teacher",
		publicSexUse: false,
		fuckdollAccepted: false,
		broodmotherAccepted: false,
		shouldWalk: false,
		shouldHold: false,
		shouldSee: true,
		shouldHear: true,
		shouldTalk: true,
		shouldThink: true,
		requiredDevotion: 51
	},
	decorated: true
};

App.Entity.Facilities.SchoolroomStudentJob = class extends App.Entity.Facilities.FacilitySingleJob {
	/**
	 * @override
	 * @returns {string}
	 */
	get assignment() {
		return `study in ${this.facility.name}`;
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @returns {string[]}
	 */
	checkRequirements(slave) {
		let r = super.checkRequirements(slave);

		if (!App.Entity.Facilities.Job._isBrokenEnough(slave, -20, -50, -20, -51)) {
			r.push(`${slave.slaveName} is too resistant to learn.`);
		}

		const maxSkill = 10 + this.facility.upgrade("Skills") * 20; // maximal skill value the school can teach
		if (((slave.intelligenceImplant >= 30 && V.schoolroomRemodelBimbo !== 1) || (slave.intelligenceImplant <= -15 && V.schoolroomRemodelBimbo === 1)) &&
			(slave.voice === 0 || slave.accent + this.facility.upgrade("Language") <= 2) &&
			(slave.skill.oral > maxSkill) && (slave.skill.whoring > maxSkill) && (slave.skill.entertainment > maxSkill) &&
			(slave.skill.anal > maxSkill) && ((slave.vagina < 0) || (slave.skill.vaginal > maxSkill)) &&
			(!needsTutoring(slave))) {
			r.push(`${slave.slaveName} has nothing left to learn.`);
		}

		if (slave.fetish === "mindbroken") {
			r.push(`${slave.slaveName}'s mind is fundamentally broken and can't learn`);
		}

		return r;
	}
};

App.Entity.Facilities.Schoolroom = class extends App.Entity.Facilities.SingleJobFacility {
	constructor() {
		super(App.Data.Facilities.schoolroom, {
			student: new App.Entity.Facilities.SchoolroomStudentJob()
		});
	}

	/** Does the schoolroom decoration impart a language-learning bonus?
	 * @returns {boolean}
	 */
	revivalistLanguageDecorationBonus() {
		const decoFS = Object.keys(App.Data.FutureSociety.records).find(FS => App.Data.FutureSociety.records[FS].deco === V.schoolroomDecoration);
		return decoFS && App.Data.FutureSociety.records[decoFS].language === V.language;
	}
};

App.Entity.facilities.schoolroom = new App.Entity.Facilities.Schoolroom();


code:
App.Facilities.StewardessSelect = function() {
	const f = document.createDocumentFragment();

	if (S.Stewardess) {
		f.append(`${SlaveFullName(S.Stewardess)} is working as your Stewardess, managing the Servants' Quarters. `);
		f.append(App.UI.DOM.link("Remove Stewardess", () => {
			removeJob(S.Stewardess, Job.STEWARD);
		},
		[], "Servants' Quarters"
		));
	} else {
		f.append(`No Stewardess assigned, appoint one from your devoted slaves.`);
	}

	f.append(App.UI.SlaveList.facilityManagerSelection(App.Entity.facilities.servantsQuarters, "Servants' Quarters"));
	return f;
};


code:
App.Facilities.ServantsQuarters.servantsQuarters = class ServantsQuarters extends App.Facilities.Facility {
	constructor() {
		const servantsQuarters = App.Entity.facilities.servantsQuarters;
		const decommissionHandler = () => {
			V.servantsQuarters = 0;
			V.servantsQuartersDecoration = "standard";
			V.servantsQuartersUpgradeMonitoring = 0;
		};

		super(
			servantsQuarters,
			decommissionHandler,
		);

		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.encyclopedia = "Servants' Quarters";
	}

	/** @returns {string} */
	get intro() {
		const text = [];

		text.push(this.facility.nameCaps, this.decorations);

		if (this.facility.hostedSlaves > 2) {
			text.push(`${this.facility.nameCaps} are busy with hurrying slaves. One shift of servants is eating, cleaning the quarters, and bathing. The second is sleeping, and the third is out in the penthouse cleaning and serving.`);
		} else if (this.facility.hostedSlaves > 0) {
			text.push(`A few slaves are working out of the servants' quarters. They must split their scant time between looking after their own needs and the superior needs of everyone else.`);
		} else if (S.Stewardess) {
			text.push(`${S.Stewardess.slaveName} is alone, and seems rather bereft without anyone to order around.`);
		} else {
			text.push(`None of your slaves are working out of the servants' quarters.`);
		}

		return text.join(' ');
	}

	/** @returns {string} */
	get decorations() {
		/** @type {FC.Facilities.Decoration} */
		const FS = {
			"Roman Revivalist": `are spartan, yet functional. At one end of the long dormitory there is an alcove with a shrine to the Roman goddess of domesticity.`,
			"Neo-Imperialist": `are surprisingly luxurious for mere servant's quarters. They're a large, public dormitory, but the barracks-like beds are soft, clean, high-tech pods stacked two to a row, separated by banners emblazoned with your family crest. A few small tapestries decorate the walls, mostly showing happy-looking servants cleaning and cooking.`,
			"Aztec Revivalist": `are simple, yet grandiose. In every corner stands a monument to a god, and as the sun peeks through, all the obsidian ornaments glisten with light.`,
			"Egyptian Revivalist": `are utilitarian, yet warm and well-kept. At one end of the long dormitory there is an alcove with a shrine to the Egyptian goddess of servility.`,
			"Edo Revivalist": `are spartan and functional. Fresh mats are laid across the floor every day, and the simple beds of blankets and wooden blocks are neatly stowed against the walls before sunrise.`,
			"Arabian Revivalist": `are utilitarian, yet warm and well-kept. They are floored with beautifully patterned tile; there are channels for clean running water that make it easy to keep clean and fresh, so long as one is willing to bathe in the open.`,
			"Chinese Revivalist": `are spartan, yet functional. At one end of the long dormitory there is an alcove with a shrine hosting a bronze statue of a watchful dragon.`,
			"Chattel Religionist": `are spartan, yet functional. Servants here are expected to be clean and hardworking, and there is a penitents cell ready for them if they are not.`,
			"Degradationist": `are severe and utilitarian. Servants sleep uncomfortably here, knowing that even on their meager bedrolls, they are fair game for abuse.`,
			"Repopulationist": `are comfortable and well-kept. Though there is little privacy here, the servants are provided for. There's even a small rest area for them to rest their pregnant bodies between shifts. A closet contains specialized cleaning tools to accommodate their distended bodies.`,
			"Eugenics": `are comfortable and well-kept. Though there is little privacy here, the servants are provided for. They are required to always be carrying prophylactics for other slaves and any low class citizens who require them.`,
			"Asset Expansionist": `are comfortable and well-kept. There are pornographic pictures on the walls, depicting slaves with gigantic breasts earnestly enjoying huge cocks.`,
			"Transformation Fetishist": `are comfortable and well-kept. There are pornographic pictures on the walls, depicting slaves with huge implants earnestly enjoying sex.`,
			"Gender Radicalist": `are comfortable and well-kept. There are pornographic pictures on the walls, depicting all sorts of slaves earnestly enjoying taking cocks up their anuses.`,
			"Gender Fundamentalist": `are comfortable and well-kept. There are pornographic pictures on the walls, depicting pretty female slaves being fucked by muscular men.`,
			"Physical Idealist": `are comfortable and well-kept. There are softcore pictures on the walls, depicting gorgeously muscled, oiled-up men and women, flexing and posing for the camera.`,
			"Supremacist": `are spartan, since that's all domestics from the inferior races need or deserve. There's a whipping post in the corner of the room so that whenever a slave is beaten, the rest must watch.`,
			"Subjugationist": `are spartan, since that's all ${V.arcologies[0].FSSubjugationistRace} domestics need or deserve. There's a whipping post in the corner of the room so that whenever a slave is beaten, the rest must watch.`,
			"Paternalist": `are comfortable and well-kept. Though there is little privacy here, the servants are provided for. There's even a small rest area for them to take their regular breaks in.`,
			"Pastoralist": `are comfortable and well-kept. There are pornographic pictures on the walls, depicting lactating slaves earnestly enjoying sex. The servants are provided with milk to drink.`,
			"Maturity Preferentialist": `are comfortable, but functional. There are motivational posters on the walls featuring cheerful MILF servants, exhorting mature slaves to smile, serve, and get fucked like good girls.`,
			"Youth Preferentialist": `are comfortable, but well equipped to corral wayward slaves. There are instructional screens in the common areas reviewing basic slave tasks like scrubbing a floor and giving head.`,
			"Body Purist": `are comfortable and well-kept. There are pornographic pictures on the walls, depicting gorgeous slaves earnestly enjoying sex.`,
			"Slimness Enthusiast": `are comfortable and well-kept. There are pornographic pictures on the walls, depicting slender slaves earnestly enjoying sex.`,
			"Hedonistic": `are comfortable and well-kept. There are plenty of soft couches to recline on between shifts and a number of feeders to keep servants full and happy. There are pornographic pictures on the walls, depicting fat slaves earnestly enjoying sex while stuffing their and their partners faces.`,
			"Intellectual Dependency": `are comfortable, but well equipped to corral wayward slaves. There are instructional screens in the common areas reviewing basic slave tasks like scrubbing a floor and not wandering off. Plenty of toys are available in an effort to keep minds focused on work and not solely on sex.`,
			"Slave Professionalism": `are comfortable and well-kept, assuming a slave is able to understand how to make use of the amenities. There are abundant menial tasks available to keep a mind busy and sharp.`,
			"Petite Admiration": `are comfortable and well-kept, assuming a slave is short enough, of course. Those that stand too tall find the accommodations cramped and the tools annoyingly short.`,
			"Statuesque Glorification": `are comfortable and well-kept, assuming a slave is tall enough, of course. Those that are too short find the accommodations out of reach and the tools painfully long.`,
			"standard": `are comfortable. Servants sleep together in a dormitory, eat together in a little kitchen, bathe together in a communal shower, and then head out into the penthouse to serve.`,
		};

		const res = FS[V.servantsQuartersDecoration];

		if (!res) {
			throw new Error(`Unknown V.servantsQuartersDecoration value of '${V.servantsQuartersDecoration}' found in decorations().`);
		}

		return res;
	}

	/** @returns {FC.Facilities.Expand} */
	get expand() {
		return {
			desc: `${this.facility.nameCaps} has room to keep ${numberWithPluralOne(V.servantsQuarters, "slave")} while they serve. There ${this.facility.hostedSlaves === 1 ? `is currently ${num(this.facility.hostedSlaves)} slave` : `are currently ${num(this.facility.hostedSlaves)} slaves`} serving in ${V.servantsQuartersName}.`,
			removeSlave: "be a servant",
		};
	}

	/** @returns {FC.IUpgrade[]} */
	get upgrades() {
		return [
			{
				property: "servantsQuartersUpgradeMonitoring",
				tiers: [
					{
						value: 0,
						upgraded: 1,
						text: `The quarters are standard.`,
						link: `Upgrade the monitoring systems to force harder work`,
						cost: 10000 * V.upgradeMultiplierArcology * V.HackingSkillMultiplier,
						handler: () => V.PC.skill.hacking += 0.1,
						notes: [`will increase upkeep costs`],
					},
					{
						value: 1,
						text: `The quarters have been upgraded with enhanced monitoring systems to make the servants work harder, improving their obedience and efficiency.`,
					},
				],
			},
		];
	}

	/** @returns {FC.Facilities.Rule[]} */
	get rules() {
		return [
			{
				property: "stewardessImpregnates",
				prereqs: [
					!!S.Stewardess,
					!!canAchieveErection(S.Stewardess) && S.Stewardess.pubertyXY === 1,
					!!V.seePreg,
				],
				options: [
					{
						get text() {
							const {He, himself} = getPronouns(S.Stewardess);
							return `${He} could be directed to keep the maids pregnant ${himself}.`;
						},
						link: `Do not impregnate maids`,
						value: 0,
					},
					{
						get text() {
							const {his} = getPronouns(S.Stewardess);
							return `Keeping the maids pregnant is part of ${his} job.`;
						},
						link: `Impregnate maids`,
						value: 1,
					},
				],
			},
		];
	}

	/** @returns {HTMLDivElement} */
	get staffing() {
		const div = document.createElement("div");

		div.append(`Your household servants are `);
		const capacity = totalServantCapacity();
		if (capacity > V.slaves.length * 1.5) {
			div.append(`frankly more numerous than you really need at this point, and some have to be occupied with unproductive "busy work." You should consider reassigning some to other tasks.`);
		} else if (capacity >= V.slaves.length * 0.95) {
			div.append(`capable of adequately cleaning your facilities and seeing to your slaves, without having too much idle time. You have neither too many nor too few.`);
		} else if (capacity >= V.slaves.length * 0.5) {
			div.append(`constantly busy, and could probably use a little more help.`);
		} else {
			div.append(`severely understaffed. Assigning additional slaves would yield financial benefits.`);
		}

		div.append(` They can currently help reduce the maintenance of up to ${num(capacity)} slaves, and you have ${num(V.slaves.length)}.`);

		return div;
	}

	/** @returns {HTMLDivElement} */
	get milking() {
		const div = document.createElement("div");

		if (V.servantMilkers) {
			div.append(`Since servants spend most of their time in the penthouse, any who are lactating will use the ubiquitous milkers.`);
		}

		return div;
	}

	/** @returns {HTMLDivElement[]} */
	get customNodes() {
		return [
			this.staffing,
			this.milking,
		];
	}
};


code:
/** @type {FC.FacilityFramework} */
App.Data.Facilities.servantsQuarters = {
	baseName: "servantsQuarters",
	genericName: "Servants' Quarters",
	jobs: {
		servant: {
			position: "servant",
			assignment: Job.QUARTER,
			publicSexUse: false,
			fuckdollAccepted: false
		}
	},
	defaultJob: "servant",
	manager: {
		position: "stewardess",
		assignment: Job.STEWARD,
		careers: App.Data.Careers.Leader.stewardess,
		skill: "stewardess",
		publicSexUse: false,
		fuckdollAccepted: false,
		broodmotherAccepted: false,
		shouldWalk: true,
		shouldHold: true,
		shouldSee: true,
		shouldHear: true,
		shouldTalk: true,
		shouldThink: true,
		requiredDevotion: 51
	},
	decorated: true
};

App.Entity.Facilities.ServantsQuartersServantJob = class extends App.Entity.Facilities.FacilitySingleJob {
	/**
	 * @override
	 * @returns {string}
	 */
	get assignment() {
		return `work in ${this.facility.name}`;
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @returns {string[]}
	 */
	checkRequirements(slave) {
		let r = super.checkRequirements(slave);

		if (!App.Entity.Facilities.Job._isBrokenEnough(slave, -20, -50, 20, -21)) {
			r.push(App.Entity.Facilities.Job._stdBreakageMessage(slave));
		}
		if (!canWalk(slave)) {
			r.push(`${slave.slaveName} can't walk and would be unable to properly clean.`);
		} else if (!canHold(slave)) {
			r.push(`${slave.slaveName} has no hands and would be unable to properly clean.`);
		}
		if (!canSee(slave)) {
			r.push(`${slave.slaveName} is blind and would be unable to properly clean.`);
		}
		return r;
	}
};

App.Entity.Facilities.ServantsQuartersStewardessJob = class extends App.Entity.Facilities.ManagingJob {
	/**
	 * @param {App.Entity.SlaveState} slave
	 * @returns {string[]}
	 */
	checkRequirements(slave) {
		let r = super.checkRequirements(slave);
		if (slave.intelligence + slave.intelligenceImplant < -50) {
			r.push(`${slave.slaveName} is not intelligent enough.`);
		}
		return r;
	}
};

App.Entity.facilities.servantsQuarters = new App.Entity.Facilities.SingleJobFacility(
	App.Data.Facilities.servantsQuarters,
	{
		servant: new App.Entity.Facilities.ServantsQuartersServantJob()
	},
	new App.Entity.Facilities.ServantsQuartersStewardessJob()
);


code:
App.Facilities.AttendantSelect = function() {
	const f = document.createDocumentFragment();

	if (S.Attendant) {
		f.append(`${SlaveFullName(S.Attendant)} is working as your Attendant, pampering and counseling your slaves. `);
		f.append(App.UI.DOM.link("Remove Attendant", () => {
			removeJob(S.Attendant, Job.ATTENDANT);
		},
		[], "Spa"
		));
	} else {
		f.append(`No Attendant assigned, appoint one from your devoted slaves.`);
	}

	f.append(App.UI.SlaveList.facilityManagerSelection(App.Entity.facilities.spa, "Spa"));
	return f;
};


code:
App.Facilities.Spa.spa = class Spa extends App.Facilities.Facility {
	constructor() {
		const spa = App.Entity.facilities.spa;
		const decommissionHandler = () => {
			V.spa = 0;
			V.spaDecoration = "standard";
			V.spaUpgrade = 0;
			V.spaFix = 0;
		};

		super(
			spa,
			decommissionHandler,
		);

		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.encyclopedia = "Spa";
	}

	/** @returns {string} */
	get intro() {
		const text = [];

		const spaUtilization = V.spaSpots / (V.spa * 20);

		text.push(this.facility.nameCaps, this.decorations);

		if (spaUtilization >= 1) {
			text.push(`It's crowded in here. Slaves are relaxing in the warm water, splashing around or just floating. Here and there some of the more sex-starved are in the early stages of intercourse, but most prefer to take time off from it all. Unfortunately there is not enough space for all of your slaves to enjoy the spa.`);
		} else if (spaUtilization >= 0.5 || (this.facility.hostedSlaves / V.spa > 0.5)) {
			text.push(`It's busy in here. Slaves are relaxing in the warm water, splashing around or just floating. Here and there some of the more sex-starved are in the early stages of intercourse, but most prefer to take time off from it all.`);
		} else if (spaUtilization > 0 || this.facility.hostedSlaves > 0) {
			text.push(`It's sparsely populated; though the few slaves here have little company they like having the water to themselves.`);
		} else if (S.Attendant) {
			const {his} = getPronouns(S.Attendant);
			text.push(`${S.Attendant.slaveName} is alone here, and has nothing to do but keep the place (and ${his} own soft, wet body) spotlessly clean.`);
		} else {
			text.push(`It's empty and quiet.`);
		}

		return text.join(' ');
	}

	/** @returns {string} */
	get decorations() {
		/** @type {FC.Facilities.Decoration} */
		const FS = {
			"Roman Revivalist": `is built as a Roman bath. The flooring is pleasantly warm due to a modernized version of hypocaust heating, and is covered in mosaic depicting slaves enjoying sex.`,
			"Neo-Imperialist": `is built as a modern Imperial garden. Bright green plants mix with the pulsating lights of high technology, and clear, sleek windows fog up with the steam of a central bath heated by the latest hydraulic technology.`,
			"Aztec Revivalist": `is built as an Aztec bathhouse. Water steams from the middle of the room and the air is heavy with the scent of herbs and essences. The idols by the door glisten with moisture.`,
			"Egyptian Revivalist": `is decorated like an Egyptian water garden. All but the hottest pools include aquatic plants around their edges, and the atmosphere is heavy with perfume.`,
			"Edo Revivalist": `is decorated like a traditional onsen. The stone-lined pools are surrounded by meticulously kept gardens, and there are proper provisions for bathing in the old Japanese style.`,
			"Arabian Revivalist": `looks like a dream of an Arabian palace garden. Every surface is richly tiled in vibrant colors, and the beguiling scents of perfumes from the Levant hang in the air.`,
			"Chinese Revivalist": `is gloomy and hot, filled with an oppressive steam that immediately dulls the senses. Though relaxation is possible and indeed easy here, it is a stultifying relaxation whose humid warmth seems to suppress independence.`,
			"Chattel Religionist": `is dedicated to the purification of the body and the spirit. The pools are arranged for the completion of self-purification procedures which include ritual masturbation.`,
			"Degradationist": `is utilitarian. There are waterproof cameras positioned throughout the spa so that anyone who wants to can watch the nude slaves. One wall has a screen showing the current viewer count to keep the slaves aware of this.`,
			"Asset Expansionist": `is utilitarian. It is equipped with all sorts of devices to help slaves care for huge assets, including lifts to help them in and out of the water, and all around showers to help clean and moisturize difficult to reach spots.`,
			"Transformation Fetishist": `is utilitarian. It is equipped with special devices to help speed surgical recovery, including a series of baths designed to prevent scarring.`,
			"Repopulationist": `is comfortable, with waterproof cushions lining the pools. It is equipped with all sorts of devices to aid pregnant slaves, including lifts to help them in and out of the water, baths just for their feet, and all around showers to help clean and moisturize difficult to reach spots.`,
			"Eugenics": `is comfortable, albeit split in half. One side for the lower classes' slaves, and the other for the Elite and their pets.`,
			"Gender Radicalist": `is comfortable, with waterproof cushions lining the pools. There are screens on the walls showing slave girls with all different varieties of genitalia orgasming from penetration, to keep the idea at the forefront of the slaves' minds.`,
			"Gender Fundamentalist": `is comfortable, with waterproof cushions lining the pools. There are screens on the walls showing light entertainment featuring a lot of beautiful women and handsome men for the slaves' edification.`,
			"Physical Idealist": `is not the gym, but it does have some workout equipment, mostly low-impact machines designed to speed recovery. There are special hot baths to ease sore muscles.`,
			"Supremacist": `is comfortable, with waterproof cushions lining the pools. There are screens on the walls showing light entertainment featuring ${V.arcologies[0].FSSupremacistRace} main characters.`,
			"Subjugationist": `is comfortable, with waterproof cushions lining the pools. There are screens on the walls showing light entertainment featuring ${V.arcologies[0].FSSubjugationistRace} characters in comic relief roles.`,
			"Paternalist": `is comfortable, with waterproof cushions lining the pools. There are screens on the walls showing light entertainment written by and intended for smart, loyal slaves.`,
			"Pastoralist": `is utilitarian. It is equipped with all sorts of devices to help slaves care for huge assets, including lifts to help them in and out of the water, and all around showers to help clean and moisturize difficult to reach spots.`,
			"Maturity Preferentialist": `is comfortable, but surprisingly businesslike. It's all about beautification here; there's a bewildering array of mud baths, resting pools, and massage setups, all designed to keep mature slaves looking their very best.`,
			"Youth Preferentialist": `is comfortable and fun. There are hot tubs and massage tables for slaves who feel like relaxing, but there's also a colder pool with pool toys for slaves who want to play. It even has a small waterslide.`,
			"Body Purist": `is comfortable, with waterproof cushions lining the pools. Everything is designed for the slaves' comfort; there are even special mud baths to perfect skin clarity.`,
			"Slimness Enthusiast": `is comfortable, with waterproof cushions lining the pools. Everything is designed for the slaves' comfort; there are even special mud baths to perfect skin clarity.`,
			"Hedonistic": `is comfortable, with waterproof cushions lining the pools. It is equipped with all sorts of devices to aid hefty slaves, including lifts to help them in and out of the water, specialized moisturizers to keep their skin healthy and smooth, and all around showers to help clean difficult to reach spots and between folds. ${V.arcologies[0].FSHedonisticDecadenceResearch === 1 ? `Platters of food and treats are readily available around the tubs so that relaxing slaves never have to strain to grab a bite to eat` : `Feeders connected to the slave food reserves line the pools so that so that relaxing slaves never have to strain to suck down their fill of food`}.`,
			"Intellectual Dependency": `is comfortable, fun and, most importantly, safe; even the dumbest slave can enjoy the pools without worrying their ${properTitle()}. There are screens on the walls showing simple entertainment designed to arouse more than titillate.`,
			"Slave Professionalism": `is comfortable, with waterproof cushions lining the pools. It is a place where a weary slave can rest their mind after a hards day's work. There are screens on the walls showing documentaries intended for smart, skilled slaves.`,
			"Petite Admiration": `is comfortable, but designed with short slaves in mind. The pools are shallow and easy to slip in and out of; taller slaves are likely to find them more frustrating than enjoyable.`,
			"Statuesque Glorification": `is comfortable, but designed with tall slaves in mind. The pools are deep; too deep for a short slave to find relaxing.`,
			"standard": `is well-appointed, with massage tables, hot tubs, and a cold pool.`,
		};

		const res = FS[V.spaDecoration];

		if (!res) {
			throw new Error(`Unknown V.spaDecoration value of '${V.spaDecoration}' found in decorations().`);
		}

		return res;
	}

	/** @returns {FC.Facilities.Expand} */
	get expand() {
		return {
			desc: `${this.facility.nameCaps} can house ${numberWithPluralOne(V.spa, "slave")} while they recuperate here. There ${this.facility.hostedSlaves === 1 ? `is currently ${num(this.facility.hostedSlaves)} slave` : `are currently ${num(this.facility.hostedSlaves)} slaves`} recuperating in ${V.spaName}.`,
		};
	}

	/** @returns {FC.IUpgrade[]} */
	get upgrades() {
		return [
			{
				property: "spaUpgrade",
				tiers: [
					{
						value: 0,
						upgraded: 1,
						text: `${this.facility.nameCaps} is a standard spa.`,
						link: `Upgrade ${V.spaName} with saunas, steam rooms, and mineral water baths`,
						cost: 1000 * V.upgradeMultiplierArcology,
						notes: [`increases the effectiveness of ${V.spaName}`],
					},
					{
						value: 1,
						text: `${this.facility.nameCaps} has been upgraded with state of the art temperature treatment options, from hot and cold mineral water pools to baking saunas and dense steam rooms.`,
					}
				],
			},
		];
	}

	/** @returns {FC.Facilities.Rule[]} */
	get rules() {
		return [
			{
				property: "spaFix",
				prereqs: [
					!!S.Attendant,
				],
				options: [
					{
						get text() {
							const {he} = getPronouns(S.Attendant);

							return `${S.Attendant.slaveName} is not following any special orders and is tending to your slaves as ${he} sees fit.`;
						},
						link: `Free reign`,
						value: 0,
					},
					{
						get text() {
							const {his} = getPronouns(S.Attendant);

							return `${S.Attendant.slaveName} is focusing on the health and wellness of the slaves under ${his} care, as well as trying to bring mindbroken slaves back, while ignoring any mental hang-ups a slave may have.`;
						},
						link: `Only mindbroken slaves`,
						value: 1,
					},
					{
						get text() {
							const {his} = getPronouns(S.Attendant);

							return `${S.Attendant.slaveName} is focusing only on the health and wellness of the slaves under ${his} care, obeying your orders to avoid attempting to right mental flaws.`;
						},
						link: `Don't fix any flaws`,
						value: 2,
					},
				],
			},
		];
	}
};


code:
/** @type {FC.FacilityFramework} */
App.Data.Facilities.spa = {
	baseName: "spa",
	genericName: null,
	jobs: {
		assignee: {
			position: "bather",
			assignment: Job.SPA,
			publicSexUse: false,
			fuckdollAccepted: false
		}
	},
	defaultJob: "assignee",
	manager: {
		position: "attendant",
		assignment: Job.ATTENDANT,
		careers: App.Data.Careers.Leader.attendant,
		skill: "attendant",
		publicSexUse: false,
		fuckdollAccepted: false,
		broodmotherAccepted: false,
		shouldWalk: true,
		shouldHold: true,
		shouldSee: false,
		shouldHear: true,
		shouldTalk: false,
		shouldThink: true,
		requiredDevotion: 51
	},
	decorated: true
};

App.Entity.Facilities.SpaAssigneeJob = class extends App.Entity.Facilities.FacilitySingleJob {
	/**
	 * @param {App.Entity.SlaveState} slave
	 * @returns {string[]}
	 */
	checkRequirements(slave) {
		let r = super.checkRequirements(slave);

		if (
			slave.fetish !== "mindbroken" &&
			(
				slave.devotion < -20 ||
				(
					slave.health.tired <= 30 &&
					slave.trust > 60 &&
					slave.devotion > 60 &&
					slave.sexualFlaw === "none" &&
					slave.behavioralFlaw === "none"
				)
			)
		) {
			if (slave.devotion < -20) {
				r.push(`${slave.slaveName} is too resistant to be trusted at ${this.facility.name}.`);
			} else {
				r.push(`${slave.slaveName} will not benefit from time at ${this.facility.name}.`);
			}
		}

		return r;
	}
};

App.Entity.facilities.spa = new App.Entity.Facilities.SingleJobFacility(
	App.Data.Facilities.spa,
	{
		assignee: new App.Entity.Facilities.SpaAssigneeJob()
	}
);


code:
App.Facilities.StatsHelper = class {
	/** Make a statistics table with given column labels
	 * @param {string[]} columns - Array of four labels for data columns. The first is wider than the others (typically used for facility revenue).
	 * @returns {HTMLTableElement}
	 */
	makeStatsTable(columns) {
		const table = document.createElement("table");
		table.border = "1";
		table.className = "facility-stats";
		const header = App.UI.DOM.appendNewElement("tr", table, null, ["header"]);
		App.UI.DOM.appendNewElement("th", header, "Items"); // first column, flexible width
		App.UI.DOM.appendNewElement("th", header, columns[0], ["wide"]); // "revenue" column, wider
		for (let i = 1; i < columns.length; ++i) {
			App.UI.DOM.appendNewElement("th", header, columns[i], ["narrow"]); // additional narrow columns
		}
		return table;
	}

	/** Adds a value row to a stats table, with the given label and value cells
	 * @param {HTMLTableElement} table
	 * @param {string|Node} label
	 * @param {HTMLTableCellElement[]} valueCells
	 * @param {boolean} [totals]
	 */
	addValueRow(table, label, valueCells, totals) {
		const row = App.UI.DOM.appendNewElement("tr", table, undefined, totals ? "total" : undefined);
		const labelCell = App.UI.DOM.appendNewElement("td", row, label);
		if (totals) {
			labelCell.style.fontWeight = "bold";
		}
		for (const cell of valueCells) {
			row.appendChild(cell);
		}
	}

	/** Makes a slave label
	 * @param {string} slaveName
	 * @param {string} customLabel
	 * @returns {Node}
	 */
	makeSlaveLabel(slaveName, customLabel) {
		if (!customLabel) {
			return document.createTextNode(slaveName);
		} else {
			const frag = document.createDocumentFragment();
			const label = App.UI.DOM.appendNewElement("span", frag, `(${customLabel}) `, "custom-label");
			frag.appendChild(label);
			frag.appendChild(document.createTextNode(slaveName));
			return frag;
		}
	}

	/** Makes a value cell
	 * @param {"reputation"|"cash"|"food"} type
	 * @param {number} value - numeric value
	 * @param {object} [flags]
	 * @param {boolean} [flags.forceNeg] - treat nonzero positive values as negative
	 * @param {boolean} [flags.showSign] - display the sign
	 * @param {boolean} [flags.showZero] - show the value, even if it's zero
	 * @param {boolean} [flags.bold] - style in bold
	 * @returns {HTMLTableCellElement}
	 */
	makeValueCell(type, value, flags = {}) {
		const cell = document.createElement("td");
		cell.classList.add("value");

		if (value !== 0 || flags.showZero) {
			// style appropriately
			if (value < 0 || flags.forceNeg) {
				cell.classList.add(type); // reputation or cash
				cell.classList.add("dec"); // loss
			} else if (value > 0) {
				cell.classList.add(type); // reputation or cash
				cell.classList.add("inc"); // gain
			}
			if (flags.bold) {
				cell.style.fontWeight = "bold";
			}

			// set contents
			let prefix = '';
			let suffix = '';
			if (type === "cash") {
				prefix += '¤';
			}
			if (type === "food") {
				suffix += 'kg';
			}
			if (flags.showSign) {
				if (flags.forceNeg) { // if the real value is negative, - sign will come from value.toFixed
					prefix += '-';
				} else if (value > 0) {
					prefix += '+';
				} else if (value === 0) {
					prefix += '±';
				}
			}
			let fixedPrecision = 1;
			if (type === "cash") {
				fixedPrecision = 2;
			} else if (type === "food") {
				fixedPrecision = 0;
			}
			const parts = value.toFixed(fixedPrecision).split('.');
			cell.appendChild(document.createTextNode(prefix + parts[0] + suffix));
			if (fixedPrecision > 0) {
				App.UI.DOM.appendNewElement("span", cell, '.' + parts[1], /^0+$/.test(parts[1]) ? ["decimalZero"] : undefined);
			} else {
				App.UI.DOM.appendNewElement("span", cell);
			}
		}

		return cell;
	}

	/** Make an empty cell (for parity)
	 * @returns {HTMLTableCellElement}
	 */
	makeEmptyCell() {
		return document.createElement("td");
	}

	/** Make the customer cell (for the facility-specific slave details column)
	 * @param {number} value
	 * @returns {HTMLTableCellElement}
	 */
	makeCustomersCell(value) {
		const cell = document.createElement("td");
		cell.classList.add("value");
		if (value <= 0) {
			cell.textContent = "none";
			cell.classList.add("red");
		} else {
			cell.textContent = value.toString();
		}
		return cell;
	}

	/** Make the production cell (for the facility-specific slave details column)
	 * @param {number} milk
	 * @param {number} cum
	 * @param {number} fluid
	 * @returns {HTMLTableCellElement}
	 */
	makeProductionCell(milk, cum, fluid) {
		const cell = document.createElement("td");
		cell.classList.add("value");
		if (milk || cum || fluid) {
			cell.textContent = `${milk}/${cum}/${fluid}`;
		} else {
			cell.textContent = `0/0/0`;
		}
		return cell;
	}

	/** Appends the slave stats row to the main stats table, and returns a reference to the slave stats table for later use.
	 * @param {HTMLTableElement} statsTable
	 * @param {string} caption
	 * @param {string[]} columns - six column labels - slave type, facility-specific output details column, wide column, then three narrow columns
	 * @returns {HTMLTableElement}
	 */
	makeSlaveStatsTable(statsTable, caption, columns) {
		const row = App.UI.DOM.appendNewElement("tr", statsTable);
		const bigCell = document.createElement("td");
		bigCell.colSpan = columns.length;
		const label = App.UI.DOM.appendNewElement("span", bigCell, caption);
		label.style.fontWeight = "bold";
		row.appendChild(bigCell);

		const table = document.createElement("table");
		table.className = "facility-stats-slave";
		const header = App.UI.DOM.appendNewElement("tr", table, "", "header");
		App.UI.DOM.appendNewElement("th", header, columns[0]); // first column, flexible width
		App.UI.DOM.appendNewElement("th", header, columns[1], "narrow"); // facility-specific output details
		App.UI.DOM.appendNewElement("th", header, columns[2], "wide"); // "revenue" column, wider
		for (let i = 3; i < columns.length; ++i) {
			App.UI.DOM.appendNewElement("th", header, columns[i], "narrow"); // additional narrow columns
		}
		bigCell.appendChild(table);
		statsTable.appendChild(bigCell);
		return table;
	}
};

App.Facilities.Brothel.Stats = (function() {
	const H = new App.Facilities.StatsHelper();
	const assureList = [ "whoreIncome", "rep", "whoreCosts", "adsIncome", "maintenance", "adsCosts", "totalIncome", "totalExpenses", "profit" ];

	/** Make the brothel slave revenue cell
	 * @param {number} revenue
	 * @param {number} adsIncome
	 * @returns {HTMLTableCellElement}
	 */
	function makeRevenueCell(revenue, adsIncome) {
		function printCash(value, cell) {
			const parts = value.toFixed(2).split('.');
			App.UI.DOM.appendNewElement("span", cell, '¤' + parts[0], ["cash", "inc"]);
			App.UI.DOM.appendNewElement("span", cell, '.' + parts[1], ["cash", "inc", "decimalPart"]);
		}
		const cell = document.createElement("td");
		cell.classList.add("value");
		printCash(revenue, cell);
		if (adsIncome > 0) {
			cell.appendChild(document.createTextNode(" ("));
			printCash(adsIncome, cell);
			cell.appendChild(document.createTextNode(" due to advertising)"));
		}
		return cell;
	}

	/** Generate the brothel statistics table
	 * @param {boolean} showDetails
	 * @returns {HTMLElement|DocumentFragment}
	 */
	function output(showDetails) {
		if (!V.showEconomicDetails) {
			return document.createDocumentFragment();
		} else if (!V.facility || !V.facility.brothel) {
			return App.UI.DOM.makeElement("h4", `- No statistics for ${V.brothelName} gathered this week -`);
		}

		const b = V.facility.brothel;
		for (const prop of assureList) {
			b[prop] = b[prop] || 0;
		}

		const stats = H.makeStatsTable(["Revenue", "Expenses", "Net Income", "Rep. Change"]);
		H.addValueRow(stats, "Total whoring income", [
			H.makeValueCell("cash", b.whoreIncome),
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreIncome),
			H.makeValueCell("reputation", b.rep, {showSign: true})
		]);
		H.addValueRow(stats, "Total whore living costs", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreCosts, {forceNeg: true}),
			H.makeValueCell("cash", b.whoreCosts, {forceNeg: true, showSign: true}),
			H.makeEmptyCell()
		]);
		if (showDetails) {
			const slaveStats = H.makeSlaveStatsTable(stats, "Whore details", ["Whore", "Customers", "Revenue", "Expenses", "Net Income", "Rep. Change"]);
			for (const record of b.income.values()) {
				const revenue = record.income + record.adsIncome;
				const netIncome = revenue - record.cost;
				H.addValueRow(slaveStats, H.makeSlaveLabel(record.slaveName, record.customLabel), [
					H.makeCustomersCell(record.customers),
					makeRevenueCell(revenue, record.adsIncome),
					H.makeValueCell("cash", record.cost, {forceNeg: true}),
					H.makeValueCell("cash", netIncome),
					H.makeValueCell("reputation", record.rep, {showSign: true})
				]);
			}
		}
		if (b.adsIncome > 0) {
			H.addValueRow(stats, "Additional income", [
				H.makeValueCell("cash", b.adsIncome),
				H.makeEmptyCell(),
				H.makeValueCell("cash", b.adsIncome),
				H.makeEmptyCell()
			]);
		}
		H.addValueRow(stats, "Brothel maintenance", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.maintenance, {forceNeg: true}),
			H.makeValueCell("cash", b.maintenance, {forceNeg: true, showSign: true}),
			H.makeEmptyCell()
		]);
		if (b.adsCosts > 0) {
			H.addValueRow(stats, "Advertising program", [
				H.makeEmptyCell(),
				H.makeValueCell("cash", b.adsCosts, {forceNeg: true}),
				H.makeValueCell("cash", b.adsCosts, {forceNeg: true, showSign: true}),
				H.makeEmptyCell()
			]);
		}
		H.addValueRow(stats, "Total", [
			H.makeValueCell("cash", b.totalIncome),
			H.makeValueCell("cash", b.totalExpenses, {forceNeg: true}),
			H.makeValueCell("cash", b.profit, {bold: true}),
			H.makeValueCell("reputation", b.rep, {showSign: true, bold: true})
		], true);

		return stats;
	}

	return output;
})();

App.Facilities.Club.Stats = (function() {
	const H = new App.Facilities.StatsHelper();
	const assureList = [ "whoreIncome", "rep", "whoreCosts", "adsIncome", "maintenance", "adsCosts", "totalIncome", "totalExpenses", "profit" ];

	/** Make the slut efficiency cell
	 * @param {number} efficiency
	 * @param {boolean} [bold]
	 * @returns {HTMLTableCellElement}
	 */
	function makeEfficiencyCell(efficiency, bold) {
		const cell = document.createElement("td");
		cell.classList.add("value");
		if (bold) {
			cell.style.fontWeight = "bold";
		}
		const parts = efficiency.toFixed(2).split('.');
		const val = App.UI.DOM.appendNewElement("span", cell, parts[0], ["reputation", "inc"]);
		App.UI.DOM.appendNewElement("span", val, '.' + parts[1], /^0+$/.test(parts[1]) ? "decimalZero" : undefined);
		cell.appendChild(document.createTextNode(" rep/¤"));
		return cell;
	}

	/** Generate the Club statistics table
	 * @param {boolean} showDetails
	 * @returns {HTMLElement|DocumentFragment}
	 */
	function output(showDetails) {
		if (!V.showEconomicDetails) {
			return document.createDocumentFragment();
		} else if (!V.facility || !V.facility.club) {
			return App.UI.DOM.makeElement("h4", `- No statistics for ${V.clubName} gathered this week -`);
		}

		const b = V.facility.club;
		for (const prop of assureList) {
			b[prop] = b[prop] || 0;
		}

		const stats = H.makeStatsTable(["Rep. Gain", "Expenses", "Rep/Expenses", "Extra Income"]);
		H.addValueRow(stats, "Total slut rep gain", [
			H.makeValueCell("reputation", b.whoreIncome, {showSign: true}),
			H.makeEmptyCell(),
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.rep)
		]);
		H.addValueRow(stats, "Total slut living costs", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreCosts, {forceNeg: true}),
			H.makeEmptyCell(),
			H.makeEmptyCell()
		]);
		if (showDetails) {
			const slaveStats = H.makeSlaveStatsTable(stats, "Public slut details", ["Slut", "Customers", "Rep. Gain", "Expenses", "Rep/Expenses", "Extra Income"]);
			for (const record of b.income.values()) {
				const netIncome = record.income / record.cost;
				H.addValueRow(slaveStats, H.makeSlaveLabel(record.slaveName, record.customLabel), [
					H.makeCustomersCell(record.customers),
					H.makeValueCell("reputation", record.income, {showSign: true, showZero: true}),
					H.makeValueCell("cash", record.cost, {forceNeg: true}),
					makeEfficiencyCell(netIncome),
					H.makeValueCell("cash", record.rep, {showZero: true})
				]);
			}
		}
		if (b.adsIncome > 0) {
			H.addValueRow(stats, "Additional rep gain", [
				H.makeValueCell("reputation", b.adsIncome),
				H.makeEmptyCell(),
				H.makeEmptyCell(),
				H.makeEmptyCell()
			]);
		}
		H.addValueRow(stats, "Club maintenance", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.maintenance, {forceNeg: true}),
			H.makeEmptyCell(),
			H.makeEmptyCell()
		]);
		if (b.adsCosts > 0) {
			H.addValueRow(stats, "Advertising program", [
				H.makeEmptyCell(),
				H.makeValueCell("cash", b.adsCosts, {forceNeg: true}),
				H.makeEmptyCell(),
				H.makeEmptyCell()
			]);
		}
		H.addValueRow(stats, "Total", [
			H.makeValueCell("reputation", b.totalIncome, {showSign: true}),
			H.makeValueCell("cash", b.totalExpenses, {forceNeg: true}),
			makeEfficiencyCell(b.profit, true),
			H.makeValueCell("cash", b.rep, {bold: true, showZero: true})
		], true);

		return stats;
	}

	return output;
})();

App.Facilities.Arcade.Stats = (function() {
	const H = new App.Facilities.StatsHelper();
	const assureList = [ "whoreIncome", "rep", "whoreCosts", "maintenance", "totalIncome", "totalExpenses", "profit" ];

	/** Generate the arcade statistics table
	 * @param {boolean} showDetails
	 * @returns {HTMLElement|DocumentFragment}
	 */
	function output(showDetails) {
		if (!V.showEconomicDetails) {
			return document.createDocumentFragment();
		} else if (!V.facility || !V.facility.arcade) {
			return App.UI.DOM.makeElement("h4", `- No statistics for ${V.arcadeName} gathered this week -`);
		}

		const b = V.facility.arcade;
		for (const prop of assureList) {
			b[prop] = b[prop] || 0;
		}

		const stats = H.makeStatsTable(["Revenue", "Expenses", "Net Income", "Rep. Change"]);
		H.addValueRow(stats, "Total arcade income", [
			H.makeValueCell("cash", b.whoreIncome),
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreIncome),
			H.makeValueCell("reputation", b.rep, {showSign: true})
		]);
		H.addValueRow(stats, "Total fuckmeat living costs", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreCosts, {forceNeg: true}),
			H.makeValueCell("cash", b.whoreCosts, {forceNeg: true, showSign: true}),
			H.makeEmptyCell()
		]);
		if (showDetails) {
			const slaveStats = H.makeSlaveStatsTable(stats, "Fuckmeat details", ["Fuckmeat", "Customers", "Revenue", "Expenses", "Net Income", "Rep. Change"]);
			for (const record of b.income.values()) {
				const revenue = record.income + record.adsIncome;
				const netIncome = revenue - record.cost;
				H.addValueRow(slaveStats, H.makeSlaveLabel(record.slaveName, record.customLabel), [
					H.makeCustomersCell(record.customers),
					H.makeValueCell("cash", revenue),
					H.makeValueCell("cash", record.cost, {forceNeg: true}),
					H.makeValueCell("cash", netIncome),
					H.makeValueCell("reputation", record.rep, {showSign: true})
				]);
			}
		}
		H.addValueRow(stats, "Arcade maintenance", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.maintenance, {forceNeg: true}),
			H.makeValueCell("cash", b.maintenance, {forceNeg: true, showSign: true}),
			H.makeEmptyCell()
		]);
		H.addValueRow(stats, "Total", [
			H.makeValueCell("cash", b.totalIncome),
			H.makeValueCell("cash", b.totalExpenses, {forceNeg: true}),
			H.makeValueCell("cash", b.profit, {bold: true}),
			H.makeValueCell("reputation", b.rep, {showSign: true, bold: true})
		], true);

		return stats;
	}

	return output;
})();

App.Facilities.Dairy.Stats = (function() {
	const H = new App.Facilities.StatsHelper();
	const assureList = [ "whoreIncome", "whoreCosts", "maintenance", "totalIncome", "totalExpenses", "profit" ];

	/** Generate the arcade statistics table
	 * @param {boolean} showDetails
	 * @returns {HTMLElement|DocumentFragment}
	 */
	function output(showDetails) {
		if (!V.showEconomicDetails) {
			return document.createDocumentFragment();
		} else if (!V.facility || !V.facility.dairy) {
			return App.UI.DOM.makeElement("h4", `- No statistics for ${V.dairyName} gathered this week -`);
		}

		const b = V.facility.dairy;
		for (const prop of assureList) {
			b[prop] = b[prop] || 0;
		}

		const stats = H.makeStatsTable(["Revenue", "Expenses", "Net Income", "Rep. Change"]);
		H.addValueRow(stats, "Total cow income", [
			H.makeValueCell("cash", b.whoreIncome),
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreIncome),
			H.makeEmptyCell(),
		]);
		H.addValueRow(stats, "Total cow living costs", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreCosts, {forceNeg: true}),
			H.makeValueCell("cash", b.whoreCosts, {forceNeg: true, showSign: true}),
			H.makeEmptyCell()
		]);
		if (showDetails) {
			const slaveStats = H.makeSlaveStatsTable(stats, "Cow details", ["Cow", "Milk/Cum/Fluids", "Revenue", "Expenses", "Net Income", "Rep. Change"]);
			for (const record of b.income.values()) {
				const netIncome = record.income - record.cost;
				H.addValueRow(slaveStats, H.makeSlaveLabel(record.slaveName, record.customLabel), [
					H.makeProductionCell(record.milk, record.cum, record.fluid),
					H.makeValueCell("cash", record.income),
					H.makeValueCell("cash", record.cost, {forceNeg: true}),
					H.makeValueCell("cash", netIncome),
					H.makeEmptyCell()
				]);
			}
		}
		H.addValueRow(stats, "Dairy maintenance", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.maintenance, {forceNeg: true}),
			H.makeValueCell("cash", b.maintenance, {forceNeg: true, showSign: true}),
			H.makeEmptyCell()
		]);
		H.addValueRow(stats, "Total", [
			H.makeValueCell("cash", b.totalIncome),
			H.makeValueCell("cash", b.totalExpenses, {forceNeg: true}),
			H.makeValueCell("cash", b.profit, {bold: true}),
			H.makeEmptyCell()
		], true);

		return stats;
	}

	return output;
})();

App.Facilities.Farmyard.Stats = (function() {
	const H = new App.Facilities.StatsHelper();
	const assureList = [ "whoreIncome", "whoreCosts", "maintenance", "totalIncome", "totalExpenses", "food", "profit" ];

	/** Generate the Farmyard statistics table
	 * @param {boolean} showDetails
	 * @returns {HTMLElement|DocumentFragment}
	 */
	function output(showDetails) {
		if (!V.showEconomicDetails) {
			return document.createDocumentFragment();
		} else if (!V.facility || !V.facility.farmyard) {
			return App.UI.DOM.makeElement("h4", `- No statistics for ${V.farmyardName} gathered this week -`);
		}

		const b = V.facility.farmyard;
		for (const prop of assureList) {
			b[prop] = b[prop] || 0;
		}

		const stats = H.makeStatsTable(["Revenue", "Expenses", "Food", "Net Income", "Rep. Change"]);
		H.addValueRow(stats, "Total farmhand income", [
			H.makeValueCell("cash", b.whoreIncome),
			H.makeEmptyCell(),
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreIncome),
			H.makeEmptyCell(),
		]);
		H.addValueRow(stats, "Total farmhand living costs", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreCosts, {forceNeg: true}),
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.whoreCosts, {forceNeg: true, showSign: true}),
			H.makeEmptyCell()
		]);
		H.addValueRow(stats, "Total food produced", [
			H.makeEmptyCell(),
			H.makeEmptyCell(),
			H.makeValueCell("food", b.food),
			H.makeValueCell("food", b.food),
			H.makeEmptyCell()
		]);
		if (showDetails) {
			const slaveStats = H.makeSlaveStatsTable(stats, "Farmhand details", ["Farmhand", "Revenue", "Expenses", "Food", "Net Income", "Rep. Change"]);
			for (const record of b.income.values()) {
				const netIncome = record.income - record.cost;
				H.addValueRow(slaveStats, H.makeSlaveLabel(record.slaveName, record.customLabel), [
					H.makeValueCell("cash", record.income),
					H.makeValueCell("cash", record.cost, {forceNeg: true}),
					H.makeValueCell("food", record.food),
					H.makeValueCell("cash", netIncome),
					H.makeEmptyCell()
				]);
			}
		}
		H.addValueRow(stats, "Farmyard maintenance", [
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.maintenance, {forceNeg: true}),
			H.makeEmptyCell(),
			H.makeValueCell("cash", b.maintenance, {forceNeg: true, showSign: true}),
			H.makeEmptyCell()
		]);
		H.addValueRow(stats, "Total", [
			H.makeValueCell("cash", b.totalIncome),
			H.makeValueCell("cash", b.totalExpenses, {forceNeg: true}),
			H.makeValueCell("food", b.food),
			H.makeValueCell("cash", b.profit, {bold: true}),
			H.makeEmptyCell()
		], true);

		return stats;
	}

	return output;
})();


code:
App.UI.analyzePCPregnancy = function() {
	const node = new DocumentFragment();

	if (lastVisited("Incubator") === 1) {
		V.storedLink = "Incubator";
	} else if (lastVisited("Manage Personal Affairs") === 1) {
		V.storedLink = "Manage Personal Affairs";
	}
	V.nextButton = "Continue";
	V.nextLink = V.storedLink;

	const WL = V.PC.womb.length;
	const incubatorReservations = WombReserveCount(V.PC, "incubator");
	const nurseryReservations = WombReserveCount(V.PC, "nursery");
	const freeTanks = V.incubator.capacity - V.incubator.tanks.length;
	const freeCribs = V.nursery - V.cribs.length;
	const reservedChildren = FetusGlobalReserveCount("incubator");
	const reservedChildrenNursery = FetusGlobalReserveCount("nursery");
	let linkArray = [];

	if (WL === 0) {
		setTimeout(() => Engine.play("Manage Personal Affairs"), Engine.minDomActionDelay);
	} else {
		App.UI.DOM.appendNewElement("p", node, `You make yourself comfortable and prep your stomach for examination.`, "scene-intro");

		App.Events.addParagraph(node, [App.Desc.Player.analyzePreg()]);

		App.UI.DOM.appendNewElement("h2", node, "Furthest developed pregnancy");
		App.UI.DOM.appendNewElement("p", node, `Fetal development week: ${V.PC.preg}`);

		let p = App.UI.DOM.appendNewElement("p", node);
		if (V.incubator.capacity > 0 || V.nursery > 0) {
			let div = App.UI.DOM.appendNewElement("div", p);
			if (V.incubator.capacity > 0) {
				if (incubatorReservations > 0) {
					linkArray.push(App.UI.DOM.link(
						`Remove all of your children from ${V.incubator.name}`,
						() => {
							WombChangeReserveType(V.PC, "incubator", "");
						},
						[],
						"Analyze PC Pregnancy"
					));
				}
				if (incubatorReservations < WL && (reservedChildren + WL - incubatorReservations <= freeTanks)) {
					linkArray.push(App.UI.DOM.link(
						`Keep all of your children in ${V.incubator.name}`,
						() => {
							WombChangeReserveType(V.PC, "nursery", "incubator");
							WombChangeReserveType(V.PC, "", "incubator");
						},
						[],
						"Analyze PC Pregnancy"
					));
				} else if (incubatorReservations < WL) {
					App.UI.DOM.appendNewElement("span", div, `There is not enough free space in ${V.incubator.name} for the rest of your children.`);
				}
				div.append(App.UI.DOM.generateLinksStrip(linkArray));
			}

			linkArray = [];
			div = App.UI.DOM.appendNewElement("div", p);
			if (V.nursery > 0) {
				if (nurseryReservations > 0) {
					linkArray.push(App.UI.DOM.link(
						`Remove all of your children from ${V.nurseryName}`,
						() => {
							WombChangeReserveType(V.PC, "nursery", "");
						},
						[],
						"Analyze Pregnancy"
					));
				}
				if (nurseryReservations < WL && (reservedChildrenNursery + WL - nurseryReservations <= freeCribs)) {
					linkArray.push(App.UI.DOM.link(
						`Keep all of your children in ${V.nurseryName}`,
						() => {
							WombChangeReserveType(V.PC, "incubator", "nursery");
							WombChangeReserveType(V.PC, "", "nursery");
						},
						[],
						"Analyze Pregnancy"
					));
				} else if (nurseryReservations < WL) {
					App.UI.DOM.appendNewElement("span", div, `There is not enough free space in ${V.nurseryName} for the rest of your children.`);
				}
				div.append(App.UI.DOM.generateLinksStrip(linkArray));
			}
		}
		App.UI.DOM.appendNewElement("h2", node, "Deep scan");
		App.UI.DOM.appendNewElement("p", node, analyzePregnancies(V.PC, false));
	}
	return node;
};