code:
/* Major props to the anons who worked together to forge the Super Pregnancy Project. Let your legacy go unforgotten.*/

/**
 * @param {FC.HumanState} s
 * @returns {number}
 */
globalThis.getPregBellySize = function(s) {
	let targetLen;
	let gestationWeek = s.preg;
	let fetuses = s.pregType;
	let phi = 1.618;

	if (gestationWeek <= 32) {
		targetLen = ((0.00006396 * Math.pow(gestationWeek, 4)) - (0.005501 * Math.pow(gestationWeek, 3)) + (0.161 * Math.pow(gestationWeek, 2)) - (0.76 * gestationWeek) + 0.208);
	} else if (gestationWeek <= 106) {
		targetLen = ((-0.0000004675 * Math.pow(gestationWeek, 4)) + (0.0001905 * Math.pow(gestationWeek, 3)) - (0.029 * Math.pow(gestationWeek, 2)) + (2.132 * gestationWeek) - 16.575);
	} else {
		targetLen = ((-0.00003266 * Math.pow(gestationWeek, 2)) + (0.076 * gestationWeek) + 43.843);
	}

	return ((4 / 3) * (Math.PI) * (phi / 2) * (Math.pow((targetLen / 2), 3)) * fetuses);
};

/** calculates and returns expected ovum count during conception
 * @param {FC.HumanState} actor
 */
globalThis.setPregType = function(actor) {
	/* IMHO rework is possible. Can be more interesting to play, if this code will take in account more body conditions - age, fat, food, hormone levels, etc. */

	let ovum = jsRandom(actor.pregData.normalOvaMin, actor.pregData.normalOvaMax); // for default human profile it's always 1.
	let fertilityStack = 0; // adds an increasing bonus roll for stacked fertility drugs
	let wombCapacity = Math.floor(actor.pregAdaptation / 15); // Used for a gene mod
	let minFertStack = 0;
	// reproduction formula applies to PCs that eat slave food, and slaves that have been here for at least one week
	const reproductionFormulaApplies = (V.reproductionFormula === 1) && (actor.ID === SID.PC ? !canEatFood(asPlayer(actor)) : (V.week - asSlave(actor).weekAcquired > 0));
	// master suite fertility applies when the option is set and the actor is a slave that is in the master suite
	const masterSuiteFertilityApplies = V.masterSuitePregnancyFertilitySupplements === 1 && asSlave(actor) && [Job.MASTERSUITE, Job.CONCUBINE].includes(asSlave(actor).assignment);

	/* Suggestion for better animal pregnancy support - usage of another variable then ovum for fertility drugs bonus, and then adding actor.pregData.drugsEffect multiplier to it before adding to ovum. Example:

		let bonus = 0;

		... (code below where ovum changed to bonus)

		bonus *= actor.pregData.drugsEffect;
		ovum += bonus;

	*/

	if (actor.broodmother < 1) { // Broodmothers should be not processed here. Necessary now.
		if (typeof actor.readyOva === "number" && actor.readyOva !== 0) {
			ovum = actor.readyOva; // just single override; for delayed impregnation cases
		} else {
			if (actor.eggType === "horse" || actor.eggType === "cow") {
				if (actor.geneticQuirks.fertility === 2 && actor.geneticQuirks.hyperFertility === 2) { // Do not mix with sperm
					ovum += jsEither([0, 0, 0, 0, 0, 0, 1]);
					fertilityStack += 0.8;
				} else if (actor.geneticQuirks.hyperFertility === 2) { // Predisposed to multiples
					fertilityStack += 0.4;
				} else if (actor.geneticQuirks.fertility === 2) { // Predisposed to twins
					fertilityStack += 0.2;
				}
				if (actor.ovaImplant === "fertility") {
					fertilityStack += 0.3;
				}
				if (actor.hormones === 2) {
					fertilityStack += 0.2;
				}
				if (actor.hormoneBalance >= 200) {
					fertilityStack += 0.3;
				}
				if (actor.diet === "fertility") {
					fertilityStack += 0.3;
				}
				if (masterSuiteFertilityApplies) {
					fertilityStack += 0.5;
				}
				if (reproductionFormulaApplies) {
					fertilityStack += 0.2;
				}
				if (actor.drugs === "super fertility drugs") {
					fertilityStack += 1.6;
				} else if (actor.drugs === "fertility drugs") {
					fertilityStack += 0.6;
				}
				fertilityStack = Math.floor(fertilityStack);
				if (V.seeHyperPreg === 1) {
					if (actor.drugs === "super fertility drugs") {
						ovum += jsRandom(0, fertilityStack * 2);
					} else {
						ovum += jsRandom(0, fertilityStack);
					}
					if (actor.ovaImplant === "sympathy") {
						ovum *= 2;
					}
				} else {
					ovum += jsRandom(0, fertilityStack);
					if (actor.ovaImplant === "sympathy") {
						ovum *= 2;
						if (ovum > 4) {
							ovum = 4;
						}
					} else if (ovum > 3) {
						ovum = 3;
					}
				}
			} else if (actor.eggType === "dog") {
				if (actor.geneticQuirks.fertility === 2 && actor.geneticQuirks.hyperFertility === 2) { // Do not mix with sperm
					ovum += jsEither([1, 2, 2, 3]);
					fertilityStack++;
					fertilityStack++;
					fertilityStack++;
				} else if (actor.geneticQuirks.hyperFertility === 2) { // Predisposed to multiples
					ovum += jsEither([0, 1, 1, 2]);
					fertilityStack++;
					fertilityStack++;
				} else if (actor.geneticQuirks.fertility === 2) { // Predisposed to twins
					ovum += jsEither([0, 0, 0, 0, 1]);
					fertilityStack++;
				}
				if (actor.ovaImplant === "fertility") {
					ovum += jsEither([0, 0, 0, 0, 1]);
					fertilityStack++;
				}
				if (actor.hormones === 2) {
					ovum += jsEither([0, 0, 0, 1, 1, 1, 1, 2]);
					fertilityStack++;
				}
				if (actor.hormoneBalance >= 200) {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2]);
					fertilityStack++;
				}
				if (actor.diet === "fertility") {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 1]);
					fertilityStack++;
				}
				if (masterSuiteFertilityApplies) {
					ovum += jsEither([0, 0, 0, 1, 1, 2, 2, 2, 3, 3]);
					fertilityStack++;
					fertilityStack++;
				}
				if (reproductionFormulaApplies) {
					fertilityStack++;
				}
				if (actor.drugs === "super fertility drugs") {
					ovum += jsEither([1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5]);
					fertilityStack++;
					fertilityStack++;
					fertilityStack++;
					fertilityStack++;
					fertilityStack++;
				} else if (actor.drugs === "fertility drugs") {
					ovum += jsEither([0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3]);
					fertilityStack++;
				}
				if (V.seeHyperPreg === 1) {
					if (actor.drugs === "super fertility drugs") {
						ovum += jsRandom(0, fertilityStack * 2);
					} else {
						ovum += jsRandom(0, fertilityStack);
					}
					if (actor.ovaImplant === "sympathy") {
						ovum *= 2;
					}
				} else {
					ovum += jsRandom(0, fertilityStack);
					if (actor.ovaImplant === "sympathy") {
						ovum *= 2;
						if (ovum > 8) {
							ovum = jsEither([6, 8]);
						}
					} else if (ovum > 8) {
						ovum = jsRandom(6, 8);
					}
				}
			} else if (actor.eggType === "pig") {
				if (actor.geneticQuirks.fertility === 2 && actor.geneticQuirks.hyperFertility === 2) { // Do not mix with sperm
					ovum += jsRandom(4, 8);
					fertilityStack += 16;
				} else if (actor.geneticQuirks.hyperFertility === 2) { // Predisposed to multiples
					ovum += jsRandom(2, 6);
					fertilityStack += 10;
				} else if (actor.geneticQuirks.fertility === 2) { // Predisposed to twins
					ovum += jsRandom(2, 4);
					fertilityStack += 6;
				}
				if (actor.ovaImplant === "fertility") {
					ovum += jsRandom(4, 12);
					fertilityStack += 6;
				}
				if (actor.hormones === 2) {
					ovum += jsRandom(0, 4);
					fertilityStack += 3;
				}
				if (actor.hormoneBalance >= 200) {
					ovum += jsRandom(0, 4);
					fertilityStack += 3;
				}
				if (actor.diet === "fertility") {
					ovum += jsRandom(4, 10);
					fertilityStack += 6;
				}
				if (masterSuiteFertilityApplies) {
					ovum += jsRandom(8, 16);
					fertilityStack += 10;
				}
				if (reproductionFormulaApplies) {
					fertilityStack += 2;
				}
				if (actor.drugs === "super fertility drugs") {
					ovum += jsRandom(10, 40);
					fertilityStack += 32;
				} else if (actor.drugs === "fertility drugs") {
					ovum += jsRandom(10, 20);
					fertilityStack += 16;
				}
				if (V.seeHyperPreg === 1) {
					if (actor.drugs === "super fertility drugs") {
						ovum += jsRandom(fertilityStack / 2, fertilityStack * 2);
					} else {
						ovum += jsRandom(fertilityStack / 4, fertilityStack);
					}
					if (actor.ovaImplant === "sympathy") {
						ovum *= 2;
					}
				} else {
					ovum += jsRandom(0, fertilityStack);
					if (actor.ovaImplant === "sympathy") {
						ovum *= 2;
						if (ovum > 76) {
							ovum = jsEither([70, 72, 74, 76]);
						}
					} else if (ovum > 75) {
						ovum = jsRandom(60, 75);
					}
				}
			} else {
				if (actor.geneticQuirks.fertility === 2 && actor.geneticQuirks.hyperFertility === 2) { // Do not mix with sperm
					ovum += jsEither([1, 2, 2, 3]);
					fertilityStack++;
					fertilityStack++;
					fertilityStack++;
					fertilityStack++;
				} else if (actor.geneticQuirks.hyperFertility === 2) { // Predisposed to multiples
					ovum += jsEither([0, 1, 1, 2]);
					fertilityStack++;
					fertilityStack++;
				} else if (actor.geneticQuirks.fertility === 2) { // Predisposed to twins
					ovum += jsEither([0, 0, 0, 0, 1]);
					fertilityStack++;
				} else {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); // base chance for twins
				}
				if (actor.ovaImplant === "fertility") {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
					fertilityStack++;
				}
				if (actor.hormones === 2) {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2]);
					fertilityStack++;
				}
				if (actor.hormoneBalance >= 200) {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2]);
					fertilityStack++;
				}
				if (actor.diet === "fertility") {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
					fertilityStack++;
				}
				if (actor.ID === SID.PC) {
					if (asPlayer(actor).forcedFertDrugs > 0) {
						ovum += jsEither([1, 1, 1, 1, 1, 2, 2, 2, 3]);
						fertilityStack++;
						fertilityStack++;
					}
				} else {
					if (masterSuiteFertilityApplies) {
						ovum += jsEither([0, 0, 0, 1, 1, 2, 2, 2, 3, 3]);
						fertilityStack++;
						fertilityStack++;
					}
				}
				if (reproductionFormulaApplies) {
					fertilityStack++;
				}
				if (actor.drugs === "super fertility drugs") {
					ovum += jsEither([1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5]);
					fertilityStack++;
					fertilityStack++;
					fertilityStack++;
					fertilityStack++;
					fertilityStack++;
				} else if (actor.drugs === ConsumerDrug.ENHANCE_FERTILITY) {
					ovum += jsEither([0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2]);
					fertilityStack++;
				} else if (actor.drugs === "fertility drugs") {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3]);
					fertilityStack++;
				}
				if (actor.geneMods.livestock === 1) {
					ovum += Math.max(0, ovum - wombCapacity);
				}
				if (actor.geneMods.progenitor === 1) {
					minFertStack = fertilityStack;
					fertilityStack += wombCapacity;
				}
				if (V.seeHyperPreg === 1) {
					if (actor.drugs === "super fertility drugs") {
						ovum += jsRandom(minFertStack, fertilityStack * 2);
					} else {
						ovum += jsRandom(minFertStack, fertilityStack);
					}
					if (actor.ovaImplant === "sympathy") {
						ovum *= 2;
					}
				} else {
					ovum += jsRandom(minFertStack, fertilityStack);
					if (actor.ovaImplant === "sympathy") {
						ovum *= 2;
						if (ovum > 12) {
							ovum = jsEither([10, 12]);
						}
					} else if (ovum > 12) {
						ovum = jsRandom(6, 12);
					}
				}
				if (actor.ID === SID.PC) { // temp until player birth is redone
					ovum = Math.clamp(ovum, 0, 8);
				}
			}
		}
	}

	if (actor.geneticQuirks.superfetation === 2 && actor.womb.length > 0) {
		let ftVol = FetusGetPrediction(actor, actor.pregData.normalBirth);
		let cmVol = ftVol * actor.womb.length;
		let maxVol = actor.pregAdaptation * 2000;
		if (V.seeHyperPreg === 0) {
			maxVol /= 10; // without hyperpreg enabled it's limited to be roughly ten times smaller.
		}
		let freeVol = maxVol - cmVol;
		let coeff = ((maxVol / actor.womb.length) / (freeVol / ftVol)) / 2; // more divide to 2 is to balance for ensured 1 ova even if over limit.

		if (coeff < 0) { coeff = 0; }

		if (V.seeHyperPreg !== 0) {
			// This is a second chance for implantation.
			// Should be affected only by chemical and genetic for easier implantation.
			// Not directly related to ova count, only to body/womb condition.
			// Raise successful implantation chance with already overfull womb. AFTER previous check.
			coeff += jsRandom(0, fertilityStack / 2);
		}

		if (ovum > coeff) {
			ovum = coeff;
		}

		/* for future, if monthly cycle will be a thing to implement, this will be useful.
		if (ovum < 0)
			ovum = jsRandom(coeff/fertilityStack, 1);
		*/
		if (ovum < 1) { ovum = 1; }

		// console.log("Name: ", actor.slaveName, " ftVol:", ftVol, " cmVol:", cmVol, " maxVol:", maxVol, " freeVol:", freeVol, " coeff:", coeff, " fertilityStack: ", fertilityStack, " ovum: ", ovum);
	}

	return Math.ceil(ovum);
};

/** Attempt to get a slave pregnant
 * Penetrative ability, ability to become pregnant, and canBreed() must be checked outside of this. Designed to assume .eggType === "human".
 * @param {FC.HumanState} target is the slave to get pregnant. Also accepts the PC.
 * @param {number} chance is the % chance to conceive.
 * @param {0|1|2} hole control's the hole involved (0 - vagina, 1 - ass, 2 - both). .mpreg did this.
 * @param {number} [fatherID] is the ID of her sire or 0 if undefined.
 * @returns {string} Returns "${He} has become pregnant." if relevant
 */
globalThis.knockMeUp = function(target, chance, hole, fatherID) {
	let r = ``;
	if (V.seePreg !== 0) {
		if (target.geneMods.progenitor === 1) {
			chance += 20;
		}
		let father = findFather(fatherID);
		if (father !== undefined) {
			if (father.geneMods.aggressiveSperm === 1) {
				chance += 75;
				// While I would like to give a chance of self-impreg here, the risk of recursion seems high.
			}
			if (father.geneticQuirks.potent === 2) {
				chance *= 1.5;
			}
			if (V.seeIncest === 0 && areRelated(target, father)) {
				chance = -100;
			}
		}
		// eslint-disable-next-line no-nested-ternary
		if (jsRandom(0, 99) < (chance + (V.reproductionFormula * ((target.pregSource <= 0) ? ((target.ID === SID.PC) ? 0 : 10) : 20)))) {
			if (target.mpreg === hole || hole === 2) {
				if (target.pregWeek <= 0) {
					target.preg = 1;
					target.pregSource = (!fatherID ? SID.GENERIC : fatherID);
					if (target.ID !== SID.PC) {
						target.pregWeek = 1;
					}
				}

				target.pregType = setPregType(target);
				WombImpregnate(target, target.pregType, fatherID, 1);

				target.pregKnown = 1;
				if (target.ID === SID.PC) {
					/* r += "You have gotten pregnant."; */
				} else {
					const {He} = getPronouns(target);
					r += `<span class="lime">${He} has become pregnant.</span>`;
				}
				if (V.menstruation === 1) {
					target.fertPeak = 1;
				}
				if (target.geneticQuirks.superfetation === 2 && target.womb.length > 0) {
					if (V.seeHyperPreg === 1) {
						target.fertPeak = 1;
					} else {
						target.fertPeak = 4;
					}
				}
			}
		}
	}
	return r;
};

/** Attempt to get an actor pregnant with another actor's child.
 * Assumes that sperm is at the point of meeting the egg, so check penetrative ability outside of this.
 * Technically deprecates canImpreg() and canFemImpreg().
 * @param {FC.HumanState} mother is the actor attempting to become pregnant.
 * @param {number} chance is the % chance to conceive.
 * @param {0|1|2} hole control's the hole involved (0 - vagina, 1 - ass, 2 - both).
 * @param {FC.HumanState} father is the actor doing the impregnating.
 * @returns {string} Returns "${He} has become pregnant." if relevant
 */
globalThis.tryKnockMeUp = function(mother, chance, hole, father) {
	let r = ``;
	if (V.seePreg !== 0) {
		if (canBreed(mother, father)) {
			if (mother.geneMods.progenitor === 1) {
				chance += 20;
			}
			if (father.geneMods.aggressiveSperm === 1) {
				chance += 75;
			}
			if (V.reproductionFormula) {
				if (mother.ID !== SID.PC) {
					chance += 10;
				}
				if (father.ID !== SID.PC) {
					chance += 10;
				}
			}
			if (father.geneticQuirks.potent === 2) {
				if (chance > 0) {
					chance *= 1.5;
				} else {
					chance += 10;
				}
			}
			if (!isVirile(father) || !isFertile(mother) || (V.seeIncest === 0 && areRelated(mother, father))) {
				chance = -10000;
			}
			if (mother.preg === -1) {
				chance -= 350; // Allows making sperm so potent that they can overpower contraceptives. Shouldn't be possible at this value, but why stop a player from tweaking this if it's their thing?
			}
			if (mother.eggType !== father.ballType) { // Special case for fantasy races to create half-breeds.
				chance -= 20;
			}
			if (random(0, 99) < chance) {
				if (mother.mpreg === hole || hole === 2) {
					if (mother.pregWeek <= 0) {
						mother.preg = 1;
						mother.pregSource = father.ID;
						if (mother.ID !== SID.PC) {
							mother.pregWeek = 1;
						}
					}

					mother.pregType = setPregType(mother);
					WombImpregnate(mother, mother.pregType, father.ID, 1);

					mother.pregKnown = 1;
					if (mother.ID === SID.PC) {
						/* r += "You have gotten pregnant."; */
					} else {
						const {He, him} = getPronouns(mother);
						if (father.ID === SID.PC) {
							r += `<span class="lime">${He} has become pregnant.</span>`;
						} else {
							r += `<span class="lime">${father.slaveName} has gotten ${him} pregnant.</span>`;
						}
					}
					if (V.menstruation === 1) {
						mother.fertPeak = 1;
					}
					if (mother.geneticQuirks.superfetation === 2 && mother.womb.length > 0) {
						if (V.seeHyperPreg === 1) {
							mother.fertPeak = 1;
						} else {
							mother.fertPeak = 4;
						}
					}
				}
			}
		}
	}
	return r;
};

/** Returns the amount of fetuses currently destined (reserved) for the incubator @returns {number} */
globalThis.getIncubatorReserved = function() {
	return FetusGlobalReserveCount("incubator");
};

/** Returns the amount of fetuses currently destined (reserved) for the nursery @returns {number} */
globalThis.getNurseryReserved = function() {
	return FetusGlobalReserveCount("nursery");
};

/** Find a slave's by ID, if they are also one of your slaves, wherever they may be
 * Note: not specific to fathers, though that's the most common use.
 * @param {number} fatherID
 * @returns {FC.HumanState}
 */
globalThis.findFather = function(fatherID) {
	/** @type {FC.HumanState} */
	let father;
	if (fatherID === -1) {
		father = V.PC;
	}
	father = getSlave(fatherID, true);

	if (father === undefined) {
		// fast methods didn't find them so do a search through all HumanState objects
		father = getHumanStates(true).find((s) => s.ID === fatherID);
	}

	return father;
};

/* not to be used until that last part is defined. It may become slave.boobWomb.volume or some shit
 * TODO update App.Medicine.fleshSize() when boobsWombVolume comes unto existence
*/
/**
 * @param {FC.SlaveState} slave
 * @returns {number}
 */
globalThis.getBaseBoobs = function(slave) {
	return slave.boobs - slave.boobsImplant - slave.boobsMilk - slave.boobsWombVolume;
};

/**
 * Terminate a pregnancy without birth (i.e. miscarriage/abortion), while automatically applying the correct postpartum length
 * @param {FC.HumanState} slave
 */
globalThis.TerminatePregnancy = function(slave) {
	if (slave.bellyPreg > 1500) {
		// late term - highly fertile slaves spring back quicker
		if ((slave.geneticQuirks.fertility + slave.geneticQuirks.hyperFertility >= 4) || slave.geneMods.progenitor === 1) {
			slave.pregWeek = -2;
		} else if (slave.geneticQuirks.hyperFertility > 1) {
			slave.pregWeek = -3;
		} else {
			slave.pregWeek = -4;
		}
	} else if (slave.pregWeek >= 4) {
		// still early
		slave.pregWeek = -2;
	} else if (slave.pregWeek > 0) {
		// very early
		slave.pregWeek = -1;
	}
	if (V.menstruation === 1) {
		slave.fertPeak = 2;
	}
	WombFlush(slave);
	SetBellySize(slave);
};


code:
App.Utils.Pronouns = class {
	/**
	 * @param {{pronoun: number}} obj
	 */
	constructor(obj) {
		/** @type {FC.Data.Pronouns.Definition} */
		this._pronouns = obj.pronoun < App.Data.Pronouns.Kind.custom
			? App.Data.Pronouns.Std[obj.pronoun]
			: V.customPronouns[obj.pronoun];
	}

	get pronoun() { return this._pronouns.pronoun; }
	get possessivePronoun() { return this._pronouns.possessivePronoun; }
	get possessive() { return this._pronouns.possessive; }
	get object() { return this._pronouns.object; }
	get objectReflexive() { return this._pronouns.objectReflexive; }
	get noun() { return this._pronouns.noun; }

	get Pronoun() { return capFirstChar(this.pronoun); }
	get PossessivePronoun() { return capFirstChar(this.possessivePronoun); }
	get Possessive() { return capFirstChar(this.possessive); }
	get Object() { return capFirstChar(this.object); }
	get ObjectReflexive() { return capFirstChar(this.objectReflexive); }
	get Noun() { return capFirstChar(this.noun); }

	get he() { return this.pronoun; }
	get him() { return this.object; }
	get his() { return this.possessive; }
	get himself() { return this.objectReflexive; }
	get boy() { return this.noun; }

	get He() { return this.Pronoun; }
	get Him() { return this.Object; }
	get His() { return this.Possessive; }
	get Himself() { return this.ObjectReflexive; }
	get Boy() { return this.Noun; }

	get she() { return this.pronoun; }
	get her() { return this.object; }
	get hers() { return this.possessivePronoun; }
	get herself() { return this.objectReflexive; }
	get girl() { return this.noun; }

	get She() { return this.Pronoun; }
	get Her() { return this.Object; }
	get Hers() { return this.PossessivePronoun; }
	get Herself() { return this.ObjectReflexive; }
	get Girl() { return this.Noun; }

	get woman() { return this.noun === "girl" ? "woman" : "man"; }
	get women() { return this.noun === "girl" ? "women" : "men"; }
	get loli() { return this.noun === "girl" ? "loli" : "shota"; }

	get Woman() { return capFirstChar(this.woman); }
	get Women() { return capFirstChar(this.women); }
	get Loli() { return capFirstChar(this.loli); }

	get daughter() { return this.noun === "girl" ? "daughter" : "son"; }
	get Daughter() { return capFirstChar(this.daughter); }

	get sister() { return this.noun === "girl" ? "sister" : "brother"; }
	get Sister() { return capFirstChar(this.sister); }

	// TODO: remove this once overhauled
	// eslint-disable-next-line sonarjs/no-all-duplicated-branches
	get wife() { return this.noun === "girl" ? "wife" : "wife"; }
	get Wife() { return capFirstChar(this.wife); }
	// TODO: remove this once overhauled
	// eslint-disable-next-line sonarjs/no-all-duplicated-branches
	get wives() { return this.noun === "girl" ? "wives" : "wives"; }
	get Wives() { return capFirstChar(this.wives); }

	get mother() { return this.noun === "girl" ? "mother" : "father"; }
	get Mother() { return capFirstChar(this.mother); }

	get father() { return this.noun === "girl" ? "mother" : "father"; }
	get Father() { return capFirstChar(this.father); }

	/**
	 * @param {string} suffix
	 * @returns {{[key: string]: string}}
	 */
	appendSuffix(suffix) {
		/** @type {{[key: string]: string}} */
		const r = {};
		// "constructor" will be the first property. slice(1) to skip it
		for (const prop of Object.getOwnPropertyNames(Object.getPrototypeOf(this)).slice(1)) {
			r[prop + suffix] = this[prop];
		}
		return r;
	}
};

/**
 * @param {{pronoun: number}} obj
 * @returns {App.Utils.Pronouns}
 */
globalThis.getPronouns = function(obj) {
	return new App.Utils.Pronouns(obj);
};

/**
 * @param {{pronoun: number}} obj
 * @param {FC.SlaveState} spokenBy
 * @returns {App.Utils.Pronouns}
 */
globalThis.getSpokenPronouns = function(obj, spokenBy) {
	const lispHandler = {
		get: function(obj, prop) {
			const result = obj[prop];
			if (typeof result === "string" && lisps) {
				return lispReplace(result);
			} else {
				return result;
			}
		}
	};

	const lisps = SlaveStatsChecker.checkForLisp(spokenBy);
	return new Proxy(new App.Utils.Pronouns(obj), lispHandler);
};

/**
 * @param {number} dickRatio
 * @returns {App.Utils.Pronouns}
 */
globalThis.getNonlocalPronouns = function(dickRatio) {
	/* a fake slave object, we need the .pronoun attribute only */
	const slave = {
		/** @type {App.Data.Pronouns.Kind} */
		pronoun: App.Data.Pronouns.Kind.female
	};
	/* Used for generic slaves, citizens, security, etc. */
	if (V.diversePronouns === 1 && dickRatio > 0 && (dickRatio >= 100 || random(1, 100) <= dickRatio)) {
		slave.pronoun = App.Data.Pronouns.Kind.male;
	}

	return getPronouns(slave);
};

/** Get a property for a given slave, with the correct pronouns.
 * @param {FC.HumanState} slave
 * @param {string} prop
 * @returns {string}
 */
globalThis.pronounsForSlaveProp = function(slave, prop) {
	if (typeof prop !== "string" || prop.length === 0) { return prop; } // nothing to do
	const pronouns = getPronouns(slave);
	return prop.replace(/\$([A-Z]?[a-z]+)/g, (match, cap1) => pronouns[cap1] || match);
};

repo_name: Nithhogg/fc

license: mit


code:
globalThis.sortDomObjects = function(objects, attrName, reverse = 0) {
	reverse = (reverse) ? -1 : 1;

	function sortingByAttr(a, b) {
		let aVal = a.getAttribute(attrName);
		let bVal = b.getAttribute(attrName);
		let aInt = parseInt(aVal);
		if (!isNaN(aInt)) {
			return ((parseInt(bVal) - aInt) * reverse);
		} else if (bVal > aVal) {
			return -1 * reverse;
		}
		return ((aVal === bVal) ? 0 : 1) * reverse;
	}

	return objects.toArray().sort(sortingByAttr);
};

globalThis.sortNurseryPossiblesByName = function() {
	let $sortedNurseryPossibles = $('#ql-nursery div.possible').detach();
	$sortedNurseryPossibles = sortDomObjects($sortedNurseryPossibles, 'data-name');
	$($sortedNurseryPossibles).appendTo($('#ql-nursery'));
};

globalThis.sortNurseryPossiblesByPregnancyWeek = function() {
	let $sortedNurseryPossibles = $('#ql-nursery div.possible').detach();
	$sortedNurseryPossibles = sortDomObjects($sortedNurseryPossibles, 'data-preg-week');
	$($sortedNurseryPossibles).appendTo($('#ql-nursery'));
};

globalThis.sortNurseryPossiblesByPregnancyCount = function() {
	let $sortedNurseryPossibles = $('#ql-nursery div.possible').detach();
	$sortedNurseryPossibles = sortDomObjects($sortedNurseryPossibles, 'data-preg-count');
	$($sortedNurseryPossibles).appendTo($('#ql-nursery'));
};

globalThis.sortNurseryPossiblesByReservedSpots = function() {
	let $sortedNurseryPossibles = $('#ql-nursery div.possible').detach();
	$sortedNurseryPossibles = sortDomObjects($sortedNurseryPossibles, 'data-reserved-spots');
	$($sortedNurseryPossibles).appendTo($('#ql-nursery'));
};

globalThis.sortNurseryPossiblesByPreviousSort = function() {
	let sort = V.sortNurseryList;
	if (sort !== 'unsorted') {
		if (sort === 'Name') {
			sortNurseryPossiblesByName();
		} else if (sort === 'Reserved Nursery Spots') {
			sortNurseryPossiblesByReservedSpots();
		} else if (sort === 'Pregnancy Week') {
			sortNurseryPossiblesByPregnancyWeek();
		} else if (sort === 'Number of Children') {
			sortNurseryPossiblesByPregnancyCount();
		}
	}
};


code:
/**
 * Gets an object with the active recruiter specializations, including
 * eugenics SMRs if the recruiter is using them
 * @returns {Record<"beauty" | "height" | "intelligence", number>}
 */
globalThis.getActiveRecruiterSpecializations = function() {
	const specs = {
		...V.recruiterSpecializations,
	};

	if (V.recruiterEugenics) {
		if (V.policies.SMR.eugenics.faceSMR) {
			specs.beauty = 1;
		}
		if (V.policies.SMR.eugenics.heightSMR) {
			specs.height = 1;
		}
		if (V.policies.SMR.eugenics.intelligenceSMR) {
			specs.intelligence = 1;
		}
	}

	return specs;
};

/**
 * Get the number of active recruiter specializations, including
 * eugenics SMRs if the recruiter is using them
 * @returns {number}
 */
globalThis.getNumActiveRecruiterSpecializations = function() {
	return Object.values(getActiveRecruiterSpecializations()).reduce((sum, spec) => spec === 0 ? sum : sum+1, 0);
};

/**
 * Get the maximum number of specializations your recruiter could have
 * @param {number} [skill] Check a specific skill level. If you don't pass this, it will use your recruiter's skill
 * @returns {number}
 */
globalThis.getMaxRecruiterSpecializations = function(skill) {
	const useRecruiter = skill === undefined;
	if (useRecruiter) {
		if (!S.Recruiter) {
			return 0;
		}
		skill = S.Recruiter.skill.recruiter;
	}

	if (useRecruiter && App.Data.Careers.Leader.recruiter.includes(S.Recruiter.career)) {
		return 3;
	} else if (skill <= 20) {
		return 0;
	} else if (skill <= 60) {
		return 1;
	} else if (skill <= 120) {
		return 2;
	} else {
		return 3;
	}
};


code:
/**
 * @param {{ rivalry: number }} actor
 * @returns {string}
 */
globalThis.rivalryTerm = function(actor) {
	if (actor.rivalry === 1) {
		return "growing rival";
	} else if (actor.rivalry === 2) {
		return "rival";
	} else {
		return "bitter rival";
	}
};

/**
 * @param {{ relationship: number, pronoun: number }} actor
 * @returns {string}
 */
globalThis.relationshipTerm = function(actor) {
	if (actor.relationship === 1) {
		return "friend";
	} else if (actor.relationship === 2) {
		return "best friend";
	} else if (actor.relationship === 3) {
		return "friend with benefits";
	} else if (actor.relationship === 4) {
		return "lover";
	} else {
		return `slave ${getPronouns(actor).wife}`;
	}
};

/**
 * @param {{ relationship: number, pronoun: number }} actor
 * @returns {string}
 */
globalThis.relationshipTermShort = function(actor) {
	if (actor.relationship === 1) {
		return "friend";
	} else if (actor.relationship === 2) {
		return "BFF";
	} else if (actor.relationship === 3) {
		return "FWB";
	} else if (actor.relationship === 4) {
		return "lover";
	} else {
		return `${getPronouns(actor).wife}`;
	}
};

/**
 * @param {{ relationship: number, pronoun: number }} id
 * @returns {string}
 */
globalThis.PCrelationshipTerm = function(id) {
	if (id.relationship === -2) {
		return "lover";
	} else if (id.relationship === -3) {
		return `${getPronouns(id).wife}`;
	}
};

/**
 * Introduces an actor by using any meaningful relationship(s) with an already on-screen actor, and their name.
 * Returns strings like: "your husband John", "his growing rival and mother Alice", or "her best friend and twin sister Carla".
 * If there is no known relationship between them, returns the name alone.
 * Use this function instead of just printing the slave's name when you'd like to let the player to know if two actors are related,
 * even though it's not going to have any mechanical impact on the scene.
 * @param {FC.HumanState} context
 * @param {FC.HumanState} actor
 * @param {boolean} [asLink] When true, instead of using the slave's first name, use their full name with a link to the slave description dialog.
 * @param {boolean} [insertComma] When true, if a relationship is found, it will be separated from the actor's name by a comma ("her father, Dave" instead of "her father Dave")
 * @param {boolean} [capitalize] When true, capitalizes the first letter of the intro. Use if called at the beginning of a sentence.
 * @returns {string|DocumentFragment}
 */
globalThis.contextualIntro = function(context, actor, asLink = false, insertComma = false, capitalize = false) {
	let r = ``;
	const preamble = (context === V.PC) ? "your" : getPronouns(context).possessive;

	const slave = asSlave(actor);
	let terms = [];
	if (slave && slave.relationship > 0 && slave.relationshipTarget === slave.ID) {
		terms.push(relationshipTerm(slave));
	} else if (context === V.PC && slave && slave.relationship < -1) {
		terms.push(PCrelationshipTerm(slave));
	} else if (actor === V.PC && slave && slave.relationship < -1) {
		terms.push(PCrelationshipTerm(slave));
	} else if (slave && slave.rivalry > 0 && slave.rivalryTarget === slave.ID) {
		terms.push(rivalryTerm(slave));
	}

	terms = terms.concat(relativeTerms(context, actor));

	if (terms.length > 0) {
		r = (capitalize ? capFirstChar(preamble) : preamble) + " " + toSentence(terms);
	}

	if (r !== ``) {
		r += (insertComma || actor === V.PC) ? ", " : " ";
	}

	if (asLink && slave) {
		const frag = document.createDocumentFragment();
		frag.append(r, App.UI.DOM.slaveDescriptionDialog(slave));
		return frag;
	} else {
		r += actor === V.PC ? "you" : actor.slaveName;
		return r;
	}
};


code:
/**
 * Returns true if two slaves are allowed to have sex according to the rules.
 * @param {FC.HumanState} slaveA
 * @param {FC.HumanState} slaveB
 * @returns {boolean}
 */
App.Utils.sexAllowed = function sexAllowed(slaveA, slaveB) {
	/* absolutely no sex of any kind with relatives, if incest is prohibited by game rules */
	const related = areRelated(slaveA, slaveB);
	if (V.seeIncest === 0 && related) {
		return false;
	}
	/* check most specific to least specific - master, partner, leader, family, slaves */
	if (slaveA === V.PC) {
		return slaveB.rules.release.master === 1;
	} else if (slaveB === V.PC) {
		return slaveA.rules.release.master === 1;
	} else if (haveRelationshipP(asSlave(slaveA), slaveB)) {
		return (slaveA.rules.release.partner === 1) && (slaveB.rules.release.partner === 1);
	} else if (App.Utils.isDevelopmentFacilityLeader(asSlave(slaveA), asSlave(slaveB))) {
		return slaveA.rules.release.facilityLeader === 1;
	} else if (App.Utils.isDevelopmentFacilityLeader(asSlave(slaveB), asSlave(slaveA))) {
		return slaveB.rules.release.facilityLeader === 1;
	} else if (related) {
		return (slaveA.rules.release.family === 1) && (slaveB.rules.release.family === 1);
	} else {
		return (slaveA.rules.release.slaves === 1) && (slaveB.rules.release.slaves === 1);
	}
};

/**
 * Returns true if the second slave ID resolves to a real slave that the first slave can fuck.
 * @param {FC.HumanState} slave
 * @param {number} id - slave ID (player not allowed)
 * @returns {boolean}
 */
App.Utils.sexAllowedByID = function sexAllowedByID(slave, id) {
	if (id > 0) {
		const target = getSlave(id);
		return target && this.sexAllowed(slave, target);
	}
	return false;
};

/**
 * Returns true if a slave has a romantic partner other than the PC who is both willing and allowed to have sex with her.
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.hasPartnerSex = function hasPartnerSex(slave) {
	const hasWillingSlavePartner = (slave.rules.relationship === "permissive") && (slave.relationship >= 3) && (slave.relationshipTarget > 0);
	return hasWillingSlavePartner && this.sexAllowedByID(slave, slave.relationshipTarget);
};

/**
 * Returns true if a slave has a close family member other than the PC who is both willing and allowed to have sex with her.
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.hasFamilySex = function hasFamilySex(slave) {
	if (V.seeIncest === 0 || slave.rules.release.family === 0) {
		return false;
	}
	// check fast cases first
	if (this.sexAllowedByID(slave, slave.mother)) {
		return true;
	}
	if (this.sexAllowedByID(slave, slave.father)) {
		return true;
	}
	// search exhaustively for sisters/daughters only if necessary
	if (slave.sisters + slave.daughters === 0) {
		return false;
	}
	return getSlaves().some(s => areRelated(slave, s) && this.sexAllowed(slave, s));
};

/**
 * Returns true if the slave has any kind of nonassignment sex with someone other than the PC.
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.hasNonassignmentSex = function hasNonassignmentSex(slave) {
	return (slave.rules.release.slaves === 1) || this.hasFamilySex(slave) || this.hasPartnerSex(slave);
};

/**
 * Returns true if the slave has an assignment that requires them to have sex.
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.hasAssignmentSex = function hasAssignmentSex(slave) {
	return [Job.PUBLIC, Job.WHORE, Job.BROTHEL, Job.CLUB, Job.GLORYHOLE, Job.ARCADE, Job.FUCKTOY, Job.MASTERSUITE, Job.SUBORDINATE].includes(slave.assignment);
};

/**
 * Returns true if the slave is to remain completely chaste.
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.isChaste = function isChaste(slave) {
	return !App.Utils.hasNonassignmentSex(slave) && !App.Utils.hasAssignmentSex(slave) && !App.Utils.sexAllowed(slave, V.PC);
};

/**
 * Returns true if there is any restriction at all on how a slave may choose to get off.
 * @param {FC.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.releaseRestricted = function releaseRestricted(slave) {
	return (slave.rules.release.slaves === 0) || (V.seeIncest && slave.rules.release.family === 0) || (slave.rules.release.masturbation === 0) || (slave.rules.release.partner === 0);
};

/**
 * Returns true if employee is an employee of a development facility, and leader manages that same facility. Non-commutative!
 * @param {FC.SlaveState} employee
 * @param {FC.SlaveState} leader
 * @returns {boolean}
 */
App.Utils.isDevelopmentFacilityLeader = function(employee, leader) {
	if (!App.Data.misc.sexFromDevelopmentLeaders.includes(leader?.assignment)) {
		return false;
	}
	try {
		const manager = App.Utils.jobForAssignment(employee?.assignment)?.facility?.manager?.currentEmployee;
		return (!!manager && leader.ID === manager.ID);
	} catch {
		return false; // could mean no job for assignment, or facility does not have a manager assigned...doesn't matter, we're only interested in matches
	}
};

/**
 * Returns a short summary of the slave's release rules
 * @param {FC.SlaveState} slave
 * @returns {string}
 */
App.Utils.releaseSummaryShort = function releaseSummaryShort(slave) {
	const rel = slave.rules.release;
	let ret = "";
	if (rel.masturbation === 1) {
		ret += "M";
	}
	if (rel.partner === 1) {
		ret += "P";
	}
	if (rel.family === 1 && V.seeIncest === 1) {
		ret += "F";
	}
	if (rel.slaves === 1) {
		ret += "O";
	}
	if (rel.master === 1) {
		ret += "Y";
	}
	if (ret === "") {
		ret = "None";
	}
	return ret;
};

/**
 * Returns a longer summary of the slave's release rules
 * @param {FC.SlaveState} slave
 * @returns {string}
 */
App.Utils.releaseSummaryLong = function releaseSummaryLong(slave) {
	const rel = slave.rules.release;
	const includeFamily = (rel.family === 1) && (V.seeIncest === 1);

	if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "chastity";
	} else if (rel.masturbation === 1 && rel.partner === 0 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "masturbation only";
	} else if (rel.masturbation === 0 && rel.partner === 1 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "partner only";
	} else if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 1 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "facility leaders only";
	} else if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 0 && includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "family only";
	} else if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 1 && rel.master === 0) {
		return "slaves only";
	} else if (rel.masturbation === 0 && rel.partner === 0 && rel.facilityLeader === 0 && !includeFamily && rel.slaves === 0 && rel.master === 1) {
		return "you only";
	} else if (rel.slaves === 1) {
		let ret = "permissive";
		let exceptions = [];
		if (rel.partner === 0) {
			exceptions.push("partner");
		}
		if (rel.facilityLeader === 0) {
			exceptions.push("facility leaders");
		}
		if (!includeFamily) {
			exceptions.push("family");
		}
		if (rel.master === 0) {
			exceptions.push("you");
		}
		if (exceptions.length > 0) {
			ret += " except " + exceptions.reduce(function(res, ch, i, arr) { return res + (i === arr.length - 1 ? ' and ' : ', ') + ch; });
		}
		if (rel.masturbation === 0) {
			ret += ", no masturbation";
		}
		return ret;
	} else {
		let permissions = [];
		if (rel.masturbation === 1) {
			permissions.push("masturbation");
		}
		if (rel.partner === 1) {
			permissions.push("partner");
		}
		if (rel.facilityLeader === 1) {
			permissions.push("facility leaders");
		}
		if (includeFamily) {
			permissions.push("family");
		}
		if (rel.master === 1) {
			permissions.push("you");
		}
		if (permissions.length > 0) {
			return permissions.reduce(function(res, ch, i, arr) { return res + (i === arr.length - 1 ? ' and ' : ', ') + ch; });
		} else {
			return "restrictive";
		}
	}
};

/**
 * Returns a description of the slave's release rules
 * @param {FC.SlaveState} slave
 * @returns {string}
 */
App.Desc.releaseDesc = function releaseDesc(slave) {
	const rel = slave.rules.release;
	const includeFamily = (rel.family === 1) && (V.seeIncest === 1);
	const {He, he, his, him} = getPronouns(slave);
	let r = "and ";
	let appendFrequency = false;
	if (rel.masturbation === 0 && rel.partner === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		r += `${he} is to remain completely chaste.`;
	} else if (rel.masturbation === 1 && rel.partner === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		r += `${he} is only allowed to masturbate`;
		if (slave.energy > 95) {
			r += `, which ${he} is constantly doing.`;
		} else {
			r += `.`;
		}
	} else if (rel.masturbation === 0 && rel.partner === 1 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		r += `${he} is not allowed to masturbate or proposition `;
		if (slave.rules.relationship === "permissive" && slave.relationship >= 3) {
			r += `slaves other than ${his} ${relationshipTerm(slave)}.`;
		} else {
			r += `other slaves; ${he} must find sexual release in ${his} duties.`;
		}
	} else if (rel.masturbation === 0 && rel.partner === 0 && includeFamily && rel.slaves === 0 && rel.master === 0) {
		r += `${he} is not allowed to masturbate. ${He} is only allowed to achieve sexual release with close family members,`;
		appendFrequency = true;
	} else if (rel.masturbation === 0 && rel.partner === 0 && !includeFamily && rel.slaves === 0 && rel.master === 1) {
		r += `${he} is not allowed to masturbate. ${He} must find you if ${he} wants sexual release,`;
		appendFrequency = true;
	} else if (rel.slaves === 1) {
		if (rel.masturbation === 0) {
			r += `${he} is not allowed to masturbate, but may `;
		} else {
			r += `${he} is allowed to masturbate. ${He} may also `;
		}
		if (V.universalRulesConsent === 1) {
			r += "proposition other slaves to find sexual release,";
		} else {
			r += "demand sex from other slaves,";
		}

		const exceptPartner = (rel.partner === 0) && (slave.rules.relationship === "permissive") && (slave.relationship >= 3);
		if (exceptPartner && !includeFamily) {
			r += ` except for ${his} ${relationshipTerm(slave)} and close family members,`;
		} else if (exceptPartner) {
			r += ` except for ${his} ${relationshipTerm(slave)},`;
		} else if (!includeFamily) {
			r += ` except for ${his} close family members,`;
		}

		appendFrequency = true;
	} else {
		if (rel.masturbation === 0) {
			r += `${he} is not allowed to masturbate, but may `;
		} else {
			r += `${he} is allowed to masturbate. ${He} may also `;
		}

		const showPartner = (rel.partner === 1) && (slave.rules.relationship === "permissive") && (slave.relationship >= 3);
		if (includeFamily && showPartner) {
			r += `have sex with ${his} ${relationshipTerm(slave)} and close family members,`;
		} else if (showPartner) {
			r += `fuck ${his} ${relationshipTerm(slave)} as much as ${he} wants,`;
		} else if (includeFamily && rel.master === 1) {
			r += `proposition sex from ${his} close family members and you,`;
		} else if (includeFamily) {
			r += `proposition sex from ${his} close family members,`;
		} else { // should mean rel.master === 1
			r += `find you for sexual relief,`;
		}

		appendFrequency = true;
	}
	if (appendFrequency) {
		if ((slave.devotion > 50) || (slave.energy > 95)) {
			r += ` which ${he} is constantly doing.`;
		} else if (slave.devotion > 20) {
			r += ` which ${he} is often willing to do.`;
		} else {
			r += ` which ${he} is rarely willing to do.`;
		}
	}
	if (rel.facilityLeader === 0) {
		r += ` Your slave leaders have been told not to touch ${him}, no matter how much ${he} begs.`;
	} // else case is "default state", no text
	return r;
};

App.Utils.testAllReleaseText = function testAllReleaseText() {
	let slave = new App.Entity.SlaveState();
	slave.rules.relationship = "permissive";
	slave.relationship = 4;
	slave.relationshipTarget = -1;
	let r = "";
	for (let i = 0; i < Math.pow(2, 5); ++i) {
		const bits = i.toString(2).padStart(5, "0");
		let rule = new App.Entity.ReleaseRulesState();
		rule.masturbation = Number(bits[0]);
		rule.partner = Number(bits[1]);
		rule.family = Number(bits[2]);
		rule.slaves = Number(bits[3]);
		rule.master = Number(bits[4]);
		slave.rules.release = rule;

		r += Serial.stringify(rule) + "\n";
		r += App.Utils.releaseSummaryShort(slave) + "\n";
		r += App.Utils.releaseSummaryLong(slave) + "\n";
		r += App.Desc.releaseDesc(slave) + "\n";
	}
	return r;
};


code:
App.Reminders = (function() {
	/**
	 *  @type {Array<function():void>}
	 */
	let activeViewRefreshers = [];
	let tooltip;
	let timeoutID;

	detectTextSelection();

	return {
		add: add,
		list: list,
		fullDisplay: fullDisplay,
		slaveDisplay: slaveDisplay,
		slaveLink: slaveLink,
		dialog: dialog,
		clear: clearActive,
	};

	/**
	 * @param {string|Node} message
	 * @param {number} week
	 * @param {string} [category]
	 * @param {number} [slaveID]
	 * @returns {boolean} Whether a reminder was actually added or we aborted.
	 */
	function add(message, week, category = "manual", slaveID = 0) {
		if (message === "" || message === null) {
			return false;
		}
		const entry = {message: message, week: week, category: category};
		if (slaveID) {
			entry.slaveID = slaveID;
		}

		// V.reminders is sorted by week from low to high, we insert at the correct place so it remains sorted.
		const index = V.reminders.findIndex(e => e.week >= week);
		if (index === -1) {
			V.reminders.push(entry);
		} else {
			V.reminders.splice(index, 0, entry);
		}
		return true;
	}

	/**
	 * @param {object} [obj]
	 * @param {number} [obj.maxFuture] how far into the future should reminders be displayed.
	 * @param {function(FC.ReminderEntry):boolean} [obj.filter]
	 * @param {boolean} [obj.link] permit passage links
	 * @returns {HTMLSpanElement|DocumentFragment}
	 */
	function list({maxFuture = Number.POSITIVE_INFINITY, filter = null, link = false} = {}) {
		if (V.reminders.length === 0) {
			return document.createDocumentFragment();
		}

		if (!filter) {
			filter = () => true;
		}

		const outerSpan = document.createElement("span");

		/**
		 * @param {FC.ReminderEntry} entry
		 */
		function clearEntry(entry) {
			V.reminders.splice(V.reminders.indexOf(entry), 1);
			refreshActive();
		}

		// We only want to remove visible entries
		function clearOverdue() {
			V.reminders = V.reminders.filter(e => e.week >= V.week || e.week > V.week + maxFuture || !filter(e));
			refreshActive();
		}

		function clearAll() {
			V.reminders = V.reminders.filter(e => e.week > V.week + maxFuture || !filter(e));
			refreshActive();
		}

		let overdue = 0;
		let any = false;

		V.reminders.filter(e => e.week <= V.week + maxFuture && filter(e))
			.forEach(entry => {
				any = true;
				let week;
				let classes = []; // has to be an array, because makeElement takes no empty strings, but empty arrays.
				if (entry.week < V.week) {
					classes = ["red"];
					week = `${numberWithPluralOne(-(entry.week - V.week), 'week')} ago`;
					overdue++;
				} else if (entry.week === V.week) {
					classes = ["orange"];
					week = "today";
				} else {
					if (entry.week <= V.week + 5) {
						classes = ["green"];
					}
					week = `in ${numberWithPluralOne(entry.week - V.week, 'week')}`;
				}
				const div = document.createElement("div");
				div.append(entry.message, " ", App.UI.DOM.makeElement("span", week.toString(), classes));
				const slave = getSlave(entry.slaveID);
				if (slave) {
					const slaveName = link
						? App.UI.DOM.passageLink(SlaveFullName(slave), "Slave Interact", () => {
							V.temp.AS = slave.ID;
							if (Dialog.isOpen()) {
								Dialog.close();
							}
						})
						: SlaveFullName(slave);
					div.append(" (for ", slaveName, ")");
				}
				div.append(" ", App.UI.DOM.link("Clear", clearEntry, [entry]));
				outerSpan.append(div);
			});

		if (overdue > 0) {
			outerSpan.append(App.UI.DOM.makeElement("div", App.UI.DOM.link("Clear overdue", clearOverdue)));
		}
		if (any) {
			outerSpan.append(App.UI.DOM.makeElement("div", App.UI.DOM.link("Clear all", clearAll)));
		}

		return outerSpan;
	}

	/**
	 * @param {string} [category]
	 * @param {number} [slaveID]
	 * @param {string} [selectedText]
	 * @returns {HTMLDivElement}
	 */
	function addField(category, slaveID, selectedText="") {
		const addDiv = document.createElement("div");

		let entry = selectedText;
		let week = 0;

		addDiv.append(
			App.UI.DOM.makeTextBox(selectedText, v => {
				entry = v;
			}),
			" in ", App.UI.DOM.makeTextBox(0, v => {
				week = v;
			}, true), " weeks.",
			" ", App.UI.DOM.link("Add", () => {
				if (add(entry, V.week + week, category, slaveID)) {
					refreshActive();
				}
			})
		);

		return addDiv;
	}

	/**
	 * @param {boolean} [link=false] show passage links
	 * @param {HTMLElement} [displayDiv] Only to be used by the refresh functionality
	 * @param {string} [selectedText]
	 * @returns {HTMLElement}
	 */
	function fullDisplay(link, displayDiv, selectedText) {
		if (displayDiv) {
			// If displayDiv is given, it either is currently being shown or the tree it belonged to was removed from
			// the page. In that case we don't need to refresh it ever again.
			if (!displayDiv.isConnected) {
				return null;
			}
			jQuery(displayDiv).empty();
		} else {
			displayDiv = document.createElement("div");
		}
		activeViewRefreshers.push(() => fullDisplay(link, displayDiv));

		displayDiv.append(App.UI.DOM.makeElement("h2", "Reminders"));

		const listEl = list({link});
		if (listEl !== null) {
			displayDiv.append(App.UI.DOM.makeElement("p", listEl, "indent"));
		}

		displayDiv.append(App.UI.DOM.makeElement("h3", "Add new"));
		displayDiv.append(App.UI.DOM.makeElement("p", addField(undefined, undefined, selectedText)));

		return displayDiv;
	}

	/**
	 * @param {number} slaveID
	 * @param {HTMLElement} [displayDiv] Only to be used by the refresh functionality
	 * @param {string} [selectedText]
	 * @returns {HTMLElement}
	 */
	function slaveDisplay(slaveID, displayDiv, selectedText) {
		if (displayDiv) {
			if (!displayDiv.isConnected) {
				return null;
			}
			jQuery(displayDiv).empty();
		} else {
			displayDiv = document.createElement("div");
		}
		activeViewRefreshers.push(() => slaveDisplay(slaveID, displayDiv));

		displayDiv.append(App.UI.DOM.makeElement("h2", `Reminders for ${SlaveFullName(getSlave(slaveID))}`));

		const listEl = list({filter: e => e.slaveID === slaveID});
		if (listEl !== null) {
			displayDiv.append(App.UI.DOM.makeElement("p", listEl, "indent"));
		}

		displayDiv.append(App.UI.DOM.makeElement("h3", "Add new"));
		displayDiv.append(App.UI.DOM.makeElement("p", addField("slave", slaveID, selectedText)));

		return displayDiv;
	}

	/**
	 * @param {number} slaveID
	 * @returns {HTMLElement}
	 */
	function slaveLink(slaveID) {
		const link = App.UI.DOM.link(String.fromCharCode(0x23f0), () => {
			dialog(slaveID);
		});
		link.style.textDecoration = "none";
		return link;
	}

	/**
	 * @param {number} [slaveID]
	 * @param {string} [selectedText]
	 */
	function dialog(slaveID, selectedText) {
		if (Dialog.isOpen()) {
			Dialog.close();
		}
		Dialog.create("Reminders", "reminders");
		const showLinks = getPassagesWithTag("jump-from-safe").some(x => x.title === passage());
		$(Dialog.body()).empty().append(slaveID ? slaveDisplay(slaveID, undefined, selectedText) : fullDisplay(showLinks, undefined, selectedText));
		Dialog.open();
	}

	function refreshActive() {
		console.log(activeViewRefreshers.length);
		const ars = activeViewRefreshers;
		activeViewRefreshers = [];
		for (const refresher of ars) {
			refresher();
		}
		App.Utils.scheduleSidebarRefresh();
	}

	function clearActive() {
		activeViewRefreshers = [];
	}

	/**
	 * This detects selected text and allows the user to add reminders for it
	 */
	function detectTextSelection() {
		const cleanup = () => {
			// remove old tooltip if it exists
			if (tooltip?.hide) {
				tooltip.hide();
			}
		};
		document.addEventListener('selectionchange', (e)=>{
			if (V.remindersFromTextSelection === 0) { return; }
			// console.log("Archor node - ", window.getSelection().anchorNode.parentElement);
			// console.log("Focus Node - ", window.getSelection().toString());
			const text = window.getSelection()?.toString() || "";
			const parentEl = window.getSelection()?.anchorNode?.parentElement;
			if (text === "") {
				if (timeoutID) {
					clearTimeout(timeoutID);
				}
				timeoutID = setTimeout(() => {
					if (window.getSelection().toString() === "") {
						cleanup();
					}
				}, 505); // delay must be slightly more than 500 ms to work reliably. I am guessing that tippy is doing something with timeouts/delays for optimization. All I know is that if you want the dialog box to open reliably you need this to be 500+ ms.
				return;
			}
			const div = document.createElement("div");
			const link = document.createElement("a");
			link.innerHTML = "Create a reminder for this text";
			link.addEventListener("click", () => {
				App.Reminders.dialog(undefined, text);
			});
			// TODO:@franklygeorge detect if we are on a page that has an active slave. If so then add a link to add a reminder to that slave instead
			// TODO:@franklygeorge detect when the selected text is in the end of week slave report and what slave it is for
			div.append(link);
			if (parentEl) {
				cleanup();
				try {
					tooltip = tippy(parentEl, {
						content: div,
						allowHTML: true,
						interactive: true,
						showOnCreate: true,
						trigger: "click",
						// eslint-disable-next-line jsdoc/require-jsdoc
						onHidden(instance) {
							instance.destroy();
						},
						delay: [500, 0],
					});
				} catch (e) {
					console.error("Error while creating tooltip for highlighted text");
					console.error(`Text: "${text}"`);
					console.error(e);
				}
			}
		});
	}
})();


code:
/**
 * Removes slave from the game
 * @param {FC.SlaveState} slave
 * @param {FC.MissingParentRecordOptionalParams} [recordExtras] if this object or one of its optional properties is not defined, they default to the defaults as defined in missingTableRecordFromHumanState()
 * @see missingTableRecordFromHumanState
 */
globalThis.removeSlave = function(slave, recordExtras = {}) {
	const AS_ID = slave.ID;
	let missing = false;
	const MID = generateMissingParentID();

	WombChangeID(V.PC, AS_ID, MID);
	if (V.PC.pregSource === MID) {
		missing = true;
	}

	if (V.PC.mother === AS_ID) {
		V.PC.mother = MID;
		missing = true;
	}
	if (V.PC.father === AS_ID) {
		V.PC.father = MID;
		missing = true;
	}
	if (V.PC.sisters > 0) {
		if (areSisters(V.PC, slave) > 0) {
			V.PC.sisters--;
		}
	}
	if (V.PC.daughters > 0) {
		if (slave.father === SID.PC || slave.mother === SID.PC) {
			V.PC.daughters--;
		}
	}

	V.favorites.deleteAll(AS_ID);
	V.reminders.deleteWith(r => r.slaveID === AS_ID);

	V.researchLab.tasks = V.researchLab.tasks.filter((t) => t.slaveID !== AS_ID);

	if (V.slaves.has(AS_ID)) {
		if (V.incubator.capacity > 0) {
			getTankSlaves().forEach(child => {
				if (AS_ID === child.mother) {
					child.mother = MID;
					missing = true;
				}
				if (AS_ID === child.father) {
					child.father = MID;
					missing = true;
				}
			});
		}
		if (V.nursery > 0) {
			getInfants().forEach(child => {
				if (AS_ID === child.mother) {
					child.mother = MID;
					missing = true;
				}
				if (AS_ID === child.father) {
					child.father = MID;
					missing = true;
				}
			});
		}
		getSlaves().forEach(s => {
			WombChangeID(s, AS_ID, MID);
			if (s.pregSource === MID) {
				missing = true;
			}
			if (slave.daughters > 0) {
				if (s.mother === AS_ID) {
					s.mother = MID;
				}
				if (s.father === AS_ID) {
					s.father = MID;
				}
				missing = true;
			}
			if (slave.mother > 0 || slave.father > 0) {
				if (slave.mother === s.ID || slave.father === s.ID) {
					s.daughters--;
				}
			}
			if (slave.sisters > 0) {
				if (areSisters(slave, s) > 0) {
					s.sisters--;
				}
			}
			if (s.cumSource === AS_ID || s.milkSource === AS_ID) {
				deflate(s);
			}
			if (s.ID === slave.relationshipTarget) {
				s.relationship = 0;
				s.relationshipTarget = 0;
			}
			if (s.ID === slave.rivalryTarget) {
				s.rivalry = 0;
				s.rivalryTarget = 0;
			}
			/* moved to saDevotion as a discovery event
				if (s.origBodyOwnerID === AS_ID) {
				s.origBodyOwnerID = 0;
				}
			*/
			if (s.ID === slave.subTarget || slave.subTarget === s.ID) {
				slave.subTarget = 0;
				s.subTarget = 0;
			}

			if (s.partners.has(AS_ID)) {
				missing = true;

				s.partners.delete(AS_ID);
				s.partners.add(MID);
			}
		});

		/* remove from Pit trainee list, if needed */
		if (V.pit && V.pit.trainingIDs) {
			V.pit.trainingIDs.deleteAll(AS_ID);
		}
		/* remove from Pit fighters list, if needed */
		if (V.pit && V.pit.fighterIDs) {
			V.pit.fighterIDs.deleteAll(AS_ID);
		}
		/* reset fighting BG ID, if needed */
		if (V.pit && V.pit.slaveFightingBodyguard === AS_ID) {
			V.slaveFightingBodyguard = null;
		}
		// scheduled pit fight
		if (V.pit && V.pit.slavesFighting?.includes(AS_ID)) {
			V.pit.slavesFighting = null;
		}

		/* remove from Coursing Association, if needed */
		if (V.LurcherID === AS_ID) {
			V.LurcherID = 0;
		}

		if (V.personalAttention.task === PersonalAttention.TRAINING) {
			V.personalAttention.slaves.deleteWith(s => s.ID === AS_ID);
			if (V.personalAttention.slaves.length === 0) {
				App.PersonalAttention.reset();
			}
		}

		/* Remove from facility array or leadership role, if needed */
		removeJob(slave, slave.assignment);

		const traitor = getTraitor().actor;

		if (traitor !== 0) {
			missing = true; /* no exceptions, fetus system relies on this */
			if (AS_ID === traitor.pregSource) {
				traitor.pregSource = SID.GENERIC;
			}
			if (traitor.mother === AS_ID) {
				traitor.mother = MID;
			}
			if (traitor.father === AS_ID) {
				traitor.father = MID;
			}
			if (traitor.origBodyOwnerID === AS_ID) {
				traitor.origBodyOwnerID = 0;
			}
			if (traitor.partners.has(AS_ID)) {
				missing = true;

				traitor.partners.delete(AS_ID);
				traitor.partners.add(MID);
			}
		}
		const boomerang = getBoomerang().actor;
		if (boomerang !== 0) {
			missing = true;
			if (AS_ID === boomerang.pregSource) {
				boomerang.pregSource = SID.GENERIC;
			}
			if (boomerang.mother === AS_ID) {
				boomerang.mother = MID;
			}
			if (boomerang.father === AS_ID) {
				boomerang.father = MID;
			}
			if (boomerang.origBodyOwnerID === AS_ID) {
				boomerang.origBodyOwnerID = 0;
			}
			if (boomerang.partners.has(AS_ID)) {
				missing = true;

				boomerang.partners.delete(AS_ID);
				boomerang.partners.add(MID);
			}
		}

		V.organs.deleteWith(s => s.ID === AS_ID);
		V.completedOrgans.deleteWith(s => s.ID === AS_ID);

		for (let o = 0; o < V.adjustProsthetics.length; o++) {
			if (V.adjustProsthetics[o].slaveID === AS_ID) {
				V.adjustProsthetics.deleteAt(o);
				V.adjustProstheticsCompleted--;
				o--;
			}
		}

		deleteGenePoolRecord(slave);

		Object.values(V.missingTable).forEach(s => {
			if (s.mother === slave.ID || s.father === slave.ID) {
				missing = true;
			}
		});
		if (missing) {
			V.missingTable[MID] = missingTableRecordFromHumanState(slave, MID, recordExtras);
			const traitor = getTraitor();
			const boomerang = getBoomerang();
			if (traitor.actor && traitor.actor.ID === slave.ID) {
				/* To link developing fetuses to their parent */
				traitor.stats.missingParentTag = MID;
			} else if (boomerang.actor && boomerang.actor.ID === slave.ID) {
				boomerang.stats.missingParentTag = MID;
			}
			Object.values(V.missingTable).forEach(s => {
				if (s.mother === slave.ID) {
					s.mother = MID;
				}
				if (s.father === slave.ID) {
					s.father = MID;
				}
			});
		}

		if (V.assignmentRecords[AS_ID]) {
			delete V.assignmentRecords[AS_ID];
		}

		// remove slaves from V.rulesToApplyOnce if needed
		removeFromRulesToApplyOnce(slave);

		// @ts-expect-error read only map
		V.slaves.delete(AS_ID);
		V.JobIDMap = makeJobIdMap(); /* need to call this once more to update count of resting slaves*/

		delete V.RAActions.slaves[slave.ID];
	}
};


code:
/**
 * @param {FC.SlaveState} slave
 * @param {object[]} rules
 * @returns {boolean}
 */
globalThis.hasSurgeryRule = function(slave, rules) {
	return rules.some(rule => ruleApplied(slave, rule));
};

/**
 * @param {FC.SlaveState} slave
 * @param {object[]} rules
 * @param {string} what
 * @returns {boolean}
 */
globalThis.hasRuleFor = function(slave, rules, what) {
	return rules.some(
		rule => ruleApplied(slave, rule) && rule[what] !== null);
};

/**
 * @param {FC.SlaveState} slave
 * @param {object[]} rules
 * @returns {boolean}
 */
globalThis.hasHColorRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "hColor");
};

/**
 * @param {FC.SlaveState} slave
 * @param {FC.RA.Rule[]} rules
 * @returns {boolean}
 * */
globalThis.hasHStyleRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "hStyle");
};

/**
 * @param {FC.SlaveState} slave
 * @param {FC.RA.Rule[]} rules
 * @returns {boolean}
 * */
globalThis.hasEyeColorRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "eyeColor");
};

/**
 * True, if the slave should be on contraceptives based on applied rules.
 *
 * @param {FC.SlaveState} slave
 * @param {FC.RA.Rule[]} rules
 * @returns {boolean}
 */
globalThis.rulesDemandContraceptives = function(slave, rules) {
	// iterate over rules backwards, as the last rule has the highest priority
	for (let i = 0; i < rules.length; i++) {
		const rule = rules[rules.length - i - 1];
		if (ruleApplied(slave, rule) && rule.set.preg != null) {
			return rule.set.preg;
		}
	}
	return false;
};

/**
 * @param {Array<[FC.RA.RuleSetters, string]>} rules
 * @returns {[FC.RA.RuleSetters,object]}
 */
globalThis.mergeRules = function(rules) {
	if (rules.length === 0) {
		return [App.RA.newRule.setters(), {}];
	}

	const combinedRule = App.RA.newRule.setters();
	const sourceRecord = {};

	rules.forEach(rule => {
		App.RA.ruleDeepAssign(combinedRule, rule[0], sourceRecord, rule[1]);
	});
	return [combinedRule, sourceRecord];
};

/**
 * return if a rule is applied on a slave
 * @param {FC.SlaveState} slave
 * @param {FC.RA.Rule} rule
 * @returns {boolean}
 */
globalThis.ruleApplied = function(slave, rule) {
	return slave.currentRules.includes(rule.ID);
};

/**
 * remove slave from the facility described by the rule
 * @param {FC.SlaveState} slave
 * @param {object} rule
 * @returns {string}
 */
globalThis.RAFacilityRemove = function(slave, rule) {
	let r = "";
	if (!rule.facilityRemove) { return r; }
	if (slave.assignment === rule.setAssignment) {
		const facilityName = App.Utils.jobForAssignment(rule.setAssignment).facility.name;
		r += `<br>${slave.slaveName} has been removed from ${facilityName} and has been assigned to ${rule.removalAssignment}.`;
		assignJob(slave, rule.removalAssignment);
	}
	return r;
};

/**
 * return whether the rule applies to the slave
 * @param {FC.RA.Rule} rule
 * @param {FC.SlaveState} slave
 * @returns {boolean} flag */
globalThis.ruleAppliesP = function(rule, slave) {
	let V = State.variables;
	let cond = rule.condition;

	// Check if slave should be excluded from having rule applied to again
	if (cond.applyRuleOnce) {
		if (!V.rulesToApplyOnce[rule.ID]) {
			V.rulesToApplyOnce[rule.ID] = [];
		}
		if (V.rulesToApplyOnce[rule.ID].includes(slave.ID)) {
			return false;
		}
	} else {
		if (V.rulesToApplyOnce[rule.ID]) {
			delete V.rulesToApplyOnce[rule.ID];
		}
	}

	//  special slaves / specific slaves check
	if (cond.selectedSlaves.length > 0 && !cond.selectedSlaves.includes(slave.ID)) {
		return false;
	} else if (cond.excludedSlaves.includes(slave.ID)) {
		return false;
	}

	// attribute / function check
	if (!App.RA.Activation.evaluate(slave, cond.activation)) {
		return false;
	}

	if (cond.applyRuleOnce) {
		V.rulesToApplyOnce[rule.ID].push(slave.ID);
	}
	// If rule always applies.
	if (cond.applyRuleOnce && !V.rulesToApplyOnce[rule.ID].includes(slave.ID)) {
		V.rulesToApplyOnce[rule.ID].push(slave.ID);
	}

	return true;
};

App.RA.newRule = function() {
	return {
		rule: emptyRule,
		conditions: emptyConditions,
		setters: emptySetters,
		growth: emptyGrowth,
		surgery: emptySurgery,
		genes: emptyGenes
	};

	/** @returns {FC.RA.Rule} */
	function emptyRule() {
		const id = generateNewID();
		return {
			ID: id,
			name: `Rule ${id}`,
			condition: emptyConditions(),
			set: emptySetters()
		};
	}

	/** @returns {FC.RA.RuleConditions} */
	function emptyConditions() {
		return {
			activation: ["devotion", 20, "gt", 1, "and"],
			advancedMode: V.raDefaultMode === 1,
			selectedSlaves: [],
			excludedSlaves: [],
			applyRuleOnce: false,
		};
	}
	/** @returns {FC.RA.RuleSetters} */
	function emptySetters() {
		return {
			releaseRules: emptyRelease(),
			toyHole: null,
			clitSetting: null,
			clitSettingXY: null,
			clitSettingXX: null,
			clitSettingEnergy: null,
			speechRules: null,
			choosesOwnClothes: null,
			clothes: null,
			collar: null,
			faceAccessory: null,
			mouthAccessory: null,
			shoes: null,
			armAccessory: null,
			legAccessory: null,
			chastityVagina: null,
			chastityAnus: null,
			chastityPenis: null,
			virginAccessory: null,
			vaginaLube: null,
			aVirginAccessory: null,
			vaginalAccessory: null,
			aVirginDickAccessory: null,
			dickAccessory: null,
			bellyAccessory: null,
			aVirginButtplug: null,
			buttplug: null,
			buttplugAttachment: null,
			vaginalAttachment: null,
			iris: null,
			sclera: null,
			pupil: null,
			makeup: null,
			nails: null,
			hColor: null,
			hornColor: null,
			hLength: null,
			haircuts: null,
			hStyle: null,
			eyebrowHColor: null,
			eyebrowHStyle: null,
			eyebrowFullness: null,
			markings: null,
			pubicHColor: null,
			pubicHStyle: null,
			piercing: new App.Entity.completePiercingStateRA(),
			boobsTat: null,
			buttTat: null,
			vaginaTat: null,
			dickTat: null,
			lipsTat: null,
			anusTat: null,
			shouldersTat: null,
			armsTat: null,
			legsTat: null,
			backTat: null,
			stampTat: null,
			birthsTat: null,
			abortionTat: null,
			brandDesign: null,
			brandTarget: null,
			scarTarget: null,
			scarDesign: null,
			curatives: null,
			livingRules: null,
			restRules: null,
			mobilityRules: null,
			relationshipRules: null,
			lactationRules: null,
			standardPunishment: null,
			standardReward: null,
			weight: null,
			diet: null,
			dietCum: null,
			dietMilk: null,
			onDiet: null,
			muscles: null,
			XY: null,
			XX: null,
			gelding: null,
			preg: null,
			abortion: null,
			growth: emptyGrowth(),
			// TODO: rename snake_case to camelCase?
			// eslint-disable-next-line camelcase
			hyper_drugs: 0,
			aphrodisiacs: null,
			autoBrand: 0,
			pornFeed: null,
			pornFameSpending: null,
			dietGrowthSupport: 0,
			eyewear: null,
			earwear: null,
			setAssignment: null,
			pitRules: null,
			arenaRules: null,
			facilityRemove: false,
			removalAssignment: Job.REST,
			surgery: emptySurgery(),
			underArmHColor: null,
			underArmHStyle: null,
			drug: null,
			eyes: null,
			pregSpeed: null,
			bellyImplantVol: -1,
			teeth: null,
			label: null,
			removeLabel: null,
			skinColor: null,
			inflationType: null,
			labelTagsClear: null,
			pronoun: null,
			posePrompt: null,
			expressionPositivePrompt: null,
			expressionNegativePrompt: null,
			positivePrompt: null,
			negativePrompt: null,
			overridePrompts: null,
			openPoseName: null,
			openPoseType: null,
			aiAutoRegenExclude: null,
		};
	}

	/** @returns {FC.RA.RuleReleaseSetters} */
	function emptyRelease() {
		return {
			masturbation: null,
			partner: null,
			facilityLeader: null,
			family: null,
			slaves: null,
			master: null
		};
	}

	/** @returns {FC.RA.RuleGrowthSetters} */
	function emptyGrowth() {
		return {
			boobs: null,
			butt: null,
			lips: null,
			dick: null,
			balls: null,
			clit: null,
			nipples: [],
			intensity: 0
		};
	}

	/** @returns {FC.RA.RuleSurgerySettings} */
	function emptySurgery() {
		return {
			voice: null,
			eyes: null,
			heels: null,
			hears: null,
			smells: null,
			tastes: null,
			lactation: null,
			prostate: null,
			ovaImplant: null,
			ovaImplantAllowReplacing: true,
			cosmetic: null,
			accent: null,
			shoulders: null,
			shouldersImplant: null,
			boobs: null,
			boobsImplantTypes: null,
			boobsImplantAllowReplacing: true,
			hips: null,
			hipsImplant: null,
			butt: null,
			buttImplantTypes: null,
			buttImplantAllowReplacing: true,
			faceShape: null,
			lips: null,
			holes: null,
			tummy: null,
			hair: null,
			bodyhair: null,
			vasectomy: null,
			earShape: null,
			horn: null,
			bellyImplant: null,
			genes: emptyGenes(),
			areolae: null,
			areolaeShape: null
		};
	}

	/** @returns {FC.RA.RuleGenesSettings} */
	function emptyGenes() {
		return {
			NCS: null,
			rapidCellGrowth: null,
			immortality: null,
			flavoring: null,
			aggressiveSperm: null,
			livestock: null,
			progenitor: null,
			macromastia: null,
			gigantomastia: null,
			potent: null,
			fertility: null,
			hyperFertility: null,
			superfetation: null,
			polyhydramnios: null,
			uterineHypersensitivity: null,
			galactorrhea: null,
			gigantism: null,
			dwarfism: null,
			neoteny: null,
			progeria: null,
			albinism: null,
			rearLipedema: null,
			wellHung: null,
			wGain: null,
			wLoss: null,
			androgyny: null,
			mGain: null,
			mLoss: null,
		};
	}
}();

/**
 * @returns {FC.RA.Rule}
 */
globalThis.emptyDefaultRule = App.RA.newRule.rule;

/**
 * Creates RA target object used in rules for body properties
 * @param {"=="|">="|"<="|">"|"<"} condition comparison condition.
 * @param {number} val target value
 * @returns {FC.RA.NumericTarget}
 */
App.RA.makeTarget = function(condition, val) {
	return {
		cond: condition,
		val: val
	};
};

/**
 * Shall the current value be increased according to the target and condition
 * @param {number} current
 * @param {FC.RA.NumericTarget} target
 * @param {number} [step=1] change step
 * @returns {boolean}
 */
App.RA.shallGrow = function(current, target, step = 1) {
	return target && (((current + step <= target.val) && (target.cond === '==')) ||
		((current < target.val) && (target.cond === '>=' || target.cond === '>')) ||
		(current === target.val && target.cond === '>'));
};

/**
 * Shall the current value be decreased according to the target and condition
 * @param {number} current
 * @param {FC.RA.NumericTarget} target
 * @param {number} [step=1]
 * @returns {boolean}
 */
App.RA.shallShrink = function(current, target, step = 1) {
	return target && (((current - step >= target.val) && (target.cond === '==')) ||
		((current > target.val) && (target.cond === '<=' || target.cond === '<')) ||
		(current === target.val && target.cond === '<'));
};

/**
 * @template {object} T
 * @param {object} target Rule or rule part
 * @param {T} source Rule or rule part, describes the same part as target
 * @param {object} sourceRecord record what was overwritten. Overwrites are located the same as in target, marked with
 * sourceName. Describes the same rule(part) as target.
 * @param {string} sourceName Name for the source object.
 * @returns {T}
 */
App.RA.ruleDeepAssign = function deepAssign(target, source, sourceRecord, sourceName) {
	function isObject(o) {
		return (o !== undefined && o !== null && typeof o === 'object' && !Array.isArray(o));
	}

	function isClitSet(rules) {
		return !(rules.clitSetting === null && rules.clitSettingEnergy === null && rules.clitSettingXX === null && rules.clitSettingXY === null);
	}

	// reset target if source sets clitSettings, to prevent low priority rules overriding
	if (isClitSet(source)) {
		target.clitSetting = null;
		target.clitSettingEnergy = null;
		target.clitSettingXX = null;
		target.clitSettingXY = null;
	}

	for (const key in source) {
		if (!source.hasOwnProperty(key)) {
			continue;
		}
		if (isObject(source[key])) {
			if (!target.hasOwnProperty(key) || target[key] === null) {
				target[key] = {};
			}
			if (!sourceRecord.hasOwnProperty(key)) {
				sourceRecord[key] = {};
			}
			deepAssign(target[key], source[key], sourceRecord[key], sourceName);
		} else if (Array.isArray(source[key])) {
			if (source[key].length > 0) {
				target[key] = source[key];
				sourceRecord[key] = sourceName;
			}
		} else if (key === "label" || key === "removeLabel") {
			if (source[key] != null) {
				if (target[key] != null) {
					target[key] += "|" + source[key];
					sourceRecord[key] += "+" + sourceName;
				} else {
					target[key] = source[key];
					sourceRecord[key] = sourceName;
				}
			}
		} else if ((key === "positivePrompt" || key === "negativePrompt") && source[key] !== null) {
			if (source.overridePrompts) {
				target[key] = source[key];
				sourceRecord[key] = sourceName;
			} else {
				if (target[key] != null) {
					target[key] = source[key] + ", " + target[key];
					sourceRecord[key] += "+" + sourceName;
				} else {
					target[key] = source[key];
					sourceRecord[key] = sourceName;
				}
			}
		} else {
			// A rule overrides any preceding ones if,
			// * there are no preceding ones,
			// * or it sets autoBrand,
			// * or it does not set autoBrand and is not null
			const overrides = (
				target[key] === undefined || target[key] === null ||
				(key === "autoBrand" && source[key]) ||
				(key !== "autoBrand" && source[key] !== null));
			if (overrides) {
				target[key] = source[key];
				if (source[key] !== null) {
					sourceRecord[key] = sourceName;
				}
			}
		}
	}
	return target;
};

globalThis.initRules = function() {
	const rule = emptyDefaultRule();
	rule.name = "Obedient Slaves";
	rule.condition.activation = ["devotion", 20, "gte", 1, "and"];

	V.defaultRules = [rule];
	V.rulesToApplyOnce = {};
};