code:
App.Porn = {};
App.Porn.GenreType = {
	paraphilia: {
		focusedViewershipFactor: 1.5,
		unfocusedViewershipFactor: 0.5,
		viewershipSoakingFactor: 0.0,
		bonusViewership: function(slave) { return slave.fetishStrength * 2.0; }
	},
	fetish: {
		focusedViewershipFactor: 2.0,
		unfocusedViewershipFactor: 0.5,
		viewershipSoakingFactor: 1.0,
		bonusViewership: function(slave) { return slave.fetishStrength; }
	},
	general: {
		focusedViewershipFactor: 4.0,
		unfocusedViewershipFactor: 0.5,
		viewershipSoakingFactor: 1.0,
		bonusViewership: function(slave) { return 0.0; }
	},
	quirk: {
		focusedViewershipFactor: 6.0,
		unfocusedViewershipFactor: 0.5,
		viewershipSoakingFactor: 1.0,
		bonusViewership: function(slave) { return 0.0; }
	},
	generic: {
		focusedViewershipFactor: 5.0,
		unfocusedViewershipFactor: 1.0,
		viewershipSoakingFactor: 0.0,
		bonusViewership: function(slave) { return 0.0; }
	}
};

App.Porn.Genre = {};

/* Paraphilia genres */

App.Porn.Genre.neglectful = {
	fameVar: "neglectful",
	fameName: "orgasm denial",
	focusName: "neglectful",
	type: App.Porn.GenreType.paraphilia,
	prestigeDesc1: "Thousands have enjoyed the sight of $him ignoring $his own pleasure",
	prestigeDesc2: "$His many fans relish the sight of $him denying $himself pleasure",
	prestigeDesc3: "Millions are intimately familiar with the sight of $him denying $himself pleasure",
	hitText: function(slave) { return `${getPronouns(slave).His} lack of interest in getting off ${getPronouns(slave).himself}, even when at the verge of climax, makes ${getPronouns(slave).him} a hit with viewers that enjoy orgasm denial.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} pleasing a line of partners without cumming once`; },
	valid: function(slave) { return slave.sexualFlaw === "neglectful"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.cumAddict = {
	fameVar: "cumAddict",
	fameName: "cum addiction",
	focusName: "cum addict",
	type: App.Porn.GenreType.paraphilia,
	prestigeDesc1: "Thousands have enjoyed watching $him do anything and everything for cum",
	prestigeDesc2: "$His many fans relish the sight of $him doing anything for cum",
	prestigeDesc3: "Millions are intimately familiar with the sight of $him doing anything for cum",
	hitText: function(slave) { return `${getPronouns(slave).His} complete obsession with cum makes ${getPronouns(slave).him} a hit with viewers that enjoy bukkake and cum drinking.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} bathing in a tub of cum`; },
	valid: function(slave) { return slave.sexualFlaw === "cum addict"; },
	uiName: function() { return capFirstChar(this.fameName); }
};

App.Porn.Genre.analAddict = {
	fameVar: "analAddict",
	fameName: "anal addiction",
	focusName: "anal addict",
	type: App.Porn.GenreType.paraphilia,
	prestigeDesc1: "Thousands have enjoyed watching $him do anything for a dick in $his ass",
	prestigeDesc2: "$His many fans relish the sight of $him doing anything for a dick up $his ass",
	prestigeDesc3: "Millions are intimately familiar with the sight of $his well-versed anus.",
	hitText: function(slave) { return `${getPronouns(slave).His} complete obsession with taking things up ${getPronouns(slave).his} ass makes ${getPronouns(slave).him} a hit with viewers that enjoy hardcore anal.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} TEMPLATE`; },
	valid: function(slave) { return slave.sexualFlaw === "anal addict" && canDoAnal(slave); },
	uiName: function() { return capFirstChar(this.fameName); }
};

App.Porn.Genre.attentionWhore = {
	fameVar: "attentionWhore",
	fameName: "exhibition",
	focusName: "attention whore",
	type: App.Porn.GenreType.paraphilia,
	prestigeDesc1: "Thousands have enjoyed watching $him do anything for attention",
	prestigeDesc2: "$His many fans relish the sight of $him doing anything for attention",
	prestigeDesc3: "Millions are intimately familiar with the sight of $him doing anything for attention",
	hitText: function(slave) { return `${getPronouns(slave).His} complete obsession with being the center of attention makes ${getPronouns(slave).him} a hit with viewers that savor ${getPronouns(slave).his} frequent exhibitionism.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} flashing strangers`; },
	valid: function(slave) { return slave.sexualFlaw === "attention whore"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.breastGrowth = {
	fameVar: "breastGrowth",
	fameName: "breast expansion",
	focusName: "breast growth",
	type: App.Porn.GenreType.paraphilia,
	prestigeDesc1: "Thousands have enjoyed charting the growth of $his breasts",
	prestigeDesc2: "$His many fans relish the sight of $his expanding bust",
	prestigeDesc3: "Millions are intimately familiar with the history of $his growing bust",
	hitText: function(slave) { return `${getPronouns(slave).His} complete obsession with the ever increasing size of ${getPronouns(slave).his} tits makes ${getPronouns(slave).him} a hit with viewers that enjoy enormous knockers and breast expansion.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} having ${getPronouns(slave).his} tits measured`; },
	valid: function(slave) { return slave.sexualFlaw === "breast growth"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.abusive = {
	fameVar: "abusive",
	fameName: "abuse",
	focusName: "abusive",
	type: App.Porn.GenreType.paraphilia,
	prestigeDesc1: "Thousands have enjoyed watching $him abuse others",
	prestigeDesc2: "$His many fans relish the sight of $him abusing others",
	prestigeDesc3: "Millions are intimately familiar with $his abusive tendencies",
	hitText: function(slave) { return `${getPronouns(slave).His} wanton enjoyment of pleasure through force amuses viewers that enjoy rape and abuse.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} taking what ${getPronouns(slave).he} wants by force`; },
	valid: function(slave) { return slave.sexualFlaw === "abusive"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.malicious = {
	fameVar: "malicious",
	fameName: "sexual torture",
	focusName: "malicious",
	type: App.Porn.GenreType.paraphilia,
	prestigeDesc1: "Thousands have enjoyed $him getting off from the suffering $he caused",
	prestigeDesc2: "$His many fans relish the sight of $him getting off from the suffering $he caused",
	prestigeDesc3: "Millions are intimately familiar with $his hunger for making others suffer",
	hitText: function(slave) { return `${getPronouns(slave).His} sexual appetite for others' suffering makes ${getPronouns(slave).him} a hit with viewers that enjoy sadism and violence.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} tormenting ${getPronouns(slave).his} prey`; },
	valid: function(slave) { return slave.sexualFlaw === "malicious"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.selfHating = {
	fameVar: "selfHating",
	fameName: "self hating",
	focusName: "self hating",
	type: App.Porn.GenreType.paraphilia,
	prestigeDesc1: "Thousands have enjoyed watching $him happily suffer",
	prestigeDesc2: "$His many fans relish $his suffering",
	prestigeDesc3: "Millions are intimately familiar with the sight of $him suffering",
	hitText: function(slave) { return `${getPronouns(slave).His} complete disregard for ${getPronouns(slave).his} own wellbeing makes ${getPronouns(slave).him} a hit with viewers that enjoy watching ${getPronouns(slave).him} suffer.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} orgasming from pain`; },
	valid: function(slave) { return slave.sexualFlaw === "self hating"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.breeder = {
	fameVar: "breeder",
	fameName: "breeder",
	focusName: "breeder",
	type: App.Porn.GenreType.paraphilia,
	prestigeDesc1: "Thousands have enjoyed watching $him obsess over pumping out babies",
	prestigeDesc2: "$His many fans relish $his obsession with having children",
	prestigeDesc3: "Millions are intimately familiar with $his obsession with being pregnant",
	hitText: function(slave) { return `${getPronouns(slave).His} complete obsession with getting and staying pregnant makes ${getPronouns(slave).him} a hit with viewers with all manner of pregnancy fetish, but particularly resonates with those as focused on it as ${getPronouns(slave).he} is.`; },
	trinketShotDesc: function(slave) {
		if (slave.counter.births > 0) {
			return `showing ${getPronouns(slave).him} having an orgasmic birth`;
		} else {
			return `showing ${getPronouns(slave).him} being bred`;
		}
	},
	valid: function(slave) { return slave.sexualFlaw === "breeder"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

/* Fetish genres */

App.Porn.Genre.sub = {
	fameVar: "sub",
	fameName: "submissive",
	focusName: "submissive",
	type: App.Porn.GenreType.fetish,
	prestigeDesc1: "Thousands have enjoyed $his submission",
	prestigeDesc2: "$His many fans relish $his submissiveness",
	prestigeDesc3: "Millions are intimately familiar with $his submissiveness",
	hitText: function(slave) { return `With ${getPronouns(slave).his} submissive streak, ${getPronouns(slave).he} has a clear advantage when it comes to fetish smut.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).his} submission`; },
	valid: function(slave) { return slave.fetish === "submissive"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.cumSlut = {
	fameVar: "cumSlut",
	fameName: "cum",
	focusName: "cumslut",
	type: App.Porn.GenreType.fetish,
	prestigeDesc1: "Thousands have enjoyed $his taste for cum",
	prestigeDesc2: "$His many fans relish $his desire for cum",
	prestigeDesc3: "Millions are intimately familiar with $his taste for cum",
	hitText: function(slave) { return `With ${getPronouns(slave).his} taste for cum, ${getPronouns(slave).he} has a clear advantage when it comes to ejaculate-based smut.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} drinking a glass of cum`; },
	valid: function(slave) { return slave.fetish === "cumslut"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.anal = {
	fameVar: "anal",
	fameName: "buttslut",
	focusName: "buttslut",
	type: App.Porn.GenreType.fetish,
	prestigeDesc1: "Thousands have enjoyed the sight of $his rear",
	prestigeDesc2: "$His many fans relish the sight of $his rear",
	prestigeDesc3: "Millions are intimately familiar with the sight of $his rear",
	hitText: function(slave) { return `With ${getPronouns(slave).his} fetish for asses, ${getPronouns(slave).he} finds friends in the company of viewers that love rear ends.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} shaking ${getPronouns(slave).his} booty`; },
	valid: function(slave) { return slave.fetish === "buttslut"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.humiliation = {
	fameVar: "humiliation",
	fameName: "humiliating",
	focusName: "humiliation",
	type: App.Porn.GenreType.fetish,
	prestigeDesc1: "Thousands have enjoyed $him humiliating $himself",
	prestigeDesc2: "$His many fans relish $his frequent humiliation",
	prestigeDesc3: "Millions are intimately familiar with $his frequent humiliation",
	hitText: function(slave) { return `With ${getPronouns(slave).his} fetish for humiliation, ${getPronouns(slave).he} has a clear advantage when it comes to demeaning smut.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} humiliated in public`; },
	valid: function(slave) { return slave.fetish === "humiliation"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.boobs = {
	fameVar: "boobs",
	fameName: "breast",
	focusName: "boobs",
	type: App.Porn.GenreType.fetish,
	prestigeDesc1: "Thousands have enjoyed the sight of $his breasts",
	prestigeDesc2: "$His many fans relish the sight of $his breasts",
	prestigeDesc3: "Millions are intimately familiar with $his breasts",
	hitText: function(slave) { return `With ${getPronouns(slave).his} fetish for tits, ${getPronouns(slave).he} has a clear advantage when it comes to breast focused smut.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).his} bare chest`; },
	valid: function(slave) { return slave.fetish === "boobs"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.dom = {
	fameVar: "dom",
	fameName: "dominant",
	focusName: "dom",
	type: App.Porn.GenreType.fetish,
	prestigeDesc1: "Thousands have enjoyed $his dominance",
	prestigeDesc2: "$His many fans relish $his dominance",
	prestigeDesc3: "Millions are intimately familiar with $his dominant streak",
	hitText: function(slave) { return `With ${getPronouns(slave).his} dominant streak, ${getPronouns(slave).he} has a clear advantage when it comes to fetish smut.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} seated upon ${getPronouns(slave).his} obedient toy`; },
	valid: function(slave) { return slave.fetish === "dom"; },
	uiName: function() { return capFirstChar(this.fameName); }
};

App.Porn.Genre.sadist = {
	fameVar: "sadist",
	fameName: "sadistic",
	focusName: "sadist",
	type: App.Porn.GenreType.fetish,
	prestigeDesc1: "Thousands have enjoyed $his sadism",
	prestigeDesc2: "$His many fans relish $his sadism",
	prestigeDesc3: "Millions are intimately familiar with $his sadistic streak",
	hitText: function(slave) { return `With ${getPronouns(slave).his} sadistic streak, ${getPronouns(slave).he} has a clear advantage when it comes to fetish smut.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} whipping ${getPronouns(slave).his} lover`; },
	valid: function(slave) { return slave.fetish === "sadist"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.masochist = {
	fameVar: "masochist",
	fameName: "masochistic",
	focusName: "masochist",
	type: App.Porn.GenreType.fetish,
	prestigeDesc1: "Thousands have enjoyed $his masochism",
	prestigeDesc2: "$His many fans relish the sight of $his masochism",
	prestigeDesc3: "Millions are intimately familiar with $his masochistic stream",
	hitText: function(slave) { return `With ${getPronouns(slave).his} masochistic streak, ${getPronouns(slave).he} has a clear advantage when it comes to fetish smut.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} being whipped`; },
	valid: function(slave) { return slave.fetish === "masochist"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.pregnancy = {
	fameVar: "pregnancy",
	fameName: "pregnancy fetish",
	focusName: "pregnancy",
	type: App.Porn.GenreType.fetish,
	prestigeDesc1: "Thousands have enjoyed $his fondness for pregnancy",
	prestigeDesc2: "$His many fans relish the sight of $his fondness for pregnancy",
	prestigeDesc3: "Millions are intimately familiar with $his pregnancy kink",
	hitText: function(slave) { return `With ${getPronouns(slave).his} fetish for all things pregnancy, ${getPronouns(slave).he} has a clear advantage when it comes to fetish smut.`; },
	trinketShotDesc: function(slave) {
		if (slave.ovaries === 1 || slave.mpreg === 1) {
			return `showing ${getPronouns(slave).him} getting knocked up`;
		} else if (slave.dick > 0) {
			return `showing ${getPronouns(slave).him} knocking a girl up`;
		} else {
			return `showing ${getPronouns(slave).him} pretending to be pregnant`;
		}
	},
	valid: function(slave) { return slave.fetish === "pregnancy"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

/* General genres */

App.Porn.Genre.fuckdoll = {
	fameVar: "fuckdoll",
	fameName: "fuckdoll",
	focusName: "fuckdoll",
	type: App.Porn.GenreType.general,
	prestigeDesc1: "$His fans relish the sight of $him being used",
	prestigeDesc2: "$His many fans relish the sight of $him being used",
	prestigeDesc3: "Millions are intimately familiar with the sight of $him being used",
	hitText: function(slave) { return `${getPronouns(slave).His} latex encased body attracts a variety of viewers with tastes ranging from bondage to dolls.`; },
	trinketShotDesc: function(slave) { return `showing it offering itself`; },
	valid: function(slave) { return slave.fuckdoll > 0; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.rape = {
	fameVar: "rape",
	fameName: "rape",
	focusName: "rape",
	type: App.Porn.GenreType.general,
	prestigeDesc1: "Thousands have enjoyed the sight of $him being raped",
	prestigeDesc2: "$His many fans relish the sight of $him being raped",
	prestigeDesc3: "Millions are intimately familiar with the sight of $him being raped",
	hitText: function(slave) { return `${getPronouns(slave).He} is too unbroken for consensual sex, but ${getPronouns(slave).his} viewers wouldn't want it any other way.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).his} rape`; },
	valid: function(slave) { return (slave.devotion < -20) && (slave.counter.anal + slave.counter.vaginal > 0); },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.preggo = {
	fameVar: "preggo",
	fameName: "preggo",
	focusName: "preggo",
	type: App.Porn.GenreType.general,
	prestigeDesc1: "Thousands have enjoyed watching $him swell with child",
	prestigeDesc2: "$His many fans relish the sight of $him swollen with child",
	prestigeDesc3: "Millions are intimately familiar with the sight of $him swollen with child",
	hitText: function(slave) { return `${getPronouns(slave).His} gravid swell may be a turn off to some, but the maiesiophiliacs love it.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} cradling ${getPronouns(slave).his} middle`; },
	valid: function(slave) { return slave.bellyPreg > 500; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.BBW = {
	fameVar: "BBW",
	fameName: "BBW",
	focusName: "BBW",
	type: App.Porn.GenreType.general,
	prestigeDesc1: "Thousands have enjoyed the sight of $his thick and soft body",
	prestigeDesc2: "$His many fans relish the sight of $his thick, soft body",
	prestigeDesc3: "Millions are intimately familiar with $his thick, soft body",
	hitText: function(slave) { return `${getPronouns(slave).His} weight gives ${getPronouns(slave).him} a heavy allure to the chubby chasers and BBW lovers out there.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} giving a bellyjob`; },
	valid: function(slave) { return slave.weight > 95; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.loli = {
	fameVar: "loli",
	fameName: "underage",
	focusName: "loli",
	type: App.Porn.GenreType.general,
	prestigeDesc1: "Thousands have enjoyed the sight of $his childish body",
	prestigeDesc2: "$His many fans relish $his immature body",
	prestigeDesc3: "Millions are intimately familiar with $his immature body",
	hitText: function(slave) { return `${getPronouns(slave).His} young age gives ${getPronouns(slave).him} a dangerous edge and a number of careful viewers.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).his} 'innocence'`; },
	valid: function(slave) { return slave.visualAge <= 12; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.gainer = {
	fameVar: "gainer",
	fameName: "weight gain",
	focusName: "gainer",
	type: App.Porn.GenreType.general,
	prestigeDesc1: "Thousands have enjoyed the sight of $him eating and gaining weight",
	prestigeDesc2: "$His many fans relish how curvy $he's gotten",
	prestigeDesc3: "Millions are intimately familiar with how with how much weight $he has gained",
	hitText: function(slave) { return `${getPronouns(slave).His} expanding waistline attracts those who enjoy seeing a ${getPronouns(slave).girl} pack on the pounds while stuffing ${getPronouns(slave).his} face.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} trying on ${getPronouns(slave).his} old clothes`; },
	valid: function(slave) { return (slave.weight > 30 && slave.diet === "fattening") || (slave.inflation > 0 && slave.inflationType === "food"); },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.stud = {
	fameVar: "stud",
	fameName: "big dick",
	focusName: "stud",
	type: App.Porn.GenreType.general,
	prestigeDesc1: "Thousands have enjoyed the sight of $his throbbing erection",
	prestigeDesc2: "$His many fans relish the sight of $his heavy dick",
	prestigeDesc3: "Millions are intimately familiar with the sight of $his erect dick",
	hitText: function(slave) { return `${getPronouns(slave).His} powerful erection excites those who see it, especially when it is put to good use.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).his} money shot`; },
	valid: function(slave) { return canPenetrate(slave) && slave.dick > 3; },
	uiName: function() { return capFirstChar(this.fameName); }
};

App.Porn.Genre.muscle = {
	fameVar: "muscle",
	fameName: "muscle",
	focusName: "muscle",
	type: App.Porn.GenreType.general,
	prestigeDesc1: "Thousands have enjoyed the sight of $his hard muscles",
	prestigeDesc2: "$His many fans relish the sight of $his hard muscles",
	prestigeDesc3: "Millions are intimately familiar with the sight of $his hard muscles",
	hitText: function(slave) { return `${getPronouns(slave).His} powerful muscles and bodybuilder physique attracts a dedicated audience.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} holding ${getPronouns(slave).his} partner in the air during sex`; },
	valid: function(slave) { return slave.muscles > 80; },
	uiName: function() { return capFirstChar(this.fameName); }
};

App.Porn.Genre.incest = {
	fameVar: "incest",
	fameName: "taboo",
	focusName: "incest",
	type: App.Porn.GenreType.general,
	prestigeDesc1: "Thousands have enjoyed seeing the love $he shares with $his family members",
	prestigeDesc2: "$His many fans relish the sight of $him loving $his family members",
	prestigeDesc3: "Millions are intimately familiar with the sight of $him loving $his family members",
	hitText: function(slave) {
		if (App.Utils.hasPartnerSex(slave) && areRelated(slave, getSlave(slave.relationshipTarget))) {
			const partner = getSlave(slave.relationshipTarget);
			return `${getPronouns(slave).His} sexual escapades with ${getPronouns(slave).his} ${relativeTerm(slave, partner)} ${partner.slaveName} excite viewers attracted to incestuous relationships.`;
		} else if (App.Utils.hasFamilySex(slave)) {
			return `${getPronouns(slave).His} sexual escapades with ${getPronouns(slave).his} close family members excite viewers attracted to incest.`;
		} else {
			return `${getPronouns(slave).His} sexual escapades with you, ${getPronouns(slave).his} own ${relativeTerm(slave, V.PC)}, excite viewers attracted to incestuous relationships.`;
		}
	},
	trinketShotDesc: function(slave) {
		if (App.Utils.hasPartnerSex(slave) && areRelated(slave, getSlave(slave.relationshipTarget))) {
			const partner = getSlave(slave.relationshipTarget);
			return `showing ${getPronouns(slave).him} having fun with ${getPronouns(slave).his} ${relativeTerm(slave, partner)} ${partner.slaveName}`;
		} else if (App.Utils.hasFamilySex(slave)) {
			return `showing ${getPronouns(slave).him} having incestuous fun with ${getPronouns(slave).his} family`;
		} else {
			return `showing ${getPronouns(slave).him} having incestuous fun with you`;
		}
	},
	valid: function(slave) {
		return (V.seeIncest !== 0) && (
			(App.Utils.hasFamilySex(slave)) // has sex with family
			|| (App.Utils.hasPartnerSex(slave) && areRelated(slave, getSlave(slave.relationshipTarget))) // or with her partner, who is related to her
			|| (App.Utils.sexAllowed(slave, V.PC) && areRelated(slave, V.PC)) // or with you, and is related to you
		);
	},
	uiName: function() { return capFirstChar(this.fameName); }
};

/* quirk genres */

App.Porn.Genre.deepThroat = {
	fameVar: "deepThroat",
	fameName: "deepthroat",
	focusName: "gagfuck queen",
	type: App.Porn.GenreType.quirk,
	prestigeDesc1: "Thousands have enjoyed the sounds $he makes when being throatfucked",
	prestigeDesc2: "$His many fans relish the sounds $he makes when being throatfucked",
	prestigeDesc3: "Millions are intimately familiar with the sounds $he makes when being throatfucked",
	hitText: function(slave) { return `${getPronouns(slave).He} impresses with just how much dick can slip down ${getPronouns(slave).his} throat.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} getting facefucked`; },
	valid: function(slave) { return slave.sexualQuirk === "gagfuck queen"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.struggleFuck = {
	fameVar: "struggleFuck",
	fameName: "unwilling",
	focusName: "strugglefuck queen",
	type: App.Porn.GenreType.quirk,
	prestigeDesc1: "Thousands have enjoyed how $he struggles during sex",
	prestigeDesc2: "$His many fans relish how perfectly $he struggles during sex",
	prestigeDesc3: "Millions are intimately familiar with how perfectly $he struggles during sex",
	hitText: function(slave) { return `${getPronouns(slave).He} impresses with ${getPronouns(slave).his} ability to put up just the right amount of fight during sex.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} struggling`; },
	valid: function(slave) { return slave.sexualQuirk === "strugglefuck queen" && (canDoVaginal(slave) || canDoAnal(slave)); },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.painal = {
	fameVar: "painal",
	fameName: "hardcore anal",
	focusName: "painal queen",
	type: App.Porn.GenreType.quirk,
	prestigeDesc1: "Thousands have enjoyed watching $his asshole pushed to its limit",
	prestigeDesc2: "$His many fans relish watching $his asshole pushed to its limit",
	prestigeDesc3: "Millions are intimately familiar with seeing $his asshole pushed to its limit",
	hitText: function(slave) { return `${getPronouns(slave).He} impresses with ${getPronouns(slave).his} ability to push ${getPronouns(slave).his} anus to its limit.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} taking an enormous dick up ${getPronouns(slave).his} ass`; },
	valid: function(slave) { return slave.sexualQuirk === "painal queen" && canDoAnal(slave); },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.tease = {
	fameVar: "tease",
	fameName: "softcore",
	focusName: "tease",
	type: App.Porn.GenreType.quirk,
	prestigeDesc1: "Thousands have enjoyed $his lewd striptease",
	prestigeDesc2: "$His many fans relish $his lewd striptease",
	prestigeDesc3: "Millions are intimately familiar with $his lewd striptease",
	hitText: function(slave) { return `${getPronouns(slave).He} tantalizes viewers with ${getPronouns(slave).his} suggestive moves.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} stripping`; },
	valid: function(slave) { return slave.sexualQuirk === "tease"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.romantic = {
	fameVar: "romantic",
	fameName: "romantic",
	focusName: "romantic",
	type: App.Porn.GenreType.quirk,
	prestigeDesc1: "Thousands have enjoyed the deep bond $he shares with $his partners",
	prestigeDesc2: "$His many fans relish the deep bond $he shares with $his partners",
	prestigeDesc3: "Millions are intimately familiar with the deep bond $he shares with $his partners",
	hitText: function(slave) { return `${getPronouns(slave).He} draws viewers in with ${getPronouns(slave).his} genuine pleasure.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} sharing an orgasm with ${getPronouns(slave).his} partner`; },
	valid: function(slave) { return slave.sexualQuirk === "romantic"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.pervert = {
	fameVar: "pervert",
	fameName: "really perverted",
	focusName: "perverted",
	type: App.Porn.GenreType.quirk,
	prestigeDesc1: "Thousands have enjoyed $his most perverted tendencies",
	prestigeDesc2: "$His many fans relish the depths of $his perversions",
	prestigeDesc3: "Millions are intimately familiar with the depths of $his perversions",
	hitText: function(slave) { return `${getPronouns(slave).His} perverted tastes shock and allure viewers in ways they didn't even realize.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} doing something perverted`; }, /* FIXME: really, that's the best we could come up with??? */
	valid: function(slave) { return slave.sexualQuirk === "perverted"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.caring = {
	fameVar: "caring",
	fameName: "voyeur",
	focusName: "caring",
	type: App.Porn.GenreType.quirk,
	prestigeDesc1: "Thousands have enjoyed watching $him devote $himself to $his partners' pleasure",
	prestigeDesc2: "$His many fans relish $his devotion to $his partners' pleasure",
	prestigeDesc3: "Millions are intimately familiar with $his devotion to $his partners' pleasure",
	hitText: function(slave) { return `${getPronouns(slave).His} sincere devotion to ${getPronouns(slave).his} partner's pleasure draws in viewers with an interest in watching couples fuck.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} bringing ${getPronouns(slave).his} partner to orgasm`; },
	valid: function(slave) { return slave.sexualQuirk === "caring"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.unflinching = {
	fameVar: "unflinching",
	fameName: "unspeakable",
	focusName: "unflinching",
	type: App.Porn.GenreType.quirk,
	prestigeDesc1: "Thousands have enjoyed $his willingness to do things not repeated in polite company",
	prestigeDesc2: "$His many fans relish $his willingness to do anything and everything",
	prestigeDesc3: "Millions are intimately familiar with $his willingness to do things not repeated in polite company",
	hitText: function(slave) { return `${getPronouns(slave).His} willingness to do anything catches the attention of those who enjoy acts that should never be mentioned to others.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} doing something unmentionable`; },
	valid: function(slave) { return slave.sexualQuirk === "unflinching"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

App.Porn.Genre.sizeQueen = {
	fameVar: "sizeQueen",
	fameName: "huge insertion",
	focusName: "size queen",
	type: App.Porn.GenreType.quirk,
	prestigeDesc1: "Thousands have enjoyed the sight of $his holes filled to their limits",
	prestigeDesc2: "$His many fans relish the sight of $his holes filled to their limits",
	prestigeDesc3: "Millions are intimately familiar with the sight of $his holes filled to their limits",
	hitText: function(slave) { return `${getPronouns(slave).His} intent on taking the largest things possible into ${getPronouns(slave).his} holes draws in viewers with an interest for huge insertions.`; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).his} belly bulging from within`; },
	valid: function(slave) { return slave.sexualQuirk === "size queen"; },
	uiName: function() { return capFirstChar(this.focusName); }
};

/* Generic porn - leave this entry last */

App.Porn.Genre.general = {
	fameVar: "general",
	fameName: "generic",
	focusName: "porn",
	type: App.Porn.GenreType.generic,
	prestigeDesc1: "Thousands have enjoyed the sight of $him being used",
	prestigeDesc2: "$His many fans relish the sight of $him being used",
	prestigeDesc3: "Millions are intimately familiar with the sight of $him mid-coitus",
	hitText: function(slave) { return ``; },
	trinketShotDesc: function(slave) { return `showing ${getPronouns(slave).him} mid-coitus`; },
	valid: function(slave) { return true; /* anyone can do generic porn */ },
	uiName: function() { return "Smut is smut"; }
};

/** Returns a given genre by its fame name. */
App.Porn.getGenreByFameName = function(fameName) {
	return _.values(App.Porn.Genre).find((g) => g.fameName === fameName);
};

/** Returns a given genre by its focus name. */
App.Porn.getGenreByFocusName = function(focusName) {
	return _.values(App.Porn.Genre).find((g) => g.focusName === focusName);
};

/** Returns all the genres in the system. */
App.Porn.getAllGenres = function() {
	return _.values(App.Porn.Genre);
};

/** Returns all the genres with a given type. */
App.Porn.getGenresByType = function(type) {
	return _.values(App.Porn.Genre).filter((g) => g.type === type);
};

/** Returns the links necessary to set any valid porn genre for this slave.
 * @param {string} page SugarCube passage name to redirect to when the link is clicked
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLElement}
 */
App.Porn.genreChoiceLinks = function(page, slave) {
	let makeLink = (genre) => App.UI.DOM.link(genre.uiName(), (s, g) => s.porn.focus = g.focusName, [slave, genre], page);
	let links = this.getAllGenres().filter((g) => g.valid(slave)).map(makeLink);
	links.push(App.UI.DOM.link("No focus", (s) => s.porn.focus = "none", [slave], page));
	return App.UI.DOM.generateLinksStrip(links);
};

repo_name: Welptard/fc

license: mit


code:
/* Major props to the anons who worked together to forge the Super Pregnancy Project. Let your legacy go unforgotten.*/
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);
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {string}
 */
globalThis.bellyAdjective = function(slave) {
	if (slave.belly >= 1500) {
		if (slave.belly >= 1000000) {
			if (slave.preg > slave.pregData.normalBirth / 4) {
				return 'unfathomably distended, brimming with life';
			} else {
				return `unfathomable`;
			}
		} else if (slave.belly >= 750000) {
			if (slave.preg > slave.pregData.normalBirth / 4) {
				return 'monolithic bulging';
			} else {
				return `monolithic`;
			}
		} else if (slave.belly >= 600000) {
			if (slave.preg > slave.pregData.normalBirth / 4) {
				return 'titanic bulging';
			} else {
				return `titanic`;
			}
		} else if (slave.belly >= 450000) {
			if (slave.preg > slave.pregData.normalBirth / 4) {
				return 'gigantic bulgy';
			} else {
				return `gigantic`;
			}
		} else if (slave.belly >= 300000) {
			return 'massive';
		} else if (slave.belly >= 100000) {
			return 'giant';
		} else if (slave.belly >= 15000) {
			return 'huge';
		} else if (slave.belly >= 10000) {
			return 'big';
		} else {
			return `swollen`;
		}
	}
	return "";
};

/* calculates and returns expected ovum count during conception*/
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

	/* 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.ID === -1) {
			if (actor.geneticQuirks.fertility === 2 && actor.geneticQuirks.hyperFertility === 2) { // Do not mix with sperm
				if (actor.fertDrugs === 1) {
					ovum += jsEither([2, 3, 3, 3, 3, 4, 4, 5]);
				} else {
					ovum += jsEither([1, 1, 2, 2, 3, 3, 4]);
				}
				if (actor.forcedFertDrugs > 0) {
					ovum += jsEither([3, 3, 4, 4, 5]);
				}
			} else if (actor.geneticQuirks.hyperFertility === 2) { // Predisposed to multiples
				if (actor.fertDrugs === 1) {
					ovum += jsEither([1, 2, 2, 2, 2, 3, 3, 4]);
				} else {
					ovum += jsEither([0, 1, 1, 1, 1, 1, 2, 3]);
				}
				if (actor.forcedFertDrugs > 0) {
					ovum += jsEither([2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4]);
				}
			} else if (actor.geneticQuirks.fertility === 2) { // Predisposed to twins
				if (actor.fertDrugs === 1) {
					ovum += jsEither([1, 1, 2, 2, 2, 2, 3, 3]);
				} else {
					ovum += jsEither([0, 0, 0, 1, 1, 1, 1, 1, 1, 2]);
				}
				if (actor.forcedFertDrugs > 0) {
					ovum += jsEither([1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4]);
				}
			} else {
				if (actor.fertDrugs === 1) {
					ovum += jsEither([0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3]);
				} else {
					ovum += jsEither([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
				}
				if (actor.forcedFertDrugs > 0) {
					ovum += jsEither([0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4]);
				}
			}
			ovum = Math.clamp(ovum, 0, 8);
		} 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 (V.masterSuitePregnancyFertilitySupplements === 1 && (actor.assignment === Job.MASTERSUITE || actor.assignment === Job.CONCUBINE)) {
					fertilityStack += 0.5;
				}
				if (V.reproductionFormula === 1 && (V.week - actor.weekAcquired > 0)) {
					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 (V.masterSuitePregnancyFertilitySupplements === 1 && (actor.assignment === Job.MASTERSUITE || actor.assignment === Job.CONCUBINE)) {
					ovum += jsEither([0, 0, 0, 1, 1, 2, 2, 2, 3, 3]);
					fertilityStack++;
					fertilityStack++;
				}
				if (V.reproductionFormula === 1 && (V.week - actor.weekAcquired > 0)) {
					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 (V.masterSuitePregnancyFertilitySupplements === 1 && (actor.assignment === Job.MASTERSUITE || actor.assignment === Job.CONCUBINE)) {
					ovum += jsRandom(8, 16);
					fertilityStack += 10;
				}
				if (V.reproductionFormula === 1 && (V.week - actor.weekAcquired > 0)) {
					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 (V.masterSuitePregnancyFertilitySupplements === 1 && (actor.assignment === Job.MASTERSUITE || actor.assignment === Job.CONCUBINE)) {
					ovum += jsEither([0, 0, 0, 1, 1, 2, 2, 2, 3, 3]);
					fertilityStack++;
					fertilityStack++;
				}
				if (V.reproductionFormula === 1 && (V.week - actor.weekAcquired > 0)) {
					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, 0, 0, 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 > 12) {
							ovum = jsEither([10, 12]);
						}
					} else if (ovum > 12) {
						ovum = jsRandom(6, 12);
					}
				}
			}
		}
	}

	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) {
			coeff += jsRandom(0, fertilityStack/2); // this 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.
		}

		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 {App.Entity.SlaveState | App.Entity.PlayerState} target is the slave to get pregnant. Also accepts the PC.
 * @param {number} chance is the % chance to conceive.
 * @param {number} 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.
 * @param {boolean} [displayOverride] is an override if defined - fatherID must be defined in this case.
 * @returns {string}
 */
globalThis.knockMeUp = function(target, chance, hole, fatherID, displayOverride) {
	let He;
	let r = ``;
	if (target.ID !== -1) {
		({He} = getPronouns(target));
	}
	if (V.seePreg !== 0) {
		// eslint-disable-next-line no-nested-ternary
		if (jsRandom(0, 99) < (chance + (V.reproductionFormula * ((target.pregSource <= 0) ? ((target.ID === -1) ? 0 : 10) : 20)))) {
			if (target.mpreg === hole) {
				if (target.pregWeek <= 0) {
					target.preg = 1;
					target.pregSource = (!fatherID ? 0 : fatherID);
					if (target.ID !== -1) {
						target.pregWeek = 1;
					}
				}

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

				if (V.menstruation === 1) {
					//
				} else if (!displayOverride) {
					target.pregKnown = 1;
					if (target.ID === -1) {
						/* r += "You have gotten pregnant."; */
					} else {
						r += `<span class="lime">${He} has become pregnant.</span>`;
					}
					if (target.geneticQuirks.superfetation === 2 && target.womb.length > 0) {
						if (V.seeHyperPreg === 1) {
							target.fertPeak = 1;
						} else {
							target.fertPeak = 4;
						}
					}
				} else {
					target.pregKnown = 1;
					if (target.geneticQuirks.superfetation === 2 && target.womb.length > 0) {
						if (V.seeHyperPreg === 1) {
							target.fertPeak = 1;
						} else {
							target.fertPeak = 4;
						}
					}
				}
			} else if (hole === 2) {
				if (target.pregWeek <= 0) {
					target.preg = 1;
					target.pregSource = (!fatherID ? 0 : fatherID);
					if (target.ID !== -1) {
						target.pregWeek = 1;
					}
				}

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

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

globalThis.getIncubatorReserved = function( /* slaves */ ) {
	return FetusGlobalReserveCount("incubator");
};

globalThis.getNurseryReserved = function( /* slaves */ ) {
	return FetusGlobalReserveCount("nursery");
};

globalThis.findFather = function(fatherID) {
	let father;

	father = V.slaves[V.slaveIndices[fatherID]];
	if (father === undefined) {
		if (V.incubator > 0) {
			father = V.tanks.find(s => s.ID === fatherID);
		}
	}
	if (father === undefined) {
		if (V.nursery > 0) {
			father = V.cribs.find(s => s.ID === fatherID);
		}
	}

	return father;
};

globalThis.adjustFatherProperty = function(actor, property, newValue) {
	let father = findFather(actor.ID);
	if (father) {
		father[property] = newValue;
	}
};

/* OLD
globalThis.adjustFatherProperty = function(actor, property, newValue) {
	let fatherIndex;

	fatherIndex = V.slaves.findIndex(function(s) { return s.ID === actor.ID; });
	if (fatherIndex > 0) {
		V.slaves[fatherIndex][property] = newValue;
	} else if (V.incubator > 0) {
		fatherIndex = V.tanks.findIndex(function(s) { return s.ID === actor.ID; });
		if (fatherIndex > 0) {
			V.tanks[fatherIndex][property] = newValue;
		}
	}
	if (fatherIndex === -1) {
		if (V.nursery > 0) {
			fatherIndex = V.cribs.findIndex(function(s) { return s.ID === actor.ID; });
			if (fatherIndex !== -1) {
				V.cribs[fatherIndex][property] = newValue;
			}
		}
	}
}
*/

/* not to be used until that last part is defined. It may become slave.boobWomb.volume or some shit */
/**
 * @param {App.Entity.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 {App.Entity.SlaveState} 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.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;
	}
	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); }

	get wife() { return this.noun === "girl" ? "wife" : "wife"; }
	get Wife() { return capFirstChar(this.wife); }
	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 {Object.<string, string>}
	 */
	appendSuffix(suffix) {
		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];
		}
		// @ts-ignore
		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 {App.Entity.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 = {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 {App.Entity.SlaveState} slave
 * @param {string} prop
 * @returns {string}
 */
globalThis.pronounsForSlaveProp = function(slave, prop) {
	const pronouns = getPronouns(slave);
	return prop.replace(/\$([A-Z]?[a-z]+)/g, (match, cap1) => pronouns[cap1] || match);
};


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.sortButtonsByDevotion = function() {
	let $sortedButtons = $('#qlWrapper button').remove();
	$sortedButtons = sortDomObjects($sortedButtons, 'data-devotion');
	$($sortedButtons).appendTo($('#qlWrapper'));
	quickListBuildLinks();
};

globalThis.sortButtonsByTrust = function() {
	let $sortedButtons = $('#qlWrapper button').remove();
	$sortedButtons = sortDomObjects($sortedButtons, 'data-trust');
	$($sortedButtons).appendTo($('#qlWrapper'));
	quickListBuildLinks();
};

globalThis.quickListBuildLinks = function() {
	$("[data-scroll-to]").click(App.UI.quickBtnScrollToHandler);
};

App.UI.quickBtnScrollToHandler = function() {
	let $this = $(this),
		$toElement = $this.attr('data-scroll-to');
	// note the * 1 enforces $offset to be an integer, without
	// it we scroll to True, which goes nowhere fast.
	let $offset = parseInt($this.attr('data-scroll-offset') || "0");
	let $speed = parseInt($this.attr('data-scroll-speed') || "500");
	// Use javascript scrollTop animation for in page navigation.
	$('html, body').animate({
		scrollTop: $($toElement).offset().top + $offset
	}, $speed);
};

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:
/* This is modified radiobutton macro, for automatic checked state setup*/
/* Usage (be sure to use quotes around parameters):

	<<rbutton "$variable" "value">>
Or:
	<<rbutton "$variable" "value" "HTML_element_ID" "Text to replace with, inside html element with ID from previous parameter. <br> HTML tags allowed.">>

Group of radiobutton will be created based on variable name. Checked state will be set up if variable contain value matched with second parameter. Full form of macro call can be used to display extended description of selected value.
*/

Macro.add('rbutton', {
	handler() {
		if (this.args.length < 2) {
			const errors = [];
			if (this.args.length === 0) { errors.push('variable name'); }
			if (this.args.length < 2) { errors.push('checked value'); }
			return this.error(`no ${errors.join(' or ')} specified`);
		}

		// Ensure that the variable name argument is a string.
		if (typeof this.args[0] !== 'string') {
			return this.error('variable name argument is not a string');
		}

		const varName = this.args[0].trim();

		// Try to ensure that we receive the variable's name (incl. sigil), not its value.
		if (varName[0] !== '$' && varName[0] !== '_') {
			return this.error(`variable name "${this.args[0]}" is missing its sigil ($ or _)`);
		}

		const initValue = Wikifier.getValue(this.args[0]);
		const varId = Util.slugify(varName);
		const checkValue = this.args[1];
		const el = document.createElement('input');

		let replaceID = "";
		let replaceText = "";
		if (typeof this.args[2] === 'string') {
			replaceID = this.args[2];
		}
		if (typeof this.args[3] === 'string') {
			replaceText = this.args[3];
		}

		/*
		Setup and initialize the group counter.
		*/
		if (!TempState.hasOwnProperty(this.name)) {
			TempState[this.name] = {};
		}

		if (!TempState[this.name].hasOwnProperty(varId)) {
			TempState[this.name][varId] = 0;
		}

		/*
		Setup and append the input element to the output buffer.
		*/
		jQuery(el)
			.attr({
				id: `${this.name}-${varId}-${TempState[this.name][varId]++}`,
				name: `${this.name}-${varId}`,
				type: 'radio',
				tabindex: 0 // for accessibility
			})
			.addClass(`macro-${this.name}`)
			.on('change', function() {
				if (this.checked) {
					Wikifier.setValue(varName, checkValue);

					if (replaceID.length > 0 && replaceText.length > 0) {
						let replaceEl = document.getElementById(replaceID);
						// alert (replaceEl);
						if (replaceEl !== null) {
							replaceEl.innerHTML = replaceText;
						}
					}
				}
			})
			.ready(function() {
				// alert ("DOM finished");
				if (el.checked && replaceID.length > 0 && replaceText.length > 0) {
					let replaceEl = document.getElementById(replaceID);
					// alert (replaceEl);
					if (replaceEl !== null) {
						replaceEl.innerHTML = replaceText;
					}
				}
			})
			.appendTo(this.output);

		/*
		Set the story variable to the checked value and the input element to checked, if requested.
		*/
		if (initValue === checkValue) {
			el.checked = true;
			Wikifier.setValue(varName, checkValue);
		}
	}
});


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

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

/**
 * @param {{ relationship: number; pronoun: number; }} id
 * @returns {string}
 */
globalThis.relationshipTermShort = function(id) {
	if (id.relationship === 1) {
		return "friend";
	} else if (id.relationship === 2) {
		return "BFF";
	} else if (id.relationship === 3) {
		return "FWB";
	} else if (id.relationship === 4) {
		return "lover";
	} else {
		return `${getPronouns(id).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|string} [asLink=false] - when true, instead of using the slave's first name, use their full name with a (SC Markup) link to the slave description dialog.  when "DOM", generate a DOM link and return a DocumentFragment.
 * @param {boolean} [insertComma=false] - 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")
 * @returns {string|DocumentFragment}
 */
globalThis.contextualIntro = function(context, actor, asLink=false, insertComma=false) {
	let first = true;
	let r = ``;
	const firstPreamble = (context === V.PC) ? "your" : getPronouns(context).possessive;
	let preamble = () => {
		let s = first ? `${firstPreamble} ` : ` and `;
		first = false;
		return s;
	};

	if (context.relationship > 0 && context.relationshipTarget === actor.ID) {
		r += preamble() + relationshipTerm(context);
	} else if (context === V.PC && actor.relationship < -1) {
		r += preamble() + PCrelationshipTerm(actor);
	} else if (actor === V.PC && context.relationship < -1) {
		r += preamble() + PCrelationshipTerm(context);
	} else if (context.rivalry > 0 && context.rivalryTarget === actor.ID) {
		r += preamble() + rivalryTerm(context);
	}

	const _relative = relativeTerm(context, actor);
	if (_relative) {
		r += preamble() + _relative;
	}

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

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


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) {
	/* 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(slaveA, slaveB)) {
		return (slaveA.rules.release.partner === 1) && (slaveB.rules.release.partner === 1);
	} else if (App.Utils.isDevelopmentFacilityLeader(slaveA, slaveB)) {
		return slaveA.rules.release.facilityLeader === 1;
	} else if (App.Utils.isDevelopmentFacilityLeader(slaveB, slaveA)) {
		return slaveB.rules.release.facilityLeader === 1;
	} else if (areRelated(slaveA, slaveB)) {
		return V.seeIncest && (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 a slave has a romantic partner other than the PC who is both willing and allowed to have sex with her.
 * @param {App.Entity.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.sexAllowed(slave, getSlave(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 {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.hasFamilySex = function hasFamilySex(slave) {
	if (V.seeIncest === 0 || slave.rules.release.family === 0) {
		return false;
	}
	return jsDef(randomRelatedSlave(slave, (s) => this.sexAllowed(slave, s)));
};

/**
 * Returns true if the slave has any kind of nonassignment sex with someone other than the PC.
 * @param {App.Entity.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 there is any restriction at all on how a slave may choose to get off.
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean}
 */
App.Utils.releaseRestricted = function releaseRestricted(slave) {
	return (slave.rules.release.slaves === 0) || (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 {App.Entity.SlaveState} employee
 * @param {App.Entity.SlaveState} leader
 * @returns {boolean}
 */
App.Utils.isDevelopmentFacilityLeader = function(employee, leader) {
	if (!App.Data.misc.sexFromDevelopmentLeaders.includes(leader.assignment)) {
		return false;
	}
	try {
		return (leader.ID === App.Utils.jobForAssignment(employee.assignment).facility.manager.currentEmployee.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 {App.Entity.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 {App.Entity.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 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "chastity";
	} else if (rel.masturbation === 1 && rel.partner === 0 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "masturbation only";
	} else if (rel.masturbation === 0 && rel.partner === 1 && !includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "partner only";
	} else if (rel.masturbation === 0 && rel.partner === 0 && includeFamily && rel.slaves === 0 && rel.master === 0) {
		return "family only";
	} else if (rel.masturbation === 0 && rel.partner === 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 (!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 (includeFamily) {
			permissions.push("family");
		}
		if (rel.master === 1) {
			permissions.push("you");
		}
		if (permissions.length < 1) { return "unknown"; } // probably means BC didn't get run, but let's not die because of it
		return permissions.reduce(function(res, ch, i, arr) { return res + (i === arr.length - 1 ? ' and ' : ', ') + ch; });
	}
};

/**
 * Returns a description of the slave's release rules
 * @param {App.Entity.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 += JSON.stringify(rule) + "\n";
		r += App.Utils.releaseSummaryShort(slave) + "\n";
		r += App.Utils.releaseSummaryLong(slave) + "\n";
		r += App.Desc.releaseDesc(slave) + "\n";
	}
	return r;
};


code:
/**
 * Removes slave from the game
 * @param {App.Entity.SlaveState} slave
 */

globalThis.removeSlave = function(slave) {
	const AS_ID = slave.ID;
	let LENGTH = V.slaves.length;
	const INDEX = V.slaveIndices[AS_ID];
	let missing = false;

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

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

	V.favorites.delete(AS_ID);

	if (INDEX >= 0 && INDEX < LENGTH) {
		if (V.incubator > 0) {
			V.tanks.forEach(child => {
				if (AS_ID === child.mother) {
					child.mother = V.missingParentID;
					missing = true;
				}
				if (AS_ID === child.father) {
					child.father = V.missingParentID;
					missing = true;
				}
			});
		}
		if (V.nursery > 0) {
			V.cribs.forEach(child => {
				if (AS_ID === child.mother) {
					child.mother = V.missingParentID;
					missing = true;
				}
				if (AS_ID === child.father) {
					child.father = V.missingParentID;
					missing = true;
				}
			});
		}
		V.slaves.forEach(s => {
			WombChangeID(s, AS_ID, V.missingParentID); /* This check is complex, should be done in JS now, all needed will be done here. */
			WombChangeGeneID(s, AS_ID, V.missingParentID);
			if (s.pregSource === V.missingParentID) {
				missing = true;
			}
			if (slave.daughters > 0) {
				if (s.mother === AS_ID) {
					s.mother = V.missingParentID;
				}
				if (s.father === AS_ID) {
					s.father = V.missingParentID;
				}
				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;
			}
		});

		/* remove from Pit fighters list, if needed */
		if (V.pit && V.pit.fighterIDs) {
			V.pit.fighterIDs.delete(AS_ID);
		}

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

		if (Array.isArray(V.personalAttention)) {
			V.personalAttention.deleteWith(s => s.ID === AS_ID);
			if (V.personalAttention.length === 0) {
				if (V.PC.career === "escort") {
					V.personalAttention = "whoring";
				} else if (V.PC.career === "servant") {
					V.personalAttention = "upkeep";
				} else {
					V.personalAttention = "business";
				}
			}
		}

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

		if (V.traitor !== 0) {
			missing = true; /* no exceptions, fetus system relies on this */
			if (AS_ID === V.traitor.pregSource) {
				V.traitor.pregSource = 0;
			}
			if (V.traitor.mother === AS_ID) {
				V.traitor.mother = V.missingParentID;
			}
			if (V.traitor.father === AS_ID) {
				V.traitor.father = V.missingParentID;
			}
			if (V.traitor.origBodyOwnerID === AS_ID) {
				V.traitor.origBodyOwnerID = 0;
			}
		}
		if (V.boomerangSlave !== 0) {
			missing = true;
			if (AS_ID === V.boomerangSlave.pregSource) {
				V.boomerangSlave.pregSource = 0;
			}
			if (V.boomerangSlave.mother === AS_ID) {
				V.boomerangSlave.mother = V.missingParentID;
			}
			if (V.boomerangSlave.father === AS_ID) {
				V.boomerangSlave.father = V.missingParentID;
			}
			if (V.boomerangSlave.origBodyOwnerID === AS_ID) {
				V.boomerangSlave.origBodyOwnerID = 0;
			}
		}

		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].ID === AS_ID) {
				V.adjustProsthetics.deleteAt(_o);
				V.adjustProstheticsCompleted--;
				_o--;
			}
		}

		const _geneIndex = V.genePool.findIndex(s => s.ID === AS_ID);
		if (_geneIndex !== -1) {
			let keep = false;
			if (V.traitor !== 0) {
				if (isImpregnatedBy(V.traitor, slave) || V.traitor.ID === AS_ID) {
					/* did we impregnate the traitor, or are we the traitor? */
					keep = true;
				}
			}
			if (V.boomerangSlave !== 0) {
				if (isImpregnatedBy(V.boomerangSlave, slave) || V.boomerangSlave.ID === AS_ID) {
					/* did we impregnate the boomerang, or are we the boomerang? */
					keep = true;
				}
			}
			if (isImpregnatedBy(V.PC, slave)) {
				/* did we impregnate the PC */
				keep = true;
			}
			if (!keep) {
				/* avoid going through this loop if possible */
				keep = V.slaves.some(slave => {
					/* have we impregnated a slave that is not ourselves? */
					return (slave.ID !== AS_ID && isImpregnatedBy(slave, slave));
				});
			}
			if (!keep) {
				V.genePool.deleteAt(_geneIndex);
			}
		}
		Object.values(V.missingTable).forEach(s => {
			if (s.mother === slave.ID || s.father === slave.ID) {
				missing = true;
			}
		});
		if (missing) {
			V.missingTable[V.missingParentID] = {
				slaveName: slave.slaveName,
				slaveSurname: slave.slaveSurname,
				fullName: SlaveFullName(slave),
				dick: slave.dick,
				vagina: slave.vagina,
				ID: V.missingParentID,
				mother: slave.mother,
				father: slave.father,
				inbreedingCoeff: slave.inbreedingCoeff
			};
			if (V.traitor && V.traitor.ID === slave.ID) {
				/* To link developing fetuses to their parent */
				V.traitor.missingParentTag = V.missingParentID;
			} else if (V.boomerangSlave && V.boomerangSlave.ID === slave.ID) {
				V.boomerangSlave.missingParentTag = V.missingParentID;
			}
			Object.values(V.missingTable).forEach(s => {
				if (s.mother === slave.ID) {
					s.mother = V.missingParentID;
				}
				if (s.father === slave.ID) {
					s.father = V.missingParentID;
				}
			});
			V.missingParentID--;
		}

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

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

		V.slaves.deleteAt(INDEX);
		V.slaveIndices = slaves2indices();
		LENGTH--;
		V.JobIDMap = makeJobIdMap(); /* need to call this once more to update count of resting slaves*/
	}
};


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

/**
 * @param {App.Entity.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 {App.Entity.SlaveState} slave
 * @param {object[]} rules
 * @returns {boolean}
 */
globalThis.hasHColorRule = function(slave, rules) {
	return hasRuleFor(slave, rules, "hColor");
};

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

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

/**
 * return if a rule is applied on a slave
 * @param {App.Entity.SlaveState} slave
 * @param {FC.RA.Rule[]} rules
 * @returns {boolean}
 */
globalThis.lastPregRule = function(slave, rules) {
	return rules.some(rule =>
		ruleApplied(slave, rule) && rule.set.preg === -1);
};

/**
 * @param {FC.RA.RuleSetters[]} rules
 * @returns {FC.RA.RuleSetters}
 */
globalThis.mergeRules = function(rules) {
	if (rules.length === 0) {
		return emptyDefaultRule().set;
	}

	const combinedRule = emptyDefaultRule().set;

	rules.forEach(rule => {
		App.RA.ruleDeepAssign(combinedRule, rule);
	});
	return combinedRule;
};

/**
 * return if a rule is applied on a slave
 * @param {App.Entity.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 {App.Entity.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 whether the rule applies to the slave
 * @param {FC.RA.Rule} rule
 * @param {App.Entity.SlaveState} slave
 * @returns {boolean} flag */
globalThis.ruleAppliesP = function(rule, slave) {
	let flag = true;
	let V = State.variables;
	let cond = rule.condition;
	let slaveAttribute = slave[cond.data.attribute];

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

	// attribute / function check
	switch (cond.function) {
		case false: // never applies
			flag = false;
			break;
		case "between": // between two values of a slave's attribute
			if (slaveAttribute === undefined && cond.data.attribute.includes(".")) {
				slaveAttribute = cond.data.attribute
					.split(".")
					.reduce(
						(reduceSlave, attribute) =>
							(reduceSlave && reduceSlave[attribute] !== undefined)
								? reduceSlave[attribute]
								: undefined,
						slave
					);
			}
			flag = between(
				slaveAttribute,
				cond.data.value[0],
				cond.data.value[1]);
			if (cond.applyRuleOnce && flag) {
				V.rulesToApplyOnce[rule.ID].push(slave.ID);
			}
			break;
		case "belongs": // the attribute belongs in the list of values
			flag = cond.data.value.includes(slave[cond.data.attribute]);
			if (cond.applyRuleOnce && flag) {
				V.rulesToApplyOnce[rule.ID].push(slave.ID);
			}
			break;
		case "custom": // user provided JS function
			// TODO: This should use a cached Function instead of 'eval'ing
			flag = eval(cond.data)(slave);
			if (cond.applyRuleOnce && flag) {
				V.rulesToApplyOnce[rule.ID].push(slave.ID);
			}
			break;
	}
	if (!flag) {
		return false;
	}
	// If rule always applies.
	if (cond.applyRuleOnce && !V.rulesToApplyOnce[rule.ID].includes(slave.ID) && flag) {
		V.rulesToApplyOnce[rule.ID].push(slave.ID);
	}
	// assignment / facility / special slaves / specific slaves check
	return (cond.assignment.length === 0 || cond.assignment.includes(slave.assignment)) &&
		(cond.selectedSlaves.length === 0 || cond.selectedSlaves.includes(slave.ID)) &&
		!(cond.excludedSlaves.includes(slave.ID));
};

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

	/** @returns {FC.RA.Rule} */
	function emptyRule() {
		const id = generateNewID();
		return {
			ID: id,
			name: `Rule ${id}`,
			condition: emptyConditions(),
			// TODO: rename properties in snake_case to camelCase?
			set: emptySetters()
		};
	}

	/** @returns {FC.RA.RuleConditions} */
	function emptyConditions() {
		return {
			function: false,
			data: {},
			assignment: [],
			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,
			clothes: null,
			collar: null,
			faceAccessory: null,
			mouthAccessory: null,
			shoes: null,
			armAccessory: null,
			legAccessory: null,
			chastityVagina: null,
			chastityAnus: null,
			chastityPenis: null,
			virginAccessory: 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,
			nipplesPiercing: null,
			areolaePiercing: null,
			clitPiercing: null,
			vaginaLube: null,
			vaginaPiercing: null,
			dickPiercing: null,
			anusPiercing: null,
			lipsPiercing: null,
			tonguePiercing: null,
			earPiercing: null,
			nosePiercing: null,
			eyebrowPiercing: null,
			navelPiercing: null,
			corsetPiercing: null,
			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(),
			// eslint-disable-next-line camelcase
			hyper_drugs: 0,
			aphrodisiacs: null,
			autoSurgery: 0,
			autoBrand: 0,
			pornFeed: null,
			pornFameSpending: null,
			dietGrowthSupport: 0,
			eyewear: null,
			earwear: null,
			setAssignment: null,
			pitRules: 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,
		};
	}

	/** @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,
			intensity: 0
		};
	}

	/** @returns {FC.RA.RuleSurgerySettings} */
	function emptySurgery() {
		return {
			eyes: null,
			hears: null,
			smells: null,
			tastes: null,
			lactation: null,
			prostate: null,
			cosmetic: null,
			accent: null,
			shoulders: null,
			shouldersImplant: null,
			boobs: null,
			hips: null,
			hipsImplant: null,
			butt: null,
			faceShape: null,
			lips: null,
			holes: null,
			tummy: null,
			hair: null,
			bodyhair: null,
			vasectomy: null,
			earShape: null,
			horn: null,
			bellyImplant: null
		};
	}
}();

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

/**
 * Saves the slave, silently fires the RA, saves the slave's after-RA state, and then reverts the slave.
 * Call and then check potential change against $slaveAfterRA to see if the RA would revert it.
 * @param {App.Entity.SlaveState} slave
 */
globalThis.RulesDeconfliction = function(slave) {
	const before = clone(slave);
	DefaultRules(slave);
	V.slaveAfterRA = clone(slave);
	slave = before;
};

/**
 * Creates a table to summarize RA
 * @returns {string}
 */
globalThis.RASummaryCell = function() {
	/**
	 * @param {object[]} objects
	 * @param {string[]} member
	 */
	function collectMemberFromObjects(objects, member) {
		let r = [];
		for (const o of objects) {
			let to = o;
			for (const m of member) {
				to = to[m];
			}
			r.push(to);
		}
		return r;
	}

	/**
	 * @callback objectWalker
	 * @param {object} obj
	 * @param {string[]} memberPath
	 */

	/**
	 * @param {object} obj
	 * @param {objectWalker} walker
	 * @param {string[]} path
	 */
	function walkObject(obj, walker, path) {
		for (const prop in obj) {
			const v = obj[prop];
			const vp = path.concat([prop]);
			if (v !== null && typeof v === 'object') {
				walkObject(v, walker, vp);
			} else {
				walker(obj, vp);
			}
		}
	}

	/**
	 * @param {string[]} path
	 * @param {Array} cells
	 * @param {string[]} table
	 */
	function addRow(path, cells, table) {
		if (!cells.some(v => v !== null)) { // skip empty rows
			return;
		}

		function ruleSetValueToString(v) {
			if (typeof v === 'object') {
				if (v.hasOwnProperty('cond') && v.hasOwnProperty('val')) {
					return `<nowiki>${v.cond}</nowiki> ${v.val}`;
				} else if (v.hasOwnProperty('min') && v.hasOwnProperty('max')) {
					return `${v.min} to ${v.max}`;
				} else {
					return JSON.stringify(v);
				}
			}
			return `${v}`;
		}

		let r = `<td>${path.join('.')}</td>`;
		for (const cell of cells) {
			r += cell !== null ? `<td>${ruleSetValueToString(cell)}</td>` : '	';
		}
		table.push(r);
	}
	/** @type {FC.RA.Rule[]} */
	const rules = V.defaultRules;
	let r = "";

	if (rules.length === 0) {
		return '';
	}

	/* start row title */
	r += `<tr><th style="position:sticky; top:0px; background:#111"></th>`;

	/* make rest of row title */
	for (const rule of rules) {
		r += `<th style="position:sticky; top:0px; background:#111">${rule.name}</th>`;
	}
	r += `</tr>`;

	const setters = rules.map(r => r.set);
	/* A row for every condition the RA can set. */
	/* start loop for row*/

	let tableRows = [];
	walkObject(emptyDefaultRule().set, (obj, path) => {
		addRow(path, collectMemberFromObjects(setters, path), tableRows);
	}, []);

	for (const row of tableRows) {
		r += `<tr>${row}</tr>`;
	}
	return r;
};

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

/**
 * Creates RA range object used in rules
 * @param {number} minValue
 * @param {number} maxValue
 * @returns {FC.RA.NumericRange}
 */
App.RA.makeRange = function(minValue, maxValue) {
	return {
		min: minValue, max: maxValue
	};
};

/**
 * 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 < target.val - step) && (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 > target.val + step) && (target.cond === '==')) ||
		((current > target.val) && (target.cond === '<=' || target.cond === '<')) ||
		(current === target.val && target.cond === '<'));
};

App.RA.ruleDeepAssign = function deepAssign(target, source) {
	function isObject(o) {
		return (o !== undefined && o !== null && typeof o === 'object' && !Array.isArray(o));
	}

	for (const key in source) {
		if (!source.hasOwnProperty(key)) {
			continue;
		}
		if (isObject(source[key])) {
			if (!target.hasOwnProperty(key) || target[key] === null) {
				target[key] = {};
			}
			deepAssign(target[key], source[key]);
		} else if (key === "label" || key === "removeLabel") {
			if (source[key] != null) {
				if (target[key] != null) {
					target[key] += "|" + source[key];
				} else {
					target[key] = source[key];
				}
			}
		} 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];
			}
		}
	}
	return target;
};