code:
/**
 * @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.
 *
 * @param {App.Entity.SlaveState} slave
 */
globalThis.applyGeneticColor = function(slave) {
	if (slave.override_Eye_Color !== 1) {
		resetEyeColor(slave, "both");
	}
	if (slave.override_H_Color !== 1) {
		slave.hColor = getGeneticHairColor(slave);
	}
	if (slave.override_Arm_H_Color !== 1) {
		slave.underArmHColor = getGeneticHairColor(slave);
	}
	if (slave.override_Pubic_H_Color !== 1) {
		slave.pubicHColor = getGeneticHairColor(slave);
	}
	if (slave.override_Brow_H_Color !== 1) {
		slave.eyebrowHColor = getGeneticHairColor(slave);
	}
	if (slave.override_Skin !== 1) {
		if (!(slave.skin === "sun tanned" || slave.skin === "spray tanned")) {
			slave.skin = getGeneticSkinColor(slave);
		}
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 */
globalThis.newSlave = function(slave) {
	if (getSlave(slave.ID)) {
		throw "Slave already exists";
	}
	if (slave.override_Race !== 1) {
		slave.origRace = slave.race;
	}

	applyGeneticColor(slave);

	/* eslint-disable camelcase */
	slave.override_Race = 0;
	slave.override_H_Color = 0;
	slave.override_Arm_H_Color = 0;
	slave.override_Pubic_H_Color = 0;
	slave.override_Brow_H_Color = 0;
	slave.override_Skin = 0;
	slave.override_Eye_Color = 0;
	/* eslint-enable camelcase */

	// too tall to be a dwarf catch for event slaves
	if (slave.geneticQuirks.dwarfism === 2 && slave.geneticQuirks.gigantism !== 2 && slave.height > 165) {
		slave.geneticQuirks.dwarfism = 1;
	}

	if (V.surnamesForbidden === 1) {
		slave.slaveSurname = 0;
	}

	if (slave.preg > 0) {
		slave.pregWeek = slave.preg;
	} else {
		slave.pregWeek = 0;
	}

	if (slave.clone !== 0) {
		slave.canRecruit = 0;
	}

	slave.sisters = 0;
	slave.daughters = 0;
	if (slave.mother === -1 || slave.father === -1) {
		V.PC.daughters += 1;
	}
	if (areSisters(V.PC, slave) > 0) {
		V.PC.sisters += 1;
	}
	for (let k = 0; k < V.slaves.length; k++) {
		if (V.slaves[k].mother === slave.ID || V.slaves[k].father === slave.ID) {
			slave.daughters++;
		}
		if (slave.mother === V.slaves[k].ID || slave.father === V.slaves[k].ID) {
			V.slaves[k].daughters++;
		}
		if (areSisters(V.slaves[k], slave) > 0) {
			slave.sisters++;
			V.slaves[k].sisters++;
		}
	}

	if (slave.genes === "XX") {
		if (slave.pubertyXX === 1) {
			if (slave.pubertyXY === 1) {
				slave.hormoneBalance = 20;
			} else {
				slave.hormoneBalance = 60;
			}
		} else {
			if (slave.pubertyXY === 1) {
				slave.hormoneBalance = -20;
			} else {
				slave.hormoneBalance = 20;
			}
		}
	} else if (slave.genes === "XY") {
		if (slave.pubertyXX === 1) {
			if (slave.pubertyXY === 1) {
				slave.hormoneBalance = 20;
			} else {
				slave.hormoneBalance = 40;
			}
		} else {
			if (slave.pubertyXY === 1) {
				slave.hormoneBalance = -40;
			} else {
				slave.hormoneBalance = 20;
			}
		}
	}

	if (slave.dick > 0 &&
		slave.balls > 0 &&
		slave.vagina < 0 &&
		slave.anus === 0 &&
		slave.genes === "XY" &&
		slave.faceShape === "masculine" &&
		slave.attrXY <= 35 &&
		slave.boobs < 400 &&
		slave.hormoneBalance < 0) {
		V.REFeminizationCheckinIDs.push(slave.ID);
	}
	if (slave.actualAge > 35 && slave.face <= 10 && slave.faceImplant === 0 && slave.energy <= 60) {
		V.REMILFCheckinIDs.push(slave.ID);
	}
	if (slave.attrXY <= 35 && slave.attrXX > 65) {
		V.REOrientationCheckinIDs.push(slave.ID);
	}
	if (slave.face < -10) {
		V.REUglyCheckinIDs.push(slave.ID);
	}
	if (slave.anus < 2) {
		V.REButtholeCheckinIDs.push(slave.ID);
	}
	if (slave.boobs < 800) {
		V.REReductionCheckinIDs.push(slave.ID);
	}

	generatePronouns(slave);
	SetBellySize(slave);
	V.slaveIndices[slave.ID] = V.slaves.push(slave) - 1;

	if (slave.origin !== "$He was your slave, but you freed $him, which $he repaid by participating in a coup attempt against you. It failed, and $he is again your chattel." && slave.ID !== V.boomerangSlave.ID) {
		V.genePool.push(clone(slave));
	} else {
		if (!V.genePool.some(s => s.ID === slave.ID)) {
			V.genePool.push(slave);
		}
	}

	/* special case for dulling intelligence via drugs in slave acquisition */
	if (slave.dullIntelligence) {
		slave.intelligence = -100;
		delete slave.dullIntelligence;
	}

	if (slave.assignment) {
		assignJob(slave, slave.assignment);
	} else {
		slave.assignment = Job.CHOICE;
	}

	/** do not run the Rules Assistant before adding the new slave to the slaves list! **/
	if (V.ui !== "start" && V.universalRulesNewSlavesRA === 1 && V.rulesAssistantAuto !== 0) {
		DefaultRules(slave);
	}
};

/**
 * @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);
};

/** 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}
 */
globalThis.fetishChangeChance = function(slave) {
	let chance = 0,
		fetish = (slave.fetishStrength / 4),
		sex = 0;

	if (slave.clitSetting !== slave.fetish) {
		// fetish should be more uncertain leading towards puberty and then steadily become more set in stone afterwards
		if (slave.balls) {
			if (V.potencyAge >= slave.actualAge) {
				sex = (50 - ((V.potencyAge - slave.actualAge) * 10));
				fetish = (slave.fetishStrength / 2);
			} else {
				sex = ((slave.actualAge - V.potencyAge) / 4);
			}
		} else if (slave.ovaries || slave.mpreg) {
			if (V.fertilityAge >= slave.actualAge) {
				sex = (50 - ((V.fertilityAge - slave.actualAge) * 10));
				fetish = (slave.fetishStrength / 2);
			} else {
				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();
	}
	return pair.join(" ");
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
globalThis.SlaveFullBirthName = function(slave) {
	const pair = slave.birthSurname ? [slave.birthName, slave.birthSurname] : [slave.birthName];
	if ((V.surnameOrder !== 1 && ["Cambodian", "Chinese", "Hungarian", "Japanese", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(slave.nationality)) || (V.surnameOrder === 2)) {
		pair.reverse();
	}
	return pair.join(" ");
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
globalThis.PoliteRudeTitle = function(slave) {
	const PC = V.PC;
	const {s, ss, title} = getEnunciation(slave);

	let r = "";
	if (slave.nationality === "Japanese") {
		if (slave.trust > 0) {
			r += `${PC.slaveName}${PC.title > 0 ? "kun" : "chan"}`;
		} else {
			r += (PC.slaveSurname ? PC.slaveSurname : `${PC.slaveName}${s}an`);
		}
	} else {
		if (slave.intelligence + slave.intelligenceImplant < -95) {
			r += title;
		} else if (slave.intelligence + slave.intelligenceImplant > 50) {
			r += (PC.title > 0 ? `Ma${s}ter` : `Mi${s}tre${ss}`);
		} else if (slave.trust > 0) {
			r += PC.slaveName;
		} else {
			r += (PC.slaveSurname ? PC.slaveSurname : PC.slaveName);
		}
	}
	return r;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
globalThis.SlaveTitle = function(slave) {
	let r;
	if (V.newDescriptions === 1) {
		if (slave.dick > 0 && slave.balls > 0 && slave.boobs > 300 && slave.vagina > -1 && slave.ovaries === 1) {
			if (jsRandom(1, 100) > 50) {
				r = "futanari";
			} else {
				r = "herm";
			}
		} else if (slave.dick > 0 && slave.balls === 0 && slave.boobs > 300 && slave.vagina > -1 && slave.ovaries === 1) {
			r = "dickgirl";
		} else if (slave.dick > 0 && slave.vagina > -1 && slave.ovaries === 0) {
			r = "shemale";
		} else if (slave.dick > 0 && slave.balls === 0 && slave.vagina === -1 && slave.ovaries === 0) {
			r = "eunuch";
		} else if (slave.dick > 0 && slave.balls > 0 && slave.vagina === -1 && slave.ovaries === 0) {
			if (slave.face > 10 && slave.hips > -1 && slave.shoulders < 1 && slave.faceShape !== "masculine") {
				r = "trap";
			} else if (slave.boobs > 800) {
				r = "tittyboy";
			} else if (slave.dick === 1 && slave.balls === 1) {
				r = "sissy";
			} else if (slave.dick > 1 && slave.balls > 1 && slave.height < 165 && slave.muscles < 5 && slave.visualAge < 19 && slave.faceShape !== "masculine") {
				r = "twink";
			} else if (slave.dick > 1 && slave.balls > 1 && slave.height < 160 && slave.muscles < 5 && slave.visualAge < 19) {
				r = "boytoy";
			} else if (slave.muscles > 95 && slave.height >= 185) {
				r = "titan";
			} else if (slave.muscles > 30) {
				r = "muscleboy";
			} else {
				r = "slaveboy";
			}
		} else if (slave.dick === 0 && slave.balls === 0 && slave.vagina > -1) {
			if ((slave.shoulders > slave.hips || slave.faceShape === "masculine") && slave.boobs < 400 && slave.genes === "XY") {
				r = "cuntboy";
			} else if (slave.ovaries === 0 && slave.genes === "XY") {
				r = "tranny";
			} else if (slave.weight > 10 && slave.boobs > 800 && slave.counter.birthsTotal > 0 && slave.physicalAge > 59) {
				r = "GMILF";
			} else if (slave.weight > 10 && slave.boobs > 800 && slave.counter.birthsTotal > 0 && slave.physicalAge > 35) {
				r = "MILF";
			} else if (slave.lips > 70 && slave.boobs > 2000 && slave.butt > 3) {
				r = "bimbo";
			} else if (slave.hips > 1 && slave.boobs > 2000 && slave.butt > 3 && slave.waist < 50) {
				r = "hourglass";
			} else if (slave.muscles > 95 && slave.height >= 185) {
				r = "amazon";
			} else if (slave.muscles > 30) {
				r = "musclegirl";
			} else {
				r = "slavegirl";
			}
		} else if (slave.dick === 0 && slave.balls === 0 && slave.vagina === -1) {
			r = "neuter";
		} else if (slave.dick === 0 && slave.vagina === -1) {
			r = "ballslave";
		} else {
			r = "slave";
		}

		if (slave.visualAge < 13) {
			if (slave.actualAge < 3) {
				if (slave.actualAge < 1) {
					r = "baby " + r;
				} else {
					r = "toddler " + r;
				}
			} else {
				if (slave.genes === "XY" && slave.vagina === -1) {
					r = "shota " + r;
				} else {
					r = "loli " + r;
				}
			}
		}

		if (slave.geneticQuirks.albinism === 2) {
			r = `albino ${r}`;
		}

		if (slave.dick > 9 && slave.balls > 9 && slave.boobs > 12000) {
			r = `hyper ${r}`;
		}

		if (slave.boobs > 4000 && slave.lactation > 0) {
			if (slave.physicalAge < 13) {
				r = `${r} calf`;
			} else {
				r = `${r} cow`;
			}
		} else if (slave.lactation > 0) {
			r = `milky ${r}`;
		}

		if (slave.boobs > 20000) {
			r = `supermassive titted ${r}`;
		} else if (slave.boobs > 10000) {
			r = `giant titted ${r}`;
		} else if (slave.boobs > 4000) {
			r = `huge titted ${r}`;
		} else if (slave.boobs > 1000) {
			r = `busty ${r}`;
		}

		if (slave.dick > 5 && slave.balls > 5) {
			r = `womb filling ${r}`;
		} else if (slave.dick > 5) {
			r = `well hung ${r}`;
		}

		if (slave.butt >= 12) {
			r = `colossal assed ${r}`;
		} else if (slave.butt >= 10) {
			r = `massive assed ${r}`;
		} else if (slave.butt >= 8) {
			r = `fat assed ${r}`;
		} else if (slave.butt >= 6) {
			r = `bottom heavy ${r}`;
		} else if (slave.butt >= 4) {
			r = `big bottomed ${r}`;
		}

		if (slave.weight > 10 && slave.weight < 100 && slave.boobs > 5000 && slave.butt > 5 && slave.hips >= 2 && slave.bellyPreg >= 30000 && slave.counter.births >= 10) {
			r = `${r} fertility goddess`;
		} else if (slave.counter.births >= 6) {
			r = `${r} broodmother`;
		} else if (slave.counter.births >= 3) {
			r = `${r} breeder`;
		}

		if (slave.indenture > -1) {
			r = `indentured ${r}`;
		}

		if (slave.preg > slave.pregData.normalBirth / 4 && slave.pregKnown === 1) {
			r = `pregnant ${r}`;
		} else if (slave.bellyFluid >= 5000) {
			r = `bloated ${r}`;
		} else if (slave.belly >= 5000) {
			r = `gravid ${r}`;
		}

		if (slave.fuckdoll > 0) {
			r = `${r} fuckdoll`;
		}
	} else {
		r = "slave"; /* I don't think there is an 'else'? */
		if ((slave.dick === 0) && (slave.vagina === -1)) {
			/* NULLS */
			r = "null";
			if ((slave.lactation > 0) && (slave.boobs > 2000)) {
				r = `${r} cow`;
			} else if ((slave.boobsImplant > 0) && (slave.buttImplant > 0)) {
				r = `${r} bimbo `;
			} else if (slave.boobs > 6000) {
				r = `${r} boob`;
			} else if (slave.butt > 6) {
				r = `${r} ass`;
			} else if ((slave.muscles > 30) && (slave.height < 185)) {
				r = `${r} muscle`;
			}
			if (slave.visualAge > 55) {
				r = `${r}GILF`;
			} else if (slave.visualAge > 35) {
				r = `${r}MILF`;
			} else if (slave.visualAge >= 25) {
				r = `${r}slave`;
			} else {
				r = `${r}girl`;
			}
		}

		if ((slave.dick === 0) && (slave.vagina !== -1)) {
			/* FEMALES */
			if (slave.visualAge > 55) {
				r = "GILF";
			} else if (slave.visualAge > 35) {
				r = "MILF";
			} else if (slave.visualAge >= 25) {
				r = "slave";
			} else {
				r = "slavegirl";
			}
			if ((slave.muscles > 30) && (slave.height < 185)) {
				r = `muscle ${r}`;
			} else if ((slave.lactation > 0) && (slave.boobs > 2000)) {
				r = `${r} cow`;
			} else if ((slave.boobsImplant > 0) && (slave.buttImplant > 0)) {
				r = `${r} bimbo`;
			} else if (slave.boobs > 6000) {
				r = `boob${r}`;
			} else if (slave.butt > 6) {
				r = `ass${r}`;
			}
		}

		if ((slave.dick !== 0) && (slave.vagina !== -1)) {
			if (slave.balls > 0) {
				/* FUTANARI: cock & balls & vagina */
				r = "futanari ";
			} else {
				/* FUTANARI: cock & vagina */
				r = "futa ";
			}
			if ((slave.lactation > 0) && (slave.boobs > 2000)) {
				r = `${r}cow`;
			} else if ((slave.boobsImplant > 0) && (slave.buttImplant > 0)) {
				r = `${r}bimbo `;
			} else if (slave.boobs > 6000) {
				r = `${r}boob`;
			} else if (slave.butt > 6) {
				r = `${r}ass`;
			} else if ((slave.muscles > 30) && (slave.height < 185)) {
				r = `${r}muscle`;
			}
			if (slave.visualAge > 55) {
				r = `${r}GILF`;
			} else if (slave.visualAge > 35) {
				r = `${r}MILF`;
			} else if (slave.visualAge >= 25) {
				r = `${r}slave`;
			} else {
				r = `${r}girl`;
			}
			if (slave.dick > 5 && slave.balls > 5 && slave.boobs > 5000) {
				r = `hyper ${r}`;
			}
		}

		if ((slave.dick !== 0) && (slave.vagina === -1) && (slave.balls > 0) && (slave.boobs > 300) && (slave.butt > 2)) {
			/* SHEMALES: cock & balls, T&A above minimum */
			if (slave.visualAge > 55) {
				r = "sheGILF";
			} else if (slave.visualAge > 35) {
				r = "sheMILF";
			} else if (slave.visualAge >= 25) {
				r = "shemale";
			} else {
				r = "tgirl";
			}
			if ((slave.muscles > 30) && (slave.height < 185)) {
				r = `muscle${r}`;
			} else if ((slave.lactation > 0) && (slave.boobs > 2000)) {
				r = `${r} cow`;
			} else if ((slave.boobsImplant > 0) && (slave.buttImplant > 0)) {
				r = `${r} bimbo`;
			} else if (slave.boobs > 6000) {
				r = `topheavy ${r}`;
			} else if (slave.butt > 6) {
				r = `bottomheavy ${r}`;
			}
		}

		if ((slave.boobs < 300) || (slave.butt < 2)) {
			if ((slave.dick !== 0) && (slave.vagina === -1) && (slave.balls > 0)) {
				if ((slave.shoulders < 1) || (slave.muscles <= 30)) {
					if ((slave.faceShape === "masculine") || (slave.faceShape === "androgynous")) {
						/* SISSIES: feminine shoulders or muscles, masculine faces */
						if (slave.visualAge > 55) {
							r = "sissyGILF";
						} else if (slave.visualAge > 35) {
							r = "sissyMILF";
						} else {
							r = "sissy";
						}
					} else {
						/* TRAPS: feminine shoulders or muscles, feminine faces */
						if (slave.visualAge > 55) {
							r = "trapGILF";
						} else if (slave.visualAge > 35) {
							r = "trapMILF";
						} else if (slave.visualAge >= 25) {
							r = "trap";
						} else {
							r = "trapgirl";
						}
					}
					if (slave.lactation > 0) {
						r = `${r} cow`;
					} else if ((slave.boobsImplant > 0) && (slave.buttImplant > 0)) {
						r = `${r} bimbo`;
					}
				}
			}
		}

		if ((slave.boobs < 300) || (slave.butt < 2)) {
			if ((slave.dick !== 0) && (slave.vagina === -1) && (slave.balls > 0)) {
				if ((slave.shoulders > 1) || (slave.muscles >= 30)) {
					/* BITCHES: masculine shoulders or muscles */
					r = "bitch";
					if ((slave.muscles > 30) && (slave.height < 185)) {
						r = `muscle${r}`;
					} else if (slave.lactation > 0) {
						r = `${r}cow`;
					} else if ((slave.boobsImplant > 0) && (slave.buttImplant > 0)) {
						r = `bimbo ${r}`;
					}
					if (slave.visualAge > 55) {
						r = `aged ${r}`;
					} else if (slave.visualAge > 35) {
						r = `mature ${r}`;
					} else if (slave.visualAge < 25) {
						r = `young ${r}`;
					}
				}
			}
		}

		if ((slave.dick !== 0) && (slave.vagina === -1) && (slave.balls === 0)) {
			r = "dick";
			if (slave.visualAge > 55) {
				r = `${r}GILF`;
			} else if (slave.visualAge > 35) {
				r = `${r}MILF`;
			} else if (slave.visualAge >= 25) {
				r = `${r}slave`;
			} else {
				r = `${r}girl`;
			}
			if ((slave.muscles > 30) && (slave.height < 185)) {
				r = `muscle${r}`;
			} else if ((slave.lactation > 0) && (slave.boobs > 2000)) {
				r = `${r} cow`;
			} else if ((slave.boobsImplant > 0) && (slave.buttImplant > 0)) {
				r = `${r} bimbo`;
			} else if (slave.boobs > 6000) {
				r = `boob ${r}`;
			} else if (slave.butt > 6) {
				r = `ass ${r}`;
			}
		}

		if ((slave.muscles > 30) && (slave.height > 185)) {
			r = `amazon ${r}`;
		} else if ((slave.muscles < 30) && (slave.height > 185)) {
			r = `statuesque ${r}`;
		} else if ((slave.boobs < 800) && (slave.height < 150)) {
			r = `petite ${r}`;
		} else if ((slave.boobs > 800) && (slave.height < 150)) {
			r = `shortstack ${r}`;
		}

		if (slave.counter.births >= 5) {
			r = `${r} broodmother`;
		} else if (slave.counter.births >= 2) {
			r = `${r} breeder`;
		}

		if (slave.geneticQuirks.albinism === 2) {
			r = `albino ${r}`;
		}

		if (slave.indenture > -1) {
			r = `indentured ${r}`;
		}

		if (slave.preg > slave.pregData.normalBirth / 4 && slave.pregKnown === 1) {
			r = `pregnant ${r}`;
		} else if (slave.bellyFluid >= 5000) {
			r = `bloated ${r}`;
		} else if (slave.belly >= 5000) {
			r = `gravid ${r}`;
		}

		if (slave.fuckdoll > 0) {
			r = `${r} fuckdoll`;
		}
	}
	return r;
};

/**
 * @param {App.Entity.SlaveState} slave
 */
globalThis.DegradingName = function(slave) {
	const leadershipPosition = [
		Job.ATTENDANT,
		Job.MATRON,
		Job.STEWARD,
		Job.MILKMAID,
		Job.FARMER,
		Job.DJ,
		Job.CONCUBINE,
		Job.MADAM,
		Job.TEACHER,
		Job.WARDEN,
		Job.NURSE,
		Job.HEADGIRL,
		Job.BODYGUARD,
		Job.RECRUITER
	];
	const names = [];
	const suffixes = [];

	if (slave.fuckdoll > 0) {
		slave.slaveName = `Fuckdoll No. ${slave.ID}`;
		slave.slaveSurname = 0;
	} else if (slave.assignment === Job.DAIRY && V.dairyRestraintsSetting >= 2) {
		slave.slaveName = `Bioreactor No. ${slave.ID}`;
		slave.slaveSurname = 0;
	} else {
		if (V.seeRace === 1) {
			switch (slave.race) {
				case "white":
					names.push("Pale", "White");
					break;
				case "asian":
					names.push("Asian", "Yellow");
					break;
				case "latina":
					names.push("Brown", "Latina");
					break;
				case "black":
					names.push("Black", "Dark");
					break;
				case "pacific islander":
					names.push("Islander", "Pacific", "Sea");
					break;
				case "malay":
					names.push("Cinnamon", "Pinoy", "Spice");
					break;
				case "southern european":
					names.push("Mediterranean", "Olive");
					break;
				case "amerindian":
					names.push("Indian", "Reservation");
					break;
				case "semitic":
					names.push("Semite", "Semitic");
					break;
				case "middle eastern":
					names.push("Arab", "Sand");
					break;
				case "indo-aryan":
					names.push("Brown", "Indian");
					break;
				case "mixed race":
					names.push("Mixed", "Mulatto", "Mutt");
					break;
			}
		}
		names.push(slave.hColor);
		if (!hasAnyEyes(slave)) {
			names.push("Blind", "Eyeless", "Sightless");
		}
		if (slave.hears === -2) {
			names.push("Deaf", "Earless", "Unhearing");
		}
		if (slave.boobs >= 2000) {
			suffixes.push("Boob", "Boobs", "Titty");
		}
		if (slave.boobs < 500 && slave.butt < 3) {
			names.push("Girly", "Slim", "Thin");
		}
		if (slave.boobs < 300) {
			names.push("Flat");
		}
		if (slave.anus > 2 || slave.vagina > 2) {
			names.push("Gaping", "Hallway", "Slit", "Wideopen");
		}
		if (slave.weight > 160) {
			names.push("Blimp", "Cow", "Fat", "Fatass", "Whale");
		} else if (slave.weight > 30) {
			names.push("Chubby", "Fat", "Whale");
		} else if (slave.weight <= -30) {
			names.push("Bony", "Rail", "Skinny");
		}
		if (slave.muscles > 30) {
			names.push("Huge", "Muscles", "Ripped", "Strong");
		}
		if (slave.fetishKnown === 1) {
			if (slave.fetish === "buttslut") {
				names.push("Anal", "Sodomy");
			}
			if (slave.fetish === "cumslut") {
				names.push("Cum", "Dicksuck", "Sucker");
			}
			if (slave.fetish === "humiliation") {
				names.push("Rape");
			}
			if (slave.fetish === "masochist") {
				names.push("Pain", "Rape", "Struggle");
			}
			if (slave.fetish === "pregnancy") {
				names.push("Fertile");
			}
		}
		if (slave.boobs * slave.lactation > 1000) {
			names.push("Creamy", "Milky");
			suffixes.push("Cow");
		}
		if (slave.skill.oral <= 30 && slave.skill.anal <= 30) {
			names.push("Cheap", "Fail", "Gutter");
		}
		if (slave.nipples === "fuckable") {
			names.push("Nipplefuck", "Nipplecunt");
		} else if (slave.nipples !== "tiny" && slave.nipples !== "cute") {
			names.push("Pointy", "Titclit");
			suffixes.push("Nipples");
		}
		if (slave.visualAge > 35) {
			names.push("Mature");
			suffixes.push("Cougar", "MILF");
		} else if (slave.visualAge < 25) {
			names.push("Girly", "Thin", "Young");
		}
		if (isAmputee(slave)) {
			names.push("Stumpy");
			suffixes.push("Stumpy");
		}
		if (slave.boobsImplant > 1000 || slave.buttImplant > 3) {
			names.push("Fake", "Plastic", "Silicone");
		}
		if (slave.dick > 5 && slave.balls > 5) {
			names.push("Potent");
			suffixes.push("Cannon", "Daddy");
		}
		if (slave.preg > slave.pregData.normalBirth / 1.33) {
			if (slave.broodmother === 2) {
				names.push("Bursting", "Seeded");
				suffixes.push("Factory", "Nursery");
			} else if (slave.broodmother === 1) {
				names.push("Bloated", "Stuffed");
				suffixes.push("Breeder", "Factory");
			}
		}
		if (slave.bellyPreg >= 450000) {
			names.push("Bulging", "Squirming");
		}
		if (slave.bellyPreg >= 5000) {
			names.push("Preg");
			suffixes.push("Belly", "Mommy");
		}
		if (slave.belly > 150000) {
			suffixes.push("Balloon");
		}
		if (slave.belly > 1500) {
			suffixes.push("Belly");
		}
		if (slave.dick > 0) {
			if (slave.dick > 4) {
				names.push("Dangle", "Hung");
				suffixes.push("Cock", "Dick");
			}
			if (slave.balls === 0) {
				names.push("Cut", "Gelded", "Soft");
			} else {
				names.push("Erect", "Hard", "Stiff");
			}
		}
		if (slave.dick === 1) {
			names.push("Micro", "Tiny");
			suffixes.push("Bitch");
		}
		if (slave.height >= 185) {
			names.push("Tall", "Top");
			suffixes.push("Tower");
		} else if (slave.height < 150) {
			names.push("Stumpy", "Tiny");
			suffixes.push("Shortstack", "Stumpy");
		}
		if (slave.skill.whoring > 95) {
			names.push("Money", "Street");
			suffixes.push("Whore");
		}
		if (slave.skill.entertainment > 95) {
			names.push("Easy", "Club");
			suffixes.push("Slut");
		}
		if (slave.skill.oral > 95) {
			names.push("Suck");
			suffixes.push("Throat");
		}
		if (slave.skill.vaginal > 95) {
			suffixes.push("Channel", "Kegel", "Pussy");
		}
		if (slave.skill.anal > 95) {
			suffixes.push("Asspussy", "Sphincter");
		}
		if (slave.intelligence + slave.intelligenceImplant > 50) {
			names.push("Bright", "Clever", "Smart");
			if (slave.intelligenceImplant >= 15) {
				names.push("College", "Graduate", "Nerdy");
			}
		} else if (slave.intelligence + slave.intelligenceImplant < -50) {
			names.push("Cretin", "Dumb", "Retarded", "Stupid");
		}
		if (slave.vagina === 1 && slave.skill.vaginal <= 10) {
			names.push("Fresh", "New", "Tight");
		}
		if (slave.devotion < -75) {
			names.push("Angry", "Biter", "Caged");
		} else if (slave.devotion < -50) {
			names.push("Cell", "Cuffs");
		} else if (slave.devotion < -20) {
			names.push("Bag", "Box");
		} else if (slave.devotion <= 20) {
			names.push("Sad", "Whiner");
		} else if (slave.devotion > 50) {
			names.push("Prize");
			if (slave.visualAge > 35) {
				names.push("Queen");
			} else if (slave.visualAge < 25) {
				names.push("Princess");
			}
		}
		if (slave.trust < -50) {
			names.push("Screaming");
			suffixes.push("Sobber");
		} else if (slave.trust < -20) {
			names.push("Crying");
			suffixes.push("Meat", "Tears", "Thing", "Weeper");
		} else if (slave.trust < 20) {
			names.push("Begging");
		}

		if (slave.dick === 0) {
			if (slave.vagina === -1) {
				suffixes.push("Null");
			} else {
				if (slave.visualAge < 25) {
					suffixes.push("Girl");
				}
			}
		} else {
			if (slave.vagina !== -1) {
				suffixes.push("Futa");
			} else {
				if (slave.balls > 0) {
					if (slave.boobs > 300 && slave.butt > 2) {
						/* SHEMALES: cock & balls, T&A above minimum */
						suffixes.push("Shemale");
					} else {
						if (slave.shoulders < 1 && slave.muscles <= 30) {
							if (slave.faceShape === "masculine" || slave.faceShape === "androgynous") {
								/* SISSIES: feminine shoulders or muscles, masculine faces */
								suffixes.push("Sissy");
							} else {
								/* TRAPS: feminine shoulders or muscles, feminine faces */
								suffixes.push("Trap");
							}
						} else {
							/* BITCHES: masculine shoulders or muscles */
							suffixes.push("Bitch");
						}
					}
				} else {
					if (slave.visualAge > 35) {
						suffixes.push("DickMILF");
					} else if (slave.visualAge >= 25) {
						suffixes.push("Dickslave");
					} else {
						suffixes.push("Dickgirl");
					}
				}
			}
		}
		if (slave.anus > 0) {
			suffixes.push("Anus", "Asshole", "Backdoor", "Butt", "Butthole");
		}
		if (slave.anus === 1) {
			suffixes.push("Tightass", "Tightbutt");
		}
		if (slave.vagina > 0) {
			suffixes.push("Cunt", "Pussy", "Vagina");
		}
		if (slave.boobs < 500 && slave.butt < 3 && slave.dick > 0) {
			suffixes.push("Bitch", "Bottom", "Sissy", "Trap");
		}
		if (slave.energy > 95) {
			suffixes.push("Fuck", "Fuckaddict", "Nympho", "Sexaddict");
		}
		if (slave.fetishKnown === 1) {
			if (slave.fetish === "humiliation") {
				suffixes.push("Rapebait", "Showgirl");
			}
			if (slave.fetish === "submissive") {
				suffixes.push("Bottom", "Fuckee", "Rapebait");
			}
			if (slave.fetish === "dom") {
				suffixes.push("Dom", "Fucker", "Top");
			}
			if (slave.fetish === "pregnancy") {
				suffixes.push("Breeder", "Mommy");
			}
			if (slave.fetish === "boobs") {
				suffixes.push("Boob", "Boobie", "Tit", "Titty");
			}
		}
		if (slave.counter.births >= 2) {
			suffixes.push("Breeder");
			if (slave.counter.births >= 5) {
				suffixes.push("Broodmother");
			}
		}
		if (slave.areolae > 2) {
			suffixes.push("Areolas", "Headlights");
		}
		if (slave.lips > 40) {
			suffixes.push("Lips", "Pillows");
		}
		if (slave.labia > 1) {
			suffixes.push("Curtains", "Flower", "Lips");
		}
		if (slave.breedingMark === 1 && V.propOutcome === 1 && V.arcologies[0].FSRestart !== "unset") {
			suffixes.push("Breeder", "Oven", "Womb");
		}
		if (slave.butt > 5) {
			suffixes.push("Ass", "Bottom", "Butt");
		}
		if (slave.vagina === 0) {
			suffixes.push("Virgin");
		}

		slave.slaveName = jsEither(names);
	}
	if (leadershipPosition.includes(slave.assignment)) {
		switch (slave.assignment) {
			case Job.ATTENDANT:
				slave.slaveName = jsEither(["Bath", "Spa"]);
				break;
			case Job.MATRON:
				slave.slaveName = jsEither(["Matron", "Nursery"]);
				break;
			case Job.STEWARD:
				slave.slaveName = jsEither(["Maid", "Servant"]);
				break;
			case Job.MILKMAID:
				if (V.cumSlaves > 3) {
					slave.slaveName = jsEither(["Fucker", "Milker"]);
				} else {
					slave.slaveName = jsEither(["Dairy", "Farm"]);
				}
				break;
			case Job.FARMER:
				slave.slaveName = jsEither(["Farmer", "Farmhand"]);
				break;
			case Job.DJ:
				slave.slaveName = jsEither(["Bass", "Booth"]);
				break;
			case Job.CONCUBINE:
				slave.slaveName = jsEither(["Bed", "Master"]);
				break;
			case Job.MADAM:
				slave.slaveName = jsEither(["Madam", "Pimp"]);
				break;
			case Job.TEACHER:
				slave.slaveName = jsEither(["Classroom", "Teacher"]);
				break;
			case Job.WARDEN:
				slave.slaveName = jsEither(["Jail", "Prison"]);
				break;
			case Job.NURSE:
				slave.slaveName = jsEither(["Clinic", "Nurse"]);
				break;
			case Job.HEADGIRL:
				slave.slaveName = jsEither(["Chief", "Head"]);
				break;
			case Job.BODYGUARD:
				slave.slaveName = jsEither(["Battle", "Guard"]);
				break;
			case Job.RECRUITER:
				slave.slaveName = jsEither(["Cam", "Recruiter"]);
				break;
		}
	}
	const surname = jsEither(suffixes);
	if (typeof surname === "string" && surname.toLowerCase() === slave.slaveName.toLowerCase()) {
		DegradingName(slave);
	}
	slave.slaveName = capFirstChar(slave.slaveName);
	slave.slaveSurname = surname;
};

globalThis.PaternalistName = function(slave) {
	if (slave.slaveName.search("Miss") === -1) {
		if (slave.slaveName.search("Ms.") === -1) {
			if (slave.slaveName.search("Mrs.") === -1) {
				if (slave.relationship > 4) {
					slave.slaveName = ("Mrs. " + slave.slaveName);
				} else if (slave.actualAge > 24) {
					slave.slaveName = ("Ms. " + slave.slaveName);
				} else {
					slave.slaveName = ("Miss " + slave.slaveName);
				}
			}
		}
	}
};

globalThis.parentNames = function(parent, child) {
	const slaves = V.slaves;

	let currentSlaveNames = slaves.map(s => s.slaveName);
	let continentNationality;
	const useMaleName = (child.genes === "XY" && V.allowMaleSlaveNames === true);

	child.slaveName = generateName(parent.nationality, child.race, useMaleName, sn => !currentSlaveNames.includes(sn));

	if (!child.slaveName) {
		for (let i = 0; i < 10; i++) {
			continentNationality = hashChoice(V.nationalities);
			child.slaveName = generateName(continentNationality, child.race, useMaleName, sn => !currentSlaveNames.includes(sn));	// jshint ignore: line
		}
	}
	if (!child.slaveName) {
		child.slaveName = generateName(parent.nationality, child.race, useMaleName);
	}
};

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

/**
 * @param {App.Entity.SlaveState} slave
 * @param {number} amount
 * @returns {string}
 */
globalThis.faceIncrease = function(slave, amount) {
	const pronouns = getPronouns(slave);
	const his = pronouns.possessive;
	const His = capFirstChar(his);
	let r = "";
	if (slave.face <= -95) {
		r += `<span class="green">${His} face is no longer horrifying,</span> and is now merely ugly.`;
	} else if (slave.face <= -40 && slave.face + amount > -40) {
		r += `<span class="green">${His} face is no longer ugly,</span> and is now merely unattractive.`;
	} else if (slave.face <= -10 && slave.face + amount > -10) {
		r += `<span class="green">${His} face is no longer unattractive,</span> and is now somewhat tolerable.`;
	} else if (slave.face <= 10 && slave.face + amount > 10) {
		r += `<span class="green">${His} face is now decently attractive,</span> rather than merely tolerable.`;
	} else if (slave.face <= 40 && slave.face + amount > 40) {
		r += `<span class="green">${His} face is now quite beautiful,</span> rather than merely pretty.`;
	} else if (slave.face <= 95 && slave.face + amount > 95) {
		r += `<span class="green">${His} face is now perfect.</span> It's difficult to imagine how it could be any more beautiful.`;
	}
	slave.face = Math.clamp(slave.face + amount, -100, 100);
	if (slave.face > 95) {
		slave.face = 100;
	}
	return r;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
globalThis.deadliness = function(slave) {
	let deadliness = 2;

	if (slave.skill.combat > 0) {
		deadliness += 2;
	}

	if (setup.bodyguardCareers.includes(slave.career)) {
		deadliness += 1;
	} else if (slave.skill.bodyguard >= V.masteredXP) {
		deadliness += 1;
	}

	if (V.AgePenalty !== 0) {
		if (slave.physicalAge >= 100) {
			deadliness -= 10;
		} else if (slave.physicalAge >= 85) {
			deadliness -= 3;
		} else if (slave.physicalAge >= 70) {
			deadliness -= 1;
		}
	}

	if (slave.muscles > 30 && slave.muscles <= 95) {
		deadliness += 1;
	} else if (slave.muscles > 95 && slave.height >= 185) {
		deadliness += 2;
	} else if (slave.muscles > 95) {
		deadliness -= 1;
	} else if (slave.muscles < -95) {
		deadliness -= 20;
	} else if (slave.muscles < -30) {
		deadliness -= 7;
	} else if (slave.muscles < -5) {
		deadliness -= 3;
	}

	if (slave.height >= 170) {
		deadliness += 1;
	}

	if (slave.health.condition > 50) {
		deadliness += 1;
	} else if (slave.health.condition < -50) {
		deadliness -= 1;
	}

	if (slave.health.illness > 3) {
		deadliness -= 3;
	} else if (slave.health.illness > 1) {
		deadliness -= 2;
	} else if (slave.health.illness > 0) {
		deadliness -= 1;
	}

	if (slave.boobs > 4000) {
		deadliness -= 2;
	} else if (slave.boobs > 2000) {
		deadliness -= 1;
	}

	if (slave.butt > 6) {
		deadliness -= 1;
	}

	if (slave.hips > 2) {
		deadliness -= 1;
	}

	if (slave.weight > 190) {
		deadliness -= 20;
	} else if (slave.weight > 160) {
		deadliness -= 10;
	} else if (slave.weight > 130) {
		deadliness -= 3;
	} else if (slave.weight > 30 || slave.weight < -10) {
		deadliness -= 1;
	}

	if (slave.bellyFluid >= 10000) {
		deadliness -= 3;
	} else if (slave.bellyFluid >= 5000) {
		deadliness -= 2;
	} else if (slave.bellyFluid >= 2000) {
		deadliness -= 1;
	}

	if (slave.pregKnown === 1 || slave.bellyPreg >= 1500 || slave.bellyImplant >= 1500) {
		if (slave.belly >= 750000) {
			deadliness -= 50;
		} else if (slave.belly >= 600000) {
			deadliness -= 25;
		} else if (slave.belly >= 450000) {
			deadliness -= 15;
		} else if (slave.belly >= 300000) {
			deadliness -= 10;
		} else if (slave.belly >= 150000) {
			deadliness -= 8;
		} else if (slave.belly >= 100000) {
			deadliness -= 7;
		} else if (slave.belly >= 10000) {
			deadliness -= 3;
		} else if (slave.belly >= 5000) {
			deadliness -= 2;
		} else {
			deadliness -= 1;
		}
	}

	if (isInLabor(slave)) {
		deadliness -= 15;
	} else if (slave.preg >= slave.pregData.normalBirth && slave.pregControl !== "labor suppressors") {
		deadliness -= 5;
	}

	if (slave.balls >= 15) {
		deadliness -= 1;
	}

	if (slave.dick >= 10) {
		deadliness -= 1;
	}

	deadliness -= getLimbCount(slave, 0) * 5;
	deadliness -= getLimbCount(slave, 2) * 0.25;
	deadliness -= getLimbCount(slave, 3) * 0.25;
	deadliness -= getLimbCount(slave, 4) * 0.25;
	deadliness += getLimbCount(slave, 5) * 1.25;
	deadliness += getLimbCount(slave, 6) * 2.5;
	if (hasBothLegs(slave) && !canStand(slave)) {
		deadliness -= 20;
	}

	if (!canSee(slave)) {
		deadliness -= 8;
	} else if (!canSeePerfectly(slave)) {
		deadliness -= 1;
	}

	if (!canHear(slave)) {
		deadliness -= 4;
	} else if ((slave.hears === -1 && slave.earwear !== "hearing aids") || (slave.hears === 0 && slave.earwear === "muffling ear plugs")) {
		deadliness -= 1;
	}

	if (slave.tail === "combat") {
		deadliness += 2;
	}

	if (slave.health.tired > 90) {
		deadliness -= 10;
	} else if (slave.health.tired > 60) {
		deadliness -= 3;
	} else if (slave.health.tired > 30) {
		deadliness -= 1;
	}

	return Math.max(deadliness, 1);
};

/** Is the slave ready to retire?
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.retirementReady = function(slave) {
	// indentured slaves don't retire, they expire
	if (slave.indenture >= 0) {
		return false;
	}

	// retirement by age
	if (V.policies.retirement.physicalAgePolicy === 0 && slave.actualAge >= V.retirementAge) {
		return true;
	} else if (V.policies.retirement.physicalAgePolicy === 1 && slave.physicalAge >= V.retirementAge) {
		return true;
	}

	// retirement by milestone
	if (V.policies.retirement.sex > 0 && (slave.counter.oral + slave.counter.anal + slave.counter.vaginal + slave.counter.penetrative + slave.counter.mammary) > V.policies.retirement.sex) {
		return true;
	}
	if (V.policies.retirement.milk > 0 && slave.counter.milk > V.policies.retirement.milk) {
		return true;
	}
	if (V.policies.retirement.cum > 0 && slave.counter.cum > V.policies.retirement.cum) {
		return true;
	}
	if (V.policies.retirement.births > 0 && slave.counter.births > V.policies.retirement.births) {
		return true;
	}
	if (V.policies.retirement.kills > 0 && slave.counter.pitKills > V.policies.retirement.kills) {
		return true;
	}

	// no retirement for you
	return false;
};

/** marks some weeks of time passage for a slave, counting birthdays and invoking aging if game settings require it
 * @param {App.Entity.SlaveState} slave
 * @param {number} [weeks=1]
 */
globalThis.ageSlaveWeeks = function(slave, weeks = 1) {
	if (V.seeAge !== 0) { // birthdays enabled
		for (let i = 0; i < weeks; ++i) {
			slave.birthWeek++;
			if (slave.birthWeek >= 52) {
				slave.birthWeek = 0;
				if (V.seeAge === 1) { // actual aging enabled
					ageSlave(slave);
				}
			}
		}
	}
};

/** advances the age of a slave by one year, incurring all aging side effects
 * @param {App.Entity.SlaveState} slave
 * @param {boolean} [forceDevelopment=false]
 */
globalThis.ageSlave = function(slave, forceDevelopment = false) {
	slave.physicalAge++;
	slave.actualAge++;
	if (slave.geneMods.NCS === 1 || (slave.geneticQuirks.neoteny >= 2 && slave.geneticQuirks.progeria !== 2)) {
		/* Induced NCS completely takes over visual aging. Additionally, because of the neoteny aspects of NCS, ovaries don't age quite as fast. */
		/* Unsurprisingly, actual neoteny has the same effect as long as progeria isn't in play. */
		slave.ovaryAge += either(0.5, 0.6, 0.7, 0.7, 0.8, 0.9, 1.0);
	} else {
		slave.visualAge++;
		slave.ovaryAge += either(0.8, 0.9, 0.9, 1.0, 1.0, 1.0, 1.1);
	}
	if (slave.broodmother === 1) {
		slave.ovaryAge += 0.2;
	}
	if (slave.physicalAge <= 18 && (forceDevelopment || V.loliGrow > 0)) {
		physicalDevelopment(slave);
	}
};

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

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


code:
/* eslint-disable no-case-declarations */

/**
 * @param {App.Entity.SlaveState} slave
 * @param {FC.Assignment} job
 * @returns {string}
 */
globalThis.assignJob = function(slave, job) {
	"use strict";
	let r = "";
	const oldJob = slave.assignment;

	// handle non-exclusive pseudo-assignments as special cases
	if (job === Job.PIT) {
		if (!V.pit.fighterIDs.includes(slave.ID)) {
			V.pit.fighterIDs.push(slave.ID);
		}
		V.JobIDMap[Job.PIT].add(slave.ID);
		return r;
	} else if (job === Job.LURCHER) {
		V.LurcherID = slave.ID;
		V.JobIDMap[Job.LURCHER].clear();
		V.JobIDMap[Job.LURCHER].add(slave.ID);
		return r;
	}

	removeJob(slave, slave.assignment, true);
	const restingAssignment = slave.assignment; // not necessary Job.REST, but the assignment chosen by removeJob() for her case
	const idx = V.slaveIndices[slave.ID];

	/**
	 * this helper makes sure global references global IDs ($HeadGirlID, $AttendantID, etc) are set correctly
	 * @param {string} propName
	 */
	function uniqueJob(propName) {
		if (App.Data.resetOnNGPlus.hasOwnProperty(propName)) {
			// TODO remove this branch when all special slaves migrate to IDs
			if (V[propName] !== 0 && V[propName].ID !== slave.ID) {
				removeJob(V[propName], job, true);
			}
			V[propName] = slave;
		} else {
			const specialIDProp = `${propName}ID`;
			const prevAssigneeID = V[specialIDProp];
			if (prevAssigneeID !== slave.ID) {
				removeJob(slaveStateById(prevAssigneeID), job, true);
			}
			V[specialIDProp] = slave.ID;
		}
	}

	/* Tracking for the following cases: */
	if (oldJob !== job && V.assignmentRecords[slave.ID] !== job && oldJob !== Job.REST) { // Check that there is a real change happening. Sometimes when you send someone to a classroom or something, this fires twice.
		switch (job.toLowerCase()) {
			case Job.CELLBLOCK:
			case "cellblock":
				if (oldJob !== Job.CELLBLOCK) { // Due to the way assignJob fires twice on assigning to a building, we have to make sure that we are keeping the original record.
					V.assignmentRecords[slave.ID] = oldJob;
				}
				break;
			case Job.CLINIC:
			case "clinic":
				if (oldJob !== Job.CLINIC) {
					V.assignmentRecords[slave.ID] = oldJob;
				}
				break;
			case Job.SCHOOL.toLowerCase():
			case "schoolroom":
				if (oldJob !== Job.SCHOOL) {
					V.assignmentRecords[slave.ID] = oldJob;
				}
				break;
			case Job.SPA.toLowerCase():
			case "spa":
				if (oldJob !== Job.SPA) {
					V.assignmentRecords[slave.ID] = oldJob;
				}
				break;
		}
	}
	/* use .toLowerCase() to get rid of a few dupe conditions. */
	switch (job.toLowerCase()) {
		case Job.ARCADE.toLowerCase():
		case "arcade":
			slave.assignment = Job.ARCADE;
			if (slave.clothes !== "a fuckdoll suit") {
				slave.clothes = "no clothing";
			}
			slave.shoes = "none";
			slave.collar = "none";
			slave.rules.living = LivingRule.SPARE;
			break;

		case Job.BROTHEL.toLowerCase():
		case "brothel":
			slave.assignment = Job.BROTHEL;
			switch (V.brothelDecoration) {
				case "Degradationist":
				case "standard":
					slave.rules.living = LivingRule.SPARE;
					break;
				default:
					slave.rules.living = LivingRule.NORMAL;
					break;
			}
			break;

		case Job.CELLBLOCK.toLowerCase():
		case "cellblock":
			slave.assignment = Job.CELLBLOCK;
			switch (V.cellblockDecoration) {
				case "Paternalist":
					slave.rules.living = LivingRule.NORMAL;
					break;
				default:
					slave.rules.living = LivingRule.SPARE;
					break;
			}
			break;

		case Job.CLINIC.toLowerCase():
		case "clinic":
			slave.assignment = Job.CLINIC;
			switch (V.clinicDecoration) {
				case "Repopulation Focus":
				case "Eugenics":
				case "Gender Radicalist":
				case "Gender Fundamentalist":
				case "Paternalist":
				case "Maturity Preferentialist":
				case "Youth Preferentialist":
				case "Slimness Enthusiast":
				case "Hedonistic":
				case "Intellectual Dependency":
				case "Petite Admiration":
				case "Statuesque Glorification":
				case "Neo Imperialist":
					slave.rules.living = LivingRule.LUXURIOUS;
					break;

				case "Roman Revivalist":
				case "Aztec Revivalist":
				case "Egyptian Revivalist":
				case "Arabian Revivalist":
				case "Chinese Revivalist":
				case "Chattel Religionist":
				case "Edo Revivalist":
					slave.rules.living = LivingRule.NORMAL;
					break;

				default:
					slave.rules.living = LivingRule.SPARE;
					break;
			}
			break;

		case Job.CLUB.toLowerCase():
		case "club":
			slave.assignment = Job.CLUB;
			slave.rules.living = LivingRule.NORMAL;
			break;

		case Job.DAIRY.toLowerCase():
		case "dairy":
			slave.assignment = Job.DAIRY;
			switch (V.dairyDecoration) {
				case "Roman Revivalist":
				case "Neo Imperialist":
				case "Aztec Revivalist":
				case "Chinese Revivalist":
				case "Chattel Religionist":
				case "Edo Revivalist":
				case "Arabian Revivalist":
				case "Egyptian Revivalist":
				case "Supremacist":
				case "Subjugationist":
				case "Degradationist":
					slave.rules.living = LivingRule.SPARE;
					break;
				default:
					slave.rules.living = LivingRule.NORMAL;
					break;
			}
			break;

		case Job.FARMYARD.toLowerCase():
		case "farmyard":
			slave.assignment = Job.FARMYARD;
			switch (V.farmyardDecoration) {
				case "Neo Imperialist":
				case "Aztec Revivalist":
				case "Chinese Revivalist":
				case "Chattel Religionist":
				case "Edo Revivalist":
				case "Arabian Revivalist":
				case "Egyptian Revivalist":
				case "Supremacist":
				case "Subjugationist":
				case "Degradationist":
					slave.rules.living = LivingRule.SPARE;
					break;
				case "Roman Revivalist":
					slave.rules.living = LivingRule.LUXURIOUS;
					break;
				default:
					slave.rules.living = LivingRule.SPARE;
					break;
			}
			break;

		case Job.HEADGIRLSUITE.toLowerCase():
		case "head girl suite":
		case "hgsuite":
			slave.assignment = Job.HEADGIRLSUITE;
			slave.rules.living = LivingRule.LUXURIOUS;
			break;

		case Job.MASTERSUITE.toLowerCase():
		case "master suite":
		case "mastersuite":
			slave.assignment = Job.MASTERSUITE;
			if (V.masterSuiteUpgradeLuxury > 0) {
				slave.rules.living = LivingRule.LUXURIOUS;
			} else {
				slave.rules.living = LivingRule.NORMAL;
			}
			break;

		case Job.SCHOOL.toLowerCase():
		case "schoolroom":
			slave.assignment = Job.SCHOOL;
			slave.rules.living = LivingRule.NORMAL;
			break;

		case Job.QUARTER.toLowerCase():
		case "servants' quarters":
		case "servantsquarters":
			slave.assignment = Job.QUARTER;
			switch (V.servantsQuartersDecoration) {
				case "Roman Revivalist":
				case "Neo Imperialist":
				case "Aztec Revivalist":
				case "Chinese Revivalist":
				case "Chattel Religionist":
				case "Edo Revivalist":
				case "Supremacist":
				case "Subjugationist":
				case "Degradationist":
				case "Arabian Revivalist":
				case "Egyptian Revivalist":
					slave.rules.living = LivingRule.SPARE;
					break;
				case "Slave Professionalism":
					if (slave.intelligence + slave.intelligenceImplant > 15) {
						slave.rules.living = LivingRule.NORMAL;
					} else {
						slave.rules.living = LivingRule.SPARE;
					}
					break;
				case "Petite Admiration":
				case "Statuesque Glorification":
					if (heightPass(slave)) {
						slave.rules.living = LivingRule.NORMAL;
					} else {
						slave.rules.living = LivingRule.SPARE;
					}
					break;
				default:
					slave.rules.living = LivingRule.NORMAL;
					break;
			}
			break;

		case Job.SPA.toLowerCase():
		case "spa":
			slave.assignment = Job.SPA;
			switch (V.spaDecoration) {
				case "Chattel Religionist":
				case "Chinese Revivalist":
					slave.rules.living = LivingRule.NORMAL;
					break;
				case "Degradationist":
					slave.rules.living = LivingRule.SPARE;
					break;
				default:
					slave.rules.living = LivingRule.LUXURIOUS;
					break;
			}
			break;

		case Job.NURSERY.toLowerCase():
		case "nursery":
			slave.assignment = Job.NURSERY;
			slave.rules.living = LivingRule.NORMAL;
			break;

		case Job.ATTENDANT.toLowerCase():
			uniqueJob("Attendant");
			slave.assignment = Job.ATTENDANT;
			slave.rules.living = LivingRule.LUXURIOUS;
			break;

		case Job.MATRON.toLowerCase():
			uniqueJob("Matron");
			slave.assignment = Job.MATRON;
			slave.rules.living = LivingRule.LUXURIOUS;
			slave.rules.rest = RestRule.MID;
			break;

		case Job.DJ.toLowerCase():
			uniqueJob("dj");
			slave.assignment = Job.DJ;
			slave.rules.living = LivingRule.LUXURIOUS;
			slave.rules.rest = RestRule.MID;
			break;

		case Job.MADAM.toLowerCase():
			uniqueJob("Madam");
			slave.assignment = Job.MADAM;
			slave.rules.living = LivingRule.LUXURIOUS;
			slave.rules.rest = RestRule.MID;
			break;

		case Job.MILKMAID.toLowerCase():
			uniqueJob("Milkmaid");
			slave.assignment = Job.MILKMAID;
			slave.rules.living = LivingRule.LUXURIOUS;
			slave.rules.rest = RestRule.MID;
			break;

		case Job.FARMER.toLowerCase():
			uniqueJob("Farmer");
			slave.assignment = Job.FARMER;
			slave.rules.living = LivingRule.LUXURIOUS;
			slave.rules.rest = RestRule.MID;
			break;

		case Job.NURSE.toLowerCase():
			uniqueJob("Nurse");
			slave.assignment = Job.NURSE;
			slave.rules.living = LivingRule.LUXURIOUS;
			slave.rules.rest = RestRule.MID;
			break;

		case Job.TEACHER.toLowerCase():
			uniqueJob("Schoolteacher");
			slave.assignment = Job.TEACHER;
			slave.rules.living = LivingRule.LUXURIOUS;
			slave.rules.rest = RestRule.MID;
			break;

		case Job.STEWARD.toLowerCase():
			uniqueJob("Stewardess");
			slave.assignment = Job.STEWARD;
			slave.rules.living = LivingRule.LUXURIOUS;
			slave.rules.rest = RestRule.MID;
			break;

		case Job.WARDEN.toLowerCase():
			uniqueJob("Wardeness");
			slave.assignment = Job.WARDEN;
			slave.rules.living = LivingRule.LUXURIOUS;
			slave.rules.rest = RestRule.MID;
			break;

		case Job.RECRUITER:
			uniqueJob("Recruiter");
			slave.assignment = Job.RECRUITER;
			break;

		case Job.CONCUBINE.toLowerCase():
			uniqueJob("Concubine");
			slave.assignment = Job.CONCUBINE;
			slave.rules.living = V.masterSuiteUpgradeLuxury > 0 ? LivingRule.LUXURIOUS : LivingRule.NORMAL;
			break;

		case Job.HEADGIRL.toLowerCase():
			uniqueJob("HeadGirl");
			slave.assignment = Job.HEADGIRL;
			if (V.HGSuite === 1) {
				slave.rules.living = LivingRule.LUXURIOUS;
			}
			slave.rules.rest = RestRule.MID;
			V.HGTimeInGrade = 0;
			break;

		case Job.BODYGUARD.toLowerCase():
			uniqueJob("Bodyguard");
			slave.assignment = Job.BODYGUARD;
			if (V.dojo > 1) {
				slave.rules.living = LivingRule.LUXURIOUS;
			}
			if (V.pit && V.pit.bodyguardFights && V.pit.fighterIDs.includes(slave.ID)) { V.pit.fighterIDs.delete(slave.ID); }
			break;

		case Job.AGENT.toLowerCase():
		case Job.AGENTPARTNER.toLowerCase():
			slave.assignment = job;
			slave.useRulesAssistant = 0; /* non-visible roles exempt from Rules Assistant */
			WombCleanGenericReserve(slave, 'incubator', 9999);
			WombCleanGenericReserve(slave, 'nursery', 9999);
			if (job === Job.AGENT) {
				if (App.activeArcology().direction !== 0) { // never assign an agent to the player's arcology
					if (App.activeArcology().leaderID !== 0) {
						const oldAgent = getSlave(App.activeArcology().leaderID);
						if (oldAgent && oldAgent.assignment === Job.AGENT) {
							// this is an error...you should never be able to assign an agent over the top of another
							// but in order to prevent game state corruption, we are going to remove the old agent
							removeJob(oldAgent, oldAgent.assignment, false);
						}
					}
					App.activeArcology().leaderID = slave.ID;
					App.activeArcology().government = "your agent";
				}
			}
			break;

		case Job.CHOICE.toLowerCase():
			slave.assignment = job;
			slave.choosesOwnAssignment = 1;
			break;

		default:
			slave.assignment = job; /* removeJob already set choosesOwnAssignment = 0 */
			break;
	}

	V.JobIDMap[restingAssignment].delete(slave.ID);
	V.JobIDMap[slave.assignment].add(slave.ID);

	if (!assignmentVisible(slave) && Array.isArray(V.personalAttention)) {
		if (V.personalAttention.deleteWith(s => s.ID === slave.ID).length > 0) {
			if (V.personalAttention.length === 0) {
				if (V.PC.career === "escort") {
					V.personalAttention = "whoring";
				} else if (V.PC.career === "servant") {
					V.personalAttention = "upkeep";
				} else {
					V.personalAttention = "business";
				}
				r += `${slave.slaveName} no longer has your personal attention; you plan to focus on ${V.personalAttention}.`;
			} else {
				r += `${slave.slaveName} no longer has your personal attention.`;
			}
		}
	}
	// maybe recreate it each time? V.JobIDArray = makeJobIdMap();
	if (idx >= 0) { V.slaves[idx] = slave; }

	return r;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {FC.Assignment} assignmentStr
 */
globalThis.assignJobSafely = function(slave, assignmentStr) {
	if (V.assignmentRecords[slave.ID] === Job.CHOICE) {
		assignJob(slave, Job.REST);
		slave.choosesOwnAssignment = 1;
	} else if ([Job.AGENT, Job.AGENTPARTNER].includes(V.assignmentRecords[slave.ID])) { // it is NEVER safe to auto-reassign agents (we don't know which arcology they came from)
		assignJob(slave, Job.REST);
	} else if (!App.Utils.jobForAssignment(assignmentStr).canEmploy(slave).length) {  // If nothing complains about job requirements not being met
		assignJob(slave, assignmentStr);
	} else {
		assignJob(slave, Job.REST);
	}
	// Whether they manage to go back or they default to rest, we don't need their record:
	if (V.assignmentRecords[slave.ID]) {
		delete V.assignmentRecords[slave.ID];
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {FC.Assignment} assignment
 * @param {boolean} [saveRecord=false]
 * @returns {string}
 */
globalThis.removeJob = function(slave, assignment, saveRecord = false) {
	if (!slave) {
		// it is well-formed, but does nothing, to remove an assignment from nobody.
		// this lets us call <<run removeJob(_S.HeadGirl, Job.HEADGIRL)>> and similar,
		// without first checking to see whether a slave is really assigned to _S.HeadGirl or not.
		return '';
	}

	let r = "";

	if (V.assignmentRecords[slave.ID] && (saveRecord === false)) {
		delete V.assignmentRecords[slave.ID];
	}

	const idx = V.slaveIndices[slave.ID];

	if (assignment === Job.PIT) {
		if (V.pit) {
			V.pit.fighterIDs.delete(slave.ID);
			V.JobIDMap[Job.PIT].delete(slave.ID);
		} else {
			return;	// TODO: should this return or just continue?
		}
	} else if (assignment === Job.LURCHER) {
		if (V.LurcherID === slave.ID) {
			V.LurcherID = 0;
		}
		V.JobIDMap[Job.LURCHER].delete(slave.ID);
	} else {
		if (V.JobIDMap.hasOwnProperty(assignment)) {
			V.JobIDMap[assignment].delete(slave.ID);
		} else {
			V.JobIDMap[slave.assignment].delete(slave.ID);
		}

		if (slave.ID === V.HeadGirlID) {
			V.HeadGirlID = 0;
		} else if (slave.ID === V.RecruiterID) {
			V.RecruiterID = 0;
			V.recruiterIOUs = 0;
		} else if (slave.ID === V.BodyguardID) {
			V.BodyguardID = 0;
		} else if (slave.ID === V.MadamID) {
			V.MadamID = 0;
		} else if (slave.ID === V.djID) {
			V.djID = 0;
		} else if (slave.ID === V.MilkmaidID) {
			V.MilkmaidID = 0;
		} else if (slave.ID === V.FarmerID) {
			V.FarmerID = 0;
		} else if (slave.ID === V.SchoolteacherID) {
			V.SchoolteacherID = 0;
		} else if (V.AttendantID === slave.ID) {
			V.AttendantID = 0;
		} else if (slave.ID === V.MatronID) {
			V.MatronID = 0;
		} else if (slave.ID === V.NurseID) {
			V.NurseID = 0;
		} else if (slave.ID === V.StewardessID) {
			V.StewardessID = 0;
		} else if (slave.ID === V.WardenessID) {
			V.WardenessID = 0;
		} else if (slave.ID === V.ConcubineID) {
			V.ConcubineID = 0;
		} else if (slave.ID === V.StudID) {
			V.StudID = 0;
		}

		/* use .toLowerCase() to get rid of a few dupe conditions. */
		switch (assignment.toLowerCase()) {
			case Job.ARCADE.toLowerCase():
			case "arcade":
				slave.assignment = Job.GLORYHOLE;
				break;

			case Job.BROTHEL.toLowerCase():
			case "brothel":
				slave.assignment = Job.WHORE;
				break;

			case Job.CELLBLOCK.toLowerCase():
			case "cellblock":
				slave.assignment = Job.REST;
				if (slave.inflation > 0) {
					deflate(slave);
				}
				break;

			case Job.CLINIC.toLowerCase():
			case "clinic":
				slave.assignment = Job.REST;
				break;

			case Job.CLUB.toLowerCase():
			case "club":
				slave.assignment = Job.PUBLIC;
				break;

			case Job.DAIRY.toLowerCase():
			case "dairy":
				slave.assignment = Job.MILKED;
				if (V.dairyRestraintsSetting > 1) {
					slave.health.tired = 100;
				}
				if (V.dairyFeedersSetting > 0) {
					slave.diet = "healthy";
				}
				if (V.dairyHormonesSetting > 0) {
					slave.hormones = 0;
				}
				break;

			case Job.FARMYARD.toLowerCase():
			case "farmyard":
				slave.assignment = Job.REST;
				break;

			case Job.SCHOOL.toLowerCase():
			case "schoolroom":
				slave.assignment = Job.REST;
				break;

			case Job.SPA.toLowerCase():
			case "spa":
				slave.assignment = Job.REST;
				break;

			case Job.QUARTER.toLowerCase():
			case "servants' quarters":
			case "servantsquarters":
				slave.assignment = Job.HOUSE;
				break;

			case Job.MASTERSUITE.toLowerCase():
			case "master suite":
			case "mastersuite":
				slave.assignment = Job.FUCKTOY;
				break;

			case "live with your head girl":
			case "head girl suite":
			case "hgsuite":
				slave.assignment = Job.REST;
				break;

			case Job.NURSERY:
			case "nursery":
				slave.assignment = Job.REST;
				break;

			case "be your head girl":
				let attentionCheck = 1;
				slave.assignment = Job.REST;
				const HGSlave = V.slaves.find(s => s.assignment === Job.HEADGIRLSUITE);
				if (HGSlave) {
					removeJob(HGSlave, Job.HEADGIRLSUITE);
					if (V.HGSuiteEquality === 1 && HGSlave.devotion > 50) {
						assignJob(HGSlave, Job.HEADGIRL);
						V.HeadGirlID = HGSlave.ID;
						HGSlave.diet = "healthy";
						attentionCheck = 0;
					}
				}
				if (V.personalAttention === "HG" && attentionCheck === 1) {
					if (V.PC.career === "escort") {
						V.personalAttention = "whoring";
					} else if (V.PC.career === "servant") {
						V.personalAttention = "upkeep";
					} else {
						V.personalAttention = "business";
					}

					r += `You no longer have a slave assigned to be your Head Girl, so you turn your personal attention to focus on ${V.personalAttention}.`;
				}
				V.HGTimeInGrade = 0;
				break;

			case Job.AGENT:
			case Job.AGENTPARTNER:
				if (slave.assignment === Job.AGENT) {
					const arc = V.arcologies.find((a) => a.leaderID === slave.ID);
					if (arc) {
						arc.leaderID = 0;
						arc.government = "your trustees";
					}
				}
				slave.assignment = Job.REST;
				if (slave.relationshipTarget > 0) {
					/* following code assumes there can be at most one companion */
					const lover = V.slaves.find(s => haveRelationshipP(s, slave) && s.assignment === Job.AGENTPARTNER);
					if (lover) {
						removeJob(lover, Job.AGENTPARTNER, saveRecord);
					}
				}
				break;

			default:
				slave.assignment = Job.REST;
				break;
		}

		V.JobIDMap[slave.assignment].add(slave.ID);

		if (slave.rules.living === LivingRule.LUXURIOUS && !assignmentVisible(slave)) {
			slave.rules.living = LivingRule.NORMAL;
		}

		slave.choosesOwnAssignment = 0;
		slave.sentence = 0;
		slave.subTarget = 0;
	}
	// reset it each time? V.JobIDArray = makeJobIdMap();
	if (idx >= 0) {
		V.slaves[idx] = slave;
	}

	return r;
};

/**
 * Indicate whether a slave's current assignment is shown in Main
 * Often used as a proxy for "penthouse slave"
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.assignmentVisible = function(slave) {
	switch (slave.assignment) {
		/* normal out-of-penthouse jobs */
		case Job.ARCADE:
		case Job.BROTHEL:
		case Job.CELLBLOCK:
		case Job.CLINIC:
		case Job.CLUB:
		case Job.DAIRY:
		case Job.FARMYARD:
		case Job.HEADGIRLSUITE:
		case Job.MASTERSUITE:
		case Job.SCHOOL:
		case Job.QUARTER:
		case Job.SPA:
		case Job.NURSERY:
			return false;

		/* outside leadership jobs */
		case Job.ATTENDANT:
		case Job.MATRON:
		case Job.DJ:
		case Job.MADAM:
		case Job.MILKMAID:
		case Job.FARMER:
		case Job.NURSE:
		case Job.TEACHER:
		case Job.STEWARD:
		case Job.WARDEN:
		case Job.CONCUBINE:
			return false;

		/* agents are not in the arcology at all */
		case Job.AGENT:
		case Job.AGENTPARTNER:
			return false;

		/* transition state */
		case Job.CHOICE:
			return true; // show
	}

	/* all other jobs are shown in penthouse */
	return true;
};

/**
 * @returns {Record<FC.Assignment, Set<number>>} dictionary assignment -> slave IDs
 */
globalThis.makeJobIdMap = function() {
	/** @type {Object.<FC.Assignment, Set<number>>} */
	const res = {};
	for (const jn of Object.values(Job)) {
		res[jn] = new Set();
	}

	for (const slave of V.slaves) {
		res[slave.assignment].add(slave.ID);
	}

	// special cases
	if (V.pit) {
		res[Job.PIT] = new Set(V.pit.fighterIDs);
	}
	res[Job.LURCHER].add(V.LurcherID);

	return res;
};

/**
 * Generates string with links for changing slave assignment
 */
App.UI.jobLinks = function() {
	"use strict";
	const facilitiesOrder = [
		/* sorted by improvement before work, within improvement in order of progress, within work alphabetical for facilities*/
		App.Entity.facilities.penthouse,
		App.Entity.facilities.cellblock,
		App.Entity.facilities.nursery,
		App.Entity.facilities.schoolroom,
		App.Entity.facilities.clinic,
		App.Entity.facilities.spa,
		App.Entity.facilities.arcade,
		App.Entity.facilities.brothel,
		App.Entity.facilities.club,
		App.Entity.facilities.dairy,
		App.Entity.facilities.farmyard,
		App.Entity.facilities.masterSuite,
		App.Entity.facilities.servantsQuarters
	];

	return {
		assignmentsFragment: assignmentsFragment,
		transfersFragment: transfersFragment
	};

	/**
	 * Generates assignment links
	 * @param {number} ID
	 * @param {string} passage
	 * @param {assignmentCallback} [callback]
	 * @returns {DocumentFragment}
	 */
	function assignmentsFragment(ID, passage, callback) {
		let penthouseJobs = App.Entity.facilities.penthouse.assignmentLinkElements(ID, undefined, passage, callback);
		const slave = slaveStateById(ID);
		const sp = getPronouns(slave);

		if (slave.fuckdoll === 0) {
			const assignment = Job.CHOICE;
			if (slave.assignment !== assignment) {
				penthouseJobs.push(App.UI.DOM.assignmentLink(slave, assignment, passage, callback, `Let ${sp.object} choose`));
			}
		} else {
			penthouseJobs.push(App.UI.DOM.disabledLink(`Let ${sp.object} choose`, ["Fuckdolls can't choose their job"]));
		}
		let res = document.createDocumentFragment();
		// there is always at least one job
		res.appendChild(penthouseJobs[0]);
		for (let i = 1; i < penthouseJobs.length; ++i) {
			res.appendChild(document.createTextNode(" | "));
			res.appendChild(penthouseJobs[i]);
		}
		return res;
	}

	/**
	 * Generates transfer links
	 * @param {number} ID
	 * @param {assignmentCallback} [callback]
	 * @returns {DocumentFragment}
	 */
	function transfersFragment(ID, callback) {
		/** @type {HTMLElement[]} */
		const transfers = [];
		const slave = slaveStateById(ID);

		for (const f of facilitiesOrder) {
			if (!f.established || f.jobs.length === 0) { continue; }
			const rejects = f.canHostSlave(slave);
			if (rejects.length === 0) {
				transfers.push(f.transferLinkElement(ID, undefined, passage(), callback));
			} else {
				transfers.push(App.UI.DOM.disabledLink(f.genericName, rejects));
			}
		}

		let res = document.createDocumentFragment();
		// there is always at least one job
		res.appendChild(transfers[0]);
		for (let i = 1; i < transfers.length; ++i) {
			res.appendChild(document.createTextNode(" | "));
			res.appendChild(transfers[i]);
		}
		return res;
	}
}();

App.activeArcology = function() {
	return V.arcologies[V.activeArcologyIdx];
};

App.currentAgent = function(arcology) {
	return getSlave(V.arcologies[arcology].leaderID);
};

/**
 * Remove all workers from the facility changing their assignments
 * @param {App.Entity.Facilities.Facility} facility
 * @param {FC.Assignment} [managerAssignment="rest"] new assignment for the facility manager
 * @param {FC.Assignment} [workerAssignment="rest"] new assignment for the facility workers
 */
App.Utils.moveFacilityWorkers = function(facility, managerAssignment = Job.REST, workerAssignment = Job.REST) {
	if (facility.manager && facility.manager.currentEmployee) {
		assignJob(facility.manager.currentEmployee, managerAssignment);
	}

	for (const w of facility.employees()) {
		assignJob(w, workerAssignment);
	}
};

App.Utils.jobForAssignment = function() {
	const map = new Map();
	function fillMap() {
		/**
		 * @param {Map} m
		 * @param {App.Entity.Facilities.Facility} f
		 */
		function addFacility(m, f) {
			if (f.manager) {
				m.set(f.desc.manager.assignment, f.manager);
			}
			for (const j of f.jobsNames) {
				m.set(f.desc.jobs[j].assignment, f.job(j));
			}
		}

		for (const f in App.Entity.facilities) {
			if (f.length > 0) {
				addFacility(map, App.Entity.facilities[f]);
			}
		}
	}

	/**
	 * @param {FC.Assignment} assignment
	 * @returns {App.Entity.Facilities.Job}
	 */
	function getJob(assignment) {
		if (map.size === 0) {
			fillMap();
		}
		const res = map.get(assignment);
		if (!res) {
			throw Error(`Can't find job object for assignment '${assignment}'`);
		}
		return res;
	}
	return getJob;
}();

/** Assign a slave, play the appropriate assignment scene if necessary, and redirect to a destination passage.
 * @param {App.Entity.SlaveState} slave
 * @param {FC.Assignment} assignTo
 * @param {string} passage
 */
globalThis.assignmentTransition = function(slave, assignTo, passage) {
	/** @param {string} scene */
	function detourThroughScene(scene) {
		V.returnTo = passage;
		Engine.play(scene);
	}

	V.AS = slave.ID;
	assignJob(slave, assignTo);
	if (V.showAssignToScenes === 1 && slave.fetish !== "mindbroken") {
		if (assignTo === Job.DAIRY && V.dairyRestraintsSetting >= 2 && ((V.dairyStimulatorsSetting >= 2) || (V.dairyFeedersSetting >= 2) || (V.dairyPregSetting >= 2))) {
			detourThroughScene("Industrial Dairy Assignment Scene");
		} else if (assignTo === Job.DAIRY && (V.dairyRestraintsSetting === 0 && slave.devotion > 0)) {
			detourThroughScene("Free Range Dairy Assignment Scene");
		} else if (assignTo === Job.BROTHEL) {
			detourThroughScene("Brothel Assignment Scene");
		} else {
			Engine.play(passage);
		}
	} else {
		Engine.play(passage);
	}
};

/**
 * Creates a link that will remove all slaves from a facility, including manager.
 * @param {string} facilityName
 * @param {FC.Assignment} [managerAssignment]
 * @param {FC.Assignment} [workerAssignment]
 * @returns {DocumentFragment}
 */
globalThis.removeFacilityWorkers = function(facilityName, managerAssignment, workerAssignment) {
	const facility = App.Entity.facilities[facilityName];
	const count = facility.totalEmployeesCount;
	const frag = new DocumentFragment();
	if (count > 0) {
		frag.append(
			App.UI.DOM.link(
				"Remove all slaves",
				() => {
					App.Utils.moveFacilityWorkers(facility, managerAssignment, workerAssignment);
					penthouseCensus();
				},
				[],
				passage(),
			)
		);
		if ((count + V.dormitoryPopulation) > V.dormitory) {
			App.UI.DOM.appendNewElement("span", frag, ` Dormitory capacity will be exceeded.`, "red");
		}
	}
	return frag;
};

repo_name: Welptard/fc

license: mit