From 831772ae5e85ba1ca18481f61fe0dd68c24080c6 Mon Sep 17 00:00:00 2001
From: lowercasedonkey <lowercasedonkey@gmail.com>
Date: Wed, 23 Dec 2020 17:06:28 -0500
Subject: [PATCH] move more functions to various files

---
 src/js/utilsArcology.js |  48 ++++++
 src/js/utilsFC.js       | 328 ----------------------------------------
 src/js/utilsSlave.js    |  53 ++++++-
 src/js/utilsSlaves.js   | 228 ++++++++++++++++++++++++++++
 4 files changed, 328 insertions(+), 329 deletions(-)
 create mode 100644 src/js/utilsSlaves.js

diff --git a/src/js/utilsArcology.js b/src/js/utilsArcology.js
index 4e75e1ee3ed..65f41139081 100644
--- a/src/js/utilsArcology.js
+++ b/src/js/utilsArcology.js
@@ -45,3 +45,51 @@ App.Utils.schoolCounter = function() {
 App.Utils.schoolFailure = function() {
 	return Array.from(App.Data.misc.schools.keys()).find(s => V[s].schoolPresent && V[s].schoolProsperity <= -10);
 };
+
+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/utilsFC.js b/src/js/utilsFC.js
index 0592144c545..ca3efee3bb2 100644
--- a/src/js/utilsFC.js
+++ b/src/js/utilsFC.js
@@ -117,53 +117,6 @@ globalThis.arrayToSentence = function(array) {
 	return array.reduce((res, ch, i, arr) => res + (i === arr.length - 1 ? ' and ' : ', ') + ch);
 };
 
-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>}
@@ -196,34 +149,6 @@ App.Utils.scheduleSidebarRefresh = (function() {
 	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.alphabetizeIterable = function(iterable) {
 	const compare = function(a, b) {
 		let aTitle = a.toLowerCase();
@@ -255,200 +180,6 @@ App.Utils.alphabetizeIterable = function(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";
@@ -460,62 +191,3 @@ globalThis.initRules = function() {
 	V.defaultRules = [rule];
 	V.rulesToApplyOnce = {};
 };
-
-/** @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);
-};
-
-/**
- * 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);
-};
-
-/**
- * @param {getBestSlavesParams} info
- * @returns {number[]}
- */
-globalThis.getBestSlavesIDs = function(info) {
-	return getBestSlaves(info).map(slave => slave.ID);
-};
diff --git a/src/js/utilsSlave.js b/src/js/utilsSlave.js
index 51c864ea895..9d055f0f8b2 100644
--- a/src/js/utilsSlave.js
+++ b/src/js/utilsSlave.js
@@ -3467,4 +3467,55 @@ globalThis.generateSlaveID = function() {
 		V.IDNumber++;
 	}
 	return V.IDNumber++;
-};
\ No newline at end of file
+};
+
+/**
+ * @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
+ * @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);
+};
diff --git a/src/js/utilsSlaves.js b/src/js/utilsSlaves.js
new file mode 100644
index 00000000000..ff68e668055
--- /dev/null
+++ b/src/js/utilsSlaves.js
@@ -0,0 +1,228 @@
+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;
+	}, {});
+};
-- 
GitLab