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

	const oldSlave = clone(V.swappingSlave);
	const m = V.slaveIndices[V.swappingSlave.ID];
	const {
		he
	} = getPronouns(V.swappingSlave);

	App.UI.DOM.appendNewElement("p", node, `You strap ${V.slaves[m].slaveName}, and the body to which ${he} will be transferred, into the remote surgery and stand back as it goes to work.`);
	bodySwap(V.slaves[m], V.activeSlave, false);
	const gps = V.genePool.findIndex(function(s) { return s.ID === V.slaves[m].ID; });
	// special exception to swap genePool since the temporary body lacks an entry. Otherwise we could just call the widget using the genePool entries
	V.genePool[gps].race = V.slaves[m].race;
	V.genePool[gps].origRace = V.slaves[m].origRace;
	V.genePool[gps].skin = V.slaves[m].skin;
	V.genePool[gps].markings = V.slaves[m].markings;
	V.genePool[gps].eye.origColor = V.slaves[m].eye.origColor;
	V.genePool[gps].origHColor = V.slaves[m].origHColor;
	V.genePool[gps].origSkin = V.slaves[m].origSkin;
	V.genePool[gps].face = V.slaves[m].face;
	V.genePool[gps].pubicHStyle = V.slaves[m].pubicHStyle;
	V.genePool[gps].underArmHStyle = V.slaves[m].underArmHStyle;
	V.genePool[gps].eyebrowHStyle = V.slaves[m].eyebrowHStyle;

	App.Events.addParagraph(node, [
		`After an honestly impressive procedure, ${V.slaves[m].slaveName} is recovering nicely.`,
		bodySwapReaction(V.slaves[m], oldSlave)
	]);

	const cost = slaveCost(oldSlave);
	const payout = Math.trunc(cost/3);
	let r = [];
	r.push(`${V.slaves[m].slaveName}'s old body was bought by the Flesh Heap for ${cashFormat(payout)}.`);
	if (V.slaves[m].bodySwap > 0) {
		const myBody = V.slaves.findIndex(function(s) { return s.origBodyOwnerID === V.slaves[m].ID; });
		if (myBody !== -1) {
			V.slaves[myBody].origBodyOwnerID = 0;
			const {
				he2, him2, his2
			} = getPronouns(V.slaves[myBody]).appendSuffix("2");
			if (V.slaves[myBody].fetish !== "mindbroken" && V.slaves[myBody].fuckdoll === 0) {
				if (V.slaves[myBody].devotion > 20) {
					r.push(`${V.slaves[myBody].slaveName} is somewhat saddened to see ${his2} body leave forever.`);
				} else if (V.slaves[myBody].devotion >= -50) {
					r.push(`${V.slaves[myBody].slaveName} is <span class="mediumorchid">disturbed</span> to find ${his2} body is gone for good, damaging ${his2} <span class="gold">ability to trust you.</span>`);
					V.slaves[myBody].devotion -= 30;
					V.slaves[myBody].trust -= 30;
				} else {
					r.push(`${V.slaves[myBody].slaveName} is deeply upset that ${he2}'ll never see ${his2} body again. With so little left, ${he2} finds it easy to take vengeance by completely rejecting your ownership of ${him2}.`);
					V.slaves[myBody].devotion -= 50;
					V.slaves[myBody].trust = 100;
				}
			}
		}
	}
	App.Events.addParagraph(node, r);
	V.slaves[m].bodySwap++;
	cashX(payout, "slaveTransfer");
	V.activeSlave = 0;
	V.swappingSlave = 0;
	return node;
};


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

	const ss1 = V.slaveIndices[V.AS];
	const ss1Clone = clone(getSlave(V.AS));

	const ss2 = V.slaveIndices[V.swappingSlave.ID];
	const ss2Clone = clone(V.swappingSlave);

	const gps1 = V.genePool.findIndex(s => s.ID === V.slaves[ss1].ID);
	const gps1Clone = clone(V.genePool[gps1]);

	const gps2 = V.genePool.findIndex(s => s.ID === V.slaves[ss2].ID);
	const gps2Clone = clone(V.genePool[gps2]);

	App.UI.DOM.appendNewElement("p", node, `You strap ${getSlave(V.AS).slaveName} and ${V.swappingSlave.slaveName} into the remote surgery and stand back as it goes to work.`);

	bodySwap(V.slaves[ss1], ss2Clone, false);
	bodySwap(V.genePool[gps1], gps2Clone, true);

	bodySwap(V.slaves[ss2], ss1Clone, false);
	bodySwap(V.genePool[gps2], gps1Clone, true);

	App.Events.addParagraph(node, [
		`After an honestly impressive procedure, ${V.slaves[ss1].slaveName} is recovering nicely.`,
		bodySwapReaction(V.slaves[ss1], ss1Clone)
	]);

	App.UI.DOM.appendNewElement("hr", node);

	App.Events.addParagraph(node, [
		`In the neighboring bed, ${V.slaves[ss2].slaveName} rests peacefully.`,
		bodySwapReaction(V.slaves[ss2], ss2Clone)
	]);

	// figuring out whom has whose body now
	whoHasWho(ss1, ss2Clone);
	whoHasWho(ss2, ss1Clone);

	// now to handle whose body it is, name-wise
	bodySwapName(V.slaves[ss1], V.slaves[ss2]);
	bodySwapName(V.slaves[ss2], V.slaves[ss1]);

	for (const ss of [ss1, ss2]) {
		if (ss1Clone.bodySwap > 0) {
			if (V.slaves[ss].origBodyOwnerID === V.slaves[ss].ID) {
				V.slaves[ss].bodySwap = 0;
				V.slaves[ss].origBodyOwnerID = 0;
				V.slaves[ss].origBodyOwner = "";
			} else {
				V.slaves[ss].bodySwap++;
			}
		} else {
			V.slaves[ss].bodySwap++;
		}
	}

	V.activeSlave = 0;
	V.swappingSlave = 0;
	return node;

	function whoHasWho(index, opposingClone) {
		if (V.slaves[index].bodySwap === 0) {
			V.slaves[index].origBodyOwnerID = opposingClone.ID;
		} else if (V.slaves[ss2].origBodyOwner !== "") { // now who's going to be looking for you?
			const myBody = V.slaves.findIndex(function(s) { return s.origBodyOwnerID === V.slaves[ss2].ID; });
			if (myBody !== -1) {
				V.slaves[myBody].origBodyOwnerID = V.slaves[index].ID;
			}
		}
	}
};

repo_name: Mirarara/fc

license: mit


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

	let eligibility = 0;
	const donatrix = V.donatrix;
	const receptrix = V.receptrix;

	let impreg;
	if (donatrix !== "undecided" && donatrix.ID === -1) {
		impreg = PlayerName();
	} else if (donatrix !== "undecided") {
		impreg = SlaveFullName(donatrix);
	} else {
		impreg = donatrix;
	}

	let receive;
	if (receptrix !== "undecided" && receptrix.ID === -1) {
		receive = PlayerName();
	} else if (receptrix !== "undecided") {
		receive = SlaveFullName(receptrix);
	} else {
		receive = receptrix;
	}

	App.UI.DOM.appendNewElement("h2", node, `Genetic Source`);
	App.UI.DOM.appendNewElement("div", node, "Blank ovum prepared, please select genetic source and surrogate.", "note");
	App.UI.DOM.appendNewElement("div", node, `Chosen source: ${impreg}`);

	App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
		"Yourself",
		() => {
			V.donatrix = V.PC;
			App.UI.reload();
		}
	));
	for (const slave of V.slaves) {
		if (donatrix !== "undecided" && donatrix.ID === slave.ID) {
			App.UI.DOM.appendNewElement("div", node, SlaveFullName(slave), "note");
		} else {
			App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
				SlaveFullName(slave),
				() => {
					V.donatrix = slave;
					App.UI.reload();
				}
			));
		}
	}
	App.UI.DOM.appendNewElement("h2", node, `Surrogate`);
	App.UI.DOM.appendNewElement("div", node, `Chosen surrogate: ${receive}`);

	for (const slave of V.slaves) {
		if (canBeReceptrix(slave)) {
			const name = SlaveFullName(slave);
			if (receptrix !== "undecided" && receptrix.ID === slave.ID) {
				App.UI.DOM.appendNewElement("div", node, name, "note");
			} else {
				App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
					name,
					() => {
						V.receptrix = slave;
						App.UI.reload();
					}, [], "",
					(slave.pregType >= 4) ? `Using a slave carrying multiples is inadvisable` : ``
				));
			}
			eligibility = 1;
		}
	}
	if (eligibility === 0) {
		App.UI.DOM.appendNewElement("div", node, `You have no slaves capable of acting as a surrogate.`, "note");
	}

	if (V.PC.vagina !== -1 && V.PC.preg >= 0 && V.PC.preg < 4 && V.PC.pregType < 8 && V.PC.physicalAge < 70) {
		if (receptrix !== "undecided" && receptrix.ID === V.PC.ID) {
			App.UI.DOM.appendNewElement("div", node, `Yourself`, "note");
		} else {
			App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
				"Use your own womb",
				() => {
					V.receptrix = V.PC;
					App.UI.reload();
				}
			));
		}
	}
	App.UI.DOM.appendNewElement("h2", node, `Implantation`);

	if (impreg !== "undecided" && receive !== "undecided") {
		App.UI.DOM.appendNewElement("div", node, `${impreg} will be cloned and ${receive} shall act as the incubator.`);
		App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
			"Implant clone ovum",
			() => {
				cashX(forceNeg(V.surgeryCost * 2), "slaveSurgery");
				V.surgeryType = "clone";
			}, [], "Surrogacy",
		));
	} else if (impreg !== "undecided") {
		App.Events.addNode(node, [
			`${impreg} will be cloned.`,
			App.UI.DOM.makeElement("span", "Please select a surrogate", "note")
		]);
	} else if (receive !== "undecided") {
		App.Events.addNode(node, [
			`${receive} shall act as the incubator.`,
			App.UI.DOM.makeElement("span", "Please select a genetic source", "note")
		]);
	} else {
		App.UI.DOM.appendNewElement("div", node, `Please select a genetic source and surrogate`, "note");
	}

	return node;
};


code:
/**
 * @param {App.Entity.SlaveState} slave
 * @param {()=>void} refreshPage
 */
App.UI.SlaveInteract.fatGraft = function(slave, refreshPage) {
	let availableFat = Math.round(slave.weight / 10);
	let boobFat = 0;
	let buttFat = 0;

	const container = document.createElement("div");
	container.append(content());

	if (Dialog.isOpen()) {
		Dialog.close();
	}
	Dialog.setup("Fat Grafting");
	Dialog.append(container);
	Dialog.open();

	function content() {
		const el = new DocumentFragment();
		let r = [];
		const {his} = getPronouns(slave);
		let p;
		let linkArray = [];
		const buttCost = (slave.butt > 10) ? 5 : 2; // Buttsizes are apparently not linear, so it takes a lot more fat to increase the largest booties.
		r.push(`All surplus body fat was harvested, and enough is graftable for an additional ${availableFat * 100}ccs per breast or an additional ${num(Math.trunc(availableFat / buttCost))} sizes to ${his} rear.`);
		if (availableFat === 0) {
			r.push(`All fat marked for use.`);
		} else {
			r.push(`A total of ${availableFat} ${(availableFat === 1) ? `unit remains` : `units remain`}.`);
		}
		App.Events.addNode(el, r, "p", "scene-intro");

		// Boobs
		p = App.UI.DOM.appendNewElement("p", el);
		App.UI.DOM.appendNewElement("div", p, `${capFirstChar(String(num(boobFat)))} units of fat will be added to ${his} breasts for a size gain of ${boobFat * 100}ccs.`);
		if (availableFat > 0) {
			linkArray.push(
				App.UI.DOM.link(
					"Increase",
					() => {
						boobFat++;
						availableFat--;
						refresh();
					}
				)
			);
		} else {
			linkArray.push(App.UI.DOM.disabledLink(`No more fat available.`, []));
		}
		if (boobFat > 0) {
			linkArray.push(
				App.UI.DOM.link(
					"Decrease",
					() => {
						boobFat--;
						availableFat++;
						refresh();
					}
				)
			);
		} else {
			linkArray.push(App.UI.DOM.disabledLink(`No fat marked for breast use.`, []));
		}
		App.UI.DOM.appendNewElement("div", p, App.UI.DOM.generateLinksStrip(linkArray));

		// Ass
		p = App.UI.DOM.appendNewElement("p", el);
		linkArray = [];
		App.UI.DOM.appendNewElement("div", p, `${capFirstChar(String(num(buttFat * buttCost)))} units of fat will be added to ${his} ass for a size gain of ${buttFat}.`);
		if (availableFat / buttCost > 1) {
			linkArray.push(
				App.UI.DOM.link(
					"Increase",
					() => {
						buttFat++;
						availableFat -= buttCost;
						refresh();
					}
				)
			);
		} else if (availableFat) {
			linkArray.push(App.UI.DOM.disabledLink(`Not enough fat remains to increase ${his} butt an entire size.`, []));
		} else {
			linkArray.push(App.UI.DOM.disabledLink(`No more fat available.`, []));
		}
		if (buttFat > 0) {
			linkArray.push(
				App.UI.DOM.link(
					"Decrease",
					() => {
						buttFat--;
						availableFat += buttCost;
						refresh();
					}
				)
			);
		} else {
			linkArray.push(App.UI.DOM.disabledLink(`No fat marked for ass use.`, []));
		}
		App.UI.DOM.appendNewElement("div", p, App.UI.DOM.generateLinksStrip(linkArray));
		App.UI.DOM.appendNewElement("p", el, App.Medicine.Surgery.makeLink(
			new App.Medicine.Surgery.Procedures.FatGraft(slave, boobFat, buttFat),
			refreshPage, false));
		return el;
	}

	function refresh() {
		App.UI.DOM.replace(container, content());
	}
};


code:
/**
 * @param {App.Entity.SlaveState} slave
 * @returns {DocumentFragment}
 */
App.Medicine.OrganFarm.growActions = function(slave) {
	const O = App.Medicine.OrganFarm.Organs;

	// find already being grown/ready to implant organs
	const slaveOrgans = {};
	O.forEach((organ, key) => slaveOrgans[key] = 0);
	V.completedOrgans.forEach(organ => {
		if (organ.ID === slave.ID) {
			slaveOrgans[organ.type] = -1;
		}
	});
	V.organs.forEach(organ => {
		if (organ.ID === slave.ID) {
			slaveOrgans[organ.type] = organ.weeksToCompletion;
		}
	});

	function weeksLeft(time) {
		if (V.organFarmUpgrade === 1) {
			return time;
		} else if (V.organFarmUpgrade === 2) {
			return Math.ceil(time / 2);
		} else {
			return Math.ceil(time / 4);
		}
	}

	// create entries for each organ
	const canImplant = [];
	const cantImplant = [];
	const growing = [];
	let any = false;
	O.forEach((organ, key) => {
		if (slaveOrgans[key] === 0) {
			if (organ.canGrow(slave)) {
				const array = [];
				array.push(App.UI.DOM.makeElement("div", App.UI.DOM.passageLink(organ.name, "Remote Surgery", () => {
					App.Medicine.OrganFarm.growOrgan(slave, key);
				})));

				const tooltip = typeof organ.tooltip === "string" ? organ.tooltip : organ.tooltip(slave);
				array.push(App.UI.DOM.makeElement("div", `Costs ${cashFormat(organ.cost)}${tooltip !== "" ? ` and ${tooltip}` : ""}.`, "detail"));

				if (organ.implantActions.some((o) => {
					return o.canImplant(slave);
				})) {
					canImplant.push(...array);
				} else {
					cantImplant.push(...array);
				}
			}
			any = true;
		} else if (slaveOrgans[key] > 0) {
			// in growth organs at the end of the list
			growing.push({
				time: slaveOrgans[key],
				organ: organ.name,
				text: `Ready for implantation in ${weeksLeft(slaveOrgans[key])} weeks.`
			});
			any = true;
		}
	});

	const fragment = new DocumentFragment();

	if (growing.length > 0) {
		App.UI.DOM.appendNewElement("h4", fragment, "Growing");
		const div = App.UI.DOM.appendNewElement("div", fragment);
		div.classList.add("grid-2columns-auto", "choices");
		// nearer to completion => higher up
		growing.sort((a, b) => a.time - b.time);
		for (let i = 0; i < growing.length; i++) {
			App.UI.DOM.appendNewElement("div", div, growing[i].organ);
			App.UI.DOM.appendNewElement("div", div, growing[i].text);
		}
	}
	if (canImplant.length > 0) {
		App.UI.DOM.appendNewElement("h4", fragment, "Can be implanted immediately");
		const div = App.UI.DOM.appendNewElement("div", fragment);
		div.classList.add("grid-2columns-auto", "choices");
		div.append(...canImplant);
	}
	if (cantImplant.length > 0) {
		App.UI.DOM.appendNewElement("h4", fragment, "Cannot be implanted immediately");
		const div = App.UI.DOM.appendNewElement("div", fragment);
		div.classList.add("grid-2columns-auto", "choices");
		div.append(...cantImplant);
	}

	if (any) {
		return fragment;
	} else {
		return null;
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} organType
 */
App.Medicine.OrganFarm.growOrgan = function(slave, organType) {
	const organ = App.Medicine.OrganFarm.Organs.get(organType);

	V.organs.push({type: organType, weeksToCompletion: organ.time, ID: slave.ID});

	cashX(-organ.cost, "slaveSurgery", slave);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} organType
 */
App.Medicine.OrganFarm.growIncubatorOrgan = function(slave, organType) {
	const organ = App.Medicine.OrganFarm.Organs.get(organType);

	V.incubator.organs.push({type: organType, weeksToCompletion: organ.time, ID: slave.ID});

	cashX(-organ.cost, "slaveSurgery", slave);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {()=>void} refresh
 * @param {boolean} cheat
 * @returns {DocumentFragment}
 */
App.Medicine.OrganFarm.implantActions = function(slave, refresh, cheat) {
	let slaveOrgans = V.completedOrgans.filter(o => o.ID === slave.ID);
	if (slaveOrgans.length === 0) {
		return null;
	}

	const F = App.Medicine.OrganFarm;
	const grid = document.createElement("div");
	grid.classList.add("grid-2columns-auto");

	for (const organ of slaveOrgans) {
		App.UI.DOM.appendNewElement("div", grid, F.Organs.get(organ.type).name);

		let links = [];
		for (let i = 0; i < F.Organs.get(organ.type).implantActions.length; i++) {
			const action = F.Organs.get(organ.type).implantActions[i];
			if (action.canImplant(slave)) {
				const link = App.Medicine.Surgery.makeLink(
					App.Medicine.OrganFarm.instantiateProcedure(slave, action), refresh, cheat,
					(s) => App.Medicine.OrganFarm.implantAction(s, organ.type));
				links.push(link);
				if (!F.Organs.get(organ.type).displayMultipleActions) {
					break; // there can only be one implant action
				}
			} else {
				const error = action.implantError(slave);
				if (error !== "") {
					links.push(App.UI.DOM.spanWithTooltip(action.name, error));
				}
			}
		}

		// all links in a row
		const div = document.createElement("div");
		for (let i = 0; i < links.length; i++) {
			div.append(links[i], " | ");
		}

		// last error or implant action has "Discard" after them.
		div.append(App.UI.DOM.link("Discard", App.Medicine.OrganFarm.removeOrgan, [slave, organ.type], "Remote Surgery"));
		grid.append(div);
	}

	return App.UI.DOM.combineNodes(`The fabricator has completed ${slaveOrgans.length} organ(s):`, grid);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {App.Medicine.OrganFarm.OrganImplantAction} action
 * @returns {App.Medicine.Surgery.Procedure}
 */
App.Medicine.OrganFarm.instantiateProcedure = function(slave, action) {
	return new action.surgeryProcedure(slave, ...action.surgeryProcedureParams);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} organType
 */
App.Medicine.OrganFarm.implantAction = function(slave, organType) {
	App.Medicine.OrganFarm.removeOrgan(slave, organType);
	if (V.organFarmUpgrade >= 2) {
		slave.chem += 20;
	}
};

/**
 * @param {string} organType
 * @param {App.Medicine.Surgery.Procedure} procedure
 * @returns {DocumentFragment}
 */
App.Medicine.OrganFarm.implant = function(organType, procedure) {
	App.Medicine.OrganFarm.implantAction(procedure._slave, organType);

	const result = App.Medicine.Surgery.apply(procedure, false);
	if (result === null) {
		Engine.play("Surgery Death");
		return;
	}

	const [diff, reaction] = result;

	const f = App.Medicine.Surgery.makeSlaveReaction(procedure.originalSlave, diff, reaction);

	App.Utils.Diff.applyDiff(procedure.originalSlave, diff);

	return f;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} type
 */
App.Medicine.OrganFarm.removeOrgan = function(slave, type) {
	let organIndex = V.completedOrgans.findIndex(o => (o.ID === slave.ID && o.type === type));

	if (organIndex !== -1) {
		V.completedOrgans.deleteAt(organIndex);
	}
};

/**
 * Organs the that can be implanted on the slave, sorted by dependencies first
 *
 * @param {App.Entity.SlaveState} slave
 * @returns {string[]}
 */
App.Medicine.OrganFarm.getSortedOrgans = function(slave) {
	const F = App.Medicine.OrganFarm;
	/** @type {string[]} */
	const organs = [];
	const dependencies = [];
	V.completedOrgans.forEach(organ => {
		if (organ.ID === slave.ID) {
			organs.push(organ.type);
		}
	});
	organs.forEach(organKey => {
		F.Organs.get(organKey).dependencies.forEach(dependencyKey => {
			if (organs.includes(dependencyKey)) { /* inefficient, is there a better way? */
				dependencies.push([dependencyKey, organKey]);
			}
		});
	});
	try {
		return App.Utils.topologicalSort(organs, dependencies);
	} catch (e) { // closed loop
		console.log(e.message);
		return [];
	}
};

/**
 * Returns the full organ farm menu, hiding empty parts
 *
 * @param {App.Entity.SlaveState} slave
 * @param {()=>void} refresh
 * @param {boolean} cheat
 * @returns {DocumentFragment}
 */
App.Medicine.OrganFarm.fullMenu = function(slave, refresh, cheat) {
	const fragment = document.createDocumentFragment();
	if (!(V.organFarmUpgrade >= 1 && slave.indentureRestrictions < 2)) {
		return fragment;
	}

	const growActions = App.Medicine.OrganFarm.growActions(slave);
	let anyAction = false;
	if (growActions != null) {
		const {him} = getPronouns(slave);
		App.UI.DOM.appendNewElement("h3", fragment, "Grow new organs");
		App.UI.DOM.appendNewElement("div", fragment, `The fabricator is ready to grow an organ for ${him}. Extract tissue to begin growing.`);
		App.UI.DOM.appendNewElement("div", fragment, growActions);
		anyAction = true;
	}

	const implantActions = App.Medicine.OrganFarm.implantActions(slave, refresh, cheat);
	if (implantActions !== null) {
		App.UI.DOM.appendNewElement("h3", fragment, "Implant Organs");
		App.UI.DOM.appendNewElement("div", fragment, implantActions, "indent");
		anyAction = true;
	}

	if (!anyAction) {
		return fragment;
	}

	fragment.prepend(App.UI.DOM.makeElement("h2", "Organ Farm"));
	return fragment;
};

/**
 * Returns a list of currently growing and finished organs.
 *
 * @returns {DocumentFragment}
 */
App.Medicine.OrganFarm.currentlyGrowing = function() {
	function weeksToCompletion(weeks) {
		if (V.organFarmUpgrade === 1) {
			return weeks;
		} else if (V.organFarmUpgrade === 2) {
			return Math.ceil(weeks / 2);
		} else {
			return Math.ceil(weeks / 4);
		}
	}

	/**
	 * @type {Array<string|HTMLSpanElement>}
	 */
	let growLines = [];
	/**
	 * @type {Array<string|HTMLSpanElement>}
	 */
	let finishLines = [];

	V.organs.forEach(o => {
		const slave = getSlave(o.ID);
		if (slave !== undefined) {
			const weeks = weeksToCompletion(o.weeksToCompletion);
			growLines.push(`${slave.slaveName}'s ${App.Medicine.OrganFarm.Organs.get(o.type).name}, ${weeks} ${weeks > 1 ? "weeks" : "week"} left.`);
		} else {
			growLines.push(App.UI.DOM.makeElement("span", `ERROR: No slave with ID ${o.ID} found.`, "error"));
		}
	});
	if (V.incubator.capacity > 0) {
		V.incubator.organs.forEach(o => {
			const tank = V.incubator.tanks.find((t) => t.ID === o.ID);
			if (tank !== undefined) {
				if (o.weeksToCompletion <= 0) {
					finishLines.push(`${tank.slaveName}'s ${App.Medicine.OrganFarm.Organs.get(o.type).name}.`);
				} else {
					growLines.push(`${tank.slaveName}'s ${App.Medicine.OrganFarm.Organs.get(o.type).name}, ${
						weeksToCompletion(o.weeksToCompletion)} week(s) left.`);
				}
			} else {
				growLines.push(App.UI.DOM.makeElement("span", `ERROR: No tank with ID ${o.ID} found.`, "error"));
			}
		});
	}

	V.completedOrgans.forEach(o => {
		const slave = getSlave(o.ID);
		if (slave !== undefined) {
			finishLines.push(`${slave.slaveName}'s ${App.Medicine.OrganFarm.Organs.get(o.type).name}.`);
		} else {
			growLines.push(App.UI.DOM.makeElement("span", `ERROR: No slave with ID ${o.ID} found.`, "error"));
		}
	});

	const frag = document.createDocumentFragment();

	if (growLines.length > 0) {
		App.UI.DOM.appendNewElement("h3", frag, "Growing Organs");

		growLines.forEach(l => {
			App.UI.DOM.appendNewElement("div", frag, l);
		});
	}

	if (finishLines.length > 0) {
		App.UI.DOM.appendNewElement("h3", frag, "Finished Organs");

		finishLines.forEach(l => {
			App.UI.DOM.appendNewElement("div", frag, l);
		});
	}

	if (growLines.length === 0 && finishLines.length === 0) {
		App.UI.DOM.appendNewElement("div", frag, "Organs must be genetically matched to their host slave. To begin growing organs for a slave, take them to the Remote Surgery.", "detail");
	}

	return frag;
};


code:
App.UI.ovaTransplantWorkaround = function() {
	const node = new DocumentFragment();
	V.receptrix = 0;
	let eligibility = 0;

	App.UI.DOM.appendNewElement("p", node, "You've decided which fertilized ovum is to be transplanted; now you must select whose womb will be its new home.", "scene-intro");

	App.UI.DOM.appendNewElement("h2", node, "Select a slave to serve as the host");

	for (const slave of V.slaves) {
		if ((V.donatrix.ID !== slave.ID && slave.ovaries > 0 || slave.mpreg > 0) && isSlaveAvailable(slave) && slave.preg >= 0 && slave.preg < slave.pregData.normalBirth / 10 && slave.pregWeek >= 0 && slave.pubertyXX === 1 && slave.pregType < 12 && slave.bellyImplant === -1 && slave.broodmother === 0 && slave.inflation <= 2 && slave.physicalAge < 70) {
			App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
				SlaveFullName(slave),
				() => {
					V.receptrix = slave;
					cashX(forceNeg(V.surgeryCost * 2), "slaveSurgery");
					V.surgeryType = "transplant";
				}, [], "Surrogacy",
				(slave.pregType >= 4) ? `Using a slave carrying multiples is inadvisable` : ``
			));
			eligibility = 1;
		}
	}
	if (eligibility === 0) {
		App.UI.DOM.appendNewElement("div", node, "You have no slaves capable of acting as a surrogate.");
	}

	if (V.PC.vagina !== -1 && V.donatrix.ID !== -1 && V.PC.preg >= 0 && V.PC.preg < 4 && V.PC.pregType < 8 && V.PC.physicalAge < 70) {
		App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
			`Use your own womb`,
			() => {
				V.receptrix = V.PC;
				cashX(forceNeg(V.surgeryCost * 2), "slaveSurgery");
				V.surgeryType = "transplant";
			},
			[],
			"Surrogacy"
		));
	}
	return node;
};


code:
/**
 * @param {App.Medicine.Surgery.Procedure} procedure
 * @param {function():void} refresh
 * @param {boolean} cheat
 * @param {(slave: App.Entity.SlaveState)=>void} [onApply] function to execute on clicking the link before any other action
 * @returns {HTMLAnchorElement|HTMLSpanElement}
 */
App.Medicine.Surgery.makeLink = function(procedure, refresh, cheat, onApply) {
	if (procedure.disabledReasons.length > 0) {
		return App.UI.DOM.disabledLink(procedure.name, procedure.disabledReasons);
	}

	const note = procedure.note;
	const slave = procedure.originalSlave;
	const res = App.UI.DOM.link(procedure.name, apply, [], "", tooltip());
	if (note) {
		res.classList.add("with-note");
	}
	return res;

	function healthCosts() {
		const hc = (V.PC.skill.medicine >= 100) ? Math.round(procedure.healthCost / 2) : procedure.healthCost;
		if (hc > 30) {
			return 'substantial';
		} else if (hc > 20) {
			return 'significant';
		} else if (hc > 10) {
			return 'moderate';
		} else if (hc > 5) {
			return 'light';
		}
		return 'insignificant';
	}

	function tooltip() {
		const tooltip = new DocumentFragment();
		const desc = procedure.description;
		if (desc !== "") {
			App.UI.DOM.appendNewElement("div", tooltip, `${capFirstChar(desc)}.`);
		}
		if (!cheat) {
			App.UI.DOM.appendNewElement("div", tooltip, `Surgery costs: ${cashFormat(procedure.cost)}.`);
			App.UI.DOM.appendNewElement("div", tooltip, `Projected health damage: ${healthCosts()}.`);
		}
		if (note) {
			App.UI.DOM.appendNewElement("div", tooltip, typeof note === "string" ? `${capFirstChar(note)}.` : note);
		}
		return tooltip;
	}

	function apply() {
		if (onApply) {
			// TODO find a better way to do this.
			onApply(procedure._slave);
		}

		const result = App.Medicine.Surgery.apply(procedure, cheat);
		if (result === null) {
			Engine.play("Surgery Death");
			return;
		}

		const [diff, reaction] = result;

		const f = App.Medicine.Surgery.makeSlaveReaction(slave, diff, reaction);

		App.Utils.Diff.applyDiff(slave, diff);

		// Refresh the surgery options or wherever the surgery originated
		refresh();

		// Finally show the slaves reaction
		Dialog.setup(procedure.name);
		Dialog.append(f);
		Dialog.open();
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {Partial<App.Entity.SlaveState>} diff
 * @param {App.Medicine.Surgery.SimpleReaction} reaction
 * @returns {DocumentFragment}
 */
App.Medicine.Surgery.makeSlaveReaction = function(slave, diff, reaction) {
	const f = new DocumentFragment();
	let r = [];

	r.push(...reaction.intro(slave, diff));

	const resultMain = reaction.reaction(slave, diff);
	for (const p of resultMain.longReaction) {
		r.push(...p);
		App.Events.addParagraph(f, r);
		r = [];
	}
	slave.devotion += resultMain.devotion;
	slave.trust += resultMain.trust;

	const resultOutro = reaction.outro(slave, diff, resultMain);
	for (const p of resultOutro.longReaction) {
		r.push(...p);
		App.Events.addParagraph(f, r);
		r = [];
	}
	slave.devotion += resultOutro.devotion;
	slave.trust += resultOutro.trust;

	return f;
};

/**
 * if null is returned the slave died
 *
 * @param {App.Medicine.Surgery.Procedure} procedure
 * @param {boolean} cheat
 * @returns {[Partial<App.Entity.SlaveState>, App.Medicine.Surgery.SimpleReaction]}
 */
App.Medicine.Surgery.apply = function(procedure, cheat) {
	const slave = procedure.originalSlave;
	if (!cheat) {
		cashX(-procedure.cost, "slaveSurgery", slave);
		surgeryDamage(slave, procedure.healthCost, cheat);
		if (procedure.invasive && slave.health.health < random(-100, -80)) {
			return null;
		}
	}

	const [diff, reaction] = procedure.apply(cheat);

	if (reaction.removeJob) {
		removeJob(slave, Job.LURCHER, true);
		removeJob(slave, Job.PIT, true);
		removeJob(slave, slave.assignment);
	}

	return [diff, reaction];
};

/**
 * Returns options to accept all possible surgeries
 * @returns {FC.Medicine.Surgery.SizingOptions}
 */
App.Medicine.Surgery.allSizingOptions = function() {
	return {
		replace: true,
	};
};

App.Medicine.Surgery.sizingProcedures = function() {
	class ForbiddenDummy extends App.Medicine.Surgery.Procedure {
		/**
		 * @param {App.Entity.SlaveState} slave
		 * @param {string} name
		 * @param {string} forbidden why the surgery can't be used
		 */
		constructor(slave, name, forbidden) {
			super(slave);
			this._name = name;
			this._forbidden = forbidden;
		}

		get name() {
			return this._name;
		}

		get disabledReasons() {
			return super.disabledReasons.concat(this._forbidden);
		}
	}

	return {
		bodyPart: bodyPart,
		boobs: boobSizingProcedures,
		butt: buttSizingProcedures,
		lips: lipsSizingProcedures,
	};

	/**
	 * Returns list of available surgeries targeted at changing size of the given body part
	 * @template {FC.SizingImplantTarget} T
	 * @param {FC.SizingImplantTarget} bodyPart
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.Medicine.Surgery.SizingOptions<T>} [options]
	 * @returns {App.Medicine.Surgery.Procedure[]}
	 */
	function bodyPart(bodyPart, slave, options) {
		switch (bodyPart) {
			case "boobs":
				return boobSizingProcedures(slave, options);
			case "butt":
				return buttSizingProcedures(slave, options);
			case "lips":
				return lipsSizingProcedures(slave, options);
			default:
				throw Error(`No sizing procedures for ${bodyPart}`);
		}
	}

	/**
	 * @template {FC.SizingImplantTarget} T
	 * @param {FC.BodyPartInstalledImplantType<T>} type
	 * @param {FC.Data.Medicine.SizingImplants.ImplantType} implantData
	 * @param {number} volume
	 * @param {FC.Medicine.ImplantInfo<T>} implantInfo
	 * @returns {boolean}
	 */
	function implantsAreSame(type, implantData, volume, implantInfo) {
		if (implantInfo.type !== type) {
			return false;
		}
		if (!implantData.fill) {
			return volume === implantInfo.volume;
		}
		return implantInfo.volume.isBetween(volume, implantData.fill.limit, true);
	}

	/**
	 * @template {FC.SizingImplantTarget} Target
	 * @param {FC.Medicine.Surgery.SizingOptions<Target>} options
	 * @param {FC.SizingImplantTarget} target
	 * @returns {FC.Medicine.Surgery.DefinitiveSizedOptions<Target>}
	 */
	function substituteImplicitOptions(options, target) {
		return {
			allowedTypes: options.allowedTypes || new Set(["none", ...App.Medicine.implantTypesForTarget(target)]),
			replace: options.replace || false,
			// reversed range to include all sizing options
			targetSize: options.targetSize || App.Utils.makeRange(App.Medicine.maxAssetSize(target), 0),
		};
	}

	/**
	 * @param {number} value
	 * @param {FC.NumericRange} range
	 * @returns {number}
	 */
	function distanceToRange(value, range) {
		if (range.max >= range.min) {
			return App.Utils.distanceToRange(value, range);
		} else { // reversed range
			return App.Utils.distanceToRange(value, App.Utils.makeRange(range.max, range.min));
		}
	}

	/**
	 * @param {number} d1
	 * @param {number} d2
	 * @param {FC.NumericRange} range
	 * @returns {boolean}
	 */
	function distanceToRangeDecreased(d1, d2, range) {
		return range.max >= range.min ? d2 < d1 : d2 <= d1;
	}

	/**
	 * @param {number} p1
	 * @param {number} p2
	 * @param {FC.NumericRange} range
	 * @returns {boolean}
	 */
	function pointIsCloserToRange(p1, p2, range) {
		return distanceToRangeDecreased(distanceToRange(p1, range), distanceToRange(p2, range), range);
	}

	/**
	 * @template {FC.SizingImplantTarget} T
	 * @param {FC.SlaveState} slave
	 * @param {T} target
	 * @param {FC.Medicine.Surgery.DefinitiveSizedOptions<T>} options
	 * @param {FC.Medicine.ImplantProcedureCreators} creators
	 * @returns {App.Medicine.Surgery.Procedure[]}
	 */
	function implantSizingProcedures(slave, target, options, creators) {
		/** @type {App.Medicine.Surgery.Procedure[]} */
		const procedures = [];

		const current = App.Medicine.implantInfo(slave, target);
		const currentDistance = distanceToRange(current.volume, options.targetSize);
		const curImplantData = current.type !== "none" ? App.Data.Medicine.sizingImplants[target][current.type] : null;
		const curFleshSize = App.Medicine.fleshSize(slave, target);
		const maxVolume = App.Medicine.pocketVolume[target](slave);
		const maxPureImplantVolume = maxVolume - curFleshSize;
		const maxAssetVolume = App.Medicine.maxAssetSize(target);

		const addMaxAssetSizeReachedDummy = (name) => {
			const {His} = getPronouns(slave);
			procedures.push(new ForbiddenDummy(slave, name, `${His} ${target} size reached the limit.`));
		};

		const distanceDecreased = (newValue) => {
			return distanceToRangeDecreased(currentDistance, distanceToRange(newValue, options.targetSize), options.targetSize);
		};

		/**
		 * @param {FC.Data.Medicine.SizingImplants.ImplantType} data
		 * @returns {number[]}
		 */
		const availableSizes = (data) => {
			const sizes = data.availableSizes;
			return typeof sizes === "function" ? sizes() : sizes;
		};

		/**
		 * @param {FC.Data.Medicine.SizingImplants.FillDrainData} fd
		 */
		const indenturePermitsFillDrain = (fd) => {
			return slave.indentureRestrictions < 2 || fd.healthCost === 0;
		};

		// filling/draining the current implants go first
		if (curImplantData && options.allowedTypes.has(current.type)) {
			if (curImplantData.fill && current.volume < options.targetSize.min && indenturePermitsFillDrain(curImplantData.fill)) {
				let maxVolumeReached = true;
				let fillingPossible = false;
				for (const fillVolume of curImplantData.fill.step) {
					const newVolume = current.volume + fillVolume;
					if (curImplantData.fill.limit >= newVolume && distanceDecreased(newVolume)) {
						if (slave[target] + fillVolume <= maxAssetVolume) {
							maxVolumeReached = false;
							if (newVolume <= maxPureImplantVolume) {
								procedures.push(creators.fill(slave, fillVolume));
								fillingPossible = true;
							}
						}
					}
				}
				if (!fillingPossible) {
					const {His} = getPronouns(slave);
					procedures.push(new ForbiddenDummy(slave, "Fill up", `${His} ${target} implants are filled to capacity.`));
				} else if (maxVolumeReached) {
					addMaxAssetSizeReachedDummy("Fill up");
				}
			}
			if (curImplantData.drain && indenturePermitsFillDrain(curImplantData.drain)) {
				for (const drainVolume of curImplantData.drain.step) {
					if (curImplantData.drain.limit <= current.volume - drainVolume && distanceDecreased(current.volume - drainVolume)) {
						procedures.push(creators.drain(slave, drainVolume));
					}
				}
			}
		}

		// all the other procedures require indentureRestrictions < 2
		if (slave.indentureRestrictions >= 2) {
			const {His} = getPronouns(slave);
			procedures.push(new ForbiddenDummy(slave, "Implantation", `${His} indenture forbids invasive surgery.`));
			return procedures;
		}

		// change implant type if required
		if (curImplantData && options.replace && !options.allowedTypes.has(current.type)) {
			for (const type of options.allowedTypes.values()) {
				if (type === "none") {
					continue; // handled below
				}
				const data = App.Data.Medicine.sizingImplants[target][type];
				for (const volume of availableSizes(data)) {
					if (App.Utils.distanceToRange(volume, options.targetSize) === 0) {
						const fleshExcess = Math.ceil(volume - maxPureImplantVolume);
						if (volume <= maxPureImplantVolume || options.allowedTypes.has("none")) {
							procedures.push(creators.replace(slave, type, volume, fleshExcess));
						}
					}
				}
			}
		}

		// install an implant or replace with a bigger one
		if (distanceDecreased(current.volume + 1)) {
			let maxVolumeReached = true;
			const curAssetSize = App.Medicine.assetSize(slave, target);
			const currentCanBeReplaced = options.replace || options.allowedTypes.has(current.type);
			for (const [type, data] of Object.entries(App.Data.Medicine.sizingImplants[target])) {
				if (!options.allowedTypes.has(type)) {
					continue;
				}
				for (const volume of availableSizes(data)) {
					if (!distanceDecreased(volume)) {
						continue;
					}
					if (curAssetSize - current.volume + volume <= maxVolume) {
						maxVolumeReached = false;
					} else {
						continue;
					}
					const fleshExcess = Math.ceil(volume - maxPureImplantVolume);
					if (volume >= current.volume && distanceDecreased(volume) && (volume <= maxPureImplantVolume ||
						(options.allowedTypes.has("none")))) {
						maxVolumeReached = false;
						if (current.volume > 0) {
							if (currentCanBeReplaced && !implantsAreSame(type, data, volume, current)) {
								procedures.push(creators.replace(slave, type, volume, fleshExcess));
							}
						} else {
							procedures.push(creators.install(slave, type, volume, fleshExcess));
						}
					}
				}
			}
			if (maxVolumeReached) {
				addMaxAssetSizeReachedDummy("Bigger implants");
			}
		}

		// remove the current implant or replace with a smaller one
		if (current.volume > 0 && distanceDecreased(current.volume - 1)) {
			if (options.targetSize.max <= 0) {
				procedures.push(creators.remove(slave));
			}

			if (options.replace) {
				for (const [type, data] of Object.entries(App.Data.Medicine.sizingImplants[target])) {
					if (!options.allowedTypes.has(type)) {
						continue;
					}
					for (const volume of availableSizes(data).filter(s => s < current.volume && distanceDecreased(s))) {
						if (!implantsAreSame(type, data, volume, current)) {
							procedures.push(creators.replace(slave, type, volume, 0));
						}
					}
				}
			}
		}
		return procedures;
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.Medicine.Surgery.SizingOptions<"boobs">} [options]
	 * @returns {App.Medicine.Surgery.Procedure[]}
	 */
	function boobSizingProcedures(slave, options = {}) {
		// check if implant-related surgeries are allowed for this slave
		if (slave.breastMesh === 1) {
			const {his, His} = getPronouns(slave);
			return [new ForbiddenDummy(slave, "Implants", `${His} supportive mesh implant blocks resizing ${his} boobs.`)];
		}

		const p = App.Medicine.Surgery.Procedures.boobImplantsProcedure;
		const dOptions = substituteImplicitOptions(options, "boobs");

		const procedures = implantSizingProcedures(slave, "boobs", dOptions, p);

		// boob-specific reduction procedures
		if (slave.indentureRestrictions < 2 && slave.boobsImplant === 0 && dOptions.allowedTypes.has("none")) {
			if (App.Medicine.fleshSize(slave, "boobs") > 300) {
				if (pointIsCloserToRange(slave.boobs, slave.boobs - 200, dOptions.targetSize)) {
					procedures.push(p.reduce(slave, "reduce", 200));
				}
				if (App.Medicine.fleshSize(slave, "boobs") < 675) {
					if (pointIsCloserToRange(slave.boobs, slave.boobs - 25, dOptions.targetSize)) {
						procedures.push(p.reduce(slave, "slightly reduce", 25));
					}
				}
			}
			if ((slave.breedingMark !== 1 || V.propOutcome !== 1 || V.eugenicsFullControl === 1 || V.arcologies[0].FSRestart === "unset")) {
				if (App.Medicine.fleshSize(slave, "boobs") >= 2000 && pointIsCloserToRange(slave.boobs, 0, dOptions.targetSize)) {
					procedures.push(new App.Medicine.Surgery.Procedures.Mastectomy(slave));
				}
			}
		}
		return procedures;
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.Medicine.Surgery.SizingOptions<"butt">} [options]
	 * @returns {App.Medicine.Surgery.Procedure[]}
	 */
	function buttSizingProcedures(slave, options = {}) {
		const dOptions = substituteImplicitOptions(options, "butt");
		const p = App.Medicine.Surgery.Procedures.buttImplantsProcedure;
		const procedures = implantSizingProcedures(slave, "butt", dOptions, p);

		if (slave.buttImplant === 0 && slave.butt > 1 && dOptions.allowedTypes.has("none") &&
			slave.indentureRestrictions < 2 && pointIsCloserToRange(slave.butt, slave.butt - 1, dOptions.targetSize)) {
			procedures.push(p.reduce(slave, "reduce", 1));
		}
		return procedures;
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.Medicine.Surgery.SizingOptions<"lips">} [options]
	 * @returns {App.Medicine.Surgery.Procedure[]}
	 */
	function lipsSizingProcedures(slave, options) {
		const dOptions = substituteImplicitOptions(options, "lips");
		const p = App.Medicine.Surgery.Procedures.lipImplantProcedure;
		const procedures = implantSizingProcedures(slave, "lips", dOptions, p);

		if (slave.lips >= 10 && slave.lipsImplant === 0 && slave.indentureRestrictions < 2) {
			procedures.push(p.reduce(slave, "reduce", 10));
		}

		return procedures;
	}
}();

/**
 * Clean up extremities on removal or piercings, tats, and brands
 * For limbs use removeLimbs()
 * @param {App.Entity.SlaveState} slave
 * @param {string} part
 * @param {boolean} [cheat]
 */
globalThis.surgeryAmp = function(slave, part, cheat = false) {
	switch (part) {
		case "left ear":
			App.Medicine.Modification.removeBrand(slave, "left ear");
			slave.earShape = "none";
			slave.earT = "none";
			slave.piercing.ear.weight = 0;
			surgeryDamage(slave, 10, cheat);
			break;
		case "right ear":
			App.Medicine.Modification.removeBrand(slave, "right ear");
			slave.earShape = "none";
			slave.earT = "none";
			slave.piercing.ear.weight = 0;
			surgeryDamage(slave, 10, cheat);
			break;
		case "dick":
			slave.dick = 0;
			slave.foreskin = 0;
			slave.skill.vaginal = 0;
			slave.dickTat = 0;
			slave.dickAccessory = "none";
			slave.chastityPenis = 0;
			if (slave.vagina === -1) {
				slave.piercing.genitals.weight = 0;
				slave.piercing.genitals.smart = false;
			}
			if (slave.drugs === "penis enhancement" || slave.drugs === "hyper penis enhancement" || slave.drugs === "breast redistributors") {
				slave.drugs = "no drugs";
			}
			surgeryDamage(slave, 20, cheat);
			break;
		case "vagina":
			slave.vagina = -1;
			slave.ovaries = 0;
			slave.preg = -2;
			slave.pregSource = 0;
			slave.skill.vaginal = 0;
			slave.vaginaTat = 0;
			slave.vaginalAccessory = "none";
			slave.vaginalAttachment = "none";
			slave.chastityVagina = 0;
			if (slave.cervixImplant === 1) {
				slave.cervixImplant = 0;
			} else if (slave.cervixImplant === 3) {
				slave.cervixImplant = 2;
			}
			surgeryDamage(slave, 20, cheat);
			break;
		case "voicebox":
			slave.voice = 0;
			slave.voiceImplant = 0;
			surgeryDamage(slave, 10, cheat);
			break;
		default:
			// eslint-disable-next-line no-console
			console.log(`ERROR: Unknown amputation type: ` + part);
	}
};

/**
 * @param {FC.HumanState} slave
 * @param {"both"|"left"|"right"} side
 * @param {"normal"|"glass"|"cybernetic"|"remove"|"blind"|"blur"|"fix"} action
 */
globalThis.eyeSurgery = function(slave, side, action) {
	if (side === "both") {
		eyeSurgery(slave, "left", action);
		eyeSurgery(slave, "right", action);
		return;
	}

	if (side !== "left" && side !== "right") {
		return;
	} // make sure side can be used to access the object
	const eyeExists = slave.eye[side] !== null;

	/* actions that don't need an eye */
	switch (action) {
		case "normal":
			addEye(slave); // color overwritten by genetics
			return;
		case "glass":
			if (!eyeExists) { // color stays the same if possible
				addEye(slave);
			}
			slave.eye[side].type = 2;
			slave.eye[side].vision = 0;
			return;
		case "cybernetic":
			if (!eyeExists) { // color stays the same if possible
				addEye(slave);
			}
			slave.eye[side].type = 3;
			slave.eye[side].vision = 2;
			return;
		case "remove":
			if (eyeExists) {
				slave.eye[side] = null;
			}
			return;
		case "blind":
			if (eyeExists) {
				slave.eye[side].vision = 0;
			}
			return;
		case "blur":
			if (eyeExists) {
				slave.eye[side].vision = 1;
			}
			return;
		case "fix":
			if (eyeExists) {
				slave.eye[side].vision = 2;
			}
			if (["corrective glasses", "corrective contacts"].includes(slave.eyewear)) {
				slave.eyewear = "none";
			}
			return;
		default:
			throw Error(`${typeof action} "${action}" not found`);
	}

	function addEye(slave) {
		slave.eye[side] = new App.Entity.SingleEyeState();
		resetEyeColor(slave, side);
	}
};

/**
 * To be used during slave generation or slave styling (auto salon)
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} color to set eye to
 * @param {string} [side] "left", "right", "both"
 */
globalThis.setEyeColor = function(slave, color, side = "both") {
	if (side === "both") {
		setEyeColor(slave, color, "left");
		setEyeColor(slave, color, "right");
		return;
	}

	if (side !== "left" && side !== "right") {
		return;
	}

	slave.eye[side].iris = color;
};

/**
 * Set all colors of the eye.
 *
 * @param {FC.HumanState} slave
 * @param {string} iris
 * @param {string} pupil
 * @param {string} sclera
 * @param {string} side
 */
globalThis.setEyeColorFull = function(slave, iris = "brown", pupil = "circular", sclera = "white", side = "both") {
	if (side === "both") {
		setEyeColorFull(slave, iris, pupil, sclera, "left");
		setEyeColorFull(slave, iris, pupil, sclera, "right");
		return;
	}

	if (side !== "left" && side !== "right") {
		return;
	}

	if (iris !== "") {
		slave.eye[side].iris = iris;
	}
	if (pupil !== "") {
		slave.eye[side].pupil = pupil;
	}
	if (sclera !== "") {
		slave.eye[side].sclera = sclera;
	}
};

/**
 * Set genetic eye color
 *
 * @param {App.Entity.SlaveState} slave
 * @param {string} color
 * @param {boolean} heterochromia
 */
globalThis.setGeneticEyeColor = function(slave, color, heterochromia = false) {
	if (heterochromia) {
		slave.geneticQuirks.heterochromia = color;
	} else {
		slave.eye.origColor = color;
	}
};

/**
 * Sets the eye color to the genetic color.
 * Takes heterochromia and albinism into account.
 *
 * @param {FC.HumanState} slave
 * @param {FC.BodySideAll} [side="both"]
 */
globalThis.resetEyeColor = function(slave, side = "both") {
	if (side === "both") {
		resetEyeColor(slave, "left");
		resetEyeColor(slave, "right");
		return;
	}

	if (side !== "left" && side !== "right") {
		return;
	}

	slave.eye[side].iris = getGeneticEyeColor(slave, side, "iris");
	slave.eye[side].pupil = getGeneticEyeColor(slave, side, "pupil");
	slave.eye[side].sclera = getGeneticEyeColor(slave, side, "sclera");
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {FC.GeneticQuirk} level
 */
globalThis.induceAlbinism = function(slave, level) {
	slave.geneticQuirks.albinism = level;
	if (level < 2) {
		slave.albinismOverride = null;
		return;
	}
	slave.albinismOverride = makeAlbinismOverride(slave.race);
};

/**
 * Allowed values for limb:
 * left arm, right arm, left leg, right leg, all
 *
 * @param {App.Entity.SlaveState} slave
 * @param {FC.LimbArgumentAll} limb
 */
globalThis.removeLimbs = function(slave, limb) {
	function remove(limb, side) {
		const prosthetic = findProsthetic(slave, limbToProsthetic(slave[limb][side].type));

		if (prosthetic) {
			prosthetic[limb][side] = slave[limb][side];
		}

		slave[limb][side] = null;
	}

	function cleanLegs() {
		if (!hasAnyLegs(slave)) {
			slave.legsTat = 0;
			slave.shoes = "none";
			slave.legAccessory = "none";
			slave.heels = 0;
			if (slave.heightImplant !== 0) {
				slave.height -= slave.heightImplant * 10;
			}
			slave.heightImplant = 0;
		}
	}

	switch (limb) {
		case "left arm":
			if (!hasLeftArm(slave)) {
				return;
			}
			remove("arm", "left");
			// slave.armsTat = 0;
			if (!hasAnyArms(slave)) {
				slave.armAccessory = "none";
				// slave.nails = 0;
			}
			break;
		case "right arm":
			if (!hasRightArm(slave)) {
				return;
			}
			remove("arm", "right");
			// slave.armsTat = 0;
			if (!hasAnyArms(slave)) {
				slave.armAccessory = "none";
				// slave.nails = 0;
			}
			break;
		case "left leg":
			if (!hasLeftLeg(slave)) {
				return;
			}
			remove("leg", "left");
			cleanLegs();
			break;
		case "right leg":
			if (!hasRightLeg(slave)) {
				return;
			}
			remove("leg", "right");
			cleanLegs();
			break;
		case "all":
			removeLimbs(slave, "left arm");
			removeLimbs(slave, "right arm");
			removeLimbs(slave, "left leg");
			removeLimbs(slave, "right leg");
			break;
	}
};

/**
 * Expects amputated limbs. Will overwrite existing limbs.
 *
 * Allowed values for limb:
 * left arm, right arm, left leg, right leg, all
 *
 * @param {App.Entity.SlaveState} slave
 * @param {FC.LimbArgumentAll} limb
 * @param {number} id
 */
globalThis.attachLimbs = function(slave, limb, id) {
	/**
	 * @param {"arm"|"leg"} limb
	 * @param {FC.BodySide} side
	 */
	function attach(limb, side) {
		let prosthetic = findProsthetic(slave, limbToProsthetic(id));

		if (prosthetic) {
			slave[limb][side] = prosthetic[limb][side];
		} else {
			if (limb === "arm") {
				slave[limb][side] = new App.Entity.ArmState();
			} else {
				slave[limb][side] = new App.Entity.LegState();
			}
			slave[limb][side].type = id;
		}
	}

	switch (limb) {
		case "left arm":
			attach("arm", "left");
			break;
		case "right arm":
			attach("arm", "right");
			break;
		case "left leg":
			attach("leg", "left");
			break;
		case "right leg":
			attach("leg", "right");
			break;
		case "all":
			attachLimbs(slave, "left arm", id);
			attachLimbs(slave, "right arm", id);
			attachLimbs(slave, "left leg", id);
			attachLimbs(slave, "right leg", id);
			break;
		default:
			// eslint-disable-next-line no-console
			console.log(`ERROR: Unknown attach point: ` + limb);
	}
};

globalThis.upgradeLimbs = function(slave, newId) {
	let changed = false;

	/**
	 * @param {number} oldId
	 * @returns {number}
	 */
	function computeUpgrade(oldId) {
		if (newId < 2) {
			return oldId;
		} else if (newId === 2 && oldId === 0) {
			return 2;
		} else if (newId < 6 && oldId <= 2) {
			return newId;
		} else if (newId > 6 && oldId <= 7) {
			return newId;
		} else if (newId < 6 && oldId > 6) {
			return newId;
		} else if (newId > oldId && newId < 7) {
			return newId;
		} else {
			return oldId;
		}
	}

	/**
	 * @param {FC.LimbArgumentAll} limb
	 * @param {function(App.Entity.SlaveState): number} idFunction
	 */
	function upgradeLimb(limb, idFunction) {
		let oldId = idFunction(slave);
		if (oldId === 1) {
			return;
		}
		if (oldId > 1) {
			removeLimbs(slave, limb);
		}
		if (newId > 1) {
			attachLimbs(slave, limb, computeUpgrade(oldId));
		}
		if (oldId !== idFunction(slave)) {
			changed = true;
		}
	}

	upgradeLimb("left arm", getLeftArmID);
	upgradeLimb("right arm", getRightArmID);
	upgradeLimb("left leg", getLeftLegID);
	upgradeLimb("right leg", getRightLegID);

	return changed;
};

/**
 * Changes a slaves limbs to the specified value AND sets all related variables.
 * Intended for giving prosthetics during slave generation and events.
 *
 * @param {App.Entity.SlaveState} slave
 * @param {FC.LimbArgumentAll} limb
 * @param {number} id
 * @param {boolean} clean if the slave should be cleaned of all existing
 */
globalThis.configureLimbs = function(slave, limb, id, clean = false) {
	if (limb === "all") {
		configureLimbs(slave, "left arm", id);
		configureLimbs(slave, "right arm", id);
		configureLimbs(slave, "left leg", id);
		configureLimbs(slave, "right leg", id);
		return;
	}

	if (clean) {
		slave.PLimb = 0;
		slave.readyProsthetics = [];
	}

	let p = limbToProsthetic(id);
	if (p) {
		addProsthetic(slave, p);
	}

	if (id === 6 && slave.PLimb < 2) {
		slave.PLimb = 2;
		addProsthetic(slave, "interfaceP2");
	} else if (id === (2 || 3 || 4 || 5) && slave.PLimb < 1) {
		slave.PLimb = 1;
		addProsthetic(slave, "interfaceP1");
	} else if (id > 6 && slave.PLimb < 3) {
		slave.PLimb = 3;
		addProsthetic(slave, "interfaceP3");
	}

	switch (limb) {
		case "left arm":
			if (id !== getLeftArmID(slave)) {
				removeLimbs(slave, "left arm");
				if (id > 0) {
					attachLimbs(slave, "left arm", id);
				}
			}
			break;
		case "right arm":
			if (id !== getRightArmID(slave)) {
				removeLimbs(slave, "right arm");
				if (id > 0) {
					attachLimbs(slave, "right arm", id);
				}
			}
			break;
		case "left leg":
			if (id !== getLeftLegID(slave)) {
				removeLimbs(slave, "left leg");
				if (id > 0) {
					attachLimbs(slave, "left leg", id);
				}
			}
			break;
		case "right leg":
			if (id !== getRightLegID(slave)) {
				removeLimbs(slave, "right leg");
				if (id > 0) {
					attachLimbs(slave, "right leg", id);
				}
			}
			break;
	}
};

/**
 * Prepare and set up for new Fuckdoll
 * @param {App.Entity.SlaveState} slave
 */
globalThis.beginFuckdoll = function(slave) {
	slave.fuckdoll = 1;
	slave.toyHole = "all her holes";
	if ((slave.pubicHStyle !== "bald") && (slave.pubicHStyle !== "hairless")) {
		slave.pubicHStyle = "waxed";
	}
	slave.rules.living = "spare";
	slave.rules.speech = "restrictive";
	slave.rules.release.masturbation = 0;
	slave.rules.release.facilityLeader = 0;
	slave.rules.release.partner = 0;
	slave.rules.release.family = 0;
	slave.rules.release.slaves = 0;
	slave.rules.relationship = "restrictive";
	slave.choosesOwnClothes = 0;
	slave.clothes = "a Fuckdoll suit";
	slave.collar = "none";
	slave.faceAccessory = "none";
	slave.mouthAccessory = "none";
	if ((!hasAnyLegs(slave)) || (slave.shoes !== "none")) {
		slave.shoes = "heels";
	}
	slave.armAccessory = "none";
	slave.legAccessory = "none";
	slave.vaginalAccessory = "none";
	slave.vaginalAttachment = "none";
	slave.dickAccessory = "none";
	slave.buttplug = "none";
	slave.chastityAnus = 0;
	slave.chastityPenis = 0;
	slave.chastityVagina = 0;
	slave.attrKnown = 1;
	slave.fetishKnown = 1;
	slave.subTarget = 0;
	slave.sentence = 0;
	slave.training = 0;
	deflate(slave);
};

/**
 * Calculates the added artificiality a face surgery introduces.
 *
 * @returns {number}
 */
globalThis.faceSurgeryArtificiality = function() {
	return 25 -
		(5 * Math.trunc(V.PC.skill.medicine / 50)) -
		(10 * V.surgeryUpgrade);
};


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

	const donatrix = V.donatrix;
	const impregnatrix = V.impregnatrix;
	const receptrix = V.receptrix;

	let eligibility = 0;
	let eligibilityI = 0;
	let eligibility2 = 0;
	const donatrixID = (donatrix.ID === V.PC.ID) ? -1 : 0;

	let impreg;
	if (impregnatrix !== "undecided" && impregnatrix.ID === -1) {
		impreg = PlayerName();
	} else if (impregnatrix !== "undecided") {
		impreg = SlaveFullName(impregnatrix);
	} else {
		impreg = impregnatrix;
	}

	let receive;
	if (receptrix !== "undecided" && receptrix.ID === -1) {
		receive = PlayerName();
	} else if (receptrix !== "undecided") {
		receive = SlaveFullName(receptrix);
	} else {
		receive = receptrix;
	}

	App.UI.DOM.appendNewElement("p", node, `${(donatrixID === -1) ? `You've prepared yourself to have an egg taken from your ovaries;` : `${getSlave(V.AS).slaveName} is prepped to have an egg harvested from ${getPronouns(getSlave(V.AS)).possessive} ovaries;`} now you must select a target to fertilize it and who will carry it to term.`, "scene-intro");

	if (impreg !== "undecided" || receive !== "undecided") {
		const bearers = [];
		if (impreg !== "undecided") {
			bearers.push(`${impreg} will provide the seed`);
		}
		if (receive !== "undecided") {
			bearers.push(`${receive} shall carry it`);
		}
		App.UI.DOM.appendNewElement("div", node, `${toSentence(bearers)}.`);

		if (impreg !== "undecided" && receive !== "undecided") {
			App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
				"Implant fertilized ovum",
				() => {
					cashX(forceNeg(V.surgeryCost * 2), "slaveSurgery");
					V.surgeryType = "surrogacy";
					App.UI.reload();
				}, [], "Surrogacy",
			));
		}
	}

	App.UI.DOM.appendNewElement("h2", node, `Semen donatrix: ${impreg}`);

	for (const slave of V.slaves) {
		if (slave.balls > 0 && slave.pubertyXY === 1 && isSlaveAvailable(slave) && canBreed(donatrix, slave)) {
			App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
				SlaveFullName(slave),
				() => {
					V.impregnatrix = slave;
					App.UI.reload();
				}
			));
			eligibility = 1;
		}
	}
	if (eligibility === 0) {
		App.UI.DOM.appendNewElement("div", node, "You have no slaves with potent sperm.");
	}

	if (V.incubator.tanks.length > 0 && V.incubator.setting.reproduction === 2) {
		App.UI.DOM.appendNewElement("h2", node, `Incubator settings are resulting in large-scale fluid secretion. Select an eligible incubatee to milk for semen:`);

		for (const tank of V.incubator.tanks) {
			if (tank.balls > 0 && tank.dick > 0 && canBreed(donatrix, tank)) {
				App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
					tank.slaveName,
					() => {
						V.impregnatrix = tank;
						App.UI.reload();
					}
				));
				eligibilityI = 1;
			}
		}
		if (eligibilityI === 0) {
			App.UI.DOM.appendNewElement("div", node, "You have no growing slaves producing sperm.");
		}
	}

	if (V.PC.balls !== 0) {
		App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
			"Use your own",
			() => {
				V.impregnatrix = V.PC;
				App.UI.reload();
			}
		));
	} else if (V.PC.counter.storedCum > 0) {
		App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
			"Use a vial of your own",
			() => {
				V.impregnatrix = V.PC;
				V.PC.counter.storedCum--;
				App.UI.reload();
			}
		));
	}

	App.UI.DOM.appendNewElement("h2", node, `Chosen surrogate: ${receive}`);

	for (const slave of V.slaves) {
		if (canBeReceptrix(slave)) {
			App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
				SlaveFullName(slave),
				() => {
					V.receptrix = slave;
					App.UI.reload();
				}, [], "",
				(slave.pregType >= 4) ? `Using a slave carrying multiples is inadvisable` : ``
			));
			eligibility2 = 1;
		}
	}
	if (eligibility2 === 0) {
		App.UI.DOM.appendNewElement("div", node, "You have no slaves capable of acting as a surrogate.");
	}

	if (V.PC.vagina !== -1 && V.PC.preg >= 0 && V.PC.preg < 4 && V.PC.pregType < 8 && V.PC.physicalAge < 70) {
		App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
			"Use your own womb",
			() => {
				V.receptrix = V.PC;
				App.UI.reload();
			}
		));
	}

	return node;
};


code:
globalThis.assistant = (function() {
	return {
		BC: BC,
		object: manage,
		pronouns: pronouns,
	};

	function BC() {
		App.Update.assistantBC();
	}

	function pronouns() {
		let o = {};
		if (V.assistant.personality <= 0 || V.assistant.appearance === "normal") {
			o.main = o.market = {pronoun: App.Data.Pronouns.Kind.ai};
		} else {
			if (V.assistant.appearance === "incubus" && V.diversePronouns > 0) {
				o.main = {pronoun: App.Data.Pronouns.Kind.male};
				o.market = {pronoun: App.Data.Pronouns.Kind.female};
			} else if (V.assistant.appearance === "succubus" && V.diversePronouns > 0) {
				o.main = {pronoun: App.Data.Pronouns.Kind.female};
				o.market = {pronoun: App.Data.Pronouns.Kind.male};
			} else {
				o.main = o.market = {pronoun: App.Data.Pronouns.Kind.female};
			}
		}
		return o;
	}

	function manage() {
		V.assistant = V.assistant || {};
		Object.assign(V.assistant, {
			personality: V.assistant.personality || 0,
			name: V.assistant.name || "your personal assistant",
			power: V.assistant.power || 0,
			appearance: V.assistant.appearance || "normal",
		});

		if (V.assistant.market) {
			Object.assign(V.assistant.market, {
				relationship: V.assistant.market.relationship || "cute",
				limit: V.assistant.market.limit || 0,
				aggressiveness: V.assistant.market.aggressiveness || 0,
			});
		}
		if (V.week < 18) {
			delete V.assistant.options;
		}
	}
})();