code:
/**
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
globalThis.isSexuallyPure = function(slave) {
	if (!slave) {
		return null;
	}
	return (slave.vagina < 1 && slave.anus < 1 && !slave.counter.anal && !slave.counter.vaginal && !slave.counter.oral && !slave.counter.mammary && !slave.counter.penetrative && !slave.counter.publicUse);
};

/**
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
globalThis.isSlaveAvailable = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.assignment === Job.AGENT) {
		return false;
	} else if (slave.assignment === Job.AGENTPARTNER) {
		return false;
	} else if (slave.assignment === Job.ARCADE) {
		return false;
	} else if (slave.assignment === Job.DAIRY && V.dairyRestraintsSetting >= 2) {
		return false;
	} else if (getPersonalAttention(slave.ID, "torture") && !onBedRest(V.PC, true)) {
		return false;
	}
	return true;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isFaceVisible = function(slave) {
	const obscuringClothes = ["a Fuckdoll suit", "restrictive latex", "a burqa", "a niqab and abaya", "a klan robe", "a slutty klan robe"].includes(slave.clothes); // Debatable whether "a cybersuit", "battlearmor", "Imperial Plate", should be added. Helmets could be off in short periods.
	const obscuringFaceAccessory = ["porcelain mask"].includes(slave.faceAccessory);
	return (!obscuringClothes && !obscuringFaceAccessory);
};

globalThis.SlaveStatsChecker = (function() {
	return {
		checkForLisp: hasLisp,
		isModded: isModded,
		isUnmodded: isUnmodded,
		modScore: modScore,
		hasImplants,
		hasAllImplants,
		isSurgicallyImproved,
	};

	/**
	 * call as SlaveStatsChecker.checkForLisp()
	 * @param {FC.HumanState} slave
	 */
	function hasLisp(slave) {
		if (V.disableLisping === 1 || !canTalk(slave)) {
			return false;
		}
		return (slave.lips > 70 || (slave.piercing.lips.weight + slave.piercing.tongue.weight > 2) || slave.teeth === "gapped");
	}

	/** call as SlaveStatsChecker.modScore()
	 * @param {FC.HumanState} slave
	 * @returns {{piercing: number, tat: number, brand: number, scar: number, total: number}}
	 */
	function modScore(slave) {
		const piercing = piercingScore(slave);
		const tat = tatScore(slave);
		const brand = brandScore(slave);
		const scar = scarScore(slave);
		return {
			piercing,
			tat,
			brand,
			scar,
			total: piercing + tat + brand + scar
		};
	}

	/**
	 * helper function, not callable
	 * @param {FC.HumanState} slave
	 * @returns {number}
	 */
	function piercingScore(slave) {
		let score = 0;

		for (const piercing in slave.piercing) {
			const impact = (["lips", "tongue", "nipple", "genitals", "vagina", "dick", "anus"].includes(piercing)) ? 0.25 : 0.5; // Piercings in sexual areas count more for piercing score.
			if (piercing === "genitals" && slave.piercing.genitals.smart) {
				score += 1.25;
			} else if (slave.piercing[piercing].weight > 0) {
				score += slave.piercing[piercing].weight * 0.75 - impact;
				if (["areola", "corset"].includes(piercing)) {
					score += 1; // bonus score for corset and areola piercings (which are actually many piercings each)
				}
			}
		}

		return score;
	}

	/**
	 * helper function, not callable
	 * @param {FC.HumanState} slave
	 * @returns {number}
	 */
	function tatScore(slave) {
		let score = 0;

		if (slave.boobsTat !== 0) {
			score += 1.25;
		}
		if (slave.buttTat !== 0) {
			score += 1.25;
		}
		if (slave.lipsTat !== 0) {
			score += 1.5;
		}
		if (slave.shouldersTat !== 0) {
			score += 1;
		}
		if (slave.backTat !== 0) {
			score += 1.25;
		}
		if (slave.armsTat !== 0) {
			score += 1;
		}
		if (slave.legsTat !== 0) {
			score += 1;
		}
		if (slave.stampTat !== 0) {
			score += 1;
		}
		if (slave.vaginaTat !== 0) {
			score += 1;
		}
		if (slave.dickTat !== 0) {
			score += 1;
		}
		if (slave.bellyTat !== 0) {
			if ((slave.preg > slave.pregData.normalBirth / 1.33 && slave.pregType >= 20) || slave.belly >= 300000) {
				score += 0.75;
			} else if ((slave.preg > slave.pregData.normalBirth / 2 && slave.pregType >= 20) || (slave.preg > slave.pregData.normalBirth / 1.33 && slave.pregType >= 10) || slave.belly >= 150000) {
				score += 1;
			} else if (slave.belly >= 10000 || slave.bellyImplant >= 8000) {
				score += 1;
			} else if ((slave.preg >= slave.pregData.normalBirth / 4 && slave.pregType >= 20) || (slave.preg > slave.pregData.normalBirth / 4 && slave.pregType >= 10) || slave.belly >= 5000) {
				score += 0.5;
			} else if (slave.belly >= 1500) {
				score += 0.25;
			} else {
				score += 0.1;
			}
		}
		if (slave.anusTat === "bleached") {
			score += 0.5;
		} else if (slave.anusTat !== 0) {
			score += 1.25;
		}
		if (slave.abortionTat > 0 || (slave.abortionTat === 0 && slave.pregKnown === 1)) {
			score += 1;
		}
		if (slave.birthsTat > 0 || (slave.birthsTat === 0 && slave.pregKnown === 1)) {
			score += 1;
		}
		return score;
	}

	/**
	 * helper function, not callable
	 * @param {FC.HumanState} slave
	 * @returns {number}
	 */
	function brandScore(slave) {
		let score = 0;
		const brands = App.Medicine.Modification.brandRecord(slave);
		score += Object.getOwnPropertyNames(brands).length;
		return score;
	}

	/**
	 * helper function, not callable
	 * @param {FC.HumanState} slave
	 * @returns {number}
	 */
	function scarScore(slave) {
		let score = 0;
		let scars;
		if (slave.hasOwnProperty("scar")) { /* For very old saves this may not be defined yet and blocks the save from loading. */
			scars = App.Medicine.Modification.scarRecord(slave);
			for (const bodypart of Object.keys(scars)) {
				for (const kind of Object.keys(scars[bodypart])) {
					score += scars[bodypart][kind];
				}
			}
		}
		return score;
	}

	/**
	 * call as SlaveStatsChecker.isModded()
	 * @param {FC.HumanState} slave
	 * @returns {boolean}
	 */
	function isModded(slave) {
		const tattoos = tatScore(slave);
		const piercings = piercingScore(slave);
		const brands = brandScore(slave);
		const scars = scarScore(slave);
		const mods = piercings + tattoos + scars;

		return (mods > 15 || (piercings > 8 && tattoos > 5) || brands > 5);
	}

	/**
	 * call as SlaveStatsChecker.isUnmodded()
	 * @param {FC.HumanState} slave
	 * @returns {boolean}
	 */
	function isUnmodded(slave) {
		const tattoos = tatScore(slave);
		const piercings = piercingScore(slave);
		const brands = brandScore(slave);
		const scars = scarScore(slave);

		return (!isModded(slave) && slave.piercing.corset.weight === 0 && piercings < 3 && tattoos < 1.5 && brands < 2 && scars <= 1);
	}

	/**
	 * call as SlaveStatsChecker.hasImplants(slave)
	 * @param {FC.HumanState} slave
	 * @returns {boolean}
	 */
	function hasImplants(slave){
		return slave.boobsImplant > 0 || slave.buttImplant > 0 || slave.lipsImplant > 0;
	}

	/**
	 * call as SlaveStatsChecker.hasImplants(slave)
	 * @param {FC.HumanState} slave
	 * @returns {boolean}
	 */
	function hasAllImplants(slave){
		return slave.boobsImplant > 0 && slave.buttImplant > 0 && slave.lipsImplant > 0;
	}

	/**
	 * call as SlaveStatsChecker.isSurgicallyImproved(slave)
	 * @param {FC.HumanState} slave
	 * @returns {boolean}
	 */
	function isSurgicallyImproved(slave){
		return globalThis.isSurgicallyImproved(slave);
	}
}());

/**
 * Returns if slave is considered slim or not by arcology standards.
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isSlim = function(slave) {
	let slim = false;
	const arcology = V.arcologies[0];

	if (arcology.FSSlimnessEnthusiastLaw === 1) {
		return (slimLawPass(slave) === 1);
	}
	if ((slave.boobs < 500) && (slave.butt < 3)) {
		if ((slave.muscles <= 30) && (!FutureSocieties.isActive('FSPhysicalIdealist', arcology)) && (slave.weight <= 10) && (!FutureSocieties.isActive('FSHedonisticDecadence', arcology))) {
			slim = true;
		} else if (FutureSocieties.isActive('FSPhysicalIdealist', arcology)) {
			if (((arcology.FSPhysicalIdealistStrongFat === 1) && (slave.weight <= 30)) || slave.weight <= 10) {
				slim = true;
			}
		} else if ((FutureSocieties.isActive('FSHedonisticDecadence', arcology)) && (slave.weight <= 30)) {
			if (arcology.FSHedonisticDecadenceStrongFat === 1 || slave.muscles <= 30) {
				slim = true;
			}
		}
	}
	return slim;
};

/**
 * Returns if slave is considered a fashionable body shape or not by arcology laws.
 * @param {FC.HumanState} slave
 * @returns {number} 1: yes, 0: no
 */
globalThis.genderLawPass = function(slave) {
	let genderLawPass = 1;

	const arcology = V.arcologies[0];

	if (!FutureSocieties.isActive('FSPhysicalIdealist', arcology) && arcology.FSHedonisticDecadenceStrongFat === 0 && slave.muscles > 30) {
		/* muscle check */
		genderLawPass = 0;
	}
	if (FutureSocieties.isActive('FSHedonisticDecadence', arcology) || arcology.FSPhysicalIdealistStrongFat === 1) {
		/* weight check */
		if (slave.weight > 130 || slave.weight <= -30) {
			genderLawPass = 0;
		}
	} else if (Math.abs(slave.weight) > 30) {
		genderLawPass = 0;
	}
	if (FutureSocieties.isActive('FSAssetExpansionist', arcology)) {
		if (slave.boobs < 1600 || slave.boobs > 4300 || slave.butt < 4 || slave.butt > 8) {
			genderLawPass = 0;
		}
	} else if (slave.boobs < 500 || slave.boobs > 800 || slave.butt < 3 || slave.butt > 4) {
		genderLawPass = 0;
	}

	return genderLawPass;
};

/**
 * Returns if slave is considered slim or not by Slimness Enthusiast Law.
 * @param {FC.HumanState} slave
 * @returns {number} 1: yes, 0: no
 */
globalThis.slimLawPass = function(slave) {
	let slimLawPass = 0;
	const arcology = V.arcologies[0];

	if (arcology.FSSlimnessEnthusiastLaw === 1) {
		if ((slave.boobs < 300) && (slave.butt <= 1) && (slave.waist <= 10)) {
			if ((!FutureSocieties.isActive('FSPhysicalIdealist', arcology)) && (arcology.FSHedonisticDecadenceStrongFat === 0) && (slave.muscles > 30)) {
				/* muscle check*/
				slimLawPass = 0;
			} else if ((FutureSocieties.isActive('FSHedonisticDecadence', arcology)) || (arcology.FSPhysicalIdealistStrongFat === 1)) {
				slimLawPass = 1;
				/* weight check*/
				if (slave.weight > 30) {
					slimLawPass = 0;
				}
			} else if (slave.weight > 10) {
				slimLawPass = 0;
			} else {
				slimLawPass = 1;
			}
		}
	}

	return slimLawPass;
};

/**
 * Returns if slave is considered an acceptable height by arcology standards.
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.heightPass = function(slave) {
	const arcology = V.arcologies[0];

	if (FutureSocieties.isActive('FSPetiteAdmiration', arcology)) {
		if (arcology.FSPetiteAdmirationLaw2 === 1) {
			if (slave.height < Height.mean(slave) - 5) {
				return true;
			}
		} else {
			if (slave.height < 160) {
				return true;
			}
		}
	} else if (FutureSocieties.isActive('FSStatuesqueGlorification', arcology)) {
		if (arcology.FSStatuesqueGlorificationLaw2 === 1) {
			if (slave.height + shoeHeight(slave) > Height.mean(slave) + 5) {
				return true;
			}
		} else {
			if (slave.height + shoeHeight(slave) >= 170) {
				return true;
			}
		}
	}
	return false;
};

/**
 * Returns slave bimbo body degree (FSIntellectualDependencyLawBeauty).
 * @param {FC.HumanState} slave
 * @returns {number}
 */
globalThis.bimboScore = function(slave) {
	let degree = 0;
	const modScore = SlaveStatsChecker.modScore(slave);

	if (slave.lips > 70) {
		degree++;
	}
	if ((slave.lipsImplant / slave.lips) >= 0.60) {
		degree++;
	}
	if (slave.boobs >= 2000) {
		degree++;
		if (slave.boobs >= 10000) {
			degree++;
		}
	}
	if ((slave.boobsImplant / slave.boobs) >= 0.60) {
		degree++;
	}
	if (slave.butt > 4) {
		degree++;
		if (slave.butt > 10) {
			degree++;
		}
	}
	if ((slave.buttImplant / slave.butt) >= 0.60) {
		degree++;
	}
	if (slave.belly >= 1500) {
		degree++;
		if (slave.belly >= 20000) {
			degree++;
		}
	}
	if (slave.waist <= -60) {
		degree++;
	}
	if (slave.hips > 1) {
		degree++;
		if (slave.hips > 2) {
			degree++;
		}
	}
	if (slave.dick > 3 && canPenetrate(slave)) {
		degree++;
		if (slave.balls > 5) {
			degree++;
		}
	}
	if (slave.hLength >= 100) {
		degree++;
	}
	if (slave.makeup > 1 && slave.nails > 1) {
		degree++;
	}
	if (modScore.total >= 10) {
		degree++;
	}
	if (modScore.piercing > 5) {
		degree++;
	}
	if (modScore.tat > 3) {
		degree++;
	}
	degree += shoeHeelCategory(slave);
	if (slave.skin === "sun tanned" || slave.skin === "spray tanned") {
		degree++;
	}
	if (getExposure(slave) === 2) {
		degree++;
	}

	// degree can go far higher than the max to allow various FS combinations to take advantage.
	// degree is capped at 6 logically, but this function return oob values for the RA to work with.
	return degree;
};

/**
 * Returns if slave is considered stacked (big T&A) or not.
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isStacked = function(slave) {
	return (slave.butt > 4) && (slave.boobs > 800);
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isXY = function(slave) {
	return (slave.dick > 0);
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isYoung = function(slave) {
	return (slave.visualAge < 30);
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isPreg = function(slave) {
	return ((slave.bellyPreg >= 5000) || (slave.bellyImplant >= 5000));
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isNotPreg = function(slave) {
	return (!isPreg(slave) && (slave.belly < 100) && (slave.weight < 30) && !App.Data.misc.fakeBellies.includes(slave.bellyAccessory));
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isPure = function(slave) {
	return ((slave.boobsImplant === 0) && (slave.buttImplant === 0) && (slave.waist >= -95) && (slave.lipsImplant === 0) && (slave.faceImplant < 30) && (slave.bellyImplant === -1) && (Math.abs(slave.shouldersImplant) < 2) && (Math.abs(slave.hipsImplant) < 2));
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isVirile = function(slave) {
	return (slave.balls > 0 && slave.pubertyXY === 1 && slave.vasectomy === 0 && slave.ballType !== "sterile");
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isSurgicallyImproved = function(slave) {
	return ((slave.boobsImplant > 0) && (slave.buttImplant > 0) && (slave.waist < -10) && (slave.lipsImplant > 0));
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isFullyPotent = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.dick > 0 && slave.balls > 0 && slave.ballType !== "sterile" && slave.hormoneBalance < 100 && slave.drugs !== "hormone blockers" && slave.pubertyXY === 1) {
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean} Whether the slave is considered at least smart
 */
globalThis.isSmart = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.intelligence > 15) {
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean} Whether the slave is considered at least slow
 */
globalThis.isStupid = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.intelligence < -15) {
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean} Whether the slave is a part of the "superior" race
 */
globalThis.isSuperiorRace = function(slave) {
	const arcology = V.arcologies[0];

	if (!slave || !FutureSocieties.isActive('FSSupremacist', arcology)) {
		return null;
	} else {
		if (slave.race === arcology.FSSupremacistRace) {
			return true;
		}
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean} Whether the slave is a part of the "inferior" race
 */
globalThis.isInferiorRace = function(slave) {
	const arcology = V.arcologies[0];

	if (!slave || !FutureSocieties.isActive('FSSubjugationist', arcology)) {
		return null;
	} else {
		if (slave.race === arcology.FSSubjugationistRace) {
			return true;
		}
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {number} Whether the slave is a breeder for the Elites
 */
globalThis.isEliteBreeder = function(slave) {
	return slave.breedingMark;
};

/**
 * @param {FC.HumanState} slave
 * @param {boolean} ignoreCycle ignoreCycle=true will not take into account menstruation
 * @returns {boolean}
 */
globalThis.canGetPregnant = function(slave, ignoreCycle = false) {
	if (!slave) {
		return null;
	} else if (slave.preg === -1) { /* contraceptives check */
		return false;
	} else if (!isFertile(slave, ignoreCycle)) { /* check other fertility factors */
		return false;
	} else if ((slave.ovaries === 1) && (canDoVaginal(slave))) {
		return true;
	} else if ((slave.mpreg === 1) && (canDoAnal(slave))) {
		/* pregmod */
		return true;
	}
	return false;
};

/**
 * contraceptives (.preg === -1) do not negate this function
 * @param {FC.HumanState} slave
 * @param {boolean} ignoreCycle ignoreCycle=true will not take into account menstruation
 * @returns {boolean}
 */
globalThis.isFertile = function(slave, ignoreCycle=false) {
	if (!slave) {
		return null;
	}

	if (slave.womb.length > 0 && slave.geneticQuirks.superfetation < 2) {
		/* currently pregnant without superfetation */
		return false;
	} else if (slave.broodmother > 0) {
		/* currently broodmother */
		return false;
	} else if (slave.preg < -1) {
		/* sterile */
		return false;
	} else if (slave.pregWeek < 0) {
		/* postpartum */
		return false;
	} else if (slave.pubertyXX === 0) {
		return false;
	} else if (slave.ovaryAge >= 47) {
		return false;
	} else if (slave.inflation > 2) {
		return false;
	} else if (slave.bellyImplant >= 0) {
		return false;
	} else if (slave.mpreg === 1 || slave.ovaries === 1) {
		if (slave.womb.length > 0) { // superfetation route
			if (slave.fertPeak > 0) {
				return false;
			}
		}
		// Menstruation route.
		if (!ignoreCycle && V.menstruation === 1 && slave.fertPeak > 0 ) {
			return false;
		}
		return true;
	}
	return false;
};

/**
 * returns the maximum dick size the slave can maintain erect
 * @param {FC.HumanState} slave
 * @returns {number}
 */
globalThis.maxErectionSize = function(slave) {
	const dickBodyRatio = 2.0; // dick to body ratio normalized to an average height of 165 and an average dick size of 3 ==> with height 165 a dick size of 3 is 1.0 and a size of 6 is 2.0
	if (V.maxErectionSizeOption === 1) { // only height
		/**
		 * Max height erection support ranges (cms)
		 * 1: 28
		 * 2: 55
		 * 3: 83
		 * 4: 110 (min height is 85, so this is the current floor)
		 * 5: 138
		 * 6: 165
		 * 7: 193
		 * 8: 220
		 * 9: 248
		 */
		return Math.floor(dickBodyRatio * ((slave.height / 165) * 3));
	} else if (V.maxErectionSizeOption === 2) { // height and health
		// muscles modify [-25%, 25%], weight modify [-5%, 5%], health modify [-25%, 25%]
		return Math.floor((dickBodyRatio * ((slave.height / 165) * 3)) * (1 + (slave.muscles / 400)) * (1 + (slave.weight / 2000)) * (1 + (slave.health.health / 400)));
	}
	return 6;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canAchieveErection = function(slave) {
	if (!slave) {
		return null;
	} else {
		const maxErection = maxErectionSize(slave);
		if (slave.dick <= 0) { // needs dick to be erect
			return false;
		} else if (slave.dick <= 10 && slave.drugs === "priapism agents") { // dick size 11 has a dickBodyRatio of 3.3333.... Note: The amount of extra changes needed for sizes > 10 tells me to just cap it at a max size of 10.
			return true;
		} else if (slave.dick > maxErection) {
			return false;
		} else if (slave.aphrodisiacs > 1 || (slave.inflationType === "aphrodisiac" && slave.inflation >= 2)) {
			return true;
		} else if (slave.ballType === "sterile") {
			return false;
		} else if ((slave.balls > 0 ? slave.hormoneBalance < 100 : slave.hormoneBalance <= -100) && slave.drugs !== "hormone blockers") {
			return true;
		} else if (slave.aphrodisiacs > 0 || slave.inflationType === "aphrodisiac") {
			return true;
		}
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canPenetrate = function(slave) {
	if (!slave) {
		return null;
	/*
	} else if (slave.belly >= 300000) {
		return false;
	*/
	} else if (slave.clit >= 3 && !slave.chastityPenis && V.experimental.clitoralPenetration) {
		return true;
	} else if (!canAchieveErection(slave)) {
		return false;
	} else if (slave.chastityPenis === 1) {
		return false;
	} else if (slave.dick > 7) {
		return false;
	}
	return true;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canPenetrateThroat = function(slave) {
	if (!slave) {
		return null;
	}
	if (!canPenetrateAny(slave)) {
		return false;
	}
	if (slave.dick >= 5 || (slave.dick >= 3 && canAchieveErection(slave))) { // Normal dicks can penetrate throat, large soft ones can too
		return true;
	}
	if (slave.clit >= 4) { // Penis-like clits can penetrate throat
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canPenetrateAny = function(slave) {
	if (!slave) {
		return null;
	}
	return canPenetrate(slave) || (slave.clit >= 3 && slave.chastityVagina === 0);
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canSee = function(slave) {
	if (!slave) {
		return null;
	}
	return (getBestVision(slave)) > 0;
};

/**
 *
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canSeePerfectly = function(slave) {
	if (!canSee(slave)) {
		return false;
	}

	if (getBestVision(slave) === 2 && (slave.eyewear === "blurring glasses") || (slave.eyewear === "blurring contacts")) {
		// could see perfectly, but being blurred
		return false;
	} else if (getBestVision(slave) < 2 && !(slave.eyewear === "corrective glasses" || slave.eyewear === "corrective contacts")) {
		// can't see perfectly and not corrected
		return false;
	}
	return true;
};

/**
 * @param {FC.HumanState} actor
 * @returns {boolean | null} returns null if the actor doesn't exist, otherwise returns true if the slave has working (natural or implant) top ears.
 * Does not check if the ears are deafened/deaf. Use canHear() for that
 */
globalThis.workingTopEars = (actor) => {
	if (!actor) { return null; }
	if (actor.earT === "none") { return false; } // no top ears
	return (actor.earImplant === 1 || actor.earTNatural === 1);
};

/**
 * @param {FC.HumanState} actor
 * @returns {boolean | null} returns null if the actor doesn't exist, otherwise returns a boolean
 */
globalThis.canHear = function(actor) {
	if (!actor) {
		return null;
	}
	if (actor.earwear === "deafening ear plugs") {
		return false;
	} else if (actor.earShape === "none" && !workingTopEars(actor)) {
		return false;
	} else if (actor.hears === -2) {
		return false;
	}
	return true;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canSmell = function(slave) {
	if (!slave) {
		return null;
	}
	return (slave.smells > -1);
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canTaste = function(slave) {
	if (!slave) {
		return null;
	}
	return (slave.tastes > -1);
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canHold = function(slave) {
	if (!slave) {
		return null;
	} else if (hasAnyQuadrupedArms(slave)) {
		return false;
	}
	return hasAnyArms(slave);
};

/** If a slave can walk, she can move and stand.
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canWalk = function(slave) {
	if (!slave) {
		return null;
	} else if (!hasBothLegs(slave)) {
		return false;
	} else if (!isQuadrupedal(slave) && hasAnyQuadrupedLegs(slave)) {
		return false;
	} else if (tooFatSlave(slave)) {
		return false;
	} else if (tooBigBreasts(slave)) {
		return false;
	} else if (tooBigDick(slave)) {
		return false;
	} else if (tooBigBalls(slave)) {
		return false;
	} else if (tooBigButt(slave)) {
		return false;
	} else if (tooBigBelly(slave)) {
		return false;
	} else if (slave.heels === 1 && shoeHeelCategory(slave) < 2) {
		return false;
	}
	// player exclusives
	const player = asPlayer(slave);
	if (player) {
		if (player.physicalImpairment > 1) {
			return false;
		}
	}
	return true;
};

/** If a slave can stand, she can move, but not necessarily walk.
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canStand = function(slave) {
	if (!slave) {
		return null;
	/*
	} else if (!canMove(slave)) {
		return false;
	*/
	} else if (!hasAnyLegs(slave)) {
		return false;
	} else if (hasAnyQuadrupedLegs(slave)) {
		return false;
	} else if (!hasBothLegs(slave) && shoeHeelCategory(slave) > 1) {
		return false; // Can't stand on one leg while in heels
	} else if (slave.heels === 1 && shoeHeelCategory(slave) === 0) {
		return false;
	} else if (tooFatSlave(slave)) {
		return false;
	} else if (tooBigBreasts(slave)) {
		return false;
	} else if (tooBigBelly(slave)) {
		return false;
	}
	// player exclusives
	const player = asPlayer(slave);
	if (player) {
		if (player.physicalImpairment > 1) {
			return false;
		}
	}
	return true;
};

/** can the actor move at all?
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canMove = function(slave) {
	if (!slave) {
		return false;
	} else if (canWalk(slave)) {
		return true; // If she can walk, no point to even check anything after
	} else if (!hasAnyLegs(slave)) {
		return false;
	} else if (!hasBothQuadrupedLegs(slave) && hasAnyQuadrupedLegs(slave)) {
		return false;
	} else if (tooFatSlave(slave)) {
		return false;
	} else if (slave.physicalAge <= 3) {
		if (slave.boobs > 25000 + (slave.muscles * 20)) {
			return false;
		}
		if (slave.belly >= 150000) {
			return false;
		}
		if (slave.balls >= 30 + (slave.muscles * 0.3)) {
			return false;
		}
	} else if (slave.physicalAge <= 12) {
		if (slave.boobs > 100000 + (slave.muscles * 50)) {
			return false;
		}
		if (slave.belly >= 300000) {
			return false;
		}
		if (slave.balls >= 60 + (slave.muscles * 0.5)) {
			return false;
		}
	} else if (slave.physicalAge < 18) {
		if (slave.boobs > 250000 + (slave.muscles * 100)) {
			return false;
		}
		if (slave.belly >= 600000) {
			return false;
		}
		if (slave.balls >= 90 + (slave.muscles * 0.7)) {
			return false;
		}
	} else if (slave.physicalAge >= 18) {
		if (slave.boobs > 500000 + (slave.muscles * 200)) {
			return false;
		}
		if (slave.belly >= 1000000) {
			return false;
		}
		if (slave.balls >= 90 + (slave.muscles * 0.7)) {
			return false;
		}
	}
	// player exclusives
	const player = asPlayer(slave);
	if (player) {
		if (player.physicalImpairment > 1) {
			return false;
		}
	}
	return true;
};

/** Assuming the actor can walk, is their movement hindered by anything? Intended for the player, but could be applied to slaves.
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isHindered = function(slave) {
	const averageHeight = Height.mean(slave);
	if (!slave) {
		return null;
	} else if (!canWalk(slave) && canMove(slave)) {
		return true;
	} else if (!isQuadrupedal(slave) && hasAnyQuadrupedLegs(slave)) {
		return true;
	} else if (hasBothQuadrupedLegs(slave) && !hasBothQuadrupedArms(slave)) {
		return true;
	} else if (!hasBothQuadrupedLegs(slave) && hasAnyQuadrupedLegs(slave) && !hasBothQuadrupedArms(slave) && hasAnyQuadrupedArms(slave)) {
		return true;
	} else if (slave.weight >= 130 || (slave.weight >= 95 + ((slave.physicalAge - 9) * 5))) {
		return true;
	} else if (slave.muscles > 95 && slave.height <= (averageHeight + 10)) {
		return true;
	} else if (slave.belly >= 60000 || slave.belly >= 60000 / (1 + Math.pow(Math.E, -0.4 * (slave.physicalAge - 14))) || slave.belly >= Math.max(10000, ((12500 / 19) * slave.height) - (1172500 / 19))) {
		return true;
	} else if (slave.boobs > 5000) {
		return true;
	} else if (slave.balls >= 14) {
		return true;
	} else if (slave.hips > 2) {
		return true;
	} else if (slave.butt > 6) {
		return true;
	} else if (slave.muscles < -30) {
		return true;
	}
	// player exclusives
	const player = asPlayer(slave);
	if (player) {
		if (onBedRest(player)) {
			return true;
		} else if (player.physicalImpairment !== 0) {
			return true;
		// } else if (player.energy > 95 || player.need > 0) {
		//	return true;
		}
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @param {boolean} checkLanguage Does a bad accent count as being unable to speak?
 * @returns {boolean}
 */
globalThis.canTalk = function(slave, checkLanguage = true) {
	if (!slave) {
		return null;
	} else if (checkLanguage && slave.accent > 2) {
		return false;
	} else if (slave.voice === 0) {
		return false;
	} else if (slave.lips > 95) {
		return false;
	} else if (slave.mouthAccessory === "dildo gag") {
		return false;
	} else if (slave.mouthAccessory === "massive dildo gag") {
		return false;
	} else if (slave.mouthAccessory === "ball gag") {
		return false;
	} else if (slave.mouthAccessory === "bit gag") {
		return false;
	} else if (slave.mouthAccessory === "ring gag") {
		return false;
	}
	return true;
};

/**
 * Returns false if the slave has something blocking their mouth
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canDoOral = function(slave) {
	if (!slave) {
		return null;
	} else if (typeof slave.mouthAccessory === "string" && !["none", ""].includes(slave.mouthAccessory)) {
		return false;
	}
	return true;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canDoAnal = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.chastityAnus === 1) {
		return false;
	}
	return true;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canDoVaginal = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.vagina < 0) {
		return false;
	} else if (slave.chastityVagina === 1) {
		return false;
	}
	return true;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.tooFatSlave = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.weight > 130 + (slave.muscles / 20) && slave.physicalAge <= 3) {
		return true;
	} else if (slave.weight > 160 + (slave.muscles / 15) && slave.physicalAge <= 12) {
		return true;
	} else if (slave.weight > 185 + (slave.muscles / 10) && slave.physicalAge < 18) {
		return true;
	} else if (slave.weight > 190 + (slave.muscles / 5)) {
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.tooBigBreasts = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.boobs > 5000 + (slave.muscles * 20) && slave.physicalAge <= 3) {
		return true;
	} else if (slave.boobs > 10000 + (slave.muscles * 50) && slave.physicalAge <= 12) {
		return true;
	} else if (slave.boobs > 25000 + (slave.muscles * 100) && slave.physicalAge < 18) {
		return true;
	} else if (slave.boobs > 40000 + (slave.muscles * 200)) {
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.tooBigBelly = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.belly >= 120000 + (slave.muscles * 500) && slave.physicalAge <= 3) { // 70k - 170k
		return true;
	} else if (slave.belly >= 150000 + (slave.muscles * 800) && slave.physicalAge <= 12) { // 70k - 230k
		return true;
	} else if (slave.belly >= 350000 + (slave.muscles * 1000) && slave.physicalAge < 18) { // 250k - 450k
		return true;
	} else if (slave.belly >= 450000 + (slave.muscles * 2000)) { // 250k - 650k
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.tooBigBalls = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.balls >= 30 + (slave.muscles * 0.3) && slave.physicalAge <= 3) {
		return true;
	} else if (slave.balls >= 60 + (slave.muscles * 0.5) && slave.physicalAge <= 12) {
		return true;
	} else if (slave.balls >= 90 + (slave.muscles * 0.7)) {
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.tooBigDick = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.dick >= 20 + (slave.muscles * 0.1) && slave.physicalAge <= 3 && slave.dick !== 0) { // 10 - 30
		return true;
	} else if (slave.dick >= 45 + (slave.muscles * 0.3) && slave.physicalAge <= 12) { // 15 - 75
		return true;
	} else if (slave.dick >= 68 + (slave.muscles * 0.4)) { // 28 - 108
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.tooBigButt = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.butt > 10 && slave.physicalAge <= 3) {
		return true;
	} else if (slave.butt > 14 && slave.physicalAge <= 12) {
		return true;
	} else if (slave.butt > 22) {
		return true;
	}
	return false;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isVegetable = function(slave) {
	if (!slave) {
		return false;
	}
	return (slave.fetish === Fetish.MINDBROKEN);
};

/**
 * Returns the hair color the slave was (or would be) born with.
 *
 * @param {FC.HumanState} slave
 * @returns {string}
 */
globalThis.getGeneticHairColor = function(slave) {
	if (slave.geneticQuirks.albinism === 2) {
		return slave.albinismOverride.hColor;
	}
	return slave.origHColor;
};

/**
 * Returns the skin color the slave was (or would be) born with.
 *
 * @param {FC.HumanState} slave
 * @returns {string}
 */
globalThis.getGeneticSkinColor = function(slave) {
	if (slave.geneticQuirks.albinism === 2) {
		return slave.albinismOverride.skin;
	}
	return slave.origSkin;
};

/**
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
globalThis.canBeReceptrix = function(slave) {
	return (
		(slave.ovaries > 0 || slave.mpreg > 0) &&
		isSlaveAvailable(slave) &&
		slave.preg >= 0 &&
		slave.preg < 4 &&
		slave.pregWeek >= 0 &&
		slave.pubertyXX === 1 &&
		slave.pregType < 12 &&
		slave.bellyImplant === -1 &&
		slave.broodmother === 0 &&
		slave.inflation <= 2 &&
		slave.physicalAge < 70
	);
};

/**
 * @param {FC.HumanState} slave
 * @returns {string}
 */
globalThis.milkFlavor = function(slave) {
	if (slave.milkFlavor === "none") {
		return ``;
	}
	return `${slave.milkFlavor}-flavored `;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canBeDeflowered = function(slave) {
	return (slave.vagina === 0 && canDoVaginal(slave)) || (slave.anus === 0 && canDoAnal(slave));
};

/**
 * A consolidated function for checking if an actor is likely to be aroused at any given moment.
 * @param {FC.HumanState} actor
 * @returns {boolean}
 */
globalThis.isHorny = function(actor) {
	return ((actor.prostate > 0 && actor.preg > 0 && actor.preg > actor.pregData.normalBirth * .75) ||
		(actor.preg > 0 && actor.preg > actor.pregData.normalBirth * .66 && actor.pregMood === 2) ||
		(actor.geneticQuirks.uterineHypersensitivity === 2 &&
			(actor.bellyPreg >= 10000) ||
			(actor.belly > (actor.pregAdaptation * 1000)) ||
			(actor.wombImplant === "restraint" && actor.belly >= 400000)
		) ||
		// (actor.need > 0) ||
		(actor.energy > 95) ||
		(actor.aphrodisiacs > 0) ||
		(actor.inflationType === "aphrodisiac") ||
		(actor.drugs === "priapism agents"));
};

/**
 * A consolidated function for checking if a slave is comfortable with underage partners.
 * @param {FC.SlaveState} actor
 * @returns {boolean}
 */
globalThis.acceptsUnderage = function(actor) { // should this consider slaves below an age threshold?
	return (actor.fetish === Fetish.MINDBROKEN ||
		(actor.behavioralQuirk === "sinful") ||
		(actor.devotion > 50) ||
		(actor.trust > 50) ||
		(actor.career === "a slave since birth"));
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isMindbroken = function(slave) {
	return slave.fetish === Fetish.MINDBROKEN;
};

/**
 * Checks if the slave is menstruating this week.
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.isMenstruating = function(slave) {
	return V.menstruation === 1 &&
		slave.geneticQuirks.superfetation !== 2 &&
		slave.broodmother === 0 &&
		(slave.preg === 0 || slave.preg === -1) &&
		slave.fertPeak === 2 &&
		isFertile(slave, true);
};

/**
 * Checks if fertility is known.
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.fertKnown = function(slave) {
	return (V.menstruationKnown === 1 && slave.fertKnown === 1) || V.menstruationKnown === 0;
};


code:
// cSpell:ignore plet, novemcentu, octocentu, septemcentu, sexcentu, quincentu, quadricentu, tricentu, bicentu, centu
// cSpell:ignore nonagint, octogint, sexagint, vigint, nonu, octu, septu, sextu, quintu, quadru
// cSpell:ignore akth, ekth, ikth, okth, ukth, ykth
// cSpell:ignore intimidatee, intimidater

globalThis.peacekeepersCanBeEstablished = function() {
	return V.invasionVictory > 0 && App.Events.effectiveWeek() > 48;
};

/**
 * @param {number} x
 * @param {number} minValue
 * @param {number} maxValue
 * @param {number} [defaultValue=0]
 * @returns {number}
 */
globalThis.variableAsNumber = function(x, minValue, maxValue, defaultValue = 0) {
	x = Number(x);
	if (isNaN(x)) {
		return defaultValue;
	}
	if (x < minValue) { // Works even if minValue is undefined.
		return minValue;
	}
	if (x > maxValue) { // Works even if maxValue is undefined.
		return maxValue;
	}
	return x;
};

if (typeof interpolate === "undefined") {
	globalThis.interpolate = function(x0, y0, x1, y1, x) {
		if (x <= x0) {
			return y0;
		} else if (x >= x1) {
			return y1;
		} else {
			return (x - x0) * ((y1 - y0) / (x1 - x0)) + y0;
		}
	};
}

/**
 * @param {any[]} arr
 * @param {any} val
 * @returns {any[]}
 */
globalThis.removeFromArray = function(arr, val) {
	for (let i = 0; i < arr.length; i++) {
		if (val === arr[i]) {
			return arr.splice(i, 1);
		}
	}
	return null;
};

/**
 * @param {any[]} arr
 * @param {any} callback
 * @param {any} thisArg
 * @returns {Array}
 */
globalThis.filterInPlace = function(arr, callback, thisArg) {
	let j = 0;

	arr.forEach(function(e, i) {
		if (callback.call(thisArg, e, i, arr)) {
			arr[j++] = e;
		}
	});

	arr.length = j;
	return arr;
};

/** pregmod: are slave2's sperm compatible with slave1's eggs?
 * @param {FC.HumanState} slave1
 * @param {FC.HumanState|FC.AnimalState} slave2
 * @returns {boolean}
 */
globalThis.canBreed = function(slave1, slave2) {
	if (!slave1 || !slave2) {
		return null;
	}
	return (slave1.eggType === slave2.ballType);
};

/** assuming slave1 is fertile, could slave2 impregnate slave1?
 * slave2 must have dick and balls with compatible sperm;
 * both slaves must not be in chastity; slave2 need not achieve erection
 * @param {FC.HumanState} slave1
 * @param {FC.HumanState} slave2
 * @param {boolean} ignoreCycle
 * @returns {boolean}
 */
globalThis.canImpreg = function(slave1, slave2, ignoreCycle = false) {
	if (!slave1 || !slave2) {
		return null;
	} else if (slave2.dick < 1) {
		return false;
	} else if (slave2.chastityPenis === 1) {
		return false;
	} else if (!isVirile(slave2)) {
		return false;
	} else if (!canBreed(slave1, slave2)) {
		return false; /* pregmod end */
	} else if (!canGetPregnant(slave1, ignoreCycle)) {
		/* includes chastity checks */
		return false;
	} else {
		return true;
	}
};

/** assuming slave1 is fertile, could slave2 impregnate slave1?
 * slave2 must have balls with compatible sperm;
 * a special function intended for tribbing scenes;
 * @param {FC.HumanState} slave1
 * @param {FC.HumanState} slave2
 * @param {boolean} ignoreCycle
 * @returns {boolean}
 */
globalThis.canFemImpreg = function(slave1, slave2, ignoreCycle = false) {
	if (!slave1 || !slave2) {
		return null;
	} else if (slave2.dick > 0) {
		return false;
	} else if (slave2.chastityVagina === 1) {
		return false;
	} else if (!isVirile(slave2)) {
		return false;
	} else if (!canBreed(slave1, slave2)) {
		return false; /* pregmod end */
	} else if (!canGetPregnant(slave1, ignoreCycle)) {
		/* includes chastity checks */
		return false;
	} else {
		return true;
	}
};

/** assuming slave1 is fertile, could slave2 impregnate slave1?
 * slave2 must have balls with compatible sperm;
 * a special function intended for nulls intent on fathering children;
 * @param {FC.HumanState} slave1
 * @param {FC.HumanState} slave2
 * @returns {boolean}
 */
globalThis.canNullImpreg = function(slave1, slave2) { // DO NOT USE YET!
	if (!slave1 || !slave2) {
		return null;
	} else if (slave2.dick > 0) {
		return false;
	} else if (slave2.vagina >= 0) {
		return false;
	} else if (slave2.chastityVagina === 1) {
		return false;
	} else if (!isVirile(slave2)) {
		return false;
	} else if (!canBreed(slave1, slave2)) {
		return false; /* pregmod end */
	} else if (!canGetPregnant(slave1)) {
		/* includes chastity checks */
		return false;
	} else if ((slave2.chastityAnus !== 0 && slave2.anus > 0 && slave2.prostate > 0) || (slave2.prostateImplant === "stimulator")) {
		return false;
	} else {
		return true;
	}
};

/**
 * The amount of milk a slave produces in liters/week
 * @param {FC.HumanState} slave
 * @returns {number}
 */
globalThis.milkAmount = function(slave) {
	let milk;
	let calcs = App.Medicine.fleshSize(slave, 'boobs');
	if (calcs > 40000) {
		milk = (158 + ((calcs - 40000) / 600));
	} else if (calcs > 25000) {
		milk = (128 + ((calcs - 25000) / 500));
	} else if (calcs > 10000) {
		milk = (78 + ((calcs - 10000) / 300));
	} else if (calcs > 5000) {
		milk = (53 + ((calcs - 5000) / 200));
	} else if (calcs > 2000) {
		milk = (29 + ((calcs - 2000) / 125));
	} else if (calcs > 800) {
		milk = (16 + ((calcs - 800) / 80));
	} else {
		milk = (8 + ((calcs - 400) / 50));
	}
	if (slave.boobsImplant > 0) {
		milk *= Math.max(0.25, (1 - (slave.boobsImplant / slave.boobs)));
	}
	if (slave.lactation === 2) {
		milk *= 1.2;
	}
	milk += (milk * ((slave.devotion - 50) / 200));
	calcs = (slave.hormoneBalance / 50);
	if (slave.balls !== 0 && calcs > -2) {
		calcs -= 1;
	} else if (slave.ovaries !== 1 && calcs < 2) {
		calcs += 1;
	}
	milk *= (1 + (calcs * 0.1));
	milk *= (1 + (slave.preg / 100));
	milk *= (1 + (slave.health.condition / 50));
	milk *= (1 + (slave.weight / 500));
	milk *= (1 + (slave.lactationAdaptation / 500));
	if (slave.geneMods.livestock === 1) {
		milk *= 1.1;
	}
	milk += (slave.boobsMilk / 100);
	milk *= healthPenalty(slave);
	milk = Math.trunc(milk);
	milk = Math.clamp(milk, 1, 1000000000000000000);
	return milk;
};

/**
 * The amount of milk a slave produces in liters in a single milking
 * @param {FC.HumanState} slave
 * @returns {number}
 */
globalThis.milkLoad = function(slave) {
	return Math.trunc(milkAmount(slave) / 14);
};

/**
 * The amount of cum a slave produces in deciliters/week
 * @param {FC.HumanState} slave
 * @returns {number}
 */
globalThis.cumAmount = function(slave) {
	let cum = 1;
	const balls = slave.ID === SID.PC ? slave.balls - slave.ballsImplant : slave.balls;
	if (slave.vasectomy !== 1) {
		if (isVirile(slave)) {
			if (slave.drugs === "testicle enhancement") {
				cum += (balls * 3.5);
			} else if (slave.drugs === "hyper testicle enhancement") {
				cum += (balls * 5);
			} else {
				cum += (balls * 2.5);
			}
			if (slave.scrotum === 0) {
				cum *= 0.8;
			}
			if (slave.geneMods.livestock === 1) {
				cum *= 1.5;
			}
			if (slave.diet === "cum production") {
				cum *= 1.2;
			}
		}
		if (slave.ballType === "sterile") {
			cum *= 0.8;
		} else if (slave.geneMods.aggressiveSperm === 1 && isVirile(slave)) {
			cum *= 0.5;
		}
	}

	const calcs = (slave.hormoneBalance / 50);
	cum *= (1 - (calcs * 0.1));

	if (slave.prostate === 0) {
		cum *= 0.2; // being generous here
	} else if (slave.prostate === 2) {
		cum *= 1.2;
	} else if (slave.prostate === 3) {
		cum *= 1.5;
	}

	if (slave.devotion > 50) {
		cum += (cum * (slave.devotion / 100));
	} else if (slave.devotion < -50) {
		cum += (cum * (slave.devotion / 100));
	}
	if (slave.health.condition > 50) {
		cum += (cum * (slave.health.condition / 50));
	} else if (slave.health.condition < -50) {
		cum += (cum * (slave.health.condition / 50));
	}
	cum *= healthPenalty(slave);
	cum = Math.trunc(cum);
	cum = Math.clamp(cum, 1, 1_000_000_000_000_000);
	return cum;
};

/**
 * The amount of cum a slave produces in liters in a single ejaculation
 * @param {FC.HumanState} slave
 * @returns {number}
 */
globalThis.cumLoad = function(slave) {
	return Math.trunc(cumAmount(slave) / 70);
};

/**
 * The amount of fem cum a slave produces in deciliters
 * @param {FC.HumanState} slave
 * @returns {number}
 */
globalThis.girlCumAmount = function(slave) {
	let fluid = ((slave.prostate * (slave.energy / 5)) + 1);
	if (slave.vagina >= 0) {
		if (slave.vaginaLube === 2) {
			fluid *= 1.3;
		} else if (slave.vaginaLube === 1) {
			fluid *= 1.1;
		}
	}
	if (slave.energy > 10) {
		if (slave.health.condition > 50) {
			fluid *= (slave.health.condition / 50);
		} else if (slave.health.condition < -50) {
			fluid *= (1 + (slave.health.condition / 50));
		}
	}
	if (slave.geneMods.livestock === 1) {
		fluid *= 1.2;
	}
	fluid = Math.trunc(fluid);
	if (fluid < 1) {
		fluid = 1;
	}
	return fluid;
};

/**
 * @param {string} text
 * @returns {string}
 */
globalThis.lispReplace = function(text) {
	text = text.replace(/Sh/g, "Th");
	text = text.replace(/SS/g, "Th");
	text = text.replace(/Ss/g, "Th");
	text = text.replace(/sS/g, "Th");
	text = text.replace(/S/g, "Th");
	text = text.replace(/aX/g, "aKTH");
	text = text.replace(/eX/g, "eKTH");
	text = text.replace(/iX/g, "iKTH");
	text = text.replace(/oX/g, "oKTH");
	text = text.replace(/uX/g, "uKTH");
	text = text.replace(/yX/g, "yKTH");
	text = text.replace(/AX/g, "AKTH");
	text = text.replace(/EX/g, "EKTH");
	text = text.replace(/IX/g, "IKTH");
	text = text.replace(/OX/g, "OKTH");
	text = text.replace(/UX/g, "UKTH");
	text = text.replace(/YX/g, "YKTH");
	text = text.replace(/Xa/g, "Tha");
	text = text.replace(/Xe/g, "The");
	text = text.replace(/Xi/g, "Thi");
	text = text.replace(/Xo/g, "Tho");
	text = text.replace(/Xu/g, "Thu");
	text = text.replace(/Xy/g, "Thy");
	text = text.replace(/XA/g, "THA");
	text = text.replace(/XE/g, "THE");
	text = text.replace(/XI/g, "THI");
	text = text.replace(/XO/g, "THO");
	text = text.replace(/XU/g, "THU");
	text = text.replace(/XY/g, "THY");
	text = text.replace(/X/g, "EKTH");
	text = text.replace(/zZ/g, "Th");
	text = text.replace(/Zz/g, "Th");
	text = text.replace(/ZZ/g, "TH");
	text = text.replace(/Z/g, "Th");
	text = text.replace(/Cia/g, "Tha");
	text = text.replace(/Ci/g, "Thi");
	text = text.replace(/Ce/g, "The");
	text = text.replace(/Cy/g, "Thy");
	text = text.replace(/CIA/g, "THA");
	text = text.replace(/CI/g, "THI");
	text = text.replace(/CE/g, "THE");
	text = text.replace(/CY/g, "THY");
	text = text.replace(/ss/g, "th");
	text = text.replace(/sh/g, "th");
	text = text.replace(/s/g, "th");
	text = text.replace(/zz/g, "th");
	text = text.replace(/z/g, "th");
	text = text.replace(/ax/g, "akth");
	text = text.replace(/ex/g, "ekth");
	text = text.replace(/ix/g, "ikth");
	text = text.replace(/ox/g, "okth");
	text = text.replace(/ux/g, "ukth");
	text = text.replace(/yx/g, "ykth");
	text = text.replace(/Ax/g, "Akth");
	text = text.replace(/Ex/g, "Ekth");
	text = text.replace(/Ix/g, "Ikth");
	text = text.replace(/Ox/g, "Okth");
	text = text.replace(/Ux/g, "Ukth");
	text = text.replace(/Yx/g, "Ykth");
	text = text.replace(/xa/g, "tha");
	text = text.replace(/xe/g, "the");
	text = text.replace(/xi/g, "thi");
	text = text.replace(/xo/g, "tho");
	text = text.replace(/xu/g, "thu");
	text = text.replace(/xy/g, "thy");
	text = text.replace(/xA/g, "thA");
	text = text.replace(/xE/g, "thE");
	text = text.replace(/xI/g, "thI");
	text = text.replace(/xO/g, "thO");
	text = text.replace(/xU/g, "thU");
	text = text.replace(/xY/g, "thY");
	text = text.replace(/x/g, "ekth");
	text = text.replace(/cia/g, "tha");
	text = text.replace(/ci/g, "thi");
	text = text.replace(/ce/g, "the");
	text = text.replace(/cy/g, "thy");
	text = text.replace(/cI/g, "thI");
	text = text.replace(/cE/g, "thE");
	text = text.replace(/cY/g, "thY");
	return text;
};

/**
 * @param {FC.SlaveState} slave
 * @param {object} arcology
 * @returns {number}
 */
globalThis.repGainSacrifice = function(slave, arcology) {
	if (!slave || !arcology || !FutureSocieties.isActive('FSAztecRevivalist', arcology) || arcology.FSAztecRevivalist <= 0) {
		return 0;
	}
	return Math.ceil(
		(Math.min(100, Math.pow(1.0926, V.week - slave.weekAcquired)) + slave.prestige * 30) * arcology.FSAztecRevivalist / 100 / ((V.slavesSacrificedThisWeek || 0) + 1));
};

/**
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
globalThis.bodyguardSuccessorEligible = function(slave) {
	if (!slave) {
		return false;
	}
	return (slave.devotion > 50 && slave.muscles >= 0 && slave.weight < 130 && slave.boobs < 8000 && slave.butt < 10 && slave.belly < 5000 && slave.balls < 10 && slave.dick < 10 && slave.preg < 20 && slave.fuckdoll === 0 && slave.fetish !== Fetish.MINDBROKEN && canWalk(slave) && canHold(slave) && canSee(slave) && canHear(slave));
};

/**
 * @param {any} obj
 * @returns {string}
 */
globalThis.toJson = function(obj) {
	let jsontext = Serial.stringify(obj);
	jsontext = jsontext.replace(/^{/, "");
	jsontext = jsontext.replace(/}$/, "");
	return jsontext;
};

/**
 * @param {FC.HumanState} slave
 * @returns {string}
 */
globalThis.nippleColor = function(slave) {
	if (skinToneLevel(slave.skin) < 8) {
		if (slave.preg > slave.pregData.normalBirth / 4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "brown";
		} else {
			return "pink";
		}
	} else if (skinToneLevel(slave.skin) < 14) {
		if (slave.preg > slave.pregData.normalBirth / 4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "dark brown";
		} else {
			return "pink";
		}
	} else if (skinToneLevel(slave.skin) > 20) {
		if (slave.preg > slave.pregData.normalBirth / 4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "black";
		} else {
			return "dark brown";
		}
	} else {
		if (slave.preg > slave.pregData.normalBirth / 4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "dark brown";
		} else {
			return "brown";
		}
	}
};

/**
 * @param {FC.SlaveState} slave
 * @param {FC.PlayerState} PC
 * @returns {number}
 */
globalThis.overpowerCheck = function(slave, PC) {
	let strength = 0;
	strength += (PC.muscles - slave.muscles);
	strength += (PC.height - slave.height);
	strength += ((PC.weight / 5) - (slave.weight / 5));
	strength -= (PC.bellyPreg / 1000);
	strength += (PC.skill.combat - slave.skill.combat);
	// strength -= (PC.health.shortDamage);
	strength -= (PC.health.longDamage);
	strength -= (PC.health.illness * 10);
	if (!canMove(V.PC)) {
		strength -= 100;
	} else if (!canWalk(V.PC)) {
		strength -= 50;
	}

	return strength;
};

/**
 * returns the degree than the intimidatee is intimidated by the intimidater.
 * @param {FC.HumanState} intimidatee
 * @param {FC.HumanState} intimidater
 * @returns {number}
 */
globalThis.intimidationDegree = function(intimidatee, intimidater) {
	let degree = 0;

	if (intimidater.ID === SID.PC) {
		if (isPCCareerInCategory("servant") || isPCCareerInCategory("escort")) {
			degree -= 1;
		} else if (isPCCareerInCategory("slaver") || isPCCareerInCategory("gang") || isPCCareerInCategory("mercenary")) {
			degree += 1;
		}
		if (asPlayer(intimidater)?.rumor === "force") {
			degree += 1;
		}
	}
	if (!canWalk(intimidater) && canWalk(intimidatee)) {
		degree -= 2;
	} else if (canWalk(intimidater) && !canWalk(intimidatee)) {
		degree += 2;
	}
	if (intimidater.visualAge < intimidatee.visualAge && intimidater.visualAge < 18) {
		degree--;
		if (intimidater.visualAge < 13) {
			degree -= 3;
		}
	}
	if (intimidater.muscles >= intimidatee.muscles + 10 && intimidater.muscles >= 0) {
		degree++;
	} else if (intimidatee.muscles >= intimidater.muscles + 10) {
		degree--;
	}
	if (intimidater.teeth === "pointy") {
		degree++;
	}
	if (intimidater.horn !== "none") {
		if (intimidatee.behavioralFlaw === "devout") {
			degree += 2;
		} else if (["curved succubus horns", "backswept horns", "two long oni horns"].includes(intimidater.horn)) {
			degree++;
		} else if (intimidater.horn === "cow horns" && intimidater.boobs < 800) {
			degree++;
		}
	}
	if (degree > 0 && intimidater.height >= intimidatee.height + 10) {
		degree++;
	} else if (degree < 0 && intimidater.height + 10 <= intimidatee.height) {
		degree--;
	}
	if (getLeftArmID(intimidater) >= 5 || getRightArmID(intimidater) >= 5) {
		degree += 10;
	}
	if (intimidater.bellyPreg >= 5000) {
		degree -= Math.min(Math.trunc(intimidater.bellyPreg / 5000), 5);
	}

	return degree;
};

/**
 * returns if arg2 can pick up and carry arg1
 * @param {FC.HumanState} liftee
 * @param {FC.HumanState} lifter
 * @returns {boolean}
 */
globalThis.canLift = function(liftee, lifter) {
	let sumWeight;

	if (lifter.belly >= 10000) {
		return false;
	}
	if (lifter.boobs >= 15000 || lifter.boobsImplant >= 10000) {
		return false;
	}
	sumWeight = liftee.height - lifter.height;
	sumWeight += liftee.weight / 2;
	sumWeight += liftee.boobs / 500;
	sumWeight += liftee.belly / 1000;
	sumWeight += liftee.butt / 2;
	if (liftee.hips > 0) {
		sumWeight += Math.pow(liftee.hips, 2);
	}
	if (liftee.dick >= 20) {
		sumWeight += Math.trunc(liftee.dick / 10);
	}
	if (liftee.balls >= 10) {
		sumWeight += Math.pow(Math.trunc(liftee.balls / 10), 3);
	}

	return 20 + lifter.muscles >= sumWeight && hasBothArms(lifter);
};

/**
 * returns array of IDs of all characters who impregnated slave
 * @param {FC.HumanState} slave
 * @param {boolean} [genepool=false]
 * @returns {number[]}
 */
globalThis.impregnatedBy = function(slave, genepool = false) {
	const IDArray = [];
	if (!Array.isArray(slave.womb)) {
		if (genepool) {
			slave.womb = [];
		} else {
			WombInit(slave);
		}
	}
	for (let i = 0; i < slave.womb.length; i++) {
		IDArray.push(slave.womb[i].fatherID);
	}
	return IDArray;
};

/**
 * returns true if mother was impregnated by father
 * @param {FC.HumanState} mother
 * @param {FC.HumanState} father
 * @param {boolean} [genepool=false]
 * @returns {boolean}
 */
globalThis.isImpregnatedBy = function(mother, father, genepool = false) {
	return impregnatedBy(mother, genepool).includes(father.ID);
};

/**
 * @param {FC.SlaveState} slave
 */
globalThis.SoftenBehavioralFlaw = function(slave) {
	switch (slave.behavioralFlaw) {
		case "arrogant":
			slave.behavioralQuirk = "confident";
			break;
		case "bitchy":
			slave.behavioralQuirk = "cutting";
			break;
		case "odd":
			slave.behavioralQuirk = "funny";
			break;
		case "hates men":
			slave.behavioralQuirk = "adores women";
			break;
		case "hates women":
			slave.behavioralQuirk = "adores men";
			break;
		case "gluttonous":
			slave.behavioralQuirk = "fitness";
			break;
		case "anorexic":
			slave.behavioralQuirk = "insecure";
			break;
		case "devout":
			slave.behavioralQuirk = "sinful";
			break;
		case "liberated":
			slave.behavioralQuirk = "advocate";
			break;
	}
	slave.behavioralFlaw = "none";
};

/**
 * @param {FC.SlaveState} slave
 */
globalThis.SoftenSexualFlaw = function(slave) {
	switch (slave.sexualFlaw) {
		case "hates oral":
			slave.sexualQuirk = "gagfuck queen";
			break;
		case "hates anal":
			slave.sexualQuirk = "painal queen";
			break;
		case "hates penetration":
			slave.sexualQuirk = "strugglefuck queen";
			break;
		case "shamefast":
			slave.sexualQuirk = "tease";
			break;
		case "idealistic":
			slave.sexualQuirk = "romantic";
			break;
		case "repressed":
			slave.sexualQuirk = "perverted";
			break;
		case "apathetic":
			slave.sexualQuirk = "caring";
			break;
		case "crude":
			slave.sexualQuirk = "unflinching";
			break;
		case "judgemental":
			slave.sexualQuirk = "size queen";
			break;
	}
	slave.sexualFlaw = "none";
};

/**
 * @param {FC.PlayerState} PC
 */
globalThis.generatePlayerPronouns = function(PC) {
	if (PC.title === 0) {
		PC.pronoun = App.Data.Pronouns.Kind.female;
	} else {
		PC.pronoun = App.Data.Pronouns.Kind.male;
	}
};

/**
 * @param {number} number1
 * @param {number} number2
 */
globalThis.pregNumberName = function(number1, number2) {
	let pt = "";
	let p1 = number1 % 10;
	let p2 = ((number1 % 100) - (number1 % 10)) / 10;
	let p3 = ((number1 % 1000) - (number1 % 100)) / 100;
	switch (number1) {
		case 1:
			pt += "bab";
			break;
		case 2:
			pt += "twin";
			break;
		default:
			switch (number1) {
				case 3:
					pt += "tri";
					break;
				case 4:
					pt += "quadru";
					break;
				case 5:
					pt += "quintu";
					break;
				case 6:
					pt += "sextu";
					break;
				case 7:
					pt += "septu";
					break;
				case 8:
					pt += "octu";
					break;
				case 9:
					pt += "nonu";
					break;
				default:
					switch (p1) {
						case 1:
							pt += "un";
							break;
						case 2:
							pt += "duo";
							break;
						case 3:
							pt += "tre";
							break;
						case 4:
							pt += "quattuor";
							break;
						case 5:
							pt += "quin";
							break;
						case 6:
							pt += "sex";
							break;
						case 7:
							pt += "septen";
							break;
						case 8:
							pt += "octo";
							break;
						case 9:
							pt += "novem";
							break;
					}
					switch (p2) {
						case 1:
							pt += "dec";
							break;
						case 2:
							pt += "vigint";
							break;
						case 3:
							pt += "trigint";
							break;
						case 4:
							pt += "quadragint";
							break;
						case 5:
							pt += "quinquagint";
							break;
						case 6:
							pt += "sexagint";
							break;
						case 7:
							pt += "septuagint";
							break;
						case 8:
							pt += "octogint";
							break;
						case 9:
							pt += "nonagint";
							break;
					}
					if (number1 >= 100) {
						if (p2 !== 0) {
							pt += "i";
						}
						switch (p3) {
							case 1:
								pt += "centu";
								break;
							case 2:
								pt += "bicentu";
								break;
							case 3:
								pt += "tricentu";
								break;
							case 4:
								pt += "quadricentu";
								break;
							case 5:
								pt += "quincentu";
								break;
							case 6:
								pt += "sexcentu";
								break;
							case 7:
								pt += "septemcentu";
								break;
							case 8:
								pt += "octocentu";
								break;
							case 9:
								pt += "novemcentu";
								break;
						}
					} else {
						pt += "u";
					}
			}
			pt += "plet";
	}
	if (number2 === 2) {
		if (number1 === 1) {
			pt += "ie";
		}
		pt += "s";
	} else {
		if (number1 === 1) {
			pt += "y";
		}
	}
	return pt;
};

/** Returns the description of the part of body a slave/PC uses to penetrate, if they can, or a description for "dildo"
 * @param {FC.HumanState} slave
 * @param {boolean} includeClit
 * @returns {string}
 */
globalThis.penetrationTool = function(slave, includeClit = true) {
	if (!slave || !slave.hasOwnProperty("dick")) {
		return "ERROR";
	}
	if (slave.dick > 0 && canPenetrate(slave)) {
		return dickDesc(slave);
	}
	if (slave.clit >= 3 && includeClit) {
		return clitDesc(slave);
	}
	let adj = ["big", "fat", "huge", "large", "long", "realistic"];
	let use = (slave.vagina === 0) ? ["strap-on", "", ""] : ["double-sided", "strap-on double", "double-headed", "double-ended", "strapless double"];
	let noun = ["dildo", "dildo", "dildo", "dildo", "fake cock", "vibrator", "vibrator"];
	return `${jsEither(adj)} ${jsEither(use)} ${jsEither(noun)}`;
};

/** Is the dick too big (1), too small(-1), or just right(0)?
 * @param {FC.HumanState} catcher
 * @param {FC.HumanState} pitcher
 * @param {string} hole
 * @returns {number}
 */
globalThis.relativeDickSize = function(catcher, pitcher, hole) {
	let size = 0;
	if (pitcher.dick === 1 || (pitcher.dick < 3 && catcher.physicalAge > 12) || (catcher[hole] >= pitcher.dick + 3)) {
		size = -1;
	}
	if (pitcher.dick > 5 || (pitcher.dick - catcher[hole] > 3) || (catcher.physicalAge <= 12 && pitcher.dick > 2)) {
		size = 1;
	}
	if (catcher.sexualQuirk === SexualQuirk.SIZEQUEEN) {
		if (pitcher.dick < 4) {
			size = -1;
		} else if (size !== -1) {
			size = 0;
		}
	}
	return size;
};

repo_name: Nithhogg/fc

license: mit


code:
/**
 * @param {FC.SlaveState} slave
 */
globalThis.SlaveStatClamp = function(slave) {
	slave.energy = Math.clamp(slave.energy, 0, 100);

	if (slave.devotion > 100) {
		if (slave.trust < -95) {
			slave.trust = -100;
		} else if (slave.trust < 100 && slave.trust > 20) {
			slave.trust += Math.trunc((slave.devotion - 100) * 5) / 10;
		} else {
			repX(4 * (slave.devotion - 100), "slavesViewOfPC", slave);
		}
		slave.devotion = 100;
	} else if (slave.devotion < -95) {
		slave.devotion = -100;
	}
	if (slave.trust > 100) {
		if (slave.devotion < -95) {
			slave.devotion = -100;
		} else if (slave.devotion < 100 && slave.devotion > 20) {
			slave.devotion += Math.trunc(slave.trust - 100);
		} else {
			repX(4 * (slave.trust - 100), "slavesViewOfPC", slave);
		}
		slave.trust = 100;
	} else if (slave.trust < -95) {
		slave.trust = -100;
	}
	if (slave.trust < -100) {
		slave.trust = -100;
	}
	if (slave.devotion < -100) {
		slave.devotion = -100;
	}
	if (isNaN(slave.counter.oral)) {
		slave.counter.oral = 0;
		alert("Oral count has broken, report what you just did!");
	}
	if (isNaN(slave.counter.anal)) {
		slave.counter.anal = 0;
		alert("Anal count has broken, report what you just did!");
	}
	if (isNaN(slave.counter.vaginal)) {
		slave.counter.vaginal = 0;
		alert("Vaginal count has broken, report what you just did!");
	}
	if (isNaN(slave.counter.mammary)) {
		slave.counter.mammary = 0;
		alert("Titfuck count has broken, report what you just did!");
	}
	if (isNaN(slave.counter.penetrative)) {
		slave.counter.penetrative = 0;
		alert("Penetration count has broken, report what you just did!");
	}
	if (slave.foreskin === undefined) {
		slave.foreskin = 0;
	}
	if (!hasAnyNaturalLegs(slave) && slave.heels === 1) {
		slave.heels = 0;
	}
	if (slave.vagina < 0 && slave.mpreg === 0 && slave.preg === -1) {
		slave.preg = 0;
		WombFlush(slave);
	}
	if ((slave.rules.lactation === "induce" && slave.lactation !== 0) || (slave.rules.lactation === "maintain" && slave.lactation !== 1)) {
		slave.rules.lactation = "none";
	}
};


code:
App.Underperformers = {};

/** Select only slaves which are not reasonably expected to produce any income (brand new slaves, servants, fucktoys, etc)
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
App.Underperformers.expectIncome = function(slave) {
	const productionJobs = [Job.ARCADE, Job.MILKED, Job.WHORE, Job.GLORYHOLE, Job.BROTHEL, Job.DAIRY, Job.CHOICE];
	return productionJobs.includes(slave.assignment) && ((slave.weekAcquired + 1) < V.week) && _.isFinite(slave.lastWeeksCashIncome);
};

App.Underperformers.highSale = function() {
	const description = App.UI.DOM.makeElement("div", "Take the rough value of a slave and divide it by how much they made overall last week. This will tell you how many weeks it might take them to earn the same amount you'd get for selling them right now.", "note");

	const frag = App.UI.SlaveList.render(
		getBestSlavesIDs(
			{
				part: (slave) => {
					const ratio = slaveCost(slave) / (slave.lastWeeksCashIncome - getSlaveCost(slave));
					return ratio > 0 ? ratio : 100000000 + ratio;
				},
				count: V.underperformersCount,
				filter: App.Underperformers.expectIncome
			}
		),
		[],
		App.UI.SlaveList.SlaveInteract.stdInteract,
		(slave) => $(document.createDocumentFragment()).append(
			`Worth ${cashFormatColor(slaveCost(slave))} / Nets ${cashFormatColor(slave.lastWeeksCashIncome - getSlaveCost(slave))} a week = ${(Math.trunc(slaveCost(slave) / (slave.lastWeeksCashIncome - getSlaveCost(slave)))) > 0 ? (Math.trunc(slaveCost(slave) / (slave.lastWeeksCashIncome - getSlaveCost(slave)))) : "infinite"} weeks`
		).get(0)
	);

	frag.prepend(description);
	return frag;
};

App.Underperformers.expensive = function() {
	const description = App.UI.DOM.makeElement("div", "This list looks for moochers by weighing their weekly income against the weekly cost of providing for them.", "note");

	const frag = App.UI.SlaveList.render(
		getBestSlavesIDs(
			{
				part: (slave) => (slave.lastWeeksCashIncome - getSlaveCost(slave)),
				largest: false,
				count: V.underperformersCount,
				filter: App.Underperformers.expectIncome
			}
		),
		[],
		App.UI.SlaveList.SlaveInteract.stdInteract,
		(slave) => $(document.createDocumentFragment()).append(
			`${cashFormatColor(Math.trunc(slave.lastWeeksCashIncome - getSlaveCost(slave)))} net last week`
		).get(0)
	);

	frag.prepend(description);
	return frag;
};

App.Underperformers.passage = function() {
	const node = new DocumentFragment();
	// App.UI.DOM.appendNewElement("h1", node, `Underperforming Slaves`);
	const r = [];
	r.push(App.UI.DOM.makeElement("div", `${properMaster()}, while many of your slaves work hard to earn ¤ each week, some succeed more than others. As a trader in slaves, you may appreciate the opportunity that comes when a particularly valuable slave didn't earn very much last week. Or perhaps you just want the chance to tweak these problem slaves and train them to be better? The choice is yours.`));
	if (V.slaveCostFactor > 1.1) {
		r.push(`Since there is a bull market for slaves, <span class="green">this is a great time to sell.</span>`);
	} else if (V.slaveCostFactor > 1) {
		r.push(`Since the slave market is bullish; <span class="green">this is a pretty good time to sell.</span>`);
	} else if (V.slaveCostFactor < 0.9) {
		r.push(`Since there is a bear market for slaves, <span class="red">this is a poor time to sell.</span>`);
	} else if (V.slaveCostFactor < 1) {
		r.push(`Since the slave market is bearish; <span class="red">this is a terrible time to sell.</span>`);
	} else {
		r.push(`Since the slave market is stable; <span class="yellow">prices are average.</span>`);
	}
	App.Events.addParagraph(node, r);

	const tabBar = new App.UI.Tabs.TabBar("underperformingSlaves");
	tabBar.addTab("Worth much but earning little", "little", App.Underperformers.highSale());
	tabBar.addTab("Costing vs earning", "earning", App.Underperformers.expensive());
	node.append(tabBar.render());

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

	return node;
};


code:
/** @implements {FC.IUpgrade} */
App.Upgrade = class Upgrade {
	/**
	 * @param {string} property The variable name of the property.
	 * @param {FC.IUpgradeTier[]} tiers A list of tiers available for the upgrade.
	 * @param {object} [object] Any object to attach the upgrade to, if not the default `V`.
	 */
	constructor(property, tiers, object = V) {
		/** @private */
		this._property = property;

		/** @private */
		this._div = document.createElement("div");
		/** @private @type {Object} */
		this._object = object || V;
		/** @private @type {FC.IUpgradeTier[]} */
		this._tiers = tiers;
	}

	/**
	 * Puts the different sections together into one passage.
	 *
	 * @private
	 * @returns {DocumentFragment}
	 */
	 _assemble() {
		const frag = new DocumentFragment();

		this.tiers.forEach(tier => {
			const {
				value, link, text, upgraded, handler, notes, prereqs, nodes,
			} = tier;

			const cost = Math.trunc(tier.cost) || 0;

			if ((!prereqs || prereqs.every(prereq => prereq === true)) &&
				_.isEqual(value, this._object[this._property])) {
				App.UI.DOM.appendNewElement("div", frag, text);

				if (link) {
					App.UI.DOM.appendNewElement("div", frag, makePurchase(link, cost, "capEx", {
						notes,
						handler: () => {
							this._object[this._property] = upgraded;

							if (handler) {
								handler();
							}
						},
					}));
				}

				if (nodes) {
					App.Events.addNode(frag, nodes);
				}
			}
		});

		return frag;
	}

	/**
	 * Renders the upgrade on-screen.
	 *
	 * @returns {HTMLDivElement}
	 */
	 render() {
		this._div.append(this._assemble());

		return this._div;
	}

	/**
	 * Refreshes the upgrade on-screen.
	 *
	 * @returns {void}
	 */
	 refresh() {
		App.UI.DOM.replace(this._div, this._assemble());
	}

	/**
	 * Adds new tiers to the upgrade.
	 *
	 * @param {FC.IUpgradeTier[]} tiers
	 * @returns {this}
	 */
	addTiers(...tiers) {
		this._tiers.push(...tiers);

		return this;
	}

	/**
	 * The variable name of the property.
	 *
	 * @returns {string}
	 */
	get property() {
		return this._property;
	}

	/**
	 * All tiers that are available.
	 *
	 * @returns {FC.IUpgradeTier[]}
	 */
	get tiers() {
		return this._tiers;
	}

	/**
	 * The object the upgrade is attached to.
	 *
	 * @returns {object}
	 */
	get object() {
		return this._object;
	}
};


code:
/** Returns the revivalist nationality associated with the player's arcology, or 0 if none
 * @returns {string|0}
 */
globalThis.getRevivalistNationality = function() {
	for (const fsKey of Object.keys(App.Data.FutureSociety.records)) {
		if (V.arcologies[0][fsKey] > 90) {
			const nationality = App.Data.FutureSociety.records[fsKey].nationality;
			if (nationality) {
				return nationality;
			}
		}
	}
	return 0;
};

/** Calculate and return economic uncertainty multiplier for a given arcology
 * @param {number} arcologyID
 * @returns {number}
 */
App.Utils.economicUncertainty = function(arcologyID) {
	let uncertainty = arcologyID === 0 ? 5 : 10;
	if (V.assistant.power === 1) {
		uncertainty -= Math.max(Math.trunc(uncertainty / 2), 0);
	} else if (V.assistant.power > 1) {
		uncertainty = 0;
	}
	return jsRandom(100 - uncertainty, 100 + uncertainty) / 100;
};

/**
 * @returns {number}
 */
App.Utils.schoolCounter = function() {
	return Array.from(App.Data.misc.schools.keys()).filter(s => V[s].schoolPresent).length;
};

/**
 * @typedef {object} menialObject
 * @property {string} text
 * @property {number} value
 */

/**
 * @returns {menialObject}
 */
globalThis.menialPopCap = function() {
	let r = "";

	let popCap = 500 * (1 + V.building.findCells(cell => cell instanceof App.Arcology.Cell.Manufacturing && cell.type === "Pens").length);

	let overMenialCap = V.menials + V.fuckdolls + V.menialBioreactors - popCap;
	if (overMenialCap > 0) {
		const price = menialSlaveCost(-overMenialCap);
		if (V.menials > 0) {
			if (V.menials > overMenialCap) {
				cashX((overMenialCap * price), "menialTrades");
				V.menialDemandFactor -= overMenialCap;
				V.menials -= overMenialCap;
				overMenialCap = 0;
				r += "You don't have enough room for all your menials and are obliged to sell some.";
			} else {
				cashX((V.menials * price), "menialTrades");
				V.menialDemandFactor -= V.menials;
				overMenialCap -= V.menials;
				V.menials = 0;
				r += "You don't have enough room for your menials and are obliged to sell them.";
			}
		}
		if (overMenialCap > 0 && V.fuckdolls > 0) {
			if (V.fuckdolls > overMenialCap) {
				cashX(overMenialCap * (price * 2), "menialTrades");
				V.menialDemandFactor -= overMenialCap;
				V.fuckdolls -= overMenialCap;
				overMenialCap = 0;
				r += "You don't have enough room for all your Fuckdolls and are obliged to sell some.";
			} else {
				cashX(V.fuckdolls * (price * 2), "menialTrades");
				V.menialDemandFactor -= V.fuckdolls;
				overMenialCap -= V.fuckdolls;
				V.fuckdolls = 0;
				r += "You don't have enough room for your Fuckdolls and are obliged to sell them.";
			}
		}
		if (overMenialCap > 0 && V.menialBioreactors > 0) {
			cashX(overMenialCap * (price - 100), "menialTrades");
			V.menialDemandFactor -= overMenialCap;
			V.menialBioreactors -= overMenialCap;
			r += "You don't have enough room for all your menial bioreactors and are obliged to sell some.";
		}
	}
	return {text: r, value: popCap};
};

/**
 * Returns the percentage (0-100) of the social acceptance of the slaves penetrating citizens in YOUR arcology.
 * Accepts a slave as an optional argument.
 *
 * @param {FC.SlaveState} [slave=null]
 * @returns {number}
 */
globalThis.penetrativeSocialUse = function(slave = null) {
	let total = 10;
	if (V.policies.sexualOpenness) {
		total += 40 + 20 * (Math.clamp(V.rep - 5000, 0, 12000) / 12000); // penetrative campaign, related to reputation, (40 to 60)
	}
	total += V.LDE.schoolPresent * 3 + V.TGA.schoolPresent * 3 + V.TFS.schoolPresent * 10; // slave schools focused on penises
	total += FutureSocieties.isActive("FSGenderRadicalist") ? 25 : 0;
	total += [0, 15, 20, 0, 5][V.arcologies[0].FSGenderRadicalistLawFuta]; // 15 futas, 20 big dicks & balls, 5 small dicks
	total += FutureSocieties.isActive("FSNull") ? 15 : 0;
	total += FutureSocieties.isActive("FSHedonisticDecadence") ? 15 : 0;
	total += FutureSocieties.isActive("FSRepopulationFocus") ? 5 : 0;
	total += V.arcologies[0].FSRepopulationFocusLaw * 10;

	total -= FutureSocieties.isActive("FSGenderFundamentalist") ? 25 : 0;
	total -= V.arcologies[0].FSGenderFundamentalistSMR ? 30 : 0;
	total -= FutureSocieties.isActive("FSEdoRevivalist") ? 10 : 0;
	total -= FutureSocieties.isActive("FSArabianRevivalist") ? 10 : 0;
	total -= FutureSocieties.isActive("FSChineseRevivalist") ? 10 : 0;
	total -= V.arcologies[0].FSRestartSMR ? 30 : 0;
	total -= FutureSocieties.isActive("FSAztecRevivalist") ? 10 : 0;
	total -= FutureSocieties.isActive("FSNeoImperialist") ? 10 : 0;
	total -= FutureSocieties.isActive("FSAntebellumRevivalist") ? 10 : 0;

	total = Math.clamp(total, 0, 100);

	if (slave) {
		total += slave.toyHole === "dick" ? 5 : 0;
		total += Math.trunc(slave.skill.penetrative ** 2 / 1000);
		if (FutureSocieties.isActive("FSSupremacist")) {
			total += V.arcologies[0].FSSupremacistRace === slave.race ? 10 : -20;
		}
		if (FutureSocieties.isActive("FSSubjugationist")) {
			total += V.arcologies[0].FSSubjugationistRace === slave.race ? 20 : 0;
		}
		if (FutureSocieties.isActive("FSRestart")) {
			total -= isVirile(slave) ? 40 : 0;
		}
		total += slave.prestige * 5;
		if (slave.porn.prestigeDesc === "fucker") {
			total += (slave.porn.prestige + 1) ** 3; // 1, 8, 27, 64
		} else if (slave.porn.prestigeDesc === "stud") {
			total += (slave.porn.prestige ** 2) * 3; // 0, 3, 12, 27
		} else {
			total += slave.porn.prestige ** 2; // 0, 1, 4, 9
		}
	}

	if (V.seeDicks === 0 && total > 0) {
		total *= .6;
	} else if (V.seeDicks > 25) {
		total += V.seeDicks < 75 ? V.seeDicks * .10 : V.seeDicks * .08;
	}

	if (total >= 40) { // Once dicks are trendy, availability starts to increase interest
		let haveDick = getSlaves().filter(s => s.dick > 0).size;
		if (haveDick) {
			let withDick = (haveDick / slaveCount()); // percentage of slaves with dicks
			total += withDick > .2 ? 15 * withDick : 0;
			let toyHole = getSlaves().filter(s => s.toyHole === "dick").size;
			total += 10 * (toyHole / haveDick);
			let penetrativeAverage = 0;
			getSlaves().filter(s => s.dick > 0).forEach(s1 => {
				penetrativeAverage += s1.skill.penetrative;
			});
			total += (penetrativeAverage / haveDick) * .1;
		}
	}

	total = Math.clamp(Math.floor(total), 0, 100);
	return total;
};

App.Utils.Arcology = class {
	/**
	 * @param {FC.ArcologyState} [arcology]
	 */
	constructor(arcology) {
		this._arcology = arcology ?? V.arcologies[0];
	}

	/**
	 * @param {FC.FutureSociety} fs
	 * @returns {boolean}
	 */
	fsActive(fs) {
		return FutureSocieties.isActive(fs, this._arcology);
	}

	/**
	 * @param {FC.FutureSociety[]} FSes
	 * @returns {boolean}
	 */
	fsActiveSome(...FSes) {
		return FSes.some((fs) => this.fsActive(fs));
	}

	/**
	 * @param {FC.FutureSociety[]} FSes
	 * @returns {boolean}
	 */
	fsActiveEvery(...FSes) {
		return FSes.every((fs) => this.fsActive(fs));
	}

	/**
	 * @param {FC.FutureSociety[]} FSes
	 * @returns {number}
	 */
	countActive(...FSes) {
		return FSes.reduce((count, fs) => this.fsActive(fs) ? count + 1 : count, 0);
	}

	/**
	 * @returns {keyof FC.RevivalSocietyFreeze | null}
	 */
	revivalSociety() {
		console.assert(this.countActive('FSAztecRevivalist', 'FSEgyptianRevivalist', 'FSEdoRevivalist', 'FSArabianRevivalist', 'FSChineseRevivalist', 'FSNeoImperialist', 'FSRomanRevivalist', 'FSAntebellumRevivalist') <= 1);

		if (this.fsActive('FSAntebellumRevivalist')) {
			return RevivalSociety.ANTEBELLUM;
		} else if (this.fsActive('FSArabianRevivalist')) {
			return RevivalSociety.ARABIAN;
		} else if (this.fsActive('FSAztecRevivalist')) {
			return RevivalSociety.AZTEC;
		} else if (this.fsActive('FSChineseRevivalist')) {
			return RevivalSociety.CHINESE;
		} else if (this.fsActive('FSEdoRevivalist')) {
			return RevivalSociety.EDO;
		} else if (this.fsActive('FSEgyptianRevivalist')) {
			return RevivalSociety.EGYPTIAN;
		} else if (this.fsActive('FSNeoImperialist')) {
			return RevivalSociety.NEO_IMPERIAL;
		} else if (this.fsActive('FSRomanRevivalist')) {
			return RevivalSociety.ROMAN;
		} else {
			return null;
		}
	}
};


code:
/*
*
* This file focuses on slave related functions that assess qualities about slaves. Are they/can they X?
*
*/

/**
 * @param {FC.SlaveState} slave
 * @returns {string}
 */
globalThis.getSlaveDevotionClass = function(slave) {
	if ((!slave) || (!State)) {
		return undefined;
	}
	if (slave.fetish === Fetish.MINDBROKEN) {
		return "mindbroken";
	}
	if (slave.devotion < -95) {
		return "very-hateful";
	} else if (slave.devotion < -50) {
		return "hateful";
	} else if (slave.devotion < -20) {
		return "resistant";
	} else if (slave.devotion <= 20) {
		return "ambivalent";
	} else if (slave.devotion <= 50) {
		return "accepting";
	} else if (slave.devotion <= 95) {
		return "devoted";
	} else {
		return "worshipful";
	}
};

/**
 * @param {FC.SlaveState} slave
 * @returns {string}
 */
globalThis.getSlaveTrustClass = function(slave) {
	if ((!slave) || (!State)) {
		return undefined;
	}

	if (slave.fetish === Fetish.MINDBROKEN) {
		return "mindbroken";
	}

	if (slave.trust < -95) {
		return "extremely-terrified";
	} else if (slave.trust < -50) {
		return "terrified";
	} else if (slave.trust < -20) {
		return "frightened";
	} else if (slave.trust <= 20) {
		return "fearful";
	} else if (slave.trust <= 50) {
		if (slave.devotion < -20) {
			return "hate-careful";
		} else {
			return "careful";
		}
	} else if (slave.trust <= 95) {
		if (slave.devotion < -20) {
			return "bold";
		} else {
			return "trusting";
		}
	} else if (slave.devotion < -20) {
		return "defiant";
	} else {
		return "profoundly-trusting";
	}
};

/**
 * Returns a "disobedience factor" between 0 (perfectly obedient) and 100 (completely defiant)
 * @param {FC.SlaveState} slave
 * @returns {number}
 */
globalThis.disobedience = function(slave) {
	const devotionBaseline = 20; // with devotion above this number slaves will obey completely
	const trustBaseline = -20; // with trust below this number slaves will obey completely

	if (slave.devotion > devotionBaseline || slave.trust < trustBaseline) {
		return 0; // no chance of disobedience
	}

	// factors are between 0 (right on the boundary of perfectly obedient) and 10 (completely disobedient)
	let devotionFactor = 10 - ((10 * (slave.devotion + 100)) / (devotionBaseline + 100));
	let trustFactor = (10 * (slave.trust - trustBaseline)) / (100 - trustBaseline);
	return Math.round(devotionFactor * trustFactor);
};

/**
 * Returns how exposing a slave's outfit is, after taking into consideration a topless outfit is more revealing for boobless slaves or female ones.
 * @param {FC.HumanState} slave
 * @returns {0|1|2|3|4}
 */
globalThis.getExposure = function(slave) {
	const clothes = App.Data.clothes.get(slave.clothes);
	return (!clothes || (clothes.topless && clothes.exposure < 3 && (slave.boobs > 299 || (slave.genes === 'XX' && slave.vagina >= 0)))) ? 3 : clothes.exposure;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canImproveIntelligence = function(slave) {
	let origIntel = getGenePoolRecord(slave)?.intelligence || slave.intelligence;
	return (slave.intelligence < origIntel + 15) && (slave.intelligence < 100);
};

/**
 * This function returns the max *growable* height; remember to consider implants at call sites.
 * @param {FC.HumanState} slave
 * @returns {number}
 */
globalThis.maxHeight = function(slave) {
	let max = Math.clamp(slave.natural.height * 1.25, 0, 274); /* max achievable height is expected height plus 25% */

	if (slave.geneticQuirks.neoteny === 2 && slave.physicalAge > 12) { /* Limit neoteny slaves to 12 year old max height */
		max = Math.clamp(Height.mean(slave.nationality, slave.race, slave.genes, 12) * 1.25, 0, 274);
	}

	if (slave.geneticQuirks.dwarfism === 2 && slave.geneticQuirks.gigantism !== 2) {
		max = Math.min(max, 160);
	} else if (slave.geneticQuirks.gigantism === 2 && slave.geneticQuirks.dwarfism !== 2) {
		max = Math.min(Math.trunc(max * 1.1), 274);
	}

	max = Math.round(max);

	return max;
};

/**
 * @param {FC.HumanState} slave
 * @returns {boolean}
 */
globalThis.canImproveHeight = function(slave) {
	return slave.height - slave.heightImplant * 10 < maxHeight(slave);
};

/**
 * @param {FC.SlaveState} slave
 * @param {FC.HumanState} target
 * @returns {boolean}
 */
globalThis.haveRelationshipP = function(slave, target) {
	return slave.relationshipTarget === target.ID;
};

/**
 * @param {FC.SlaveState} slave
 * @param {FC.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 {FC.SlaveState} slave
 * @returns {boolean}
 */
globalThis.isLeaderP = function(slave) {
	const leaders = [S.HeadGirl, S.Bodyguard, S.Recruiter, S.Concubine, S.Nurse, S.Attendant, S.Matron, S.Madam, S.DJ, S.Milkmaid, S.Farmer, S.Stewardess, S.Schoolteacher, S.Wardeness];

	return leaders.some(leader => leader && leader.ID === slave.ID);
};

/** Get the written variant of a slave's title for the player, without messing with global state.
 * @param {FC.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 {Pick<FC.HumanState, "slaveSurname" | "slaveName"> & {nationality?: FC.HumanState["nationality"]}} actor
 * @returns {string}
 * TODO:@franklygeorge rename to `fullName`
 */
globalThis.SlaveFullName = function(actor) {
	const pair = actor.slaveSurname ? [actor.slaveName, actor.slaveSurname] : [actor.slaveName];
	if ((V.surnameOrder !== 1 && ["Cambodian", "Chinese", "Ancient Chinese Revivalist", "Hungarian", "Japanese", "Edo Revivalist", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(actor?.nationality)) || (V.surnameOrder === 2)) {
		pair.reverse();
	}
	return pair.join(" ");
};

/** Is the slave a shelter slave?
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
globalThis.isShelterSlave = function(slave) {
	return slave.origin.includes("Slave Shelter");
};

/**
 * Returns if a person appears male, female, or androgynous.
 * @param {FC.HumanState} slave
 * @param {boolean} spoken
 * @returns {number} - negative means male, positive means female, more androgynous is closer to zero
 */
globalThis.perceivedGender = function(slave, spoken = false) {
	let degree = 0;
	if (["cute", "sensual"].includes(slave.faceShape)) {
		degree++;
	} else if (["masculine"].includes(slave.faceShape)) {
		degree--;
	}
	if (spoken) {
		if (slave.voice > 1) {
			degree++;
		} else if (slave.voice === 1) {
			degree -= 2;
		}
	}
	if (slave.shoulders > slave.hips) {
		degree += slave.hips - slave.shoulders;
	} else if (slave.shoulders < slave.hips) {
		degree += slave.shoulders - slave.hips;
	}
	if ((slave.shoulders > 1 && slave.boobs < 650) || (slave.shoulders > 0 && slave.boobs < 500)) {
		// wide shoulders muddle breast size
	} else if (slave.boobs >= 500 || (slave.boobs >= 400 && slave.weight <= 30) || (slave.boobs >= 300 && slave.weight < -10)) {
		degree++;
	}
	let weightAffectsWaist = 0;
	if (slave.hips > 2) {
		if (slave.weight > 0) {
			weightAffectsWaist = Math.round(slave.weight / 6);
		}
		degree++;
	} else if (slave.hips > 1) {
		if (slave.weight > 0) {
			weightAffectsWaist = Math.round(slave.weight / 4);
		}
		if (slave.butt <= 3) {
			degree--;
		} else {
			degree++;
		}
	} else if (slave.hips > 0) {
		if (slave.weight > 0) {
			weightAffectsWaist = Math.round(slave.weight / 2);
		}
		if (slave.butt <= 2) {
			degree--;
		} else {
			degree++;
		}
	} else if (slave.hips > -1) {
		if (slave.weight > 0) {
			weightAffectsWaist = slave.weight;
		}
		if (slave.butt <= 1) {
			degree--;
		} else {
			degree++;
		}
	} else if (slave.butt > 2) {
		degree++;
	} else {
		degree--;
	}
	if (slave.weight > 0) {
		if (slave.hips === -1) {
			weightAffectsWaist = Math.round(slave.weight * 2);
		} else if (slave.hips === -2) {
			weightAffectsWaist = Math.round(slave.weight * 4);
		}
	}
	if (slave.waist < -10 - weightAffectsWaist) {
		degree++;
	} else if (slave.waist > 10 - weightAffectsWaist) {
		degree--;
	}
	if (slave.muscles > 50) {
		degree--;
	}
	if (slave.belly >= 5000) {
		degree++;
	}
	if (slave.dick > 2 || slave.balls > 3) {
		degree--;
	}
	degree += Math.trunc(slave.hormoneBalance / 200);
	const PC = asPlayer(slave);
	if (PC) {
		if (PC.title === 1) {
			degree--;
			if (degree > 0) {
				degree *= .85;
			} else {
				degree *= 1.15;
			}
		} else {
			degree++;
			if (degree > 0) {
				degree *= 1.15;
			} else {
				degree *= .85;
			}
		}
	}
	if (slave.genes === GenderGenes.MALE) {
		if (degree > 0) {
			degree *= .75;
		} else {
			degree *= 1.25;
		}
	} else if (slave.genes === GenderGenes.FEMALE) {
		if (degree > 0) {
			degree *= 1.25;
		} else {
			degree *= .75;
		}
	}
	return degree;
};

/**
 * @param {FC.SlaveState} A
 * @param {FC.SlaveState} B
 * @returns {boolean}
 */
globalThis.sameAssignmentP = function(A, B) {
	return A.assignment === B.assignment;
};

/** Determine whether a given penthouse slave can move into a private room or not.
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
globalThis.canMoveToRoom = function(slave) {
	const partner = slave.relationship >= 4 ? getSlave(slave.relationshipTarget) : null;
	const partnerHasRoom = partner && assignmentVisible(partner) && partner.rules.living === "luxurious";
	return partnerHasRoom || V.rooms - V.roomsPopulation >= 1;
};

/**
 * @param {FC.HumanState} slave
 * @returns {0|1|2|3} 0: No heel boost at all. 1: Pumps, slight boost. 2: High heels. 3: Painfully/extreme high heels
 */
globalThis.shoeHeelCategory = function(slave) {
	const height = App.Data.shoes.get(slave.shoes) ? App.Data.shoes.get(slave.shoes).heelHeight : 0; // Height is in cm
	if (height > 20) {
		return 3;
	} else if (height > 5) {
		return 2;
	} else if (height > 0) {
		return 1;
	} else {
		return 0;
	}
};

/**
 * @param {FC.HumanState} slave
 * @returns {number} shoe height in cm (heel + platform height)
 */
globalThis.shoeHeight = function(slave) {
	const heelHeight = App.Data.shoes.get(slave.shoes) ? App.Data.shoes.get(slave.shoes).heelHeight : 0;
	const platformHeight = App.Data.shoes.get(slave.shoes) ? App.Data.shoes.get(slave.shoes).platformHeight : 0;
	return heelHeight + platformHeight;
};

/**
 * @param {FC.SlaveState} slave
 * @returns {0|1|2|3}
 */
globalThis.plugWidth = function(slave) {
	const plug = App.Data.buttplug.get(slave.buttplug) || V.customItem.buttplug.get(slave.buttplug);
	return plug.width || 0;
};

/**
 * @param {FC.SlaveState} slave
 * @returns {0|1|2|3}
 */
globalThis.plugLength = function(slave) {
	const plug = App.Data.buttplug.get(slave.buttplug) || V.customItem.buttplug.get(slave.buttplug);
	return plug.length || 0;
};

/**
 * @param {FC.SlaveState} slave
 * @returns {0|1|2|3}
 */
globalThis.dildoWidth = function(slave) {
	const dildo = App.Data.vaginalAccessory.get(slave.vaginalAccessory) || V.customItem.vaginalAccessory.get(slave.vaginalAccessory);
	if (dildo === undefined) {
		console.log("missing dildo: ", slave.vaginalAccessory);
		return 0;
	}
	return dildo.width || 0;
};

/**
 * @param {FC.SlaveState} slave
 * @returns {0|1|2}
 */
globalThis.dildoLength = function(slave) {
	const dildo = App.Data.vaginalAccessory.get(slave.vaginalAccessory) || V.customItem.vaginalAccessory.get(slave.vaginalAccessory);
	return dildo.length || 0;
};

/**
 * Returns the best vibe mode available between the dildo itself, and any attachment that may be present.
 * @param {FC.SlaveState} slave
 * @returns {0|1|2}
 */
globalThis.dildoVibeLevel = function(slave) {
	// Vaginal accessory/dildo
	const dildo = App.Data.vaginalAccessory.get(slave.vaginalAccessory) || V.customItem.vaginalAccessory.get(slave.vaginalAccessory);
	const dildoVibrationLevel = ((dildo) ? dildo.vibrates : 0) || 0;

	// Attachment, if present
	const vaginalAttachment = App.Data.slaveWear.vaginalAttachment.get(slave.vaginalAttachment) || 0;
	const vaginalAttachmentVibrationLevel = ((vaginalAttachment) ? vaginalAttachment.vibrates : 0) || 0;
	return Math.max(dildoVibrationLevel, vaginalAttachmentVibrationLevel);
};