code:
/*
receiver - What is the state of the network box.
	-1: PC has not been given a welcome packet. (default)
	0: PC has been given a welcome packet but has not installed it.
	1+: box is installed.
channel[numberAsString] - how many times it has been viewed.
channel.last - program viewed last week.
pcViewership.count - How many weeks since the PC last watched FCTV.
pcViewership.frequency - How often should the PC watch FCTV.
	-1: Never.
	1: at least once a week.
	2: least every two weeks.
	4: at least once a month. (default)
remote - Does the PC have a FCTV branded remote (also used to trigger a slave acquisition event).
weekEnabled - The week FCTV was installed.
*/
globalThis.FCTV = (function() {
	return {
		channels: channels,
		initChannels: initChannels,
		channelCount: channelCount,
		FinalTouches: FinalTouches,
	};

	/** Produce an array of numbers representing all available channels in App.Data.FCTV.channels
	 * @returns {number[]}
	 */
	function channels() {
		return Object.keys(App.Data.FCTV.channels).map(chStr => parseInt(chStr, 10));
	}

	function initChannels() {
		const channelList = FCTV.channels();
		for (let i = 0; i < channelList.length; i++) {
			V.FCTV.channel[num(channelList[i], true)] = 0;
		}
	}

	/**
	 * checks value of selected channel in relation to an arbitrary number. Have we watched it more/less/same than i.
	 * @param {number} selectedChannel
	 * @param {number} i
	 * @param {string} operation eq/gt/lt: equals/greater than/less than.
	 * @returns {boolean}
	 */
	function channelCount(selectedChannel, i, operation = 'eq') {
		if (operation === 'eq') {
			if (V.FCTV.channel[num(selectedChannel, true)] === i) {
				return true;
			}
		} else if (operation === 'gt') {
			if (V.FCTV.channel[num(selectedChannel, true)] > i) {
				return true;
			}
		} else if (operation === 'lt') {
			if (V.FCTV.channel[num(selectedChannel, true)] < i) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Applies some universal changes to FCTV slaves
	 * @param {App.Entity.SlaveState} slave
	 * @returns {App.Entity.SlaveState}
	 */
	function FinalTouches(slave) {
		slave.pubertyXX = 1;
		slave.career = "a slave";
		slave.origin = "You purchased $him from FCTV's Home Slave Shopping stream channel.";
		setHealth(slave, 80, 0, 0, 0, 0);
		return slave;
	}
})();


code:
// @ts-nocheck
App.Update.FCTV = function() {
	function convert(a, b) {
		return V.FCTVreceiver ? a : b;
	}

	function initChannels() {
		const channelList = FCTV.channels();
		for (let i = 0; i < channelList.length; i++) {
			const channel = num(channelList[i], true);
			const currentChannel = 'show' + capFirstChar(channel);
			V.FCTV.channel[channel] = convert(V[currentChannel], V.FCTV.channel[channel]) || 0;
		}
	}

	V.FCTV = V.FCTV || {};
	V.FCTV.receiver = V.FCTV.receiver > -1 ? V.FCTV.receiver : -1;
	if (convert && V.receiverAvailable) {
		V.FCTV.receiver = V.FCTVreceiver > 0 ? V.FCTVreceiver : 0;
	}
	V.FCTV.pcViewership = V.FCTV.pcViewership || {};
	V.FCTV.channel = V.FCTV.channel || {};
	initChannels();
	V.FCTV.channel.last = convert(V.lastShow, V.FCTV.channel.last) || -1;

	V.FCTV.pcViewership.count = convert(V.FCTVcount, V.FCTV.pcViewership.count) || 0;
	V.FCTV.pcViewership.frequency = convert(V.FCTVrate, V.FCTV.pcViewership.frequency) || 4;
	V.FCTV.remote = convert(V.FCTVremote, V.FCTV.remote) || 0;

	if (V.FCTVreceiver && !V.FCTV.weekEnabled) {
		V.FCTV.weekEnabled = V.receiverAvailable > 1 ? V.receiverAvailable : 0;
	}

	if (V.FCTV.channel.hasOwnProperty("selected")) {
		delete V.FCTV.channel.selected;
	}
};

repo_name: Fanatey/fc

license: mit


code:
button.selectedslave { background-color: #b84; }
table.slave-genetic-details { width: 100%; margin: 0 10px; border: 1px solid white; table-layout: fixed; font-size: 85%; border-collapse: separate; padding: 2px; }
table.slave-genetic-details tr { padding: 0 3px; }
table.slave-genetic-details td { vertical-align: top; border: 1px solid black; padding: 0 3px; }
table.slave-genetic-details th { vertical-align: top; background-color: rgba(127, 127, 127, 0.2); padding: 0 3px; }
table.slave-genetic-details td.editor { border: 1px solid #777; }
table.slave-genetic-details td.editor:hover { border: 1px solid #8cf; transition-duration: 0.3s; }
table.slave-genetic-details td.editor input { width: calc(100% + 8px); height: 100%; min-width: 0; margin: 0 -4px; padding: 4px 5px; box-sizing: border-box; }
table.slave-genetic-details td.editor select { width: calc(100% + 8px); height: 100%; min-width: 0; margin: 0 -4px; padding: 3px 5px; box-sizing: border-box; border: 1px solid #777; }
table.slave-genetic-details td.editor select:hover { border: 1px solid #8cf; transition-duration: 0.3s; }


code:
App.UI.editGenetics = function() {
	const node = new DocumentFragment();
	const cupCat = new Categorizer(
		[0, 'flat'],
		[300, 'A-cup'],
		[400, 'B-cup'],
		[500, 'C-cup'],
		[650, 'D-cup'],
		[800, 'DD-cup'],
		[1000, 'F-cup'],
		[1200, 'G-cup'],
		[1400, 'H-cup'],
		[1600, 'I-cup'],
		[1800, 'J-cup'],
		[2050, 'K-cup'],
		[2300, 'L-cup'],
		[2600, 'M-cup'],
		[2900, 'N-cup'],
		[3250, 'O-cup'],
		[3600, 'P-cup'],
		[3950, 'Q-cup'],
		[4300, 'R-cup'],
		[4700, 'S-cup'],
		[5100, 'massive']
	);
	const faceCat = new Categorizer(
		[-Infinity, 'very ugly'],
		[-95, 'ugly'],
		[-40, 'unattractive'],
		[-10, 'attractive'],
		[11, 'very pretty'],
		[41, 'gorgeous'],
		[96, 'mind blowing']
	);
	const lipsCat = new Categorizer(
		[0, 'thin'],
		[11, 'normal'],
		[21, 'pretty'],
		[41, 'plush'],
		[71, 'huge'],
		[96, 'facepussy']
	);
	const hormonalCat = new Categorizer(
		[-Infinity, 'heavy male'],
		[-49, 'male'],
		[-24, 'natural'],
		[25, 'female'],
		[50, 'heavy female']
	);
	const intelligenceCat = new Categorizer(
		[-Infinity, 'borderline retarded'],
		[-95, 'very slow'],
		[-50, 'slow'],
		[-15, 'average'],
		[16, 'smart'],
		[51, 'very smart'],
		[96, 'brilliant']
	);
	function birthFullName(s) {
		let r = [];
		if (V.surnameOrder !== 1) {
			if (["Cambodian", "Chinese", "Ancient Chinese Revivalist", "Hungarian", "Japanese", "Edo Revivalist", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(s.nationality)) {
				if (s.birthSurname || s.surname) {
					r.push(s.birthSurname || s.surname);
				}
				r.push(App.UI.DOM.makeElement("span", (s.birthName || s.name), ["bold", "underline"]));
			} else {
				r.push(App.UI.DOM.makeElement("span", (s.birthName || s.name), ["bold", "underline"]));
				if (s.birthSurname || s.surname) {
					r.push(s.birthSurname || s.surname);
				}
			}
		} else {
			r.push(App.UI.DOM.makeElement("span", (s.birthName || s.name), ["bold", "underline"]));
			if (s.birthSurname || s.surname) {
				r.push(s.birthSurname || s.surname);
			}
		}
		return App.Events.makeNode(r);
	}
	function currentFullName(s) {
		let r = [];
		if (V.surnameOrder !== 1) {
			if (["Cambodian", "Chinese", "Ancient Chinese Revivalist", "Hungarian", "Japanese", "Edo Revivalist", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(s.nationality)) {
				if (s.slaveSurname) {
					r.push(s.slaveSurname);
				}
				r.push(App.UI.DOM.makeElement("span", s.slaveName, ["bold", "underline"]));
			} else {
				r.push(App.UI.DOM.makeElement("span", s.slaveName, ["bold", "underline"]));
				if (s.slaveSurname) {
					r.push(s.slaveSurname);
				}
			}
		} else {
			r.push(App.UI.DOM.makeElement("span", s.slaveName, ["bold", "underline"]));
			if (s.slaveSurname) {
				r.push(s.slaveSurname);
			}
		}
		return App.Events.makeNode(r);
	}
	function nameButton(s) {
		const button = App.UI.DOM.makeElement("button", birthFullName(s), "slavepicker");
		button.setAttribute("data-slave", s.ID);
		button.style.width = "100%";
		return button;
	}
	function geneDetailsFunction(s) {
		function makeRow() {
			row = App.UI.DOM.appendNewElement("tr", tbody);
		}
		function makeCell(text, colSpan) {
			let cell = App.UI.DOM.appendNewElement("td", row, text);
			if (colSpan) {
				cell.colSpan = colSpan;
			}
		}
		function makeHeader(text) {
			App.UI.DOM.appendNewElement("th", row, text);
		}

		function makeHr() {
			makeRow();
			row.append(document.createElement("td"));
			cell = App.UI.DOM.appendNewElement("td", row, App.UI.DOM.makeElement("hr"));
			cell.colSpan = 6;
			row.append(document.createElement("td"));
		}

		const table = document.createElement("table");
		table.classList.add("slave-genetic-details");
		const tbody = App.UI.DOM.appendNewElement("tbody", table);

		let row = document.createElement("tr");
		let cell;
		makeRow();

		makeHeader(`Given name(s)`);
		makeCell(s.birthName, 2);

		makeHeader(`Family name`);
		makeCell(s.birthSurname || '', 2);

		makeHeader(`Karyotype`);
		cell = App.UI.DOM.appendNewElement("td", row, `${s.genes} (${toSex(s.genes)})`, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "genes");
		cell.setAttribute("data-choices", "XX, XY");

		makeRow();

		makeHeader(`Nationality`);
		cell = App.UI.DOM.appendNewElement("td", row, s.nationality, ["editor", "string-editor"]);
		cell.colSpan = 2;
		cell.setAttribute("data-param", "nationality");

		makeHeader(`Ethnicity`);
		cell = App.UI.DOM.appendNewElement("td", row, s.origRace, ["editor", "choice-editor"]);
		cell.colSpan = 2;
		cell.setAttribute("data-param", "origRace");
		cell.setAttribute("data-choices", "amerindian, asian, black, indo-aryan, latina, malay, middle eastern, mixed race, pacific islander, semitic, southern european, catgirl, white");

		makeHeader(`Career`);
		cell = App.UI.DOM.appendNewElement("td", row, s.career ? s.career : '', ["note"]);

		makeRow();

		makeHeader(`Father`);
		makeCell(parentFullName(s.father), 2);

		makeHeader(`Mother`);
		makeCell(parentFullName(s.mother), 2);

		makeHeader(`Age`);
		makeCell(ageDesc(s));

		if (s.birthName !== s.slaveName || s.birthSurname !== s.slaveSurname) {
			makeRow();

			cell = App.UI.DOM.appendNewElement("td", row, App.Events.makeNode([`Currently called`, currentFullName(s)]), ["note"]);
			cell.colSpan = 8;
		}

		makeHr();

		makeRow();

		makeHeader(`Skin`);
		cell = App.UI.DOM.appendNewElement("td", row, s.origSkin, ["editor", "string-editor"]);
		cell.setAttribute("data-param", "origSkin");

		makeHeader(`Skin markings`);
		cell = App.UI.DOM.appendNewElement("td", row, s.origSkin, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "markings");
		cell.setAttribute("data-choices", "none, beauty mark, birthmark, freckles, heavily freckled");

		makeHeader(`Eyes`);
		cell = App.UI.DOM.appendNewElement("td", row, s.eye.origColor, ["editor", "string-editor"]);
		cell.setAttribute("data-param", "eye.origColor");

		makeHeader(`Hair`);
		cell = App.UI.DOM.appendNewElement("td", row, s.origHColor, ["editor", "string-editor"]);
		cell.setAttribute("data-param", "origHColor");

		makeRow();

		makeHeader(`Height`);
		cell = App.UI.DOM.appendNewElement("td", row, `${s.height} cm (${toFeet(s.height)})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "height");
		cell.setAttribute("data-min", "50");
		cell.setAttribute("data-max", "250");

		makeHeader(`Weight`);
		cell = App.UI.DOM.appendNewElement("td", row, percent(s.weight), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "weight");

		makeHeader(`Muscles`);
		cell = App.UI.DOM.appendNewElement("td", row, percent(s.muscles), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "muscles");

		cell = App.UI.DOM.appendNewElement("td", row);
		cell.colSpan = 2;

		makeRow();

		makeHeader(`Shoulders`);
		cell = App.UI.DOM.appendNewElement("td", row, shouldersDesc(s.shoulders), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "shoulders");
		cell.setAttribute("data-min", "-2");
		cell.setAttribute("data-max", "2");

		makeHeader(`Waist`);
		cell = App.UI.DOM.appendNewElement("td", row, percent(s.waist), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "waist");

		makeHeader(`Hips`);
		cell = App.UI.DOM.appendNewElement("td", row, hipsDesc(s.hips), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "hips");
		cell.setAttribute("data-min", "-2");
		cell.setAttribute("data-max", "3");

		makeHeader(`Rear`);
		cell = App.UI.DOM.appendNewElement("td", row, buttDesc(s.butt), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "butt");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "20");

		makeRow();

		makeHeader(`Breasts`);
		cell = App.UI.DOM.appendNewElement("td", row, `${s.boobs} cc (${cupCat.cat(s.boobs)})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "boobs");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "100000");

		makeHeader(`Breast shape`);
		cell = App.UI.DOM.appendNewElement("td", row, s.boobShape, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "boobShape");
		cell.setAttribute("data-choices", "normal, perky, saggy, torpedo-shaped, downward-facing, wide-set");

		makeHeader(`Nipples`);
		cell = App.UI.DOM.appendNewElement("td", row, s.nipples, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "nipples");
		cell.setAttribute("data-choices", "huge, puffy, inverted, tiny, cute, partially inverted");

		makeHeader(`Areolae`);
		cell = App.UI.DOM.appendNewElement("td", row, areolaeDesc(s.areolae), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "areolae");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "5");

		makeRow();

		makeHeader(`Face`);
		cell = App.UI.DOM.appendNewElement("td", row, `${faceCat.cat(s.face)} (${percent(s.face)})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "face");

		makeHeader(`Lips`);
		cell = App.UI.DOM.appendNewElement("td", row, `${lipsCat.cat(s.lips)} (${s.lips})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "lips");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "100");

		makeHeader(`Teeth`);
		cell = App.UI.DOM.appendNewElement("td", row, s.teeth, ["editor", "choice-editor"]);
		cell.setAttribute("data-param", "teeth");
		cell.setAttribute("data-choices", "normal, crooked, straightening braces, cosmetic braces, removable, pointy, baby, mixed");

		makeHeader(`Voice`);
		cell = App.UI.DOM.appendNewElement("td", row, voiceDesc(s.voice), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "voice");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "3");

		makeHr();

		if (s.vagina >= 0) {
			makeRow();

			makeHeader(`Labia`);
			cell = App.UI.DOM.appendNewElement("td", row, labiaDesc(s.labia), ["editor", "number-editor"]);
			cell.setAttribute("data-param", "labia");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "3");

			makeHeader(`Clitoris`);
			cell = App.UI.DOM.appendNewElement("td", row, clitDesc(s.clit), ["editor", "number-editor"]);
			cell.setAttribute("data-param", "clit");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "5");

			makeHeader(`Ovaries`);
			cell = App.UI.DOM.appendNewElement("td", row,
				s.ovaries
					? App.Events.makeNode(['working', App.UI.DOM.makeElement("span", "\u2714", "green")])
					: App.Events.makeNode(['broken', App.UI.DOM.makeElement("span", "\u2718", "red")])
			);
			cell.setAttribute("data-param", "ovaries");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "5");

			makeHeader(`Ovum type`);
			makeCell(s.eggType);
		}
		if (s.dick > 0 || s.balls > 0) {
			makeRow();

			makeHeader(`Penis`);
			cell = App.UI.DOM.appendNewElement("td", row, penisDesc(s.dick), ["editor", "number-editor"]);
			cell.setAttribute("data-param", "dick");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "11");

			makeHeader(`Testes`);
			cell = App.UI.DOM.appendNewElement("td", row, testesDesc(s.balls), ["editor", "number-editor"]);
			cell.setAttribute("data-param", "balls");
			cell.setAttribute("data-min", "0");
			cell.setAttribute("data-max", "10");

			makeHeader(`Prostate`);
			cell = App.UI.DOM.appendNewElement("td", row,
				s.prostate
					? App.Events.makeNode(['working', App.UI.DOM.makeElement("span", "\u2714", "green")])
					: App.Events.makeNode(['broken', App.UI.DOM.makeElement("span", "\u2718", "red")]),
			);
			cell.setAttribute("data-param", "prostate");

			makeHeader(`Sperm type`);
			makeCell(s.ballType);
		}

		makeRow();

		makeHeader(`Hormonal`);
		cell = App.UI.DOM.appendNewElement("td", row, `${hormonalCat.cat(s.hormoneBalance)} (${s.hormoneBalance})`, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "hormoneBalance");

		makeHeader(`Anal area`);
		cell = App.UI.DOM.appendNewElement("td", row, s.analArea, ["editor", "number-editor"]);
		cell.setAttribute("data-param", "analArea");
		cell.setAttribute("data-min", "0");
		cell.setAttribute("data-max", "3");

		cell = App.UI.DOM.appendNewElement("td", row);
		cell.colSpan = 4;

		makeHr();

		makeRow();

		makeHeader(`Intelligence`);
		cell = App.UI.DOM.appendNewElement("td", row, intelligenceCat.cat(s.intelligence), ["editor", "number-editor"]);
		cell.setAttribute("data-param", "intelligence");
		cell.setAttribute("data-min", "-100");
		cell.setAttribute("data-max", "100");

		makeHeader(`Behavioral`);
		cell = App.UI.DOM.appendNewElement("td", row, s.behavioralFlaw !== 'none' ? s.behavioralFlaw : s.behavioralQuirk);

		makeHeader(`Sexual`);
		cell = App.UI.DOM.appendNewElement("td", row, s.sexualFlaw !== 'none' ? s.sexualFlaw : s.sexualQuirk);

		cell = App.UI.DOM.appendNewElement("td", row);
		cell.colSpan = 2;

		if (s.chem > 0 || s.addict > 0) {
			makeRow();
			makeCell(chemicalsDesc(s), 8);
		}
		return table;
	}
	function percent(n) {
		if (n > 0) { return '+' + Number(n).toFixed(0) + '%'; }
		if (n === 0) { return '\u00B10%'; }
		return Number(n).toFixed(0) + '%';
	}
	function toFeet(n) {
		let realFeet = Number(n) / 30.48;
		let feet = Math.floor(realFeet);
		let inches = Math.round((realFeet - feet) * 12);
		return feet + "\u2032" + inches + '\u2033';
	}
	function toSex(karyotype) {
		return {
			XX: 'female',
			XY: 'male',
			X: 'Turner syndrome female',
			X0: 'Turner syndrome female',
			XYY: 'XYY syndrome male',
			XXY: 'Klinefelter syndrome male',
			XXX: 'triple X syndrome female'
		}[String(karyotype).toUpperCase()] || 'unknown/not viable';
	}
	function ageDesc(slave) {
		let age = slave.actualAge + (slave.birthWeek + V.week - slave.weekAcquired) / 52.0;
		let years = Math.floor(age);
		let months = Math.floor((age - years) * 12);
		return (years > 0 ? years + 'y ' + months + 'm' : months + 'm');
	}
	function shouldersDesc(s) {
		return ({
			'-2': 'very narrow',
			'-1': 'narrow',
			'0': 'feminine',
			'1': 'broad',
			'2': 'very broad'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function hipsDesc(s) {
		return ({
			'-2': 'very narrow',
			'-1': 'narrow',
			'0': 'normal',
			'1': 'wide',
			'2': 'very wide',
			'3': 'inhumanly wide'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function buttDesc(s) {
		return ({
			'0': 'flat',
			'1': 'small',
			'2': 'plump',
			'3': 'big',
			'4': 'huge',
			'5': 'enormous',
			'6': 'gigantic',
			'7': 'ridiculous',
			'8': 'immense',
			'9': 'immense',
			'10': 'immense',
			'11': 'inhuman',
			'12': 'inhuman',
			'13': 'inhuman',
			'14': 'inhuman',
			'15': 'inhuman',
			'16': 'inhuman',
			'17': 'inhuman',
			'18': 'inhuman',
			'19': 'inhuman',
			'20': 'inhuman'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function areolaeDesc(s) {
		return ({
			'0': 'normal',
			'1': 'large',
			'2': 'unusually wide',
			'3': 'huge',
			'4': 'heart shaped',
			'5': 'star shaped'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function voiceDesc(s) {
		return ({
			'0': 'mute',
			'1': 'deep',
			'2': 'feminine',
			'3': 'high'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function labiaDesc(s) {
		return ({
			'0': 'minimal',
			'1': 'big',
			'2': 'huge',
			'3': 'huge dangling'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function clitDesc(s) {
		return ({
			'0': 'normal',
			'1': 'large',
			'2': 'huge',
			'3': 'enormous',
			'4': 'penis-like',
			'5': 'like a massive penis'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function penisDesc(s) {
		return ({
			'0': 'none',
			'1': 'tiny',
			'2': 'little',
			'3': 'normal',
			'4': 'big',
			'5': 'huge',
			'6': 'gigantic',
			'7': 'gigantic',
			'8': 'titanic',
			'9': 'absurd',
			'10': 'inhuman',
			'11': 'hypertrophied'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function testesDesc(s) {
		return ({
			'0': 'none',
			'1': 'vestigial',
			'2': 'small',
			'3': 'average',
			'4': 'big',
			'5': 'huge',
			'6': 'enormous',
			'7': 'hypertrophied', '8': 'hypertrophied', '9': 'hypertrophied', '10': 'hypertrophied'
		}[s] || 'unknown') + ' (' + Number(s) + ')';
	}
	function chemicalsDesc(slave) {
		let res = [];
		if (slave.chem > 0) {
			res.push('Immune system damaged (' + slave.chem.toFixed(0) + ').');
		}
		if (slave.addict > 0) {
			res.push('Addicted to aphrodisiacs (' + slave.addict.toFixed(0) + ').');
		}
		return res.join(' ');
	}
	function parentFullName(id) {
		if (id === 0) {
			return App.UI.DOM.makeElement("span", "unknown", "note");
		}
		if (id === -1) {
			/* The PC */
			return birthFullName(V.PC) + ' (PC)';
		} else {
			let parent = V.genePool.find(function(s) {
				return s.ID === id;
			});
			return parent
				? birthFullName(parent)
				: App.Events.makeNode([
					App.UI.DOM.makeElement("span", "missing", "note"),
					`(${id})`
				]);
		}
	}

	let outerDiv = App.UI.DOM.appendNewElement("div", node);
	outerDiv.style.position = "relative";
	outerDiv.style.width = "100%";
	outerDiv.style.height = "100%";

	let innerDiv = App.UI.DOM.appendNewElement("div", outerDiv);
	innerDiv.id = "slaveList";
	innerDiv.style.width = "20em";
	innerDiv.style.float = "left";

	innerDiv = App.UI.DOM.appendNewElement("div", outerDiv);
	innerDiv.id = "geneDetails";
	innerDiv.style.position = "fixed";
	innerDiv.style.left = "0";
	innerDiv.style.right = "0";
	innerDiv.style.top = "0";
	innerDiv.style.bottom = "0";
	innerDiv.style.marginLeft = "40em";
	innerDiv.style.marginRight = "2.5em";
	innerDiv.style.marginTop = "2.5em";

	jQuery(function() {
		let slaveList = jQuery('#slaveList');
		_(V.genePool).sortBy(['birthName', 'birthSurname']).forEach(function(s) {
			slaveList.append(nameButton(s));
		});
		jQuery(document).off('.edit-genetics');
		jQuery(document).on('keyup.edit-genetics', function(e) {
			if (slaveList.is(':visible')) {
				let currentSelection = jQuery('button.selectedslave');
				switch (e.keyCode) {
					case 38: /* ArrowUp */
						if (currentSelection.length === 0) {
							slaveList.find('button').last().trigger('click');
						} else {
							currentSelection.prev().trigger('click');
						}
						break;
					case 40: /* ArrowDown */
						if (currentSelection.length === 0) {
							slaveList.find('button').first().trigger('click');
						} else {
							currentSelection.next().trigger('click');
						}
						break;
					default:
						/* do nothing */
						break;
				}
			}
		});
		let geneDetails = jQuery('#geneDetails');
		jQuery('.slavepicker').each(function() {
			let el = jQuery(this);
			let id = Number(el.attr('data-slave'));
			el.on('click', function() {
				jQuery('button.selectedslave').removeClass('selectedslave');
				el.addClass('selectedslave');
				let slave = V.genePool.find(function(s) { return s.ID === id; });
				geneDetails.html(geneDetailsFunction(slave));

				let numberEditorOpen = function() {
					let td = jQuery(this);
					td.off('click');
					let attr = String(td.attr('data-param'));
					let min = Number(td.attr('data-min')) || -100;
					let max = Number(td.attr('data-max')) || 100;
					let editableText = jQuery('<input type="number" min="' + min + '" max="' + max + '" />');
					editableText.val(Number(slave[attr]));
					editableText.keyup(function(e) {
						if (e.keyCode === 13) {
							slave[attr] = Number(jQuery(this).val());
							el.trigger('click');
						}
					});
					editableText.blur(function() {
						slave[attr] = Number(jQuery(this).val());
						el.trigger('click');
					});
					td.empty().append(editableText);
					editableText.focus();
				};
				geneDetails.find('td.number-editor').click(numberEditorOpen);
				let stringEditorOpen = function() {
					let td = jQuery(this);
					td.off('click');
					let attr = String(td.attr('data-param'));
					let editableText = jQuery('<input type="text" />');
					editableText.val(String(slave[attr]));
					editableText.keyup(function(e) {
						if (e.keyCode === 13) {
							slave[attr] = String(jQuery(this).val());
							el.trigger('click');
						}
					});
					editableText.blur(function() {
						slave[attr] = String(jQuery(this).val());
						el.trigger('click');
					});
					td.empty().append(editableText);
					editableText.focus();
				};
				geneDetails.find('td.string-editor').click(stringEditorOpen);
				let choiceEditorOpen = function() {
					let td = jQuery(this);
					td.off('click');
					let attr = String(td.attr('data-param'));
					let choices = String(td.attr('data-choices')).split(/,\s*/);
					let editableText = jQuery('<select>' + choices.map(ch => '<option>' + ch + '</option>').join('') + '</select>');
					editableText.val(String(slave[attr]));
					editableText.keyup(function(e) {
						if (e.keyCode === 13) {
							slave[attr] = String(jQuery(this).val());
						}
					});
					editableText.blur(function() {
						slave[attr] = String(jQuery(this).val());
					});
					td.empty().append(editableText);
					editableText.focus();
				};
				geneDetails.find('td.choice-editor').each(choiceEditorOpen);
			});
		});
	});

	return node;
};


code:
App.Interact.eliteBreedingExam = function(slave = null) {
	const frag = new DocumentFragment();
	const eliteAngered = V.failedElite > 100;
	const SlaveProfessionalismEstablished = V.arcologies[0].FSSlaveProfessionalism > 20;
	const cost = 5000;

	let r = [];

	if (!slave) {
		r.push(`A fee of <span class='yellowgreen'>${cashFormat(cost)}</span> will be deducted from your account for each slave be to examined.`);
	}
	r.push(
		`Updates will be posted periodically. It is your responsibility to keep up to date.`,
		`Failures will be sterilized. Please make sure your slave fits the following criteria before submitting them for testing. We shall not be held responsible for time wasted.`,
	);
	if (!slave) {
		r.push(`The slave must not be inside a fuckdoll suit, infertile or already pregnant.`);
	} else {
		r.push(`${slave.slaveName} is up for review:`);
	}
	App.Events.addNode(frag, r, "div");
	r = [];

	const table = App.UI.DOM.appendNewElement("table", frag, null, ["padded-columns"]);
	/** @type {[(slave: FC.SlaveState) => boolean, string][]} */
	const checks = [];

	checks.push(
		[
			slave => slave.intelligence + slave.intelligenceImplant >= (eliteAngered ? 95 : 50),
			`Must be ${eliteAngered ? 'highly intelligent' : 'very smart'}`
		],
		[
			slave => Beauty(slave) >= (eliteAngered ? 120 : 100),
			`Must be ${eliteAngered ? 'unbelievably' : ''} beautiful`
		],
		[
			slave => slave.face >= (eliteAngered ? 95 : 40),
			`Must have a ${eliteAngered ? 'breathtaking' : 'gorgeous'} face`
		],
		[
			slave => slave.chem <= 20,
			`Must not be overly impacted by drug use`
		],
		[
			slave => slave.addict === 0,
			`Must not be addicted to drugs`
		],
	);

	if (V.arcologies[0].FSMaturityPreferentialist > 20 || V.arcologies[0].FSYouthPreferentialist > 20) {
		const targetAge = 24;

		if (V.arcologies[0].FSMaturityPreferentialist > 20) {
			checks.push([
				slave => slave.physicalAge > targetAge,
				`Must be older than ${targetAge}`
			]);
		} else {
			checks.push([
				slave => slave.physicalAge <= targetAge,
				`Must be ${targetAge} or younger`
			]);
		}
	}

	if (V.arcologies[0].FSIntellectualDependency > 20) {
		checks.push([
			slave => slave.energy >= 96,
			`Must be sexually receptive at all times`
		]);
	} else if (V.arcologies[0].FSSlaveProfessionalism > 20) {
		checks.push(
			[
				slave => slave.skill.vaginal + slave.skill.anal + slave.skill.oral + slave.skill.entertainment + slave.skill.whoring >= (eliteAngered ? 500 : 400),
				`Must be capable of mastering skills`
			],
			[
				slave => slave.accent <= 1,
				`Must be capable of eloquent and accurate speech`
			],
		);
	}

	if (V.arcologies[0].FSPetiteAdmiration > 20 || V.arcologies[0].FSStatuesqueGlorification > 20) {
		const targetHeight = V.arcologies[0].FSPetiteAdmiration > 20 ? 150 : 186;
		if (V.arcologies[0].FSPetiteAdmiration > 20) {
			checks.push(
				[
					slave => (slave.height - (slave.heightImplant * 10)) < targetHeight,
					`Must naturally be very short (${targetHeight}cm)`
				],
				[
					slave => (slave.height - (slave.heightImplant * 10)) < Height.mean(slave) - 15,
					`Must naturally be very short for their age`
				],
			);
		} else if (V.arcologies[0].FSStatuesqueGlorification > 20) {
			checks.push([
				slave => (slave.height - (slave.heightImplant * 10)) > targetHeight,
				`Must naturally be very tall (${targetHeight}cm)`
			]);
		}
	}

	if (V.arcologies[0].FSSupremacist > 20 || V.arcologies[0].FSSubjugationist > 20) {
		const targetRace = V.arcologies[0].FSSupremacist > 20 ? V.arcologies[0].FSSupremacistRace : V.arcologies[0].FSSubjugationistRace;
		if (V.arcologies[0].FSSupremacist > 20) {
			checks.push([
				slave => slave.race === targetRace,
				`Must be ${targetRace}`
			]);
		} else {
			checks.push([
				slave => slave.race !== targetRace,
				`Must not be ${targetRace}`
			]);
		}
	}

	if (V.arcologies[0].FSGenderRadicalist > 20) {
		checks.push([
			slave => slave.dick >= 1 && slave.balls >= 1,
			`Must have a functional penis`
		]);
	} else if (V.arcologies[0].FSGenderFundamentalist > 20) {
		checks.push([
			slave => slave.dick === 0 && slave.balls === 0,
			`Must be physically female`
		]);
	}

	if (V.arcologies[0].FSPaternalist > 20) {
		checks.push([
			slave => slave.intelligenceImplant >= (SlaveProfessionalismEstablished ? 30 : 15),
			`Must be ${SlaveProfessionalismEstablished ? 'well' : ''} educated`
		]);
		checks.push([
			slave => slave.health.condition >= 60,
			`Must be in good health`
		]);
	}

	if (SlaveProfessionalismEstablished) {
		checks.push([
			slave => slave.intelligenceImplant >= 15,
			`Must be well educated`
		]);
	}

	if (V.arcologies[0].FSBodyPurist > 20) {
		checks.push([
			slave => slave.chem <= (eliteAngered ? 0 : 15),
			`Must have low carcinogen levels`
		]);
		checks.push([
			slave => slave.boobsImplant + slave.buttImplant + slave.lipsImplant + slave.hipsImplant + slave.shouldersImplant === 0 && slave.faceImplant <= 5 && slave.bellyImplant === -1,
			`Must be implant free`
		]);
	}

	if (V.arcologies[0].FSSlimnessEnthusiast > 20) {
		if (V.arcologies[0].FSHedonisticDecadence > 20) {
			checks.push([
				slave => slave.weight <= 30,
				`May be no larger than "plush"`
			]);
		} else {
			checks.push([
				slave => slave.weight <= 0,
				`Must be thin`
			]);
		}
		checks.push([
			slave => slave.butt <= 2,
			`Must have a trim rear`
		]);
		checks.push([
			slave => slave.boobs <= 400,
			`Must have a sleek chest`
		]);
	} else if (V.arcologies[0].FSAssetExpansionist > 20) {
		if (V.arcologies[0].FSTransformationFetishist > 20) {
			checks.push(
				[
					slave => slave.butt >= 6,
					`Must have a gigantic rear`
				],
				[
					slave => slave.boobs >= 1000,
					`Must be busty`
				],
			);
		} else {
			checks.push(
				[
					slave => slave.butt - slave.buttImplant >= 6,
					`Must have a naturally gigantic rear`
				],
				[
					slave => slave.boobs - slave.boobsImplant >= 1000,
					`Must be naturally busty`
				],
			);
		}
	}

	if (V.arcologies[0].FSPastoralist > 20) {
		checks.push([
			slave => slave.lactation === 1,
			`Must be lactating naturally`
		]);
	}
	if (V.arcologies[0].FSPhysicalIdealist > 20) {
		if (V.arcologies[0].FSPhysicalIdealistLaw === 1) {
			const musclesMin = 20;
			const musclesMax = 50;

			checks.push(
				[
					slave => slave.weight <= 30,
					`Must not be overweight`
				],
				[
					slave => slave.muscles.isBetween(musclesMin, musclesMax),
					`Must be fit, but not too muscular`
				],
			);
		} else {
			checks.push([
				slave => slave.muscles > 95,
				`Must be extremely muscular`
			]);
		}
	} else if (V.arcologies[0].FSHedonisticDecadence > 20) {
		const targetWeight = V.arcologies[0].FSSlimnessEnthusiast > 20 ? 10 : 95;

		if (V.arcologies[0].FSSlimnessEnthusiast > 20) {
			checks.push([
				slave => slave.weight > targetWeight,
				`Must be more than "trim"`
			]);
		} else {
			checks.push([
				slave => slave.weight > targetWeight,
				`Must be big, soft and fat`
			]);
		}
	}

	if (slave) {
		checks.forEach(check => {
			const tr = App.UI.DOM.appendNewElement("tr", table);

			App.UI.DOM.appendNewElement("td", tr, `• ${check[1]}`);

			if (!check[0](slave)) {
				App.UI.DOM.appendNewElement("td", tr, `FAILED`, ["red"]);
			} else {
				App.UI.DOM.appendNewElement("td", tr, `PASSED`, ["lime"]);
			}
		});

		if (checks.some(check => !check[0](slave))) {
			r.push(`The aforementioned slave has been deemed <span class="red">unsuitable</span> for breeding.`);

			slave.preg = -3;
		} else {
			r.push(`The aforementioned slave has been deemed <span class="green">worthy</span> of being used for breeding and has been marked as such. Please note the increased restrictions on breeding slaves.`);
			r.push(`They are not permitted for public use or anything that may harm their growing child. The child within them is considered a member of the Elite class and as such, any harm that comes to them will result in severe penalties to the breeder's owner. Development of the child will be closely monitored; should the fetus be identified as not of the owner's blood (or any other member of the Elite class), said owner shall face severe fines.`);

			slave.breedingMark = 1;
			slave.pregControl = "none";

			const job = App.Utils.jobForAssignment(slave.assignment);
			const consequences = [];
			if (slave.assignment === Job.BODYGUARD || (job && job.desc.publicSexUse)) {
				removeJob(slave, slave.assignment);
				consequences.push(`reassigned to <span class="green">rest</span>`);
			}
			if (V.pit && V.pit.fighterIDs.includes(slave.ID)) {
				removeJob(slave, Job.PIT);
				consequences.push(`<span class="yellow">removed</span> from ${V.pit.name}'s fighting roster`);
			}
			if (consequences.length > 0) {
				App.Events.addNode(frag, r, "div");
				r = [];
				const {He} = getPronouns(slave);
				r.push(`${He} has been automatically ${toSentence(consequences)}.`);
			}
		}

		cashX(-cost, "capEx");
	} else {
		checks.forEach(check => {
			const tr = App.UI.DOM.appendNewElement("tr", table);

			App.UI.DOM.appendNewElement("td", tr, check[1]);
		});
	}

	// hack to circumvent default SugarCube styling
	table.style.marginLeft = "1em";

	App.Events.addNode(frag, r, "div");

	return frag;
};


code:
App.UI.surrogacy = function() {
	const node = new DocumentFragment();
	const receptrix = getReceptrix();
	const donatrix = getDonatrix();
	const impregnatrix = getImpregnatrix();
	const wombIndex = V.wombIndex;
	const slave = getSlave(V.AS);
	let r = [];

	function getReceptrix() {
		if (V.receptrix && V.receptrix.ID) {
			return V.receptrix.ID === -1 ? V.PC : getSlave(V.receptrix.ID);
		}
	}

	function getDonatrix() {
		if (V.donatrix && V.donatrix.ID) {
			return V.donatrix.ID === -1 ? V.PC : getSlave(V.donatrix.ID);
		}
	}

	function getImpregnatrix() {
		if (V.impregnatrix && V.impregnatrix.ID) {
			if (V.impregnatrix.ID === -1) {
				return V.PC;
			} else {
				return getSlave(V.impregnatrix.ID) || V.incubator.tanks.find(t => t.ID === V.impregnatrix.ID);
			}
		}
	}

	switch (V.surgeryType) {
		case "surrogacy":
			if (receptrix.ID === -1) {
				r.push(`Since the surgery required only a local anesthetic, you remain fully aware of the procedure as the autosurgery carries it out. You slowly rise to your feet, a hand to your lower belly, appreciating the new life growing within you.`);
				V.PC.pregKnown = 1;
				WombSurrogate(V.PC, 1, donatrix, impregnatrix.ID, 1);
				WombNormalizePreg(V.PC);
			} else {
				receptrix.pregKnown = 1;
				WombSurrogate(receptrix, 1, donatrix, impregnatrix.ID, 1);
				WombNormalizePreg(receptrix);
				const {
					He,
					he, his, him
				} = getPronouns(receptrix);
				if (receptrix.fetish === "mindbroken") {
					r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
				} else if (receptrix.fetish === "pregnancy" && receptrix.fetishStrength > 60 && receptrix.fetishKnown === 1) {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. ${He} was overjoyed at the sight of the syringe containing ${his} future child emptying into ${his} womb.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
					}
					r.push(`${He} is  filled with joy over the life settling into ${his} womb and can't wait to see the result. ${He}'s so pleased that ${he} now trusts your plans for ${his} body.`);
					receptrix.trust += 4;
					receptrix.devotion += 10;
				} else if (receptrix.devotion > 50) {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. ${He} watched the syringe containing ${his} new pregnancy empty into ${his} womb with rapt attention.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
					}
					r.push(`${He}'s <span class="devotion inc">grateful</span> that you think ${him} worthy of carrying this child, and a little nervous about how ${he}'ll perform as a surrogate.`);
					receptrix.devotion += 4;
				} else if (receptrix.devotion >= -20) {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. From the syringe making contact with ${his} skin, to the egg's delivery into ${his} womb and ${his} subsequent impregnation, ${he} couldn't look away.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
					}
					r.push(`${He} understands the realities of ${his} life as a slave, so it isn't much of a shock. ${He} is <span class="trust dec">sensibly fearful</span> of your total power over ${his} body.`);
					receptrix.trust -= 10;
				} else {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. The moment ${he} realized what was happening, ${he} shut ${his} eyes tight, only opening them again as ${he} feels the slight tingle of the injector exiting ${his} lower abdomen.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
					}
					r.push(`${He} does not understand the realities of ${his} life as a slave at a core level, so ${he}'s terrified and angry that you have forced ${him} to bear this child, even more so as ${he} realizes ${he} doesn't know who the father is. ${He} is sensibly fearful of your total power over ${his} body and the future of the life ${he} now harbors within ${him}.`);
					receptrix.trust -= 15;
					receptrix.devotion -= 15;
				}
			}
			V.receptrix = 0;
			V.impregnatrix = 0;
			V.donatrix = 0;
			break;
		case "transplant":
			if (receptrix.ID === -1) {
				r.push(`Since the surgery required only a local anesthetic, you are very aware that you are now carrying ${slave.slaveName}'s child. You slowly rise to your feet, a hand to your lower belly, appreciating the new life growing within you.`);
				const fetus = WombRemoveFetus(slave, wombIndex);
				WombAddFetus(V.PC, fetus);
				V.PC.pregKnown = 1;
				V.PC.preg = WombMaxPreg(V.PC);
				slave.preg = WombMaxPreg(slave);
				WombNormalizePreg(V.PC);
				WombNormalizePreg(slave);
			} else {
				const fetus = WombRemoveFetus(donatrix, wombIndex);
				WombAddFetus(receptrix, fetus);
				receptrix.pregKnown = 1;
				receptrix.preg = WombMaxPreg(receptrix);
				donatrix.preg = WombMaxPreg(donatrix);
				WombNormalizePreg(receptrix);
				WombNormalizePreg(donatrix);
				if (donatrix.ID === -1) {
					V.PC = donatrix;
				}
				const {
					He,
					he, his, him
				} = getPronouns(receptrix);
				if (receptrix.fetish === "mindbroken") {
					r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
				} else if ((receptrix.fetish === "pregnancy") && (receptrix.fetishStrength > 60) && (receptrix.fetishKnown === 1)) {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. ${He} was overjoyed at the sight of the syringe containing ${his} future child emptying into ${his} womb.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
					}
					r.push(`${He} is  filled with joy over the life resettling into ${his} womb and can't wait to see the result. ${He}'s so pleased that ${he} now trusts your plans for ${his} body.`);
					receptrix.trust += 4;
					receptrix.devotion += 10;
				} else if (receptrix.devotion > 50) {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. ${He} watched the syringe containing ${his} new pregnancy empty into ${his} womb with rapt attention.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
					}
					r.push(`${He}'s <span class="devotion inc">grateful</span> that you think ${him} worthy of carrying this child in`);
					if (donatrix.ID === -1) {
						r.push(`your stead,`);
					} else {
						r.push(`lieu of ${donatrix.slaveName},`);
					}
					r.push(`and a little nervous about how ${he}'ll perform as a surrogate.`);
					receptrix.devotion += 4;
				} else if (receptrix.devotion >= -20) {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. From the syringe making contact with ${his} skin, to the egg's delivery into ${his} womb and ${his} subsequent impregnation, ${he} couldn't look away.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
					}
					r.push(`${He} understands the realities of ${his} life as a slave, so it isn't much of a shock. ${He} is sensibly fearful of your total power over ${his} body and how you are forcing`);
					if (donatrix.ID === -1) {
						r.push(`your pregnancy upon ${him}.`);
					} else {
						r.push(`${him} to carry what should have been ${donatrix.slaveName}'s burden.`);
					}
					receptrix.trust -= 10;
				} else {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. The moment ${he} realized what was happening, ${he} shut ${his} eyes tight, only opening them again as ${he} feels the slight tingle of the injector exiting ${his} lower abdomen.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
					}
					r.push(`${He} does not understand the realities of ${his} life as a slave at a core level, so ${he}'s <span class="devotion dec">terrified and angry</span> that you have forced ${him} to`);
					if (donatrix.ID === -1) {
						r.push(`carry your child instead of doing so yourself.`);
					} else {
						r.push(`bear this child in lieu of ${donatrix.slaveName}.`);
					}
					r.push(`${He} is <span class="trust dec">sensibly fearful</span> of your total power over ${his} body and who else's troubles may be levied upon ${him}.`);
					receptrix.trust -= 15;
					receptrix.devotion -= 15;
				}
			}
			V.receptrix = 0;
			V.donatrix = 0;
			V.wombIndex = 0;
			break;
		case "clone":
			if (receptrix.ID === -1) {
				r.push(`Since the surgery required only a local anesthetic, you remain fully aware of the procedure as the autosurgery carries it out. You slowly rise to your feet, a hand to your lower belly, appreciating the clone growing within you.`);
				V.PC.pregKnown = 1;
				WombImpregnateClone(V.PC, 1, donatrix, 1);
				WombNormalizePreg(V.PC);
			} else {
				receptrix.pregKnown = 1;
				WombImpregnateClone(receptrix, 1, donatrix, 1);
				WombNormalizePreg(receptrix);
				const {
					He,
					he, his, him
				} = getPronouns(receptrix);
				if (receptrix.fetish === "mindbroken") {
					r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen,`);
					if (receptrix.ID === donatrix.ID) {
						r.push(`but has no idea ${he} carries ${his} own clone.`);
					} else {
						r.push(`${he} knows that ${he} has been impregnated.`);
					}
				} else if (receptrix.fetish === "pregnancy" && receptrix.fetishStrength > 60 && receptrix.fetishKnown === 1) {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. ${He} was overjoyed at the sight of the syringe containing ${his}`);
						if (receptrix.ID === donatrix.ID) {
							r.push(`clone`);
						} else {
							r.push(`future child`);
						}
						r.push(`emptying into ${his} womb.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated${(receptrix.ID === donatrix.ID) ? `, but not that ${he} now bears ${his} own clone` : ''}.`);
					}
					r.push(`${He} is  filled with joy over the life settling into ${his} womb and can't wait to see the result. ${He}'s so pleased that ${he} now trusts your plans for ${his} body.`);
					receptrix.trust += 4;
					receptrix.devotion += 10;
				} else if (receptrix.devotion > 50) {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. ${He} watched the syringe containing ${his} new pregnancy empty into ${his} womb with rapt attention.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated${(receptrix.ID === donatrix.ID) ? `, but not that ${he} now bears ${his} own clone` : ''}.`);
					}
					r.push(`${He}'s <span class="devotion inc">grateful</span> that you think ${him} worthy of carrying this child, and a little nervous about how ${he}'ll perform as a surrogate.`);
					receptrix.devotion += 4;
				} else if (receptrix.devotion >= -20) {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. From the syringe making contact with ${his} skin, to the egg's delivery into ${his} womb and ${his} subsequent impregnation, ${he} couldn't look away.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated${(receptrix.ID === donatrix.ID) ? `, but not that ${he} now bears ${his} own clone` : ''}.`);
					}
					r.push(`${He} understands the realities of ${his} life as a slave, so it isn't much of a shock. ${He} is <span class="trust dec">sensibly fearful</span> of your total power over ${his} body.`);
					receptrix.trust -= 10;
				} else {
					if (canSee(receptrix)) {
						r.push(`Since the surgery required only a local anesthetic, ${receptrix.slaveName} remained fully aware throughout the procedure. The moment ${he} realized what was happening, ${he} shut ${his} eyes tight, only opening them again as ${he} feels the slight tingle of the injector exiting ${his} lower abdomen.`);
					} else {
						r.push(`${receptrix.slaveName} leaves the surgery with a certain warmth in ${his} lower abdomen, ${he} knows that ${he} has been impregnated.`);
					}
					r.push(`${He} does not understand the realities of ${his} life as a slave at a core level, so ${he}'s <span class="devotion dec">terrified and angry</span> that you have forced ${him} to bear`);
					if (receptrix.ID === donatrix.ID) {
						r.push(`${his} own clone and potential replacement.`);
					} else {
						r.push(`this child, even more so as ${he} realizes ${he} doesn't know who the father is.`);
					}
					r.push(`${He} is <span class="trust dec">sensibly fearful</span> of your total power over ${his} body and the future of the life ${he} now harbors within ${him}.`);
					receptrix.trust -= 15;
					receptrix.devotion -= 15;
				}
			}
			V.receptrix = 0;
			V.donatrix = 0;
	}
	App.Events.addParagraph(node, r);
	return node;
};


code:
App.Art.cacheArtData();
App.Corporate.Init();
App.RA.Activation.populateGetters();

// TODO: remove once setup object is no longer required.
for (let key in App.Data.misc) {
	setup[key] = App.Data.misc[key];
}


code:
/* subscribe to game save/load events */
Save.onLoad.add(App.EventHandlers.onLoad);
Save.onSave.add(App.EventHandlers.onSave);

/* ### Every-time listeners #### */

$(document).on(":storyready", () => {
	App.EventHandlers.storyReady();
});

$(document).on(":passageinit", () => {
	if (V.passageSwitchHandler) {
		V.passageSwitchHandler();
		delete V.passageSwitchHandler;
	}
	App.UI.Tabs.clear();
	profileEvents.passageinit();
});

$(document).on(":passagestart", event => {
	App.Debug.slavesConsistency(event);
	Object.defineProperty(State.temporary, "S", {
		get: () => S,
		enumerable: true
	});
	profileEvents.passagestart();
});

$(document).on(":passagerender", () => {
	profileEvents.passagerender();
});

$(document).on(":passagedisplay", () => {
	profileEvents.passagedisplay();
});

$(document).on(":passageend", ev => {
	profileEvents.passageend(ev.content);
});

/* ### One-time listeners ### */

$(document).one(":passagestart", () => {
	App.EventHandlers.optionsChanged();
});


code:
[submodule "submodules/sugarcube-2"]
	branch = fc


code:
{
	const getProxy = App.Utils.Diff.getProxy;

	App.Testing.executeTest("Overwrite first level", () => {
	}, () => {
		let proxy = getProxy({a: 1});

		proxy.a = 2;

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {a: 1});
		App.Testing.equals(proxy.a, 2);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {a: 2});
	}, () => {
	});

	App.Testing.executeTest("Overwrite second level", () => {
	}, () => {
		let proxy = getProxy({c: {a: 1}});

		proxy.c.a = 2;

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {c: {a: 1}});
		App.Testing.equals(proxy.c.a, 2);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {c: {a: 2}});
	}, () => {
	});

	App.Testing.executeTest("Delete first level", () => {
	}, () => {
		let proxy = getProxy({a: 1});

		delete proxy.a;

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {a: 1});
		App.Testing.hasNoProperty(proxy, "a");

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {});
	}, () => {
	});

	App.Testing.executeTest("Delete in second level", () => {
	}, () => {
		let proxy = getProxy({c: {a: 1}});

		delete proxy.c.a;

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {c: {a: 1}});
		App.Testing.hasProperty(proxy, "c");
		App.Testing.hasNoProperty(proxy.c, "a");

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {c: {}});
	}, () => {
	});

	App.Testing.executeTest("Delete with second level", () => {
	}, () => {
		let proxy = getProxy({c: {a: 1}});

		delete proxy.c;

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {c: {a: 1}});
		App.Testing.hasNoProperty(proxy, "c");

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {});
	}, () => {
	});

	App.Testing.executeTest("add value", () => {
	}, () => {
		let proxy = getProxy({});

		proxy.d = 7;

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {});
		App.Testing.hasProperty(proxy, "d");
		App.Testing.equals(proxy.d, 7);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {d: 7});
	}, () => {
	});

	App.Testing.executeTest("add object", () => {
	}, () => {
		let proxy = getProxy({});

		proxy.d = {a: 5};

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {});
		App.Testing.hasProperty(proxy, "d");
		App.Testing.hasProperty(proxy.d, "a");
		App.Testing.equals(proxy.d.a, 5);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {d: {a: 5}});
	}, () => {
	});

	App.Testing.executeTest("Overwrite array entry", () => {
	}, () => {
		let proxy = getProxy({b: [1, 2]});

		proxy.b[1] = 5;

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {b: [1, 2]});
		App.Testing.equals(proxy.b, [1, 5]);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {b: [1, 5]});
	}, () => {
	});

	App.Testing.executeTest("array push", () => {
	}, () => {
		let proxy = getProxy({b: [1, 2]});

		proxy.b.push(5);

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {b: [1, 2]});
		App.Testing.equals(proxy.b, [1, 2, 5]);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {b: [1, 2, 5]});
	}, () => {
	});

	App.Testing.executeTest("array pop", () => {
	}, () => {
		let proxy = getProxy({b: [1, 2]});

		proxy.b.pop();

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {b: [1, 2]});
		App.Testing.equals(proxy.b, [1]);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {b: [1]});
	}, () => {
	});

	App.Testing.executeTest("new array unshift", () => {
	}, () => {
		let proxy = getProxy({});

		proxy.d = {a: [5]};
		proxy.d.a.unshift(9);

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {});
		App.Testing.hasProperty(proxy.d, "a");
		App.Testing.equals(proxy.d.a, [9, 5]);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original, {d: {a: [9, 5]}});
	}, () => {
	});

	App.Testing.executeTest("remove one eye", () => {
	}, () => {
		let proxy = getProxy({eye: new App.Entity.EyeState()});

		eyeSurgery(proxy, "left", "remove");

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {eye: new App.Entity.EyeState()});
		App.Testing.equals(proxy.eye.left, null);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original.eye.left, null);
	}, () => {
	});

	App.Testing.executeTest("remove both eyes", () => {
	}, () => {
		let proxy = getProxy({eye: new App.Entity.EyeState()});

		eyeSurgery(proxy, "both", "remove");

		const original = proxy.diffOriginal;

		App.Testing.equals(original, {eye: new App.Entity.EyeState()});
		App.Testing.equals(proxy.eye.left, null);
		App.Testing.equals(proxy.eye.right, null);

		App.Utils.Diff.applyDiff(original, proxy.diffChange);
		App.Testing.equals(original.eye.left, null);
		App.Testing.equals(original.eye.right, null);
	}, () => {
	});

	// Do this last
	App.Testing.unitDone();
}


code:
{
	class MockSlave {
		ID;
		mother;
		father;

		constructor(id) {
			this.ID = id;
		}
	}

	class MockMating {
		constructor(fatherId, motherId, ...childrenIds) {
			for (let id of childrenIds) {
				if (id === fatherId || id === motherId) {
					throw new Error("cannot give birth to self");
				}
			}

			this.fatherId = fatherId;
			this.motherId = motherId;
			this.childrenIds = childrenIds;
		}
	}

	class MockWorld {
		slavesArray;

		constructor(matings) {
			this.slavesArray = [];

			let slavesById = {};
			let meetSlave = (id) => {
				if (!slavesById[id]) {
					let slave = new MockSlave(id);
					this.slavesArray.push(slave);
					slavesById[id] = slave;
				}

				return slavesById[id];
			};

			for (let mating of matings) {
				let father = meetSlave(mating.fatherId);
				let mother = meetSlave(mating.motherId);
				for (let id of mating.childrenIds) {
					let child = meetSlave(id);
					child.father = father.ID;
					child.mother = mother.ID;
				}
			}
		}

		findSlaveState(id) {
			return this.slavesArray.find(slave => slave.ID === id) || null;
		}
	}

	let testCoefficientForSlave = function(name, mockMatings, slaveId, expectedCoefficientOfInbreeding) {
		App.Testing.executeTest(name, () => {}, () => {
			let tolerance = .00000000001;

			let world = new MockWorld(mockMatings);
			let c = ibc._test.coeff_slave(world, world.findSlaveState(slaveId));
			App.Testing.isType(c, "number");
			App.Testing.notNaN(c);
			App.Testing.inRange(c, expectedCoefficientOfInbreeding, tolerance);
		}, () => {});
	};

	// references:
	// Introduction to Quantitative Genetics - Doulas S. Falconer, 1989
	// Genetic and Quantitative Aspects of Genealogy - F.M. Lancaster, 2015
	testCoefficientForSlave("basic outbred mating, 1",[new MockMating(1, 2, 3, 4)], 1, 0);
	testCoefficientForSlave("basic outbred mating, 2",[new MockMating(1, 2, 3, 4)], 4, 0);
	testCoefficientForSlave("basic self-mating, 1", [new MockMating(1, 1, 3, 4)], 4, 1 / 2);
	testCoefficientForSlave("basic self-mating, 2", [new MockMating(1, 1, 3, 4)], 1, 0);
	testCoefficientForSlave("basic child-parent mating",[new MockMating(1, 2, 3), new MockMating(2, 3, 4)], 4, 1 / 4);
	testCoefficientForSlave("basic sibling mating",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5)], 5, 1 / 4);
	testCoefficientForSlave("basic half-sibling mating",[new MockMating(1, 2, 3), new MockMating(2, 4, 5), new MockMating(3, 5, 6)], 6, 1 / 8);
	testCoefficientForSlave("basic first cousin mating",[new MockMating(1, 2, 3, 4), new MockMating(5, 3, 6), new MockMating(4, 7, 8), new MockMating(6, 8, 9)], 9, 1 / 16);
	testCoefficientForSlave("double first cousin mating",[new MockMating(1, 2, 3, 4), new MockMating(5, 6, 7, 8),
		new MockMating(3, 7, 9), new MockMating(4, 8, 10), new MockMating(9, 10, 11)], 11, 1 / 8);
	testCoefficientForSlave("aunt-niece mating",[new MockMating(1, 2, 3, 4), new MockMating(4, 5, 6), new MockMating(3, 6, 7)], 7, 1 / 8);
	let scenario53 = [new MockMating(1, 2, 3, 4), new MockMating(5, 6, 7, 8), new MockMating(3, 7, 9, 10), new MockMating(4, 8, 11),
		new MockMating(12, 9, 13), new MockMating(10, 11, 14), new MockMating(13, 14, 15)];
	testCoefficientForSlave("problem 5.3 from Falconer, 1",scenario53, 14, 1 / 8);  //
	testCoefficientForSlave("problem 5.3 from Falconer, 2",scenario53, 15, 3 / 32);
	testCoefficientForSlave("example in figure 64 from Lancaster",[new MockMating(1, 2, 4, 5), new MockMating(3, 4, 7), new MockMating(5, 6, 8), new MockMating(7, 8, 9), new MockMating(9, 10, 12, 13),
		new MockMating(11, 12, 15), new MockMating(13, 14, 16), new MockMating(15, 16, 17, 18)], 17, 33 / 512);
	testCoefficientForSlave("example in figure 65 from Lancaster",[new MockMating(1, 2, 4, 5), new MockMating(3, 4, 7), new MockMating(5, 6, 8), new MockMating(7, 8, 10, 11),
		new MockMating(9, 10, 13), new MockMating(11, 12, 14), new MockMating(13, 14, 15)], 15, 9 / 128);
	testCoefficientForSlave("example in figure 66 from Lancaster",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9)], 9, 1 / 2);
	testCoefficientForSlave("example in figure 66 from Lancaster, extended by one more generation's worth of mating between siblings",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10),
		new MockMating(9, 10, 11)], 11, 19 / 32);
	// from here the preceding scenario of regular sibling-mating is extended by several more steps; by rules given in Falconer, the coefficient ck for step k, where step k follows
	// step j which follows step i, should be ck = 1/4 + cj/2 + ci/4, and so that is how the expected values used in the next few tests were calculated
	testCoefficientForSlave("extended, 1: 43/64 = 1/4 + (19/32) / 2 + (1/2) / 4",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10),
		new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13)], 13, 43 / 64);
	testCoefficientForSlave("extended, 2",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10),
		new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15)], 15, 94 / 128);
	testCoefficientForSlave("extended, 3", [new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10),
		new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15, 16), new MockMating(15, 16, 17)], 17, 201 / 256);
	testCoefficientForSlave("extended, 4", [new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10),
		new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15, 16), new MockMating(15, 16, 17, 18), new MockMating(17, 18, 19)], 19, 423 / 512);
	testCoefficientForSlave("extended, 5", [new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10),
		new MockMating(9, 10, 11, 12), new MockMating(11, 12, 13, 14), new MockMating(13, 14, 15, 16), new MockMating(15, 16, 17, 18), new MockMating(17, 18, 19, 20),
		new MockMating(19, 20, 21)], 21, 880 / 1024);
	// Does not apply to current algorithm: as of this writing, the ten-generations-of-sisterfucking test immediately above is enough to occupy the game's coefficient-of-inbreeding calculation algorithm for a few minutes
	// on a ~4Ghz CPU.  this is very silly.  when you read this, that algorithm should have been replaced by a much faster one, allowing the above test to be comfortably run again.
	let scenario70 = [new MockMating(1, 2, 6), new MockMating(2, 3, 7), new MockMating(3, 4, 8), new MockMating(5, 6, 9), new MockMating(7, 8, 10),
		new MockMating(9, 10, 12), new MockMating(10, 11, 13), new MockMating(12, 13, 14), new MockMating(12, 14, 15)];  // example in figure 70 from Lancaster
	testCoefficientForSlave("example in figure 70 from Lancaster, 1", scenario70, 2, 0);
	testCoefficientForSlave("example in figure 70 from Lancaster, 2", scenario70, 10, 1 / 8);
	testCoefficientForSlave("example in figure 70 from Lancaster, 3", scenario70, 12, 1 / 32);
	testCoefficientForSlave("example in figure 70 from Lancaster, 4", scenario70, 15, 85 / 256);
	let scenarioXXX = [new MockMating(1, 2, 5, 6), new MockMating(3, 4, 7, 8), new MockMating(5, 6, 9, 10), new MockMating(5, 7, 11), new MockMating(5, 8, 12),
		new MockMating(5, 9, 13), new MockMating(9, 10, 14), new MockMating(5, 11, 15), new MockMating(10, 12, 16), new MockMating(13, 16, 17),
		new MockMating(4, 16, 18), new MockMating(14, 15, 19), new MockMating(6, 17, 20), new MockMating(6, 18, 21), new MockMating(6, 19, 22),
		new MockMating(20, 21, 23), new MockMating(22, 23, 24)];  // an invented example meant to represent a typical situation in a respectable arcology
	testCoefficientForSlave("change, 1",scenarioXXX, 24, 633 / 2048);  // here the expected coefficient was retrieved from the algorithm being tested and was not separately verified by hand,
	// so this test could conceivably be wrong, but at least it can catch changes...
	testCoefficientForSlave("change, 2",[new MockMating(1, 2, 3, 4), new MockMating(3, 4, 5, 6), new MockMating(5, 6, 7, 8), new MockMating(7, 8, 9, 10), new MockMating(9, 10, 11, 12),
		new MockMating(11, 12, 13), new MockMating(13, 14, 15),
		new MockMating(5, 5, 16), new MockMating(15, 16, 17)], 17, 1 / 4);  // an example in which the old and new algorithms actually gave different results - the first one such to have been observed! It seems that the old algorithm was wrong
	testCoefficientForSlave("negative IDs, 1",[new MockMating(-120, -120, 87)], 87, 1 / 2);  // negative IDs need to be accepted (not bothering to test any weird special IDs like -1 here though)
	testCoefficientForSlave("negative IDs, 2",[new MockMating(-999, -998, -300), new MockMating(-300, -998, 300)], 300, 1 / 4);

	// Do this last
	App.Testing.unitDone();
}


code:
// Define Test framework and register all tests

// To run all tests, type 'App.Loader.executeTests()' in the browser console. Make sure the HTML file has not been
// moved from the 'bin/' directory

// First, create the test framework
App.Testing = (function() {
	// First, abstract away anything test unrelated
	const group = App.Loader.getGroup("../tests");

	function addTestUnit(path) {
		group.queueSubscript(path);
	}

	let unitTotal = 0;
	let unitSuccesses = 0;

	function start() {
		App.Loader.nextScript();
	}

	function unitDone() {
		console.log("Group '" + App.Loader.lastScript + "' done. Successful tests:", unitSuccesses, "of", unitTotal);
		unitTotal = 0;
		unitSuccesses = 0;
		App.Loader.nextScript();
	}

	// Actual Testing functionality
	class TestError extends Error {
	}

	/**
	 * @param {string} name Unique test name
	 * @param {null|(()=>void)} prepare Prepare the global state
	 * @param {!(()=>void)} test Do the actual testing
	 * @param {null|(()=>void)} cleanup Clean the global state up
	 */
	function executeTest(name, prepare, test, cleanup) {
		unitTotal++;
		try {
			if (prepare != null) {
				prepare();
			}
		} catch (e) {
			console.log("PREPARE_FAILED", name, e);
			tryCleanup(name, cleanup);
			return;
		}
		try {
			test();
		} catch (e) {
			console.log("TEST_FAILED", name, e);
			tryCleanup(name, cleanup);
			return;
		}
		if (tryCleanup(name, cleanup)) {
			unitSuccesses++;
		}
	}

	/**
	 * @param {string} name Unique test name
	 * @param {null|(()=>void)} cleanup Clean the global state up
	 */
	function tryCleanup(name, cleanup) {
		try {
			if (cleanup != null) {
				cleanup();
			}
		} catch (e) {
			console.log("CLEANUP_FAILED", name, e);
			return false;
		}
		return true;
	}

	function equals(actual, expected) {
		if (!_.isEqual(actual, expected)) {
			throw new TestError("Actual value does not match expected value");
		}
	}

	/**
	 * @param {Object} obj
	 * @param {string} property
	 */
	function hasProperty(obj, property) {
		if (obj[property] === undefined) {
			throw new TestError("Expected property does not exist");
		}
	}

	/**
	 * @param {Object} obj
	 * @param {string} property
	 */
	function hasNoProperty(obj, property) {
		if (obj[property] !== undefined) {
			throw new TestError("Unexpected property exists");
		}
	}

	function isType(value, expected) {
		if (!(typeof value === expected)) {
			throw new TestError("Actual type does not match expected type");
		}
	}

	function notNaN(value) {
		if (Number.isNaN(value)) {
			throw new TestError("Expected valid number, got NaN.");
		}
	}

	function inRange(value, target, tolerance) {
		if (Math.abs(value - target) > tolerance) {
			throw new TestError(`Value ${value} outside range. Expected ${target} with tolerance ${tolerance}.`);
		}
	}

	return {
		addTestUnit, start, unitDone, executeTest,
		equals, hasProperty, hasNoProperty: hasNoProperty, isType, notNaN, inRange,
	};
})();

// Now load all tests
App.Testing.addTestUnit("diffProxy");
App.Testing.addTestUnit("ibc");

// Finally, execute the tests
App.Testing.start();


code:
:root {
    /* base */
    --background-color: #1c1d24;
    --sidebar-color: #212534;/*#2b2f42;*/
    --sidebar-hover-color: #40486e;
    --text-color: #e1e1e1;
    --divider-color: #888;

    /* button */
    --button-color: #2b2640;
    --button-border-color: #2b2640;
    --button-selected-color: #524064;
    --button-hover-color: #524064;
    --button-disabled-color: #22162e;

    --link-color: #6574cd;
    --link-hover-color: #6589e7;
}

.aquamarine, .aquamarine a, .skill, .skill a { color: #43e9e9 }
.coral, .coral a, .fetish.loss, .fetish.loss a { color: #ff8b39 }
.cyan, .cyan a, .hotkey { color: #00dfdf }
/*
.darkgoldenrod, .darkgoldenrod a, .trust.extremely-terrified, .trust.extremely-terrified a { color: darkgoldenrod }
.darkred, .darkred a, .defiant.full, .defiant.full a { color: darkred }
.darkviolet, .darkviolet a, .devotion.hateful, .devotion.hateful a { color: darkviolet }
*/
.deeppink, .deeppink a, .devotion.devoted, .devotion.devoted a { color: #ff4dad }
/*
.steelblue, .steelblue a { color: steelblue }
*/
.deepskyblue, .deepskyblue a, .intelligent, .intelligent a { color: #26bdef }
/*
.dodgerblue, .dodgerblue a { color: dodgerblue }
.blue, .blue a { color: blue }
*/
.gold, .gold a, .trust.dec, .trust.dec a, .trust.frightened, .trust.frightened a { color: #ffd90c }
/*
.goldenrod, .goldenrod a, .trust.terrified, .trust.terrified a { color: goldenrod }
.gray, .gray a { color: gray }
*/
.green, .green a, .reputation.inc, .reputation.inc a, .improvement, .improvement a, .flaw.break, .flaw.break a, .skill.inc, .skill.inc a, .positive, .positive a { color: #18af18 }
.hotpink, .hotpink a, .devotion.inc, .devotion.inc a, .devotion.accept, .devotion.accept a { color: #ff86c3 }
/*
.lawngreen, .lawngreen a { color: lawngreen }
.lightblue, .lightblue a { color: lightblue }
*/
.lightcoral, .lightcoral a, .fetish.gain, .fetish.gain a { color: #e762e3 }
.lightgreen, .lightgreen a, .relationship, .relationship a { color: #00cb7a }
/*
.lightpink, .lightpink a { color: lightpink }
.lightsalmon, .lightsalmon a, .fetish.inc, .fetish.inc a { color: lightsalmon }
*/
.lime, .lime a, .change.positive, .change.positive a, .virginity.loss, .virginity.loss a, .pregnant, .pregnant a { color: #40ff40 }
.limegreen, .limegreen a { color: limegreen }
.magenta, .magenta a, .devotion.worship, .devotion.worship a { color: #ff36ff }
.mediumaquamarine, .mediumaquamarine a, .trust.inc, .trust.inc a, .trust.careful, .trust.careful a { color: #52d3d3 }

.mediumorchid, .mediumorchid a, .devotion.dec, .devotion.dec a, .devotion.resistant, .devotion.resistant a { color: #bc60d3 }
/*
.mediumseagreen, .mediumseagreen a, .trust.trusting, .trust.trusting a { color: mediumseagreen }
*/
.orange, .orange a, .stupid, .stupid a, .change.negative, .change.negative a, .defiant.careful, .defiant.careful a, .intro.question { color: #ffbb3e }
/*
.orangered, .orangered a, .defiant.inc, .defiant.inc a, .defiant.bold, .defiant.bold, .education.neg a { color: orangered }
.orchid, .orchid a { color: orchid }
*/
.pink, .pink a, .slave.name.simple, .slave-name { color: #ff9fc3 }
.red, .red a, .health.dec, .health.dec a, .cash.dec, .cash.dec a, .flaw.gain, .flaw.gain a, .error, .error a, .elites.loss, .elites.loss a, .reputation.dec, .reputation.dec a, .warning, .warning a { color: #ff3131 }
/*.seagreen, .seagreen a, .trust.prof-trusting, .trust.prof-trusting a { color: seagreen }
.springgreen, .springgreen a, .skill.player, .skill.player a { color: springgreen }
*/
.tan, .tan a { color: #d2b48c }
/*
.chocolate, .chocolate a { color: chocolate }
.saddlebrown, .saddlebrown a { color: saddlebrown }
.teal, .teal a { color: teal }
*/
.yellow, .yellow a, .noteworthy, .noteworthy a, .devotion.ambivalent, .devotion.ambivalent a, .trust.fearful, .trust.fearful a { color: #f7f754 }
.yellowgreen, .yellowgreen a, .cash.inc, .cash.inc a, .cash, .cash a { color: #9acd32 }


code:
#init-screen {
	color: var(--text-color);
	background-color: var(--background-color);
}


code:
body {
	color: var(--text-color);
	background-color: var(--background-color);
}

a {
	color: var(--link-color);
}

a:hover {
	color: var(--link-hover-color);
}

button {
	color: var(--text-color);
	background-color: var(--button-color);
	border: 1px solid var(--button-border-color);
}

button:hover {
	background-color: var(--button-hover-color);
	border-color: var(--button-border-color);
}

button:disabled {
	background-color: var(--button-disabled-color);
	border: 1px solid var(--button-border-color);
}

input, select, textarea {
	color: #e1e1e1;
	background-color: transparent;
	border: 1px solid var(--button-border-color);
}

input:not(:disabled):focus, select:not(:disabled):focus, textarea:not(:disabled):focus,
input:not(:disabled):hover, select:not(:disabled):hover, textarea:not(:disabled):hover {
	background-color: #2b2640;
	border-color: var(--button-selected-color);
}

hr {
	border-top: 1px solid var(--divider-color);
}


code:
#ui-dialog-titlebar {
	background-color: var(--sidebar-color);
}
#ui-dialog-body {
	background-color: var(--background-color);
	border: 1px solid var(--divider-color);
}
#ui-dialog-body hr {
	background-color: var(--divider-color);
}


code:
/* Saves dialog styling. */
#ui-dialog-body.saves > *:not(:first-child) {
	border-top-color: var(--divider-color);
}
#ui-dialog-body.saves tr:not(:first-child) {
	border-top-color: var(--divider-color);
}
#ui-dialog-body.saves .empty {
	color: var(--text-color);
}


code:
#ui-bar {
	background-color: var(--sidebar-color);
	border-right: 1px solid var(--divider-color);
}
#ui-bar a:hover {
	text-decoration: underline;
}
#ui-bar hr {
	border-color: var(--divider-color);
}
#ui-bar-toggle{
	color: #444;
	border: 1px solid var(--divider-color);
	border-right: none;
}
#ui-bar-toggle:hover {
	background-color: var(--sidebar-hover-color);
	border-color: var(--divider-color);
}
#menu ul {
	border: 1px solid var(--divider-color);
}
#menu li:not(:first-child) {
	border-top: 1px solid var(--divider-color);
}
#menu li a {
	color: var(--text-color);
	background-color: transparent;
	border: none;
}
#menu li a:hover {
	background-color: var(--sidebar-hover-color);
	text-decoration: none;
}


code:
div.building.basement {
	border-top-color: #8F8F8F;
	background-image: repeating-linear-gradient(135deg, transparent, transparent 20px, #8F8F8F 20px, #8F8F8F 30px);
}

div.building .row .penthouse{
	border-color: #5daad7;
}

div.building .row .apartments{
	border-color: #3eaf3e;
}

div.building .row .private{
	border-color: #cb1a1a;
}

div.building .row .pit{
	border-color: #d94c18;
}

div.building div.innerCell {
	background-color: var(--background-color);
}

div.building div.decorative {
	outline-color: black;
}

/* introduction special formatting */
/* makes all links unusable */
.intro div.building a {
	color: var(--text-color);
}

div.building div.row div.dairy {
	border-color: saddlebrown;
}


code:
div.card {
	/*border: none;*/
	border-color: #384678;
	background-color: #212534;
	box-shadow: 10px 10px 5px #282d40;
}


code:
.lrgRender > div.mask, .lrgVector > div.mask {
	background: linear-gradient(90deg, rgba(28, 29, 36, 1), rgba(28, 29, 36, 0.8) 60%, rgba(28, 29, 36, 0));
}

.clear-formatting {
	color: var(--text-color);
}


code:
div.options-group .comment {
	color: #575757;
}


code:
div.quick-links div.toggle {
	background-color: var(--button-color);
}

div.quick-links div.toggle:hover {
	background-color: var(--button-hover-color);
}

div.quick-links div.menu-link {
	background-color: var(--button-disabled-color);
	border-color: var(--button-border-color);
	box-shadow: var(--button-border-color);
}

div.quick-links div.notification:not(.category)::after,
div.quick-links div.category.notification.collapsed > div.toggle::after {
	content: "\e80b";
}


code:
:root {
    /* base */
    --background-color: #fff;
    --sidebar-color: #ccf;
    --sidebar-hover-color: #99f;
    --text-color: #000;
    --divider-color: #888;

    /* button */
    --button-color: #b38cd9;
    --button-border-color: #9966cc;
    --button-selected-color: #663399;
    --button-hover-color: #ccb3e6;
    --button-disabled-color: #8c70a9;

    --link-color: #2A52B7;
    --link-hover-color: #446DD5;
}

.aquamarine, .aquamarine a, .skill, .skill a { color: #008563 }
.coral, .coral a, .fetish.loss, .fetish.loss a { color: #8a5c2e }
.cyan, .cyan a, .hotkey { color: #008585 }
/*
.darkgoldenrod, .darkgoldenrod a, .trust.extremely-terrified, .trust.extremely-terrified a { color: darkgoldenrod }
.darkred, .darkred a, .defiant.full, .defiant.full a { color: darkred }
.darkviolet, .darkviolet a, .devotion.hateful, .devotion.hateful a { color: darkviolet }
.deeppink, .deeppink a, .devotion.devoted, .devotion.devoted a { color: deeppink }
.steelblue, .steelblue a { color: steelblue }
*/
.deepskyblue, .deepskyblue a, .intelligent, .intelligent a { color: #00B4F0 }
/*
.dodgerblue, .dodgerblue a { color: dodgerblue }
.blue, .blue a { color: blue }
*/
.gold, .gold a, .trust.dec, .trust.dec a, .trust.frightened, .trust.frightened a { color: #cc8d00 }
/*
.goldenrod, .goldenrod a, .trust.terrified, .trust.terrified a { color: goldenrod }
.gray, .gray a { color: gray }
*/
.green, .green a, .reputation.inc, .reputation.inc a, .improvement, .improvement a, .flaw.break, .flaw.break a, .skill.inc, .skill.inc a, .positive, .positive a { color: #006600 }
/*
.hotpink, .hotpink a, .devotion.inc, .devotion.inc a, .devotion.accept, .devotion.accept a { color: hotpink }
.lawngreen, .lawngreen a { color: lawngreen }
.lightblue, .lightblue a { color: lightblue }
*/
.lightcoral, .lightcoral a, .fetish.gain, .fetish.gain a { color: #B85C00 }
.lightgreen, .lightgreen a, .relationship, .relationship a { color: #008550 }
/*
.lightpink, .lightpink a { color: lightpink }
.lightsalmon, .lightsalmon a, .fetish.inc, .fetish.inc a { color: lightsalmon }
.lime, .lime a, .change.positive, .change.positive a, .virginity.loss, .virginity.loss a, .pregnant, .pregnant a { color: lime }
.limegreen, .limegreen a { color: limegreen }
.magenta, .magenta a, .devotion.worship, .devotion.worship a { color: magenta }
*/
.mediumaquamarine, .mediumaquamarine a, .trust.inc, .trust.inc a, .trust.careful, .trust.careful a { color: #0F6161 }
/*
.mediumorchid, .mediumorchid a, .devotion.dec, .devotion.dec a, .devotion.resistant, .devotion.resistant a { color: mediumorchid }
.mediumseagreen, .mediumseagreen a, .trust.trusting, .trust.trusting a { color: mediumseagreen }
*/
.orange, .orange a, .stupid, .stupid a, .change.negative, .change.negative a, .defiant.careful, .defiant.careful a, .intro.question { color: #B85C00 }
/*
.orangered, .orangered a, .defiant.inc, .defiant.inc a, .defiant.bold, .defiant.bold, .education.neg a { color: orangered }
.orchid, .orchid a { color: orchid }
*/
.pink, .pink a, .slave.name.simple, .slave-name { color: #E00070 }
/*
.red, .red a, .health.dec, .health.dec a, .cash.dec, .cash.dec a, .flaw.gain, .flaw.gain a, .error, .error a, .elites.loss, .elites.loss a, .reputation.dec, .reputation.dec a, .warning, .warning a { color: red }
.seagreen, .seagreen a, .trust.prof-trusting, .trust.prof-trusting a { color: seagreen }
.springgreen, .springgreen a, .skill.player, .skill.player a { color: springgreen }
*/
.tan, .tan a { color: #846E58 }
/*
.chocolate, .chocolate a { color: chocolate }
.saddlebrown, .saddlebrown a { color: saddlebrown }
.teal, .teal a { color: teal }
*/
.yellow, .yellow a, .noteworthy, .noteworthy a, .devotion.ambivalent, .devotion.ambivalent a, .trust.fearful, .trust.fearful a { color: #cc9600 }
.yellowgreen, .yellowgreen a, .cash.inc, .cash.inc a, .cash, .cash a { color: #677f1f }


code:
body {
	color: var(--text-color);
	background-color: var(--background-color);
}

a {
	color: var(--link-color);
}

a:hover {
	color: var(--link-hover-color);
}

button {
	color: var(--text-color);
	background-color: var(--button-color);
	border: 1px solid var(--button-border-color);
}

button:hover {
	background-color: var(--button-hover-color);
	border-color: var(--button-border-color);
}

button:disabled {
	background-color: var(--button-disabled-color);
	border: 1px solid var(--button-border-color);
}

input, select, textarea {
	color: #333;
	background-color: transparent;
	border: 1px solid var(--button-border-color);
}

input:not(:disabled):focus, select:not(:disabled):focus, textarea:not(:disabled):focus,
input:not(:disabled):hover, select:not(:disabled):hover, textarea:not(:disabled):hover {
	background-color: #e5d9f2;
	border-color: var(--button-selected-color);
}

hr {
	border-top: 1px solid var(--divider-color);
}


code:
#ui-dialog-titlebar {
	background-color: var(--divider-color);
}
#ui-dialog-body {
	background-color: var(--sidebar-color);
	border: 1px solid var(--divider-color);
}
#ui-dialog-body hr {
	background-color: var(--divider-color);
}


code:
div.building.basement {
	border-top-color: #8F8F8F;
	background-image: repeating-linear-gradient(135deg, transparent, transparent 20px, #8F8F8F 20px, #8F8F8F 30px);
}

div.building div.innerCell {
	background-color: var(--background-color);
}

div.building div.decorative {
	outline-color: black;
}

/* introduction special formatting */
/* makes all links unusable */
.intro div.building a {
	color: var(--text-color);
}

div.building div.row div.dairy {
	border-color: saddlebrown;
}


code:
div.card {
	/*border: none;*/
	border-color: #cccccc;
	background-color: #e6e6e6;
	box-shadow: 10px 10px 5px #b3b3b3;
}


code:
.lrgRender > div.mask, .lrgVector > div.mask {
	background: linear-gradient(90deg, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.8) 60%, rgba(255, 255, 255, 0));
}

.clear-formatting {
	color: var(--text-color);
}


code:
# Themes

Every subdirectory is a distinct theme. The compiled theme is placed in `bin/` and can be loaded ingame via the options
page.

## Creating new themes

Files inside these directories are combined into one CSS file based on
alphabetical ordering. The light theme is recommended as a base for creating new themes.

For quick testing run `App.UI.Theme.loadTheme("yourTheme.css")` in the browser console. This will unload the current
theme and load the new theme. If you modify the same file it is effectively reloading. Supplying no argument (eg.
`App.UI.Theme.loadTheme()`) will unload any theme.


code:
# To customize your server, make a copy of this file to `.env` and edit any
# values you want to change. Be sure to remove the `#` at the beginning of each
# line you want to modify.

# All values have reasonable defaults, so you only need to change the ones you
# want to override.

# ------------------------------------------------------------------------------
# General settings:

# The title displayed on the info page.
# SERVER_TITLE=Coom Tunnel

# Text model requests allowed per minute per user.
# TEXT_MODEL_RATE_LIMIT=4
# Image model requests allowed per minute per user.
# IMAGE_MODEL_RATE_LIMIT=2

# Max number of context tokens a user can request at once.
# Increase this if your proxy allow GPT 32k or 128k context
# MAX_CONTEXT_TOKENS_OPENAI=16384

# Max number of output tokens a user can request at once.
# MAX_OUTPUT_TOKENS_OPENAI=400
# MAX_OUTPUT_TOKENS_ANTHROPIC=400

# Whether to show the estimated cost of consumed tokens on the info page.
# SHOW_TOKEN_COSTS=false

# Whether to automatically check API keys for validity.
# Note: CHECK_KEYS is disabled by default in local development mode, but enabled
# by default in production mode.
# CHECK_KEYS=true

# Which model types users are allowed to access.
# The following model families are recognized:
# turbo | gpt4 | gpt4-32k | gpt4-turbo | dall-e | claude | gemini-pro | mistral-tiny | mistral-small | mistral-medium | aws-claude | azure-turbo | azure-gpt4 | azure-gpt4-32k | azure-gpt4-turbo
# By default, all models are allowed except for 'dall-e'. To allow DALL-E image
# generation, uncomment the line below and add 'dall-e' to the list.
# ALLOWED_MODEL_FAMILIES=turbo,gpt4,gpt4-32k,gpt4-turbo,claude,gemini-pro,mistral-tiny,mistral-small,mistral-medium,aws-claude,azure-turbo,azure-gpt4,azure-gpt4-32k,azure-gpt4-turbo

# URLs from which requests will be blocked.
# BLOCKED_ORIGINS=reddit.com,9gag.com
# Message to show when requests are blocked.
# BLOCK_MESSAGE="You must be over the age of majority in your country to use this service."
# Destination to redirect blocked requests to.
# BLOCK_REDIRECT="https://roblox.com/"

# Comma-separated list of phrases that will be rejected. Only whole words are matched.
# Surround phrases with quotes if they contain commas.
# Avoid short or common phrases as this tests the entire prompt.
# REJECT_PHRASES="phrase one,phrase two,"phrase three, which has a comma",phrase four"
# Message to show when requests are rejected.
# REJECT_MESSAGE="This content violates /aicg/'s acceptable use policy."

# Whether prompts should be logged to Google Sheets.
# Requires additional setup. See `docs/google-sheets.md` for more information.
# PROMPT_LOGGING=false

# The port to listen on.
# PORT=7860

# Whether cookies should be set without the Secure flag, for hosts that don't support SSL.
# USE_INSECURE_COOKIES=false

# Detail level of logging. (trace | debug | info | warn | error)
# LOG_LEVEL=info

# ------------------------------------------------------------------------------
# Optional settings for user management, access control, and quota enforcement:
# See `docs/user-management.md` for more information and setup instructions.
# See `docs/user-quotas.md` to learn how to set up quotas.

# Which access control method to use. (none | proxy_key | user_token)
# GATEKEEPER=none
# Which persistence method to use. (memory | firebase_rtdb)
# GATEKEEPER_STORE=memory

# Maximum number of unique IPs a user can connect from. (0 for unlimited)
# MAX_IPS_PER_USER=0
# Whether user_tokens should be automatically disabled when reaching the IP limit.
# MAX_IPS_AUTO_BAN=true

# With user_token gatekeeper, whether to allow users to change their nickname.
# ALLOW_NICKNAME_CHANGES=true

# Default token quotas for each model family. (0 for unlimited)
# DALL-E "tokens" are counted at a rate of 100000 tokens per US$1.00 generated,
# which is similar to the cost of GPT-4 Turbo.
# DALL-E 3 costs around US$0.10 per image (10000 tokens).
# See `docs/dall-e-configuration.md` for more information.
# TOKEN_QUOTA_TURBO=0
# TOKEN_QUOTA_GPT4=0
# TOKEN_QUOTA_GPT4_32K=0
# TOKEN_QUOTA_GPT4_TURBO=0
# TOKEN_QUOTA_DALL_E=0
# TOKEN_QUOTA_CLAUDE=0
# TOKEN_QUOTA_GEMINI_PRO=0
# TOKEN_QUOTA_AWS_CLAUDE=0

# How often to refresh token quotas. (hourly | daily)
# Leave unset to never automatically refresh quotas.
# QUOTA_REFRESH_PERIOD=daily

# ------------------------------------------------------------------------------
# Secrets and keys:
# Do not put any passwords or API keys directly in this file.
# For Huggingface, set them via the Secrets section in your Space's config UI.
# For Render, create a "secret file" called .env using the Environment tab.

# You can add multiple API keys by separating them with a comma.
# For AWS credentials, separate the access key ID, secret key, and region with a colon.
OPENAI_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ANTHROPIC_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# See `docs/aws-configuration.md` for more information, there may be additional steps required to set up AWS.
AWS_CREDENTIALS=myaccesskeyid:mysecretkey:us-east-1,anotheraccesskeyid:anothersecretkey:us-west-2
# See `docs/azure-configuration.md` for more information, there may be additional steps required to set up Azure.
AZURE_CREDENTIALS=azure-resource-name:deployment-id:api-key,another-azure-resource-name:another-deployment-id:another-api-key

# With proxy_key gatekeeper, the password users must provide to access the API.
# PROXY_KEY=your-secret-key

# With user_token gatekeeper, the admin password used to manage users.
# ADMIN_KEY=your-very-secret-key

# With firebase_rtdb gatekeeper storage, the Firebase project credentials.
# FIREBASE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# FIREBASE_RTDB_URL=https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.firebaseio.com

# With prompt logging, the Google Sheets credentials.
# GOOGLE_SHEETS_SPREADSHEET_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# GOOGLE_SHEETS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

license: unknown


code:
*.7z filter=lfs diff=lfs merge=lfs -text
*.arrow filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.ckpt filter=lfs diff=lfs merge=lfs -text
*.ftz filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.h5 filter=lfs diff=lfs merge=lfs -text
*.joblib filter=lfs diff=lfs merge=lfs -text
*.lfs.* filter=lfs diff=lfs merge=lfs -text
*.mlmodel filter=lfs diff=lfs merge=lfs -text
*.model filter=lfs diff=lfs merge=lfs -text
*.msgpack filter=lfs diff=lfs merge=lfs -text
*.npy filter=lfs diff=lfs merge=lfs -text
*.npz filter=lfs diff=lfs merge=lfs -text
*.onnx filter=lfs diff=lfs merge=lfs -text
*.ot filter=lfs diff=lfs merge=lfs -text
*.parquet filter=lfs diff=lfs merge=lfs -text
*.pb filter=lfs diff=lfs merge=lfs -text
*.pickle filter=lfs diff=lfs merge=lfs -text
*.pkl filter=lfs diff=lfs merge=lfs -text
*.pt filter=lfs diff=lfs merge=lfs -text
*.pth filter=lfs diff=lfs merge=lfs -text
*.rar filter=lfs diff=lfs merge=lfs -text
*.safetensors filter=lfs diff=lfs merge=lfs -text
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.tar.* filter=lfs diff=lfs merge=lfs -text
*.tflite filter=lfs diff=lfs merge=lfs -text
*.tgz filter=lfs diff=lfs merge=lfs -text
*.wasm filter=lfs diff=lfs merge=lfs -text
*.xz filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text

path: .gitattributes


code:
.env
.venv
.vscode
.idea
build
greeting.md
node_modules
http-client.private.env.json

path: .gitignore


code:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run type-check

path: .husky/pre-push


code:
# OAI Reverse Proxy

Reverse proxy server for the OpenAI and Anthropic APIs. Forwards text generation requests while rejecting administrative/billing requests. Includes optional rate limiting and prompt filtering to prevent abuse.

### Table of Contents
- [What is this?](#what-is-this)
- [Why?](#why)
- [Usage Instructions](#setup-instructions)
  - [Deploy to Huggingface (Recommended)](#deploy-to-huggingface-recommended)
  - [Deploy to Repl.it (WIP)](#deploy-to-replit-wip)
- [Local Development](#local-development)

## What is this?
If you would like to provide a friend access to an API via keys you own, you can use this to keep your keys safe while still allowing them to generate text with the API. You can also use this if you'd like to build a client-side application which uses the OpenAI or Anthropic APIs, but don't want to build your own backend. You should never embed your real API keys in a client-side application. Instead, you can have your frontend connect to this reverse proxy and forward requests to the downstream service.

This keeps your keys safe and allows you to use the rate limiting and prompt filtering features of the proxy to prevent abuse.

## Why?
OpenAI keys have full account permissions. They can revoke themselves, generate new keys, modify spend quotas, etc. **You absolutely should not share them, post them publicly, nor embed them in client-side applications as they can be easily stolen.**

This proxy only forwards text generation requests to the downstream service and rejects requests which would otherwise modify your account.

---

## Usage Instructions
If you'd like to run your own instance of this proxy, you'll need to deploy it somewhere and configure it with your API keys. A few easy options are provided below, though you can also deploy it to any other service you'd like.

### Deploy to Huggingface (Recommended)
[See here for instructions on how to deploy to a Huggingface Space.](./docs/deploy-huggingface.md)

### Deploy to Render
[See here for instructions on how to deploy to Render.com.](./docs/deploy-render.md)

## Local Development
To run the proxy locally for development or testing, install Node.js >= 18.0.0 and follow the steps below.

1. Clone the repo
2. Install dependencies with `npm install`
3. Create a `.env` file in the root of the project and add your API keys. See the [.env.example](./.env.example) file for an example.
4. Start the server in development mode with `npm run start:dev`.

You can also use `npm run start:dev:tsc` to enable project-wide type checking at the cost of slower startup times. `npm run type-check` can be used to run type checking without starting the server.


code:
FROM node:18-bullseye-slim
RUN apt-get update && \
    apt-get install -y git
RUN git clone https://gitgud.io/khanon/oai-reverse-proxy.git /app
WORKDIR /app
RUN chown -R 1000:1000 /app
USER 1000
RUN npm install
COPY Dockerfile greeting.md* .env* ./
RUN npm run build
EXPOSE 7860
ENV NODE_ENV=production
# Huggigface free VMs have 16GB of RAM so we can be greedy
ENV NODE_OPTIONS="--max-old-space-size=12882"
CMD [ "npm", "start" ]

path: docker/huggingface/Dockerfile


code:
# syntax = docker/dockerfile:1.2

FROM node:18-bullseye-slim
RUN apt-get update && \
    apt-get install -y curl

# Unlike Huggingface, Render can only deploy straight from a git repo and
# doesn't allow you to create or modify arbitrary files via the web UI.
# To use a greeting file, set `GREETING_URL` to a URL that points to a raw
# text file containing your greeting, such as a GitHub Gist.

# You may need to clear the build cache if you change the greeting, otherwise
# Render will use the cached layer from the previous build.

WORKDIR /app
ARG GREETING_URL
RUN if [ -n "$GREETING_URL" ]; then \
    curl -sL "$GREETING_URL" > greeting.md; \
    fi
COPY package*.json greeting.md* ./
RUN npm install
COPY . .
RUN npm run build
RUN --mount=type=secret,id=_env,dst=/etc/secrets/.env cat /etc/secrets/.env >> .env
EXPOSE 10000
ENV NODE_ENV=production
CMD [ "npm", "start" ]

path: docker/render/Dockerfile