diff --git a/src/gui/userButton.js b/src/gui/sideBar.js
similarity index 66%
rename from src/gui/userButton.js
rename to src/gui/sideBar.js
index 0c8cf11d416de2f83b950d4cc7650cd3bb5bcfad..9c1e8e3508a06265f8f80745c8a64a4805fae3cf 100644
--- a/src/gui/userButton.js
+++ b/src/gui/sideBar.js
@@ -1,3 +1,26 @@
+/** Notify the game that the sidebar needs to be refreshed as soon as possible, but do not do it immediately.
+ *  This allows us to call this function repeatedly without impacting performance (for example, from repX() and cashX()).
+ *  The game will redraw the sidebar exactly once, as soon as all the scripts have finished executing.
+ */
+App.Utils.scheduleSidebarRefresh = (function() {
+	let refresh = false;
+
+	function updateSidebar() {
+		refresh = false;
+		UIBar.update();
+		App.UI.updateSidebarTooltips();
+	}
+
+	function schedule() {
+		if (!refresh) {
+			refresh = true;
+			setTimeout(updateSidebar, 0);
+		}
+	}
+
+	return schedule;
+})();
+
 App.Utils.userButton = function(nextButton = V.nextButton, nextLink = V.nextLink) {
 	const el = document.createElement("span");
 	el.id = "next-button-wrapper"; // We must always have a named element so we have something to refresh even if the button is hidden for a scene
diff --git a/src/interaction/prostheticLab.js b/src/interaction/prostheticLab.js
new file mode 100644
index 0000000000000000000000000000000000000000..5a34abceb4f036a6730c7dd06fbdf59bcf0ace65
--- /dev/null
+++ b/src/interaction/prostheticLab.js
@@ -0,0 +1,26 @@
+/**
+ * @returns {string}
+ */
+
+globalThis.getProstheticsStockpile = function() {
+	return `<div>Prosthetics interfaces: ${num(V.prosthetics.interfaceP1.amount + V.prosthetics.interfaceP2.amount)}</div>` +
+		`<div class="choices">Basic: ${V.prosthetics.interfaceP1.amount}</div>` +
+		`<div class="choices">Advanced: ${V.prosthetics.interfaceP2.amount}</div>` +
+		`<div>Limbs: ${num(V.prosthetics.basicL.amount + V.prosthetics.sexL.amount + V.prosthetics.beautyL.amount +
+			V.prosthetics.combatL.amount + V.prosthetics.cyberneticL.amount)}</div>` +
+		`<div class="choices">Basic: ${V.prosthetics.basicL.amount}</div>` +
+		`<div class="choices">Sex: ${V.prosthetics.sexL.amount}</div>` +
+		`<div class="choices">Beauty: ${V.prosthetics.beautyL.amount}</div>` +
+		`<div class="choices">Combat: ${V.prosthetics.combatL.amount}</div>` +
+		`<div class="choices">Cybernetic: ${V.prosthetics.cyberneticL.amount}</div>` +
+		`<div>Implants: ${num(V.prosthetics.ocular.amount + V.prosthetics.cochlear.amount + V.prosthetics.electrolarynx.amount)}</div>` +
+		`<div class="choices">Ocular: ${V.prosthetics.ocular.amount}</div>` +
+		`<div class="choices">Cochlear: ${V.prosthetics.cochlear.amount}</div>` +
+		/* `<div class="choices">Erectile: ${V.prosthetics.erectile.amount}</div>` + */
+		`<div class="choices">Electrolarynx: ${V.prosthetics.electrolarynx.amount}</div>` +
+		`<div>Tail interface: ${V.prosthetics.interfaceTail.amount}</div>` +
+		`<div>Tails: ${num(V.prosthetics.modT.amount + V.prosthetics.sexT.amount + V.prosthetics.combatT.amount)}</div>` +
+		`<div class="choices">Modular: ${V.prosthetics.modT.amount}</div>` +
+		`<div class="choices">Pleasure: ${V.prosthetics.sexT.amount}</div>` +
+		`<div class="choices">Combat: ${V.prosthetics.combatT.amount}</div>`;
+};
diff --git a/src/js/rulesAssistant.js b/src/js/rulesAssistant.js
index b25965885ae9744963c8a840e280933f6bd85ffa..52199fd9c050ef135adb64295328edc9dd7e1c47 100644
--- a/src/js/rulesAssistant.js
+++ b/src/js/rulesAssistant.js
@@ -602,3 +602,15 @@ App.RA.ruleDeepAssign = function deepAssign(target, source) {
 	}
 	return target;
 };
+
+globalThis.initRules = function() {
+	const rule = emptyDefaultRule();
+	rule.name = "Obedient Slaves";
+	rule.condition.function = "between";
+	rule.condition.data.attribute = "devotion";
+	rule.condition.data.value = [20, null];
+	rule.set.removalAssignment = "rest";
+
+	V.defaultRules = [rule];
+	V.rulesToApplyOnce = {};
+};
diff --git a/src/js/utilsArcology.js b/src/js/utilsArcology.js
new file mode 100644
index 0000000000000000000000000000000000000000..4dd1a73849d74394f265fc3c2fe54401ba66624a
--- /dev/null
+++ b/src/js/utilsArcology.js
@@ -0,0 +1,104 @@
+/** Returns the revivalist nationality associated with the player's arcology, or 0 if none
+ * @returns {string|0}
+ */
+globalThis.getRevivalistNationality = function() {
+	if (V.arcologies[0].FSRomanRevivalist > 90) {
+		return "Roman Revivalist";
+	} else if (V.arcologies[0].FSAztecRevivalist > 90) {
+		return "Aztec Revivalist";
+	} else if (V.arcologies[0].FSEgyptianRevivalist > 90) {
+		return "Ancient Egyptian Revivalist";
+	} else if (V.arcologies[0].FSEdoRevivalist > 90) {
+		return "Edo Revivalist";
+	} else if (V.arcologies[0].FSArabianRevivalist > 90) {
+		return "Arabian Revivalist";
+	} else if (V.arcologies[0].FSChineseRevivalist > 90) {
+		return "Ancient Chinese Revivalist";
+	}
+	return 0;
+};
+
+/** Calculate and return economic uncertainty multiplier for a given arcology
+ * @param {number} arcologyID
+ * @returns {number}
+ */
+App.Utils.economicUncertainty = function(arcologyID) {
+	let uncertainty = arcologyID === 0 ? 5 : 10;
+	if (assistant.power === 1) {
+		uncertainty -= Math.max(Math.trunc(uncertainty/2), 0);
+	} else if (assistant.power > 1) {
+		uncertainty = 0;
+	}
+	return jsRandom(100 - uncertainty, 100 + uncertainty) / 100;
+};
+
+/**
+ * @returns {number}
+ */
+App.Utils.schoolCounter = function() {
+	return Array.from(App.Data.misc.schools.keys()).filter(s => V[s].schoolPresent).length;
+};
+
+/**
+ * @returns {string}
+ */
+App.Utils.schoolFailure = function() {
+	return Array.from(App.Data.misc.schools.keys()).find(s => V[s].schoolPresent && V[s].schoolProsperity <= -10);
+};
+
+/**
+ * @typedef {Object} menialObject
+ * @property {string} text
+ * @property {number} value
+ */
+
+/**
+ * @returns {menialObject}
+ */
+globalThis.menialPopCap = function() {
+	let r = "";
+
+	let popCap = 500 * (1 + V.building.findCells(cell => cell instanceof App.Arcology.Cell.Manufacturing && cell.type === "Pens").length);
+
+	let overMenialCap = V.menials + V.fuckdolls + V.menialBioreactors - popCap;
+	if (overMenialCap > 0) {
+		const price = menialSlaveCost(-overMenialCap);
+		if (V.menials > 0) {
+			if (V.menials > overMenialCap) {
+				cashX((overMenialCap * price), "menialTrades");
+				V.menialDemandFactor -= overMenialCap;
+				V.menials -= overMenialCap;
+				overMenialCap = 0;
+				r += "You don't have enough room for all your menials and are obliged to sell some.";
+			} else {
+				cashX((V.menials * price), "menialTrades");
+				V.menialDemandFactor -= V.menials;
+				overMenialCap -= V.menials;
+				V.menials = 0;
+				r += "You don't have enough room for your menials and are obliged to sell them.";
+			}
+		}
+		if (overMenialCap > 0 && V.fuckdolls > 0) {
+			if (V.fuckdolls > overMenialCap) {
+				cashX(overMenialCap * (price * 2), "menialTrades");
+				V.menialDemandFactor -= overMenialCap;
+				V.fuckdolls -= overMenialCap;
+				overMenialCap = 0;
+				r += "You don't have enough room for all your Fuckdolls and are obliged to sell some.";
+			} else {
+				cashX(V.fuckdolls * (price * 2), "menialTrades");
+				V.menialDemandFactor -= V.fuckdolls;
+				overMenialCap -= V.fuckdolls;
+				V.fuckdolls = 0;
+				r += "You don't have enough room for your Fuckdolls and are obliged to sell them.";
+			}
+		}
+		if (overMenialCap > 0 && V.menialBioreactors > 0) {
+			cashX(overMenialCap * (price - 100), "menialTrades");
+			V.menialDemandFactor -= overMenialCap;
+			V.menialBioreactors -= overMenialCap;
+			r += "You don't have enough room for all your menial bioreactors and are obliged to sell some.";
+		}
+	}
+	return {text: r, value: popCap};
+};
diff --git a/src/js/utilsAssessSlave.js b/src/js/utilsAssessSlave.js
new file mode 100644
index 0000000000000000000000000000000000000000..f42e0ad0677b34ed9d951769daf66905feea0e47
--- /dev/null
+++ b/src/js/utilsAssessSlave.js
@@ -0,0 +1,243 @@
+/*
+*
+* This file focuses on slave related functions that assess qualities about slaves.  Are they/can they X?
+*
+*/
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @returns {string}
+ */
+globalThis.getSlaveDevotionClass = function(slave) {
+	if ((!slave) || (!State)) {
+		return undefined;
+	}
+	if (slave.fetish === "mindbroken") {
+		return "mindbroken";
+	}
+	if (slave.devotion < -95) {
+		return "very-hateful";
+	} else if (slave.devotion < -50) {
+		return "hateful";
+	} else if (slave.devotion < -20) {
+		return "resistant";
+	} else if (slave.devotion <= 20) {
+		return "ambivalent";
+	} else if (slave.devotion <= 50) {
+		return "accepting";
+	} else if (slave.devotion <= 95) {
+		return "devoted";
+	} else {
+		return "worshipful";
+	}
+};
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @returns {string}
+ */
+globalThis.getSlaveTrustClass = function(slave) {
+	if ((!slave) || (!State)) {
+		return undefined;
+	}
+
+	if (slave.fetish === "mindbroken") {
+		return "";
+	}
+
+	if (slave.trust < -95) {
+		return "extremely-terrified";
+	} else if (slave.trust < -50) {
+		return "terrified";
+	} else if (slave.trust < -20) {
+		return "frightened";
+	} else if (slave.trust <= 20) {
+		return "fearful";
+	} else if (slave.trust <= 50) {
+		if (slave.devotion < -20) {
+			return "hate-careful";
+		} else {
+			return "careful";
+		}
+	} else if (slave.trust <= 95) {
+		if (slave.devotion < -20) {
+			return "bold";
+		} else {
+			return "trusting";
+		}
+	} else if (slave.devotion < -20) {
+		return "defiant";
+	} else {
+		return "profoundly-trusting";
+	}
+};
+
+/**
+ * Returns a "disobedience factor" between 0 (perfectly obedient) and 100 (completely defiant)
+ * @param {App.Entity.SlaveState} slave
+ * @returns {number}
+ */
+globalThis.disobedience = function(slave) {
+	const devotionBaseline = 20; // with devotion above this number slaves will obey completely
+	const trustBaseline = -20; // with trust below this number slaves will obey completely
+
+	if (slave.devotion > devotionBaseline || slave.trust < trustBaseline) {
+		return 0; // no chance of disobedience
+	}
+
+	// factors are between 0 (right on the boundary of perfectly obedient) and 10 (completely disobedient)
+	let devotionFactor = 10 - ((10 * (slave.devotion + 100)) / (devotionBaseline + 100));
+	let trustFactor = (10 * (slave.trust - trustBaseline)) / (100 - trustBaseline);
+	return Math.round(devotionFactor * trustFactor);
+};
+
+/**
+ * Returns how exposing a slave's outfit is, after taking into consideration a topless outfit is more revealing for beboobed slaves or female ones.
+ * @param {App.Entity.SlaveState} slave
+ * @returns {0|1|2|3|4}
+ */
+globalThis.getExposure = function(slave) {
+	const clothes = App.Data.clothes.get(slave.clothes);
+	return (clothes.topless && clothes.exposure < 3 && (slave.boobs > 299 || (slave.genes === 'XX' && slave.vagina >= 0))) ? 3 : clothes.exposure;
+};
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @returns {boolean}
+ */
+globalThis.canImproveIntelligence = function(slave) {
+	let origIntel = V.genePool.find(function(s) { return s.ID === slave.ID; }).intelligence;
+	return (slave.intelligence < origIntel + 15) && (slave.intelligence < 100);
+};
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @returns {number}
+ */
+globalThis.maxHeight = function(slave) {
+	let max = Math.trunc(Math.clamp((Height.mean(slave) * 1.25), 0, 274)); /* max achievable height is expected height plus 25% */
+
+	if (slave.geneticQuirks.dwarfism === 2 && slave.geneticQuirks.gigantism !== 2) {
+		max = Math.min(max, 160);
+	}
+
+	return max;
+};
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @returns {boolean}
+ */
+globalThis.canImproveHeight = function(slave) {
+	return slave.height < maxHeight(slave);
+};
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} target
+ * @returns {boolean}
+ */
+globalThis.haveRelationshipP = function(slave, target) {
+	return slave.relationshipTarget === target.ID;
+};
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @param {App.Entity.SlaveState} target
+ * @returns {boolean}
+ */
+globalThis.isRivalP = function(slave, target) {
+	return slave.rivalryTarget === target.ID;
+};
+
+/**
+ * @param {FC.HumanState} slave
+ * @returns {boolean}
+ */
+globalThis.supremeRaceP = function(slave) {
+	return V.arcologies[0].FSSupremacistRace === slave.race;
+};
+
+/**
+ * @param {FC.HumanState} slave
+ * @returns {boolean}
+ */
+globalThis.inferiorRaceP = function(slave) {
+	return V.arcologies[0].FSSubjugationistRace === slave.race;
+};
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @returns {boolean}
+ */
+globalThis.isLeaderP = function(slave) {
+	const leaders = [S.HeadGirl, S.Bodyguard, S.Recruiter, S.Concubine, S.Nurse, S.Attendant, S.Matron, S.Madam, S.DJ, S.Milkmaid, S.Farmer, S.Stewardess, S.Schoolteacher, S.Wardeness];
+
+	return leaders.some(leader => leader && leader.ID === slave.ID);
+};
+
+/** Get the written title for a given slave, without messing with global state.
+ * @param {App.Entity.SlaveState} [slave]
+ * @returns {string}
+ */
+globalThis.getWrittenTitle = function(slave) {
+	if (slave && slave.custom.title !== undefined && slave.custom.title !== "" && slave.rudeTitle === 0) {
+		return slave.custom.title;
+	}
+	if (V.PC.customTitle !== undefined) {
+		return V.PC.customTitle;
+	} else if (V.PC.title !== 0) {
+		return "Master";
+	} else {
+		return "Mistress";
+	}
+};
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @returns {string}
+ */
+globalThis.SlaveFullName = function(slave) {
+	const pair = slave.slaveSurname ? [slave.slaveName, slave.slaveSurname] : [slave.slaveName];
+	if ((V.surnameOrder !== 1 && ["Cambodian", "Chinese", "Hungarian", "Japanese", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(slave.nationality)) || (V.surnameOrder === 2)) {
+		pair.reverse();
+	}
+	return pair.join(" ");
+};
+
+/** Is the slave a shelter slave?
+ * @param {App.Entity.SlaveState} slave
+ * @returns {boolean}
+ */
+globalThis.isShelterSlave = function(slave) {
+	return (typeof slave.origin === "string" && slave.origin.includes("Slave Shelter"));
+};
+
+/**
+ * Returns if a slave appears male, female, or androgynous.
+ *
+ * @param {App.Entity.SlaveState} slave
+ * @returns {number}
+ */
+globalThis.perceivedGender = function(slave) {
+	return -1;
+};
+
+/**
+ * @param {App.Entity.SlaveState} A
+ * @param {App.Entity.SlaveState} B
+ * @returns {boolean}
+ */
+globalThis.sameAssignmentP = function(A, B) {
+	return A.assignment === B.assignment;
+};
+
+/** Determine whether a given penthouse slave can move into a private room or not.
+ * @param {App.Entity.SlaveState} slave
+ * @returns {boolean}
+ */
+globalThis.canMoveToRoom = function(slave) {
+	const partner = slave.relationship >= 4 ? getSlave(slave.relationshipTarget) : null;
+	const partnerHasRoom = partner && assignmentVisible(partner) && partner.rules.living === "luxurious";
+	return partnerHasRoom || V.rooms - V.roomsPopulation >= 1;
+};
diff --git a/src/js/utilsFC.js b/src/js/utilsFC.js
deleted file mode 100644
index 59ed3ac912bb4d544253d6a78bf53cbed248abdb..0000000000000000000000000000000000000000
--- a/src/js/utilsFC.js
+++ /dev/null
@@ -1,514 +0,0 @@
-globalThis.Categorizer = class {
-	/**
-	 * @param  {...[]} pairs
-	 */
-	constructor(...pairs) {
-		this.cats = Array.prototype.slice.call(pairs)
-			.filter(function(e, i, a) {
-				return Array.isArray(e) && e.length === 2 && typeof e[0] === "number" && !isNaN(e[0]) &&
-					a.findIndex(function(val) {
-						return e[0] === val[0];
-					}) === i; /* uniqueness test */
-			})
-			.sort(function(a, b) {
-				return b[0] - a[0]; /* reverse sort */
-			});
-	}
-
-	cat(val, def) {
-		let result = def;
-		if (typeof val === "number" && !isNaN(val)) {
-			let foundCat = this.cats.find(function(e) {
-				return val >= e[0];
-			});
-			if (foundCat) {
-				result = foundCat[1];
-			}
-		}
-		// Record the value for the result's getter, if it is an object
-		// and doesn't have the property yet
-		if (typeof result === "object" && !isNaN(result)) {
-			result.value = val;
-		}
-		return result;
-	}
-};
-
-/**
- * @param {string} category
- * @param {string} title
- * @returns {string}
- */
-globalThis.budgetLine = function(category, title) {
-	let income;
-	let expenses;
-
-	if (passage() === "Rep Budget") {
-		income = "lastWeeksRepIncome";
-		expenses = "lastWeeksRepExpenses";
-
-		if (V[income][category] || V[expenses][category] || V.showAllEntries.repBudget) {
-			return `<tr>\
-				<td>${title}</td>\
-				<td>${repFormat(V[income][category])}</td>\
-				<td>${repFormat(V[expenses][category])}</td>\
-				<td>${repFormat(V[income][category] + V[expenses][category])}</td>\
-				</tr>`;
-		}
-	} else if (passage() === "Costs Budget") {
-		income = "lastWeeksCashIncome";
-		expenses = "lastWeeksCashExpenses";
-
-		if (V[income][category] || V[expenses][category] || V.showAllEntries.costsBudget) {
-			return `<tr>\
-				<td>${title}</td>\
-				<td>${cashFormatColor(V[income][category])}</td>\
-				<td>${cashFormatColor(-Math.abs(V[expenses][category]))}</td>\
-				<td>${cashFormatColor(V[income][category] + V[expenses][category])}</td>\
-				</tr>`;
-		}
-	}
-	return ``;
-};
-
-/*
-Make everything waiting for this execute. Usage:
-
-let doSomething = function() {
-	... your initialization code goes here ...
-};
-if(typeof Categorizer === 'function') {
-	doSomething();
-} else {
-	jQuery(document).one('categorizer.ready', doSomething);
-}
-*/
-jQuery(document).trigger("categorizer.ready");
-
-globalThis.getProstheticsStockpile = function() {
-	return `<div>Prosthetics interfaces: ${num(V.prosthetics.interfaceP1.amount + V.prosthetics.interfaceP2.amount)}</div>` +
-		`<div class="choices">Basic: ${V.prosthetics.interfaceP1.amount}</div>` +
-		`<div class="choices">Advanced: ${V.prosthetics.interfaceP2.amount}</div>` +
-		`<div>Limbs: ${num(V.prosthetics.basicL.amount + V.prosthetics.sexL.amount + V.prosthetics.beautyL.amount +
-			V.prosthetics.combatL.amount + V.prosthetics.cyberneticL.amount)}</div>` +
-		`<div class="choices">Basic: ${V.prosthetics.basicL.amount}</div>` +
-		`<div class="choices">Sex: ${V.prosthetics.sexL.amount}</div>` +
-		`<div class="choices">Beauty: ${V.prosthetics.beautyL.amount}</div>` +
-		`<div class="choices">Combat: ${V.prosthetics.combatL.amount}</div>` +
-		`<div class="choices">Cybernetic: ${V.prosthetics.cyberneticL.amount}</div>` +
-		`<div>Implants: ${num(V.prosthetics.ocular.amount + V.prosthetics.cochlear.amount + V.prosthetics.electrolarynx.amount)}</div>` +
-		`<div class="choices">Ocular: ${V.prosthetics.ocular.amount}</div>` +
-		`<div class="choices">Cochlear: ${V.prosthetics.cochlear.amount}</div>` +
-		/* `<div class="choices">Erectile: ${V.prosthetics.erectile.amount}</div>` + */
-		`<div class="choices">Electrolarynx: ${V.prosthetics.electrolarynx.amount}</div>` +
-		`<div>Tail interface: ${V.prosthetics.interfaceTail.amount}</div>` +
-		`<div>Tails: ${num(V.prosthetics.modT.amount + V.prosthetics.sexT.amount + V.prosthetics.combatT.amount)}</div>` +
-		`<div class="choices">Modular: ${V.prosthetics.modT.amount}</div>` +
-		`<div class="choices">Pleasure: ${V.prosthetics.sexT.amount}</div>` +
-		`<div class="choices">Combat: ${V.prosthetics.combatT.amount}</div>`;
-};
-
-/**
- * Converts an array of strings into a sentence parted by commas.
- * @param {Array} array ["apple", "bannana", "carrot"]
- * @returns {string} "apple, bannana and carrot"
- */
-globalThis.arrayToSentence = function(array) {
-	return array.reduce((res, ch, i, arr) => res + (i === arr.length - 1 ? ' and ' : ', ') + ch);
-};
-
-/** Returns the revivalist nationality associated with the player's arcology, or 0 if none
- * @returns {string|0}
- */
-globalThis.getRevivalistNationality = function() {
-	if (V.arcologies[0].FSRomanRevivalist > 90) {
-		return "Roman Revivalist";
-	} else if (V.arcologies[0].FSAztecRevivalist > 90) {
-		return "Aztec Revivalist";
-	} else if (V.arcologies[0].FSEgyptianRevivalist > 90) {
-		return "Ancient Egyptian Revivalist";
-	} else if (V.arcologies[0].FSEdoRevivalist > 90) {
-		return "Edo Revivalist";
-	} else if (V.arcologies[0].FSArabianRevivalist > 90) {
-		return "Arabian Revivalist";
-	} else if (V.arcologies[0].FSChineseRevivalist > 90) {
-		return "Ancient Chinese Revivalist";
-	}
-	return 0;
-};
-
-globalThis.penthouseCensus = function() {
-	function occupiesRoom(slave) {
-		if (slave.rules.living !== "luxurious") {
-			return false; // assigned to dormitory
-		} else if (slave.assignment === Job.HEADGIRL && V.HGSuite > 0) {
-			return false; // lives in HG suite
-		} else if (slave.assignment === Job.BODYGUARD && V.dojo > 0) {
-			return false; // lives in dojo
-		} else if (slave.relationship >= 4) {
-			const partner = getSlave(slave.relationshipTarget);
-			if (assignmentVisible(partner) && partner.ID < slave.ID && partner.rules.living === "luxurious") {
-				return false; // living with partner, who is already assigned a room (always allocate a room to the partner with the lower ID)
-			}
-		}
-		return true; // takes her own room
-	}
-
-	const penthouseSlaves = V.slaves.filter(s => assignmentVisible(s));
-	V.roomsPopulation = penthouseSlaves.filter(occupiesRoom).length;
-	V.dormitoryPopulation = penthouseSlaves.filter(s => s.rules.living !== "luxurious").length;
-};
-
-/** Determine whether a given penthouse slave can move into a private room or not.
- * @param {App.Entity.SlaveState} slave
- * @returns {boolean}
- */
-globalThis.canMoveToRoom = function(slave) {
-	const partner = slave.relationship >= 4 ? getSlave(slave.relationshipTarget) : null;
-	const partnerHasRoom = partner && assignmentVisible(partner) && partner.rules.living === "luxurious";
-	return partnerHasRoom || V.rooms - V.roomsPopulation >= 1;
-};
-
-/**
- * @param {App.Entity.Facilities.Job|App.Entity.Facilities.Facility} jobOrFacility job or facility object
- * @returns {App.Entity.SlaveState[]} array of slaves employed at the job or facility, sorted in accordance to user choice
- */
-App.Utils.sortedEmployees = function(jobOrFacility) {
-	const employees = jobOrFacility.employees();
-	SlaveSort.slaves(employees);
-	return employees;
-};
-
-/**
- * @param {Array<string|App.Entity.Facilities.Facility>} [facilities]
- * @param {Object.<string, string>} [mapping] Optional mapping for the property names in the result object. Keys
- * are the standard facility names, values are the desired names.
- * @returns {Object.<string, number>}
- */
-App.Utils.countFacilityWorkers = function(facilities = null, mapping = {}) {
-	facilities = facilities || Object.values(App.Entity.facilities);
-	/** @type {App.Entity.Facilities.Facility[]} */
-	const fObjects = facilities.map(f => typeof f === "string" ? App.Entity.facilities[f] : f);
-	return fObjects.reduce((acc, cur) => {
-		acc[mapping[cur.desc.baseName] || cur.desc.baseName] = cur.employeesIDs().size; return acc;
-	}, {});
-};
-
-/**
- * @param {string[]} [assignments] array of assignment strings. The default is to count for all assignments
- * @returns {Object.<string, number>}
- */
-App.Utils.countAssignmentWorkers = function(assignments) {
-	assignments = assignments || Object.values(Job);
-	return assignments.reduce((acc, cur) => { acc[cur] = V.JobIDMap[cur].size; return acc; }, {});
-};
-
-/** Calculate and return economic uncertainty multiplier for a given arcology
- * @param {number} arcologyID
- * @returns {number}
- */
-App.Utils.economicUncertainty = function(arcologyID) {
-	let uncertainty = arcologyID === 0 ? 5 : 10;
-	if (assistant.power === 1) {
-		uncertainty -= Math.max(Math.trunc(uncertainty/2), 0);
-	} else if (assistant.power > 1) {
-		uncertainty = 0;
-	}
-	return jsRandom(100 - uncertainty, 100 + uncertainty) / 100;
-};
-
-/** Notify the game that the sidebar needs to be refreshed as soon as possible, but do not do it immediately.
- *  This allows us to call this function repeatedly without impacting performance (for example, from repX() and cashX()).
- *  The game will redraw the sidebar exactly once, as soon as all the scripts have finished executing.
- */
-App.Utils.scheduleSidebarRefresh = (function() {
-	let refresh = false;
-
-	function updateSidebar() {
-		refresh = false;
-		UIBar.update();
-		App.UI.updateSidebarTooltips();
-	}
-
-	function schedule() {
-		if (!refresh) {
-			refresh = true;
-			setTimeout(updateSidebar, 0);
-		}
-	}
-
-	return schedule;
-})();
-
-/** Calculate various averages for the master suite slaves
- * @returns {{energy: number, milk: number, cum: number, dom: number, sadism: number, dick: number, preg: number}}
- */
-App.Utils.masterSuiteAverages = (function() {
-	const domMap = {dom: 1, submissive: -1};
-	const sadismMap = {sadism: 1, masochism: -1};
-
-	/** Returns either zero or the results of mapping the slave's fetish through an object containing fetish names and result values
-	 * @param {App.Entity.SlaveState} s
-	 * @param {Object.<string, number>} map
-	 * @returns {number}
-	 */
-	const fetishMapOrZero = (s, map) => map.hasOwnProperty(s.fetish) ? map[s.fetish] : 0;
-
-	return () => {
-		const msSlaves = App.Entity.facilities.masterSuite.employees();
-		return {
-			energy: _.mean(msSlaves.map(s => s.energy)),
-			milk: _.mean(msSlaves.map(s => s.lactation*(s.boobs-s.boobsImplant))),
-			cum: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.balls : 0)),
-			dick: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.dick : 0)),
-			preg: _.mean(msSlaves.map(s => s.preg)),
-			sadism: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, sadismMap)))),
-			dom: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, domMap))))
-		};
-	};
-})();
-
-App.Utils.schoolCounter = function() {
-	return Array.from(App.Data.misc.schools.keys()).filter(s => V[s].schoolPresent).length;
-};
-
-App.Utils.schoolFailure = function() {
-	return Array.from(App.Data.misc.schools.keys()).find(s => V[s].schoolPresent && V[s].schoolProsperity <= -10);
-};
-
-App.Utils.alphabetizeIterable = function(iterable) {
-	const compare = function(a, b) {
-		let aTitle = a.toLowerCase();
-		let bTitle = b.toLowerCase();
-
-		aTitle = removeArticles(aTitle);
-		bTitle = removeArticles(bTitle);
-
-		if (aTitle > bTitle) {
-			return 1;
-		}
-		if (aTitle < bTitle) {
-			return -1;
-		}
-		return 0;
-	  };
-
-	function removeArticles(str) {
-		const words = str.split(" ");
-		if (words.length <= 1) {
-			return str;
-		}
-		if ( words[0] === 'a' || words[0] === 'the' || words[0] === 'an' ) {
-			return words.splice(1).join(" ");
-		}
-		return str;
-	}
-	const clonedArray = (Array.from(iterable));
-	return clonedArray.sort(compare);
-};
-
-/**
- * @param {App.Entity.SlaveState[]} [slaves]
- * @returns {Object.<number, number>}
- */
-globalThis.slaves2indices = function(slaves = V.slaves) {
-	return slaves.reduce((acc, slave, i) => { acc[slave.ID] = i; return acc; }, {});
-};
-
-/**
- * @param {number} ID
- * @returns {App.Entity.SlaveState}
- */
-globalThis.getSlave = function(ID) {
-	const index = V.slaveIndices[ID];
-	return index === undefined ? undefined : V.slaves[index];
-};
-
-/**
- * @param {number} ID
- * @returns {App.Entity.SlaveState}
- */
-globalThis.slaveStateById = function(ID) {
-	const index = V.slaveIndices[ID];
-	return index === undefined ? null : V.slaves[index];
-};
-
-globalThis.getChild = function(ID) {
-	return V.cribs.find(s => s.ID === ID);
-};
-
-globalThis.SlaveSort = function() {
-	const effectivePreg = (slave) => {
-		// slave.preg is only *mostly* usable for sorting
-		if (slave.preg > 0 && !slave.pregKnown) {
-			// don't reveal unknown pregnancies
-			return 0;
-		}
-		if (slave.pubertyXX === 0 && (slave.ovaries === 1 || slave.mpreg === 1)) {
-			// not ovulating yet - sort between barren slaves and slaves on contraceptives
-			return -1.2;
-		} else if (slave.ovaryAge >= 47 && (slave.ovaries === 1 || slave.mpreg === 1)) {
-			// menopausal - sort between barren slaves and slaves on contraceptives
-			return -1.1;
-		} else if (slave.pregWeek < 0) {
-			// postpartum - sort between slaves on contraceptives and fertile slaves
-			return -0.1;
-		}
-		return slave.preg;
-	};
-
-	const effectiveEnergy = (slave) => {
-		return slave.attrKnown === 1 ? slave.energy : -101;
-	};
-
-	const comparators = {
-		Aassignment: (a, b) => a.assignment < b.assignment ? -1 : 1,
-		Dassignment: (a, b) => a.assignment > b.assignment ? -1 : 1,
-		Aname: (a, b) => a.slaveName < b.slaveName ? -1 : 1,
-		Dname: (a, b) => a.slaveName > b.slaveName ? -1 : 1,
-		Aseniority: (a, b) => b.weekAcquired - a.weekAcquired,
-		Dseniority: (a, b) => a.weekAcquired - b.weekAcquired,
-		AactualAge: (a, b) => a.actualAge - b.actualAge,
-		DactualAge: (a, b) => b.actualAge - a.actualAge,
-		AvisualAge: (a, b) => a.visualAge - b.visualAge,
-		DvisualAge: (a, b) => b.visualAge - a.visualAge,
-		AphysicalAge: (a, b) => a.physicalAge - b.physicalAge,
-		DphysicalAge: (a, b) => b.physicalAge - a.physicalAge,
-		Adevotion: (a, b) => a.devotion - b.devotion,
-		Ddevotion: (a, b) => b.devotion - a.devotion,
-		AID: (a, b) => a.ID - b.ID,
-		DID: (a, b) => b.ID - a.ID,
-		AweeklyIncome: (a, b) => a.lastWeeksCashIncome - b.lastWeeksCashIncome,
-		DweeklyIncome: (a, b) => b.lastWeeksCashIncome - a.lastWeeksCashIncome,
-		Ahealth: (a, b) => a.health.health - b.health.health,
-		Dhealth: (a, b) => b.health.health - a.health.health,
-		Aweight: (a, b) => a.weight - b.weight,
-		Dweight: (a, b) => b.weight - a.weight,
-		Amuscles: (a, b) => a.muscles - b.muscles,
-		Dmuscles: (a, b) => b.muscles - a.muscles,
-		AsexDrive: (a, b) => effectiveEnergy(a) - effectiveEnergy(b),
-		DsexDrive: (a, b) => effectiveEnergy(b) - effectiveEnergy(a),
-		Apregnancy: (a, b) => effectivePreg(a) - effectivePreg(b),
-		Dpregnancy: (a, b) => effectivePreg(b) - effectivePreg(a),
-	};
-
-	return {
-		slaves: sortSlaves,
-		IDs: sortIDs,
-		indices: sortIndices
-	};
-
-	/** @param {App.Entity.SlaveState[]} [slaves] */
-	function sortSlaves(slaves) {
-		slaves = slaves || V.slaves;
-		slaves.sort(_comparator());
-		if (slaves === V.slaves) {
-			V.slaveIndices = slaves2indices();
-		}
-	}
-
-	/** @param {number[]} [slaveIDs] */
-	function sortIDs(slaveIDs) {
-		const slaves = V.slaves;
-		const slaveIndices = V.slaveIndices;
-		const cmp = _comparator();
-		slaveIDs = slaveIDs || slaves.map(s => s.ID);
-		slaveIDs.sort((IDa, IDb) => cmp(slaves[slaveIndices[IDa]], slaves[slaveIndices[IDb]]));
-	}
-
-	/** @param {number[]} [slaveIdxs] */
-	function sortIndices(slaveIdxs) {
-		const slaves = V.slaves;
-		const cmp = _comparator();
-		slaveIdxs = slaveIdxs || [...slaves.keys()];
-		slaveIdxs.sort((ia, ib) => cmp(slaves[ia], slaves[ib]));
-	}
-
-	/**
-	 * @callback slaveComparator
-	 * @param {App.Entity.SlaveState} a
-	 * @param {App.Entity.SlaveState} b
-	 * @returns {number}
-	 */
-	/** @returns {slaveComparator} */
-	function _comparator() {
-		return _makeStableComparator(comparators[(V.sortSlavesOrder === "ascending" ? 'A' : 'D') + V.sortSlavesBy]);
-	}
-
-	/** secondary-sort by ascending ID if the primary comparator would return 0 (equal), so we have a guaranteed stable order regardless of input
-	 * @param {slaveComparator} comparator
-	 * @returns {slaveComparator}
-	 */
-	function _makeStableComparator(comparator) {
-		return function(a, b) {
-			return comparator(a, b) || comparators.AID(a, b);
-		};
-	}
-}();
-
-/**
- * @param {App.Entity.SlaveState[]} slaves
- */
-globalThis.slaveSortMinor = function(slaves) {
-	slaves.sort((a, b) => a.slaveName < b.slaveName ? -1 : 1);
-};
-
-globalThis.menialPopCap = function() {
-	let r = "";
-
-	let popCap = 500 * (1 + V.building.findCells(cell => cell instanceof App.Arcology.Cell.Manufacturing && cell.type === "Pens").length);
-
-	let overMenialCap = V.menials + V.fuckdolls + V.menialBioreactors - popCap;
-	if (overMenialCap > 0) {
-		const price = menialSlaveCost(-overMenialCap);
-		if (V.menials > 0) {
-			if (V.menials > overMenialCap) {
-				cashX((overMenialCap * price), "menialTrades");
-				V.menialDemandFactor -= overMenialCap;
-				V.menials -= overMenialCap;
-				overMenialCap = 0;
-				r += "You don't have enough room for all your menials and are obliged to sell some.";
-			} else {
-				cashX((V.menials * price), "menialTrades");
-				V.menialDemandFactor -= V.menials;
-				overMenialCap -= V.menials;
-				V.menials = 0;
-				r += "You don't have enough room for your menials and are obliged to sell them.";
-			}
-		}
-		if (overMenialCap > 0 && V.fuckdolls > 0) {
-			if (V.fuckdolls > overMenialCap) {
-				cashX(overMenialCap * (price * 2), "menialTrades");
-				V.menialDemandFactor -= overMenialCap;
-				V.fuckdolls -= overMenialCap;
-				overMenialCap = 0;
-				r += "You don't have enough room for all your Fuckdolls and are obliged to sell some.";
-			} else {
-				cashX(V.fuckdolls * (price * 2), "menialTrades");
-				V.menialDemandFactor -= V.fuckdolls;
-				overMenialCap -= V.fuckdolls;
-				V.fuckdolls = 0;
-				r += "You don't have enough room for your Fuckdolls and are obliged to sell them.";
-			}
-		}
-		if (overMenialCap > 0 && V.menialBioreactors > 0) {
-			cashX(overMenialCap * (price - 100), "menialTrades");
-			V.menialDemandFactor -= overMenialCap;
-			V.menialBioreactors -= overMenialCap;
-			r += "You don't have enough room for all your menial bioreactors and are obliged to sell some.";
-		}
-	}
-	return {text: r, value: popCap};
-};
-
-globalThis.initRules = function() {
-	const rule = emptyDefaultRule();
-	rule.name = "Obedient Slaves";
-	rule.condition.function = "between";
-	rule.condition.data.attribute = "devotion";
-	rule.condition.data.value = [20, null];
-	rule.set.removalAssignment = "rest";
-
-	V.defaultRules = [rule];
-	V.rulesToApplyOnce = {};
-};
diff --git a/src/js/utilsMisc.js b/src/js/utilsMisc.js
new file mode 100644
index 0000000000000000000000000000000000000000..ab9e8f6928c3a475339641231554e4dd3d127654
--- /dev/null
+++ b/src/js/utilsMisc.js
@@ -0,0 +1,75 @@
+globalThis.Categorizer = class {
+	/**
+	 * @param  {...[]} pairs
+	 */
+	constructor(...pairs) {
+		this.cats = Array.prototype.slice.call(pairs)
+			.filter(function(e, i, a) {
+				return Array.isArray(e) && e.length === 2 && typeof e[0] === "number" && !isNaN(e[0]) &&
+					a.findIndex(function(val) {
+						return e[0] === val[0];
+					}) === i; /* uniqueness test */
+			})
+			.sort(function(a, b) {
+				return b[0] - a[0]; /* reverse sort */
+			});
+	}
+
+	cat(val, def) {
+		let result = def;
+		if (typeof val === "number" && !isNaN(val)) {
+			let foundCat = this.cats.find(function(e) {
+				return val >= e[0];
+			});
+			if (foundCat) {
+				result = foundCat[1];
+			}
+		}
+		// Record the value for the result's getter, if it is an object
+		// and doesn't have the property yet
+		if (typeof result === "object" && !isNaN(result)) {
+			result.value = val;
+		}
+		return result;
+	}
+};
+
+/**
+ * Converts an array of strings into a sentence parted by commas.
+ * @param {Array} array ["apple", "banana", "carrot"]
+ * @returns {string} "apple, banana and carrot"
+ */
+globalThis.arrayToSentence = function(array) {
+	return array.reduce((res, ch, i, arr) => res + (i === arr.length - 1 ? ' and ' : ', ') + ch);
+};
+
+App.Utils.alphabetizeIterable = function(iterable) {
+	const compare = function(a, b) {
+		let aTitle = a.toLowerCase();
+		let bTitle = b.toLowerCase();
+
+		aTitle = removeArticles(aTitle);
+		bTitle = removeArticles(bTitle);
+
+		if (aTitle > bTitle) {
+			return 1;
+		}
+		if (aTitle < bTitle) {
+			return -1;
+		}
+		return 0;
+	  };
+
+	function removeArticles(str) {
+		const words = str.split(" ");
+		if (words.length <= 1) {
+			return str;
+		}
+		if ( words[0] === 'a' || words[0] === 'the' || words[0] === 'an' ) {
+			return words.splice(1).join(" ");
+		}
+		return str;
+	}
+	const clonedArray = (Array.from(iterable));
+	return clonedArray.sort(compare);
+};
diff --git a/src/js/utilsSlave.js b/src/js/utilsSlave.js
index 34fbfd2d6b5a576ee2ed668724c51dc9305fb653..9d055f0f8b2cd0565aced7afa8dc2983195db75f 100644
--- a/src/js/utilsSlave.js
+++ b/src/js/utilsSlave.js
@@ -909,119 +909,6 @@ As a categorizer
 <</if>>
 <<print $cats.muscleCat.cat(_Slave.muscles)>>
 */
-/**
- * @param {App.Entity.SlaveState} slave
- * @returns {string}
- */
-globalThis.getSlaveDevotionClass = function(slave) {
-	if ((!slave) || (!State)) {
-		return undefined;
-	}
-	if (slave.fetish === "mindbroken") {
-		return "mindbroken";
-	}
-	if (slave.devotion < -95) {
-		return "very-hateful";
-	} else if (slave.devotion < -50) {
-		return "hateful";
-	} else if (slave.devotion < -20) {
-		return "resistant";
-	} else if (slave.devotion <= 20) {
-		return "ambivalent";
-	} else if (slave.devotion <= 50) {
-		return "accepting";
-	} else if (slave.devotion <= 95) {
-		return "devoted";
-	} else {
-		return "worshipful";
-	}
-};
-
-/**
- * @param {App.Entity.SlaveState} slave
- * @returns {string}
- */
-globalThis.getSlaveTrustClass = function(slave) {
-	if ((!slave) || (!State)) {
-		return undefined;
-	}
-
-	if (slave.fetish === "mindbroken") {
-		return "";
-	}
-
-	if (slave.trust < -95) {
-		return "extremely-terrified";
-	} else if (slave.trust < -50) {
-		return "terrified";
-	} else if (slave.trust < -20) {
-		return "frightened";
-	} else if (slave.trust <= 20) {
-		return "fearful";
-	} else if (slave.trust <= 50) {
-		if (slave.devotion < -20) {
-			return "hate-careful";
-		} else {
-			return "careful";
-		}
-	} else if (slave.trust <= 95) {
-		if (slave.devotion < -20) {
-			return "bold";
-		} else {
-			return "trusting";
-		}
-	} else if (slave.devotion < -20) {
-		return "defiant";
-	} else {
-		return "profoundly-trusting";
-	}
-};
-
-/**
- * @param {App.Entity.SlaveState} slave
- * @param {number} [induce]
- * @returns {string}
- */
-globalThis.induceLactation = function(slave, induce = 0) {
-	const {His} = getPronouns(slave);
-	let r = "";
-	let lactationStartChance = jsRandom(10, 100);
-	slave.induceLactation += induce;
-	if (slave.boobs < 300) {
-		lactationStartChance *= 1.5;
-	} else if (slave.boobs < 400 || slave.boobs >= 5000) {
-		lactationStartChance *= 1.2;
-	}
-	if (slave.pubertyXX === 0) {
-		lactationStartChance *= 1.5;
-	}
-	if (slave.preg > (slave.pregData.normalBirth / 1.33)) {
-		lactationStartChance *= .5;
-	}
-	if (slave.health.condition < -20) {
-		lactationStartChance *= 2;
-	}
-	if (slave.weight <= -30) {
-		lactationStartChance *= 1.5;
-	}
-	if (slave.boobsImplant > 0) {
-		lactationStartChance *= (1 + (slave.boobsImplant / slave.boobs));
-	}
-	if (slave.lactationAdaptation > 0) {
-		lactationStartChance = (lactationStartChance / (slave.lactationAdaptation / 10));
-	}
-	if (slave.geneticQuirks.galactorrhea === 2) {
-		lactationStartChance *= .5;
-	}
-	lactationStartChance = Math.floor(lactationStartChance);
-	if (slave.induceLactation >= lactationStartChance) {
-		r += `${His} breasts have been stimulated often enough to <span class="lime">induce lactation.</span>`;
-		slave.induceLactation = 0;
-		slave.lactationDuration = 2;
-		slave.lactation = 1;
-	}
-	return r;
-};
 
 globalThis.pronounReplacer = function(slavetext) {
 	switch (slavetext) {
@@ -1484,6 +1371,7 @@ globalThis.pronounReplacer = function(slavetext) {
 	}
 	return slavetext;
 };
+
 globalThis.convertCareer = function(slave) {
 	let job = slave.career;
 	if ((V.diversePronouns === 1) && (slave.pronoun === App.Data.Pronouns.Kind.male)) {
@@ -1587,151 +1475,10 @@ globalThis.convertCareer = function(slave) {
 };
 
 /**
- * @param {string} targetSkill - Skill to be checked.
- * @param {Object} slave - Slave to be checked.
- * @param {number} [skillIncrease=1]
- * @returns {string}
+ * 
+ * @param {App.Entity.SlaveState} slave
+ * @returns {string|null}
  */
-globalThis.slaveSkillIncrease = function(targetSkill, slave, skillIncrease = 1) {
-	let r = "", skillDec;
-	const {He, his, him} = getPronouns(slave);
-	const isleadershipRole = function() {
-		if (['headGirl', 'recruiter', 'bodyguard', 'madam', 'DJ', 'nurse', 'teacher', 'attendant', 'matron', 'stewardess', 'milkmaid', 'farmer', 'wardeness'].includes(targetSkill)) {
-			return true;
-		}
-		return false;
-	};
-
-	if (slave.skill[targetSkill] <= 10) {
-		switch(targetSkill) {
-			case 'oral':
-			case 'vaginal':
-			case 'anal':
-				skillDec = `knowledge about ${targetSkill} sex,`; break;
-			case 'whoring':
-				skillDec = `knowledge about how to whore,`; break;
-			case 'entertainment':
-				skillDec = `knowledge about how to be entertaining,`; break;
-		}
-		if (isleadershipRole()) {
-			skillDec = `${capFirstChar(targetSkill)} skills.`;
-		}
-
-		if (slave.skill[targetSkill] + skillIncrease > 10) {
-			r = `<span class="green">${He} now has basic ${skillDec}</span>`;
-			switch(targetSkill) {
-				case 'oral':
-					r += ` and at least suck a dick without constant gagging.`; break;
-				case 'vaginal':
-					r += ` and can avoid some of the common pitfalls and turnoffs.`; break;
-				case 'anal':
-					r += ` and can accept penetration of ${his} anus without danger.`; break;
-				case 'whoring':
-					r += ` and can avoid some potentially dangerous situations.`; break;
-				case 'entertainment':
-					r += ` and can usually avoid serious faux pas.`; break;
-			}
-		}
-	} else if (slave.skill[targetSkill] <= 30) {
-		switch(targetSkill) {
-			case 'oral':
-			case 'vaginal':
-			case 'anal':
-				skillDec = `${targetSkill} skills,`; break;
-			case 'whoring':
-				skillDec = `skill as a whore,`; break;
-			case 'entertainment':
-				skillDec = `skill as an entertainer,`; break;
-		}
-		if (isleadershipRole()) {
-			skillDec = `skill as a ${capFirstChar(targetSkill)}.`;
-		}
-
-		if (slave.skill.oral + skillIncrease > 30) {
-			r = `<span class="green">${He} now has some ${skillDec}</span>`;
-			switch(targetSkill) {
-				case 'oral':
-					r += ` and can reliably bring dicks and pussies to climax with ${his} mouth.`; break;
-				case 'vaginal':
-					r += ` and can do more than just lie there and take it.`; break;
-				case 'anal':
-					r += ` and needs less preparation before taking rough penetration.`; break;
-				case 'whoring':
-					r += ` and knows how to sell ${his} body at a good price.`; break;
-				case 'entertainment':
-					r += ` and can flirt, dance, and strip acceptably.`; break;
-			}
-		}
-	} else if (slave.skill[targetSkill] <= 60) {
-		switch(targetSkill) {
-			case 'oral':
-			case 'vaginal':
-			case 'anal':
-				skillDec = `${targetSkill} sex expert,`; break;
-			case 'whoring':
-				skillDec = `expert whore,`; break;
-			case 'entertainment':
-				skillDec = `expert entertainer,`; break;
-		}
-		if (isleadershipRole()) {
-			skillDec = `expert ${capFirstChar(targetSkill)}.`;
-		}
-
-		if (slave.skill[targetSkill] + skillIncrease > 60) {
-			r = `<span class="green">${He} is now an ${skillDec}</span>`;
-			switch(targetSkill) {
-				case 'oral':
-					r += ` and has a delightfully experienced tongue.`; break;
-				case 'vaginal':
-					r += ` and has the muscular control to massage anything that's inside ${him}.`; break;
-				case 'anal':
-					r += ` and knows how to use ${his} sphincter to please.`; break;
-				case 'whoring':
-					r += ` and can often make clients forget that $he's a prostitute they're paying for.`; break;
-				case 'entertainment':
-					r += ` and can flirt engagingly, dance alluringly, and strip arousingly.`; break;
-			}
-		}
-	} else if (slave.skill[targetSkill] < 100) {
-		switch(targetSkill) {
-			case 'oral':
-			case 'vaginal':
-			case 'anal':
-				skillDec = `has mastered ${targetSkill} sex,`; break;
-			case 'whoring':
-				skillDec = `is now a masterful whore,`; break;
-			case 'entertainment':
-				skillDec = `is now a masterful entertainer,`; break;
-		}
-		if (isleadershipRole()) {
-			skillDec = `is now a masterful ${capFirstChar(targetSkill)}.`;
-		}
-
-		if (slave.skill[targetSkill] + skillIncrease >= 100) {
-			r = `<span class="green">${He} ${skillDec}</span>`;
-			switch(targetSkill) {
-				case 'oral':
-					r += ` and can learn nothing more about sucking dick or eating pussy.`; break;
-				case 'vaginal':
-					r += ` and can learn nothing more about tribbing or taking dick.`; break;
-				case 'anal':
-					r += ` and can learn nothing more about taking it up the ass.`; break;
-				case 'whoring':
-					r += ` and can learn nothing more about prostitution.`; break;
-				case 'entertainment':
-					r += ` and can learn nothing more about flirting, dancing, or stripping.`; break;
-			}
-		}
-	}
-
-	if (isleadershipRole() && slave.skill[targetSkill] + skillIncrease >= 100) {
-		V.tutorGraduate.push(slave.ID);
-		V.slaveTutor[capFirstChar(targetSkill)].delete(slave.ID);
-	}
-	slave.skill[targetSkill] += skillIncrease;
-	return r;
-};
-
 globalThis.tutorForSlave = function(slave) {
 	for (const tutor of Object.keys(V.slaveTutor)) {
 		const pupils = V.slaveTutor[tutor];
@@ -1742,6 +1489,11 @@ globalThis.tutorForSlave = function(slave) {
 	return null;
 };
 
+/**
+ * 
+ * @param {string} skill
+ * @returns {number}
+ */
 globalThis.upgradeMultiplier = function(skill) {
 	if (skill === 'medicine' && V.PC.career === "medicine" || skill === 'engineering' && V.PC.career === "engineer"
 		|| ((skill === 'medicine' || skill === 'engineering') && V.arcologies[0].FSRestartDecoration >= 100 && V.eugenicsFullControl === 0)) {
@@ -2136,125 +1888,6 @@ globalThis.moreNational = function(nation) {
 	return country;
 };
 
-/**
- * Returns a "disobedience factor" between 0 (perfectly obedient) and 100 (completely defiant)
- * @param {App.Entity.SlaveState} slave
- * @returns {number}
- */
-globalThis.disobedience = function(slave) {
-	const devotionBaseline = 20; // with devotion above this number slaves will obey completely
-	const trustBaseline = -20; // with trust below this number slaves will obey completely
-
-	if (slave.devotion > devotionBaseline || slave.trust < trustBaseline) {
-		return 0; // no chance of disobedience
-	}
-
-	// factors are between 0 (right on the boundary of perfectly obedient) and 10 (completely disobedient)
-	let devotionFactor = 10 - ((10 * (slave.devotion + 100)) / (devotionBaseline + 100));
-	let trustFactor = (10 * (slave.trust - trustBaseline)) / (100 - trustBaseline);
-	return Math.round(devotionFactor * trustFactor);
-};
-
-/**
- * Returns a valid rape target for a slave who is going to rape one of his peers into rivalry with him.
- * @param {App.Entity.SlaveState} slave
- * @param {function(App.Entity.SlaveState): boolean} predicate
- * @returns {App.Entity.SlaveState | undefined}
- */
-globalThis.randomRapeRivalryTarget = function(slave, predicate) {
-	const willIgnoreRules = disobedience(slave) > jsRandom(0, 100);
-
-	function canBeARapeRival(s) {
-		return (s.devotion <= 95 && s.energy <= 95 && !s.rivalry && !s.fuckdoll && s.fetish !== "mindbroken");
-	}
-
-	function canRape(rapist, rapee) {
-		const opportunity = (assignmentVisible(rapist) && assignmentVisible(rapee)) || rapist.assignment === rapee.assignment;
-		const taboo = V.seeIncest === 0 && areRelated(rapist, rapee);
-		const desire = !(rapist.relationship >= 3 && rapist.relationshipTarget === rapee.id) && !taboo;
-		const permission = willIgnoreRules || App.Utils.sexAllowed(rapist, rapee);
-		return opportunity && desire && permission;
-	}
-
-	if (typeof predicate !== 'function') {
-		predicate = (() => true);
-	}
-
-	const arr = V.slaves.filter((s) => { return canBeARapeRival(s) && canRape(slave, s); }).shuffle();
-	return arr.find(predicate);
-};
-
-
-/** @typedef {object} getBestSlavesParams
- * @property {string|function(App.Entity.SlaveState): number} part slave object property or custom function
- * @property {number} [count] number of slaves to return
- * @property {boolean} [largest] should it search for the biggest or smallest value
- * @property {function(App.Entity.SlaveState): boolean} [filter] filter out undesired slaves
- */
-
-/**
- * @param {getBestSlavesParams} params
- * @returns {App.Entity.SlaveState[]} sorted from best to worst
- */
-globalThis.getBestSlaves = function({part, count = 3, largest = true, filter = (() => true)}) {
-	const partCB = _.isFunction(part) ? part :  (slave) => slave[part];
-
-	const sortMethod = largest ? (left, right) => right.value - left.value : (left, right) => left.value - right.value;
-	return V.slaves.filter(slave => filter(slave))
-		.map(slave => ({slave, value: partCB(slave)}))
-		.sort(sortMethod)
-		.slice(0, count)
-		.map(slaveInfo => slaveInfo.slave);
-};
-/**
- * @param {getBestSlavesParams} info
- * @returns {number[]}
- */
-globalThis.getBestSlavesIDs = function(info) {
-	return getBestSlaves(info).map(slave => slave.ID);
-};
-
-/*
-//Example
-getBestSlaves({part:"butt", count: 5});
-getBestSlaves({part:"boobs"});//defaults to top 3
-getBestSlaves({part:"dick", smallest:true, filter:(slave)=>slave.dick > 0});//defaults to top 3
-getBestSlaves({part:slave=>slave.intelligence+slave.intelligenceImplant});
-*/
-
-/**
- * Generates a new slave ID that is guaranteed to be unused
- * @returns {number} slave ID
- */
-globalThis.generateSlaveID = function() {
-	// household liquidators and recETS generate slaves at an offset of 1000 (and many such slaves already exist)
-	// if you go through enough slaves you WILL generate collisions, so make sure we haven't just done that.
-	let allSlaveIDs = [...V.slaves.map((s) => s.ID), ...V.tanks.map((s) => s.ID), ...V.cribs.map((s) => s.ID)];
-	while (allSlaveIDs.includes(V.IDNumber)) {
-		V.IDNumber++;
-	}
-	return V.IDNumber++;
-};
-
-globalThis.ASDump = function() {
-	if ((typeof V.activeSlave === undefined) || (V.activeSlave === 0)) {
-		return `<span class="red">ERROR:</span> AS Dump, activeSlave invalid, returnTo is 'V.returnTo', previous passage was '${previous()}'. Please report this. `;
-	} else {
-		let SL = V.slaves.length;
-		let ID = V.activeSlave.ID;
-		if (V.i >= 0 && V.i < SL && V.slaves[V.i].ID === ID) { /* shortcut if V.i is already pointing to this slave */
-			V.slaves[V.i] = V.activeSlave;
-		} else {
-			V.i = V.slaveIndices[ID]; // find V.i if exists
-			if (typeof V.i === undefined) { /* not found, so new slave */
-				newSlave(V.activeSlave);
-			} else {
-				V.slaves[V.i] = V.activeSlave;
-			}
-		}
-	}
-};
-
 /** Deflate a slave (reset inflation to none)
  * @param {App.Entity.SlaveState} slave
  */
@@ -2267,100 +1900,6 @@ globalThis.deflate = function(slave) {
 	SetBellySize(slave);
 };
 
-/**
- * Returns how exposing a slave's outfit is, after taking into consideration a topless outfit is more revealing for beboobed slaves or female ones.
- * @param {App.Entity.SlaveState} slave
- * @returns {0|1|2|3|4}
- */
-globalThis.getExposure = function(slave) {
-	const clothes = App.Data.clothes.get(slave.clothes);
-	return (clothes.topless && clothes.exposure < 3 && (slave.boobs > 299 || (slave.genes === 'XX' && slave.vagina >= 0))) ? 3 : clothes.exposure;
-};
-
-/**
- * @param {App.Entity.SlaveState} A
- * @param {App.Entity.SlaveState} B
- * @returns {boolean}
- */
-globalThis.sameAssignmentP = function(A, B) {
-	return A.assignment === B.assignment;
-};
-
-/**
- * @param {App.Entity.SlaveState} slave
- * @returns {boolean}
- */
-globalThis.canImproveIntelligence = function(slave) {
-	let origIntel = V.genePool.find(function(s) { return s.ID === slave.ID; }).intelligence;
-	return (slave.intelligence < origIntel + 15) && (slave.intelligence < 100);
-};
-
-/**
- * @param {App.Entity.SlaveState} slave
- * @returns {number}
- */
-globalThis.maxHeight = function(slave) {
-	let max = Math.trunc(Math.clamp((Height.mean(slave) * 1.25), 0, 274)); /* max achievable height is expected height plus 25% */
-
-	if (slave.geneticQuirks.dwarfism === 2 && slave.geneticQuirks.gigantism !== 2) {
-		max = Math.min(max, 160);
-	}
-
-	return max;
-};
-
-/**
- * @param {App.Entity.SlaveState} slave
- * @returns {boolean}
- */
-globalThis.canImproveHeight = function(slave) {
-	return slave.height < maxHeight(slave);
-};
-
-/**
- * @param {App.Entity.SlaveState} slave
- * @param {FC.HumanState} target
- * @returns {boolean}
- */
-globalThis.haveRelationshipP = function(slave, target) {
-	return slave.relationshipTarget === target.ID;
-};
-
-/**
- * @param {App.Entity.SlaveState} slave
- * @param {App.Entity.SlaveState} target
- * @returns {boolean}
- */
-globalThis.isRivalP = function(slave, target) {
-	return slave.rivalryTarget === target.ID;
-};
-
-/**
- * @param {FC.HumanState} slave
- * @returns {boolean}
- */
-globalThis.supremeRaceP = function(slave) {
-	return V.arcologies[0].FSSupremacistRace === slave.race;
-};
-
-/**
- * @param {FC.HumanState} slave
- * @returns {boolean}
- */
-globalThis.inferiorRaceP = function(slave) {
-	return V.arcologies[0].FSSubjugationistRace === slave.race;
-};
-
-/**
- * @param {App.Entity.SlaveState} slave
- * @returns {boolean}
- */
-globalThis.isLeaderP = function(slave) {
-	const leaders = [S.HeadGirl, S.Bodyguard, S.Recruiter, S.Concubine, S.Nurse, S.Attendant, S.Matron, S.Madam, S.DJ, S.Milkmaid, S.Farmer, S.Stewardess, S.Schoolteacher, S.Wardeness];
-
-	return leaders.some(leader => leader && leader.ID === slave.ID);
-};
-
 /**
  * colors skin, eyes and hair based on genetic Color.
  * Takes .override_*_Color into account.
@@ -2546,23 +2085,6 @@ globalThis.newSlave = function(slave) {
 	}
 };
 
-/** Get the written title for a given slave, without messing with global state.
- * @param {App.Entity.SlaveState} [slave]
- * @returns {string}
- */
-globalThis.getWrittenTitle = function(slave) {
-	if (slave && slave.custom.title !== undefined && slave.custom.title !== "" && slave.rudeTitle === 0) {
-		return slave.custom.title;
-	}
-	if (V.PC.customTitle !== undefined) {
-		return V.PC.customTitle;
-	} else if (V.PC.title !== 0) {
-		return "Master";
-	} else {
-		return "Mistress";
-	}
-};
-
 /**
  * @param {App.Entity.SlaveState} slave
  * @returns {number}
@@ -2589,22 +2111,10 @@ globalThis.fetishChangeChance = function(slave) {
 				sex = ((slave.actualAge - V.fertilityAge) / 4);
 			}
 		}
-		chance = Math.trunc(Math.clamp((slave.devotion / 4) - (fetish) - (sex), 0, 100));
-	}
-
-	return chance;
-};
-
-/**
- * @param {App.Entity.SlaveState} slave
- * @returns {string}
- */
-globalThis.SlaveFullName = function(slave) {
-	const pair = slave.slaveSurname ? [slave.slaveName, slave.slaveSurname] : [slave.slaveName];
-	if ((V.surnameOrder !== 1 && ["Cambodian", "Chinese", "Hungarian", "Japanese", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(slave.nationality)) || (V.surnameOrder === 2)) {
-		pair.reverse();
+		chance = Math.trunc(Math.clamp((slave.devotion / 4) - (fetish) - (sex), 0, 100));
 	}
-	return pair.join(" ");
+
+	return chance;
 };
 
 /**
@@ -3428,6 +2938,9 @@ globalThis.DegradingName = function(slave) {
 	slave.slaveSurname = surname;
 };
 
+/**
+ * @param {App.Entity.SlaveState} slave
+ */
 globalThis.PaternalistName = function(slave) {
 	if (slave.slaveName.search("Miss") === -1) {
 		if (slave.slaveName.search("Ms.") === -1) {
@@ -3444,6 +2957,11 @@ globalThis.PaternalistName = function(slave) {
 	}
 };
 
+/**
+ * 
+ * @param {App.Entity.SlaveState} parent
+ * @param {App.Entity.SlaveState} child
+ */
 globalThis.parentNames = function(parent, child) {
 	const slaves = V.slaves;
 
@@ -3737,20 +3255,267 @@ globalThis.ageSlave = function(slave, forceDevelopment = false) {
 	}
 };
 
-/** Is the slave a shelter slave?
+/**
  * @param {App.Entity.SlaveState} slave
- * @returns {boolean}
+ * @param {number} [induce]
+ * @returns {string}
  */
-globalThis.isShelterSlave = function(slave) {
-	return (typeof slave.origin === "string" && slave.origin.includes("Slave Shelter"));
+globalThis.induceLactation = function(slave, induce = 0) {
+	const {His} = getPronouns(slave);
+	let r = "";
+	let lactationStartChance = jsRandom(10, 100);
+	slave.induceLactation += induce;
+	if (slave.boobs < 300) {
+		lactationStartChance *= 1.5;
+	} else if (slave.boobs < 400 || slave.boobs >= 5000) {
+		lactationStartChance *= 1.2;
+	}
+	if (slave.pubertyXX === 0) {
+		lactationStartChance *= 1.5;
+	}
+	if (slave.preg > (slave.pregData.normalBirth / 1.33)) {
+		lactationStartChance *= .5;
+	}
+	if (slave.health.condition < -20) {
+		lactationStartChance *= 2;
+	}
+	if (slave.weight <= -30) {
+		lactationStartChance *= 1.5;
+	}
+	if (slave.boobsImplant > 0) {
+		lactationStartChance *= (1 + (slave.boobsImplant / slave.boobs));
+	}
+	if (slave.lactationAdaptation > 0) {
+		lactationStartChance = (lactationStartChance / (slave.lactationAdaptation / 10));
+	}
+	if (slave.geneticQuirks.galactorrhea === 2) {
+		lactationStartChance *= .5;
+	}
+	lactationStartChance = Math.floor(lactationStartChance);
+	if (slave.induceLactation >= lactationStartChance) {
+		r += `${His} breasts have been stimulated often enough to <span class="lime">induce lactation.</span>`;
+		slave.induceLactation = 0;
+		slave.lactationDuration = 2;
+		slave.lactation = 1;
+	}
+	return r;
 };
 
 /**
- * Returns if a slave appears male, female, or androgynous.
- *
+ * @param {string} targetSkill - Skill to be checked.
+ * @param {Object} slave - Slave to be checked.
+ * @param {number} [skillIncrease=1]
+ * @returns {string}
+ */
+globalThis.slaveSkillIncrease = function(targetSkill, slave, skillIncrease = 1) {
+	let r = "", skillDec;
+	const {He, his, him} = getPronouns(slave);
+	const isleadershipRole = function() {
+		if (['headGirl', 'recruiter', 'bodyguard', 'madam', 'DJ', 'nurse', 'teacher', 'attendant', 'matron', 'stewardess', 'milkmaid', 'farmer', 'wardeness'].includes(targetSkill)) {
+			return true;
+		}
+		return false;
+	};
+
+	if (slave.skill[targetSkill] <= 10) {
+		switch(targetSkill) {
+			case 'oral':
+			case 'vaginal':
+			case 'anal':
+				skillDec = `knowledge about ${targetSkill} sex,`; break;
+			case 'whoring':
+				skillDec = `knowledge about how to whore,`; break;
+			case 'entertainment':
+				skillDec = `knowledge about how to be entertaining,`; break;
+		}
+		if (isleadershipRole()) {
+			skillDec = `${capFirstChar(targetSkill)} skills.`;
+		}
+
+		if (slave.skill[targetSkill] + skillIncrease > 10) {
+			r = `<span class="green">${He} now has basic ${skillDec}</span>`;
+			switch(targetSkill) {
+				case 'oral':
+					r += ` and at least suck a dick without constant gagging.`; break;
+				case 'vaginal':
+					r += ` and can avoid some of the common pitfalls and turnoffs.`; break;
+				case 'anal':
+					r += ` and can accept penetration of ${his} anus without danger.`; break;
+				case 'whoring':
+					r += ` and can avoid some potentially dangerous situations.`; break;
+				case 'entertainment':
+					r += ` and can usually avoid serious faux pas.`; break;
+			}
+		}
+	} else if (slave.skill[targetSkill] <= 30) {
+		switch(targetSkill) {
+			case 'oral':
+			case 'vaginal':
+			case 'anal':
+				skillDec = `${targetSkill} skills,`; break;
+			case 'whoring':
+				skillDec = `skill as a whore,`; break;
+			case 'entertainment':
+				skillDec = `skill as an entertainer,`; break;
+		}
+		if (isleadershipRole()) {
+			skillDec = `skill as a ${capFirstChar(targetSkill)}.`;
+		}
+
+		if (slave.skill.oral + skillIncrease > 30) {
+			r = `<span class="green">${He} now has some ${skillDec}</span>`;
+			switch(targetSkill) {
+				case 'oral':
+					r += ` and can reliably bring dicks and pussies to climax with ${his} mouth.`; break;
+				case 'vaginal':
+					r += ` and can do more than just lie there and take it.`; break;
+				case 'anal':
+					r += ` and needs less preparation before taking rough penetration.`; break;
+				case 'whoring':
+					r += ` and knows how to sell ${his} body at a good price.`; break;
+				case 'entertainment':
+					r += ` and can flirt, dance, and strip acceptably.`; break;
+			}
+		}
+	} else if (slave.skill[targetSkill] <= 60) {
+		switch(targetSkill) {
+			case 'oral':
+			case 'vaginal':
+			case 'anal':
+				skillDec = `${targetSkill} sex expert,`; break;
+			case 'whoring':
+				skillDec = `expert whore,`; break;
+			case 'entertainment':
+				skillDec = `expert entertainer,`; break;
+		}
+		if (isleadershipRole()) {
+			skillDec = `expert ${capFirstChar(targetSkill)}.`;
+		}
+
+		if (slave.skill[targetSkill] + skillIncrease > 60) {
+			r = `<span class="green">${He} is now an ${skillDec}</span>`;
+			switch(targetSkill) {
+				case 'oral':
+					r += ` and has a delightfully experienced tongue.`; break;
+				case 'vaginal':
+					r += ` and has the muscular control to massage anything that's inside ${him}.`; break;
+				case 'anal':
+					r += ` and knows how to use ${his} sphincter to please.`; break;
+				case 'whoring':
+					r += ` and can often make clients forget that $he's a prostitute they're paying for.`; break;
+				case 'entertainment':
+					r += ` and can flirt engagingly, dance alluringly, and strip arousingly.`; break;
+			}
+		}
+	} else if (slave.skill[targetSkill] < 100) {
+		switch(targetSkill) {
+			case 'oral':
+			case 'vaginal':
+			case 'anal':
+				skillDec = `has mastered ${targetSkill} sex,`; break;
+			case 'whoring':
+				skillDec = `is now a masterful whore,`; break;
+			case 'entertainment':
+				skillDec = `is now a masterful entertainer,`; break;
+		}
+		if (isleadershipRole()) {
+			skillDec = `is now a masterful ${capFirstChar(targetSkill)}.`;
+		}
+
+		if (slave.skill[targetSkill] + skillIncrease >= 100) {
+			r = `<span class="green">${He} ${skillDec}</span>`;
+			switch(targetSkill) {
+				case 'oral':
+					r += ` and can learn nothing more about sucking dick or eating pussy.`; break;
+				case 'vaginal':
+					r += ` and can learn nothing more about tribbing or taking dick.`; break;
+				case 'anal':
+					r += ` and can learn nothing more about taking it up the ass.`; break;
+				case 'whoring':
+					r += ` and can learn nothing more about prostitution.`; break;
+				case 'entertainment':
+					r += ` and can learn nothing more about flirting, dancing, or stripping.`; break;
+			}
+		}
+	}
+
+	if (isleadershipRole() && slave.skill[targetSkill] + skillIncrease >= 100) {
+		V.tutorGraduate.push(slave.ID);
+		V.slaveTutor[capFirstChar(targetSkill)].delete(slave.ID);
+	}
+	slave.skill[targetSkill] += skillIncrease;
+	return r;
+};
+
+/*
+//Example
+getBestSlaves({part:"butt", count: 5});
+getBestSlaves({part:"boobs"});//defaults to top 3
+getBestSlaves({part:"dick", smallest:true, filter:(slave)=>slave.dick > 0});//defaults to top 3
+getBestSlaves({part:slave=>slave.intelligence+slave.intelligenceImplant});
+*/
+
+/**
+ * Generates a new slave ID that is guaranteed to be unused
+ * @returns {number} slave ID
+ */
+globalThis.generateSlaveID = function() {
+	// household liquidators and recETS generate slaves at an offset of 1000 (and many such slaves already exist)
+	// if you go through enough slaves you WILL generate collisions, so make sure we haven't just done that.
+	let allSlaveIDs = [...V.slaves.map((s) => s.ID), ...V.tanks.map((s) => s.ID), ...V.cribs.map((s) => s.ID)];
+	while (allSlaveIDs.includes(V.IDNumber)) {
+		V.IDNumber++;
+	}
+	return V.IDNumber++;
+};
+
+/**
+ * @param {number} ID
+ * @returns {App.Entity.SlaveState}
+ */
+globalThis.slaveStateById = function(ID) {
+	const index = V.slaveIndices[ID];
+	return index === undefined ? null : V.slaves[index];
+};
+
+/**
+ * @param {number} ID
+ * @returns {App.Entity.SlaveState}
+ */
+globalThis.getSlave = function(ID) {
+	const index = V.slaveIndices[ID];
+	return index === undefined ? undefined : V.slaves[index];
+};
+
+globalThis.getChild = function(ID) {
+	return V.cribs.find(s => s.ID === ID);
+};
+
+/**
+ * Returns a valid rape target for a slave who is going to rape one of his peers into rivalry with him.
  * @param {App.Entity.SlaveState} slave
- * @returns {number}
+ * @param {function(App.Entity.SlaveState): boolean} predicate
+ * @returns {App.Entity.SlaveState | undefined}
  */
-globalThis.perceivedGender = function(slave) {
-	return -1;
+globalThis.randomRapeRivalryTarget = function(slave, predicate) {
+	const willIgnoreRules = disobedience(slave) > jsRandom(0, 100);
+
+	function canBeARapeRival(s) {
+		return (s.devotion <= 95 && s.energy <= 95 && !s.rivalry && !s.fuckdoll && s.fetish !== "mindbroken");
+	}
+
+	function canRape(rapist, rapee) {
+		const opportunity = (assignmentVisible(rapist) && assignmentVisible(rapee)) || rapist.assignment === rapee.assignment;
+		const taboo = V.seeIncest === 0 && areRelated(rapist, rapee);
+		const desire = !(rapist.relationship >= 3 && rapist.relationshipTarget === rapee.id) && !taboo;
+		const permission = willIgnoreRules || App.Utils.sexAllowed(rapist, rapee);
+		return opportunity && desire && permission;
+	}
+
+	if (typeof predicate !== 'function') {
+		predicate = (() => true);
+	}
+
+	const arr = V.slaves.filter((s) => { return canBeARapeRival(s) && canRape(slave, s); }).shuffle();
+	return arr.find(predicate);
 };
diff --git a/src/js/utilsSlaves.js b/src/js/utilsSlaves.js
new file mode 100644
index 0000000000000000000000000000000000000000..79222bba4de3558011b209d9be326bb6d86eba17
--- /dev/null
+++ b/src/js/utilsSlaves.js
@@ -0,0 +1,237 @@
+globalThis.SlaveSort = function() {
+	const effectivePreg = (slave) => {
+		// slave.preg is only *mostly* usable for sorting
+		if (slave.preg > 0 && !slave.pregKnown) {
+			// don't reveal unknown pregnancies
+			return 0;
+		}
+		if (slave.pubertyXX === 0 && (slave.ovaries === 1 || slave.mpreg === 1)) {
+			// not ovulating yet - sort between barren slaves and slaves on contraceptives
+			return -1.2;
+		} else if (slave.ovaryAge >= 47 && (slave.ovaries === 1 || slave.mpreg === 1)) {
+			// menopausal - sort between barren slaves and slaves on contraceptives
+			return -1.1;
+		} else if (slave.pregWeek < 0) {
+			// postpartum - sort between slaves on contraceptives and fertile slaves
+			return -0.1;
+		}
+		return slave.preg;
+	};
+
+	const effectiveEnergy = (slave) => {
+		return slave.attrKnown === 1 ? slave.energy : -101;
+	};
+
+	const comparators = {
+		Aassignment: (a, b) => a.assignment < b.assignment ? -1 : 1,
+		Dassignment: (a, b) => a.assignment > b.assignment ? -1 : 1,
+		Aname: (a, b) => a.slaveName < b.slaveName ? -1 : 1,
+		Dname: (a, b) => a.slaveName > b.slaveName ? -1 : 1,
+		Aseniority: (a, b) => b.weekAcquired - a.weekAcquired,
+		Dseniority: (a, b) => a.weekAcquired - b.weekAcquired,
+		AactualAge: (a, b) => a.actualAge - b.actualAge,
+		DactualAge: (a, b) => b.actualAge - a.actualAge,
+		AvisualAge: (a, b) => a.visualAge - b.visualAge,
+		DvisualAge: (a, b) => b.visualAge - a.visualAge,
+		AphysicalAge: (a, b) => a.physicalAge - b.physicalAge,
+		DphysicalAge: (a, b) => b.physicalAge - a.physicalAge,
+		Adevotion: (a, b) => a.devotion - b.devotion,
+		Ddevotion: (a, b) => b.devotion - a.devotion,
+		AID: (a, b) => a.ID - b.ID,
+		DID: (a, b) => b.ID - a.ID,
+		AweeklyIncome: (a, b) => a.lastWeeksCashIncome - b.lastWeeksCashIncome,
+		DweeklyIncome: (a, b) => b.lastWeeksCashIncome - a.lastWeeksCashIncome,
+		Ahealth: (a, b) => a.health.health - b.health.health,
+		Dhealth: (a, b) => b.health.health - a.health.health,
+		Aweight: (a, b) => a.weight - b.weight,
+		Dweight: (a, b) => b.weight - a.weight,
+		Amuscles: (a, b) => a.muscles - b.muscles,
+		Dmuscles: (a, b) => b.muscles - a.muscles,
+		AsexDrive: (a, b) => effectiveEnergy(a) - effectiveEnergy(b),
+		DsexDrive: (a, b) => effectiveEnergy(b) - effectiveEnergy(a),
+		Apregnancy: (a, b) => effectivePreg(a) - effectivePreg(b),
+		Dpregnancy: (a, b) => effectivePreg(b) - effectivePreg(a),
+	};
+
+	return {
+		slaves: sortSlaves,
+		IDs: sortIDs,
+		indices: sortIndices
+	};
+
+	/** @param {App.Entity.SlaveState[]} [slaves] */
+	function sortSlaves(slaves) {
+		slaves = slaves || V.slaves;
+		slaves.sort(_comparator());
+		if (slaves === V.slaves) {
+			V.slaveIndices = slaves2indices();
+		}
+	}
+
+	/** @param {number[]} [slaveIDs] */
+	function sortIDs(slaveIDs) {
+		const slaves = V.slaves;
+		const slaveIndices = V.slaveIndices;
+		const cmp = _comparator();
+		slaveIDs = slaveIDs || slaves.map(s => s.ID);
+		slaveIDs.sort((IDa, IDb) => cmp(slaves[slaveIndices[IDa]], slaves[slaveIndices[IDb]]));
+	}
+
+	/** @param {number[]} [slaveIdxs] */
+	function sortIndices(slaveIdxs) {
+		const slaves = V.slaves;
+		const cmp = _comparator();
+		slaveIdxs = slaveIdxs || [...slaves.keys()];
+		slaveIdxs.sort((ia, ib) => cmp(slaves[ia], slaves[ib]));
+	}
+
+	/**
+	 * @callback slaveComparator
+	 * @param {App.Entity.SlaveState} a
+	 * @param {App.Entity.SlaveState} b
+	 * @returns {number}
+	 */
+	/** @returns {slaveComparator} */
+	function _comparator() {
+		return _makeStableComparator(comparators[(V.sortSlavesOrder === "ascending" ? 'A' : 'D') + V.sortSlavesBy]);
+	}
+
+	/** secondary-sort by ascending ID if the primary comparator would return 0 (equal), so we have a guaranteed stable order regardless of input
+	 * @param {slaveComparator} comparator
+	 * @returns {slaveComparator}
+	 */
+	function _makeStableComparator(comparator) {
+		return function(a, b) {
+			return comparator(a, b) || comparators.AID(a, b);
+		};
+	}
+}();
+
+/**
+ * @param {App.Entity.SlaveState[]} slaves
+ */
+globalThis.slaveSortMinor = function(slaves) {
+	slaves.sort((a, b) => a.slaveName < b.slaveName ? -1 : 1);
+};
+
+/** @typedef {object} getBestSlavesParams
+ * @property {string|function(App.Entity.SlaveState): number} part slave object property or custom function
+ * @property {number} [count] number of slaves to return
+ * @property {boolean} [largest] should it search for the biggest or smallest value
+ * @property {function(App.Entity.SlaveState): boolean} [filter] filter out undesired slaves
+ */
+
+/**
+ * @param {getBestSlavesParams} params
+ * @returns {App.Entity.SlaveState[]} sorted from best to worst
+ */
+globalThis.getBestSlaves = function({part, count = 3, largest = true, filter = (() => true)}) {
+	const partCB = _.isFunction(part) ? part :  (slave) => slave[part];
+
+	const sortMethod = largest ? (left, right) => right.value - left.value : (left, right) => left.value - right.value;
+	return V.slaves.filter(slave => filter(slave))
+		.map(slave => ({slave, value: partCB(slave)}))
+		.sort(sortMethod)
+		.slice(0, count)
+		.map(slaveInfo => slaveInfo.slave);
+};
+
+/**
+ * @param {getBestSlavesParams} info
+ * @returns {number[]}
+ */
+globalThis.getBestSlavesIDs = function(info) {
+	return getBestSlaves(info).map(slave => slave.ID);
+};
+
+/**
+ * @param {App.Entity.SlaveState[]} [slaves]
+ * @returns {Object.<number, number>}
+ */
+globalThis.slaves2indices = function(slaves = V.slaves) {
+	return slaves.reduce((acc, slave, i) => { acc[slave.ID] = i; return acc; }, {});
+};
+
+/** Calculate various averages for the master suite slaves
+ * @returns {{energy: number, milk: number, cum: number, dom: number, sadism: number, dick: number, preg: number}}
+ */
+App.Utils.masterSuiteAverages = (function() {
+	const domMap = {dom: 1, submissive: -1};
+	const sadismMap = {sadism: 1, masochism: -1};
+
+	/** Returns either zero or the results of mapping the slave's fetish through an object containing fetish names and result values
+	 * @param {App.Entity.SlaveState} s
+	 * @param {Object.<string, number>} map
+	 * @returns {number}
+	 */
+	const fetishMapOrZero = (s, map) => map.hasOwnProperty(s.fetish) ? map[s.fetish] : 0;
+
+	return () => {
+		const msSlaves = App.Entity.facilities.masterSuite.employees();
+		return {
+			energy: _.mean(msSlaves.map(s => s.energy)),
+			milk: _.mean(msSlaves.map(s => s.lactation*(s.boobs-s.boobsImplant))),
+			cum: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.balls : 0)),
+			dick: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.dick : 0)),
+			preg: _.mean(msSlaves.map(s => s.preg)),
+			sadism: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, sadismMap)))),
+			dom: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, domMap))))
+		};
+	};
+})();
+
+globalThis.penthouseCensus = function() {
+	function occupiesRoom(slave) {
+		if (slave.rules.living !== "luxurious") {
+			return false; // assigned to dormitory
+		} else if (slave.assignment === Job.HEADGIRL && V.HGSuite > 0) {
+			return false; // lives in HG suite
+		} else if (slave.assignment === Job.BODYGUARD && V.dojo > 0) {
+			return false; // lives in dojo
+		} else if (slave.relationship >= 4) {
+			const partner = getSlave(slave.relationshipTarget);
+			if (assignmentVisible(partner) && partner.ID < slave.ID && partner.rules.living === "luxurious") {
+				return false; // living with partner, who is already assigned a room (always allocate a room to the partner with the lower ID)
+			}
+		}
+		return true; // takes her own room
+	}
+
+	const penthouseSlaves = V.slaves.filter(s => assignmentVisible(s));
+	V.roomsPopulation = penthouseSlaves.filter(occupiesRoom).length;
+	V.dormitoryPopulation = penthouseSlaves.filter(s => s.rules.living !== "luxurious").length;
+};
+
+/**
+ * @param {App.Entity.Facilities.Job|App.Entity.Facilities.Facility} jobOrFacility job or facility object
+ * @returns {App.Entity.SlaveState[]} array of slaves employed at the job or facility, sorted in accordance to user choice
+ */
+App.Utils.sortedEmployees = function(jobOrFacility) {
+	const employees = jobOrFacility.employees();
+	SlaveSort.slaves(employees);
+	return employees;
+};
+
+/**
+ * @param {Array<string|App.Entity.Facilities.Facility>} [facilities]
+ * @param {Object.<string, string>} [mapping] Optional mapping for the property names in the result object. Keys
+ * are the standard facility names, values are the desired names.
+ * @returns {Object.<string, number>}
+ */
+App.Utils.countFacilityWorkers = function(facilities = null, mapping = {}) {
+	facilities = facilities || Object.values(App.Entity.facilities);
+	/** @type {App.Entity.Facilities.Facility[]} */
+	const fObjects = facilities.map(f => typeof f === "string" ? App.Entity.facilities[f] : f);
+	return fObjects.reduce((acc, cur) => {
+		acc[mapping[cur.desc.baseName] || cur.desc.baseName] = cur.employeesIDs().size; return acc;
+	}, {});
+};
+
+/**
+ * @param {string[]} [assignments] array of assignment strings. The default is to count for all assignments
+ * @returns {Object.<string, number>}
+ */
+App.Utils.countAssignmentWorkers = function(assignments) {
+	assignments = assignments || Object.values(Job);
+	return assignments.reduce((acc, cur) => { acc[cur] = V.JobIDMap[cur].size; return acc; }, {});
+};
diff --git a/src/uncategorized/repBudget.js b/src/uncategorized/repBudget.js
new file mode 100644
index 0000000000000000000000000000000000000000..dbdc715633d06773cb7ba87f766e9fd105f1c354
--- /dev/null
+++ b/src/uncategorized/repBudget.js
@@ -0,0 +1,36 @@
+/**
+ * @param {string} category
+ * @param {string} title
+ * @returns {string}
+ */
+globalThis.budgetLine = function(category, title) {
+	let income;
+	let expenses;
+
+	if (passage() === "Rep Budget") {
+		income = "lastWeeksRepIncome";
+		expenses = "lastWeeksRepExpenses";
+
+		if (V[income][category] || V[expenses][category] || V.showAllEntries.repBudget) {
+			return `<tr>\
+				<td>${title}</td>\
+				<td>${repFormat(V[income][category])}</td>\
+				<td>${repFormat(V[expenses][category])}</td>\
+				<td>${repFormat(V[income][category] + V[expenses][category])}</td>\
+				</tr>`;
+		}
+	} else if (passage() === "Costs Budget") {
+		income = "lastWeeksCashIncome";
+		expenses = "lastWeeksCashExpenses";
+
+		if (V[income][category] || V[expenses][category] || V.showAllEntries.costsBudget) {
+			return `<tr>\
+				<td>${title}</td>\
+				<td>${cashFormatColor(V[income][category])}</td>\
+				<td>${cashFormatColor(-Math.abs(V[expenses][category]))}</td>\
+				<td>${cashFormatColor(V[income][category] + V[expenses][category])}</td>\
+				</tr>`;
+		}
+	}
+	return ``;
+};