code:
/* eslint-disable no-undef */
/*
 * <<span>> macro
 * A minimal macro which allows to create <span> elements with dynamic IDs.
 *
 * Usage: <<span $variable>>...<</span>>
 */
Macro.add('span', {
	skipArgs : true,
	tags : null,

	handler() {
		const payload = this.payload[0].contents.replace(/(^\n+|\n+$)/, '');
		let statement = this.args.raw.trim();
		let result;

		if(statement.length === 0) {
			return this.error('invalid syntax, format: <<span id>>');
		}

		try {
			result = Scripting.evalTwineScript(statement);
		} catch (ex) {
			return this.error(`bad evaluation: ${typeof ex === 'object' ? ex.message : ex}`);
		}

		Config.debug && this.debugView.modes({block: true});

		jQuery(`<span id='${ String(result) }' />`)
			.wiki(payload)
			.appendTo(this.output);
	}
});

repo_name: Mizako/fc


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

window.variableAsNumber = function(x, defaultValue, minValue, maxValue) {
	x = Number(x);
	if (x !== x) {// NaN
		return defaultValue || 0;// In case the default value was not supplied.
	}
	if (x < minValue) {// Works even if minValue is undefined.
		return minValue;
	}
	if (x > maxValue) {// Works even if maxValue is undefined.
		return maxValue;
	}
	return x;
};

window.isSexuallyPure = /** @param {App.Entity.SlaveState} slave */ function (slave) {
	if (!slave) {
		return null;
	}
	return (slave.vagina < 1 && slave.anus < 1 && !slave.counter.anal && !slave.counter.vaginal && !slave.counter.oral && !slave.counter.mammary && !slave.counter.penetrative);
};

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

window.removeFromArray = function(arr, val) {
	for (let i = 0; i < arr.length; i++) {
		if (val === arr[i]) {
			return arr.splice(i, 1);
		}
	}
	return null;
};

window.filterInPlace = function(arr, callback, thisArg) {
	let j = 0;

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

	arr.length = j;
	return arr;
};

/** pregmod: are slave2's sperm compatible with slave1's eggs?
 * @param {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 */
window.canBreed = function(slave1, slave2) {
	if (!slave1 || !slave2) {
		return null;
	}
	return (slave1.eggType === slave2.ballType);
};

/** assuming slave1 is fertile, could slave2 impregnate slave1?
 * slave2 must have dick and balls with compatible sperm;
 * both slaves must not be in chastity; slave2 need not achieve erection
 * @param {App.Entity.SlaveState} slave1
 * @param {App.Entity.SlaveState} slave2
 */
window.canImpreg = function(slave1, slave2) {
	if (!slave1 || !slave2) {
		return null;
	} else if (slave2.dick < 1) {
		return false;
	} else if (slave2.ID === -1) {
		if (slave1.eggType !== "human") {
			return false;
		} else if (!canGetPregnant(slave1)) { /* includes chastity checks */
			return false;
		} else {
			return true;
		}
	} else if (slave2.balls < 1) {
		return false;
	} else if (slave2.chastityPenis === 1) {
		return false;
	} else if (slave2.pubertyXY === 0) { /* pregmod start */
		return false;
	} else if (slave2.vasectomy === 1) {
		return false;
	} else if (slave1.ID === -1) {
		if (slave2.ballType !== "human") {
			return false;
		} else if (!isPlayerFertile(slave1)) {
			return false;
		} else {
			return true;
		}
	} else if (!canBreed(slave1, slave2)) {
		return false; /* pregmod end */
	} else if (!canGetPregnant(slave1)) { /* includes chastity checks */
		return false;
	} else {
		return true;
	}
};

window.isPlayerFertile = function(PC) {
	if (!PC) {
		return null;
	} else if (PC.preg !== 0) { /* currently pregnant, sterile, menopausal or on contraceptives */
		return false;
	} else if (PC.pregWeek < 0) { /* postpartum */
		return false;
	} else if (PC.vagina === 1) {
		return true;
	} else {
		return false;
	}
};

window.relationTargetWord = /** @param {App.Entity.SlaveState} slave */ function (slave) {
	if (!slave) {
		return null;
	} else if (slave.relation === "daughter") {
		return "mother";
	} else if (slave.relation === "mother") {
		return "daughter";
	}
	return slave.relation;
};

window.milkAmount = /** @param {App.Entity.SlaveState} slave */ function (slave) {
	let milk;
	let calcs;
	if (!slave) {
		return null;
	}
	calcs = slave.boobs - slave.boobsImplant - slave.boobsMilk;
	if (calcs > 40000) {
		milk = (158 + ((calcs - 10000) / 600));
	} else if (calcs > 25000) {
		milk = (128 + ((calcs - 10000) / 500));
	} else if (calcs > 10000) {
		milk = (78 + ((calcs - 10000) / 300));
	} else if (calcs > 5000) {
		milk = (53 + ((calcs - 5000) / 200));
	} else if (calcs > 2000) {
		milk = (29 + ((calcs - 2000) / 125));
	} else if (calcs > 800) {
		milk = (16 + ((calcs - 800) / 80));
	} else {
		milk = (8 + ((calcs - 400) / 50));
	}
	if (slave.lactation === 2) {
		milk *= 1.2;
	}
	milk += (milk * ((slave.devotion - 50) / 200));
	if (slave.boobsImplant > 200) {
		milk *= 0.9;
	}
	calcs = (slave.hormoneBalance / 50);
	if (slave.balls !== 0 && calcs > -2) {
		calcs -= 1;
	} else if (slave.ovaries !== 1 && calcs < 2) {
		calcs += 1;
	}
	milk *= (1 + (calcs * 0.1));
	milk *= (1 + (slave.preg / 100));
	milk *= (1 + (slave.health / 50));
	milk *= (1 + (slave.weight / 500));
	milk *= (1 + (slave.lactationAdaptation / 500));
	milk += (slave.boobsMilk / 100);
	milk = Math.trunc(milk);
	milk = Math.clamp(milk, 1, 1000000000000000000);
	return milk;
};

window.cumAmount = /** @param {App.Entity.SlaveState} slave */ function (slave) {
	let cum = 0;
	let calcs = 0;
	if (!slave) {
		return null;
	}
	if (slave.drugs === "testicle enhancement") {
		cum = ((slave.balls * 3.5) + 1);
	} else if (slave.drugs === "hyper testicle enhancement") {
		cum = ((slave.balls * 5) + 1);
	} else {
		cum = ((slave.balls * 2.5) + 1);
	}
	if (slave.ballType === "sterile") {
		cum *= 0.8;
	}
	if (slave.diet === "cum production") {
		cum *= 1.2;
	}
	calcs = (slave.hormoneBalance / 50);
	cum *= (1 - (calcs * 0.1));
	if (slave.scrotum === 0) {
		cum *= 0.8;
	}
	if (slave.prostate === 0) {
		cum *= 0.2; // being generous here
	} else if (slave.prostate === 2) {
		cum *= 1.2;
	} else if (slave.prostate === 3) {
		cum *= 1.5;
	}
	if (slave.devotion > 50) {
		cum += (cum * (slave.devotion / 100));
	} else if (slave.devotion < -50) {
		cum += (cum * (slave.devotion / 100));
	}
	if (slave.health > 50) {
		cum += (cum * (slave.health / 50));
	} else if (slave.health < -50) {
		cum += (cum * (slave.health / 50));
	}
	cum = Math.trunc(cum);
	cum = Math.clamp(cum, 1, 1000000000000000000);
	return cum;
};

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

window.repGainSacrifice = /** @param {App.Entity.SlaveState} slave */ function (slave, arcology) {
	slave = slave || State.variables.activeSlave;
	arcology = arcology || State.variables.arcologies[0];
	if (!slave || !arcology || arcology.FSAztecRevivalist === "unset" || arcology.FSAztecRevivalist <= 0) {
		return 0;
	}
	return Math.ceil(
		(Math.min(100, Math.pow(1.0926, State.variables.week - slave.weekAcquired)) + slave.prestige * 30)
			* arcology.FSAztecRevivalist / 100 / ((State.variables.slavesSacrificedThisWeek || 0) + 1));
};

window.bodyguardSuccessorEligible = /** @param {App.Entity.SlaveState} slave */ function (slave) {
	if (!slave) { return false; }
	return (slave.devotion > 50 && slave.muscles >= 0 && slave.weight < 100 && slave.boobs < 8000 && slave.butt < 10 && slave.belly < 5000 && slave.balls < 10 && slave.dick < 10 && slave.preg < 20 && slave.fuckdoll === 0 && slave.fetish !== "mindbroken" && canWalk(slave));
};

window.ngUpdateGenePool = function(genePool) {
	const transferredSlaveIds = (State.variables.slaves || [])
		.filter(function(s) { return s.ID >= 1200000; })
		.map(function(s) { return s.ID - 1200000; });
	return (genePool || [])
		.filter(function(s) { return transferredSlaveIds.indexOf(s.ID) >= 0; })
		.map(function(s) {
			const result = jQuery.extend(true, {}, s);
			result.ID += 1200000;
			return result;
		});
};

window.ngUpdateMissingTable = function(missingTable) {
	const newTable = {};

	(State.variables.slaves || [])
		.forEach(s => ([s.pregSource+1200000, s.mother+1200000, s.father+1200000]
			.filter(i => (i in missingTable))
			.forEach(i => {
				newTable[i-1200000] = missingTable[i];
				newTable[i-1200000].ID -= 1200000;
			})));

	return newTable;
};

window.toJson = function(obj) {
	let jsontext = JSON.stringify(obj);
	jsontext = jsontext.replace(/^{/, "");
	jsontext = jsontext.replace(/}$/, "");
	return jsontext;
};

window.nippleColor = /** @param {App.Entity.SlaveState} slave */ function (slave) {
	slave = slave || State.variables.activeSlave;
	if (skinToneLevel(slave.skin) < 8) {
		if (slave.preg > slave.pregData.normalBirth/4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "brown";
		} else {
			return "pink";
		}
	} else if (skinToneLevel(slave.skin) < 14) {
		if (slave.preg > slave.pregData.normalBirth/4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "dark brown";
		} else {
			return "pink";
		}
	} else if (skinToneLevel(slave.skin) > 20) {
		if (slave.preg > slave.pregData.normalBirth/4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "black";
		} else {
			return "dark brown";
		}
	} else {
		if (slave.preg > slave.pregData.normalBirth/4 || (slave.counter.birthsTotal > 0 && slave.lactation > 0)) {
			return "dark brown";
		} else {
			return "brown";
		}
	}
};

window.overpowerCheck = /** @param {App.Entity.SlaveState} slave */ function (slave, PC) {
	let strength;

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

	return strength;
};

/**
 * returns array of IDs of all characters who impregnated slave
 * @param {App.Entity.SlaveState} slave
 * @returns {number[]}
 */
window.impregnatedBy = function(slave) {
	const IDArray = [];
	if (!Array.isArray(slave.womb)) {
		WombInit(slave);
	}
	for (let i = 0; i < slave.womb.length; i++) {
		IDArray.push(slave.womb[i].fatherID);
	}
	return IDArray;
};

/**
 * returns true if mother was impregnated by father
 * @param {App.Entity.SlaveState} mother
 * @param {App.Entity.SlaveState} father
 */
window.isImpregnatedBy = function(mother, father) {
	return impregnatedBy(mother).includes(father.ID);
};

window.jsAlert = function(obj) {
	alert(obj);
};

window.jsConsoleInfo = function(obj) {
	console.info(obj);
};

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

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

window.generatePlayerPronouns = function(PC) {
	if (PC.title === 0) {
		PC.pronoun = "she";
		PC.possessivePronoun = "hers";
		PC.possessive = "her";
		PC.object = "her";
		PC.objectReflexive = "herself";
		PC.noun = "woman";
	} else {
		PC.pronoun = "he";
		PC.possessivePronoun = "his";
		PC.possessive = "his";
		PC.object = "him";
		PC.objectReflexive = "himself";
		PC.noun = "man";
	}
};

window.generateAssistantPronouns = function() {
	const V = State.variables;
	if (V.assistant === 0) {
		V.assistantPronouns.pronoun = "it";
		V.assistantPronouns.possessivePronoun = "its";
		V.assistantPronouns.possessive = "its";
		V.assistantPronouns.object = "it";
		V.assistantPronouns.objectReflexive = "itself";
		V.assistantPronouns.noun = "program";
	} else if ((V.assistantAppearance === "incubus") && (V.diversePronouns === 1)) {
		V.assistantPronouns.pronoun = "he";
		V.assistantPronouns.possessivePronoun = "his";
		V.assistantPronouns.possessive = "his";
		V.assistantPronouns.object = "him";
		V.assistantPronouns.objectReflexive = "himself";
		V.assistantPronouns.noun = "boy";
	} else {
		V.assistantPronouns.pronoun = "she";
		V.assistantPronouns.possessivePronoun = "hers";
		V.assistantPronouns.possessive = "her";
		V.assistantPronouns.object = "her";
		V.assistantPronouns.objectReflexive = "herself";
		V.assistantPronouns.noun = "girl";
	}

	if (V.assistant === 0) {
		V.marketAssistantPronouns.pronoun = "it";
		V.marketAssistantPronouns.possessivePronoun = "its";
		V.marketAssistantPronouns.possessive = "its";
		V.marketAssistantPronouns.object = "it";
		V.marketAssistantPronouns.objectReflexive = "itself";
		V.marketAssistantPronouns.noun = "program";
	} else if ((V.assistantAppearance === "succubus") && (V.diversePronouns === 1)) {
		V.marketAssistantPronouns.pronoun = "he";
		V.marketAssistantPronouns.possessivePronoun = "his";
		V.marketAssistantPronouns.possessive = "his";
		V.marketAssistantPronouns.object = "him";
		V.marketAssistantPronouns.objectReflexive = "himself";
		V.marketAssistantPronouns.noun = "boy";
	} else {
		V.marketAssistantPronouns.pronoun = "she";
		V.marketAssistantPronouns.possessivePronoun = "hers";
		V.marketAssistantPronouns.possessive = "her";
		V.marketAssistantPronouns.object = "her";
		V.marketAssistantPronouns.objectReflexive = "herself";
		V.marketAssistantPronouns.noun = "girl";
	}
};

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

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

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

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

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


code:
/* eslint-disable no-undef */
/* eslint-disable no-unused-vars */
/**
 * @param {App.Entity.SlaveState} slave
 */
window.SlaveStatClamp = function SlaveStatClamp(slave) {
	slave.energy = Math.clamp(slave.energy, 0, 100);
	const V = State.variables;

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


code:
/* eslint-disable no-undef */
Macro.add("textinput", {
	// Signifies that the macro is a container macro.
	tags: null,

	handler: function() {
		if (this.args.length < 2) {
			const errors = [];
			if (this.args.length < 1) { errors.push("variable name"); }
			if (this.args.length < 2) { errors.push("default 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 '${varName}' is missing its sigil ($ or _)`);
		}

		const that = this;
		const defaultValue = this.args[1];
		const el = document.createElement("textarea");

		// Setup and append the textarea element to the output buffer.
		jQuery(el)
			.attr({
				rows: 4,
				// cols: 68, // instead of setting "cols" we set the `min-width` in CSS
				tabindex: 0 // for accessibility
			})
			.addClass("macro-textarea") // "hijack" the .macro-textarea class
			.on("input", function() {
				Wikifier.setValue(varName, this.value);
				if (that.payload[0].contents !== "") {
					Wikifier.wikifyEval(that.payload[0].contents.trim());
				}
			})
			.appendTo(this.output);

		// Set the story variable and textarea element to the default value.
		Wikifier.setValue(varName, defaultValue);

		// Ideally, we should be setting `.defaultValue` here, but IE doesn't support it,
		// so we have to use `.textContent`, which is equivalent.
		el.textContent = defaultValue;
	}
});


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

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

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

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


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

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


code:
/* eslint-disable no-unused-vars */
/* eslint-disable no-console */
/* eslint-disable no-undef */
/*
 * Height.mean(nationality, race, genes, age) - returns the mean height for the given combination and age in years (>=2)
 * Height.mean(nationality, race, genes) - returns the mean adult height for the given combination
 * Height.mean(slave) - returns the mean (expected) height for the given slave
 *
 * Height.random(nationality, race, genes, age) - returns a random height using the skew-normal distribution
 *													around the mean height for the given arguments
 * Height.random(nationality, race, genes) - returns a random height for the given combination of an adult, as above
 * Height.random(slave[, options]) - returns a random height for the given slave, as above.
 *										The additional options object can modify how the values are generated
 *										in the same way setting them as global configuration would, but only for this
 *										specific generation.
 *
 *										Example: Only generate above-average heights based on $activeSlave:
 *										Height.random($activeSlave, {limitMult: [0, 5]})
 *
 * Height.forAge(height, age, genes) - returns the height adapted to the age and genes
 * Height.forAge(height, slave) - returns the height adapted to the slave's age and genes
 *
 * Height.config(configuration) - configures the random height generator globally and returns the current configuration
 *	The options and their default values are:
 *	limitMult: [-3, 3] - Limit to the values the underlying (normal) random generator returns.
 *						In normal use, the values are almost never reached; only 0.27% of values are
 *						outside this range and need to be regenerated. With higher skew (see below),
 *						this might change.
 *	spread: 0.05 - The random values generated are multiplied by this and added to 1 to generate
 *					the final height multiplier. The default value together with the default limitMult
 *					means that the generated height will always fall within (1 - 0.05 * 3) = 85% and
 *					(1 + 0.05 * 3) = 115% of the mean height.
 *					Minimum value: 0.001; maximum value: 0.5
 *	skew: 0 - How much the height distribution skews to the right (positive) or left (negative) side
 *				of the height.
 *				Minimum value: -1000, maximum value: 1000
 *	limitHeight: [0, 999] - Limit the resulting height range. Warning: A small height limit range
 *							paired with a high spread value results in the generator having to
 *							do lots of work generating and re-generating random heights until
 *							one "fits".
 *
 * Anon's explanation:
 * limitMult: [0, -30]
 *
 * This specifies a range going up from 0 to -30. It needs to go [-30, 0] instead. Same thing with [0, -5] two lines down. note: technically, this isn't true, because for some bizarre reason Height.random reverses the numbers for you if you get them wrong. But it's important to establish good habits, so.
 *
 * Skew, spread, limitMult: These are statistics things. BTW, a gaussian distribution is a normal distribution. Just a naming thing.
 *
 * Skew: The shape parameter of a skew-normal distribution. See http://azzalini.stat.unipd.it/SN/Intro/intro.html for more details. Basically a measure of how asymmetrical the curve is. It doesn't move the main mass of the distribution. Rather, it's more like it moves mass from one of the tails into the main mass of the distribution.
 *
 * Spread: Changes the average distance from the mean, making the graph wider and shorter. Moves "mass" from the center to the tail. It's basically standard deviation, but named funny because FC codebase. Changing this can have dramatic effects. It's advised to keep this at or below 0.1 for usual height generation.
 *
 * limitMult: A clamp, expressed in z-score. (z=1 is one standard dev above mean, for ex.) If it excludes too much of the distribution the other parameters describe, you're going to spend lots of CPU making and throwing away heights. Don't worry about this unless you run into it.
 *
 * There's also limitHeight which you're not using. It's basically limitMult in different units.
 */
window.Height = (function() {
	"use strict";

	// Global configuration (for different game modes/options/types)
	let minMult = -3.0;
	let maxMult = 3.0;
	let skew = 0.0;
	let spread = 0.05;
	let minHeight = 0;
	let maxHeight = 999;

	// Configuration method for the above values
	const _config = function(conf) {
		if (_.isUndefined(conf)) {
			return {limitMult: [minMult, maxMult], limitHeight: [minHeight, maxHeight], skew: skew, spread: spread};
		}
		if (_.isFinite(conf.skew)) {
			skew = Math.clamp(conf.skew, -1000, 1000);
		}
		if (_.isFinite(conf.spread)) {
			spread = Math.clamp(conf.spread, 0.001, 0.5);
		}
		if (_.isArray(conf.limitMult) && conf.limitMult.length === 2 && conf.limitMult[0] !== conf.limitMult[1] &&
			_.isFinite(conf.limitMult[0]) && _.isFinite(conf.limitMult[1])) {
			minMult = Math.min(conf.limitMult[0], conf.limitMult[1]);
			maxMult = Math.max(conf.limitMult[0], conf.limitMult[1]);
		}
		if (_.isArray(conf.limitHeight) && conf.limitHeight.length === 2 && conf.limitHeight[0] !== conf.limitHeight[1] &&
			_.isFinite(conf.limitHeight[0]) && _.isFinite(conf.limitHeight[1])) {
			minHeight = Math.min(conf.limitHeight[0], conf.limitHeight[1]);
			maxHeight = Math.max(conf.limitHeight[0], conf.limitHeight[1]);
		}
		return {limitMult: [minMult, maxMult], limitHeight: [minHeight, maxHeight], skew: skew, spread: spread};
	};

	/* if you can find an average for an undefined, add it in! */
	const xxMeanHeight = {
		"Afghan": 155.08, "Albanian": 161.77, "Algerian": 159.09, "American.asian": 158.4, "American.black": 163.6, "American.latina": 158.9, "American.white": 165, "American": 163.54,
		"Andorran": 162.90, "Angolan": 157.31, "Antiguan": 160.65, "Argentinian": 159.18, "Armenian": 158.09, "Aruban": 158, "Australian": 165.86, "Austrian": 164.62, "Azerbaijani": 158.25,
		"Bahamian": 160.68, "Bahraini": 156.69, "Bangladeshi": 150.79, "Barbadian": 165.28, "Belarusian": 166.35, "Belgian": 165.49, "Belizean": 156.88, "Beninese": 156.16, "Bermudian": 160.69,
		"Bhutanese": 153.63, "Bissau-Guinean": 158.24, "Bolivian": 153.89, "Bosnian": 165.85, "Brazilian": 160.86, "British": 164.40, "Bruneian": 153.98, "Bulgarian": 164.80,
		"Burkinabé": 160.19, "Burmese": 154.37, "Burundian": 154.02, "Cambodian": 152.91, "Cameroonian": 158.82, "Canadian": 163.91, "Cape Verdean": 161.65, "Catalan": 163.4,
		"Central African": 158.04, "Chadian": 160.17, "Chilean": 159.36, "Chinese": 159.71, "Colombian": 156.85, "Comorian": 155.58, "Congolese": 157.57, "a Cook Islander": 163.19, "Costa Rican": 156.37,
		"Croatian": 165.63, "Cuban": 157.98, "Curaçaoan": 158, "Cypriot": 162.27, "Czech": 168.46, "Danish": 167.21, "Djiboutian": 156.11, "Dominican": 159.03, "Dominiquais": 164.34, "Dutch": 168.72, "East Timorese": 151.15,
		"Ecuadorian": 154.23, "Egyptian": 157.33, "Emirati": 158.68, "Equatoguinean": 157.33, "Eritrean": 156.39, "Estonian": 168.67, "Ethiopian": 155.71, "Fijian": 161.69, "Filipina": 149.60,
		"Finnish": 165.90, "French Guianan": 157, "French Polynesian": 164.52, "French": 164.88, "Gabonese": 158.84, "Gambian": 160.93, "Georgian": 162.98, "German": 165.86, "Ghanan": 157.91,
		"Greek": 164.87, "Greenlandic": 161.55, "Grenadian": 164.51, "Guamanian": 153.7, "Guatemalan": 149.39, "Guinean": 157.80, "Guyanese": 157.92, "Haitian": 158.72, "Honduran": 153.84, "Hungarian": 163.66,
		"I-Kiribati": 157.00, "Icelandic": 165.95, "Indian": 152.59, "Indonesian": 152.80, "Iranian": 159.67, "Iraqi": 158.67, "Irish": 165.11, "Israeli": 161.80, "Italian": 164.61, "Ivorian": 158.07,
		"Jamaican": 163.12, "Japanese": 158.31, "Jordanian": 158.83, "Kazakh": 158.58, "Kenyan": 158.16, "Kittitian": 159.20, "Korean": 160.65, "Kosovan": 165.7, "Kurdish": 165, "Kuwaiti": 159.43,
		"Kyrgyz": 159.35, "Laotian": 151.28, "Latvian": 169.80, "Lebanese": 162.43, "Liberian": 157.3, "Libyan": 162.08, "a Liechtensteiner": 164.3, "Lithuanian": 166.61, "Luxembourgian": 164.43,
		"Macedonian": 159.75, "Malagasy": 151.18, "Malawian": 154.40, "Malaysian": 156.30, "Maldivian": 155.02, "Malian": 160.47, "Maltese": 160.85, "Marshallese": 151.31, "Mauritanian": 157.72,
		"Mauritian": 157.24, "Mexican": 156.85, "Micronesian": 156.09, "Moldovan": 163.24, "Monégasque": 164.61, "Mongolian": 158.22, "Montenegrin": 164.86, "Moroccan": 157.82, "Mosotho": 155.71,
		"Motswana": 161.38, "Mozambican": 153.96, "Namibian": 158.78, "Nauruan": 153.98, "Nepalese": 150.86, "New Caledonian": 158.0, "a New Zealander": 164.94, "Ni-Vanuatu": 158.17, "Nicaraguan": 154.39, "Nigerian": 156.32,
		"Nigerien": 158.25, "Niuean": 164.80, "Norwegian": 165.56, "Omani": 157.19, "Pakistani": 153.84, "Palauan": 156.22, "Palestinian": 158.75, "Panamanian": 155.47, "Papua New Guinean": 154.87,
		"Paraguayan": 159.86, "Peruvian": 152.93, "Polish": 164.59, "Portuguese": 163.04, "Puerto Rican": 159.20, "Qatari": 159.38, "Romanian": 162.73, "Russian": 165.27, "Rwandan": 154.79, "Sahrawi": 157.82,
		"Saint Lucian": 162.31, "Salvadoran": 154.55, "Sammarinese": 164.61, "Samoan": 161.97, "São Toméan": 158.91, "Saudi": 155.88, "Scottish": 163, "Senegalese": 162.52, "Serbian": 167.69,
		"Seychellois": 162.08, "Sierra Leonean": 156.60, "Singaporean": 160.32, "Slovak": 167.47, "Slovene": 166.05, "a Solomon Islander": 154.42, "Somali": 156.06, "South African": 158.03,
		"South Sudanese": 169.0, "Spanish": 163.40, "Sri Lankan": 154.56, "Sudanese": 156.04, "Surinamese": 160.66, "Swazi": 158.64, "Swedish": 165.70, "Swiss": 163.45, "Syrian": 158.65, "Taiwanese": 161.45,
		"Tajik": 157.33, "Tanzanian": 156.6, "Thai": 157.87, "Tibetan": 158.75, "Togolese": 158.30, "Tongan": 165.52, "Trinidadian": 160.64, "Tunisian": 160.35, "Turkish": 160.50, "Turkmen": 161.73,
		"Tuvaluan": 158.10, "Ugandan": 156.72, "Ukrainian": 166.34, "Uruguayan": 162.13, "Uzbek": 157.82, "Vatican": 162.5, "Venezuelan": 157.44, "Vietnamese": 153.59, "Vincentian": 160.70, "Yemeni": 153.97,
		"Zairian": 155.25, "Zambian": 155.82, "Zimbabwean": 158.22,
		"": 159.65, // default
	};
	const xyMeanHeight = {
		"Afghan": 165.26, "Albanian": 173.39, "Algerian": 170.07, "American.asian": 172.5, "American.black": 177.4, "American.latina": 172.5, "American.white": 178.2, "American": 177.13,
		"Andorran": 176.06, "Angolan": 167.31, "Antiguan": 164.8, "Argentinian": 174.62, "Armenian": 172.00, "Aruban": 165.1, "Australian": 179.20, "Austrian": 177.41, "Azerbaijani": 169.75,
		"Bahamian": 172.75, "Bahraini": 167.74, "Bangladeshi": 163.81, "Barbadian": 175.92, "Belarusian": 178.44, "Belgian": 181.70, "Belizean": 168.73, "Beninese": 167.06, "Bermudian": 172.69,
		"Bhutanese": 165.31, "Bissau-Guinean": 167.90, "Bolivian": 166.85, "Bosnian": 180.87, "Brazilian": 173.55, "British": 177.49, "Bruneian": 165.01, "Bulgarian": 178.24, "Burkinabé": 169.33,
		"Burmese": 164.67, "Burundian": 166.64, "Cambodian": 163.33, "Cameroonian": 167.82, "Canadian": 178.09, "Cape Verdean": 173.22, "Catalan": 175.8, "Central African": 166.67,
		"Chadian": 170.44, "Chilean": 171.81, "Chinese": 171.83, "Colombian": 169.50, "Comorian": 166.19, "Congolese": 167.45, "a Cook Islander": 174.77, "Costa Rican": 168.93, "Croatian": 180.78,
		"Cuban": 172.00, "Curaçaoan": 165.1, "Cypriot": 174.99, "Czech": 180.10, "Danish": 181.39, "Djiboutian": 166.57, "Dominican": 172.75, "Dominiquais": 176.31, "Dutch": 182.54, "East Timorese": 159.79, "Ecuadorian": 167.08,
		"Egyptian": 166.68, "Emirati": 170.46, "Equatoguinean": 167.36, "Eritrean": 168.36, "Estonian": 181.59, "Ethiopian": 166.23, "Fijian": 173.90, "Filipina": 163.23, "Finnish": 179.59,
		"French Guianan": 168, "French Polynesian": 177.41, "French": 179.74, "Gabonese": 167.94, "Gambian": 165.40, "Georgian": 174.34, "German": 179.88, "Ghanan": 168.85, "Greek": 177.32, "Greenlandic": 174.87,
		"Grenadian": 176.97, "Guamanian": 169.8, "Guatemalan": 163.41, "Guinean": 167.54, "Guyanese": 170.21, "Haitian": 172.64, "Honduran": 166.39, "Hungarian": 177.26, "I-Kiribati": 169.20, "Icelandic": 180.49,
		"Indian": 164.95, "Indonesian": 163.55, "Iranian": 170.3, "Iraqi": 170.43, "Irish": 178.93, "Israeli": 176.86, "Italian": 177.77, "Ivorian": 166.53, "Jamaican": 174.53, "Japanese": 170.82, "Jordanian": 171.03,
		"Kazakh": 171.14, "Kenyan": 169.64, "Kittitian": 169.62, "Korean": 173.46, "Kosovan": 179.5, "Kurdish": 175, "Kuwaiti": 172.07, "Kyrgyz": 171.24, "Laotian": 160.52, "Latvian": 181.42, "Lebanese": 174.39,
		"Liberian": 163.66, "Libyan": 173.53, "a Liechtensteiner": 175.4, "Lithuanian": 179.03, "Luxembourgian": 177.86, "Macedonian": 178.33, "Malagasy": 161.55, "Malawian": 166, "Malaysian": 167.89,
		"Maldivian": 167.68, "Malian": 171.3, "Maltese": 173.32, "Marshallese": 162.81, "Mauritanian": 163.28, "Mauritian": 170.50, "Mexican": 169.01, "Micronesian": 168.51, "Moldovan": 175.49,
		"Monégasque": 177.77, "Mongolian": 169.07, "Montenegrin": 178.28, "Moroccan": 170.40, "Mosotho": 165.59, "Motswana": 171.63, "Mozambican": 164.80, "Namibian": 166.96, "Nauruan": 167.83,
		"Nepalese": 162.32, "New Caledonian": 171.0, "a New Zealander": 177.74, "Ni-Vanuatu": 168.09, "Nicaraguan": 166.71, "Nigerian": 165.91, "Nigerien": 167.68, "Niuean": 175.83, "Norwegian": 179.75, "Omani": 169.16, "Pakistani": 166.95,
		"Palauan": 167.69, "Palestinian": 172.09, "Panamanian": 168.49, "Papua New Guinean": 163.57, "Paraguayan": 172.83, "Peruvian": 165.23, "Polish": 177.33, "Portuguese": 172.93, "Puerto Rican": 172.08, "Qatari": 170.48,
		"Romanian": 174.74, "Russian": 176.46, "Rwandan": 162.68, "Sahrawi": 170.40, "Saint Lucian": 171.95, "Salvadoran": 169.77, "Sammarinese": 177.77, "Samoan": 174.38, "São Toméan": 167.38,
		"Saudi": 167.67, "Scottish": 177.6, "Senegalese": 173.14, "Serbian": 180.57, "Seychellois": 174.21, "Sierra Leonean": 164.41, "Singaporean": 172.57, "Slovak": 179.50, "Slovene": 179.80,
		"a Solomon Islander": 164.14, "Somali": 166.60, "South African": 166.68, "South Sudanese": 175.9, "Spanish": 176.59, "Sri Lankan": 165.69, "Sudanese": 166.63, "Surinamese": 172.72, "Swazi": 168.13,
		"Swedish": 179.74, "Swiss": 178.42, "Syrian": 170.43, "Taiwanese": 174.52, "Tajik": 171.26, "Tanzanian": 164.80, "Thai": 169.16, "Tibetan": 168.91, "Togolese": 168.33, "Tongan": 176.76,
		"Trinidadian": 173.74, "Tunisian": 173.95, "Turkish": 174.21, "Turkmen": 171.97, "Tuvaluan": 169.64, "Ugandan": 165.62, "Ukrainian": 178.46, "Uruguayan": 173.43, "Uzbek": 169.38, "Vatican": 176.5,
		"Venezuelan": 171.59, "Vietnamese": 164.45, "Vincentian": 172.78, "Yemeni": 159.89, "Zairian": 166.80, "Zambian": 166.52, "Zimbabwean": 168.59,
		"": 171.42, // defaults
	};

	// Helper method - table lookup for nationality/race combinations
	const nationalityMeanHeight = function(table, nationality, race, def) {
		return table[`${nationality }.${ race}`] || table[nationality] || table[`.${ race}`] || table[""] || def;
	};

	// Helper method: Generate a skewed normal random variable with the skew s
	// Reference: http://azzalini.stat.unipd.it/SN/faq-r.html
	const skewedGaussian = function(s) {
		let randoms = gaussianPair();
		if (s === 0) {
			// Don't bother, return an unskewed normal distribution
			return randoms[0];
		}
		let delta = s / Math.sqrt(1 + s * s);
		let result = delta * randoms[0] + Math.sqrt(1 - delta * delta) * randoms[1];
		return randoms[0] >= 0 ? result : -result;
	};

	// Height multiplier generator; skewed gaussian according to global parameters
	const multGenerator = function() {
		let result = skewedGaussian(skew);
		while (result < minMult || result > maxMult) {
			result = skewedGaussian(skew);
		}
		return result;
	};

	// Helper method: Generate a height based on the mean one and limited according to config.
	const heightGenerator = function(mean) {
		let result = mean * (1 + multGenerator() * spread);
		while (result < minHeight || result > maxHeight) {
			result = mean * (1 + multGenerator() * spread);
		}
		return Math.round(result);
	};

	// Helper method - apply age and genes to the adult height
	const applyAge = function(height, age, genes) {
		if (!_.isFinite(age) || age < 2 || age >= 20) {
			return height;
		}
		let minHeight = 0; let midHeight = 0; let midAge = 15;
		switch (genes) {
			case "XX": // female
			case "XXX": // Triple X syndrome female
				minHeight = 85;
				midHeight = height * 157 / 164;
				midAge = 13;
				break;
			case "XY": // male
			case "XXY": // Klinefelter syndrome male
			case "XYY": // XYY syndrome male
				minHeight = 86;
				midHeight = height * 170 / 178;
				midAge = 15;
				break;
			case "X0": case "X": // Turner syndrome female
				minHeight = 85 * 0.93;
				midHeight = height * 157 / 164;
				midAge = 13;
				break;
			default:
				minHeight = 85.5;
				midHeight = height * 327 / 342;
				midAge = 14;
				break;
		}
		if (age > midAge) {
			// end of puberty to 20
			return interpolate(midAge, midHeight, 20, height, age);
		} else {
			// 2 to end of puberty
			return interpolate(2, minHeight, midAge, midHeight, age);
		}
	};

	const _meanHeight = function(nationality, race, genes, age) {
		if (_.isObject(nationality)) {
			// We got called with a single slave as the argument
			return _meanHeight(nationality.nationality, nationality.race, nationality.genes, nationality.physicalAge + nationality.birthWeek / 52.0);
		}
		let result = 0;
		switch (genes) {
			case "XX": // female
				result = nationalityMeanHeight(xxMeanHeight, nationality, race);
				break;
			case "XY": // male
				result = nationalityMeanHeight(xyMeanHeight, nationality, race);
				break;
			// special cases. Extra SHOX genes on X and Y chromosomes make for larger people
			case "X0": case "X": // Turner syndrome female
				result = nationalityMeanHeight(xxMeanHeight, nationality, race) * 0.93;
				break;
			case "XXX": // Triple X syndrome female
				result = nationalityMeanHeight(xxMeanHeight, nationality, race) * 1.03;
				break;
			case "XXY": // Klinefelter syndrome male
				result = nationalityMeanHeight(xyMeanHeight, nationality, race) * 1.03;
				break;
			case "XYY": // XYY syndrome male
				result = nationalityMeanHeight(xyMeanHeight, nationality, race) * 1.04;
				break;
			case "Y": case "Y0": case "YY": case "YYY":
				console.log(`Height.mean(): non-viable genes ${ genes}`);
				break;
			default:
				console.log(`Height.mean(): unknown genes ${ genes }, returning mean between XX and XY`);
				result = nationalityMeanHeight(xxMeanHeight, nationality, race) * 0.5 + nationalityMeanHeight(xyMeanHeight, nationality, race) * 0.5;
				break;
		}
		return applyAge(result, age, genes);
	};

	const _randomHeight = function(nationality, race, genes, age) {
		const mean = _meanHeight(nationality, race, genes, age);
		// If we got called with a slave object and options, temporarily modify
		// our configuration.
		if (_.isObject(nationality) && _.isObject(race)) {
			const currentConfig = _config();
			_config(race);
			const result = heightGenerator(mean);
			_config(currentConfig);
			return result;
		}
		return heightGenerator(mean);
	};

	const _forAge = function(height, age, genes) {
		if (_.isObject(age)) {
			// We got called with a slave as a second argument
			return applyAge(height, age.physicalAge + age.birthWeek / 52.0, age.genes);
		} else {
			return applyAge(height, age, genes);
		}
	};

	return {
		mean: _meanHeight,
		random: _randomHeight,
		forAge: _forAge,
		config: _config,
	};
})();

/*
 *  Intelligence.random(options) - returns a random intelligence. If no options are passed, the generated number
 *									will be on a normal distribution with mean 0 and standard deviation 45.
 *
 *										Example: Only generate above-average intelligence based on $activeSlave:
 *										Intelligence.random({limitIntelligence: [0, 100]})
 *
 *  Intelligence.config(configuration) - configures the random height generator globally and returns the current configuration
 *
 *	The options and their default values are:
 *	mean: 0 - What the average intelligence will be. Increasing this will make it more likely
 *				to generate a smart slave, but will not guarantee it.
 *				Minimum value: -100, maximum value: 100
 *	limitMult: [-3, 3] - Limit to this many standard deviations from the mean.
 *						In normal use, the values are almost never reached; only 0.27% of values are
 *						outside this range and need to be regenerated. With higher skew (see below),
 *						this might change.
 *	spread: 45 - The random standard deviation of the calculated distribution. A higher value
 *				will make it more likely to have extreme values, a lower value will make any
 *				generated values cluster around the mean. If spread is 0, it will always return the mean.
 *	skew: 0 - How much the height distribution skews to the right (positive) or left (negative) side
 *				of the height. Unless you have a very specific reason, you should not need to change this.
 *				Minimum value: -1000, maximum value: 1000
 *	limitIntelligence: [-100,100] - Limit the resulting height range.
 *									Warning: A small intelligence limit range not containing the
 *									mean, and with a low spread value results in the generator
 *									having to do lots of work generating and re-generating random
 *									intelligences until one "fits".
 *
 *  This was modeled using the Height generator above. For some more information, see the comments for that.
 */
window.Intelligence = (function() {
	"use strict";

	// Global configuration (for different game modes/options/types)
	let mean = 0;
	let minMult = -3.0;
	let maxMult = 3.0;
	let skew = 0.0;
	let spread = 45;
	let minIntelligence = -101;
	let maxIntelligence = 100;

	// Configuration method for the above values
	const _config = function(conf) {
		if (_.isUndefined(conf)) {
			return {mean: mean, limitMult: [minMult, maxMult], limitIntelligence: [minIntelligence, maxIntelligence], skew: skew, spread: spread};
		}
		if (_.isFinite(conf.mean)) {
			mean = Math.clamp(conf.mean, -100, 100);
		}
		if (_.isFinite(conf.skew)) {
			skew = Math.clamp(conf.skew, -1000, 1000);
		}
		if (_.isFinite(conf.spread)) {
			spread = Math.clamp(conf.spread, 0.1, 100);
		}
		if (_.isArray(conf.limitMult) && conf.limitMult.length === 2 && conf.limitMult[0] !== conf.limitMult[1] &&
			_.isFinite(conf.limitMult[0]) && _.isFinite(conf.limitMult[1])) {
			minMult = Math.min(conf.limitMult[0], conf.limitMult[1]);
			maxMult = Math.max(conf.limitMult[0], conf.limitMult[1]);
		}
		if (_.isArray(conf.limitIntelligence) && conf.limitIntelligence.length === 2 && conf.limitIntelligence[0] !== conf.limitIntelligence[1] &&
			_.isFinite(conf.limitIntelligence[0]) && _.isFinite(conf.limitIntelligence[1])) {
			minIntelligence = Math.clamp(Math.min(conf.limitIntelligence[0], conf.limitIntelligence[1]), -101, 100);
			maxIntelligence = Math.clamp(Math.max(conf.limitIntelligence[0], conf.limitIntelligence[1]), -101, 100);
		}
		return {limitMult: [minMult, maxMult], limitIntelligence: [minIntelligence, maxIntelligence], skew: skew, spread: spread};
	};

	// Helper method: Generate a skewed normal random variable with the skew s
	// Reference: http://azzalini.stat.unipd.it/SN/faq-r.html
	const skewedGaussian = function(s) {
		let randoms = gaussianPair();
		if (s === 0) {
			// Don't bother, return an unskewed normal distribution
			return randoms[0];
		}
		let delta = s / Math.sqrt(1 + s * s);
		let result = delta * randoms[0] + Math.sqrt(1 - delta * delta) * randoms[1];
		return randoms[0] >= 0 ? result : -result;
	};

	// Intelligence multiplier generator; skewed gaussian according to global parameters
	const multGenerator = function() {
		let result = skewedGaussian(skew);
		while (result < minMult || result > maxMult) {
			result = skewedGaussian(skew);
		}
		return result;
	};

	// Helper method: Transform the values from multGenerator to have the appropriate mean and standard deviation.
	const intelligenceGenerator = function() {
		let result = multGenerator() * spread + mean;
		while (result < minIntelligence || result > maxIntelligence) {
			result = multGenerator() * spread + mean;
		}
		return Math.ceil(result);
	};

	const _randomIntelligence = function(settings) {
		if (settings) {
			const currentConfig = _config();
			_config(settings);
			const result = intelligenceGenerator();
			_config(currentConfig);
			return result;
		}
		return intelligenceGenerator();
	};

	return {
		random: _randomIntelligence,
		config: _config,
	};
})();

// Helper method - generate two independent Gaussian numbers using Box-Muller transform
window.gaussianPair = function() {
	let r = Math.sqrt(-2.0 * Math.log(1 - Math.random()));
	let sigma = 2.0 * Math.PI * (1 - Math.random());
	return [r * Math.cos(sigma), r * Math.sin(sigma)];
};

/*
A categorizer is used to "slice" a value range into distinct categories in an efficient manner.

If the values are objects their property named 'value' will be set to whatever
the value used for the choice was. This is important for getters, where it can be accessed
via this.value.

--- Example ---
Original SugarCube code
<<if _Slave.muscles > 95>>
	Musc++
<<elseif _Slave.muscles > 30>>
	Musc+
<<elseif _Slave.muscles > 5>>
	Toned
<<elseif _Slave.muscles > -6>>
<<elseif _Slave.muscles > -31>>
	@@.red;weak@@
<<elseif _Slave.muscles > -96>>
	@@.red;weak+@@
<<else>>
	@@.red;weak++@@
<</if>>

As a categorizer
<<if ndef $cats>><<set $cats = {}>><</if>>
<<if ndef $cats.muscleCat>>
	<!-- This only gets set once, skipping much of the code evaluation, and can be set outside of the code in an "init" passage for further optimization -->
	<<set $cats.muscleCat = new Categorizer([96, 'Musc++'], [31, 'Musc+'], [6, 'Toned'], [-5, ''], [-30, '@@.red;weak@@'], [-95, '@@.red;weak+@@'], [-Infinity, '@@.red;weak++@@'])>>
<</if>>
<<print $cats.muscleCat.cat(_Slave.muscles)>>
*/

window.Categorizer = function() {
	this.cats = Array.prototype.slice.call(arguments)
		.filter(function(e, i, a) {
			return e instanceof Array && e.length === 2 && typeof e[0] === "number" && !isNaN(e[0]) &&
				a.findIndex(function(val) {
					return e[0] === val[0];
				}) === i; /* uniqueness test */
		})
		.sort(function(a, b) {
			return b[0] - a[0]; /* reverse sort */
		});
};

window.Categorizer.prototype.cat = function(val, def) {
	let result = def;
	if (typeof val === "number" && !isNaN(val)) {
		let foundCat = this.cats.find(function(e) {
			return val >= e[0];
		});
		if (foundCat) {
			result = foundCat[1];
		}
	}
	// Record the value for the result's getter, if it is an object
	// and doesn't have the property yet
	if (result === Object(result)) {
		result.value = val;
	}
	return result;
};

window.num = function(x) {
	const V = State.variables;
	const max = V.showNumbersMax;

	const ONE_TO_NINETEEN = [
		"one", "two", "three", "four", "five",
		"six", "seven", "eight", "nine", "ten",
		"eleven", "twelve", "thirteen", "fourteen", "fifteen",
		"sixteen", "seventeen", "eighteen", "nineteen",
	];

	const TENS = [
		"ten", "twenty", "thirty", "forty", "fifty",
		"sixty", "seventy", "eighty", "ninety",
	];

	const SCALES = ["thousand", "million", "billion", "trillion"];

	// helper function for use with Array.filter
	function isTruthy(item) {
		return !!item;
	}

	// convert a number into "chunks" of 0-999
	function chunk(number) {
		const thousands = [];

		while (number > 0) {
			thousands.push(number % 1000);
			number = Math.floor(number / 1000);
		}

		return thousands;
	}

	// translate a number from 1-999 into English
	function inEnglish(number) {
		let hundreds;
		let tens;
		let ones;
		const words = [];

		if (number < 20) {
			return ONE_TO_NINETEEN[number - 1]; // may be undefined
		}

		if (number < 100) {
			ones = number % 10;
			tens = number / 10 | 0; // equivalent to Math.floor(number / 10)

			words.push(TENS[tens - 1]);
			words.push(inEnglish(ones));

			return words.filter(isTruthy).join("-");
		}

		hundreds = number / 100 | 0;
		words.push(inEnglish(hundreds));
		words.push("hundred");
		words.push(inEnglish(number % 100));

		return words.filter(isTruthy).join(" ");
	}

	// append the word for a scale. Made for use with Array.map
	function appendScale(chunk, exp) {
		let scale;
		if (!chunk) {
			return null;
		}
		scale = SCALES[exp - 1];
		return [chunk, scale].filter(isTruthy).join(" ");
	}

	function commaNum(s) {
		if (!s) {
			return 0;
		}
		if (State.variables.formatNumbers !== 1) {
			return s;
		} else {
			return s.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
		}
	}

	if (V.showNumbers !== 2) {
		if (x === 0) {
			return "zero";
		}

		let string = chunk(x)
			.map(inEnglish)
			.map(appendScale)
			.filter(isTruthy)
			.reverse()
			.join(" ");

		if (V.showNumbers === 1) {
			if (x <= max) {
				if (x > 0) {
					return string;
				} else {
					return `negative ${ string}`;
				}
			} else {
				return commaNum(x);
			}
		} else {
			if (x > 0) {
				return string;
			} else {
				return `negative ${ string}`;
			}
		}
	} else {
		return commaNum(x);
	}
};

window.cashFormat = function(s) {
	if (!s) {
		s = 0;
	} else {
		s = Math.trunc(s);
	}
	if (s > 0) {
		return `¤${ num(s)}`;
	} else {
		return `¤${ s.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`;
	}
};

window.repFormat = function(s) {
	const V = State.variables;
	/* if (!s) { s = 0; }*/
	if (V.cheatMode === 1 || V.debugMode === 1) {
		if (s > 0) {
			return `@@.green;${ num(Math.round(s * 100) / 100) } rep@@`;
		} else if (s < 0) {
			return `@@.red;${ num(Math.round(s * 100) / 100) } rep@@`;
		} else {
			return `${num(Math.round(s * 100) / 100) } rep`;
		}
	} else {
		/* In order to calculate just how much any one category matters so we can show a "fuzzy" symbolic value to the player, we need to know how "busy" reputation was this week. To calculate this, I ADD income to expenses. Why? 100 - 100 and 10000 - 10000 BOTH are 0, but a +50 event matters a lot more in the first case than the second. I exclude overflow from the calculation because it's not a "real" expense for our purposes, and divide by half just to make percentages a bit easier. */
		let weight = s/(((V.lastWeeksRepIncome.Total - V.lastWeeksRepExpenses.Total) + V.lastWeeksRepExpenses.overflow)/2);
		if (weight > 0.60) {
			return "@@.green;+++++ rep@@";
		} else if (weight > 0.45) {
			return "@@.green;++++ rep@@";
		} else if (weight > 0.30) {
			return "@@.green;+++ rep@@";
		} else if (weight > 0.15) {
			return "@@.green;++ rep@@";
		} else if (weight > 0.0) {
			return "@@.green;+ rep@@";
		} else if (weight === 0) {
			return "0 rep";
		} else if (weight < -0.60) {
			return "@@.red;----- rep@@";
		} else if (weight < -0.45) {
			return "@@.red;---- rep@@";
		} else if (weight < -0.30) {
			return "@@.red;--- rep@@";
		} else if (weight < -0.15) {
			return "@@.red;-- rep@@";
		} else if (weight < 0) {
			return "@@.red;- rep@@";
		}
		/* return weight;*/
	}
};

window.massFormat = function (s) {
	if (!s) {
		s = 0;
	}
	if (s >= 1000) {
		s = num(Math.trunc(s / 1000));
		if (s !== 1) {
			return `${s} tons`;
		} else {
			return `${s} ton`;
		}
	} else {
		return `${num(s)} kg`;
	}
};

window.budgetLine = function(category, title) {
	let income;
	let expenses;
	let profits;
	if (passage() === "Rep Budget") {
		income = "$lastWeeksRepIncome";
		expenses = "$lastWeeksRepExpenses";
		profits = "$lastWeeksRepProfits";

		return `<<if ${income}.${category} || ${expenses}.${category}>><tr>\
		<td>${title}</td>\
		<td><<print repFormat(${income}.${category})>></td>\
		<td><<print repFormat(${expenses}.${category})>></td>\
		<<set ${profits}.${category} = (${income}.${category} + ${expenses}.${category})>>\
		<td><<print repFormat(${profits}.${category})>></td>\
		</tr><</if>>`;
	} else if (passage() === "Costs Budget") {
		income = "$lastWeeksCashIncome";
		expenses = "$lastWeeksCashExpenses";
		profits = "$lastWeeksCashProfits";

		return `<<if ${income}.${category} || ${expenses}.${category}>><tr>\
		<td>${title}</td>\
		<td>\
			<<if (${income}.${category}) > 0>>\
				@@.yellowgreen;<<print cashFormat(${income}.${category})>>@@ /*please don't put a plus sign in front of income, it's not done on a budget sheet. Safe to assume money is money unless it's in parenthesis or with a - sign.*/
			<<else>>\
				<<print cashFormat(${income}.${category})>>\
			<</if>>\
		</td>\
		<td>\
			<<if (${expenses}.${category}) < 0>>\
				@@.red;-<<print cashFormat(Math.abs(${expenses}.${category}))>>\
			<<else>>\
				<<print cashFormat(${expenses}.${category})>>\
			<</if>>\
		</td>\
		<<set ${profits}.${category} = (${income}.${category} + ${expenses}.${category})>>\
		<td>\
			<<if (${profits}.${category}) > 0>>\
				@@.yellowgreen;<<print cashFormat(${profits}.${category})>>.@@
			<<elseif (${profits}.${category}) < 0>>\
				@@.red;-<<print cashFormat(Math.abs(${profits}.${category}))>>.@@
			<<else>>\
				<<print cashFormat(${profits}.${category})>>\
			<</if>>\
		</td>\
		</tr><</if>>`;
	}
};

window.isFloat = function(n) {
	return Number.isFinite(n) && Math.floor(n) !== n;
};

window.jsRandom = function(min, max) {
	return Math.floor(Math.random()*(max-min+1)+min);
};

window.jsRandomMany = function(arr, count) {
	let result = [];
	let _tmp = arr.slice();
	for (let i = 0; i < count; i++) {
		let index = Math.ceil(Math.random() * 10) % _tmp.length;
		result.push(_tmp.splice(index, 1)[0]);
	}
	return result;
};

// This function wants an array - which explains why it works like array.random(). Give it one or you'll face a NaN
window.jsEither = function(choices) {
	let index = Math.floor(Math.random() * choices.length);
	return choices[index];
};

/*
Make everything waiting for this execute. Usage:

let doSomething = function() {
	... your initialization code goes here ...
};
if(typeof Categorizer === 'function') {
	doSomething();
} else {
	jQuery(document).one('categorizer.ready', doSomething);
}
*/
jQuery(document).trigger("categorizer.ready");

window.hashChoice = function hashChoice(obj) {
	let randint = Math.floor(Math.random()*hashSum(obj));
	let ret;
	Object.keys(obj).some((key) => {
		if (randint < obj[key]) {
			ret = key;
			return true;
		} else {
			randint -= obj[key];
			return false;
		}
	});
	return ret;
};

window.hashSum = function hashSum(obj) {
	let sum = 0;
	Object.keys(obj).forEach((key) => {
		sum += obj[key];
	});
	return sum;
};

window.arr2obj = function arr2obj(arr) {
	const obj = {};
	arr.forEach((item) => {
		obj[item] = 1;
	});
	return obj;
};

window.hashPush = function hashPush(obj, ...rest) {
	rest.forEach((item) => {
		if (obj[item] === undefined) obj[item] = 1;
		else obj[item] += 1;
	});
};

window.weightedArray2HashMap = function weightedArray2HashMap(arr) {
	const obj = {};
	arr.forEach((item) => {
		if (obj[item] === undefined) obj[item] = 1;
		else obj[item] += 1;
	});
	return obj;
};

window.between = function between(a, low, high) {
	if (low === null) low = -Infinity;
	if (high === null) high = Infinity;
	return (a > low && a < high);
};

// generate a random, almost unique ID that is compliant (possibly) with RFC 4122
window.generateNewID = function generateNewID() {
	let date = Date.now(); // high-precision timer
	let uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
		let r = (date + Math.random()*16)%16 | 0;
		date = Math.floor(date/16);
		return (c==="x" ? r : (r & 0x3 | 0x8)).toString(16);
	});
	return uuid;
};

window.arraySwap = function arraySwap(array, a, b) {
	const tmp = array[a];
	array[a] = array[b];
	array[b] = tmp;
};

// circumvents sugarcube, allowing a plain HTML5 UI within it
window.html5passage = function html5passage(passageFunction) {
	$(document).one(":passagedisplay", (ev) => {
		const element = document.createElement("div");
		element.classList.add("passage");
		document.getElementById("passages").appendChild(element);
		passageFunction(element);
		$(document).off(":passagedisplay");
	});
};

// If you want to include a SugarCube passage in a JS function use this. The result must be printed using the <<print>> macro. passageTitle must be a string.
window.jsInclude = function(passageTitle) {
	if (Story.has(passageTitle)) {
		return Story.get(passageTitle).processText();
	} else {
		return `<span class="red">Error: Passage ${passageTitle} does not exist.</span>`;
	}
};

window.capFirstChar = function capFirstChar(string) {
	return string.charAt(0).toUpperCase() + string.substr(1);
};

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

window.getSlaveTrustClass = /** @param {App.Entity.SlaveState} slave */ function(slave) {
	if ((!slave) || (!State)) {
		return undefined;
	}

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

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

// takes an integer e.g. $activeSlave.hLength, returns a string in the format 10 inches
window.cmToInchString = function(s) {
	let inches = Math.round(s/2.54);
	if (inches === 0) {
		if (s === 0) {
			inches += " inches";
		} else {
			inches = "less than an inch";
		}
	} else if (inches === 1) {
		inches += " inch";
	} else {
		inches += " inches";
	}
	return inches;
};

// takes an integer e.g. $activeSlave.height, returns a string in the format 6'5"
window.cmToFootInchString = function(s) {
	if (Math.round(s/2.54) < 12) {
		return cmToInchString(s);
	}
	return `${Math.trunc(Math.round(s/2.54)/12) }'${ Math.round(s/2.54)%12 }"`;
};

// takes a dick value e.g. $activeSlave.dick, returns a string in the format 6 inches
window.dickToInchString = function(s) {
	return cmToInchString(dickToCM(s));
};

// takes a dick value e.g. $activeSlave.dick, returns an int of the dick length in cm
window.dickToCM = function(s) {
	if (s < 9) {
		return s*5;
	} else if (s === 9) {
		return 50;
	}
	return s*6;
};

// takes a ball value e.g. $activeSlave.balls, returns a string in the format 3 inches
window.ballsToInchString = function(s) {
	return cmToInchString(ballsToCM(s));
};

// takes a ball value e.g. $activeSlave.balls, returns an int of the ball size in cm
window.ballsToCM = function(s) {
	if (s < 2) {
		return 0;
	}
	return (s<10?(s-1)*2:s*2);
};

// takes a dick value e.g. $activeSlave.dick, returns a string in the format of either `20cm (8 inches)`, `8 inches`, or `20cm`
window.dickToEitherUnit = function(s) {
	if (State.variables.showInches === 1) {
		return `${dickToCM(s) }cm (${ dickToInchString(s) })`;
	}
	if (State.variables.showInches === 2) {
		return dickToInchString(s);
	}
	return `${dickToCM(s) }cm`;
};

// takes a ball value e.g. $activeSlave.balls, returns a string in the format of either `20cm (8 inches)`, `8 inches`, or `20cm`
window.ballsToEitherUnit = function(s) {
	if (State.variables.showInches === 1) {
		return `${ballsToCM(s) }cm (${ ballsToInchString(s) })`;
	}
	if (State.variables.showInches === 2) {
		return ballsToInchString(s);
	}
	return `${ballsToCM(s) }cm`;
};

// takes an int in centimeters e.g. $activeSlave.height, returns a string in the format of either `200cm (6'7")`, `6'7"`, or `200cm`
window.heightToEitherUnit = function(s) {
	if (State.variables.showInches === 1) {
		return `${s }cm (${ cmToFootInchString(s) })`;
	}
	if (State.variables.showInches === 2) {
		return cmToFootInchString(s);
	}
	return `${s }cm`;
};

// takes an int in centimeters e.g. $activeSlave.hLength, returns a string in the format of either `30cm (12 inches)`, `12 inches`, or `30cm`
window.lengthToEitherUnit = function(s) {
	if (State.variables.showInches === 1) {
		return `${s }cm (${ cmToInchString(s) })`;
	}
	if (State.variables.showInches === 2) {
		return cmToInchString(s);
	}
	return `${s }cm`;
};

window.ordinalSuffix = function ordinalSuffix(i) {
	let j = i % 10;
	let k = i % 100;
	if (j === 1 && k !== 11) {
		return `${i }st`;
	}
	if (j === 2 && k !== 12) {
		return `${i }nd`;
	}
	if (j === 3 && k !== 13) {
		return `${i }rd`;
	}
	return `${i }th`;
};

window.removeDuplicates = function removeDuplicates(array) {
	return [...new Set(array)];
};

window.induceLactation = /** @param {App.Entity.SlaveState} slave */ function induceLactation(slave) {
	let pronouns = getPronouns(slave);
	let His = capFirstChar(pronouns.possessive);
	let r = "";
	if (slave.induceLactation >= 10) {
		if (jsRandom(1, 100) < slave.induceLactation) {
			r += `${His} breasts have been stimulated often enough to @@.lime;induce lactation.@@`;
			slave.induceLactation = 0;
			slave.lactationDuration = 2;
			slave.lactation = 1;
		}
	}
	return r;
};

window.ResearchLabStockPile = function() {
	const V = State.variables;
	return `__Stockpile__
	Prosthetics interfaces: ${num(V.stockpile.basicPLimbInterface + V.stockpile.advPLimbInterface)}
	 Basic : $stockpile.basicPLimbInterface
	 Advanced: $stockpile.advPLimbInterface
	Limbs: ${num(V.stockpile.basicPLimb + V.stockpile.advSexPLimb + V.stockpile.advGracePLimb + V.stockpile.advCombatPLimb + V.stockpile.cyberneticPLimb)}
	 Basic: $stockpile.basicPLimb
	 Sex: $stockpile.advSexPLimb
	 Beauty: $stockpile.advGracePLimb
	 Combat: $stockpile.advCombatPLimb
	 Cybernetic: $stockpile.cyberneticPLimb
	Implants: ${num(V.stockpile.ocularImplant + V.stockpile.cochlearImplant + V.stockpile.erectileImplant)}
	 Ocular: $stockpile.ocularImplant
	 Cochlear: $stockpile.cochlearImplant
	// Erectile: $stockpile.erectileImplant//
	Electrolarynx: $stockpile.electrolarynx`;
};

window.originPronounReplace = /** @param {App.Entity.SlaveState} slave */ function(slave) {
	let r = slave.origin;
	switch (r) {
		case "She was the result of unprotected sex with a client. Her mother tracked you down years after her birth to force her upon you.":
			return `${capFirstChar(slave.pronoun)} was the result of unprotected sex with a client. ${capFirstChar(slave.possessive)} mother tracked you down years after ${slave.possessive} birth to force ${slave.object} upon you.`;
		case "You kept her after her owner failed to pay your bill for performing surgery on her.":
			return `You kept ${slave.object} after ${slave.possessive} owner failed to pay your bill for performing surgery on ${slave.object}.`;
		case "She comes from old money and sold herself into slavery to satisfy her obsession with the practice, believing her family would buy her back out of slavery later.":
			return `${capFirstChar(slave.pronoun)} comes from old money and sold herself into slavery to satisfy ${slave.possessive} obsession with the practice, believing ${slave.possessive} family would buy ${slave.object} back out of slavery later.`;
		case "When you took her from her previous owner, she was locked into a beautiful rosewood box lined with red velvet, crying.":
			return `When you took ${slave.object} from ${slave.possessive} previous owner, ${slave.pronoun} was locked into a beautiful rosewood box lined with red velvet, crying.`;
		case "Her husband sold her into slavery to escape his debts.":
			return `${capFirstChar(slave.possessive)} husband sold ${slave.object} into slavery to escape his debts.`;
		case "She was voluntarily enslaved after she decided that your arcology was the best place for her to get the steroids that she'd allowed to define her life.":
			return `${capFirstChar(slave.pronoun)} was voluntarily enslaved after ${slave.pronoun} decided that your arcology was the best place for ${slave.object} to get the steroids that ${slave.pronoun}'d allowed to define ${slave.possessive} life.`;
		case "She came to you to escape being sold to a cruel master after her producer informed her of her debt.":
			return `${capFirstChar(slave.pronoun)} came to you to escape being sold to a cruel master after ${slave.possessive} producer informed ${slave.object} of ${slave.possessive} debt.`;
		case "You tricked her into enslavement, manipulating her based on her surgical addiction.":
			return `You tricked ${slave.object} into enslavement, manipulating ${slave.object} based on ${slave.possessive} surgical addiction.`;
		case "You helped free her from a POW camp after being abandoned by her country, leaving her deeply indebted to you.":
			return `You helped free ${slave.object} from a POW camp after being abandoned by ${slave.possessive} country, leaving ${slave.object} deeply indebted to you.`;
		case "You purchased her in order to pave the way for her brother to take the throne.":
			return `You purchased ${slave.object} in order to pave the way for ${slave.possessive} brother to take the throne.`;
		case "You purchased her as a favor to her father.":
			return `You purchased ${slave.object} as a favor to ${slave.possessive} father.`;
		case "You purchased her from a King after his son put an illegitimate heir in her womb.":
			return `You purchased ${slave.object} from a King after his son put an illegitimate heir in ${slave.possessive} womb.`;
		case "You acquired her in the last stages of your career as a successful venture capitalist.":
		case "Drugs and alcohol can be a potent mix; the night that followed it can sometimes be hard to remember. Needless to say, once your belly began swelling with her, you had to temporarily switch to a desk job for your mercenary group.":
		case "You acquired her in the last stages of your career as a noted private military contractor.":
		case "You never thought you would be capable of impregnating yourself, but years of pleasuring yourself with yourself after missions managed to create her.":
		case "A fresh capture once overpowered you and had his way with you. You kept her as a painful reminder to never lower your guard again.":
		case "Your slaving troop kept several girls as fucktoys; you sired her in your favorite.":
		case "You enslaved her personally during the last stages of your slaving career.":
		case "You sired her in yourself after an arcology owner, impressed by your work, rewarded you with a night you'll never forget.":
		case "You conceived her after a male arcology owner, impressed by your work, rewarded you with a night you'll never forget.":
		case "You sired her after a female arcology owner, impressed by your work, rewarded you with a night you'll never forget.":
		case "You received her as a gift from an arcology owner impressed by your work.":
		case "You captured her during your transition to the arcology":
		case "You won her at cards, a memento from your life as one of the idle rich before you became an arcology owner.":
		case "You brought her into the arcology mindbroken, little more than a walking collection of fuckable holes.":
		case "You brought her into the arcology mindbroken, little more than a human onahole.":
		case "She grew up sheltered and submissive, making her an easy target for enslavement.":
		case "She was fresh from the slave markets when you acquired her.":
		case "She chose to be a slave because the romanticized view of it she had turns her on.":
		case "She was previously owned by a creative sadist, who has left a variety of mental scars on her.":
		case "She was taken as a slave by a Sultan, who presented her as a gift to a surveyor.":
		case "She is the winner of a martial arts slave tournament. You won her in a bet.":
		case "She was homeless and willing to do anything for food, which in the end resulted in her becoming a slave.":
		case "She was sold to you by an anonymous person who wanted her to suffer.":
		case "You received her from a surgeon who botched an implant operation on her and needed to get her out of sight.":
		case "She offered herself to you for enslavement to escape having plastic surgery foisted on her.":
		case "You turned her into a slave girl after she fell into debt to you.":
		case "She was raised in a radical slave school that treated her with drugs and surgery from a very young age.":
		case "She was raised in a radical slave school that treated her from a very young age, up to the point that she never experienced male puberty.":
		case "She was a runaway slave captured by a gang outside your arcology. You bought her cheap after she was harshly used by them.":
		case "She was the private slave of a con artist cult leader before he had to abandon her and flee.":
		case "You helped her give birth, leaving her deeply indebted to you.":
		case "You purchased her from a King after she expressed knowledge of the prince's affair with another servant.":
			r = r.replace(/\bherself\b/g, slave.objectReflexive);
			r = r.replace(/\bHerself\b/g, capFirstChar(slave.objectReflexive));
			r = r.replace(/\bshe\b/g, slave.pronoun);
			r = r.replace(/\bShe\b/g, capFirstChar(slave.pronoun));
			r = r.replace(/\bher\b/g, slave.object);
			r = r.replace(/\bHer\b/g, capFirstChar(slave.object));
			return r;
		default:
			r = r.replace(/\bherself\b/g, slave.objectReflexive);
			r = r.replace(/\bHerself\b/g, capFirstChar(slave.objectReflexive));
			r = r.replace(/\bshe\b/g, slave.pronoun);
			r = r.replace(/\bShe\b/g, capFirstChar(slave.pronoun));
			r = r.replace(/\bher\b/g, slave.possessive);
			r = r.replace(/\bHer\b/g, capFirstChar(slave.possessive));
			return r;
	}
};

window.HackingSkillMultiplier = function() {
	const V = State.variables;
	if (V.PC.hacking <= -100) {
		return 1.5;
	} else if (V.PC.hacking <= -75) {
		return 1.35;
	} else if (V.PC.hacking <= -50) {
		return 1.25;
	} else if (V.PC.hacking <= -25) {
		return 1.15;
	} else if (V.PC.hacking < 0) {
		return 1.10;
	} else if (V.PC.hacking === 0) {
		return 1;
	} else if (V.PC.hacking <= 10) {
		return 0.97;
	} else if (V.PC.hacking <= 25) {
		return 0.95;
	} else if (V.PC.hacking <= 50) {
		return 0.90;
	} else if (V.PC.hacking <= 75) {
		return 0.85;
	} else if (V.PC.hacking <= 100) {
		return 0.80;
	} else {
		return 0.75;
	}
};

window.opentab = function(evt, tabName) {
	const V = State.variables;
	/* var passage = passage().trim().replace(/ /g,"+");*/
	const tabcontent = document.getElementsByClassName("tabcontent");
	for (let i = 0; i < tabcontent.length; i++) {
		tabcontent[i].style.display = "none";
	}
	const tablinks = document.getElementsByClassName("tablinks");
	for (let i = 0; i < tablinks.length; i++) {
		tablinks[i].className = tablinks[i].className.replace(" active", "");
	}
	V.tabChoice[passage().trim().replace(/ |'/g, "")] = tabName; /* The regex strips spaces and " ' " from passage names, making "Servants' Quarters" into "ServantsQuarters" and allowing it to be used as a label in this object. */
	document.getElementById(tabName).style.display = "block";
	evt.currentTarget.className += " active";
};

/**
 * Creates a HTML element with custom SugarCube attributes which works as a passage link
 *
 * The result works in the same way as the wiki markup in the SugarCube
 * @see https://www.motoslave.net/sugarcube/2/docs/#markup-html-attribute
 * @param {string} linkText link text
 * @param {string} passage the passage name to link to
 * @param {string} [setter=''] setter tet (optional)
 * @param {string} [elementType='a'] element type (optional) default is 'a'.
 * Could be any of 'a', 'audio', img', 'source', 'video'
 * @returns {string} element text
 *
 * @example
 * // equal to [[Go to town|Town]]
 * App.UI.passageLink("Go to town", "Town")
 */
App.UI.passageLink = function(linkText, passage, setter, elementType) {
	if (!elementType) elementType = "a";

	let res = `<${elementType} data-passage="${passage}"`;
	if (setter) {
		res += ` data-setter="${setter}"`;
	}
	res += `>${linkText}</${elementType}>`;
	return res;
};

window.SkillIncrease = (function() {
	return {
		Oral: OralSkillIncrease,
		Vaginal: VaginalSkillIncrease,
		Anal: AnalSkillIncrease,
		Whore: WhoreSkillIncrease,
		Entertain: EntertainSkillIncrease
	};

	/* call as SkillIncrease.Oral() */
	/** @param {App.Entity.SlaveState} slave */
	function OralSkillIncrease(slave, skillIncrease) {
		const He = capFirstChar(slave.pronoun);
		const his = slave.possessivePronoun;
		let r = "";
		skillIncrease = skillIncrease || State.variables.skillIncrease || 1;

		if (slave.skill.oral <= 10) {
			if (slave.skill.oral + skillIncrease > 10) {
				r = ` <span class="green">${He} now has basic knowledge about oral sex,</span> and can at least suck a dick without constant gagging.`;
			}
		} else if (slave.skill.oral <= 30) {
			if (slave.skill.oral + skillIncrease > 30) {
				r = ` <span class="green">${He} now has some oral skills,</span> and can reliably bring dicks and pussies to climax with ${his} mouth.`;
			}
		} else if (slave.skill.oral <= 60) {
			if (slave.skill.oral + skillIncrease > 60) {
				r = ` <span class="green">${He} is now an oral expert,</span> and has a delightfully experienced tongue.`;
			}
		} else if (slave.skill.oral < 100) {
			if (slave.skill.oral + skillIncrease >= 100) {
				r = ` <span class="green">${He} has mastered oral sex,</span> and can learn nothing more about sucking dick or eating pussy.`;
			}
		}
		slave.skill.oral += skillIncrease;
		return r;
	}

	/* call as SkillIncrease.Vaginal() */
	/** @param {App.Entity.SlaveState} slave */
	function VaginalSkillIncrease(slave, skillIncrease) {
		const He = capFirstChar(slave.pronoun);
		let r = "";
		skillIncrease = skillIncrease || State.variables.skillIncrease || 1;

		if (slave.skill.vaginal <= 10) {
			if (slave.skill.vaginal + skillIncrease > 10) {
				r = `<span class="green">${He} now has basic knowledge about vaginal sex,</span> and can avoid some of the common pitfalls and turnoffs.`;
			}
		} else if (slave.skill.vaginal <= 30) {
			if (slave.skill.vaginal + skillIncrease > 30) {
				r = `<span class="green">${He} now has some vaginal sex skills,</span> and can do more than just lie there and take it.`;
			}
		} else if (slave.skill.vaginal <= 60) {
			if (slave.skill.vaginal + skillIncrease > 60) {
				r = `<span class="green">${He} is now a vaginal sex expert,</span> and has the muscular control to massage anything that's inside $him.`;
			}
		} else if (slave.skill.vaginal < 100) {
			if (slave.skill.vaginal + skillIncrease >= 100) {
				r = `<span class="green">${He} has mastered vaginal sex,</span> and can learn nothing more about tribbing or taking dick.`;
			}
		}
		slave.skill.vaginal += skillIncrease;
		return r;
	}

	/* call as SkillIncrease.Anal() */
	/** @param {App.Entity.SlaveState} slave */
	function AnalSkillIncrease(slave, skillIncrease) {
		const He = capFirstChar(slave.pronoun);
		const his = slave.possessivePronoun;
		let r ="";
		skillIncrease = skillIncrease || State.variables.skillIncrease || 1;

		if (slave.skill.anal <= 10) {
			if (slave.skill.anal + skillIncrease > 10) {
				r = `<span class="green">${He} now has basic knowledge about anal sex,</span> and can accept penetration of ${his} anus without danger.`;
			}
		} else if (slave.skill.anal <= 30) {
			if (slave.skill.anal + skillIncrease > 30) {
				r = `<span class="green">${He} now has some anal sex skills,</span> and needs less preparation before taking rough penetration.`;
			}
		} else if (slave.skill.anal <= 60) {
			if (slave.skill.anal + skillIncrease > 60) {
				r = `<span class="green">${He} is now an anal sex expert,</span> and knows how to use ${his} sphincter to please.`;
			}
		} else if (slave.skill.anal < 100) {
			if (slave.skill.anal + skillIncrease >= 100) {
				r = `<span class="green">${He} has mastered anal sex,</span> and can learn nothing more about taking it up the ass.`;
			}
		}
		slave.skill.anal += skillIncrease;
		return r;
	}

	/* call as SkillIncrease.Whore() */
	/** @param {App.Entity.SlaveState} slave */
	function WhoreSkillIncrease(slave, skillIncrease) {
		const He = capFirstChar(slave.pronoun);
		const his = slave.possessivePronoun;
		let r ="";
		skillIncrease = skillIncrease || State.variables.skillIncrease || 1;

		if (slave.skill.whoring <= 10) {
			if (slave.skill.whoring + skillIncrease > 10) {
				r = `<span class="green">${He} now has basic knowledge about how to whore,</span> and can avoid some potentially dangerous situations.`;
			}
		} else if (slave.skill.whoring <= 30) {
			if (slave.skill.whoring + skillIncrease > 30) {
				r = `<span class="green">${He} now has some skill as a whore,</span> and knows how to sell ${his} body at a good price.`;
			}
		} else if (slave.skill.whoring <= 60) {
			if (slave.skill.whoring + skillIncrease > 60) {
				r = `<span class="green">${He} is now an expert whore,</span> and can often make clients forget that $he's a prostitute they're paying for.`;
			}
		} else if (slave.skill.whoring < 100) {
			if (slave.skill.whoring + skillIncrease >= 100) {
				r = `<span class="green">${He} is now a masterful whore,</span> and can learn nothing more about prostitution.`;
			}
		}
		slave.skill.whoring += skillIncrease;
		return r;
	}

	/* call as SkillIncrease.Entertain() */
	/** @param {App.Entity.SlaveState} slave */
	function EntertainSkillIncrease(slave, skillIncrease) {
		const He = capFirstChar(slave.pronoun);
		let r ="";
		skillIncrease = skillIncrease || State.variables.skillIncrease || 1;

		if (slave.skill.entertainment <= 10) {
			if (slave.skill.entertainment + skillIncrease > 10) {
				r = `<span class="green">${He} now has basic knowledge about how to be entertaining,</span> and can usually avoid serious faux pas.`;
			}
		} else if (slave.skill.entertainment <= 30) {
			if (slave.skill.entertainment + skillIncrease > 30) {
				r = `<span class="green">${He} now has some skill as an entertainer,</span> and can flirt, dance, and strip acceptably.`;
			}
		} else if (slave.skill.entertainment <= 60) {
			if (slave.skill.entertainment + skillIncrease > 60) {
				r = `<span class="green">${He} is now an expert entertainer,</span> and can flirt engagingly, dance alluringly, and strip arousingly.`;
			}
		} else if (slave.skill.entertainment < 100) {
			if (slave.skill.entertainment + skillIncrease >= 100) {
				r = `<span class="green">${He} is now a masterful entertainer,</span> and can learn nothing more about flirting, dancing, or stripping.`;
			}
		}
		slave.skill.entertainment += skillIncrease;
		return r;
	}
})();

window.upgradeMultiplierArcology = function() {
	const V = State.variables;
	if (V.PC.career === "engineer" || (V.arcologies[0].FSRestartDecoration >= 100 && V.eugenicsFullControl === 0)) {
		return 0.6;
	} else if (V.PC.engineering <= -100) {
		return 1.5;
	} else if (V.PC.engineering <= -75) {
		return 1.35;
	} else if (V.PC.engineering <= -50) {
		return 1.25;
	} else if (V.PC.engineering <= -25) {
		return 1.15;
	} else if (V.PC.engineering < 0) {
		return 1.10;
	} else if (V.PC.engineering === 0) {
		return 1;
	} else if (V.PC.engineering <= 10) {
		return 0.97;
	} else if (V.PC.engineering <= 25) {
		return 0.95;
	} else if (V.PC.engineering <= 50) {
		return 0.90;
	} else if (V.PC.engineering <= 75) {
		return 0.85;
	} else if (V.PC.engineering <= 100) {
		return 0.80;
	} else {
		return 0.75;
	}
};

window.upgradeMultiplierMedicine = function() {
	const V = State.variables;
	if (V.PC.career === "medicine" || (V.arcologies[0].FSRestartDecoration >= 100 && V.eugenicsFullControl === 0)) {
		return 0.6;
	} else if (V.PC.medicine <= -100) {
		return 1.5;
	} else if (V.PC.medicine <= -75) {
		return 1.35;
	} else if (V.PC.medicine <= -50) {
		return 1.25;
	} else if (V.PC.medicine <= -25) {
		return 1.15;
	} else if (V.PC.medicine < 0) {
		return 1.10;
	} else if (V.PC.medicine === 0) {
		return 1;
	} else if (V.PC.medicine <= 10) {
		return 0.97;
	} else if (V.PC.medicine <= 25) {
		return 0.95;
	} else if (V.PC.medicine <= 50) {
		return 0.90;
	} else if (V.PC.medicine <= 75) {
		return 0.85;
	} else if (V.PC.medicine <= 100) {
		return 0.80;
	} else {
		return 0.75;
	}
};

window.upgradeMultiplierTrade = function() {
	const V = State.variables;
	if (V.PC.trading <= -100) {
		return 1.5;
	} else if (V.PC.trading <= -75) {
		return 1.35;
	} else if (V.PC.trading <= -50) {
		return 1.25;
	} else if (V.PC.trading <= -25) {
		return 1.15;
	} else if (V.PC.trading < 0) {
		return 1.10;
	} else if (V.PC.trading === 0) {
		return 1;
	} else if (V.PC.trading <= 10) {
		return 0.97;
	} else if (V.PC.trading <= 25) {
		return 0.95;
	} else if (V.PC.trading <= 50) {
		return 0.90;
	} else if (V.PC.trading <= 75) {
		return 0.85;
	} else if (V.PC.trading <= 100) {
		return 0.80;
	} else {
		return 0.75;
	}
};

window.jsNdef = function(input) {
	if (typeof input === "undefined") return true; else return false;
};

window.jsDef = function(input) {
	if (typeof input !== "undefined") return true; else return false;
};

/* Return a career at random that would be suitable for the given slave.

	Currently only considers their age
 */
window.randomCareer = function(slave) {
	if (slave.actualAge < 16) {
		return setup.veryYoungCareers.random();
	} else if (slave.actualAge <= 24) {
		return setup.youngCareers.random();
	} else if (slave.intelligenceImplant >= 10) {
		return setup.educatedCareers.random();
	} else {
		return setup.uneducatedCareers.random();
	}
};

window.resyncSlaveToAge = function(slave) {
	slave.height = Height.random(slave);
	slave.pubertyXX = slave.actualAge < slave.pubertyAgeXX ? 0 : 1;
	slave.pubertyXY = slave.actualAge < slave.pubertyAgeXY ? 0 : 1;
	if (slave.actualAge < 12) {
		slave.vagina = 0;
		slave.trueVirgin = 0;
		slave.preg = -1;
		slave.belly = 0;
		slave.bellyPreg = 0;
		slave.ovaries = 1;
		slave.anus = 0;
		slave.skill.anal = 0;
		slave.skill.oral = 0;
		slave.skill.whoring = 0;
		slave.skill.entertainment = 0;
		slave.skill.combat = 0;
		slave.skill.vaginal = 0;
		slave.attrXY = 50;
		slave.attrXX = 50;
		slave.boobs = 200;
		slave.birthWeek = 0;
		SetBellySize(slave);
		if (slave.dick > 0) {
			slave.dick = 1;
		}
		if (slave.balls > 0) {
			slave.balls = 1;
		}
	} else {
		slave.boobs = Math.max(slave.boobs, 500);
		if (slave.dick > 2) {
			slave.dick = 2;
		}
		if (slave.balls > 2) {
			slave.balls = 2;
		}
	}
	slave.career = randomCareer(slave);
};

window.IncreasePCSkills = function(input, increase = 1) {
	const player = State.variables.PC, oldSkill = player[input];
	player[input] += increase;
	if (oldSkill <= 10) {
		if (player[input] >= 10) {
			return ` <span class='green'> <br>You have gained basic knowlege in ${input}.</span>`;
		}
	} else if (oldSkill <= 30) {
		if (player[input] >= 30) {
			return ` <span class='green'> <br>You have gained some knowlege in ${input}.</span>`;
		}
	} else if (oldSkill <= 60) {
		if (player[input] >= 60) {
			return ` <span class='green'> <br>You have become an expert in ${input}.</span>`;
		}
	} else if (oldSkill < 100) {
		if (player[input] >= 100) {
			return ` <span class='green'> <br>You have mastered ${input}.</span>`;
		}
	}
};

window.randomRaceSkin = function(raceName) {
	let skin;
	switch (raceName) {
		case "asian":
			skin = jsEither(["dark olive", "light olive", "light"]);
			break;
		case "amerindian":
		case "indo-aryan":
		case "malay":
		case "pacific islander":
			skin = jsEither(["dark", "light"]);
			break;
		case "black":
			skin = jsEither(["black", "brown", "dark brown"]);
			break;
		case "latina":
			skin = jsEither(["brown", "dark brown", "dark olive", "light olive", "tan"]);
			break;
		case "middle eastern":
		case "semitic":
		case "southern european":
			skin = jsEither(["fair", "light olive", "light", "tan"]);
			break;
		case "white":
			skin = jsEither(["fair", "light", "pale"]);
			break;
		default:
			skin = jsEither(["dark", "light", "pale"]);
			break;
	}
	return skin;
};

window.randomRaceEye = function(raceName) {
	let eye;
	switch (raceName) {
		case "asian":
		case "black":
		case "indo-aryan":
		case "middle eastern":
		case "pacific islander":
			eye = jsEither(["brown", "brown", "brown", "brown", "green"]);
			break;
		case "amerindian":
		case "latina":
			eye = jsEither(["brown", "brown", "brown", "green"]);
			break;
		case "malay":
		case "southern european":
			eye = jsEither(["blue", "brown", "brown", "brown", "brown", "brown", "brown", "green"]);
			break;
		case "semitic":
		case "white":
			eye = jsEither(["blue", "brown", "brown", "brown", "green", "green"]);
			break;
		default:
			eye = jsEither(["blue", "brown", "green"]);
			break;
	}
	return eye;
};

window.randomRaceHair = function(raceName) {
	let hair;
	switch (raceName) {
		case "asian":
		case "amerindian":
		case "indo-aryan":
		case "malay":
		case "middle eastern":
		case "pacific islander":
			hair = jsEither(["black", "black", "black", "black", "black", "brown"]);
			break;
		case "black":
		case "latina":
		case "semitic":
		case "southern european":
			hair = jsEither(["black", "black", "brown", "brown"]);
			break;
		case "white":
			hair = jsEither(["black", "black", "blonde", "brown", "brown", "red"]);
			break;
		default:
			hair = jsEither(["black", "black", "black", "black", "blonde", "brown", "brown", "red"]);
			break;
	}
	return hair;
};

window.skinToneLevel = function(skinTone) {
	if (!setup.naturalSkins.includes(skinTone)) {
		return undefined;
	}
	const skinToMelanin = {
		"pure black": 25,
		"ebony": 24,
		"black": 23,
		"dark brown": 22,
		"brown": 21,
		"light brown": 20,
		"dark beige": 19,
		"beige": 18,
		"light beige": 17,
		"dark": 16,
		"dark olive": 15,
		"bronze": 14,
		"olive": 13,
		"tan": 12,
		"light olive": 11,
		"light": 10,
		"fair": 9,
		"very fair": 8,
		"extremely fair": 7,
		"pale": 6,
		"very pale": 5,
		"extremely pale": 4,
		"white": 3,
		"ivory": 2,
		"pure white": 1
	};
	return skinToMelanin[skinTone];
};

window.changeSkinTone = function(skin, value) {
	if (!setup.naturalSkins.includes(skin)) {
		return skin;
	}
	const skinToMelanin = {
		"pure black": 25,
		"ebony": 24,
		"black": 23,
		"dark brown": 22,
		"brown": 21,
		"light brown": 20,
		"dark beige": 19,
		"beige": 18,
		"light beige": 17,
		"dark": 16,
		"dark olive": 15,
		"bronze": 14,
		"olive": 13,
		"tan": 12,
		"light olive": 11,
		"light": 10,
		"fair": 9,
		"very fair": 8,
		"extremely fair": 7,
		"pale": 6,
		"very pale": 5,
		"extremely pale": 4,
		"white": 3,
		"ivory": 2,
		"pure white": 1
	};
	let newSkin = (skinToMelanin[skin] + value);
	if (newSkin > 25) {
		newSkin = 25;
	} else if (newSkin < 1) {
		newSkin = 1;
	}
	let prop;
	for (prop in skinToMelanin) {
		if (!skinToMelanin.hasOwnProperty(prop)) continue;
		if (newSkin >= skinToMelanin[prop]) return prop;
	}
	return prop;
};