code:
App.EventHandlers = function() {
	/**
	 * @param {TwineSugarCube.SaveObject} save
	 */
	function onLoad(save) {
		const v = /** @type {FC.GameVariables} */(save.state.history[0].variables);
		if (v.releaseID === 2000) {
			v.releaseID = 1100;
		}
		if (v.releaseID > App.Version.release) {
			console.error("Save game version problem. Loaded : " + v.releaseID + ", above expected:" + App.Version.release); // eslint-disable-line no-console
			throw new Error("The save you're attempting to load was created with the game version newer than one you are running. Please download the latest game version.");
		}
		// updating settings only for the same releaseID, otherwise user will run
		// backwards compatibility and we update settings from there
		if (v.releaseID === App.Version.release) {
			App.UI.SlaveSummary.settingsChanged(v);
		}
		App.Utils.PassageSwitchHandler.clear();
	}

	/**
	 * @param {TwineSugarCube.SaveObject} save
	 */
	function onSave(save) {
		if (App.Utils.isEndWeek() && V.endweekSaveWarning && Dialog.isOpen()) {
			$(document).one(':dialogclosed', () => {
				Dialog.setup("Saving during End Week");
				Dialog.append("
Saves created during End Week should ONLY be loaded in the SAME game version.

Please save on Main or Options if you plan on upgrading your game.
");
				Dialog.append("You can disable this message in the Game Options.");
				Dialog.open();
			});
		}
	}

	function storyReady() {
		App.Status.storyReady = true;
		App.UI.Theme.init();
		App.UI.Hotkeys.init();
		App.UI.GlobalTooltips.update();
		// Load player mods. Do this last
		App.Modding.internal.load();
	}

	function optionsChanged() {
		App.UI.SlaveSummary.settingsChanged();
	}

	return {
		onLoad: onLoad,
		onSave: onSave,
		storyReady: storyReady,
		optionsChanged: optionsChanged
	};
}();


code:
/* see documentation for details /devNotes/Extended Family Mode Explained.txt */

/** @typedef Relative
 * An object that represents a entity in a family tree. Represents a group of common properties shared by SlaveState, InfantState, and PlayerState, as well as genepool objects.
 * @type {object}
 * @property {number} ID
 * @property {number} mother
 * @property {number} father
 * @property {number} actualAge
 * @property {number} birthWeek
 * @property {string} slaveName
 * @property {FC.Zeroable<string>} slaveSurname
 * @property {string} genes
 */

/** Given an ID, gets a relative entry that represents that ID, from wherever it may be
 * @param {number} ID
 * @returns {Relative}
 */
globalThis.getRelative = function(ID) {
	// PC
	if (ID === -1) {
		return V.PC;
	}
	// active slave, child, or tank baby
	const slave = findFather(ID);
	if (slave) {
		return slave;
	}
	// ex-slave
	const genePool = V.genePool.find(s => s.ID === ID);
	if (genePool) {
		return genePool;
	}
	return null;
};

/** Returns true if mother is the mother of daughter
 * @param {Relative} daughter
 * @param {Relative} mother
 * @returns {boolean}
 */
globalThis.isMotherP = function(daughter, mother) {
	return daughter.mother === mother.ID;
};

/** Returns true if father is the father of daughter
 * @param {Relative} daughter
 * @param {Relative} father
 * @returns {boolean}
 */
globalThis.isFatherP = function(daughter, father) {
	return daughter.father === father.ID;
};

/** Returns true if parent is either the father or mother of daughter
 * @param {Relative} daughter
 * @param {Relative} parent
 * @returns {boolean}
 */
globalThis.isParentP = function(daughter, parent) {
	return isMotherP(daughter, parent) || isFatherP(daughter, parent);
};

/** Returns true if grandmother is the grandmother of granddaughter
 * @param {Relative} granddaughter
 * @param {Relative} grandmother
 * @returns {boolean}
 */
globalThis.isGrandmotherP = function(granddaughter, grandmother) {
	if (!V.showDistantRelatives) {
		return false;
	}

	const father = getRelative(granddaughter.father);
	const mother = getRelative(granddaughter.mother);
	return (mother && (mother.mother === grandmother.ID)) ||
		(father && (father.mother === grandmother.ID));
};

/** Returns true if grandfather is the grandfather of granddaughter
 * @param {Relative} granddaughter
 * @param {Relative} grandfather
 * @returns {boolean}
 */
globalThis.isGrandfatherP = function(granddaughter, grandfather) {
	if (!V.showDistantRelatives) {
		return false;
	}

	const father = getRelative(granddaughter.father);
	const mother = getRelative(granddaughter.mother);
	return (mother && (mother.father === grandfather.ID)) ||
		(father && (father.father === grandfather.ID));
};

/** Returns true if grandparent is the either the grandmother or grandfather of granddaughter
 * @param {Relative} granddaughter
 * @param {Relative} grandparent
 * @returns {boolean}
 */
globalThis.isGrandparentP = function(granddaughter, grandparent) {
	if (!V.showDistantRelatives) {
		return false;
	}

	return isGrandmotherP(granddaughter, grandparent) || isGrandfatherP(granddaughter, grandparent);
};

/** Returns true if slave1 and slave2 share the same father
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {boolean}
 */
globalThis.sameDad = function(slave1, slave2) {
	return (slave1.father === slave2.father) && (specificDad(slave1));
};

/** Returns true if slave1 and slave2 share the same mother
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {boolean}
 */
globalThis.sameMom = function(slave1, slave2) {
	return (slave1.mother === slave2.mother) && (specificMom(slave1));
};

/** Returns true if slave1 and slave2 have at least one common parent
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {boolean}
 */
globalThis.sameParent = function(slave1, slave2) {
	return sameMom(slave1, slave2) || sameDad(slave1, slave2);
};

/** Returns true if ID represents a unique, specific, and traceable character; false if it represents a nonspecific or group ID.
 * @param {number} ID
 * @returns {boolean}
 */
globalThis.specificCharacterID = function(ID) {
	return (ID > 0 ||		// active slave
			ID < -20 ||		// missing slave
			ID === -1 ||	// player
			ID === -3);		// player's old master
};

/** Returns true if the father of slave is a specific, unique character (current or former slave, or the PC)
 * @param {Relative} slave
 * @returns {boolean}
 */
globalThis.specificDad = function(slave) {
	return specificCharacterID(slave.father);
};

/** Returns true if the mother of slave is a specific, unique character (current or former slave, or the PC)
 * @param {Relative} slave
 * @returns {boolean}
 */
globalThis.specificMom = function(slave) {
	return specificCharacterID(slave.mother);
};

/** Returns true if aunt is the aunt of niece
 * @param {Relative} niece
 * @param {Relative} aunt
 * @returns {boolean}
 */
globalThis.isAunt = function(niece, aunt) {
	if (!V.showDistantRelatives) {
		return false;
	}

	if (!niece || !aunt || (niece.ID === aunt.ID)) {
		return false;
	}
	const father = getRelative(niece.father);
	const mother = getRelative(niece.mother);
	if (mother && (mother.ID !== aunt.ID) && !sameTParent(mother, aunt) && sameMom(mother, aunt) && sameDad(mother, aunt)) {
		return true;
	} else if (father && (father.ID !== aunt.ID) && !sameTParent(father, aunt) && sameMom(father, aunt) && sameDad(father, aunt)) {
		return true;
	}

	return false;
};

// catches the case if a mother is a father or a father a mother - thank you familyAnon, for this code
globalThis.sameTParent = function(slave1, slave2) {
	if (slave1.mother === -1 && slave1.father === -1 && slave2.mother === -1 && slave2.father === -1) {
		return 1;
	} else if (slave1.mother === slave2.father && slave1.father === slave2.mother && specificMom(slave1) && specificDad(slave1) && slave1.mother !== slave1.father) {
		return 2;
	} else if ((slave1.mother === slave2.father || slave1.father === slave2.mother) && specificMom(slave1) && specificMom(slave2) && slave1.mother !== slave1.father) {
		return 3;
	}
	return 0;
};

/** Returns the degree of siblinghood shared by two entities
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {number} - 0: not siblings; 1: twins; 2: full siblings; 3: half-siblings
 */
globalThis.areSisters = function(slave1, slave2) {
	if (slave1.ID === slave2.ID) {
		return 0; // you are not your own sister
	} else if (!specificDad(slave1) && !specificMom(slave1)) {
		return 0; // not related
	} else {
		if (!sameDad(slave1, slave2) && sameMom(slave1, slave2)) {
			return 3; // half sisters
		} else if (sameDad(slave1, slave2) && !sameMom(slave1, slave2)) {
			return 3; // half sisters
		} else if (sameTParent(slave1, slave2) === 3) {
			return 3; // half sisters
		} else if (sameTParent(slave1, slave2) === 2) {
			return 2; // sisters
		} else if (sameDad(slave1, slave2) && sameMom(slave1, slave2)) {
			if (slave1.actualAge === slave2.actualAge && slave1.birthWeek === slave2.birthWeek) {
				return 1; // twins
			} else {
				return 2; // sisters
			}
		} else {
			return 0; // not related
		}
	}
};

/** Returns true if two entities are simple first cousins (i.e. at least one parent of one slave is a full sibling of at least one parent of the other slave)
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {boolean}
 */
globalThis.areCousins = function(slave1, slave2) {
	if (!V.showDistantRelatives) {
		return false;
	}

	if (!slave1 || !slave2 || (slave1.ID === slave2.ID) || areSisters(slave1, slave2)) {
		return false;
	}

	const slave1Mom = getRelative(slave1.mother);
	const slave1Dad = getRelative(slave1.father);
	const slave2Mom = getRelative(slave2.mother);
	const slave2Dad = getRelative(slave2.father);

	if (slave1Mom && slave2Mom && !sameTParent(slave1Mom, slave2Mom) && sameMom(slave1Mom, slave2Mom) && sameDad(slave1Mom, slave2Mom)) {
		return true;
	} else if (slave1Mom && slave2Dad && !sameTParent(slave1Mom, slave2Dad) && sameMom(slave1Mom, slave2Dad) && sameDad(slave1Mom, slave2Dad)) {
		return true;
	} else if (slave1Dad && slave2Mom && !sameTParent(slave1Dad, slave2Mom) && sameMom(slave1Dad, slave2Mom) && sameDad(slave1Dad, slave2Mom)) {
		return true;
	} else if (slave1Dad && slave2Dad && !sameTParent(slave1Dad, slave2Dad) && sameMom(slave1Dad, slave2Dad) && sameDad(slave1Dad, slave2Dad)) {
		return true;
	}

	return false;
};

/** Returns whether two entities are *closely* related (i.e. siblings or parent/child).
 * Distant relatives are not checked by this function.
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {boolean}
 */
globalThis.areRelated = function(slave1, slave2) {
	return (slave1.father === slave2.ID || slave1.mother === slave2.ID || slave2.father === slave1.ID || slave2.mother === slave1.ID || areSisters(slave1, slave2) > 0);
};

/** Returns the total number of relatives that a slave has
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
globalThis.totalRelatives = function(slave) {
	let relatives = 0;
	if (slave.mother > 0) {
		relatives += 1;
	}
	if (slave.father > 0) {
		relatives += 1;
	}
	if (slave.daughters > 0) {
		relatives += slave.daughters;
	}
	if (slave.sisters > 0) {
		relatives += slave.sisters;
	}
	return relatives;
};

/** Returns the slaves which are mutual children of two entities
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @param {App.Entity.SlaveState[]} slaves
 * @returns {number}
 */
globalThis.mutualChildren = function(slave1, slave2, slaves) {
	return slaves.filter(function(s) {
		return s.ID !== slave1.ID && s.ID !== slave2.ID && s.mother > 0 && s.father > 0 && ((s.mother === slave1.ID && s.father === slave2.ID) || (s.mother === slave2.ID && s.father === slave1.ID));
	}).length;
};

/** Returns a random slave related to a given entity
 * @param {Relative} slave
 * @param {function(App.Entity.SlaveState): boolean} [filterFunction]
 * @returns {App.Entity.SlaveState}
 */
globalThis.randomRelatedSlave = function(slave, filterFunction) {
	if (!slave) {
		return undefined;
	}
	if (typeof filterFunction !== 'function') {
		filterFunction = () => true;
	}
	const arr = V.slaves.filter((s) => areRelated(slave, s) && filterFunction(s));
	return arr.random();
};

/**
 * @param {Relative} slave
 * @returns {App.Entity.SlaveState}
 */
globalThis.randomRelatedAvailableSlave = function(slave) {
	return randomRelatedSlave(slave, (s) => isSlaveAvailable(s));
};

globalThis.totalPlayerRelatives = function(pc) {
	let relatives = 0;
	if (pc.mother > 0) {
		relatives += 1;
	}
	if (pc.father > 0) {
		relatives += 1;
	}
	if (pc.daughters > 0) {
		relatives += pc.daughters;
	}
	if (pc.sisters > 0) {
		relatives += pc.sisters;
	}
	return relatives;
};

/** Returns the term used for slave2's sibling relationship to slave1, or null if there is no sibling relationship
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {string}
 */
globalThis.siblingTerm = function(slave1, slave2) {
	const useMaleTerms = (V.diversePronouns || slave2.ID === -1);
	switch (areSisters(slave2, slave1)) {
		case 1:
			if (slave2.genes === "XY" && useMaleTerms) {
				return "twin brother";
			} else {
				return "twin sister";
			}
		case 2:
			if (slave2.genes === "XY" && useMaleTerms) {
				return "brother";
			} else {
				return "sister";
			}
		case 3:
			if (slave2.genes === "XY" && useMaleTerms) {
				return "half-brother";
			} else {
				return "half-sister";
			}
	}
	return null;
};

/** Returns the term used for slave2's parental relationship to slave1, or null if there is no parental relationship
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {string}
 */
globalThis.parentTerm = function(slave1, slave2) {
	if (slave1.mother === slave2.ID && slave1.father === slave2.ID) {
		return "sole parent";
	} else if (slave1.mother === slave2.ID) {
		return "mother";
	} else if (slave1.father === slave2.ID) {
		return "father";
	}
	return null;
};

/**
 * Returns an array of any terms for slave2's relationship to slave1 (i.e. ["daughter"] if slave2 is slave1's daughter, or ["daughter", "niece"] if slave2 is both slave1's daughter and her niece).
 * Performs distant relative checking if enabled.
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {string[]}
 */
globalThis.relativeTerms = function(slave1, slave2) {
	let relations = [];
	const useMaleTerms = (V.diversePronouns || slave2.ID === -1);
	const parent = parentTerm(slave1, slave2);
	if (parent) {
		relations.push(parent);
	} else if (slave2.mother === slave1.ID || slave2.father === slave1.ID) {
		if (slave2.genes === "XY" && useMaleTerms) {
			relations.push("son");
		} else {
			relations.push("daughter");
		}
	}
	const sibling = siblingTerm(slave1, slave2);
	if (sibling) {
		relations.push(sibling);
	}
	if (isAunt(slave1, slave2)) {
		if (slave2.genes === "XY" && useMaleTerms) {
			relations.push("uncle");
		} else {
			relations.push("aunt");
		}
	}
	if (isAunt(slave2, slave1)) {
		if (slave2.genes === "XY" && useMaleTerms) {
			relations.push("nephew");
		} else {
			relations.push("niece");
		}
	}
	if (areCousins(slave2, slave1)) {
		relations.push("cousin");
	}
	if (isGrandfatherP(slave1, slave2)) {
		relations.push("grandfather");
	} else if (isGrandmotherP(slave1, slave2)) {
		relations.push("grandmother");
	} else if (isGrandparentP(slave2, slave1)) {
		if (slave2.genes === "XY" && useMaleTerms) {
			relations.push("grandson");
		} else {
			relations.push("granddaughter");
		}
	}

	return relations;
};

/**
 * Returns the term for slave2's relationship to slave1 (i.e. "daughter" if slave2 is slave1's daughter).
 * If multiple relationships exist, a compound noun will be returned (i.e. "daughter, granddaughter and niece")
 * Performs distant relative checking if enabled.
 * @param {Relative} slave1
 * @param {Relative} slave2
 * @returns {string|null} - returns null if the slaves are not related, even distantly.
 */
globalThis.relativeTerm = function(slave1, slave2) {
	const relations = relativeTerms(slave1, slave2);
	if (relations.length > 0) {
		return toSentence(relations);
	}
	return null;
};

/** completely reset all the family counters in the game state (for both PC and slaves) */
globalThis.resetFamilyCounters = function() {
	for (let slave of V.slaves) {
		slave.daughters = 0;
		slave.sisters = 0;
	}
	V.PC.daughters = 0;
	V.PC.sisters = 0;

	for (let slave of V.slaves) {
		if (slave.mother === -1 || slave.father === -1) {
			V.PC.daughters++;
		}
		if (areSisters(slave, V.PC)) {
			V.PC.sisters++;
		}
		for (let otherSlave of V.slaves) {
			if (isParentP(otherSlave, slave)) {
				slave.daughters++;
			}
			if (areSisters(otherSlave, slave)) {
				slave.sisters++;
			}
		}
	}
};

/** Set any missing parents to a known ID for the given entity (usually so that a sibling can be safely generated)
 * @param {Relative} slave
 */
globalThis.setMissingParents = function(slave) {
	const gp = V.genePool.find(s => s.ID === slave.ID);
	if (!specificMom(slave)) {
		slave.mother = V.missingParentID;
		if (gp) {
			gp.mother = slave.mother;
		}
		V.missingParentID--;
	}
	if (!specificDad(slave)) {
		slave.father = V.missingParentID;
		if (gp) {
			gp.father = slave.father;
		}
		V.missingParentID--;
	}
};

repo_name: Arkerthan/fc

license: mit


code:
/* eslint-disable camelcase */

/**
 * @param {FC.HumanState[]} slaves
 * @param {number} filterID
 * @returns {Element}
 */
globalThis.renderFamilyTree = function(slaves, filterID) {
	'use strict';

	const svgNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
	const svg = d3.select(svgNode);

	const data = buildFamilyTree(slaves, filterID);

	const ftreeWidth = Math.clamp(data.nodes.length * 45, 600, 1920);
	const ftreeHeight = Math.clamp(data.nodes.length * 35, 480, 1200);

	initFtreeSVG();
	runFtreeSim(data);

	return svgNode;

	function initFtreeSVG() {
		svg.attr('width', ftreeWidth).attr('height', ftreeHeight);

		svg.append('defs').append('marker')
			.attr('id', 'arrowhead')
			.attr('viewBox', '-0 -5 10 10')
			.attr('refX', 13)
			.attr('refY', 0)
			.attr('orient', 'auto')
			.attr('markerWidth', 13)
			.attr('markerHeight', 13)
			.append('svg:path')
			.attr('d', 'M 0,-1 L 5,0 L 0,1')
			.attr('fill', '#a1a1a1')
			.style('stroke', 'none');
	}

	function runFtreeSim(data) {
		let simulation = d3.forceSimulation()
			.force('collide', d3.forceCollide(60).iterations(4))
			.force('charge', d3.forceManyBody().strength(-200).distanceMin(100).distanceMax(1000))
			.force('center', d3.forceCenter(ftreeWidth / 2, ftreeHeight / 2))
			.force('y', d3.forceY(100))
			.force('x', d3.forceX(200));

		let link = svg.append('g')
			.attr('class', 'link')
			.selectAll('link')
			.data(data.links)
			.enter()
			.append('line')
			.attr('marker-end', 'url(#arrowhead)')
			.attr('stroke', function(d) {
				if (d.type === 'homologous') {
					return '#862d59';
				} else if (d.type === 'paternal') {
					return '#24478f';
				} else {
					return '#aa909b';
				}
			})
			.attr('stroke-width', 2)
			.attr('fill', 'none');

		let node = svg.selectAll('.node')
			.data(data.nodes)
			.enter().append('g')
			.attr('class', 'node')
			.call(d3.drag()
				.on('start', dragstarted)
				.on('drag', dragged)
				.on('end', dragended));

		node.append('circle')
			.attr('stroke', function(d) {
				if (d.ID === filterID) {
					return '#ffff20';
				} else {
					return '#5a5a5a';
				}
			})
			.attr('class', 'node-circle')
			.attr('r', 20);

		node.append('text')
			.text(function(d) {
				let ssym;
				if (d.dick > 0 && d.vagina > -1) {
					ssym = '☿';
				} else if (d.dick > 0) {
					ssym = '♂';
				} else if (d.vagina > -1) {
					ssym = '♀';
				} else {
					ssym = '?';
				}
				return `${d.name}(${ssym})`;
			})
			.attr('dy', 4)
			.attr('dx', function(d) {
				return -(8 * d.name.length) / 2;
			})
			.attr('class', 'node-text')
			.style('fill', function(d) {
				if (d.is_mother && d.is_father) {
					return '#b84dff';
				} else if (d.is_father) {
					return '#00ffff';
				} else if (d.is_mother) {
					return '#ff3399';
				} else if (d.unborn) {
					return '#a3a3c2';
				} else {
					return '#66cc66';
				}
			});

		let ticked = function() {
			link
				.attr('x1', function(d) { return d.source.x; })
				.attr('y1', function(d) { return d.source.y; })
				.attr('x2', function(d) { return d.target.x; })
				.attr('y2', function(d) { return d.target.y; });

			node
				.attr("transform", function(d) { return `translate(${d.x}, ${d.y})`; });
		};

		simulation.nodes(data.nodes)
			.on('tick', ticked);

		simulation.force('link', d3.forceLink().links(data.links));

		function dragstarted(event, d) {
			if (!event.active) { simulation.alphaTarget(0.3).restart(); }
			d.fx = d.x;
			d.fy = d.y;
		}

		function dragged(event, d) {
			d.fx = event.x;
			d.fy = event.y;
		}

		function dragended(event, d) {
			if (!event.active) { simulation.alphaTarget(0); }
			d.fx = null;
			d.fy = null;
		}
	}
};

/**
 * @param {FC.HumanState[]} slaves
 * @param {number} filterID
 */
globalThis.buildFamilyTree = function(slaves, filterID) {
	let family_graph = {nodes: [], links: []};
	let node_lookup = {};
	let preset_lookup = {
		'-2': 'A citizen',
		'-3': 'Former Master',
		'-4': 'An arcology owner',
		'-5': 'Client',
		'-6': 'Social Elite',
		'-7': 'Gene lab'
	};
	/** @type {Record<string, Array<string>>} */
	let outdads = {};
	/** @type {Record<string, Array<string>>} */
	let outmoms = {};
	let kids = {};

	const realPC = slaves.deleteWith(s => s.ID === -1).first() || V.PC;
	let fake_pc = {
		slaveName: `${realPC.slaveName}(You)`,
		mother: realPC.mother,
		father: realPC.father,
		dick: realPC.dick,
		vagina: realPC.vagina,
		ID: realPC.ID
	};
	let charList = [fake_pc].concat(slaves).concat(V.incubator.tanks);

	let unborn = {};
	for (const child of V.incubator.tanks) {
		unborn[child.ID] = true;
	}
	for (const child of V.cribs) {
		unborn[child.ID] = true;
	}

	for (const character of charList) {
		let mom = character.mother;
		let dad = character.father;

		if (mom) {
			if (!kids[mom]) {
				kids[mom] = {};
			}
			kids[mom].mother = true;
		}
		if (dad) {
			if (!kids[dad]) {
				kids[dad] = {};
			}
			kids[dad].father = true;
		}
	}

	const haveChar = (ID) => charList.includes(c => c.ID === ID);
	for (const character of charList) {
		if (character.mother === 0 && character.father === 0 && !kids[character.ID]) {
			continue;
		}
		let mom = character.mother;
		if (mom < -6) {
			if (mom in V.missingTable && V.showMissingSlaves && !haveChar(mom)) {
				const missing = V.missingTable[mom];
				charList.push({
					ID: mom,
					mother: missing.mother,
					father: missing.father,
					is_mother: true,
					dick: missing.dick,
					vagina: missing.vagina,
					slaveName: missing.slaveName
				});
			} else {
				if (typeof outmoms[mom] === 'undefined') {
					outmoms[mom] = [];
				}
				outmoms[mom].push(character.slaveName);
			}
		} else if (mom < 0 && typeof preset_lookup[mom] !== 'undefined' && !haveChar(mom)) {
			charList.push({
				ID: mom,
				mother: 0,
				father: 0,
				is_father: true,
				dick: 0,
				vagina: 1,
				slaveName: preset_lookup[mom]
			});
		}

		let dad = character.father;
		if (dad < -6) {
			if (dad in V.missingTable && V.showMissingSlaves && !haveChar(dad)) {
				const missing = V.missingTable[dad];
				charList.push({
					ID: dad,
					mother: missing.mother,
					father: missing.father,
					is_father: true,
					dick: missing.dick,
					vagina: missing.vagina,
					slaveName: missing.slaveName
				});
			} else {
				if (typeof outdads[dad] === 'undefined') {
					outdads[dad] = [];
				}
				outdads[dad].push(character.slaveName);
			}
		} else if (dad < 0 && typeof preset_lookup[dad] !== 'undefined' && !haveChar(dad)) {
			charList.push({
				ID: dad,
				mother: 0,
				father: 0,
				is_father: true,
				dick: 1,
				vagina: -1,
				slaveName: preset_lookup[dad]
			});
		}
	}
	let mkeys = Object.keys(outmoms);
	for (const key of mkeys) {
		let name;
		let names = outmoms[key];
		if (names.length === 1) {
			name = names[0];
		} else if (names.length === 2) {
			name = names.join(' and ');
		} else {
			names[-1] = `and ${names[-1]}`;
			name = names.join(', ');
		}
		// Outside extant slaves set
		charList.push({
			ID: key,
			mother: 0,
			father: 0,
			is_mother: true,
			dick: 0,
			vagina: 1,
			slaveName: `${name}'s mother`
		});
	}

	let dkeys = Object.keys(outdads);
	for (const key of dkeys) {
		let name;
		let names = outdads[key];
		if (names.length === 1) {
			name = names[0];
		} else if (names.length === 2) {
			name = names.join(' and ');
		} else {
			names[-1] = `and ${names[-1]}`;
			name = names.join(', ');
		}
		// Outside extant slaves set
		charList.push({
			ID: key,
			mother: 0,
			father: 0,
			is_father: true,
			dick: 1,
			vagina: -1,
			slaveName: `${name}'s father`
		});
	}

	let charHash = {};
	for (const character of charList) {
		charHash[character.ID] = character;
	}

	let related = {};
	let seen = {};
	let saveTree = {};

	function relatedTo(character, targetID, relIDs = {tree: {}, related: false}) {
		relIDs.tree[character.ID] = true;
		if (related[character.ID]) {
			relIDs.related = true;
			return relIDs;
		}
		if (character.ID === targetID) {
			relIDs.related = true;
		}
		if (seen[character.ID]) {
			return relIDs;
		}
		seen[character.ID] = true;
		if (character.mother !== 0) {
			if (charHash[character.mother]) {
				relatedTo(charHash[character.mother], targetID, relIDs);
			}
		}
		if (character.father !== 0) {
			if (charHash[character.father]) {
				relatedTo(charHash[character.father], targetID, relIDs);
			}
		}
		return relIDs;
	}
	if (filterID) {
		if (charHash[filterID]) {
			let relIDs = relatedTo(charHash[filterID], filterID);
			for (let k in relIDs.tree) {
				related[k] = true;
			}
			for (const character of charList) {
				let pRelIDs = relatedTo(character, filterID);
				if (pRelIDs.related) {
					for (let k in pRelIDs.tree) {
						related[k] = true;
						if (saveTree[k]) {
							for (let k2 in saveTree[k].tree) {
								related[k2] = true;
							}
						}
					}
				}
				saveTree[character.ID] = pRelIDs;
			}
		}
	}

	for (const character of charList) {
		let char_id = character.ID;
		if (char_id !== filterID) {
			if (character.mother === 0 && character.father === 0 && !kids[char_id]) {
				continue;
			}
			if (filterID && !related[char_id]) {
				continue;
			}
		}
		node_lookup[char_id] = family_graph.nodes.length;
		let char_obj = {
			ID: char_id,
			name: character.slaveName,
			dick: character.dick,
			unborn: !!unborn[char_id],
			vagina: character.vagina
		};
		if (kids[char_id]) {
			char_obj.is_mother = !!kids[char_id].mother;
			char_obj.is_father = !!kids[char_id].father;
		} else {
			char_obj.is_mother = false;
			char_obj.is_father = false;
		}
		family_graph.nodes.push(char_obj);
	}

	for (const character of charList) {
		let char_id = character.ID;
		if (character.mother === 0 && character.father === 0 && !kids[char_id]) {
			continue;
		}
		if (filterID && !related[char_id]) {
			continue;
		}
		if (typeof node_lookup[character.mother] !== 'undefined') {
			const ltype = (character.mother === character.father) ? 'homologous' : 'maternal';
			family_graph.links.push({
				type: ltype,
				target: node_lookup[char_id] * 1,
				source: node_lookup[character.mother] * 1
			});
		}
		if (character.mother === character.father) {
			continue;
		}
		if (typeof node_lookup[character.father] !== 'undefined') {
			family_graph.links.push({type: 'paternal', target: node_lookup[char_id] * 1, source: node_lookup[character.father] * 1});
		}
	}
	return family_graph;
};


code:
App.UI.findSlave = function() {
	// When we open this for the first time - forget the previous search data
	App.UI.findSlave.searchData = null;
	App.UI.findSlave.searchName = null;

	const wrapper = document.createElement("div");
	wrapper.append(structure());
	return wrapper;

	function structure() {
		const node = new DocumentFragment();

		// App.UI.DOM.appendNewElement("h1", node, `Find slave`);

		App.UI.DOM.appendNewElement("p", node, `After spending a minute trying to remember some details about one of your slaves, you sit down behind your desk and tell ${V.assistant.name} that you need to locate a particular slave's records.`);

		App.UI.DOM.appendNewElement("p", node, `"Certainly, ${properMaster()}. What can you tell me about them?"`);

		// convert it into the array, if it's not (this will make this not backwards compatible though I think?
		if (!Array.isArray(V.findName)) { V.findName = [V.findName]; }
		if (!Array.isArray(V.findBackground)) { V.findBackground = [V.findBackground]; }
		if (!Array.isArray(V.findData)) { V.findData = [V.findData]; }

		// clean up empty values
		V.findName = V.findName.filter(function(element) { return !!element; });
		V.findBackground = V.findBackground.filter(function(element) { return !!element; });
		V.findData = V.findData.filter(function(element) { return !!element; });
		// by searching for empty variable - you clean up history, otherwise its kept

		// always have last element empty - so we can input something new and well... also limit it to 20 elements
		V.findName.push(""); if (V.findName.length > 20) { V.findName.length = 20; }
		V.findBackground.push(""); if (V.findBackground.length > 20) { V.findBackground.length = 20; }
		V.findData.push(""); if (V.findData.length > 20) { V.findData.length = 20; }

		function _findData(vArray, search) {
			let array = [];
			for (let i = 0; i < vArray.length; i++) {
				let textBox = App.UI.DOM.makeTextBox(vArray[i], v => vArray[i] = v);
				if (search === "searchByExpression") {
					textBox.style.width="60%";
				} // probably not the best, but personally for searchByExpression - we can do long expressions...
				array.push(`<br>`);
				array.push(textBox);
				array.push(
					App.UI.DOM.link(
						"Locate",
						// () => $(slaveList).empty().append(vFunc(vArray[i]))
						() => {
							App.UI.findSlave.searchName = search;
							App.UI.findSlave.searchData = vArray[i];
							refresh(); // tell us what search to perform
						}
					)
				);
			}
			return array;
		}
		App.Events.addParagraph(node, [
			`"They're called something like:`,
			..._findData(V.findName, "searchByName"),
			App.UI.DOM.makeElement("div", `(Enter a fragment of their nickname, name, surname, birth name, or birth surname)`, "note")
		]);

		App.Events.addParagraph(node, [
			`"In the past, they were:`,
			..._findData(V.findBackground, "searchByBackground"),
			App.UI.DOM.makeElement("div", `(Enter a fragment of their origin or past job, for example, "shelter" or "lawyer")`, "note")
		]);

		App.Events.addParagraph(node, [
			`"Their data should meet this condition:`,
			..._findData(V.findData, "searchByExpression"),
			App.UI.DOM.makeElement("div", `(Enter a conditional expression which evaluates to true for the slave you want to find, such as "slave.physicalAge >= 18 && slave.physicalAge < 21")`, "note")
		]);

		let slaveList;
		if (App.UI.findSlave.searchName !== undefined && App.UI.findSlave.searchName !== null && App.UI.findSlave.searchData !== undefined && App.UI.findSlave.searchData !== null) {
			switch (App.UI.findSlave.searchName) {
				case "searchByExpression":
					slaveList = App.UI.DOM.appendNewElement("span", node, searchByExpression(App.UI.findSlave.searchData));
					break;
				case "searchByBackground":
					slaveList = App.UI.DOM.appendNewElement("span", node, searchByBackground(App.UI.findSlave.searchData));
					break;
				case "searchByName":
					slaveList = App.UI.DOM.appendNewElement("span", node, searchByName(App.UI.findSlave.searchData));
					break;
			}
		} else {
			slaveList = App.UI.DOM.appendNewElement("span", node);
		}

		return node;
	}

	function refresh() {
		App.UI.DOM.replace(wrapper, structure());
	}

	/**
	 * Fragment searching: See if every needle can found somewhere in the field of haystacks
	 * @param {FC.Zeroable<string>[]} haystacks
	 * @param {RegExp[]} needles
	 * @returns {boolean}
	 */
	function _fragmentSearch(haystacks, needles) {
		const hs = haystacks.filter(h => !!h).join(" ");
		return needles.every((needle) => needle.test(hs));
	}

	/**
	 * Get slave ids which match a predicate
	 * @param {function(App.Entity.SlaveState): boolean} predicate
	 * @returns {number[]}
	 */
	 function _slaveIDs(predicate) {
		return V.slaves.reduce((acc, slave) => {
			if (predicate(createReadonlyProxy(slave))) {
				acc.push(slave.ID);
			}
			return acc;
		}, []);
	}

	/**
	 * Display a list of results, or text indicating that there were none
	 * @param {number[]} ids
	 * @param {DocumentFragment} frag
	 */
	function _appendResultList(ids, frag) {
		App.UI.DOM.appendNewElement("p", frag, `${ids.length} matching slave${ids.length === 1 ? `` : `s`}.`);
		if (ids.length !== 0) {
			frag.appendChild(App.UI.SlaveList.render(ids, [], App.UI.SlaveList.SlaveInteract.stdInteract));
		}
	}

	/**
	 * Generate a slave list as the result of fragment searching all the name-type fields
	 * @param {string} query
	 * @returns {DocumentFragment}
	 */
	function searchByName(query) {
		const frag = document.createDocumentFragment();
		if (query) {
			const resultTitle = App.UI.DOM.appendNewElement("p", frag, "Query results for name: ");
			App.UI.DOM.appendNewElement("code", resultTitle, query);
			const needles = query.split(" ").map((needle) => { return new RegExp(needle, "i"); });
			const ids = _slaveIDs((slave) => { return _fragmentSearch([slave.slaveName, slave.slaveSurname, slave.birthName, slave.birthSurname], needles); });
			_appendResultList(ids, frag);
		}
		return frag;
	}

	/**
	 * Generate a slave list as the result of fragment searching profession and origin
	 * @param {string} query
	 * @returns {DocumentFragment}
	 */
	function searchByBackground(query) {
		const frag = document.createDocumentFragment();
		if (query) {
			const resultTitle = App.UI.DOM.appendNewElement("p", frag, "Query results for background: ");
			App.UI.DOM.appendNewElement("code", resultTitle, query);
			const needles = query.split(" ").map((needle) => { return new RegExp(needle, "i"); });
			const ids = _slaveIDs((slave) => { return _fragmentSearch([slave.career, slave.origin], needles); });
			_appendResultList(ids, frag);
		}
		return frag;
	}

	/**
	 * Generate a slave list as the result of evaluating an expression
	 * @param {string} query
	 * @returns {DocumentFragment}
	 */
	function searchByExpression(query) {
		const frag = document.createDocumentFragment();
		if (query) {
			const resultTitle = App.UI.DOM.appendNewElement("p", frag, "Query results from expression: ");
			App.UI.DOM.appendNewElement("code", resultTitle, query);
			/** @type {function(App.Entity.SlaveState):boolean} */
			// @ts-ignore
			const pred = new Function("slave", "return (" + query + ");");
			const ids = runWithReadonlyProxy(() => { return _slaveIDs(pred); });
			_appendResultList(ids, frag);
		}
		return frag;
	}
};


code:
/** Utilities to list slave basing on their conformance to FS policies */
App.FSConformance = function() {
	/** @type {Record<FC.FSHumanDevelopmentVector, [FC.FutureSocietyDeco, FC.FutureSocietyDeco]>} */
	const decosForVector = {
		[FSHumanVector.AGE]: ['Maturity Preferentialist', 'Youth Preferentialist'],
		[FSHumanVector.HEIGHT]: ['Statuesque Glorification', 'Petite Admiration'],
		[FSHumanVector.WEIGHT]: ['Hedonistic', 'Physical Idealist'],
		[FSHumanVector.MODIFICATIONS]: ['Transformation Fetishist', 'Body Purist'],
		[FSHumanVector.ASSETS]: ['Asset Expansionist', 'Slimness Enthusiast'],
		[FSHumanVector.INTELLIGENCE]: ['Slave Professionalism', 'Intellectual Dependency'],
		[FSHumanVector.GENDER]: ['Gender Radicalist', 'Gender Fundamentalist'],
		[FSHumanVector.BREEDING]: ['Repopulationist', 'Eugenics'],
		[FSHumanVector.LIFE_QUALITY]: ['Paternalist', 'Degradationist'],
	};

	function anyVectorsDefined() {
		return Object.values(FSHumanVector).some(v => FutureSocieties.humanVector(v, V.arcologies[0]) !== null);
	}

	/**
	 * @param {{slave: FC.ReportSlave, effects: FC.SlaveSocialEffect[]}[]} slavesWithEffects
	 * @param {FC.FutureSocietyDeco} fsDeco
	 * @returns {{slave: FC.ReportSlave, magnitude: number}[]}
	 */
	function affectedSlaves(slavesWithEffects, fsDeco) {
		return slavesWithEffects
			.filter(item => item.effects.some(ef => fsDeco === ef.FS))
			.map(item => {
				const totalMagnitude = item.effects.filter(ef => fsDeco === ef.FS).reduce((total, ef) => total + ef.magnitude, 0);
				return {slave: item.slave, magnitude: totalMagnitude};
			});
	}

	/**
	 * @param {{slave: FC.ReportSlave, magnitude: number}[]} slavesWithEffects
	 * @param {FC.FutureSocietyDeco} deco
	 * @param {boolean} top
	 */
	function listSlaves(slavesWithEffects, deco, top) {
		if (top) {
			slavesWithEffects.sort((left, right) => right.magnitude - left.magnitude);
		} else {
			slavesWithEffects.sort((left, right) => left.magnitude - right.magnitude);
		}

		return App.UI.SlaveList.render(
			slavesWithEffects.slice(0, V.underperformersCount).map(item => item.slave.ID),
			[],
			App.UI.SlaveList.SlaveInteract.stdInteract,
		);
	}

	function slaveConformancePassage() {
		const node = new DocumentFragment();
		const r = [];
		r.push(App.UI.DOM.makeElement("div", `${properMaster()}, while you spend numerous hours and put a lot of efforts to shape the society in your arcology for the envisioned future, you might be interested to know how well your own slaves conform to that vision.`));

		App.Events.addParagraph(node, r);

		/** @type {{slave: FC.ReportSlave, effects: FC.SlaveSocialEffect[]}[]} */
		const slaveSocialEffects = [];
		for (const rpSlave of App.SlaveAssignment.reportSlaves(V.slaves)) {
			slaveSocialEffects.push({slave: rpSlave, effects: App.SlaveAssignment.saSocialEffects(rpSlave).effects()});
		}

		const conformandDiv = App.UI.DOM.makeElement("div");
		const conformantTabBar = new App.UI.Tabs.TabBar("fs-conformance-list");

		const nonConformantDiv = App.UI.DOM.makeElement("div");
		const nonConformantTabBar = new App.UI.Tabs.TabBar("fs-non-conformance-list");

		for (const vector of Object.values(FSHumanVector)) {
			const vectorValue = FutureSocieties.humanVector(vector, V.arcologies[0]);
			if (vectorValue === null) { continue; }
			const deco = vectorValue > 0 ? decosForVector[vector][0] : decosForVector[vector][1];

			const slavesForDeco = affectedSlaves(slaveSocialEffects, deco);

			conformantTabBar.addTab(deco, `${deco}-conform`, listSlaves(slavesForDeco.filter(item => item.magnitude > 0), deco, true));
			nonConformantTabBar.addTab(deco, `${deco}-non-conform`, listSlaves(slavesForDeco.filter(item => item.magnitude <= 0), deco, false));
		}

		conformandDiv.append(conformantTabBar.render());
		nonConformantDiv.append(nonConformantTabBar.render());

		node.append(App.UI.DOM.accordion("Conformant", conformandDiv));
		node.append(App.UI.DOM.accordion("Non-conformant", nonConformantDiv));

		const g = new App.UI.OptionsGroup();
		g.addOption("Maximum number of slaves", "underperformersCount")
			.addValue("Default", 7).showTextBox();
		node.append(g.render());

		V.nextButton = "Back";
		V.nextLink = "Future Society";

		return node;
	}

	return {
		anyVectorsDefined,
		slaveConformancePassage,
	};
}();


code:
/**
 * checks for illness and reduces the effectiveness of the slave accordingly -- 10, 25, 50, 85, 95% reduced
 * also checks for tiredness and reduces effectiveness from that as well -- 25% reduction at 90+ and 10% at 60-90
 * @param {FC.HumanState} slave
 * @returns {number}
 */
globalThis.healthPenalty = function(slave) {
	const H = slave.health;
	let penalty = 100;
	const realSlave = asSlave(slave);
	if (!realSlave || realSlave.assignment !== Job.GLORYHOLE) {
		if (H.illness > 0) {
			penalty *= (100 - Math.min((Math.pow(H.illness, 2) * 5 + 5), 95)) / 100;
		}
		// tiredness does not affect the productivity of restrained slaves (or the PC)
		const tirednessPenaltyApplies = realSlave && realSlave.fuckdoll === 0 && (realSlave.assignment !== Job.DAIRY || V.dairyRestraintsSetting <= 1);
		if (tirednessPenaltyApplies) {
			if (H.tired > 90) {
				penalty *= 0.75;
			} else if (H.tired > 60) {
				penalty *= 0.90;
			} else if (H.tired === 0) { // Experiment: Fulled rested slaves perform slightly better.
				penalty *= 1.05;
			}
		}
	}
	penalty = Math.trunc(penalty) / 100;
	return penalty;
};

/**
 * All things hurting a slave go through this to update short term damage and slave health.
 * @param {App.Entity.SlaveState|App.Entity.PlayerState} slave
 * @param {number} damage
 * @param {number} mult
 * @returns {void}
 */
globalThis.healthDamage = function(slave, damage, mult = 1) {
	if (!_.isFinite(damage)) {
		throw Error("Health damage must be a finite number.");
	}
	const H = slave.health;
	damage = Math.max(Math.trunc(damage), 0);
	H.shortDamage += damage * mult;
	H.condition -= damage;
	updateHealth(slave);
};

/**
 * All things directly curing wounds on a slave go through this to update short term damage and slave health.
 * Use sparingly and for direct medical treatment by qualified professionals only; improveCondition should be used instead for drugs, natural healing, etc.
 * @param {App.Entity.SlaveState} slave
 * @param {number} cure
 * @returns {void}
 */
globalThis.healthCure = function(slave, cure) {
	if (!_.isFinite(cure)) {
		throw Error("Health cure must be a finite number.");
	}
	const H = slave.health;
	cure = Math.max(Math.trunc(cure), 0);
	if (cure > H.shortDamage) {
		cure = H.shortDamage;
	}
	H.shortDamage -= cure;
	H.condition += cure;
	updateHealth(slave);
};

/**
 * Surgical procedures also depend on the PC's medicine skill
 * @param {App.Entity.SlaveState} slave
 * @param {number} damage
 * @param {boolean} [cheat]
 * @returns {void}
 */
globalThis.surgeryDamage = function(slave, damage, cheat = false) {
	if (!cheat) {
		const playerSkillFactor = 1 + Math.clamp(Math.pow(V.PC.skill.medicine / 100, 2), 0, 1);
		healthDamage(slave, Math.trunc(damage / playerSkillFactor));
	}
};

/**
 * All things improving a slave's condition go through this to update condition and slave health.
 * @param {App.Entity.SlaveState|App.Entity.PlayerState} slave
 * @param {number} condition
 * @returns {void}
 */
globalThis.improveCondition = function(slave, condition) {
	if (!_.isFinite(condition)) {
		throw Error("Health condition change must be a finite number.");
	}
	const H = slave.health;
	H.condition += Math.max(Math.trunc(condition), 0);
	updateHealth(slave);
};

/**
 * Computes the aggregate health value for a slave given an assumed condition value.
 * Generally should not be called directly (use updateHealth instead); needed for health gingering.
 * @param {App.Entity.SlaveState|App.Entity.PlayerState} slave
 * @param {number} condition
 * @returns {number}
 */
globalThis.computeAggregateHealth = function(slave, condition) {
	const H = slave.health;

	// Converting the other variables to better fit the same scale as .condition
	const damage = (40 - H.shortDamage - H.longDamage) * 2.5; // 100 / -inf
	const tired = (40 - H.tired) * 2.5; // 100 / -150
	const illness = (5 - Math.pow(H.illness, 2)) * 20; // 100 / -400

	// Assigning weights to the different components and aggregating
	return condition * 0.6 + damage * 0.2 + tired * 0.1 - illness * 0.1;
};

/**
 * Updates slave.health.health
 * @param {App.Entity.SlaveState|App.Entity.PlayerState} slave
 * @returns {void}
 */
globalThis.updateHealth = function(slave) {
	const H = slave.health;
	H.health = computeAggregateHealth(slave, H.condition);
};

/**
 * A function for setting valid health values for new slaves, comes with a set of defaults.
 * Not meant for use with existing slaves (consider healthDamage(), surgeryDamage() and improveCondition() first), but sometimes useful still.
 * @param {App.Entity.SlaveState} slave
 * @param {number} condition
 * @param {number} [shortDamage]
 * @param {number} [longDamage]
 * @param {number} [illness]
 * @param {number} [tired]
 * @returns {void}
 */
globalThis.setHealth = function(slave, condition = -101, shortDamage = -1, longDamage = -1, illness = -1, tired = -1) {
	const H = slave.health;

	// Making sure all values get set to either a default or their desired value
	if (condition < -100) {
		H.condition = jsRandom(-50, 50); // Default value
	} else {
		H.condition = condition;
	}
	if (shortDamage < 0) {
		H.shortDamage = Math.max(normalRandInt(0, 3), 0);
	} else {
		H.shortDamage = shortDamage;
	}
	if (longDamage < 0) {
		H.longDamage = Math.max(normalRandInt(0, 3), 0);
	} else {
		H.longDamage = longDamage;
	}
	if (V.seeIllness !== 0) {
		if (illness < 0 || illness > 5) {
			H.illness = Math.max(normalRandInt(0, 0.5), 0);
		} else {
			H.illness = illness;
		}
	} else {
		H.illness = 0;
	}
	if (tired < 0 || tired > 100) {
		H.tired = jsRandom(10, 40);
	} else {
		H.tired = tired;
	}

	// Adjusting long term damage for age
	if (slave.physicalAge > 29) {
		for (let i = 1; i < (slave.physicalAge - 29); i++) {
			H.longDamage += Math.trunc((i + 4 + jsRandom(1, 15)) / 20);
		}
	}

	// Making sure their overall health doesn't go too low (and would lead to a very likely death)
	if (H.condition - H.shortDamage - H.longDamage < -100) {
		let excess = -100 + H.condition + H.shortDamage + H.longDamage;
		if (H.shortDamage + H.longDamage < 50) { // As long as the damage doesn't get too crazy we'll compensate condition for it
			H.condition += excess;
		} else if (H.longDamage < 50) { // As long as long damage doesn't get too high we'll compensate by improving shortDamage and condition
			if (H.shortDamage - Math.ceil(excess / 2) > 0) {
				H.shortDamage -= Math.ceil(excess / 2);
				H.condition += Math.ceil(excess / 2);
			} else {
				excess -= H.shortDamage;
				H.shortDamage = 0;
				H.condition += excess;
			}
		} else { // If such high longDamage was intentional, they really should have done better basic math to ensure these slaves wouldn't just die immediately
			if (H.shortDamage - Math.ceil(excess / 3) > 0 && H.longDamage - Math.ceil(excess / 3) > 0) {
				H.longDamage -= Math.ceil(excess / 3);
				H.shortDamage -= Math.ceil(excess / 3);
				H.condition += Math.ceil(excess / 3);
			} else {
				excess -= H.shortDamage;
				H.shortDamage = 0;
				if (H.longDamage - Math.ceil(excess / 2) > 0) {
					H.longDamage -= Math.ceil(excess / 2);
					H.condition += Math.ceil(excess / 2);
				} else {
					excess -= H.longDamage;
					H.longDamage = 0;
					H.condition += excess;
				}
			}
		}
	}

	// Finally giving slaves their overall health value
	updateHealth(slave);
};


code:
/** @typedef IBCRelative
 * An very simple object that represents a entity in a family tree.
 * Represents a group of common properties shared by SlaveState, InfantState, and PlayerState,
 * as well as genepool objects.
 * @type {object}
 * @property {number} ID
 * @property {number} mother
 * @property {number} father
 * @property {number} [inbreedingCoeff]
 */

// TODO: replace snake_case with camelCase

/* eslint-disable camelcase */
globalThis.ibc = (() => {
	let realWorld =
	{
		getPlayerCharacter() { return V.PC; },

		findSlaveState(id) {
			return (slaveStateById(id) || V.genePool.find((s) => s.ID === id) || ((id in V.missingTable) ? V.missingTable[id] : null) || null);
		},

		getSlavesAndSuch() {
			let all_slave_like = V.slaves.concat(V.genePool).concat(V.cribs).concat(V.incubator.tanks).concat(Object.values(V.missingTable));

			if (V.boomerangSlave !== 0) all_slave_like.push(V.boomerangSlave);
			if (V.traitor !== 0) all_slave_like.push(V.traitor);
			if (V.activeSlave !== 0) all_slave_like.push(V.activeSlave);

			return all_slave_like;
		},

		getFetuses() {
			/** @type {FC.FetusGenetics[]} */
			return V.slaves.filter(s => s.preg > 0).map(s => s.womb.map(i => i.genetics)).reduce((res, cur) => res.concat(cur), []);
		}
	};

	let hasInbreedingCoefficient = (s) => "inbreedingCoeff" in s && s["inbreedingCoeff"] !== -1;

	// These IDs are considered to be unknown parents
	let or_null = (s) => specificCharacterID(s) ? s : null;

	/** The player's old master is a known parent, but we don't normally have a slavelike object
	 * for him. We use this one instead, wherever necessary.
	 * @type {IBCRelative} */
	const oldMaster = {
		ID: -3,
		mother: 0,
		father: 0,
		inbreedingCoeff: 0
	};

	/** Create a node for the given ID
	 * @param {number} id
	 */
	function IBCNode(id) {
		this.id = id; // Node ID
		this.mother = null;
		this.father = null;
		this._coeff = null; // Cached CoI
		this._ancestorNodeCount = null;
	}

	class CoancestryCache
	{
		valuesByLeastParentId = {};

		coefficientOfInbreeding(node) {
			if (node._coeff === null)
				node._coeff = this.kinship(node.mother, node.father);

			return node._coeff;
		}

		kinship(motherNode, fatherNode) {  // also known as "coancestry"; this is equal to the coefficient of inbreeding for any children the two given parents may have
			if (!motherNode || !fatherNode)
				return 0;
			else if (motherNode === fatherNode)
				// just two simple rules are needed to express it: self-with-self coancestry is (1 + coefficient of inbreeding of self) / 2, and...
				return (1 + this.coefficientOfInbreeding(motherNode)) / 2;

			return this.kinshipCache(motherNode.id, fatherNode.id,
				() => {
					let p1 = motherNode;
					let p2 = fatherNode;
					if (p1._ancestorNodeCount > p2._ancestorNodeCount)
					{
						// (to exercise the second rule mentioned in the comment below, it's fine to step up to either person's parents; but here the stepping up is aimed into
						// the larger of the two ancestry trees, in a crude effort to keep the kinship() arguments at about the same generational level when possible, thereby
						// maybe maximizing the chance that the same few pairs of arguments will keep coming into the function - which may possibly make caching extra effective.
						// as the wording here indicates, the benefit of this has not actually been verified, but what the heck it's simple)
						let p_ = p1;
						p1 = p2;
						p2 = p_;
					}

					// ...one-with-another coancestry is (one-with-other's-father coancestry + one-with-other's-mother coancestry) / 2
					return (this.kinship(p1, p2.mother) + this.kinship(p1, p2.father)) / 2;
				});
		}

		kinshipCache(motherId, fatherId, calculateValue) {  // this method exists solely for caching purposes
			let id1 = motherId;
			let id2 = fatherId;
			if (id1 > id2) { let id_ = id1; id1 = id2; id2 = id_; }

			let vs = this.valuesByLeastParentId;

			let vs1 = vs[id1];
			if (vs1 === undefined)
				vs1 = vs[id1] = [];

			// cache format, conceptually: a set of arrays of numbers. the top-level set is indexed by id1. a lower-level array has the format
			// [id, kinship value, id, kinship value, ...] with the id in an entry corresponding to id2 here and with the pairs in the array being kept invariably sorted by id.
			return this.kinship_cache(vs1, id2, calculateValue);
		}

		kinship_cache(vs, id, calculateValue) {  // this method exists solely for caching purposes
			let entryCount = vs.length / 2;

			// do a simple binary search
			let searchRangeStart = 0;
			let searchRangeEnd = entryCount;
			while (searchRangeEnd > searchRangeStart)
			{
				let middleIndex = searchRangeStart + Math.floor((searchRangeEnd - searchRangeStart) / 2);
				let idThere = vs[middleIndex * 2];
				if (idThere === id)
					return vs[middleIndex * 2 + 1];  // cached value found
				else if (id < idThere)
					searchRangeEnd = middleIndex;
				else
					searchRangeStart = middleIndex + 1;
			}

			// the appropriate location for the cached value was found, but the value isn't there yet, so add it
			let v = calculateValue();
			vs.splice(searchRangeStart * 2, 0, id, v);
			return v;
		}
	}

	/** Make nodes for an array of slaves
	 * @param {IBCRelative[]} slaves
	 * @param {boolean} [ignore_coeffs=false]
	 * @returns {Object.<number, IBCNode>|{}}
	 */
	let nodes_slaves = (world, slaves, ignore_coeffs = false) => {
		let nodes = {};

		/** Recursively create the nodes we need, moving upwards from the given slave
		 * @param {IBCRelative} s
		 */
		let create_node_rec = s => {
			if (!(s.ID in nodes)) {
				nodes[s.ID] = new IBCNode(s.ID);

				// Certain parents (e.g. 0, societal elite) are not considered to be related, despite
				// having the same ID; convert them to null
				const m = or_null(s.mother);
				const f = or_null(s.father);

				// Ensure that parent nodes are created
				[m, f].forEach(p => {
					if (p !== null && !(p in nodes)) { // Not created, we have to do something
						if (p === -1) {
							create_node_rec(world.getPlayerCharacter());
						} else {
							// Search for a slave state, genePool entry, or missingTable entry
							let gp = world.findSlaveState(p);
							if (gp !== null) {
								// If we find one, we might have ancestry information: recurse
								create_node_rec(gp);
							} else {
								// Otherwise, just create a plain node
								nodes[p] = new IBCNode(p);
							}
						}
					}
				});

				// Set parents to the actual nodes
				nodes[s.ID].mother = (m === null) ? m : nodes[m];
				nodes[s.ID].father = (f === null) ? f : nodes[f];

				// Try to use a cached CoI for performance
				let sg = world.findSlaveState(s.ID);
				if (!ignore_coeffs && sg !== null && hasInbreedingCoefficient(sg)) {
					nodes[s.ID]._coeff = sg.inbreedingCoeff;
				}
			}
		};

		slaves.forEach(s => create_node_rec(s));

		let detectCycle = function(node, visited, recStack) { // doesn't have to be fast, will only be called after the stack explodes
			visited[node.id] = true;
			recStack.push(node.id);
			if (node.father) {
				if (!visited[node.father.id] && detectCycle(node.father, visited, recStack)) {
					return true;
				} else if (recStack.includes(node.father.id)) {
					recStack.push(node.father.id);
					return true;
				}
			}
			if (node.mother) {
				if (!visited[node.mother.id] && detectCycle(node.mother, visited, recStack)) {
					return true;
				} else if (recStack.includes(node.mother.id)) {
					recStack.push(node.mother.id);
					return true;
				}
			}
			recStack.pop();
			return false;
		}

		let countSelfAndAncestors = function(node) {
			if (node === null)
				return 0 + 0;  // no self, no ancestors

			if (node._ancestorNodeCount === null)
				node._ancestorNodeCount = countSelfAndAncestors(node.mother) + countSelfAndAncestors(node.father);

			return 1 + node._ancestorNodeCount;  // 1 self, maybe some ancestors
		};
		for (let slave of slaves) {
			try {
				countSelfAndAncestors(nodes[slave.ID]);
			} catch (e) {
				// probably a cycle, try to go find it and throw a better error
				const visited = {};
				const recStack = []; // track back edges in node graph
				const IDToName = (id) => {
					try {
						return SlaveFullName(world.findSlaveState(id));
					} catch {
						return ``;
					}
				}
				if (detectCycle(nodes[slave.ID], visited, recStack)) {
					throw new Error(`Heritance cycle detected: ${toSentence(recStack.map(s => `${s} (${IDToName(s)})`), " was born to ", " was born to ")}, making them their own ancestor...`);
				} else {
					throw e; // not a cycle? no idea then
				}
			}
		}

		return nodes;
	};

	/** Determine the coefficients of inbreeding of an array of slaves. Returns a mapping of their
	 * ID to their coefficient of inbreeding
	 * @param {IBCRelative[]} slaves
	 * @param {boolean} [ignore_coeffs=false]
	 * @returns {Object.<number, number>}
	 */
	let coeff_slaves = (world, slaves, ignore_coeffs = false) => {
		/** @type {Object.<number, number>} */
		let ret = {};
		if (!ignore_coeffs) {
			// First, pull as many existing CoI off the slaves
			slaves.forEach(s => {
				let sg = world.findSlaveState(s.ID);
				if (sg !== null && hasInbreedingCoefficient(sg)) {
					ret[s.ID] = sg.inbreedingCoeff;
				}
			});
		}

		// Now do any we haven't done already
		slaves = slaves.filter(s => (!(s.ID in ret)));
		if (slaves.length > 0) {
			let nodes = nodes_slaves(world, slaves, ignore_coeffs);
			let coancestryCache = new CoancestryCache();

			// Compute coefficients
			slaves.forEach(s => {
				ret[s.ID] = coancestryCache.coefficientOfInbreeding(nodes[s.ID]);
			});
		}

		return ret;
	};

	/** Determine the kinship between slaves `a` and `b`
	 * @param {IBCRelative|0} [a] or zero
	 * @param {IBCRelative|0} [b] or zero
	 * @param {boolean} [ignore_coeffs=false]
	 * @returns {number}
	 */
	let kinship_slaves = (world, a, b, ignore_coeffs = false) => {
		if (!a || !b) {
			return 0;
		}

		return kinship_one_many(world, a, [b], ignore_coeffs)[b.ID];
	};

	/** Determine the coefficient of inbreeding of a single slave
	 * @param {IBCRelative} slave
	 * @param {boolean} [ignore_coeffs=false]
	 * @returns {number}
	 */
	let coeff_slave = (world, slave, ignore_coeffs = false) => {
		if (!ignore_coeffs && hasInbreedingCoefficient(slave)) {
			return slave.inbreedingCoeff;
		}

		let gp = world.findSlaveState(slave.ID);
		if (!ignore_coeffs && gp !== null && hasInbreedingCoefficient(gp)) {
			return gp.inbreedingCoeff;
		}

		return coeff_slaves(world, [slave], ignore_coeffs)[slave.ID];
	};

	/** Determine the kinship between one and many slaves. Returns an mapping from the ID of each of
	 * the slaves in `others` to its kinship with slave `a`
	 * @param {IBCRelative} a
	 * @param {IBCRelative[]} others
	 * @param {boolean} [ignore_coeffs=false]
	 * @returns {Object.<number, number>}
	 */
	let kinship_one_many = (world, a, others, ignore_coeffs = false) => {
		let nodes = nodes_slaves(world, others.concat([a]), ignore_coeffs);

		let ks = {0: 0};
		let coancestryCache = new CoancestryCache();
		others.forEach(s => {
			ks[s.ID] = coancestryCache.kinship(nodes[a.ID], nodes[s.ID]);
		});

		return ks;
	};

	/** Recalculate the inbreeding coefficient for all slaves dependent on the passed IDs (e.g. the
	 * slaves themselves and all of their children). This will replace the inbreeding coefficients
	 * wherever they exist with the computed values, ignoring all cached values.
	 * This should be called if parents are changed.
	 * @param {number[]} ids
	 */
	let recalculate_coeff_ids = (world, ids) => {
		// These are all the slave-like objects, i.e. they have ID, mother, and father. There will
		// be multiple elements with the same ID: we want this, since we have to replace all
		// occurrences of the COI for the affected slaves
		/** @type {IBCRelative[]} */
		let all_slave_like = world.getSlavesAndSuch();
		all_slave_like.push(world.getPlayerCharacter());
		// Add a fake entry for the PC's old master
		all_slave_like.push(oldMaster);

		// Gather the genetics of all current fetuses
		let all_fetuses = world.getFetuses();

		/** Recursively find all of the given ID's children, born and unborn
		 * @param {number} id
		 * @param {Set<number>} cur_slaves
		 * @param {Set<FC.FetusGenetics>} cur_fetuses
		 * @param {Set<number>} cur_fetus_parents
		 */
		let find_children_rec = (id, cur_slaves, cur_fetuses, cur_fetus_parents) => {
			// Add fetuses
			all_fetuses.filter(f => (f.father === id || f.mother === id)).forEach(f => {
				// We may have to manually add the parents later
				if (specificCharacterID(f.father)) {
					cur_fetus_parents.add(f.father);
				}
				if (specificCharacterID(f.mother)) {
					cur_fetus_parents.add(f.mother);
				}

				cur_fetuses.add(f);
			});

			// Recursively add slaves
			all_slave_like.filter(s => (s.father === id || s.mother === id)).forEach(s => {
				if (!cur_slaves.has(s.ID)) {
					cur_slaves.add(s.ID);
					find_children_rec(s.ID, cur_slaves, cur_fetuses, cur_fetus_parents);
				}
			});
		};

		// We only need slave IDs, since we have to update all of their entries (including GP)
		/** @type {Set<number>} */
		let needed_slave_ids = new Set();
		// Since each fetus has a unique record, a set still suffices
		/** @type {Set<FC.FetusGenetics>} */
		let needed_fetuses = new Set();
		/** @type {Set<number>} */
		let needed_parent_ids = new Set();

		// Find all the children of the IDs we need to do
		ids.forEach(id => {
			needed_slave_ids.add(id);
			find_children_rec(id, needed_slave_ids, needed_fetuses, needed_parent_ids);
		});

		// Now we assemble the tree from the slaves
		let needed_slaves = all_slave_like.filter(s => (needed_slave_ids.has(s.ID) || needed_parent_ids.has(s.ID)));
		let nodes = nodes_slaves(world, needed_slaves, true);

		// Now calculate the inbreeding coefficients (they're cached in the tree once calculated)
		let coancestryCache = new CoancestryCache();
		needed_slaves.filter(s => needed_slave_ids.has(s.ID)).forEach(s => {
			s.inbreedingCoeff = coancestryCache.coefficientOfInbreeding(nodes[s.ID]);
		});

		// Finally, handle all of the kinship for the fetuses
		needed_fetuses.forEach(f => {
			if (or_null(f.mother) === null || or_null(f.father) === null) {
				f.inbreedingCoeff = 0;
				return;
			}

			f.inbreedingCoeff = coancestryCache.kinship(nodes[f.mother], nodes[f.father]);
		});
	};

	/** Recalculate the coefficient of inbreeding for a single slave
	 * @see recalculate_coeff_ids
	 * @param {number} id
	 */
	let recalculate_coeff_id = (world, id) => {
		return recalculate_coeff_ids(world, [id]);
	};

	return {
		_test: {
			coeff_slave,
		},
		coeff: coeff_slave.bind(null, realWorld),
		coeff_slaves: coeff_slaves.bind(null, realWorld),
		kinship: kinship_slaves.bind(null, realWorld),
		kinship_one_many: kinship_one_many.bind(null, realWorld),
		recalculate_coeff_ids: recalculate_coeff_ids.bind(null, realWorld),
		recalculate_coeff_id: recalculate_coeff_id.bind(null, realWorld)
	};
})();


code:
globalThis.isItemAccessible = (function() {
	return {
		array: array,
		entry: entry,
	};

	/**
	 * Checks whether an item is accessible
	 * @param {string} string Name of wearable item
	 * @param {string} [category="clothes"] that item is in clothing, collar, etc
	 * @param {App.Entity.SlaveState} [slave]
	 * @returns {boolean|string} Returns true if item is accessible, and false if it is not. If the slave param is set, it may sometimes return a string instead of false, explaining why the item can't be used with that slave.
	 */
	function entry(string, category = "clothes", slave) {
		if (V.cheatMode === 1) {
			return true;
		}
		const selectedDB = App.Data.slaveWear[category] || App.Data[category];
		const item = selectedDB.get(string);
		if (!item) {
			console.log(`${string} is not a registered piece of clothing! Check App.Data.slaveWear.${category}`);
			return false; /* couldn't be found */
		}
		return isAvailable(item, category, slave);
	}

	/**
	 * Returns array of wearable clothing in format [name, value], basically player facing / game data.
	 * @param {Map} map Map to look in (such as App.Data.clothes)
	 * @param {(arg0: any) => any} [filter] The callback function to run.
	 * @returns {Array}
	 */
	function array(map, filter) {
		const array = [];
		for (const [key, obj] of map) {
			if (typeof filter === "function" && !filter(obj)) {
				continue;
			}
			if (V.cheatMode || isAvailable(obj)) {
				const name = (obj.fs && obj.fs.unlocks) ? `${obj.name} (FS)` : obj.name;
				array.push([name, key]);
			}
		}
		return array;
	}

	/**
	 * @param {object} item
	 * @param {string} [category="clothes"] that item is in clothing, collar, etc
	 * @param {App.Entity.SlaveState} [slave]
	 * @returns {boolean|string} Returns true if item is accessible, and false if it is not. If the slave param is set, it may sometimes return a string instead of false, explaining why the item can't be used with that slave.
	 */
	function isAvailable(item, category = "clothes", slave) {
		let slaveResults;
		if (slave) {
			slaveResults = isAvailableForSlave(item, category, slave);
			if (slaveResults === false) {
				return slaveResults;
			}
		}
		if (!(item.hasOwnProperty("fs") && item.fs.hasOwnProperty("unlocks")) && !(item.hasOwnProperty("requirements"))) {
			// No restriction, this clothing item is available to everyone
			return true;
		}
		if (item.hasOwnProperty("requirements")) {
			if (item.requirements === true) {
				return true;
			}
		}
		if (item.hasOwnProperty("fs") && item.fs.hasOwnProperty("unlocks")) {
			if (V.arcologies[0][item.fs.unlocks] > 0) {
				return true;
			}
		}
		if (slaveResults && slaveResults !== true) { // If we still haven't returned true or false, then we display why this particular slave can't use the item.
			return slaveResults;
		}
		return false;
	}

	/**
	 * @param {object} item
	 * @param {string} [category="clothes"] that item is in clothing, collar, etc
	 * @param {App.Entity.SlaveState} [slave]
	 * @returns {boolean|string} Returns true if item is accessible, and false if it is not. If the slave param is set, it may sometimes return a string instead of false, explaining why the item can't be used with that slave.
	 */
	function isAvailableForSlave(item, category, slave) {
		switch (category) {
			case "clothes":
				break;
			case "collar":
				break;
			case "bellyAccessory": {
				switch (item.value) {
					case "a support band": {
						if (slave.belly > 10000) {
							return true;
						} else {
							return `Slave belly too small for this`;
						}
					}
					case "a small empathy belly":
					case "a medium empathy belly":
					case "a large empathy belly":
					case "a huge empathy belly": {
						if (slave.belly < 1500 && slave.weight < 130) {
							return true;
						} else {
							return `Slave belly too big for this`;
						}
					}
					default:
						return true;
				}
			}
			case "buttplug": {
				switch (item.value) {
					case "huge plug": {
						if (slave.anus < 2) {
							return `Slave's anus is too small for this right now`;
						} else {
							return true;
						}
					}
					case "long plug":
					case "long, large plug": {
						if (!(slave.breedingMark !== 1 || V.propOutcome === 0 || V.eugenicsFullControl === 1 || !FutureSocieties.isActive('FSRestart'))) {
							return "Elites frown on this";
						} else {
							return true;
						}
					}
					case "long, huge plug": {
						if (slave.anus < 2) {
							return `Slave's anus is too small for this right now`;
						} else if (!(slave.breedingMark !== 1 || V.propOutcome === 0 || V.eugenicsFullControl === 1 || !FutureSocieties.isActive('FSRestart'))) {
							return "Elites frown on this";
						} else {
							return true;
						}
					}
					default:
						return true;
				}
			}
			case "buttplugAttachment":
				break;
			case "vaginalAccessory": {
				if (slave.vagina < 0) {
					return false;
				}
				switch (item.value) {
					case "huge dildo": {
						if (slave.vagina < 2) {
							return `Slave's vagina is too small for this right now`;
						} else {
							return true;
						}
					}
					case "long dildo":
					case "long, large dildo": {
						if (!(slave.breedingMark !== 1 || V.propOutcome === 0 || V.eugenicsFullControl === 1 || !FutureSocieties.isActive('FSRestart'))) {
							return "Elites frown on this";
						} else {
							return true;
						}
					}
					case "long, huge dildo": {
						if (slave.vagina < 2) {
							return `Slave's vagina is too small for this right now`;
						} else if (!(slave.breedingMark !== 1 || V.propOutcome === 0 || V.eugenicsFullControl === 1 || !FutureSocieties.isActive('FSRestart'))) {
							return "Elites frown on this";
						} else {
							return true;
						}
					}
					default:
						return true;
				}
			}
			case "vaginalAttachment": {
				if (slave.vagina < 0) {
					return false;
				}
				switch (item.value) {
					case "vibrator":
						if (slave.vaginalAccessory === "none") {
							return "No vaginal accessory to attach it to";
						} else if (dildoWidth(slave) === 0) {
							return "Vaginal accessory is too small";
						} else if (dildoVibeLevel(slave)) {
							return "Vaginal accessory already vibrates";
						} else {
							return true;
						}
				}
				break;
			}
			case "dickAccessory":
				return (slave.dick >= 1);
			case "shoes":
				break;
			case "chastity": {
				switch (item.value) {
					case "chastity belt":
					case "combined chastity belt":
						return (slave.vagina > -1);
					case "chastity cage":
					case "combined chastity cage":
						return (slave.dick > 0);
					case "genital chastity":
					case "full chastity":
						return (slave.vagina > -1 && slave.dick > 0);
					case "choose own chastity":
						return (slave.choosesOwnChastity !== 1 && slave.devotion > 20 && slave.trust > 0);
					case "revoke choosing own chastity":
						return (slave.choosesOwnChastity > 0);
					default:
						return true;
				}
			}
			default:
				break;
		}
		return true;
	}
})();

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} prosthetic
 * @returns {boolean}
 */
globalThis.isProstheticAvailable = function(slave, prosthetic) {
	return slave.readyProsthetics.findIndex(function(p) { return p.id === prosthetic; }) !== -1;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} prosthetic
 */
globalThis.addProsthetic = function(slave, prosthetic) {
	if (!isProstheticAvailable(slave, prosthetic)) {
		let limb = prostheticToLimb(prosthetic);
		if (limb > 0) {
			let p = {
				id: prosthetic,
				arm: {left: new App.Entity.ArmState(), right: new App.Entity.ArmState()},
				leg: {left: new App.Entity.LegState(), right: new App.Entity.LegState()}
			};
			p.arm.left.type = limb;
			p.arm.right.type = limb;
			p.leg.left.type = limb;
			p.leg.right.type = limb;
			slave.readyProsthetics.push(p);
		} else {
			slave.readyProsthetics.push({id: prosthetic});
		}
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} prosthetic
 * @returns {{}}
 */
globalThis.findProsthetic = function(slave, prosthetic) {
	return slave.readyProsthetics.find(p => p.id === prosthetic);
};

/**
 * @param {string} prosthetic
 * @returns {FC.LimbType}
 */
globalThis.prostheticToLimb = function(prosthetic) {
	switch (prosthetic) {
		case "basicL":
			return 2;
		case "sexL":
			return 3;
		case "beautyL":
			return 4;
		case "combatL":
			return 5;
		case "cyberneticL":
			return 6;
		case "felidaeL":
			return 7;
		case "canidaeL":
			return 8;
		case "felidaeCL":
			return 9;
		case "canidaeCL":
			return 10;
		default:
			return 0;
	}
};

/**
 *
 * @param {number} limb
 * @returns {string}
 */
globalThis.limbToProsthetic = function(limb) {
	switch (limb) {
		case 2:
			return "basicL";
		case 3:
			return "sexL";
		case 4:
			return "beautyL";
		case 5:
			return "combatL";
		case 6:
			return "cyberneticL";
		case 7:
			return "felidaeL";
		case 8:
			return "canidaeL";
		case 9:
			return "felidaeCL";
		case 10:
			return "canidaeCL";
		default:
			return "";
	}
};

/**
 *
 * @param {FC.FutureSociety} FS
 * @returns {Array}
 */
globalThis.clothingLovedByAnFS = function(FS) {
	const clothingOptions = [];
	for (const [key, value] of App.Data.clothes) {
		if (value.fs && value.fs.loves && value.fs.loves.has(FS)) {
			clothingOptions.push(key);
		}
	}
	return clothingOptions;
};


code:
/**
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLDivElement}
 */
App.MainView.useFucktoy = function(slave) {
	const {him, his} = getPronouns(slave);

	const div = document.createElement("div");
	div.classList.add("note");

	div.append(App.Interact.ToyChest(slave));

	div.append(" In the coming week you plan to concentrate on ");

	if (slave.toyHole !== "all her holes") {
		div.append(`${his} ${slave.toyHole}`);
	} else {
		div.append(`all of ${his} holes equally`);
	}

	if (slave.fuckdoll === 0) {
		div.append(", but for now:");

		const showScene = (passage, ...args) => $(linkDiv).empty().append(App.Interact[passage](slave, ...args));

		const linkArray = [];

		linkArray.push(
			App.UI.DOM.link(`Use ${his} mouth`, showScene, ["fLips"]),
			App.UI.DOM.link(`Play with ${his} tits`, showScene, ["fBoobs"])
		);

		if (canDoVaginal(slave)) {
			linkArray.push(App.UI.DOM.link(`Fuck ${him}`, showScene, ["fVagina"]));
			if (canDoAnal(slave)) {
				linkArray.push(App.UI.DOM.link(`Use ${his} holes`, showScene, ["fButt"]));
			}
		}
		if (canDoAnal(slave)) {
			linkArray.push(App.UI.DOM.link(`Fuck ${his} ass`, showScene, ["fAnus"]));
		}
		if (canDoVaginal(slave) || canDoAnal(slave)) {
			if (slave.belly >= 300000) {
				linkArray.push(App.UI.DOM.link(`Fuck ${him} over ${his} belly`, showScene, ["fBellyFuck"]));
			}
		}
		if (slave.dick > 0 && !slave.chastityPenis) {
			if (V.experimental.interactions) {
				linkArray.push(App.UI.DOM.link(`Suck ${him}`, showScene, ["fSuckDick"]));
			}
		}
		if (canPenetrate(slave)) {
			linkArray.push(App.UI.DOM.link(`Ride ${him}`, showScene, ["fDick"]));
		}
		linkArray.push(App.UI.DOM.link(`Abuse ${him}`, showScene, ["fAbuse"]));

		const linkDiv = App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(linkArray), "indent");
	} else {
		div.append(".");
	}

	return div;
};

/**
 * @returns {HTMLDivElement}
 */
App.MainView.useGuard = function() {
	const guard = S.Bodyguard;

	const outerDiv = document.createElement("div");

	if (!guard || guard.assignment !== Job.BODYGUARD) {
		return outerDiv;
	}

	App.UI.DOM.appendNewElement("span", outerDiv, App.Interact.guardPose(guard), "scene-intro");

	const showScene = (passage, ...args) => $(linkDiv).empty().append(App.Interact[passage](guard, ...args));

	const {him, his} = getPronouns(guard);
	const linkArray = [];

	linkArray.push(
		App.UI.DOM.link(`Use ${his} mouth`, showScene, ["fLips"]),
		App.UI.DOM.link(`Play with ${his} tits`, showScene, ["fBoobs"])
	);

	if (canDoVaginal(guard)) {
		linkArray.push(App.UI.DOM.link(`Fuck ${him}`, showScene, ["fVagina"]));
		if (canDoAnal(guard)) {
			linkArray.push(App.UI.DOM.link(`Use ${his} holes`, showScene, ["fButt"]));
		}
	}
	if (canDoAnal(guard)) {
		linkArray.push(App.UI.DOM.link(`Fuck ${his} ass`, showScene, ["fAnus"]));
	}
	if (canDoVaginal(guard) || canDoAnal(guard)) {
		if (guard.belly >= 300000) {
			linkArray.push(App.UI.DOM.link(`Fuck ${him} over ${his} belly`, showScene, ["fBellyFuck"]));
		}
	}
	if (guard.dick > 0 && !guard.chastityPenis) {
		if (V.experimental.interactions) {
			linkArray.push(App.UI.DOM.link(`Suck ${him}`, showScene, ["fSuckDick"]));
		}
	}
	if (canPenetrate(guard)) {
		linkArray.push(App.UI.DOM.link(`Ride ${him}`, showScene, ["fDick"]));
	}
	linkArray.push(App.UI.DOM.passageLink(`Abuse ${him}`, "Gameover", () => { V.gameover = "idiot ball"; }));
	const linkDiv = App.UI.DOM.appendNewElement("div", outerDiv, App.UI.DOM.generateLinksStrip(linkArray), "indent");

	return outerDiv;
};

App.MainView.full = function() {
	/**
	 * @returns {DocumentFragment}
	 */
	function errors() {
		const fragment = document.createDocumentFragment();

		/**
		 * @returns {HTMLParagraphElement}
		 */
		function newError() {
			const error = document.createElement("p");
			fragment.append(error);
			return error;
		}

		// check for correct version
		if (V.releaseID >= 1000 || ["0.9", "0.8", "0.7", "0.6"].includes(V.ver)) {
			if (V.releaseID < App.Version.release) {
				newError().append(App.UI.DOM.makeElement("span", "INCOMPATIBILITY WARNING:", "major-warning"),
					` Your saved game was created using version: ${V.ver}, build: ${V.releaseID}. You must run `,
					App.UI.DOM.passageLink("Backwards Compatibility", "Backwards Compatibility"));
			}
		} else {
			newError().append(App.UI.DOM.makeElement("span", "INCOMPATIBLE SAVE WARNING:", "major-warning"),
				` Your saved game was created using version: ${V.ver}, and you are using a later version which New Game Plus cannot reconcile. Please start a new game.`);
		}

		// check for correct rules
		if (V.defaultRules.length > 0 && (V.defaultRules[0].condition === undefined || V.defaultRules[0].set === undefined)) {
			const error = newError();
			error.append(App.UI.DOM.makeElement("span", "INCOMPATIBILITY WARNING:", "major-warning"),
				" The rules assistant format has changed. In the Options Menu, please ");
			const ra = document.createElement("strong");
			ra.append("Reset RA Rules");
			error.append(ra);
		}

		// check for NaN
		if (V.NaNArray.length > 0) {
			const error = newError();
			error.id = "NaNArray";
			error.append(App.UI.DOM.makeElement("span", "ERROR: The following variables are NaN! Please report this.", "error"));
			V.NaNArray.forEach(e => {
				const div = document.createElement("div");
				div.append(e);
				error.append(div);
			});
			error.append(App.UI.DOM.link("Hide NaN variables until next week",
				() => {
					error.outerHTML = "";
					V.NaNArray = [];
				})
			);
		}

		// check custom slaves
		if (App.Utils.IsCustomSlaveMutated(V.customSlave)) {
			newError().append(App.UI.DOM.makeElement("span", "ERROR: Your custom slave order has taken on a mutated life of its own and has been summarily shot. Refile your custom slave order, if necessary, and notify the appropriate authorities if you see this message again.", "error"));
			V.customSlave = new App.Entity.CustomSlaveOrder();
		}
		if (App.Utils.IsCustomSlaveMutated(V.huskSlave)) {
			newError().append(App.UI.DOM.makeElement("span", "ERROR: Your husk slave order has taken on a mutated life of its own and has been summarily shot. Refile your husk slave order, if necessary, and notify the appropriate authorities if you see this message again.", "error"));
			V.huskSlave = new App.Entity.CustomSlaveOrder();
		}

		return fragment;
	}

	/**
	 * All main passage logic that should be performed at the beginning of the passage but generate no visual output.
	 */
	function cleanup() {
		if (V.tabChoice.SlaveInteract !== "Description") {
			V.tabChoice.SlaveInteract = "Description";
		}

		penthouseCensus();
		V.costs = Math.trunc(calculateCosts.predict());
		if (V.defaultRules.length > 0) {
			V.currentRule = V.defaultRules[0].ID;
		} else {
			V.currentRule = null;
		}
		SlaveSort.slaves(V.slaves);

		App.UI.SlaveList.ScrollPosition.restore();
	}

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

		const links = [];

		App.UI.DOM.appendNewElement("h2", div, `Main Menu`, ['margin-top', 'uppercase', 'center']);

		if (V.rulesAssistantMain) {
			const RALink = App.UI.DOM.passageLink("Rules Assistant Options", "Rules Assistant");

			RALink.append(' ', App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Rules Assistant"), "hotkey"));
			links.push(RALink);

			if (V.rulesAssistantAuto !== 1) {
				links.push(App.UI.DOM.link(`Apply Rules Assistant at week end`, () => {
					V.rulesAssistantAuto = 1;

					App.UI.reload();
				}));
			} else {
				links.push(App.UI.DOM.link(`Stop applying Rules Assistant at week end`, () => {
					V.rulesAssistantAuto = 0;

					App.UI.reload();
				}));
			}

			links.push(App.UI.DOM.link(`Re-apply Rules Assistant now`, () => {
				for (const slave of V.slaves) {
					if (assignmentVisible(slave) && slave.useRulesAssistant === 1) {
						DefaultRules(slave);
					}
				}

				App.UI.reload();
			}, null, '', `This will only check slaves in the Penthouse.`));
		}

		App.UI.DOM.appendNewElement("div", div, App.UI.DOM.generateLinksStrip(links), ['center', 'margin-bottom']);

		return div;
	}

	/**
	 * @returns {DocumentFragment}
	 */
	function useFucktoys() {
		const fragment = document.createDocumentFragment();
		for (const slave of V.slaves) {
			if (slave.assignment !== Job.FUCKTOY) {
				continue;
			}
			fragment.append(App.MainView.useFucktoy(slave));
		}
		return fragment;
	}

	/**
	 * @returns {HTMLDivElement}
	 */
	function walkPast() {
		const outerDiv = document.createElement("div");
		outerDiv.id = "walkpast";

		const slave = V.slaves.filter(s => ![Job.BODYGUARD, Job.FUCKTOY].includes(s.assignment) && (!getPersonalAttention(s.ID, "torture") || onBedRest(V.PC, true))).random();
		if (slave) {
			App.UI.DOM.appendNewElement("span", outerDiv, globalThis.walkPast(slave), "scene-intro");
		}

		return outerDiv;
	}

	/**
	 * @returns {DocumentFragment}
	 */
	function assemble() {
		const fragment = new DocumentFragment();

		App.UI.DOM.appendNewElement("h1", fragment, V.arcologies[0].name, ['margin-bottom', 'center']);

		if (V.newModelUI === 1) {
			fragment.append(V.building.render());
		}

		if (V.seeArcology === 1) {
			fragment.append(App.Desc.playerArcology(
				App.UI.DOM.passageLink("Hide", "Main", () => { V.seeArcology = 0; })));
		}

		if (V.seeDesk === 1) {
			fragment.append(App.Desc.officeDescription(
				App.UI.DOM.passageLink("Hide", "Main", () => { V.seeDesk = 0; })));
		}

		if (V.seeFCNN === 1) {
			const div = document.createElement("div");
			div.classList.add("main-fcnn");
			div.append(App.FCNN.getText(), App.UI.DOM.passageLink("Hide", passage(), () => { V.seeFCNN = 0; })
			);
			fragment.append(div);
		}

		fragment.append(mainMenu());

		fragment.append(App.UI.SlaveList.penthousePage());

		if (V.fucktoyInteractionsPosition === 0) {
			fragment.append(useFucktoys());
		}
		if (V.useSlaveSummaryOverviewTab === 0) {
			fragment.append(App.MainView.useGuard());
		}
		fragment.append(walkPast());

		return fragment;
	}

	const fragment = document.createDocumentFragment();

	fragment.append(errors());

	// wrap everything in a try/catch statement, so App.MainView.errors() always gets shown. At the end print error out
	// as well.
	try {
		cleanup();

		fragment.append(assemble());
	} catch (ex) {
		fragment.append(App.UI.DOM.formatException(ex));
	}

	return fragment;
};


code:
/**
 * Creates a user-facing link or button allowing the user to purchase something.
 * @param {string} text The text to display.
 * @param {number} cost The amount of ¤ the purchase costs.
 * @param {keyof App.Data.Records.LastWeeksCash} what What the purchase is for.
 * @param {object} [args] Any additional arguments to pass.
 * @param {string[]} [args.notes] Any additional information to display. Must be lowercase and end in no punctuation.
 * @param {function():void} args.handler Any custom handler to run upon purchase.
 * @param {[boolean, string][]} [args.prereqs] Any prerequisites that must be met for the purchase to be available, with a note for when the prerequisites are not met.
 * @param {function():void} [args.refresh] Any function to run that updates the screen, if not the default `App.UI.reload()`.
 */
globalThis.makePurchase = function(text, cost, what, {notes, handler, prereqs, refresh}) {
	return App.UI.DOM.makeElement("div", V.purchaseStyle === 'button' ? renderButton() : renderLink());

	function execute() {
		cashX(forceNeg(cost), what);

		if (handler) {
			handler();
		}

		if (!refresh) {
			App.UI.reload();
		} else {
			refresh();
		}
	}

	function renderButton() {
		const span = App.UI.DOM.makeElement("span", null, ['note']);
		const price = cost !== 0 ? `${cashFormat(Math.trunc(cost))}` : `free`;
		const button = App.UI.DOM.makeElement("button", capFirstChar(price), ['purchase-button']);

		const disabledReasons = isDisabled();

		if (disabledReasons.length === 0) {
			button.onclick = execute;

			if (notes) {
				const span = document.createElement("span");
				const ul = document.createElement("ul");

				if (notes.length > 1) {
					ul.append(...notes.map(note => {
						const li = document.createElement("li");
						li.append(capFirstChar(note));

						return li;
					}));

					span.append(ul);
				} else {
					span.append(capFirstChar(notes[0]));
				}

				tippy(button, {
					content: span,
				});
			}
		} else {
			const span = document.createElement("span");
			const ul = document.createElement("ul");

			if (disabledReasons.length > 1) {
				for (const li of disabledReasons.map(reason => {
					const li = document.createElement("li");
					li.append(reason);
					return li;
				})) {
					ul.append(li);
				}

				span.append(ul);
			} else {
				span.append(disabledReasons[0]);
			}

			button.classList.add("disabled");

			tippy(button, {
				content: span,
			});
		}

		span.append(
			button,
			text,
		);

		return span;
	}

	function renderLink() {
		const span = App.UI.DOM.makeElement("span", null, ['indent']);
		const price = [`${cost !== 0 ? `Costs ${cashFormatColor(Math.trunc(cost), V.cash < cost)}` : `Free`}`];

		if (notes) {
			price.push(...notes);
		}

		const disabledReasons = isDisabled();

		if (disabledReasons.length === 0) {
			span.append(App.UI.DOM.link(text, execute), " ");
		} else {
			span.append(App.UI.DOM.disabledLink(text, disabledReasons), " ");
		}

		App.Events.addNode(span, [toSentence(price)], "span", ["note"]);

		return span;
	}

	function isDisabled() {
		const disabledReasons = [];
		if (V.cash < cost) {
			disabledReasons.push(`You cannot afford this purchase`);
		}
		if (prereqs) {
			prereqs.forEach(prereq => {
				if (prereq[0] !== true) {
					disabledReasons.push(prereq[1]);
				}
			});
		}
		return disabledReasons;
	}
};