code:
/**
 * @param {App.Entity.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 {App.Entity.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;
	}
	return true;
};

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

	/* call as SlaveStatsChecker.checkForLisp() */
	function hasLisp(slave) {
		if (V.disableLisping === 1 || !canTalk(slave)) {
			return false;
		}
		return (slave.lips > 70 || (slave.lipsPiercing + slave.tonguePiercing > 2) || slave.teeth === "gapped");
	}

	/** call as SlaveStatsChecker.modScore()
	 * @param {App.Entity.SlaveState} slave
	 * @returns {number} // I think
	 */
	function modScore(slave) {
		V.piercingScore = piercingScore(slave);
		V.tatScore = tatScore(slave);
		V.brandScore = brandScore(slave);
		V.scarScore = scarScore(slave);
		return V.tatScore + V.piercingScore + V.brandScore + V.scarScore;
	}

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

		if (slave.earPiercing > 0) {
			score += slave.earPiercing * 0.75 - 0.5;
		}
		if (slave.nosePiercing > 0) {
			score += slave.nosePiercing * 0.75 - 0.5;
		}
		if (slave.eyebrowPiercing > 0) {
			score += slave.eyebrowPiercing * 0.75 - 0.5;
		}
		if (slave.navelPiercing > 0) {
			score += slave.navelPiercing * 0.75 - 0.5;
		}
		if (slave.corsetPiercing > 0) {
			score += slave.corsetPiercing * 0.75 + 0.5;
		}
		if (slave.nipplesPiercing > 0) {
			score += slave.nipplesPiercing * 0.75 - 0.25;
		}
		if (slave.areolaePiercing > 0) {
			score += slave.areolaePiercing * 0.75 + 0.5;
		}
		if (slave.lipsPiercing > 0) {
			score += slave.lipsPiercing * 0.75 - 0.25;
		}
		if (slave.tonguePiercing > 0) {
			score += slave.tonguePiercing * 0.75 - 0.25;
		}
		if (slave.clitPiercing === 3) /* smart piercing */ {
			score += 1.25;
		} else if (slave.clitPiercing > 0) {
			score += slave.clitPiercing * 0.75 - 0.25;
		}

		if (slave.vaginaPiercing > 0) {
			score += slave.vaginaPiercing * 0.75 - 0.25;
		}
		if (slave.dickPiercing > 0) {
			score += slave.dickPiercing * 0.75 - 0.25;
		}
		if (slave.anusPiercing > 0) {
			score += slave.anusPiercing * 0.75 - 0.25;
		}

		return score;
	}

	/**
	 * helper function, not callable
	 * @param {App.Entity.SlaveState} 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.25;
		}
		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 {App.Entity.SlaveState} slave
	 * @returns {number}
	 */
	function brandScore(slave) {
		let score = 0;
		score += Object.getOwnPropertyNames(slave.brand).length;
		return score;
	}

	/**
	 * helper function, not callable
	 * @param {App.Entity.SlaveState} 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 = Object.keys(slave.scar);
			for (const bodypart of scars) {
				const bodyparts = Object.keys(slave.scar[bodypart]);
				for (const kind of bodyparts) {
					score += slave.scar[bodypart][kind];
				}
			}
		}
		return score;
	}

	/**
	 * call as SlaveStatsChecker.isModded()
	 * @param {App.Entity.SlaveState} 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 {App.Entity.SlaveState} 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.corsetPiercing === 0 && piercings < 3 && tattoos < 2 && brands < 2 && scars <= 1);
	}
}());

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

	if (ArcologyZero.FSSlimnessEnthusiastLaw === 1) {
		return (slimLawPass(slave) === 1);
	}
	if ((slave.boobs < 500) && (slave.butt < 3)) {
		if ((slave.muscles <= 30) && (ArcologyZero.FSPhysicalIdealist === "unset") && (slave.weight <= 10) && (ArcologyZero.FSHedonisticDecadence === "unset")) {
			slim = true;
		} else if (ArcologyZero.FSPhysicalIdealist !== "unset") {
			if ((ArcologyZero.FSPhysicalIdealistStrongFat === 1) && (slave.weight <= 30)) {
				slim = true;
			} else if (slave.weight <= 10) {
				slim = true;
			}
		} else if ((ArcologyZero.FSHedonisticDecadence !== "unset") && (slave.weight <= 30)) {
			if (ArcologyZero.FSHedonisticDecadenceStrongFat === 1) {
				slim = true;
			} else if (slave.muscles <= 30) {
				slim = true;
			}
		}
	}
	return slim;
};

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

	const arcology = V.arcologies[0];
	if (arcology.FSPhysicalIdealist === "unset" && arcology.FSHedonisticDecadenceStrongFat === 0 && slave.muscles > 30) {
		/* muscle check */
		genderLawPass = 0;
	}
	if (arcology.FSHedonisticDecadence !== "unset" || arcology.FSPhysicalIdealistStrongFat === 1) {
		/* weight check */
		if (slave.weight > 130 || slave.weight <= -30) {
			genderLawPass = 0;
		}
	} else if (Math.abs(slave.weight) > 30) {
		genderLawPass = 0;
	}
	if (arcology.FSAssetExpansionist !== "unset") {
		if (slave.boobs < 500 || slave.boobs > 1600 || slave.butt < 3 || slave.butt > 6) {
			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 {App.Entity.SlaveState} slave
 * @returns {number} 1: yes, 0: no
 */
globalThis.slimLawPass = function(slave) {
	let slimLawPass = 0;
	const ArcologyZero = V.arcologies[0];

	if (ArcologyZero.FSSlimnessEnthusiastLaw === 1) {
		if ((slave.boobs < 300) && (slave.butt <= 1) && (slave.waist <= 10)) {
			if ((ArcologyZero.FSPhysicalIdealist === "unset") && (ArcologyZero.FSHedonisticDecadenceStrongFat === 0) && (slave.muscles > 30)) {
				/* muscle check*/
				slimLawPass = 0;
			} else if ((ArcologyZero.FSHedonisticDecadence !== "unset") || (ArcologyZero.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 {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.heightPass = function(slave) {
	let arcology = V.arcologies[0];

	if (arcology.FSPetiteAdmiration !== "unset") {
		if (arcology.FSPetiteAdmirationLaw2 === 1) {
			if (slave.height < Height.mean(slave) - 5) {
				return true;
			}
		} else {
			if (slave.height < 160) {
				return true;
			}
		}
	} else if (arcology.FSStatuesqueGlorification !== "unset") {
		if (arcology.FSStatuesqueGlorificationLaw2 === 1) {
			if (slave.height + heelLength(slave) > Height.mean(slave) + 5) {
				return true;
			}
		} else {
			if (slave.height + heelLength(slave) >= 170) {
				return true;
			}
		}
	}
	return false;
};

/**
 * Returns the height, in cm, of a slave's heels
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
globalThis.heelLength = function(slave) {
	switch (slave.shoes) {
		case "pumps":
			// 2 inch heels
			return 5;
		case "platform shoes":
			// 3 inch platform, no heels
			return 8;
		case "heels":
			// 5 inch heels
			return 13;
		case "extreme heels":
			// 8 inch heels
			return 21;
		case "platform heels":
			// 8 inches, but not painful like extremes (3 inch platforms)
			return 21;
		case "extreme platform heels":
			// 12 inches! 8 inch heel, 4 inch platform
			return 30;
	}
	return 0;
};

/**
 * Returns slave bimbo body degree (FSIntellectualDependencyLawBeauty).
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
globalThis.bimboScore = function(slave) {
	let degree = 0;
	let mods = 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 (mods >= 10) {
		degree++;
	}
	if (V.piercingScore > 5) {
		degree++;
	}
	if (V.tatScore > 3) {
		degree++;
	}
	if (setup.highHeels.includes(slave.shoes)) {
		degree++;
	}
	if (slave.skin === "sun tanned" || slave.skin === "spray tanned") {
		degree++;
	}
	if (setup.sluttyClothes.includes(slave.clothes)) {
		degree++;
	}

	// degree can go far higher than the max to allow various FS combinations to take advantage.
	return Math.clamp(degree, 0, 6);
};

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

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

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

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

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

/**
 * @param {App.Entity.SlaveState} 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 {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.isSurgicallyImproved = function(slave) {
	return ((slave.boobsImplant > 0) && (slave.buttImplant > 0) && (slave.waist < -10) && (slave.lipsImplant > 0));
};

/**
 * @param {App.Entity.SlaveState} 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 {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.canGetPregnant = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.preg === -1) { /* contraceptives check */
		return false;
	} else if (!isFertile(slave)) { /* 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 {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.isFertile = function(slave) {
	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) {
		/* pregmod start */
		return false;
	} else if (slave.ovaryAge >= 47) {
		return false;
	} else if (slave.inflation > 2) {
		return false;
	} else if (slave.bellyImplant !== -1) {
		return false;
	} else if (slave.mpreg === 1 || slave.ovaries === 1) {
		if (slave.womb.length > 0) { // superfetation route
			if (slave.fertPeak !== 0) {
				return false;
			}
		}
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.canAchieveErection = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.dick <= 0) {
		return false;
	} else if (slave.dick < 11 && slave.drugs === "priapism agents") {
		return true;
	} else if (slave.dick > 6) {
		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 {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.canPenetrate = function(slave) {
	if (!slave) {
		return null;
	} else if (!canAchieveErection(slave)) {
		return false;
	} else if (slave.chastityPenis === 1) {
		return false;
	} else if (slave.dick > 7) {
		return false;
	}
	return true;
};

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

/**
 *
 * @param {App.Entity.SlaveState} 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 {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.canHear = function(slave) {
	if (!slave) {
		return null;
	}
	return ((slave.hears > -2) && (slave.earwear !== "deafening ear plugs"));
};

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

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

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.canHold = function(slave) {
	if (!slave) {
		return null;
	}
	return hasAnyArms(slave);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.canWalk = function(slave) {
	if (!slave) {
		return null;
	} else if (!hasBothLegs(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 && !setup.highHeels.includes(slave.shoes)) {
		return false;
	}
	return true;
};

/**
 * @param {App.Entity.SlaveState} 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;
};

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

/**
 * @param {App.Entity.SlaveState} 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 {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.tooFatSlave = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.weight > 190 + (slave.muscles / 5) && slave.physicalAge >= 18) {
		return true;
	} 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;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.tooBigBreasts = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.boobs > 30000 + (slave.muscles * 100) && slave.physicalAge >= 18) {
		return true;
	} else if (slave.boobs > 5000 + (slave.muscles * 10) && slave.physicalAge <= 3) {
		return true;
	} else if (slave.boobs > 10000 + (slave.muscles * 20) && slave.physicalAge <= 12) {
		return true;
	} else if (slave.boobs > 20000 + (slave.muscles * 50) && slave.physicalAge < 18) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.tooBigBelly = function(slave) {
	if (!slave) {
		return null;
	} else if (slave.belly >= 450000 + (slave.muscles * 2000) && slave.physicalAge >= 18) {
		return true;
	} else if (slave.belly >= 350000 + (slave.muscles * 1000) && slave.physicalAge >= 13) {
		return true;
	} else if (slave.belly >= 30000 + (slave.muscles * 500) && slave.physicalAge <= 3) {
		return true;
	} else if (slave.belly >= 150000 + (slave.muscles * 800) && slave.physicalAge <= 12) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} 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 {App.Entity.SlaveState} 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) {
		return true;
	} else if (slave.dick >= 45 + (slave.muscles * 0.3) && slave.physicalAge <= 12) {
		return true;
	} else if (slave.dick >= 68 + (slave.muscles * 0.4)) {
		return true;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} 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;
	}
	return false;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.isVegetable = function(slave) {
	if (!slave) {
		return false;
	}
	return (slave.fetish === "mindbroken");
};

/**
 * Returns the hair color the slave was (or would be) born with.
 *
 * @param {App.Entity.SlaveState} 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 {App.Entity.SlaveState} slave
 * @returns {string}
 */
globalThis.getGeneticSkinColor = function(slave) {
	if (slave.geneticQuirks.albinism === 2) {
		return slave.albinismOverride.skin;
	}
	return slave.origSkin;
};


code:
/* config.history.tracking = false;*/
// State.expired.disable;

/**
 * @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") {
	const 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;
		}
	};
	globalThis.interpolate = interpolate;
}

/**
 * @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 {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} 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 {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 * @returns {boolean}
 */
globalThis.canImpreg = function(slave1, slave2) {
	if (!slave1 || !slave2) {
		return null;
	} else if (slave2.dick < 1) {
		return false;
	} else if (slave2.balls < 1) {
		return false;
	} else if (slave2.chastityPenis === 1) {
		return false;
	} else if (slave2.pubertyXY === 0) {
		/* pregmod start */
		return false;
	} else if (slave2.vasectomy === 1) {
		return false;
	} else if (!canBreed(slave1, slave2)) {
		return false; /* pregmod end */
	} else if (!canGetPregnant(slave1)) {
		/* 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 {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 * @returns {boolean}
 */
globalThis.canFemImpreg = function(slave1, slave2) {
	if (!slave1 || !slave2) {
		return null;
	} else if (slave2.balls < 1) {
		return false;
	} else if (slave2.chastityVagina === 1) {
		return false;
	} else if (slave2.pubertyXY === 0) {
		/* pregmod start */
		return false;
	} else if (slave2.vasectomy === 1) {
		return false;
	} else if (!canBreed(slave1, slave2)) {
		return false; /* pregmod end */
	} else if (!canGetPregnant(slave1)) {
		/* includes chastity checks */
		return false;
	} else {
		return true;
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
globalThis.milkAmount = function(slave) {
	let milk;
	let calcs;
	if (!slave) {
		return null;
	}
	calcs = slave.boobs - slave.boobsImplant - slave.boobsMilk;
	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));
	milk += (slave.boobsMilk / 100);
	milk *= healthPenalty(slave);
	milk = Math.trunc(milk);
	milk = Math.clamp(milk, 1, 1000000000000000000);
	return milk;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
globalThis.cumAmount = function(slave) {
	let cum = 0;
	let calcs = 0;
	if (!slave) {
		return null;
	}
	if (slave.drugs === "testicle enhancement") {
		cum = ((slave.balls * 3.5) + 1);
	} else if (slave.drugs === "hyper testicle enhancement") {
		cum = ((slave.balls * 5) + 1);
	} else {
		cum = ((slave.balls * 2.5) + 1);
	}
	if (slave.ballType === "sterile") {
		cum *= 0.8;
	}
	if (slave.diet === "cum production") {
		cum *= 1.2;
	}
	calcs = (slave.hormoneBalance / 50);
	cum *= (1 - (calcs * 0.1));
	if (slave.scrotum === 0) {
		cum *= 0.8;
	}
	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 = Math.trunc(cum);
	cum = Math.clamp(cum, 1, 1000000000000000000);
	return cum;
};

/**
 * @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 {App.Entity.SlaveState} slave
 * @param {object} arcology
 * @returns {number}
 */
globalThis.repGainSacrifice = function(slave, arcology) {
	if (!slave || !arcology || arcology.FSAztecRevivalist === "unset" || 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 {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
globalThis.bodyguardSuccessorEligible = function(slave) {
	if (!slave) {
		return false;
	}
	return (slave.devotion > 50 && slave.muscles >= 0 && slave.weight < 100 && slave.boobs < 8000 && slave.butt < 10 && slave.belly < 5000 && slave.balls < 10 && slave.dick < 10 && slave.preg < 20 && slave.fuckdoll === 0 && slave.fetish !== "mindbroken" && canWalk(slave) && canHold(slave) && canSee(slave) && canHear(slave));
};

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

/**
 * @param {App.Entity.SlaveState} 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 {App.Entity.SlaveState} slave
 * @param {object} PC
 * @returns {number}
 */
globalThis.overpowerCheck = function(slave, PC) {
	let strength;

	if (V.arcologies[0].FSPhysicalIdealist !== "unset") {
		if (PC.title === 1) {
			strength = 130;
		} else {
			strength = 100;
		}
	} else if (PC.title === 1) {
		strength = 50;
	} else {
		strength = 30;
	}
	strength += (185-slave.height);
	strength -= (PC.belly/1000);
	strength += (PC.skill.warfare/3);
	strength -= (V.PC.health.shortDamage);

	return strength;
};

/**
 * returns array of IDs of all characters who impregnated slave
 * @param {App.Entity.SlaveState} slave
 * @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 {App.Entity.SlaveState} mother
 * @param {App.Entity.SlaveState} father
 * @returns {boolean}
 */
globalThis.isImpregnatedBy = function(mother, father, genepool=false) {
	return impregnatedBy(mother, genepool).includes(father.ID);
};

/**
 * @param {App.Entity.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 {App.Entity.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 {object} PC
 */
globalThis.generatePlayerPronouns = function(PC) {
	if (PC.title === 0) {
		PC.pronoun = App.Data.Pronouns.Kind.female;
	} else {
		PC.pronoun = App.Data.Pronouns.Kind.male;
	}
};

globalThis.printTrinkets = function() {
	function trinketPluralReplacer(desc) {
		let r;
		switch (desc) {
			/* not supported
				best in show ribbons
				napkins
				App.SlaveAssignment.porn trinkets
				wedding photos
			*/
			// should never have plurals
			case "a collection of diplomas from expensive schools":
			case "a framed low denomination piece of paper money from your native country":
			case "a battered old assault rifle":
			case "a framed picture of a slave with her sale price scrawled across the bottom":
			case "an artist's impression of an early arcology design":
			case "a framed copy of the first news story featuring yourself":
			case "a miniature model of your first arcology":
			case "a copy of the first porno you starred in":
			case "a framed picture of your late Master":
			case "your favorite handgun, whose sight has instilled fear in many":
			case "a news clipping of your first successful live hack":
			case "a damaged plate carrier bearing Daughters of Liberty insignia":
			case "a Daughters of Liberty flag that once hung in their forward command post within your arcology":
			case "a Daughters of Liberty brassard":
			case "a shot-torn flag of the failed nation whose militants attacked the Free City":
				return desc;
				// manual replacement
			case "a thank-you note from a MILF tourist whom you made feel welcome in the arcology":
				return "several thank-you notes from MILF tourists whom you made feel welcome in the arcology";
				// replacement by groups
			default:
				r = desc;
				if (desc.endsWith("citizen")) { // will not reduce spam from different future societies
					r = r.replace("message", "messages");
					r = r.replace("from a", "from");
					r = r.replace("a ", "several ");
					r = r.replace("citizen", "citizens");
					r = r.replace("number", "numbers");
					r = r.replace("test", "tests");
				} else if (desc.endsWith("acquaintance")) {
					r = r.replace("note", "notes");
					r = r.replace("from a", "from");
					r = r.replace("a ", "several ");
					r = r.replace("owner", "owners");
				}
				return r;
		}
	}

	if (V.trinkets.length === 0) {
		return "";
	}

	const trinkets = weightedArray2HashMap(V.trinkets);
	let trinketString = [];
	let plurals = false;

	for (const trinketDesc in trinkets) {
		if (trinkets[trinketDesc] === 1) {
			trinketString.push(trinketDesc);
		} else if (trinkets[trinketDesc] > 1) {
			trinketString.push(trinketPluralReplacer(trinketDesc));
			plurals = true;
		}
	}

	// depending on length of trinketString, add necessary conjunctions
	if (trinketString.length === 1) {
		if (plurals === false) {
			trinketString = `a single item: ${trinketString[0]}`;
		} else {
			trinketString = trinketString[0];
		}
	} else if (trinketString.length === 2 && plurals === false) {
		trinketString = `a couple of items: ${trinketString[0]}, and ${trinketString[1]}`;
	} else {
		trinketString[trinketString.length - 1] = `and ${trinketString[trinketString.length - 1]}`;
		trinketString = trinketString.join(", ");
	}
	return `There's a display case behind your desk, with ${trinketString}.`;
};

/**
 * @param {number} nmbr1
 * @param {number} nmbr2
 */
globalThis.pregNumberName = function(nmbr1, nmbr2) {
	let pt = "";
	let p1 = nmbr1 % 10;
	let p2 = ((nmbr1 % 100) - (nmbr1 % 10)) / 10;
	let p3 = ((nmbr1 % 1000) - (nmbr1 % 100)) / 100;
	switch (nmbr1) {
		case 1:
			pt += "bab";
			break;
		case 2:
			pt += "twin";
			break;
		default:
			switch (nmbr1) {
				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 (nmbr1 >= 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 (nmbr2 === 2) {
		if (nmbr1 === 1) {
			pt += "ie";
		}
		pt += "s";
	} else {
		if (nmbr1 === 1) {
			pt += "y";
		}
	}
	return pt;
};

repo_name: KnightBoulegard/fc

license: mit


code:
/**
 * @param {App.Entity.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);
	}
};


code:
Macro.add("textbox2", {
	handler: function() {
		if (this.args.length < 2) {
			const e = [];
			return this.args.length < 1 && e.push("variable name"), this.args.length < 2 && e.push("default value"), this.error(`no ${e.join(" or ")} specified`);
		}
		if (typeof this.args[0] !== "string") { return this.error("variable name argument is not a string"); }
		const t = this.args[0].trim();
		if (t[0] !== "$" && t[0] !== "_") { return this.error(`variable name "${this.args[0]}" is missing its sigil ($ or _)`); }
		Config.debug && this.debugView.modes({
			block: true
		});
		const r = Util.slugify(t);
		const a = this.args[1];
		const isNumber = typeof(a) === "number";
		const inputElement = document.createElement("input");
		let autofocus = false;
		let passage = void 0;
		let setargs = null;
		if (this.args.length > 3) {
			passage = this.args[2];
			autofocus = this.args[3] === "autofocus";
			if (!autofocus) {
				setargs = this.args[3];
			}
		} else if (this.args.length > 2) {
			if (this.args[2] === "autofocus") {
				autofocus = true;
			} else {
				passage = this.args[2];
			}
		}
		if (passage !== (void 0) && typeof(passage) === "object") {
			passage = passage.link;
		}
		if (!passage) {
			passage = State.passage;
		}

		function gotoPassage() {
			if (passage) {
				const currentScrollPosition = window.pageYOffset;
				const currentPassage = State.passage;
				if (setargs) {
					Scripting.evalTwineScript(setargs);
				}
				Engine.play(passage);
				if (currentPassage === passage) {
					Scripting.evalJavaScript(`window.scrollTo(0, ${currentScrollPosition});`);
				}
			}
		}

		function valueToNumberIfSame(v) {
			if (!isNumber) {
				return v;
			} // Do nothing
			try {
				return parseInt(v, 10);
			} catch (error) {
				return v;
			}
		}

		jQuery(inputElement).attr({
			id: `${this.name}-${r}`,
			name: `${this.name}-${r}`,
			// type: isNumber ? "number" : "text", /* TODO - hide spinner if we do this */
			tabindex: 0
		}).addClass(`macro-${this.name}`)
			.on("change", function() {
				State.setVar(t, valueToNumberIfSame(this.value));
			}).on("blur", function() {
				State.setVar(t, valueToNumberIfSame(this.value));
				// eslint-disable-next-line eqeqeq
				if (this.value != a) { // If the value has actually changed, reload the page. Note != and not !== because types might be different
					gotoPassage();
				}
			})
			.on("keypress", function(e) {
				e.which === 13 && (e.preventDefault(), State.setVar(t, valueToNumberIfSame(this.value)), gotoPassage());
			}).appendTo(this.output), State.setVar(t, a), inputElement.value = a, autofocus && (inputElement.setAttribute("autofocus", "autofocus"), postdisplay[`#autofocus:${inputElement.id}`] = function(e) {
			delete postdisplay[e], setTimeout(function() {
				return inputElement.focus();
			}, Engine.minDomActionDelay);
		});
	}
});


code:
/* Nicked off greyelf, works for replace textboxes */
globalThis.setReplaceTextboxMaxLength = function(storyVarName, maxLength) {
	const textboxId = `#textbox-${Util.slugify(storyVarName)}`;
	$(textboxId)
		.attr("maxlength", maxLength)
		.css({
			"min-width": "initial",
			"width": `${maxLength}em`,
			"padding": "3px 2px"
		});
};

/* Nicked off TheMadExile, works for non-replace textboxes */
globalThis.setTextboxMaxLength = function(storyVarName, maxLength) {
	const textboxId = `#textbox-${Util.slugify(storyVarName)}`;
	postdisplay[`${textboxId}-maxlength`] = function(taskName) {
		delete postdisplay[taskName];
		$(textboxId)
			.attr("maxlength", maxLength)
			.css({
				"min-width": "initial",
				"width": `${maxLength}em`,
				"padding": "3px 2px"
			});
	};
};


code:
/**
 * @callback passageLinkHandler
 * @returns {void}
 */
/**
 * Creates a HTML element with custom SugarCube attributes which works as a passage link
 *
 * The result works in the same way as the wiki markup in the SugarCube
 * @see https://www.motoslave.net/sugarcube/2/docs/#markup-html-attribute
 * @param {string} linkText link text
 * @param {string} passage the passage name to link to
 * @param {passageLinkHandler} [handler] setter text (optional)
 * @param {string} [tooltip=''] tooltip text (optional)
 * @param {string} [elementType='a'] element type (optional) default is 'a'.
 * Could be any of 'a', 'audio', img', 'source', 'video'
 * @returns {HTMLElement} element text
 *
 * @example
 * // equal to [[Go to town|Town]]
 * App.UI.passageLink("Go to town", "Town")
 */
App.UI.DOM.passageLink = function(linkText, passage, handler, tooltip = '', elementType = 'a') {
	let res = document.createElement(elementType);
	res.setAttribute("data-passage", passage);
	res.onclick = (ev) => {
		ev.preventDefault();
		if (handler) {
			handler();
		}
		Engine.play(passage);
	};

	if (tooltip) {
		res.title = tooltip;
	}
	res.textContent = linkText;
	return res;
};

/**
 * Returns link element for an assignment
 * @param {App.Entity.SlaveState} slave
 * @param {string} assignment
 * @param {string} [passage] passage to go to
 * @param {assignmentCallback} [action] action that changes slave state. The default one is a call to assignJob()
 * @param {string} [linkText]
 * @returns {HTMLAnchorElement}
 */
App.UI.DOM.assignmentLink = function(slave, assignment, passage, action, linkText) {
	let res = document.createElement("a");
	res.textContent = linkText;
	res.onclick = (e) => {
		e.preventDefault();
		if (action) {
			action(slave, assignment);
		} else {
			assignJob(slave, assignment);
		}
		if (passage !== '') {
			SugarCube.Engine.play(passage);
		}
	};
	return res;
};

/**
 * Creates a markup for a SugarCube link which executes given function with given arguments
 *
 * @param {string} linkText link text
 * @param {function(...any):void} handler callable object
 * @param {any|Array<any>} args arguments
 * @param {string} [passage] the passage name to link to
 * @param {string} [tooltip]
 * @returns {HTMLAnchorElement} link in SC markup
 */
App.UI.DOM.link = function(linkText, handler, args = [], passage = "", tooltip = "") {
	const hArgs = Array.isArray(args) ? args : [args];
	const link = document.createElement("a");
	link.textContent = linkText;
	link.title = tooltip;
	link.onclick = () => {
		handler(...hArgs);
		if (passage !== '') {
			SugarCube.Engine.play(passage);
		}
	};
	return link;
};

/**
 * Creates a span for an link with tooltip containing the reasons why it is disabled
 * @param {string} link
 * @param {string[]} reasons
 * @returns {HTMLSpanElement}
 */
App.UI.DOM.disabledLink = function(link, reasons) {
	/** @type {HTMLElement} */
	let tooltip;
	if (reasons.length === 1) {
		tooltip = document.createElement("span");
		tooltip.textContent = reasons[0];
	} else {
		tooltip = document.createElement("div");
		let ul = document.createElement("ul");
		tooltip.appendChild(ul);
		for (const li of reasons.map(r => {
			const li = document.createElement("li");
			li.textContent = r;
			return li;
		})) {
			ul.appendChild(li);
		}
	}
	tooltip.className = "tooltip";
	let res = document.createElement("span");
	res.textContent = link;
	res.className = "textWithTooltip";
	res.appendChild(tooltip);
	return res;
};

/**
 * @param {string} tag - valid HTML tag
 * @param {string|Node} [content]
 * @param {string|Array<string>} [classNames]
 * @returns {HTMLElement}
 */
// @ts-ignore
App.UI.DOM.makeElement = function(tag, content, classNames) {
	const element = document.createElement(tag);
	if (classNames !== undefined) {
		if (Array.isArray(classNames)) {
			element.classList.add(...classNames);
		} else {
			element.classList.add(classNames);
		}
	}
	if (content) {
		element.append(content);
	}
	return element;
};

/**
 * @param {string} tag - valid HTML tag
 * @param {ParentNode} parent
 * @param {string|Node} [content]
 * @param {string|Array<string>} [classNames]
 * @returns {HTMLElement}
 */
// @ts-ignore
App.UI.DOM.appendNewElement = function(tag, parent, content, classNames) {
	// @ts-ignore
	const element = App.UI.DOM.makeElement(tag, content, classNames);
	parent.append(element);
	return element;
};

/**
 * @param {string} linkText
 * @param {string|Node} newContent
 * @returns {HTMLSpanElement}
 */
App.UI.DOM.linkReplace = function(linkText, newContent) {
	const span = document.createElement("span");
	span.append(App.UI.DOM.link(linkText, () => {
		span.innerHTML = "";
		span.append(newContent);
	}));
	return span;
};

/**
 * @param {string} selector for jQuery
 * @param {JQuery.Node} newContent
 */
App.UI.DOM.replace = function(selector, newContent) {
	$(selector).empty().append(newContent);
};

/**
 * @param {string} passage
 * @returns {Element}
 */
App.Utils.passageElement = function(passage) {
	return document.querySelector(`tw-passagedata[name="${passage}"]`);
};

/**
 * @param {Node|string} content
 * @returns {DocumentFragment}
 */
App.UI.DOM.combineNodes = function(...content) {
	let fragment = document.createDocumentFragment();
	fragment.append(...content);
	return fragment;
};

/**
 * @param {string|number} defaultValue
 * @param {function(string):void|function(number):void} onEnter - accepts number if numberOnly = true, otherwise string
 * @param {boolean} [numberOnly]
 * @returns {HTMLInputElement}
 */
App.UI.DOM.makeTextBox = function(defaultValue, onEnter, numberOnly = false) {
	const input = document.createElement("input");
	input.type = "text";
	input.value = defaultValue;

	let updateValue;
	if (numberOnly) {
		/*
		We could use input.type = "number", but at least in firefox submitting an invalid value will set input.value
		to 0 and trigger a change event we can't distinguish from setting the value to 0 explicitly.
		The workaround is resetting the value to the last known valid value and not triggering onEnter.
		*/
		let oldValue = defaultValue;
		updateValue = event => {
			const newValue = Number(event.target.value);
			if (!Number.isNaN(newValue)) {
				onEnter(newValue);
				oldValue = newValue;
			} else {
				// reset the value to the last known valid value
				event.target.value = oldValue;
			}
		};
	} else {
		updateValue = e => {
			onEnter(e.target.value);
		};
	}
	input.addEventListener('change', updateValue);

	return input;
};

/**
 * @param {string|number} defaultValue
 * @param {function(string):void} onEnter
 * @returns {HTMLInputElement}
 */
App.UI.DOM.colorInput = function(defaultValue, onEnter) {
	const input = document.createElement("input");
	input.type = "color";
	input.value = defaultValue;

	input.addEventListener('change', e => { onEnter(e.target.value); });

	return input;
};

/**
 * Concats an array of DOM nodes or strings into a human readable list.
 *
 * @param {Array<Node|string>} content
 * @param {string} [delimiter]
 * @param {string} [lastDelimiter]
 * @returns {DocumentFragment|string}
 */
App.UI.DOM.arrayToList = function(content, delimiter = ", ", lastDelimiter = " and ") {
	if (content.length === 0) {
		return "none";
	}
	if (content.length === 1) {
		return content[0];
	}
	const fragment = document.createDocumentFragment();
	const last = content.pop();
	for (let i = 0; i < content.length; i++) {
		fragment.append(content[i]);
		if (i < content.length - 1) {
			fragment.append(delimiter);
		}
	}
	content.push(last); // don't leave the array modified
	fragment.append(lastDelimiter, last);
	return fragment;
};

/**
 * @param {function (*):void} callback
 * @param {Object.<string, *>} options
 * @param {*|function (): *} currentValue
 * @param {string} [description]
 * @returns {HTMLSpanElement}
 */
App.UI.DOM.Widgets.optionSelector = function(callback, options, currentValue, description) {
	const res = document.createElement("span");
	res.classList.add("optionMacro");
	if (description) {
		App.UI.DOM.appendNewElement("span", res, description, "optionDescription");
	}
	const curVal = (currentValue instanceof Function) ? currentValue() : currentValue;
	const valueNode = App.UI.DOM.appendNewElement("span", res, null, "optionValue");
	for (const s in options) {
		const v = options[s];
		const choice = App.UI.DOM.appendNewElement("span", valueNode, null, "optionMacroOption");
		if (v === curVal) {
			choice.classList.add("optionMacroSelected");
			choice.innerText = s;
		} else {
			choice.append(App.UI.DOM.link(s, () => { callback(v); }));
		}
	}
	return res;
};

/**
 * @param {string} text
 * @returns {HTMLElement}
 */
App.Utils.htmlToElement = function(text) {
	const template = document.createElement("template");
	text = text.trim(); // Never return a text node of whitespace as the result
	template.innerHTML = text;
	// @ts-ignore
	return template.content.firstChild;
};

/*
 * Shows tooltips if they are enabled. Must be called on every passage load, because <body> gets reset.
 */
$(document).on(":passagestart", event => {
	if (V.tooltipsEnabled === 1) {
		event.content.classList.add("tooltipsEnabled");
	}
});

/**
 * Show a list of links (or disabled links) as a delimited strip
 * @param {Node[]} links
 * @returns {HTMLUListElement}
 */
App.UI.DOM.generateLinksStrip = function(links) {
	const strip = document.createElement('ul');
	strip.className = "choicesStrip";

	links.reduce((list, lnk) => {
		const li = document.createElement("li");
		li.appendChild(lnk);
		list.appendChild(li);
		return list;
	}, strip);
	return strip;
};

/*
	<<includeDOM element>>
	Simply inserts a given DOM element.
 */
Macro.add("includeDOM", {
	handler() {
		// Basically the same as <<set>>, we just use whatever Scripting.evalJavaScript(this.args.full) returns instead
		// of discarding it.
		try {
			this.output.append(Scripting.evalJavaScript(this.args.full));
		} catch (ex) {
			return this.error(`bad evaluation: ${typeof ex === 'object' ? `${ex.name}: ${ex.message}` : ex}`, null,
				ex.stack);
		}
	}
});