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} genes
 */

/** 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) {
	let father;
	let mother;
	return ((mother = getSlave(granddaughter.mother)) && (mother.mother === grandmother.ID))
		|| ((father = getSlave(granddaughter.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) {
	let father;
	let mother;
	return ((mother = getSlave(granddaughter.mother)) && (mother.father === grandfather.ID))
		|| ((father = getSlave(granddaughter.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) {
	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;
	}
	let father;
	let mother;
	if ((mother = getSlave(niece.mother)) && (mother.ID !== aunt.ID) && !sameTParent(mother, aunt) && sameMom(mother, aunt) && sameDad(mother, aunt)) {
		return true;
	} else if ((father = getSlave(niece.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;
	}

	let slave1Mom;
	let slave1Dad;
	let slave2Mom;
	let slave2Dad;

	if ((slave1Mom = getSlave(slave1.mother)) && (slave2Mom = getSlave(slave2.mother)) && !sameTParent(slave1Mom, slave2Mom) && sameMom(slave1Mom, slave2Mom) && sameDad(slave1Mom, slave2Mom)) {
		return true;
	} else if ((slave1Mom = getSlave(slave1.mother)) && (slave2Dad = getSlave(slave2.father)) && !sameTParent(slave1Mom, slave2Dad) && sameMom(slave1Mom, slave2Dad) && sameDad(slave1Mom, slave2Dad)) {
		return true;
	} else if ((slave1Dad = getSlave(slave1.father)) && (slave2Mom = getSlave(slave2.mother)) && !sameTParent(slave1Dad, slave2Mom) && sameMom(slave1Dad, slave2Mom) && sameDad(slave1Dad, slave2Mom)) {
		return true;
	} else if ((slave1Dad = getSlave(slave1.father)) && (slave2Dad = getSlave(slave2.father)) && !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));
};

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

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

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

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

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

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

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

/**
 * @param {Relative} slave
 * @returns {object}
 */
globalThis.availableRelatives = function(slave) {
	let avail = {
		mother: false,
		motherName: null,
		father: false,
		fatherName: null,
		sisters: 0,
		daughters: 0,
		oneSisterRel: null,
		oneDaughterRel: null
	};

	V.slaves.forEach((other) => {
		if (slave.mother === other.ID) {
			avail.motherName = other.slaveName;
		}
		if (slave.father === other.ID) {
			avail.fatherName = other.slaveName;
		}
		if (isSlaveAvailable(other)) {
			if (slave.mother === other.ID) {
				avail.mother = true;
			}
			if (slave.father === other.ID) {
				avail.father = true;
			}
			if (slave.ID === other.mother || slave.ID === other.father) {
				avail.daughters++;
				if (avail.daughters === 1) {
					avail.oneDaughterRel = relativeTerm(slave, other);
				}
			}
			if (areSisters(slave, other) > 0) {
				avail.sisters++;
				if (avail.sisters === 1) {
					avail.oneSisterRel = relativeTerm(slave, other);
				}
			}
		}
	});

	return avail;
};

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 for slave2's relationship to slave1 (i.e. "daughter" if slave2 is slave1's daughter).
 * 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 useMaleTerms = (V.diversePronouns || slave2.ID === -1);
	if (slave2.mother === slave1.ID || slave2.father === slave1.ID) {
		if (slave2.genes === "XY" && useMaleTerms) {
			return "son";
		} else {
			return "daughter";
		}
	} else 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";
	} else if (areSisters(slave2, slave1) === 1) {
		if (slave2.genes === "XY" && useMaleTerms) {
			return "twin brother";
		} else {
			return "twin sister";
		}
	} else if (areSisters(slave2, slave1) === 2) {
		if (slave2.genes === "XY" && useMaleTerms) {
			return "brother";
		} else {
			return "sister";
		}
	} else if (areSisters(slave2, slave1) === 3) {
		if (slave2.genes === "XY" && useMaleTerms) {
			return "half-brother";
		} else {
			return "half-sister";
		}
	} else if (isAunt(slave1, slave2)) {
		if (slave2.genes === "XY" && useMaleTerms) {
			return "uncle";
		} else {
			return "aunt";
		}
	} else if (isAunt(slave2, slave1)) {
		if (slave2.genes === "XY" && useMaleTerms) {
			return "nephew";
		} else {
			return "niece";
		}
	} else if (areCousins(slave2, slave1)) {
		return "cousin";
	} else if (isGrandfatherP(slave1, slave2)) {
		return "grandfather";
	} else if (isGrandmotherP(slave1, slave2)) {
		return "grandmother";
	} else if (isGrandparentP(slave2, slave1)) {
		if (slave2.genes === "XY" && useMaleTerms) {
			return "grandson";
		} else {
			return "granddaughter";
		}
	}

	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--;
	}
};


code:
/* eslint-disable camelcase */
/* eslint-disable no-console */

/**
 * @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('link', d3.forceLink().id(function(d) {
				return d.index.toString();
			}))
			// eslint-disable-next-line no-unused-vars
			.force('collide', d3.forceCollide(function(d) {
				return 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('r', function(d) { return d.r; })
			.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')
			.links(data.links);

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

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

		function dragended(d) {
			if (!d3.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'
	};
	let outdads = {};
	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,
		/** @type {number} */
		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;
		}
	}

	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) {
				if (typeof node_lookup[mom] === 'undefined') {
					node_lookup[mom] = family_graph.nodes.length;
					let 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 node_lookup[mom] === 'undefined' && typeof preset_lookup[mom] !== 'undefined') {
			node_lookup[mom] = family_graph.nodes.length;
			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) {
				if (typeof node_lookup[dad] === 'undefined') {
					node_lookup[dad] = family_graph.nodes.length;
					let 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 node_lookup[dad] === 'undefined' && typeof preset_lookup[dad] !== 'undefined') {
			node_lookup[dad] = family_graph.nodes.length;
			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(', ');
		}
		node_lookup[key] = family_graph.nodes.length;
		// 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(', ');
		}
		node_lookup[key] = family_graph.nodes.length;
		// 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]) {
			if (related[character.mother]) {
				console.log('wtf, mom');
			}
			if (related[character.father]) {
				console.log('wtf, dad');
			}
			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;
};

repo_name: Elohiem/fc

license: mit


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

	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 ${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?"`);

	App.Events.addParagraph(node, [
		`"They're called something like:`,
		App.UI.DOM.makeTextBox(V.findName, v => V.findName = v),
		App.UI.DOM.link(
			"Locate",
			() => $(slaveList).empty().append(searchByName(V.findName))
		),
		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:`,
		App.UI.DOM.makeTextBox(V.findBackground, v => V.findBackground = v),
		App.UI.DOM.link(
			"Locate",
			() => $(slaveList).empty().append(searchByBackground(V.findBackground))
		),
		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:`,
		App.UI.DOM.makeTextBox(V.findData, v => V.findData = v),
		App.UI.DOM.link(
			"Locate",
			() => $(slaveList).empty().append(searchByExpression(V.findData))
		),
		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")
	]);

	const slaveList = App.UI.DOM.appendNewElement("span", node); // results list gets populated here by jQuery

	return node;
	/**
	 * 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) {
		if (ids.length === 0) {
			App.UI.DOM.appendNewElement("p", frag, "No matching slaves.");
		} else {
			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:
/**
 * 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 {App.Entity.SlaveState} slave
 * @returns {number}
 */
globalThis.healthPenalty = function(slave) {
	const H = slave.health;
	let penalty = 100;
	if (slave.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
		const tirednessPenaltyApplies = slave.fuckdoll === 0 && (slave.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
 * @returns {void}
 */
globalThis.healthDamage = function(slave, damage) {
	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;
	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} 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
	H.health = H.condition - H.shortDamage - H.longDamage;
};


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 */
/* eslint-disable eqeqeq */
globalThis.ibc = (() => {
	// These IDs are considered to be unknown parents
	let or_null = (s) => specificCharacterID(s) ? s : null;

	// Find some sort of state for a slave. Checks first the gene pool, then V.slaves, then the
	// missing table
	let find_gp = (id) => (slaveStateById(id) || V.genePool.find((s) => s.ID === id) || ((id in V.missingTable) ? V.missingTable[id] : null) || 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.nodecodes = []; // NodeCodes
		this.nodecodes_str = []; // String version of the NodeCodes, for comparison
		this._coeff = null; // Cached CoI
		this.child_count = 0; // Number of children of the node, for computing NodeCodes
	}

	// Determine the length of the shared prefix between the two NodeCode parameters
	let prefix_len = (nca, ncb) => {
		let i = 0;
		for (i = 0; i < Math.min(nca.length, ncb.length); i++) {
			if (nca[i] !== ncb[i]) {
				break;
			}
		}
		return i;
	};

	// Determine the set of longest common prefixes for a node pair
	let prefixes = (a, b) => {
		let found = [];
		let found_s = [];
		a.nodecodes.forEach(nca => {
			let match = false;
			b.nodecodes.forEach(ncb => {
				let l = prefix_len(nca, ncb);
				if (l === 0 && match) {
					return;
				}

				if (l > 0) {
					match = true;
					let pfx = nca.slice(0, l);
					let pfx_s = pfx.join(';');

					if (!found_s.includes(pfx_s)) {
						found_s.push(pfx_s);
						found.push(pfx);
					}
				}
			});
		});

		return found;
	};

	// Search up the tree to find a given NodeCode, starting at `n`
	let find_nc = (nc, n) => {
		if (n.nodecodes_str.includes(nc.join(';'))) {
			return n;
		}

		let ret = null;
		if (n.mother !== null) {
			ret = find_nc(nc, n.mother);
		}
		if (n.father !== null && ret === null) {
			ret = find_nc(nc, n.father);
		}

		return ret;
	};

	// Determine the set of common ancestors between a node pair
	let common = (a, b) => {
		let pfx = prefixes(a, b);
		let pfx_s = pfx.map(s => s.join(';'));
		let anc = [];

		while (pfx.length > 0) {
			let p = pfx.shift();
			pfx_s.shift();
			let ret = find_nc(p, a);

			ret.nodecodes.forEach(nc => {
				let i = pfx_s.indexOf(nc.join(';'));
				if (i === -1) {
					return;
				}

				pfx.splice(i, 1);
				pfx_s.splice(i, 1);
			});

			if (anc.findIndex(s => (s[0] == ret)) === -1) {
				anc.push([ret, p]);
			}
		}

		return anc;
	};

	// Determine the set of NodeCodes on `n` with prefix `x`
	let mps = (n, x) => {
		let x_s = x.join(';');
		return n.nodecodes.filter(nc => (nc.slice(0, x.length).join(';') === x_s));
	};

	// Compute the set of all paths to the parents of `n` with prefix `x`
	let pp = (mother, father, x) => {
		let m = mps(mother, x);
		let f = mps(father, x);

		let prod = [];
		m.forEach(i => {
			f.forEach(j => {
				prod.push([i, j]);
			});
		});

		return prod;
	};

	let kinship = (mother, father) => {
		let coeffNum = 0;
		if (!mother || !father) {
			coeffNum = 0;
		} else if (mother === father) {
			coeffNum = 0.5 * (1 + coeff(mother));
		} else {
			let cf = 0;
			let cmn = common(mother, father);

			cmn.forEach(el => {
				let c = el[0];
				let p = el[1];
				let p_s = p.join(';');

				let paths = pp(mother, father, p);
				let paths_s = paths.map(p => [p[0].join(';'), p[1].join(';')].join(','));

				cmn.forEach(el2 => {
					let co = el2[0];
					if (co == c) {
						return;
					}
					let m_pp = [];
					let f_pp = [];

					co.nodecodes.forEach(nc => {
						if (nc.slice(0, p.length).join(';') != p_s) {
							return;
						}

						m_pp = m_pp.concat(mps(mother, nc));
						f_pp = f_pp.concat(mps(father, nc));
					});

					m_pp.forEach(mp => {
						f_pp.forEach(fp => {
							let mf_s = [mp.join(';'), fp.join(';')].join(',');
							let i = paths_s.indexOf(mf_s);
							if (i === -1) {
								return;
							}
							paths_s.splice(i, 1);
							paths.splice(i, 1);
						});
					});
				});
				paths.forEach(p => {
					let pfx = prefix_len(p[0], p[1]);

					cf += 0.5 ** (p[0].length + p[1].length + 1 - 2 * pfx) * (1 + coeff(c));
				});
			});

			coeffNum = cf;
		}

		return coeffNum;
	};

	// Determine the coefficient of inbreeding of a node `n`
	let coeff = n => {
		if (n._coeff === null) {
			n._coeff = kinship(n.mother, n.father);
		}
		return n._coeff;
	};

	// Populate the NodeCodes.
	//
	// Each node has a set of NodeCodes, which represent the set of paths from it to its ancestors.
	// NodeCodes here are represented by arrays of integers.
	//
	// NodeCodes are constructed recursively in this fashion:
	//
	// - Assign each of the founders (nodes with both parents === null) an unique ID, starting from
	//   0 and incrementing each time (the order doesn't matter); a founder's set of NodeCodes has
	//   exactly one NodeCode, which is [ID] (an array containing only their ID)
	//
	// - For each other node, let M be its child number w.r.t. its mother and N its child number
	//   w.r.t. its father, i.e. the number of children that the respective parent has had before
	//   this one (the order is not important to the algorithm, it's arbitrary here for
	//   convenience). Its set of NodeCodes is the set of all its mother's NodeCodes with M appended
	//   and all of its father's NodeCodes with N appended. For example, if its mother has the
	//   NodeCodes [[2]] and M = 3 and its father has the NodeCodes [[0,1], [3,1]] and N = 1 then
	//   the set of NodeCodes for this node would be
	//
	//       [[2, 3], [0, 1, 1], [3, 1, 1]]
	//
	// We do this iteratively here, looping over the set of all nodes until each has been assigned
	// a NodeCode. This requires looping through a number of times equal to the number of
	// generations, since as soon as both parents have NodeCodes their children's NodeCodes may be
	// computed.
	let make_nc = nodes => {
		// Generate founder NodeCodes
		let total = Object.keys(nodes).length;
		let seen = [];
		let curid = 0;
		Object.values(nodes).forEach(n => {
			if (n.mother !== null || n.father !== null) {
				return;
			}
			n.nodecodes.push([curid]);
			curid += 1;
			seen.push(n.id);
		});

		// Generate the rest of the NodeCodes
		let oldSeenLength = -1;
		while (seen.length !== total) {
			oldSeenLength = seen.length;
			Object.keys(nodes).forEach(s => {
				let n = nodes[s];
				if (seen.includes(+s)) { // We've already done this
					return;
				} else if ((n.mother !== null && n.mother.nodecodes.length === 0) || (n.father !== null && n.father.nodecodes.length === 0)) { // Too soon, we haven't done its parents
					return;
				}

				seen.push(n.id);
				// Compute the NodeCodes from its parents
				[n.mother, n.father].forEach((a, i) => {
					if (a === null || (n.mother === n.father && i === 1)) { // Ignore missing parents/repeated
						return;
					}

					a.nodecodes.forEach(nc => {
						// Copy the NodeCode, push the child number, then add it
						let nnc = nc.slice();
						nnc.push(a.child_count);
						n.nodecodes.push(nnc);
					});
					a.child_count += 1;
				});

				// NodeCodes must be sorted; this suffices
				n.nodecodes.sort();
			});
			// check to make sure we're progressing...if not, there's a cycle in the graph
			// dump all the nodes participating in or descended from the cycle and let the player figure it out
			if (oldSeenLength === seen.length) {
				const badSlaveIDs = Object.keys(nodes).filter(s => !seen.includes(+s)).map(k => nodes[k].id);
				throw (`Inbreeding calculation: heritance cycle detected. Check slave IDs: ${badSlaveIDs}`);
			}
		}

		// Cache the string NodeCodes
		Object.values(nodes).forEach(n => {
			n.nodecodes_str = n.nodecodes.map(nc => nc.join(';'));
		});
	};

	/** Make nodes for an array of slaves
	 * @param {IBCRelative[]} slaves
	 * @param {boolean} [ignore_coeffs=false]
	 * @returns {Object.<number, IBCNode>|{}}
	 */
	let nodes_slaves = (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(V.PC);
						} else {
							// Search for a slave state, genePool entry, or missingTable entry
							let gp = find_gp(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 = find_gp(s.ID);
				if (!ignore_coeffs && sg !== null && "inbreedingCoeff" in sg && sg.inbreedingCoeff !== -1) {
					nodes[s.ID]._coeff = sg.inbreedingCoeff;
				}
			}
		};

		// Populate the nodes
		slaves.forEach(s => create_node_rec(s));

		// Populate NodeCodes
		make_nc(nodes);

		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 = (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 = find_gp(s.ID);
				if (sg !== null && "inbreedingCoeff" in sg && sg.inbreedingCoeff !== -1) {
					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(slaves, ignore_coeffs);

			// Compute coefficients
			slaves.forEach(s => {
				ret[s.ID] = coeff(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 = (a, b, ignore_coeffs = false) => {
		if (!a || !b) {
			return 0;
		}

		return kinship_one_many(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 = (slave, ignore_coeffs = false) => {
		if (!ignore_coeffs && "inbreedingCoeff" in slave && slave.inbreedingCoeff !== -1) {
			return slave.inbreedingCoeff;
		}

		let gp = find_gp(slave.ID);
		if (!ignore_coeffs && gp !== null && "inbreedingCoeff" in gp && gp.inbreedingCoeff !== -1) {
			return gp.inbreedingCoeff;
		}

		return coeff_slaves([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 = (a, others, ignore_coeffs = false) => {
		let nodes = nodes_slaves(others.concat([a]), ignore_coeffs);

		let ks = {0: 0};
		others.forEach(s => {
			ks[s.ID] = 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 = (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 = 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);
		}
		all_slave_like.push(V.PC);
		// Add a fake entry for the PC's old master
		all_slave_like.push(oldMaster);

		// Gather the genetics of all current fetuses
		/** @type {FC.FetusGenetics[]} */
		let all_fetuses = V.slaves.filter(s => s.preg > 0).map(s => s.womb.map(i => i.genetics)).reduce((res, cur) => res.concat(cur), []);

		/** 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(needed_slaves, true);

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

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

			// Use a string of the form "parent;parent" to store the cache value; since kinship is
			// commutative, the minimum parent ID will be first
			let kinship_str = Math.min(f.mother, f.father) + ';' + Math.max(f.mother, f.father);
			if (!kinship_cache.has(kinship_str)) {
				kinship_cache.set(kinship_str, kinship(nodes[f.mother], nodes[f.father]));
			}

			f.inbreedingCoeff = kinship_cache.get(kinship_str);
		});
	};

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

	return {
		coeff: coeff_slave,
		coeff_slaves,
		kinship: kinship_slaves,
		kinship_one_many,
		recalculate_coeff_ids,
		recalculate_coeff_id
	};
})();


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 || V.arcologies[0].FSRestart === "unset")) {
							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 || V.arcologies[0].FSRestart === "unset")) {
							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 || V.arcologies[0].FSRestart === "unset")) {
							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 || V.arcologies[0].FSRestart === "unset")) {
							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.LimbState(), right: new App.Entity.LimbState()},
				leg: {left: new App.Entity.LimbState(), right: new App.Entity.LimbState()}
			};
			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 {number}
 */
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;
		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";
		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 (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 (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());
		V.currentRule = V.defaultRules[0];
		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.`));

			if (DefaultRulesError()) {
				App.UI.DOM.appendNewElement("div", div, `One or more rules' custom conditions has errors!`,
					['center', 'warning', 'notification']);
			}
		}

		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)).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;
};