code:
App.Medicine.Surgery.SimpleReaction = class {
	/**
	 * If a surgery was invasive a slave can die, but high player skill also has a positive effect on the slaves health
	 * @returns {boolean}
	 */
	get invasive() { return true; }

	get removeJob() { return false; }

	get permanentChanges() { return true; }

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @returns {boolean}
	 */
	_hasEmotion(slave) { return slave.fetish !== "mindbroken" && slave.fuckdoll === 0; }

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.Fetish} fetish
	 * @returns {boolean}
	 * @protected
	 */
	_strongKnownFetish(slave, fetish) {
		return (slave.fetish === fetish && slave.fetishStrength > 60 && slave.fetishKnown === 1);
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {Partial<App.Entity.SlaveState>} diff
	 */
	intro(slave, diff) {
		const {he, his, himself} = getPronouns(slave);
		const r = [];

		r.push(`As the remote surgery's long recovery cycle completes, ${slave.slaveName}`);
		if (!hasAnyLegs(slave)) {
			r.push(`is carried`);
		} else if (canWalk(slave)) {
			r.push(`walks`);
		} else {
			r.push(`is escorted`);
		}
		r.push(`out of the surgery room`);
		if (canSee(slave)) {
			r.push(`and catches sight of ${himself} in the floor-length mirror outside the door.`);
		} else {
			r.push(`and is detailed the modifications done to ${his} body, assuming ${he} hasn't already realized them.`);
		}

		return r;
	}

	/**
	 * @typedef {object} reactionResult
	 * @property {Array<Array<string|HTMLElement>>} longReaction can contain HTML, every array is a new paragraph. Shown when manually applied.
	 * @property {Array<string|HTMLElement>} shortReaction can contain HTML. May be empty. Intended for use with RA.
	 * @property {number} devotion 0 means no change
	 * @property {number} trust 0 means no change
	 */

	/**
	 * @returns {reactionResult}
	 * @protected
	 */
	_createReactionResult() {
		return {
			longReaction: [],
			shortReaction: [],
			devotion: 0,
			trust: 0,
		};
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {Partial<App.Entity.SlaveState>} diff
	 * @returns {reactionResult}
	 */
	reaction(slave, diff) {
		return this._createReactionResult();
	}

	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {Partial<App.Entity.SlaveState>} diff
	 * @param {reactionResult} previousReaction
	 * @returns {reactionResult}
	 */
	outro(slave, diff, previousReaction) {
		const reaction = this._createReactionResult();
		if (V.PC.skill.medicine < 100) {
			return reaction;
		}

		const {He, he, his, him, himself} = getPronouns(slave);
		const r = [];
		if (this.invasive) {
			r.push(`Since you performed the surgery yourself, and you do an artist's work, ${his} health is less affected by the surgery than it would have been if you'd paid some hack to do it remotely.`);
			improveCondition(slave, 5);
		}
		if (this._hasEmotion(slave)) {
			r.push(`${He} spent the surgery very aware that you were performing it personally.`);
			if (reaction.devotion < 0) {
				if (slave.devotion > 50) {
					r.push(`Though ${he} is unhappy with the results, ${he} consoles ${himself} with the knowledge that you cared enough to do it personally.`);
				} else if (slave.devotion >= -20) {
					r.push(`${He} is <span class="gold">even more afraid</span> of you afterward than ${he} would otherwise be. You must seem a cruel and near-omnipotent power to ${him}.`);
					reaction.trust -= 5;
				} else {
					r.push(`${He} is <span class="mediumorchid">even more hateful</span> of you afterward than ${he} would otherwise be. It must seem to ${him} that ${he}'s nothing more than a test subject to you.`);
					reaction.devotion -= 5;
				}
			} else if (this.permanentChanges) {
				if (slave.devotion > 50) {
					r.push(`Since ${he}'s happy with the results, ${he}'s almost beside ${himself} with <span class="hotpink">gratitude,</span> and filled with <span class="mediumaquamarine">admiration</span> of your skill.`);
					reaction.devotion += 4;
					reaction.trust += 4;
				} else if (slave.devotion >= -20) {
					r.push(`${He} is quite struck by how you performed the surgery personally. ${He} admires your refusal to be one of the idle rich, and <span class="hotpink">likes you more.</span>`);
					reaction.devotion += 5;
				} else {
					r.push(`${He} knows that ${he} should be grateful to you for performing the surgery personally, but the emotional turmoil of the occasion is too much for ${him}, and the realization makes little lasting impact.`);
				}
			}
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

/**
 * @type {Map<string, App.Medicine.Surgery.Reaction>}
 */
App.Medicine.Surgery.ReactionsMap = new Map();

App.Medicine.Surgery.Reaction = class extends App.Medicine.Surgery.SimpleReaction {
	constructor() {
		super();
		// Don't allow overwriting.
		if (App.Medicine.Surgery.ReactionsMap.has(this.key)) {
			throw new Error(`Key "${this.key}" already exists.`);
		}
		App.Medicine.Surgery.ReactionsMap.set(this.key, this);
	}

	// eslint-disable-next-line jsdoc/require-returns-check
	/**
	 * @returns {string}
	 */
	get key() { throw new Error("Method 'key()' must be implemented."); }
};


code:
App.Medicine.Surgery.Procedure = class {
	/**
	 * @param {App.Entity.SlaveState} slave
	 */
	constructor(slave) {
		/**
		 * @type {FC.Util.DiffRecorder<App.Entity.SlaveState>}
		 * @protected
		 */
		this._slave = App.Utils.Diff.getProxy(slave);
	}

	get originalSlave() {
		return this._slave.diffOriginal;
	}

	// eslint-disable-next-line jsdoc/require-returns-check
	/**
	 * @abstract
	 * @returns {string}
	 */
	get name() { throw new Error("Method 'name()' must be implemented."); }

	/**
	 * Lowercase and without punctuation at the end.
	 *
	 * @returns {string}
	 */
	get description() { return ""; }

	/**
	 * Additional note to put at the bottom of the tooltip
	 * @returns {string | DocumentFragment | HTMLElement}
	 */
	get note() { return null; }

	/**
	 * May die, but high player skill also reduces health impact.
	 *
	 * In some cases the damage may be applied outside the procedure system, the slave would still be able to die
	 * however. In these cases, invasive needs to be overwritten.
	 *
	 * @returns {boolean}
	 */
	get invasive() { return this.healthCost > 0; }

	/**
	 * The monetary cost of the surgery.
	 * @returns {number}
	 */
	get cost() {
		return Math.round(this._materialCost + this._workCost);
	}

	/**
	 * Part of the surgery cost related to the required materials
	 * @protected
	 * @returns {number}
	 */
	get _materialCost() {
		return 0;
	}

	/**
	 * Part of the surgery cost related to the work efforts
	 * This is the default cost. Subclasses should base their cost on this value if modifying it.
	 * @example
	 * get _workCost() {
	 *     // Make the surgery 4 times more expensive
	 *     return super._workCost * 4;
	 * }
	 * @protected
	 * @returns {number}
	 */
	get _workCost() {
		// TODO phase out V.surgeryCost and replace with static calculation
		return V.surgeryCost;
	}

	/**
	 * The health impact of a surgery.
	 * @returns {number}
	 */
	get healthCost() { return 0; }

	/**
	 * The value this surgery applies to the relevant slave property. Used by the RA to evaluate the effectiveness of
	 * the surgery. If numeric, it is the delta change, otherwise the result.
	 * @returns {any}
	 */
	get changeValue() { return null; }

	/**
	 * If there are any entries, the procedure cannot be applied. The reasons are the array entries.
	 * When overwriting always include disabled reasons of superclasses!
	 *
	 * @returns {Array<string>} May contain HTML
	 */
	get disabledReasons() {
		const reasons = [];

		/* disallow surgery for slaves on certain assignments */
		if (this.originalSlave.assignment === Job.AGENT ||
			this.originalSlave.assignment === Job.AGENTPARTNER) {
			const {his, him} = getPronouns(this.originalSlave);
			reasons.push(`You must retrieve ${this.originalSlave.slaveName} from ${his} assignment before performing surgery on ${him}.`);
		}

		return reasons;
	}

	// eslint-disable-next-line jsdoc/require-returns-check
	/**
	 * @param {boolean} cheat
	 * @returns {[Partial<App.Entity.SlaveState>, App.Medicine.Surgery.SimpleReaction]}
	 */
	apply(cheat) { throw new Error("Method 'apply()' must be implemented."); }

	/**
	 * Convenience function to prepare the return value for apply()
	 *
	 * @param {App.Medicine.Surgery.SimpleReaction} reaction
	 * @returns {[Partial<App.Entity.SlaveState>, App.Medicine.Surgery.SimpleReaction]}
	 * @protected
	 */
	_assemble(reaction) {
		return [this._slave.diffChange, reaction];
	}
};

license: mit


code:
App.Art.cacheArtData = function() {
	/**
	 * @param {NodeListOf<Element>} imagePassages
	 * @returns {Map<string, Element>}
	 */
	function makeCache(imagePassages) {
		const dict = new Map();

		for (const ip of imagePassages) {
			const name = ip.attributes.getNamedItem("name").value;
			let div = document.createElement("div");
			const svgData = atob(ip.innerHTML.replace(/data:image\/svg\+xml;base64,/, ''));
			div.innerHTML = svgData.trim();
			dict.set(name, div.children.item(0));
		}

		return dict;
	}

	App.Data.Art = {};
	App.Data.Art.Vector = makeCache(document.querySelectorAll('[tags="Twine.image"][name^="Art_Vector"]:not([name^="Art_Vector_Revamp"])'));
	App.Data.Art.VectorRevamp = makeCache(document.querySelectorAll('[tags="Twine.image"][name^="Art_Vector_Revamp"]'));
};

App.Art.URLIDMatcher = /url\(#(.*)\)/g;
App.Art.SvgQueue = class {
	/**
	 * @param {{trigger:string, action:string, value:string}[]} transformRules - when a 'data-transform' attribute with value "trigger" is seen on an element, perform 'action' with 'value'
	 * @param {Map<string, Element>} cache
	 * @param {string} displayClass
	 */
	constructor(transformRules, cache, displayClass) {
		this._transformRules = transformRules;
		/** @type {{attrs: NamedNodeMap, nodes: Element[]}[]} */
		this._container = [];
		this._cache = cache;
		this._displayClass = displayClass;
		this._rndID = Math.floor(Math.random() * 9007199254740991);
	}

	/** transform a node via the transform rules
	 * @param {Element} node
	 */
	_transform(node) {
		const trigger = node.getAttribute("data-transform");
		if (trigger) {
			const rule = this._transformRules.find((r) => r.trigger === trigger);
			if (rule && rule.value) {
				if (rule.action === "text-content") {
					node.textContent = rule.value;
				} else {
					// by default, set attribute (usually 'transform')
					node.setAttribute(rule.action, rule.value);
				}
			}
		}
	}

	/** select clip-path via the transform rules
	 * @param {Element} node
	 */
	_setclip(node) {
		const trigger = node.getAttribute("select_clip");
		if (trigger) {
			const rule = this._transformRules.find((r) => r.trigger === trigger);
			if (rule && rule.value) {
				// by default, set attribute (usually 'clip-path')
				node.setAttribute(rule.action, this._replaceURLRefs(rule.value));
			}
		}
	}

	/** Turn a single fixed ID into a unique one
	 * @param {string} id
	 */
	_makeUniqueID(id) {
		return `${id}_rndID_${this._rndID}`;
	}

	/** Turn any ID URL references in the target attribute string into unique ones
	 * @param {string} attr
	 * @returns {string}
	 */
	_replaceURLRefs(attr) {
		return attr.replace(App.Art.URLIDMatcher, (a, b) => `url(#${this._makeUniqueID(b)})`);
	}

	/** Append unique IDs to clip-path and filter references
	 * @param {Element} node
	 */
	_replaceIDs(node) {
		const cp = node.getAttribute('clip-path');
		if (cp) {
			node.setAttribute('clip-path', this._replaceURLRefs(cp));
		}
		const style = node.getAttribute('style');
		if (style && style.search("filter") > -1) {
			node.setAttribute('style', this._replaceURLRefs(style));
		}
		for (const nodeChild of node.children) {
			this._replaceIDs(nodeChild);
		}
	}

	/** add an SVG from the cache to the render queue
	 * @param {string} id
	 */
	add(id) {
		const res = this._cache.get(id);
		let clones = [];
		if (!res) {
			console.error(`Missing art resource: ${id}`);
			return;
		}
		for (const srcNode of res.children) {
			const node = /** @type {Element} */ (srcNode.cloneNode(true));
			if (node.nodeName === "defs") {
				for (const defNode of node.children) {
					defNode.setAttribute("id", this._makeUniqueID(defNode.id));
				}
			} else {
				this._replaceIDs(node);
			}
			this._transform(node);
			this._setclip(node);
			let transformNodes = node.querySelectorAll('g[data-transform]');
			for (const child of transformNodes) {
				this._transform(child);
			}
			let clipNodes = node.querySelectorAll('g[select_clip]');
			for (const child of clipNodes) {
				this._setclip(child);
			}
			clones.push(node);
		}
		this._container.push({attrs: res.attributes, nodes: clones});
	}

	/** concatenate the contents of a second queue into this one.
	 * displayClass must match. cache and transformFunc may differ (they are used only by add).
	 * @param {App.Art.SvgQueue} queue
	 */
	concat(queue) {
		if (this._displayClass !== queue._displayClass) {
			throw Error("Incompatible SVG queues. displayClass must match.");
		}
		this._container.push(...queue._container);
	}

	/** merge consecutive svg child nodes in the queue with the same svg attributes into bigger svg nodes, and write them out
	 * this prevents re-evaluating viewboxes and classes unnecessarily and improves layout performance with lots of art
	 * @returns {DocumentFragment}
	 */
	output() {
		/** evaluate whether an attribute list is equivalent or not
		 * @param {NamedNodeMap} left
		 * @param {NamedNodeMap} right
		 */
		function equalAttributes(left, right) {
			/** get all the attribute names from an attribute list
			 * @param {NamedNodeMap} attrs
			 * @returns {string[]}
			 */
			function attrNames(attrs) {
				let names = [];
				for (let index = 0; index < attrs.length; ++index) {
					names.push(attrs[index].nodeName);
				}
				return names;
			}

			if (!left && !right) {
				return true; // both are nullish, treat as equal
			} else if (!left || !right) {
				return false; // only one is nullish, not equal
			}

			const leftNames = attrNames(left);
			const rightNames = attrNames(right);
			const intersectionLength = _.intersection(leftNames, rightNames).length;
			if (leftNames.length !== intersectionLength || rightNames.length !== intersectionLength) {
				return false; // contain different attributes, not equal
			}
			// are all values equal?
			return leftNames.every((attr) => left.getNamedItem(attr).nodeValue === right.getNamedItem(attr).nodeValue);
		}

		let frag = document.createDocumentFragment();
		let currentAttributes;
		let outSvg;
		for (const svg of this._container) {
			if (!equalAttributes(currentAttributes, svg.attrs)) {
				outSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
				outSvg.setAttribute("class", this._displayClass);
				frag.appendChild(outSvg);
				for (let index = 0; index < svg.attrs.length; ++index) {
					outSvg.setAttribute(svg.attrs[index].nodeName, svg.attrs[index].nodeValue);
				}
				currentAttributes = svg.attrs;
			}
			outSvg.append(...svg.nodes);
		}
		return frag;
	}
};


code:
/**
 * The diff proxy records all changes to the original object without changing the original object while emulating the
 * changes for access from outside. The resulting diff object can then be applied on the original. The resulting object
 * is the same as if the changes were applied directly to the original object.
 *
 * NOTE: When doing any changes to this, rerun the tests cases at devNotes/tests.
 */
App.Utils.Diff = (function() {
	const deletedSymbol = Symbol("deleted property");

	/**
	 * @template {object} T
	 * @param {T} base
	 * @returns {FC.Util.DiffRecorder<T>}
	 */
	function getProxy(base) {
		const diff = {};

		/**
		 * @typedef {Array<string|symbol>} path
		 * Can only point to objects or arrays, never to primitives
		 */

		/**
		 * @param {path} path
		 * @returns {any}
		 */
		function localDiff(path) {
			let value = diff;
			for (const key of path) {
				if (key in value) {
					value = value[key];
				} else {
					return {};
				}
			}
			return value;
		}

		/**
		 * @param {path} path
		 * @param {string|symbol} prop
		 * @param {any} value
		 */
		function setDiff(path, prop, value) {
			let localDiff = diff;
			/**
			 * @type {object}
			 */
			let original = base;
			let originalLost = false; // True, if the original object does not have this path
			for (const key of path) {
				if (key in original) {
					original = original[key];
				} else {
					originalLost = true;
				}
				if (!(key in localDiff)) {
					if (originalLost) {
						// Should not happen
						throw new TypeError("Neither original nor diff have this property: " + path);
					}

					if (_.isArray(original)) {
						// clone arrays to make sure push, pop, etc. operations don't mess anything up later
						// Deep copy in case array entries get modified later
						localDiff[key] = _.cloneDeep(original);
					} else if (_.isObject(original)) {
						// TODO: check if there is a way to keep the data structure valid without the need for deep clone
						localDiff[key] = _.cloneDeep(original);
					}
				}
				localDiff = localDiff[key];
			}
			localDiff[prop] = value;
		}

		/**
		 * @template {object} T
		 * @param {T} target
		 * @param {path} path
		 * @returns {FC.Util.DiffRecorder<T>|T}
		 */
		function makeProxy(target, path) {
			if (target == null) { // intentionally ==
				return target;
			}

			if (_.isArray(target)) {
				return new Proxy(target, {
					get: function(o, prop) {
						if (prop === "diffOriginal") {
							return o;
						} else if (prop === "diffChange") {
							return localDiff(path);
						}

						const value = prop in localDiff(path) ? localDiff(path)[prop] : o[prop];
						if (typeof value === "function") {
							if (prop in localDiff(path)) {
								return value.bind(localDiff(path));
							}
							if (["push", "pop", "shift", "unshift", "splice", "fill", "reverse"].includes(prop)) {
								// Deep copy in case array entries get modified later
								const diffArray = _.cloneDeep(o);
								setDiff(path.slice(0, -1), path.last(), diffArray);
								return value.bind(diffArray);
							}
							return value.bind(o);
						}
						if (value === deletedSymbol) {
							return undefined;
						}
						return makeProxy(value, [...path, prop]);
					},
					set: function(o, prop, value) {
						setDiff(path, prop, value);
						return true;
					},
					deleteProperty: function(o, prop) {
						setDiff(path, prop, deletedSymbol);
						return true;
					}
				});
			}

			if (_.isObject(target)) {
				return new Proxy(target, {
					get: function(o, prop) {
						if (prop === "diffOriginal") {
							return o;
						} else if (prop === "diffChange") {
							return localDiff(path);
						}

						if (prop in localDiff(path)) {
							if (localDiff(path)[prop] === deletedSymbol) {
								return undefined;
							}
							return makeProxy(localDiff(path)[prop], [...path, prop]);
						}
						return makeProxy(o[prop], [...path, prop]);
					},
					set: function(o, prop, value) {
						setDiff(path, prop, value);
						return true;
					},
					deleteProperty: function(o, prop) {
						setDiff(path, prop, deletedSymbol);
						return true;
					}
				});
			}

			return target;
		}

		// base is an object, therefore makeProxy() returns FC.Util.DiffRecorder
		// @ts-ignore
		return makeProxy(base, []);
	}

	/**
	 * @template {object} T
	 * @param {T} base
	 * @param {Partial<T>} diff
	 */
	function applyDiff(base, diff) {
		for (const key in diff) {
			const value = diff[key];
			// @ts-ignore
			if (value === deletedSymbol) {
				delete base[key];
			} else if (!_.isObject(value)) {
				base[key] = value;
			} else if (_.isArray(value)) {
				base[key] = value;
			} else if (base[key] === undefined || base[key] === null) {
				base[key] = value;
			} else {
				applyDiff(base[key], value);
			}
		}
	}

	return {
		getProxy: getProxy,
		applyDiff: applyDiff,
	};
})();


code:
App.Loader = (function() {
	/**
	 * Remember the last loaded script.
	 * @type {string}
	 */
	let lastScript = "";

	/**
	 * @param {string} name
	 * @param {string} path Relative to the HTML file
	 * @returns {HTMLScriptElement}
	 */
	function loadScript(name, path) {
		lastScript = name;

		const script = document.createElement("script");
		script.setAttribute("src", `${path}`);
		document.head.append(script);
		return script;
	}

	/**
	 * To make sure the scripts are loaded series, keep a queue of scripts to be loaded and only load the next once the
	 * previous one is finished.
	 *
	 * @see nextScript
	 *
	 * @type {Array<()=>void>}
	 */
	const scriptQueue = [];

	class Group {
		/**
		 * @param {string} path
		 */
		constructor(path) {
			this._path = path;
			this._scripts = [];

			group.set(path, this);
			scriptQueue.push(() => {
				this._scripts.push(loadScript(path, path + "/index.js"));
			});
		}

		/**
		 * Loads a script as part of this group
		 * @param {string} subPath relative to group path
		 */
		queueSubscript(subPath) {
			scriptQueue.push(() => {
				this._scripts.push(loadScript(subPath, this._path + "/" + subPath + ".js"));
			});
		}

		/**
		 * Removes all script elements belonging to this Group. Does not undo any changes these scripts did.
		 */
		unload() {
			for (const script of this._scripts) {
				document.head.removeChild(script);
			}
		}
	}

	/**
	 * @type {Map<string, Group>}
	 */
	const group = new Map();

	/**
	 * Loads the group located at path
	 *
	 * @param {string} path
	 */
	function loadGroup(path) {
		if (group.has(path)) {
			group.get(path).unload();
		}
		new Group(path);
	}

	/**
	 * Gives the group located at path
	 *
	 * @param {string} path
	 * @returns {Group}
	 */
	function getGroup(path) {
		return group.get(path);
	}

	function nextScript() {
		if (scriptQueue.length > 0) {
			scriptQueue.shift()();
		}
	}

	return {
		loadGroup: loadGroup,
		getGroup: getGroup,
		nextScript: nextScript,
		executeTests: () => {
			loadGroup("../tests");
			nextScript();
		},
		get lastScript() {
			return lastScript;
		},
		get hasNextScript() {
			return scriptQueue.length > 0;
		}
	};
})();

/**
 * Dedicated Modding API to abstract the loading mechanisms
 */
App.Modding = (function() {
	let loadingDone = false;
	/**
	 * @type {Mod}
	 */
	let currentMod = null;

	/**
	 * @type {string[]}
	 */
	let modsToLoad = [];

	/**
	 * @type {Mod[]}
	 */
	const loadedMods = [];

	class Mod {
		/**
		 * @param {string} directory where this mod is located relative to 'mods/'
		 */
		constructor(directory) {
			this._path = "./mods/" + directory;
			this.name = directory;
			this.version = "0";
			this.description = "";

			currentMod = this;
			loadedMods.push(this);

			App.Loader.loadGroup(this._path);
			App.Loader.nextScript();
		}

		/**
		 * @returns {string}
		 */
		get path() {
			return this._path;
		}

		/**
		 * @param {string} path
		 */
		addSubscript(path) {
			App.Loader.getGroup(this._path).queueSubscript(path);
		}
	}

	/**
	 * Loads the next subscript or mod
	 */
	function nextScript() {
		if (App.Loader.hasNextScript) {
			App.Loader.nextScript();
		} else if (modsToLoad.length > 0) {
			new Mod(modsToLoad.shift());
		} else {
			loadingDone = true;
		}
	}

	/**
	 * Load all mods
	 */
	function loadMods() {
		modsToLoad = loadModList();
		nextScript();
	}

	/**
	 * @returns {string[]} modList
	 */
	function loadModList() {
		return SugarCube.storage.get("modList") || [];
	}

	return {
		internal: {
			load: loadMods,
			/** @returns {string[]} modList */
			get modList() {
				return loadModList();
			},
			/** @param {string[]} modList */
			set modList(modList) {
				SugarCube.storage.set("modList", modList);
			},
			get loadedMods() {
				return loadedMods;
			},
			get done() {
				return loadingDone;
			}
		},
		scriptDone: nextScript,
		get currentMod() {
			return currentMod;
		},
	};
})();

App.UI.playerMods = function() {
	let modList = loadModList();
	const container = document.createElement("div");
	container.append(makeModSettings());
	return container;

	function makeModSettings() {
		const f = new DocumentFragment();
		App.UI.DOM.appendNewElement("h2", f, "Player mods");
		App.UI.DOM.appendNewElement("div", f, "Player mods are mods loaded from external files located at 'mods/'. If a mod does not exist loading will fail and never finish. Mods are save independent.");
		if (!App.Modding.internal.done) {
			f.append("Mods have not finished loading. ", App.UI.DOM.link("Refresh", () => {
				modList = loadModList();
				refresh();
			}));
		}
		App.UI.DOM.appendNewElement("h3", f, "Currently loaded mods");
		f.append(loadedList());
		App.UI.DOM.appendNewElement("h3", f, "Edit mod list");
		f.append(makeEditor());
		return f;
	}

	/**
	 * @returns {string[]}
	 */
	function loadModList() {
		if (App.Status.storyReady) {
			return App.Modding.internal.modList;
		} else {
			return [];
		}
	}

	function loadedList() {
		const loadedMods = App.Modding.internal.loadedMods;
		const div = document.createElement("div");
		div.classList.add("grid-3columns-auto");
		for (const mod of loadedMods) {
			App.UI.DOM.appendNewElement("div", div, mod.name);
			App.UI.DOM.appendNewElement("div", div, mod.version);
			App.UI.DOM.appendNewElement("div", div, mod.description);
		}
		return div;
	}

	function refresh() {
		$(container).empty().append(makeModSettings());
	}

	function makeEditor() {
		const div = document.createElement("div");
		div.append("Add new mod. Input the exact name of the directory for the mod at 'mods/': ",
			App.UI.DOM.makeTextBox("", name => {
				modList.push(name);
				refresh();
			}));

		App.UI.DOM.appendNewElement("div", div, "Mods are loaded from top to bottom.");

		const listDiv = document.createElement("div");
		for (let i = 0; i < modList.length; i++) {
			const row = document.createElement("div");
			row.append(up(i), " ", down(i), " ", remove(i), " ", modList[i]);
			listDiv.append(row);
		}
		div.append(listDiv);

		div.append(App.UI.DOM.link("Finalize (Will reload the game!)", () => {
			App.Modding.internal.modList = modList;
			window.location.reload();
		}));
		return div;
	}

	/**
	 * @param {number} index
	 * @returns {HTMLElement}
	 */
	function up(index) {
		if (index === 0) {
			return App.UI.DOM.makeElement("span", "Up", ["gray"]);
		}
		const button = App.UI.DOM.makeElement("a", "Up");
		button.onclick = () => {
			arraySwap(modList, index, index - 1);
			refresh();
		};
		return button;
	}

	/**
	 * @param {number} index
	 * @returns {HTMLElement}
	 */
	function down(index) {
		if (index === modList.length - 1) {
			return App.UI.DOM.makeElement("span", "Down", ["gray"]);
		}
		const button = App.UI.DOM.makeElement("a", "Down");
		button.onclick = () => {
			arraySwap(modList, index, index + 1);
			refresh();
		};
		return button;
	}

	/**
	 * @param {number} index
	 * @returns {HTMLElement}
	 */
	function remove(index) {
		const button = App.UI.DOM.makeElement("a", "Remove");
		button.onclick = () => {
			modList.splice(index, 1);
			refresh();
		};
		return button;
	}
};


code:
Number.prototype.isBetween = function(min, max, inclusive = false) {
	return inclusive ? this >= min && this <= max : this > min && this < max;
};


code:
App.Medicine.Surgery.Reactions.AddDick = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, His, his, him} = getPronouns(slave);
		const r = [];

		if (slave.fetish === "mindbroken") {
			r.push(`The surgery is invasive, and ${he} spends some time in the autosurgery, slowly recovering. As feeling slowly returns to the lower half of ${his} body and ${his} thoughts coalesce against the dissipating fog of surgical pharmacology, ${he} strains to focus on ${his} groin${(canSee(slave)) ? `, visible in a ceiling mirror above ${him}` : ``}. ${His} eyes open wide as ${he} takes in the new member between ${his} legs. Suddenly, it twitches for the first time. The mixed pain of the terribly sore area and pleasure of new sensations floods ${him}, and ${he} jerks against the surgical restraints, gasping for breath. ${his} health has been severely affected by the intrusive surgery.`);
		} else if (slave.devotion > 50) {
			r.push(`The surgery is invasive, and ${he} spends some time in the autosurgery, slowly recovering. As feeling slowly returns to the lower half of ${his} body and ${his} thoughts coalesce against the dissipating fog of surgical pharmacology, ${he} strains to focus on ${his} groin${(canSee(slave)) ? `, visible in a ceiling mirror above ${him}` : ``}. As a devoted slave, ${he} knew the essentials of the surgery before it was performed, so ${he}'s excited to`);
			if (canSee(slave)) {
				r.push(`see`);
			} else {
				r.push(`feel`);
			}
			r.push(`the result. ${His} eyes open wide as ${he} takes in the new member between ${his} legs. Suddenly, it twitches for the first time. The mixed pain of the terribly sore area and pleasure of new sensations floods ${him}, and ${he} jerks against the surgical restraints, gasping for breath. ${He} is tremendously impressed that you would devote such immense resources to altering ${his} body, and is more willing than ever to submit to your plans for ${his} future. ${his} health has been severely affected by the intrusive surgery.`);
			reaction.trust += 5;
			reaction.devotion += 5;
		} else if (slave.devotion >= -20) {
			r.push(`The surgery is invasive, and ${he} spends some time in the autosurgery, slowly recovering. As feeling slowly returns to the lower half of ${his} body and ${his} thoughts coalesce against the dissipating fog of surgical pharmacology, ${he} focuses on ${his} groin${(canSee(slave)) ? `, visible in a ceiling mirror above ${him}` : ``}. ${He} slowly becomes aware of the soft new member resting between ${his} legs. ${He} has a penis. The thought process is visible on ${his} face as the fact sinks in. ${He} has a penis! Unsure what to make of this unbelievable development, ${he}`);
			if (canSee(slave)) {
				r.push(`turns ${his} gaze away from the mirror and`);
			}
			r.push(`shuts ${his} eyes tightly. ${He} is terrified of your apparently untrammeled power over ${his} body, so much so that ${he} is now more willing to submit to your plans for ${his} future. ${his} health has been severely affected by the intrusive surgery.`);
			reaction.trust -= 5;
			reaction.devotion += 5;
		} else {
			r.push(`The surgery is invasive, and ${he} spends some time in the autosurgery, slowly recovering. As feeling slowly returns to the lower half of ${his} body and ${his} thoughts coalesce against the dissipating fog of surgical pharmacology, ${he} focuses on ${his} groin${(canSee(slave)) ? `, visible in a ceiling mirror above ${him}` : ``}. ${He} slowly becomes aware of the soft new member resting between ${his} legs. ${He} has a penis. The thought process is visible on ${his} face as the fact sinks in. ${He} has a penis!`);
			if (slave.voice === 0) {
				r.push(`${He} tries to scream, but as a mute all ${he} can manage is pathetic, silent thrashing against the surgical restraints.`);
			} else {
				r.push(`A huge scream rises out of ${him}, going on and on, until ${he} pauses to gasp for breath so ${he} can go on screaming.`);
			}
			r.push(`${He} is terrified of your apparently untrammeled power over ${his} body, and furious at ${his} lack of control over ${his} own person. ${his} health has been severely affected by the intrusive surgery.`);
			reaction.trust -= 5;
			reaction.devotion -= 5;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAddDick = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Implant";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		if (this._slave.prostate === 0) {
			this._slave.prostate = 1;
		}
		this._slave.dick = 2;
		this._slave.clit = 0;
		this._slave.foreskin = this._slave.dick;
		return this._assemble(new App.Medicine.Surgery.Reactions.AddDick());
	}
};


code:
App.Medicine.Surgery.Reactions.AddForeskin = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, His, his, him} = getPronouns(slave);
		const r = [];

		if (slave.dick > 0) {
			r.push(`${He} leaves the surgery gingerly, knowing ${he}'s had surgery on ${his} dick. Taking the first opportunity to check out ${his} member${(canSee(slave)) ? ` in a mirror` : ``}, ${he}'s`);
			if (slave.fetish === "mindbroken") {
				r.push(`confused to find nothing has really changed.`);
			} else if (slave.devotion > 50) {
				r.push(`titillated to find that ${he} has a foreskin. ${He} examines it`);
				if (canSee(slave)) {
					r.push(`carefully and then touches it,`);
				}
				r.push(`very gently. ${He}'s very sore, obviously, but the sudden sensation is almost too much for ${him}. ${His} resolution to investigate ${his} remodeled dick, but later, is almost visible. It's obvious that ${he} can hardly wait, anticipating all the new sensations that this new and very sensitive patch of skin can offer ${him}. ${He}'s grateful to you for improving ${his} dick.`);
				reaction.trust += 5;
				reaction.devotion += 5;
			} else if (slave.devotion >= -20) {
				r.push(`quite surprised, and a little relieved, to find that ${he} has a foreskin. ${He} examines it`);
				if (canSee(slave)) {
					r.push(`carefully and then touches it,`);
				}
				r.push(`very gently. ${He}'s very sore, obviously, but the sudden sensation is almost too much for ${him}. ${He} seems to have been worried that something more dramatic than a reversal of circumcision had been done to ${him}, but ${his} chief reaction is mystified submission to you afterward. Your total power over ${his} body has been made clear to ${him} in a way that provokes confusion, not fear.`);
				reaction.devotion += 5;
			} else {
				r.push(`shocked to find that ${he} has a foreskin. ${He}'s not exactly resentful of you for doing this, since whatever ${his} feelings about circumcision might be, it's very far from what ${he} feared might be done to ${his} dick. ${His} reaction is dominated by fear of the unknown, since ${he}'s now wondering whether ${he} has any ability to predict your actions at all. Whatever ${his} mental model of you was before this, it probably didn't include you restoring ${his} foreskin.`);
				reaction.trust -= 5;
			}
		} else {
			r.push(`${He} leaves the surgery gingerly, knowing ${he}'s had surgery on ${his} genitals. Taking the first opportunity to check out ${his} crotch${(canSee(slave)) ? ` in a mirror` : ``}, ${he}'s`);
			if (slave.fetish === "mindbroken") {
				r.push(`confused to find that ${his} clitoris is missing.`);
			} else if (slave.devotion > 50) {
				r.push(`titillated to find that ${his} clitoris has a hood. ${He} examines it`);
				if (canSee(slave)) {
					r.push(`carefully and then touches it,`);
				}
				r.push(`very gently. ${He}'s very sore, obviously, but the sudden sensation is almost too much for ${him}. ${His} resolution to investigate ${his} remodeled clit, but later, is almost visible. It's obvious that ${he} can hardly wait, anticipating all the new sensations that this new patch of skin can offer ${him}. ${He}'s grateful to you for improving ${his} pussy.`);
				reaction.trust += 5;
				reaction.devotion += 5;
			} else if (slave.devotion >= -20) {
				r.push(`quite surprised, and a little relieved, to find that ${his} clit has a hood. ${He} examines it`);
				if (canSee(slave)) {
					r.push(`carefully and then touches it,`);
				}
				r.push(`very gently. ${He}'s very sore, obviously, but the sudden sensation is almost too much for ${him}. ${He} seems to have been worried that something more dramatic than a reversal of circumcision had been done to ${him}, but ${his} chief reaction is mystified submission to you afterward. Your total power over ${his} body has been made clear to ${him} in a way that provokes confusion, not fear.`);
				reaction.devotion += 5;
			} else {
				r.push(`shocked to find that ${his} clitoris has a hood. ${He}'s not exactly resentful of you for doing this, since whatever ${his} feelings about circumcision might be, it's very far from what ${he} feared might be done to ${his} clit. ${His} reaction is dominated by fear of the unknown, since ${he}'s now wondering whether ${he} has any ability to predict your actions at all. Whatever ${his} mental model of you was before this, it probably didn't include you restoring ${his} clitoral hood.`);
				reaction.trust -= 5;
			}
		}
		r.push(`As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAddForeskin = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Graft on";
	}

	get healthCost() {
		return 10;
	}

	apply(cheat) {
		if (this._slave.dick > 0) {
			this._slave.foreskin = this._slave.dick;
		} else if (this._slave.clit > 0) {
			this._slave.foreskin = this._slave.clit;
		} else {
			this._slave.foreskin = 1;
		}
		return this._assemble(new App.Medicine.Surgery.Reactions.AddForeskin());
	}
};


code:
App.Medicine.Surgery.Reactions.AddOvaries = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him} = getPronouns(slave);
		const r = [];

		if (slave.fetish === "mindbroken") {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but lacks the mental faculties to realize that ${he} now has a chance to get pregnant. As with all surgery ${his} health has been slightly affected.`);
		} else if (this._strongKnownFetish(slave, "pregnancy") || slave.origin === "$He sold $himself to you in the hope of someday bearing children.") {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to know that it means ${he} now has a chance to get pregnant. ${He} is filled with joy whenever ${he} thinks about the fact that feeling a life growing within ${him} is now, at last, a possibility. ${He} now places total faith in your plans for ${his} body and adores you for giving ${him} a gift ${he} once thought impossible. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust += 10;
			reaction.devotion += 10;
		} else if (slave.devotion > 50) {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to know that it means ${he} is now internally indistinguishable from a natural, unaltered woman. ${He} is tremendously impressed that you would devote such immense resources to altering ${his} body, and is more willing than ever to submit to your plans for ${his} future. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust += 5;
			reaction.devotion += 5;
		} else if (slave.devotion >= -20) {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to know that it means ${he} is now internally indistinguishable from a natural, unaltered woman. ${He} is terrified of your apparently untrammeled power over ${his} body, so much so that ${he} is now more willing to submit to your plans for ${his} future. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust -= 5;
			reaction.devotion += 5;
		} else {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to know that it means ${he} is now internally indistinguishable from a natural, unaltered woman. ${He} is terrified of your apparently untrammeled power over ${his} body, and furious at ${his} lack of control over ${his} own person. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust -= 5;
			reaction.devotion -= 5;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};
App.Medicine.Surgery.Reactions.AddAnimalOvaries = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him} = getPronouns(slave);
		const r = [];

		if (slave.fetish === "mindbroken") {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but lacks the mental faculties to realize that ${he} now has a chance of carrying an animal baby to term. As with all surgery ${his} health has been slightly affected.`);
		} else if (this._strongKnownFetish(slave, "pregnancy") || slave.origin === "$He sold $himself to you in the hope of someday bearing children.") {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to know that it means ${he} now has a chance to get pregnant. ${He} is filled with joy whenever ${he} thinks about the fact that feeling a life growing within ${him} is now, at last, a possibility. ${He} now places total faith in your plans for ${his} body and adores you for giving ${him} a gift ${he} once thought impossible. ${He} doesn't realize exactly what the surgery entailed, however — the happiness ${he} felt at first will most likely be replaced with horror once ${he} realizes the babies in ${his} womb are not human. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust += 10;
			reaction.devotion += 10;
		} else if (slave.devotion > 50) {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to know that it means ${he} is now internally indistinguishable from a natural, unaltered woman — at least, that's what ${he} believes. Little does ${he} know that ${he} is now capable of carrying animal babies in ${his} womb. ${He} is tremendously impressed that you would devote such immense resources to altering ${his} body, and is more willing than ever to submit to your plans for ${his} future. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust += 5;
			reaction.devotion += 5;
		} else if (slave.devotion >= -20) {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to know that it means ${he} is now internally indistinguishable from a natural, unaltered woman — at least, that's what ${he} believes. Little does ${he} know that ${he} is now capable of carrying animal babies in ${his} womb. ${He} is terrified of your apparently untrammeled power over ${his} body, so much so that ${he} is now more willing to submit to your plans for ${his} future. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust -= 5;
			reaction.devotion += 5;
		} else {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to know that it means ${he} is now internally indistinguishable from a natural, unaltered woman — at least, that's what ${he} believes. Little does ${he} know that ${he} is now capable of carrying animal babies in ${his} womb. ${He} is terrified of your apparently untrammeled power over ${his} body, and furious at ${his} lack of control over ${his} own person. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust -= 5;
			reaction.devotion -= 5;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAddOvaries = class extends App.Medicine.Surgery.Procedure {
	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.AnimalType} eggType
	 * @param {string} pregDataKey
	 */
	constructor(slave, eggType, pregDataKey) {
		super(slave);
		this.eggType = eggType;
		this.pregDataKey = pregDataKey;
	}

	get name() {
		return "Implant";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		this._slave.ovaries = 1;
		this._slave.eggType = this.eggType;
		this._slave.preg = 0;
		/**
		 * @type {FC.PregnancyData}
		 */
		// @ts-ignore
		const data = {};
		deepAssign(data, App.Data.misc.pregData[this.pregDataKey]);
		this._slave.pregData = data;
		if (this._slave.pubertyXX === 0 && this._slave.physicalAge >= V.fertilityAge) {
			if (V.precociousPuberty === 1) {
				this._slave.pubertyAgeXX = this._slave.physicalAge + 1;
			} else {
				this._slave.pubertyXX = 1;
			}
		}

		if (this.eggType === "human") {
			return this._assemble(new App.Medicine.Surgery.Reactions.AddOvaries());
		} else {
			return this._assemble(new App.Medicine.Surgery.Reactions.AddAnimalOvaries());
		}
	}
};

App.Medicine.Surgery.Procedures.OFReplaceOvaries = class extends App.Medicine.Surgery.Procedure {
	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.AnimalType} eggType
	 * @param {string} pregDataKey
	 */
	constructor(slave, eggType, pregDataKey) {
		super(slave);
		this.eggType = eggType;
		this.pregDataKey = pregDataKey;
	}

	get name() {
		return "Replace";
	}

	get description() {
		return "you can replace the existing ovaries with a new pair";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		this._slave.eggType = this.eggType;
		this._slave.preg = 0;
		/**
		 * @type {FC.PregnancyData}
		 */
		// @ts-ignore
		const data = {};
		deepAssign(data, App.Data.misc.pregData[this.pregDataKey]);
		this._slave.pregData = data;
		if (this._slave.pubertyXX === 0 && this._slave.physicalAge >= V.fertilityAge) {
			if (V.precociousPuberty === 1) {
				this._slave.pubertyAgeXX = this._slave.physicalAge + 1;
			} else {
				this._slave.pubertyXX = 1;
			}
		}

		if (this.eggType === "human") {
			return this._assemble(new App.Medicine.Surgery.Reactions.AddOvaries());
		} else {
			return this._assemble(new App.Medicine.Surgery.Reactions.AddAnimalOvaries());
		}
	}
};


code:
App.Medicine.Surgery.Reactions.AddProstate = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, His, his, him, himself} = getPronouns(slave);
		const r = [];

		r.push(`It's not immediately apparent to ${him} what kind of surgery ${he} received, since all ${he}'s left with is a terrible nonspecific ache in ${his} lower parts. As with all surgery ${his} health has been slightly affected.`);
		if (this._hasEmotion(slave)) {
			if (slave.devotion > 50) {
				if (slave.anus === 0) {
					r.push(`Since ${he}'s still an anal virgin, ${he}'s denied the most obvious way of discovering what's been added to ${him}. The revelation is deferred until the next time ${he} orgasms:`);
				} else if (canDoAnal(slave) || canDoVaginal(slave)) {
					r.push(`The next time ${he}'s penetrated, though, it's obvious:`);
				} else if (canPenetrate(slave)) {
					r.push(`The next time ${he} uses ${his} cock, though, it's obvious:`);
				} else {
					r.push(`The next time ${he} climaxes, though, it's obvious:`);
				}
				r.push(`${he}'s shocked by the astounding volume of ${his}`);
				if (slave.dick === 0) {
					r.push(`squirting female`);
				}
				r.push(`ejaculation, since ${his} new organ is of course hooked into ${his} urethra and does its duty whenever ${he} climaxes${(slave.balls === 0) ? ` despite ${his} lack of testicles to add actual semen to ${his} copious emissions` : ``}.`);
				if (slave.fetishKnown === 1 && slave.fetish === "buttslut") {
					r.push(`${He} was already an anal slut, but ${he} can now experience anal pleasure along an entirely new dimension. ${He}'s extremely grateful, and a more eager buttslut than ever.`);
					reaction.devotion += 5;
					slave.fetishStrength = Math.clamp(slave.fetishStrength + 20, 0, 100);
				} else {
					r.push(`As a sex slave, ${he}`);
					if (canHear(slave)) {
						r.push(`has of course heard of`);
					} else {
						r.push(`is of course familiar with`);
					}
					r.push(`the pleasure of prostate stimulation,`);
					if (slave.genes === "XY") {
						r.push(`not to mention having once had one ${himself},`);
					}
					r.push(`and ${he}'s <span class="devotion inc">duly grateful</span> that you've altered ${him} in a way that will make sexual duties more enjoyable.`);
				}
				reaction.devotion += 5;
			} else if (slave.devotion > 20) {
				r.push(`The next time ${he} orgasms, ${he}'s shocked by the astounding volume of ${his}`);
				if (slave.dick === 0) {
					r.push(`squirting female`);
				}
				r.push(`ejaculation, since ${his} new organ is of course hooked into ${his} urethra and does its duty whenever ${he} climaxes${(slave.balls === 0) ? ` despite ${his} lack of testicles to add actual semen to ${his} copious emissions` : ``}. ${His} main reaction is confusion; this alteration is pretty far outside any of the more conventional surgeries slaves might expect to receive, and ${his} mixed feeling tend to cancel each other out.`);
			} else {
				r.push(`${He} doesn't have to know exactly what you did, though, to be mortified and infuriated by your messing around inside ${his} body. ${He} hasn't yet learned to accept that you control ${him} completely, down to the arrangement and even presence of ${his} internal organs.`);
				reaction.trust -= 10;
				reaction.devotion -= 10;
			}
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAddProstate = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Prostate";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		this._slave.prostate = 1;
		return this._assemble(new App.Medicine.Surgery.Reactions.AddProstate());
	}
};


code:
App.Medicine.Surgery.Reactions.AddScrotum = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him, himself} = getPronouns(slave);
		const r = [];

		r.push(`${He} leaves the surgery gingerly, knowing ${he}'s had surgery on ${his} junk. Taking the first opportunity to`);
		if (canSee(slave)) {
			r.push(`look at ${his} crotch in a mirror,`);
		} else {
			r.push(`feel ${his} crotch,`);
		}
		r.push(`${he}'s`);
		if (slave.fetish === "mindbroken") {
			r.push(`confused to find something new dangling there.`);
		} else if (slave.devotion > 50) {
			r.push(`overjoyed to find that ${he} has a ballsack. ${He} very obviously starts to think about how it will affect ${his} various functions as a sex slave. First, ${he} flexes ${his} Kegel muscles,`);
			if (canSee(slave)) {
				r.push(`watching raptly as`);
			} else {
				r.push(`giggling as ${he} feels`);
			}
			r.push(`${his} testicles move within the new pouch of soft skin beneath ${his} cock. Then ${he} starts to bounce and hump a little, this way and that, experimenting with how they move as ${he} performs fucking motions. ${He}'s looking forward to having fun with this latest alteration of ${his} body, and is more willing than ever to submit to your plans for ${his} future.`);
			reaction.trust += 5;
			reaction.devotion += 5;
		} else if (slave.devotion >= -20) {
			r.push(`shocked to find that ${he} has a ballsack. ${He} flexes ${his} Kegel muscles, mostly for lack of anything better to try,`);
			if (canSee(slave)) {
				r.push(`watching raptly as`);
			} else {
				r.push(`thinking deeply as`);
			}
			r.push(`${his} testicles move within the new pouch of soft skin beneath ${his} cock.`);
			if (canSee(slave)) {
				r.push(`Staring at the sight`);
			} else {
				r.push(`Transfixed on the motion`);
			}
			r.push(`with obviously mixed feelings, ${he} struggles for a while before bursting into girlish tears from the sheer ambivalence. ${He}'s frightened of your ability to modify ${him}, and is more willing than ever to submit to your plans for ${his} future.`);
			reaction.trust -= 5;
			reaction.devotion += 5;
		} else {
			r.push(`shocked and appalled to find that ${he} has a ballsack. ${He}`);
			if (canSee(slave)) {
				r.push(`stares at ${his} newly visible`);
			} else {
				r.push(`feels ${his} new`);
			}
			r.push(`balls in horror; it's obvious ${he} thought they were out of sight, and out of mind. ${He} checks ${himself} over carefully, and then bursts into tears, seemingly feeling that even if ${he} has a ballsack, ${his} cock is still a slave's bitchclit, and ${his} asshole is still a slut's asspussy. ${He}'s frightened of your ability to modify ${him}, and resentful of how you use it.`);
			reaction.trust -= 5;
			reaction.devotion -= 5;
		}
		r.push(`As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAddScrotum = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Graft on";
	}

	get healthCost() {
		return 10;
	}

	apply(cheat) {
		this._slave.scrotum = this._slave.balls;
		return this._assemble(new App.Medicine.Surgery.Reactions.AddScrotum());
	}
};


code:
App.Medicine.Surgery.Reactions.AddTesticles = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him, himself} = getPronouns(slave);
		const r = [];

		if (slave.fetish === "mindbroken") {
			r.push(`It's not immediately apparent to ${him} what kind of surgery ${he} received, since all ${he}'s left with is a nonspecific ache in ${his} abdomen. As with all surgery ${his} health has been slightly affected.`);
		} else if (slave.devotion > 50) {
			r.push(`${He} lies back in the surgical chair${(canSee(slave)) ? `, gazing at ${himself} in the ceiling mirror` : ``} as the fog of anesthetics lifts and feeling returns to ${his} lower half. As a devoted slave, ${he} knew the essentials of the surgery before it was performed, so ${he}'s excited to`);
			if (canSee(slave)) {
				r.push(`see`);
			} else {
				r.push(`feel`);
			}
			r.push(`the results. ${He} is tremendously impressed that you would devote such immense resources to altering ${his} body, and is more willing than ever to submit to your plans for ${his} future.`);
			reaction.trust += 5;
			reaction.devotion += 5;
		} else if (slave.devotion >= -20) {
			r.push(`It's not immediately apparent to ${him} what kind of surgery ${he} received, since all ${he}'s left with is a nonspecific ache in ${his} abdomen. As with all surgery ${his} health has been slightly affected.`);
		} else {
			r.push(`It's not immediately apparent to ${him} what kind of surgery ${he} received, since all ${he}'s left with is a nonspecific ache in ${his} abdomen. As with all surgery ${his} health has been slightly affected. ${He} doesn't have to know exactly what you did, though, to be mortified and infuriated by your messing around inside ${his} body. ${He} hasn't yet learned to accept that you control ${him} completely, down to the arrangement and even presence of ${his} internal organs.`);
			reaction.trust -= 10;
			reaction.devotion -= 10;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAddTesticles = class extends App.Medicine.Surgery.Procedure {
	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.AnimalType} ballType
	 */
	constructor(slave, ballType) {
		super(slave);
		this.ballType = ballType;
	}

	get name() {
		return "Implant";
	}

	get description() {
		return "you can forgo standard procedure and implant testicles directly into their abdomen";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		if (this._slave.prostate === 0) {
			this._slave.prostate = 1;
		}
		this._slave.balls = 2;
		this._slave.ballType = this.ballType;
		if (this._slave.pubertyAgeXY === 0) {
			if (V.precociousPuberty === 1) {
				if (this._slave.physicalAge >= V.potencyAge) {
					this._slave.pubertyAgeXY = this._slave.physicalAge + 1;
				}
			} else {
				if (this._slave.physicalAge >= V.potencyAge) {
					this._slave.pubertyXY = 1;
				}
			}
		}
		return this._assemble(new App.Medicine.Surgery.Reactions.AddTesticles());
	}
};

App.Medicine.Surgery.Procedures.OFReplaceTesticles = class extends App.Medicine.Surgery.Procedure {
	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.AnimalType} ballType
	 */
	constructor(slave, ballType) {
		super(slave);
		this.ballType = ballType;
	}

	get name() {
		return "Replace";
	}

	get description() {
		return "you can replace the existing testicles with a new pair";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		this._slave.balls = 2;
		this._slave.ballType = this.ballType;
		if (this._slave.pubertyAgeXY === 0) {
			if (V.precociousPuberty === 1) {
				if (this._slave.physicalAge >= V.potencyAge) {
					this._slave.pubertyAgeXY = this._slave.physicalAge + 1;
				}
			} else {
				if (this._slave.physicalAge >= V.potencyAge) {
					this._slave.pubertyXY = 1;
				}
			}
		}
		return this._assemble(new App.Medicine.Surgery.Reactions.AddTesticles());
	}
};


code:
App.Medicine.Surgery.Reactions.AsexualReproOvaries = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him, girl} = getPronouns(slave);
		const r = [];

		if (slave.fetish === "mindbroken") {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but lacks the mental faculties to realize that ${he} is now self-impregnating. As with all surgery ${his} health has been slightly affected.`);
		} else if (slave.devotion > 50) {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to suspect that something has critically changed about ${his} reproductive system. ${He} is pleased that you would go out of your way to alter ${his} body, and is more willing than ever to submit to your plans for ${his} future. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust += 5;
			reaction.devotion += 5;
		} else if (slave.devotion >= -20) {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to shudder at the thought of just what could have possibly been put into ${him}. ${He} is terrified of your apparently untrammeled power over ${his} body, so much so that ${he} is now more willing to submit to your plans for ${his} future. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust -= 5;
			reaction.devotion += 5;
		} else {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen, but ${he} has managed to catch enough slave rumors about what the organ farm can do to dread the discovery of what now resides inside ${him}. ${He} is terrified of your apparently untrammeled power over ${his} body, and furious at ${his} lack of control over ${his} own person. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust -= 5;
			reaction.devotion -= 5;
		}
		if (isFertile(slave) && slave.preg === 0) {
			knockMeUp(slave, 100, 2, slave.ID);
			r.push(`${He} doesn't even have the chance to reach the slave quarters before ${his} new reproductive system detects ${his} unprotected fertility and begins to internally cum.`);
			if (canWalk(slave)) {
				r.push(`${He} struggles to stand as wave after wave of pleasure radiates from inside ${his} self-fertilizing womb.`);
			} else {
				r.push(`The slave carrying ${him} struggles to keep their grip on the orgasm wracked ${girl}.`);
			}
			r.push(`By the time ${he} is finished, ${he} is a sweat-soaked, panting mess with a womb <span class="pregnant">`);
			if (slave.pregType > 50) {
				r.push(`stuffed full of fertilized eggs.`);
			} else if (slave.pregType > 20) {
				r.push(`filled with new life.`);
			} else if (slave.pregType > 1) {
				r.push(`housing several new lives.`);
			} else {
				r.push(`filled with seed and a new life.`);
			}
			r.push(`</span>`);
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAsexualReproOvaries = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Implant";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		if (this._slave.preg < 0) {
			this._slave.preg = 0;
		}
		this._slave.eggType = "human";
		this._slave.pregData = _.cloneDeep(App.Data.misc.pregData.human);
		if (this._slave.pubertyXX === 0 && this._slave.physicalAge >= V.fertilityAge) {
			if (V.precociousPuberty === 1) {
				this._slave.pubertyAgeXX = this._slave.physicalAge + 1;
			} else {
				this._slave.pubertyXX = 1;
			}
		}
		this._slave.ovaImplant = "asexual";
		return this._assemble(new App.Medicine.Surgery.Reactions.AsexualReproOvaries());
	}
};


code:
App.Medicine.Surgery.Reactions.FreshOvaries = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him} = getPronouns(slave);
		const r = [];

		if (slave.ovaryAge >= 45 && this._hasEmotion(slave)) {
			if (this._strongKnownFetish(slave, "pregnancy") || slave.origin === "$He sold $himself to you in the hope of someday bearing children.") {
				r.push(`${He} leaves the surgery with nothing but a nonspecific ache, yet a familiar warmth, in ${his} lower abdomen, but ${he} knows enough about surgery and sex slaves to know that it means ${he} now has a chance to get pregnant once more. ${He} is filled with joy whenever ${he} thinks about the fact that ${he}'ll have the chance to feel a life growing within ${him} again. ${He}'s so pleased that ${he} now trusts your plans for ${his} body. As with all surgery ${his} health has been slightly affected.`);
				reaction.trust += 4;
				reaction.devotion += 5;
			} else if (slave.devotion > 50) {
				r.push(`${He} leaves the surgery with nothing but a nonspecific ache, yet a familiar warmth, in ${his} lower abdomen, but ${he} knows enough about surgery and sex slaves to know that it means ${he} now has the chance to get pregnant again. ${He}'s grateful that you think ${him} worthy of extending ${his} fertility, and even a little nervous about how ${he}'ll perform as a mother that has tasted menopause. As with all surgery ${his} health has been slightly affected.`);
				reaction.devotion += 4;
			} else if (slave.devotion >= -20) {
				r.push(`${He} leaves the surgery with nothing but a nonspecific ache, yet a familiar warmth, in ${his} lower abdomen, but ${he} knows enough about surgery and sex slaves to know that it means ${he} may now be impregnated once more. ${He} understands the realities of ${his} life as a slave, but didn't expect to have ${his} waning fertility renewed. As with all surgery ${his} health has been slightly affected. ${He} is sensibly fearful of your total power over ${his} body.`);
				reaction.trust -= 5;
			} else {
				r.push(`${He} leaves the surgery with nothing but a nonspecific ache, yet a familiar warmth, in ${his} lower abdomen, but ${he} knows enough about surgery and sex slaves to believe you have forced fertility upon ${him} again. ${He} does not understand the realities of ${his} life as a slave at a core level, so ${he}'s terrified and angry at the potential that ${he}'ll be forced to carry children. As with all surgery ${his} health has been slightly affected. ${He} is sensibly fearful of your total power over ${his} body.`);
				reaction.trust -= 5;
				reaction.devotion -= 5;
			}
		} else {
			r.push(`${He} leaves the surgery with nothing but a nonspecific ache in ${his} lower abdomen; it won't be clear to ${him} that menopause is not a concern for now. As with all surgery ${his} health has been slightly affected.`);
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFFreshOvaries = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Implant";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		if (this._slave.ovaryAge >= 47) {
			this._slave.ovaryAge = 45;
		} else {
			this._slave.ovaryAge = -2; // It shouldn't matter if this goes negative as it is just a signal for menopause to occur.
		}
		if (this._slave.preg < 0) {
			this._slave.preg = 0;
		}
		if (this._slave.pubertyXX === 0 && this._slave.physicalAge >= V.fertilityAge) {
			if (V.precociousPuberty === 1) {
				this._slave.pubertyAgeXX = this._slave.physicalAge + 1;
			} else {
				this._slave.pubertyXX = 1;
			}
		}
		return this._assemble(new App.Medicine.Surgery.Reactions.FreshOvaries());
	}
};


code:
App.Medicine.Surgery.Reactions.AddMPreg = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him} = getPronouns(slave);
		const r = [];

		r.push(`${He} leaves the surgery with a certain fullness in ${his} lower abdomen, ${he} knows that, despite lacking female reproductive organs, ${he} can now become pregnant.`);
		if (slave.fetish === "mindbroken") {
			r.push(`As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);
		} else if (this._strongKnownFetish(slave, "pregnancy")) {
			r.push(`${He} is  filled with joy about the possibility of becoming pregnant and gleefully rubs ${his} softer belly. ${He}'s so pleased that ${he} now trusts your plans for ${his} body. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust += 4;
			reaction.devotion += 10;
		} else if (slave.devotion > 50) {
			r.push(`${He}'s grateful that you think ${his} offspring are valuable enough to give ${him} this gift, and a little nervous about how ${he}'ll perform as a mother. As with all surgery ${his} health has been slightly affected.`);
			reaction.devotion += 4;
		} else if (slave.devotion >= -20) {
			r.push(`${He} understands the realities of ${his} life as a slave, so it isn't much of a shock. As with all surgery ${his} health has been slightly affected. ${He} is sensibly fearful of your total power over ${his} body and ${his} inevitable pregnancy.`);
			reaction.trust -= 10;
		} else {
			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 become fertile in such an unnatural way. As with all surgery ${his} health has been slightly affected. ${He} is sensibly fearful of your total power over ${his} body and ${his} inevitable pregnancy.`);
			reaction.trust -= 15;
			reaction.devotion -= 15;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.OFAddMPreg = class extends App.Medicine.Surgery.Procedure {
	/**
	 * @param {App.Entity.SlaveState} slave
	 * @param {FC.AnimalType} eggType
	 * @param {string} pregDataKey
	 */
	constructor(slave, eggType, pregDataKey) {
		super(slave);
		this.eggType = eggType;
		this.pregDataKey = pregDataKey;
	}

	get name() {
		return "Implant";
	}

	get healthCost() {
		return 40;
	}

	apply(cheat) {
		this._slave.mpreg = 1;
		this._slave.eggType = this.eggType;
		this._slave.preg = 0;
		/**
		 * @type {FC.PregnancyData}
		 */
		// @ts-ignore
		const data = {};
		deepAssign(data, App.Data.misc.pregData[this.pregDataKey]);
		this._slave.pregData = data;
		if (this._slave.pubertyXX === 0 && this._slave.physicalAge >= V.fertilityAge) {
			if (V.precociousPuberty === 1) {
				this._slave.pubertyAgeXX = this._slave.physicalAge + 1;
			} else {
				this._slave.pubertyXX = 1;
			}
		}
		return this._assemble(new App.Medicine.Surgery.Reactions.AddMPreg());
	}
};


code:
App.Medicine.Surgery.Procedures.SizingImplantProcedure = class extends App.Medicine.Surgery.Procedure {
	/**
	 * @param {FC.SlaveState} slave
	 * @param {FC.SizingImplantTarget} target
	 * @param {FC.SizingImplantType} implantType
	 * @param {number} size
	 */
	constructor(slave, target, implantType, size) {
		super(slave);
		this._target = target;
		this._implantType = implantType;
		this._size = size;
	}

	get name() {
		return `${capFirstChar(this._resolveDynamic(this._implantData.name, this._size))} implants`;
	}

	get changeValue() {
		return this._size;
	}

	apply(cheat) {
		this._slave[this._target] += this._size;
		this._slave[`${this._target}Implant`] += this._size;
		this._setSlaveImplantType(this._implantType);

		if (this.changeValue >= 0) {
			return this._assemble(this._doTargetGain());
		} else {
			if (this._slave[`${this._target}Implant`] < 1) {
				this._slave[`${this._target}Implant`] = 0;
				this._setSlaveImplantType("none");
			}
			return this._assemble(this._doTargetLoss());
		}
	}

	_surgeryWorkPriceFactor() {
		return V.surgeryCost * V.localEcon / 3e4;
	}

	/**
	 * @param {FC.SizingImplantType} type
	 */
	_implantDataForType(type) {
		return App.Data.Medicine.sizingImplants[this._target][type];
	}

	/**
	 * @param {FC.SizingImplantType} type
	 * @param {number} size
	 */
	_installationHealthCost(type, size) {
		return this._implantDataForType(type).healthCostFactor.installation * size;
	}

	/**
	 * @param {FC.InstalledSizingImplantType} type
	 * @param {number} size
	 */
	_removalHealthCost(type, size) {
		return type === "none" ? 0
			: this._implantDataForType(type).healthCostFactor.removal * size;
	}

	/**
	 * @param {FC.InstalledSizingImplantType} type
	 * @param {number} size
	 */
	_installationWorkCost(type, size) {
		return type === "none" ? 0
			: this._implantDataForType(type).workCostFactor.installation * size * this._surgeryWorkPriceFactor();
	}

	/**
	 * @param {FC.InstalledSizingImplantType} type
	 * @param {number} size
	 */
	_removalWorkCost(type, size) {
		return type === "none" ? 0
			: this._implantDataForType(type).workCostFactor.removal * size * this._surgeryWorkPriceFactor();
	}

	get _implantData() {
		return this._implantDataForType(this._implantType);
	}

	get _implantName() {
		return this._resolveDynamic(this._implantData.name, this._size);
	}

	get _materialCost() {
		return this._resolveDynamic(this._implantData.specificMaterialCost, this._size) * this._size;
	}

	/**
	 * @param {*} amount Positive for fill, negative for drain
	 */
	_fillDrainMaterialCost(amount) {
		const id = this._implantData;
		return amount >= 0
			? (id.fill ? this._implantData.fill.materialCostFactor * amount : 0)
			: 0; // draining does not impose material cost as of now
	}

	/**
	 * @template {string | number} T
	 * @param {T | ((size: number) => T)} value
	 * @param {number} size
	 * @returns {T}
	 */
	_resolveDynamic(value, size) {
		return typeof value !== "function" ? value : value(size);
	}

	get _targetName() {
		switch (this._target) {
			case "boobs":
				return ["mammaries", "chest", "breasts"].random();
			case "butt":
				return ["buttocks", "posterior"].random();
			case "lips":
				return "lip";
			default:
				return this._target;
		}
	}

	/**
	 * Returns a string containign the name (depends on size) and possible volume of the implant
	 * @param {FC.SizingImplantType} type
	 * @param {number} volume
	 */
	_describeImplant(type, volume) {
		let r = this._resolveDynamic(this._implantDataForType(type).name, volume);
		const volumeDesc = this._volumeStr(volume);
		if (volumeDesc) {
			r += ` (${volumeDesc})`;
		}
		return r;
	}

	_setSlaveImplantType(implantType) {
		if (this.originalSlave[`${this._target}ImplantType`] !== implantType) {
			this._slave[`${this._target}ImplantType`] = implantType;
		}
	}

	/**
	 * @abstract
	 * @returns {App.Medicine.Surgery.SimpleReaction}
	 */
	_doTargetGain() {
		throw Error("Abstract method called");
	}
	/**
	 * @abstract
	 * @returns {App.Medicine.Surgery.SimpleReaction}
	 */
	_doTargetLoss() {
		throw Error("Abstract method called");
	}

	/**
	 * @abstract
	 * @param {number} volume
	 * @returns {string}
	 */
	_volumeStr(volume) {
		throw Error("Abstract method called");
	}

	/**
	 * @abstract
	 * @param {number} volume
	 * @returns {string}
	 */
	 _fleshExcessStr(volume) {
		throw Error("Abstract method called");
	}
};

App.Medicine.Surgery.Procedures.InstallSizingImplantProcedure = class extends App.Medicine.Surgery.Procedures.SizingImplantProcedure {
	/**
	 * @param {FC.SlaveState} slave
	 * @param {FC.SizingImplantTarget} target
	 * @param {FC.SizingImplantType} implantType
	 * @param {number} size how big is the implant
	 * @param {number} fleshExcess Excess flesh volume to remove for installing this implant
	 */
	constructor(slave, target, implantType, size, fleshExcess) {
		super(slave, target, implantType, size);
		this._fleshExcess = fleshExcess;
	}

	get description() {
		const {his} = getPronouns(this._slave);
		return `place ${this._describeImplant(this._implantType, this._size)} implants into ${his} ${this._targetName}`;
	}

	get note() {
		if (this._fleshExcess > 0) {
			const {his} = getPronouns(this._slave);
			return App.UI.DOM.makeElement('span',
				`Installation requires removing ${this._fleshExcessStr(this._fleshExcess)} of ${his} ${this._targetName} flesh.`, ["warning"]);
		}
		return null;
	}

	get healthCost() {
		return this._installationHealthCost(this._implantType, this._size);
	}

	get _workCost() {
		return super._workCost + super._installationWorkCost(this._implantType, this._size);
	}
};

App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase = class extends App.Medicine.Surgery.Procedures.SizingImplantProcedure {
	/**
	 * @param {FC.SlaveState} slave
	 * @param {FC.SizingImplantTarget} target
	 * @param {number} size
	 * @param {FC.SizingImplantType} [implantType]
	 */
	constructor(slave, target, size, implantType = null) {
		const oldImplantInfo = App.Medicine.implantInfo(slave, target);
		if (oldImplantInfo.type === "none") {
			throw Error(`Slave has no ${target} implant to work on`);
		}
		super(slave, target, implantType || oldImplantInfo.type, size);
		this._oldImplantInfo = oldImplantInfo;
	}

	/**
	 * @protected
	 */
	get _oldImplantsDescription() {
		return this._describeImplant(/** @type {FC.SizingImplantType}*/(this._oldImplantInfo.type), this._oldImplantInfo.volume);
	}
};

App.Medicine.Surgery.Procedures.RemoveSizingImplantProcedure = class extends App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase {
	/**
	 * @param {FC.SlaveState} slave
	 * @param {FC.SizingImplantTarget} target
	 */
	constructor(slave, target) {
		super(slave, target, -slave[`${target}Implant`]);
	}

	get name() {
		return `Remove ${this._targetName} implants`;
	}

	get description() {
		const {his} = getPronouns(this._slave);
		return `remove ${his} ${this._oldImplantsDescription} ${this._targetName} implants`;
	}

	get _materialCost() {
		return 0;
	}

	get _workCost() {
		return super._workCost + super._removalWorkCost(this._implantType, -this._size);
	}

	get healthCost() {
		return this._removalHealthCost(this._implantType, -this._size);
	}
};

App.Medicine.Surgery.Procedures.ReplaceSizingImplantProcedure = class extends App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase {
	/**
	 * @param {FC.SlaveState} slave
	 * @param {FC.SizingImplantTarget} target
	 * @param {FC.SizingImplantType} implantType
	 * @param {number} size how big is the implant
	 * @param {number} fleshExcess Excess flesh volume to remove for installing this implant
	 */
	constructor(slave, target, implantType, size, fleshExcess) {
		super(slave, target, size, implantType);
		this._fleshExcess = fleshExcess;
	}

	get description() {
		const {his} = getPronouns(this._slave);
		return `replace ${his} ${this._oldImplantsDescription} ${this._targetName} implants with ${this._describeImplant(this._implantType, this._size)} ones`;
	}

	get note() {
		if (this._fleshExcess > 0) {
			const {his} = getPronouns(this._slave);
			return App.UI.DOM.makeElement('span',
				`Installation of the new implant requires removing ${this._fleshExcessStr(this._fleshExcess)} of ${his} ${this._targetName} flesh.`, ["warning"]);
		}
		return null;
	}

	get healthCost() {
		return this._removalHealthCost(this._oldImplantInfo.type, this._oldImplantInfo.volume) +
			this._installationHealthCost(this._implantType, this._size);
	}

	get _workCost() {
		const previousImplant = App.Medicine.implantInfo(this.originalSlave, this._target);
		return super._workCost + super._removalWorkCost(previousImplant.type, previousImplant.volume) +
			this._installationWorkCost(this._implantType, this._size);
	}

	get changeValue() {
		return this._size - this._oldImplantInfo.volume;
	}

	apply(cheat) {
		this._slave[this._target] -= this._oldImplantInfo.volume;
		this._slave[`${this._target}Implant`] = 0;

		return super.apply(cheat);
	}
};

App.Medicine.Surgery.Procedures.FillSizingImplantProcedure = class extends  App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase {
	/**
	 * @param {FC.SlaveState} slave
	 * @param {FC.SizingImplantTarget} target
	 * @param {number} amount
	 */
	constructor(slave, target, amount) {
		super(slave, target, amount);
	}

	get name() {
		return `Add inert filler`;
	}

	get description() {
		const {his} = getPronouns(this._slave);
		const volumeStr = this._volumeStr(this._size);
		return `add ${volumeStr ? `${volumeStr} of` : 'some'} inert filler to each of ${his} ${this._oldImplantsDescription} ${this._targetName} implants`;
	}

	get _materialCost() {
		return this._fillDrainMaterialCost(this._size);
	}

	get healthCost() {
		return this._implantData.fill.healthCost;
	}
};

App.Medicine.Surgery.Procedures.DrainSizingImplantProcedure = class extends  App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase {
	/**
	 * @param {FC.SlaveState} slave
	 * @param {FC.SizingImplantTarget} target
	 * @param {number} amount
	 */
	constructor(slave, target, amount) {
		super(slave, target, -amount);
	}

	get name() {
		return `Drain inert filler`;
	}

	get description() {
		const {his} = getPronouns(this._slave);
		const volumeStr = this._volumeStr(this._size);
		return `drain ${volumeStr ? `${volumeStr} of` : 'some'} inert filler from ${his} ${this._oldImplantsDescription} ${this._targetName} implants`;
	}

	get _materialCost() {
		return 0;
	}

	get healthCost() {
		return this._implantData.drain.healthCost;
	}
};


code:
App.Medicine.Surgery.Reactions.BoobsGain = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him, himself} = getPronouns(slave);
		const heGlaresDaggers = canSee(slave) ? `${he} glares daggers` : `${his} face contorts with distaste`;
		const r = [];

		if (diff.areolae > slave.areolae) {
			r.push(`The increase in breast size <span class="lime">stretches and broadens ${his} areolae.</span>`);
		}
		if (diff.nipples === "cute" && slave.nipples === "puffy") {
			r.push(`The breast surgery is invasive, and when ${his} nipples heal, <span class="orange">they're a bit more normal.</span>`);
		} else if (diff.nipples === "puffy" && slave.nipples === "huge") {
			r.push(`The breast surgery is invasive, and when ${his} nipples heal, <span class="orange">they're a bit smaller.</span>`);
		} else if (diff.nipples === "flat" && (slave.nipples === "cute" || slave.nipples === "tiny")) {
			r.push(`The sudden increase in breast size has <span class="orange">stretched ${his} already small nipples flat.</span>`);
		}
		if (slave.boobShape !== "spherical") {
			if (diff.boobShape === "spherical") {
				r.push(`With so little actual flesh left, the shape of ${his} breasts are now entirely dictated by the implants within, <span class="lime">rendering them comically spherical.</span>`);
			} else if (diff.boobShape === "normal" && slave.boobShape !== "normal") {
				r.push(`The natural shape of ${his} breasts has been eliminated by the cosmetic surgery, <span class="lime">rendering ${his} boobs pretty and rounded.</span>`);
			}
		}

		if (slave.fetish === "mindbroken") {
			r.push(`${He} shows little reaction to the new weight on ${his} chest. As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);
		} else if (slave.devotion > 20 && this._strongKnownFetish(slave, "boobs")) {
			if (hasAnyArms(slave)) {
				r.push(`${He}'s barely out of the surgery before ${he}'s playing with ${his} new assets.`);
			} else {
				r.push(`${He}'s barely out of the surgery before ${he}'s rubbing ${his} new assets against anything ${he} can reach.`);
			}
			r.push(`${He}'s deliriously happy with your changes to what ${he} thinks of as ${his} primary sexual organs, so much so that ${he} now trusts your plans for ${his} body. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust += 4;
			reaction.devotion += 4;
		} else if (slave.devotion > 50) {
			if (hasAnyArms(slave)) {
				r.push(`${He} hefts ${his} new breasts experimentally and turns to you with a smile to show them off. ${He}'s still sore, so ${he} doesn't bounce or squeeze, but ${he} turns from side to side to let you see them from all angles.`);
			} else {
				r.push(`${He} bounces a little to feel ${his} new breasts move and turns ${his} torso to you with a smile to show them off. ${He}'s still sore, so ${he} doesn't move too much, but ${he} wiggles ${himself} a little to make them bounce for you.`);
			}
			r.push(`<span class="devotion inc">${He}'s happy with your changes to ${his} boobs.</span> As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);
			reaction.devotion += 4;
		} else if (slave.devotion >= -20) {
			if (canSee(slave)) {
				r.push(`${He} eyes ${his} new breasts`);
			} else {
				r.push(`${He} shifts them`);
			}
			r.push(`skeptically.`);
			if (hasAnyArms(slave)) {
				r.push(`${He}'s still sore, so ${he} doesn't touch them.`);
			} else {
				r.push(`${He}'s still sore, so ${he} keeps ${his} torso still.`);
			}
			r.push(`${He}'s come to terms with the fact that ${he}'s a slave, so ${he} expected something like this when ${he} was sent to the surgery. ${He} isn't much affected mentally. As with all surgery ${his} health has been slightly affected. ${He} is sensibly fearful of your total power over ${his} body.`);
			reaction.trust -= 5;
		} else {
			if (canSee(slave)) {
				r.push(`${He} eyes ${his} new breasts`);
			} else {
				r.push(`The new weight on ${his} chest fills ${him}`);
			}
			r.push(`with resentment.`);
			if (hasAnyArms(slave)) {
				r.push(`${He}'s still sore, so ${he} doesn't touch them, but ${heGlaresDaggers}.`);
			} else {
				r.push(`${He}'s still sore, so ${he} keeps ${his} torso still, but ${heGlaresDaggers}.`);
			}
			r.push(`${He} still thinks of ${himself} as a person, so ${he} isn't used to the idea of being surgically altered to suit your every whim. For now, ${he} seems to view these fake breasts as a cruel imposition. As with all surgery ${his} health has been slightly affected. ${He} is now terribly afraid of your total power over ${his} body.`);
			reaction.trust -= 10;
			reaction.devotion -= 5;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Reactions.BoobsLoss = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him, himself} = getPronouns(slave);
		const r = [];

		if (diff.areolae < slave.areolae) {
			r.push(`The breast reduction surgery also <span class="orange">slightly reduces ${his} massive areolae.</span>`);
		}
		if (slave.nipples === "huge") {
			r.push(`The breast reduction surgery also <span class="orange">slightly reduces ${his} massive nipples.</span>`);
		} else if (slave.nipples === "fuckable" && diff.boobs < 500) {
			r.push(`Without the tissue needed to support their unusual shape, ${his} fuckable nipples have reverted <span class="orange">to being huge and protruding.</span>`);
		} else if (slave.nipples === "flat") {
			r.push(`Without the ${his} massive implants forcing them flat, ${his} nipples have reverted <span class="lime">to being huge and protruding.</span>`);
		}
		if (slave.boobShape === "spherical") {
			r.push(`With the removal of ${his} load bearing implants, <span class="orange">${his} breasts are left deflated and sagging.</span>`);
		}

		if (slave.fetish === "mindbroken") {
			r.push(`${He} shows little awareness that ${his} breasts are smaller. As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);
		} else if (slave.devotion > 50) {
			if (hasAnyArms(slave)) {
				r.push(`${He} hefts ${his} new, sleeker breasts experimentally and turns to you with a smile to show off ${his} new, slimmer form. ${He}'s still sore, so ${he} doesn't bounce or squeeze, but ${he} turns from side to side to let you see them from all angles.`);
			} else {
				r.push(`${He} bounces a little to feel ${his} smaller breasts move and turns ${his} torso to you with a smile to show them off. ${He}'s still sore, so ${he} doesn't bounce too much.`);
			}
			r.push(`<span class="devotion inc">${He}'s happy with your changes to ${his} boobs.</span> As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);
			reaction.devotion += 4;
		} else if (slave.devotion >= -20) {
			if (canSee(slave)) {
				r.push(`${He} eyes ${his} new, smaller breasts skeptically.`);
			} else {
				r.push(`${He} attempts to sway ${his} big tits experimentally, only to find them substantially less bouncy.`);
			}
			if (hasAnyArms(slave)) {
				r.push(`${He}'s still sore, so ${he} doesn't touch them.`);
			} else {
				r.push(`${He}'s still sore, so ${he} keeps ${his} torso still.`);
			}
			r.push(`${He}'s come to terms with the fact that ${he}'s a slave, but ${he} expected something other than this when ${he} was sent to the surgery. ${He} isn't much affected mentally. As with all surgery ${his} health has been slightly affected. ${He} is sensibly fearful of your total power over ${his} body.`);
			reaction.trust -= 5;
		} else {
			if (canSee(slave)) {
				r.push(`${He} eyes ${his} sudden lack of ${his} former breasts with resentment.`);
			} else {
				r.push(`The sudden lack of weight on ${his} chest fills ${him} with resentment.`);
			}
			if (hasAnyArms(slave)) {
				r.push(`${He}'s still sore, so ${he} doesn't touch them,`);
			} else {
				r.push(`${He}'s still sore, so ${he} keeps ${his} torso still,`);
			}
			r.push(`but ${canSee(slave) ? `${he} glares daggers` : `${his} face contorts with distaste`}.`);
			r.push(`${He} still thinks of ${himself} as a person, so ${he} isn't used to the idea of being surgically altered to suit your every whim. For now, ${he} seems to view this surgical theft as a cruel imposition. As with all surgery ${his} health has been slightly affected. ${He} is now terribly afraid of your total power over ${his} body.`);
			reaction.trust -= 10;
			reaction.devotion -= 5;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

/**
 * @type {FC.Medicine.AssetSizingProcedureSet}
 */
App.Medicine.Surgery.Procedures.boobImplantsProcedure = function() {
	/**
	 * @param {FC.SlaveState} slave
	 */
	const applyBoobsGain = (slave) => {
		if (slave.areolae < 2 && Math.random() > 0.7) {
			slave.areolae += 1;
		}
		if (slave.nipples === "puffy") {
			if (Math.random() > 0.7) {
				slave.nipples = "cute";
			}
		} else if (slave.nipples === "huge") {
			if (Math.random() > 0.9) {
				slave.nipples = "puffy";
			}
		} else if ((slave.nipples === "cute" || slave.nipples === "tiny") && (slave.boobsImplant / slave.boobs >= 0.75)) {
			slave.nipples = "flat";
		}
		if (slave.boobShape !== "spherical") {
			if (slave.boobsImplant / slave.boobs >= 0.90) {
				slave.boobShape = "spherical";
			} else if (slave.boobShape !== "normal" && Math.random() > 0.5) {
				slave.boobShape = "normal";
			}
		}
	};

	/**
	 * @param {FC.SlaveState} slave
	 */
	const applyBoobsLoss = (slave) => {
		if (slave.areolae > 2) {
			slave.areolae -= 1;
		}
		if (slave.nipples === "huge") {
			slave.nipples = "puffy";
		} else if (slave.nipples === "fuckable" && slave.boobs < 500) {
			slave.nipples = "huge";
		} else if (slave.nipples === "flat") {
			slave.nipples = "huge";
		}
		if (slave.boobShape === "spherical") {
			slave.boobShape = "saggy";
		}
	};

	/**
	 * @template {FC.Medicine.SizingImplantProcedureConstructor} T
	 * @param {T} Procedure
	 * @param {ConstructorParameters<T>} args
	 */
	function makeBoobProcedure(Procedure, args) {
		return new (class extends Procedure {
			_doTargetGain() {
				applyBoobsGain(this._slave);
				return new App.Medicine.Surgery.Reactions.BoobsGain();
			}

			_doTargetLoss() {
				applyBoobsLoss(this._slave);
				return new App.Medicine.Surgery.Reactions.BoobsLoss();
			}

			/**
			 * @param {number} volume
			 * @returns {string}
			 */
			_volumeStr(volume) {
				return V.showBoobCCs ? App.Utils.expandHTML(`${volume} cm³`) : '';
			}

			/**
			 * @param {number} volume
			 * @returns {string}
			 */
			_fleshExcessStr(volume) {
				const percentsStr = `${Math.round(100. * volume / App.Medicine.fleshSize(this.originalSlave, "boobs"))} %`;

				return V.showBoobCCs
					? App.Utils.expandHTML(`${volume} cm³ (${percentsStr})`)
					: App.Utils.expandHTML(percentsStr);
			}
		})(...args);
	}

	class Reduce extends App.Medicine.Surgery.Procedure {
		/**
		 * @param {FC.SlaveState} slave
		 * @param {string} procedureName
		 * @param {number} amount
		 */
		constructor(slave, procedureName, amount) {
			super(slave);
			this._procedureName = capFirstChar(procedureName);
			this._amount = amount;
		}

		get name() {
			return `${this._procedureName} boobs`;
		}

		get description() {
			const {his} = getPronouns(this._slave);
			return `${this._procedureName} ${his} boobs`;
		}

		get healthCost() { return 5; }

		get changeValue() { return -this._amount; }

		apply(cheat) {
			this._slave.boobs -= this._amount;

			applyBoobsLoss(this._slave);

			return this._assemble(new App.Medicine.Surgery.Reactions.BoobsLoss());
		}
	}

	return {
		install: (slave, type, size, fleshExcess) =>
			makeBoobProcedure(App.Medicine.Surgery.Procedures.InstallSizingImplantProcedure, [slave, "boobs", type, size, fleshExcess]),
		replace: (slave, type, size, fleshExcess) =>
			makeBoobProcedure(App.Medicine.Surgery.Procedures.ReplaceSizingImplantProcedure, [slave, "boobs", type, size, fleshExcess]),
		remove: (slave) =>
			makeBoobProcedure(App.Medicine.Surgery.Procedures.RemoveSizingImplantProcedure, [slave, "boobs"]),

		fill: (slave, amount) =>
			makeBoobProcedure(App.Medicine.Surgery.Procedures.FillSizingImplantProcedure, [slave, "boobs", amount]),
		drain: (slave, amount) =>
			makeBoobProcedure(App.Medicine.Surgery.Procedures.DrainSizingImplantProcedure, [slave, "boobs", amount]),

		reduce: (slave, name, amount) => new Reduce(slave, name, amount)
	};
}();


code:
App.Medicine.Surgery.Reactions.BreastLift = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him, himself} = getPronouns(slave);
		const r = [];

		if (slave.fetish === "mindbroken") {
			r.push(`The changes to ${his} breasts are lost on ${him}. As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);
		} else if (slave.devotion > 20 && this._strongKnownFetish(slave, "boobs")) {
			if (hasAnyArms(slave)) {
				r.push(`${He}'s barely out of the surgery before ${he}'s playing with ${his} new, perkier breasts despite the pain.`);
			} else {
				r.push(`${He}'s barely out of the surgery before ${he}'s rubbing ${his} new, perkier breasts against anything ${he} can reach, despite the pain.`);
			}
			r.push(`${He}'s deliriously happy with your changes to what ${he} thinks of as ${his} primary sexual organs, so much so that ${he} now trusts your plans for ${his} body. As with all surgery ${his} health has been slightly affected.`);
			reaction.trust += 4;
			reaction.devotion += 4;
		} else if (slave.devotion > 50) {
			if (hasAnyArms(slave)) {
				r.push(`${He} runs ${his}`);
				if (hasBothArms(slave)) {
					r.push(`hands`);
				} else {
					r.push(`hand`);
				}
				r.push(`over ${his} perkier breasts experimentally and turns to you with a smile to show them off. ${He}'s still sore, so ${he} doesn't touch them much, but ${he} turns from side to side to let you see them from all angles.`);
			} else {
				r.push(`${He} bounces a little to feel ${his} perkier tits move before ${he} turns ${his} torso to you with a smile to show them off. ${He}'s still sore, so ${he} doesn't move too violently, but ${he} wiggles ${himself} a little to show off.`);
			}
			r.push(`<span class="devotion inc">${He}'s thrilled that you firmed up ${his} breasts.</span> As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);
			reaction.devotion += 4;
		} else if (slave.devotion >= -20) {
			if (canSee(slave)) {
				r.push(`${He} eyes ${his} new, perkier breasts skeptically.`);
			} else {
				r.push(`The cool air flowing over the new location of ${his} nipples draws a skeptical expression to ${his} face.`);
			}
			if (hasAnyArms(slave)) {
				r.push(`${He}'s still sore, so ${he} doesn't touch them.`);
			} else {
				r.push(`${He}'s still sore, so ${he} keeps ${his} body still.`);
			}
			r.push(`${He}'s come to terms with the fact that ${he}'s a slave, but ${he} is quite happy you'd take the time to make ${his} breasts prettier. As with all surgery ${his} health has been slightly affected. ${He} is sensibly fearful of your total power over ${his} body and that this may be only the beginning of extensive breast surgery.`);
			reaction.devotion += 3;
			reaction.trust -= 5;
		} else {
			if (canSee(slave)) {
				r.push(`${He} eyes ${his} new, perkier breasts`);
			} else {
				r.push(`The feel of the air running over the new location of ${his} nipples fills ${him}`);
			}
			r.push(`with resentment.`);
			if (hasAnyArms(slave)) {
				r.push(`${He}'s still sore, so ${he} doesn't touch them,`);
			} else {
				r.push(`${He}'s still sore, so ${he} keeps ${his} torso still,`);
			}
			r.push(`but ${canSee(slave) ? `${he} glares daggers` : `${his} face contorts with distaste`}.`);
			r.push(`${He} still thinks of ${himself} as a person, so ${he} isn't used to the idea of being surgically altered to suit your every whim. Oddly, ${he} seems to like that ${his} breasts are no longer saggy despite ${his} immediate reaction. As with all surgery ${his} health has been slightly affected. ${He} is now terribly afraid of your total power over ${his} body and fears that this is only the beginning of ${his} breast surgeries.`);
			reaction.trust -= 10;
			reaction.devotion += 2;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.BreastLift = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Breast Lift";
	}

	get healthCost() {
		return 20;
	}

	apply(cheat) {
		this._slave.boobShape = "normal";
		return this._assemble(new App.Medicine.Surgery.Reactions.BreastLift());
	}
};