﻿:: UtilJS [script]

/*
 * 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 explination:
 * 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)
	var minMult = -3.0;
	var maxMult = 3.0;
	var skew = 0.0;
	var spread = 0.05;
	var minHeight = 0;
	var 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": 163.8, "Albanian": 161.8, "Algerian": 162, "American.asian": 158.4, "American.black": 163.6, "American.latina": 158.9, "American.white": 165, "American": 161.8, 
		"Andorran": undefined, "Angolan": undefined, "Antiguan": 156.8, "Argentinian": 159.6, "Armenian": 158.1, "Aruban": 158, "Australian": 161.8, "Austrian": 166, "Azerbaijani": 162.4, 
		"Bahamian": 157.1, "Bahraini": 165.2, "Bangladeshi": 150.6, "Barbadian": 158.5, "Belarusian": 166.8, "Belgian": 168.1, "Belizean": undefined, "Beninese": 159.3, "Bermudian": undefined, 
		"Bhutanese": 153.4, "Bissau-Guinean": undefined, "Bolivian": 142.2, "Bosnian": 171.1, "Brazilian": 158.8, "British": 161.9, "Bruneian": undefined, "Bulgarian": 163.2,
		"Burkinabé": 161.6, "Burmese": undefined, "Burundian": 157, "Cambodian": 152.4, "Cameroonian": 161.3, "Canadian": 162.3, "Cape Verdean": undefined, "Catalan": undefined, 
		"Central African": 158.9, "Chadian": 162.6, "Chilean": 157.2, "Chinese": 155.8, "Colombian": 158.7, "Comorian": 154.8, "Congolese": 159, "a Cook Islander": 162.3, "Costa Rican": undefined, 
		"Croatian": 166.3, "Cuban": 156, "Cypriot": 163.4, "Czech": 167.22, "Danish": 168.7, "Djiboutian": undefined, "Dominican": 156.4, "Dominiquais": 157.2, "Dutch": 169, "East Timorese": 149.1, 
		"Ecuadorian": 153.4, "Egyptian": 158.9, "Emirati": 158.9, "Equatoguinean": undefined, "Eritrean": undefined, "Estonian": 165.5, "Ethiopian": 157.6, "Fijian": 161.8, "Filipina": 151.8, 
		"Finnish": 165.3, "French Guianan": 157, "French Polynesian": undefined, "French": 162.5, "Gabonese": 158.4, "Gambian": 157.8, "Georgian": 160.5, "German": 162.8, "Ghanan": 158.5, 
		"Greek": 165, "Greenlandic": undefined, "Grenadian": undefined, "Guamanian": undefined, "Guatemalan": 147.3, "Guinean": 158.8, "Guyanese": 157.9, "Haitian": 158.6, "Honduran": 152.0, "Hungarian": 164, 
		"I-Kiribati": undefined, "Icelandic": 168, "Indian": 151.9, "Indonesian": 147, "Iranian": 157.2, "Iraqi": 155.8, "Irish": 163, "Israeli": 166, "Italian": 162.5, "Ivorian": 158.9, 
		"Jamaican": 160.8, "Japanese": 158, "Jordanian": 158.2, "Kazakh": 159.8, "Kenyan": 159.4, "Kittitian": 156.9, "Korean": 156.15, "Kosovan": 165.7, "Kurdish": undefined, "Kuwaiti": 156.1, 
		"Kyrgyz": 158.6, "Laotian": undefined, "Latvian": 165.7, "Lebanese": 165, "Liberian": 157.3, "Libyan": 160.5, "a Liechtensteiner": 164.3, "Lithuanian": 167.5, "Luxembourgian": 164.8, 
		"Macedonian": undefined, "Malagasy": 154.3, "Malawian": 155, "Malaysian": 154.7, "Maldivian": undefined, "Malian": 160.4, "Maltese": 159.9, "Marshallese": undefined, "Mauritanian": undefined, 
		"Mauritian": undefined, "Mexican": 154, "Micronesian": undefined, "Moldovan": 161.2, "Monégasque": undefined, "Mongolian": 157.7, "Montenegrin": 168.4, "Moroccan": 158.5, "Mosotho": 157.6, 
		"Motswana": undefined, "Mozambican": 156, "Namibian": 160.7, "Nauruan": undefined, "Nepalese": 150.8, "New Caledonian": undefined, "a New Zealander": 164, "Ni-Vanuatu": 149.8, "Nicaraguan": 153.7, "Nigerian": 163.8, 
		"Nigerien": 157.8, "Niuean": 160.4, "Norwegian": 157.8, "Omani": undefined, "Pakistani": 151.9, "Palauan": 161.4, "Palestinian": 158.2, "Panamanian": 152.0, "Papua New Guinean": 150.7, 
		"Paraguayan": 158.3, "Peruvian": 151, "Polish": 165.1, "Portuguese": 165.1, "Puerto Rican": 158.9, "Qatari": 155, "Romanian": 157, "Russian": 164.1, "Rwandan": 157.7, "Sahrawi": undefined, 
		"Saint Lucian": 157.1, "Salvadoran": 160.3, "Sammarinese": undefined, "Samoan": 161, "São Toméan": undefined, "Saudi": 156.3, "Scottish": 163, "Senegalese": 163, "Serbian": 166.8, 
		"Seychellois": 155.8, "Sierra Leonean": undefined, "Singaporean": 160.0, "Slovak": 165.6, "Slovene": 167.4, "a Solomon Islander": 151.8, "Somali": undefined, "South African": 159, 
		"South Sudanese": undefined, "Spanish": 162.6, "Sri Lankan": 151.4, "Sudanese": 180.0, "Surinamese": 155.7, "Swazi": 159.1, "Swedish": 166.8, "Swiss": 162.5, "Syrian": 156.3, "Taiwanese": 160.4, 
		"Tajik": 161.2, "Tanzanian": 156.6, "Thai": 159, "Tibetan": undefined, "Togolese": 159, "Tongan": 159.5, "Trinidadian": 157.4, "Tunisian": 160, "Turkish": 161.9, "Turkmen": 158.2, 
		"Tuvaluan": undefined, "Ugandan": 159.2, "Ukrainian": 164.8, "Uruguayan": 158.0, "Uzbek": 159.9, "Vatican": 162.5, "Venezuelan": 159, "Vietnamese": 155.2, "Vincentian": 156.2, "Yemeni": undefined, 
		"Zairian": 157.7, "Zambian": 158.5, "Zimbabwean": 160.3,
		"": 162.5 // default
	};
	const xyMeanHeight = {
		"Afghan": undefined, "Albanian": 174.0, "Algerian": 172.2, "American.asian": 172.5, "American.black": 177.4, "American.latina": 172.5, "American.white": 178.2, "American": 176.4, 
		"Andorran": undefined, "Angolan": undefined, "Antiguan": 164.8, "Argentinian": 174.46, "Armenian": undefined, "Aruban": 165.1, "Australian": 175.6, "Austrian": 179, "Azerbaijani": 171.8, 
		"Bahamian": 167.2, "Bahraini": 165.1, "Bangladeshi": 150.8, "Barbadian": 169.3, "Belarusian": 176.9, "Belgian": 178.7, "Belizean": undefined, "Beninese": undefined, "Bermudian": undefined, 
		"Bhutanese": 167.2, "Bissau-Guinean": undefined, "Bolivian": 160, "Bosnian": 183.9, "Brazilian": 170.7, "British": 175.3, "Bruneian": undefined, "Bulgarian": 175.2, "Burkinabé": undefined, 
		"Burmese": 168.0, "Burundian": 164.1, "Cambodian": 162.5, "Cameroonian": 170.6, "Canadian": 175.1, "Cape Verdean": undefined, "Catalan": undefined, "Central African": undefined, 
		"Chadian": undefined, "Chilean": 169.6, "Chinese": 167.1, "Colombian": 170.6, "Comorian": undefined, "Congolese": undefined, "a Cook Islander": 173.4, "Costa Rican": undefined, "Croatian": 180.5, 
		"Cuban": 168, "Cypriot": 177.2, "Czech": 180.31, "Danish": 180.4, "Djiboutian": undefined, "Dominican": 168.4, "Dominiquais": 168.1, "Dutch": 181, "East Timorese": 161.2, "Ecuadorian": 167.5, 
		"Egyptian": 170.3, "Emirati": 170.3, "Equatoguinean": undefined, "Eritrean": undefined, "Estonian": 179.1, "Ethiopian": undefined, "Fijian": 170.4, "Filipina": 163.5, "Finnish": 178.9, 
		"French Guianan": 168, "French Polynesian": undefined, "French": 175.6, "Gabonese": undefined, "Gambian": 168, "Georgian": 172.7, "German": 175.4, "Ghanan": 169.5, "Greek": 177, "Greenlandic": undefined, 
		"Grenadian": undefined, "Guamanian": undefined, "Guatemalan": 157.5, "Guinean": undefined, "Guyanese": 168.6, "Haitian": undefined, "Honduran": undefined, "Hungarian": 176, "I-Kiribati": undefined, "Icelandic": 181, 
		"Indian": 164.7, "Indonesian": 158, "Iranian": 170.3, "Iraqi": 165.4, "Irish": 177, "Israeli": 177, "Italian": 176.5, "Ivorian": 170.1, "Jamaican": 171.8, "Japanese": 172, "Jordanian": undefined, 
		"Kazakh": 169, "Kenyan": 170.0, "Kittitian": 164.4, "Korean": 168.15, "Kosovan": 179.5, "Kurdish": undefined, "Kuwaiti": 169.2, "Kyrgyz": 170.4, "Laotian": 160.5, "Latvian": 174.2, "Lebanese": 176, 
		"Liberian": undefined, "Libyan": 171.3, "a Liechtensteiner": 175.4, "Lithuanian": 177.2, "Luxembourgian": 179.9, "Macedonian": undefined, "Malagasy": 161.5, "Malawian": 166, "Malaysian": 166.3, 
		"Maldivian": undefined, "Malian": 171.3, "Maltese": 169.9, "Marshallese": undefined, "Mauritanian": undefined, "Mauritian": undefined, "Mexican": 167, "Micronesian": undefined, "Moldovan": undefined, 
		"Monégasque": undefined, "Mongolian": 168.4, "Montenegrin": 183.2, "Moroccan": 172.7, "Mosotho": undefined, "Motswana": undefined, "Mozambican": undefined, "Namibian": undefined, "Nauruan": undefined, 
		"Nepalese": 163, "New Caledonian": undefined, "a New Zealander": 177, "Ni-Vanuatu": 160.5, "Nicaraguan": undefined, "Nigerian": 163.8, "Nigerien": 163.8, "Niuean": 169.7, "Norwegian": 179.63, "Omani": undefined, "Pakistani": 164.7, 
		"Palauan": 174.6, "Palestinian": 169.7, "Panamanian": 165.0, "Papua New Guinean": 163.5, "Paraguayan": 168.8, "Peruvian": 164, "Polish": 178.7, "Portuguese": 173.9, "Puerto Rican": 172.5, "Qatari": 166.2, 
		"Romanian": 172, "Russian": 177.2, "Rwandan": undefined, "Sahrawi": undefined, "Saint Lucian": 168.3, "Salvadoran": undefined, "Sammarinese": undefined, "Samoan": 173, "São Toméan": undefined, 
		"Saudi": 168.9, "Scottish": 177.6, "Senegalese": undefined, "Serbian": 182, "Seychellois": 168.5, "Sierra Leonean": undefined, "Singaporean": 170.6, "Slovak": 179.4, "Slovene": 180.3, 
		"a Solomon Islander": 163.1, "Somali": undefined, "South African": 168, "South Sudanese": undefined, "Spanish": 173.1, "Sri Lankan": 163.6, "Sudanese": 190.0, "Surinamese": 165.2, "Swazi": undefined, 
		"Swedish": 181.5, "Swiss": 178.2, "Syrian": 173.0, "Taiwanese": 171.4, "Tajik": 175.2, "Tanzanian": undefined, "Thai": 170.3, "Tibetan": undefined, "Togolese": undefined, "Tongan": 168.2, 
		"Trinidadian": 170.8, "Tunisian": 172.3, "Turkish": 173.6, "Turkmen": 171.4, "Tuvaluan": undefined, "Ugandan": undefined, "Ukrainian": 176.5, "Uruguayan": 170.0, "Uzbek": 175.4, "Vatican": 176.5, 
		"Venezuelan": 169, "Vietnamese": 165.7, "Vincentian": 165.4, "Yemeni": 159.9, "Zairian": 158.9, "Zambian": undefined, "Zimbabwean": undefined,
		"": 172.5 // 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 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, midHeight = 0, 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': // Kinefelter 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': // Kinefelter 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)
	var mean = 0;
	var minMult = -3.0;
	var maxMult = 3.0;
	var skew = 0.0;
	var spread = 45;
	var minIntelligence = -101;
	var 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)];
};

if(!Array.prototype.findIndex) {
	Array.prototype.findIndex = function(predicate) {
		if (this == null) {
			throw new TypeError('Array.prototype.find called on null or undefined');
		}
		if (typeof predicate !== 'function') {
			throw new TypeError('predicate must be a function');
		}
		var list = Object(this);
		var length = list.length >>> 0;
		var thisArg = arguments[1];
		var value;

		for (var i = 0; i < length; i++) {
			value = list[i];
			if (predicate.call(thisArg, value, i, list)) {
				return i;
			}
		}
		return -1;
	};
}

/*
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) {
	var result = def;
	if(typeof val === 'number' && !isNaN(val)) {
		var 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.commaNum = function(s) {
	if(!s) { return 0; }
	if(State.variables.formatNumbers != 1) { return s; }
	return s.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};

window.cashFormat = function(s) {
	if(!s) { s = 0; }
	return "¤"+commaNum(s);
};

window.isFloat = function(n){
	return n === +n && n !== (n|0);
};

window.isInt = function(n) {
	return n === +n && n === (n|0);
};

window.numberWithCommas = function(x) {
	return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};

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

window.jsRandomMany = function (arr, count) {
	var result = [];
	var _tmp = arr.slice();
	for (var i = 0; i < count; i++) {
		var 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) {
	var index = Math.floor(Math.random() * choices.length);
	return choices[index];
};

//This function is alternative to clone - usage needed if nested objects present. Slower but result is separate object tree, not with reference to source object.
window.deepCopy = function (o) {
	var output, v, key;
	output = Array.isArray(o) ? [] : {};
	for (key in o) {
		v = o[key];
		output[key] = (typeof v === "object") ? deepCopy(v) : v;
	}
	return output;
};

/*
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
// if two are generated the same second every second,
// duplicates appear approximately once every 100 trillion years
// this is ~7000 times the age of the universe
window.generateNewID = function generateNewID() {
	let randint = "";
	for(let i = 0; i < 12; i++)
		randint += jsEither("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
	const timestamp = Date.now();
	return (randint + timestamp.toString(36));
};

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(passage_function) {
	$(document).one(':passagedisplay', ev => {
		const element = document.createElement("div");
		element.classList.add("passage");
		document.getElementById("passages").appendChild(element);
		passage_function(element);
		$(document).off(':passagedisplay');
	});
};

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

window.getSlaveDisplayName = function (slave) {
	if ((!slave) || (!State))
		return undefined;
	var surnamesFirstCountries = [
		"Cambodian",
		"Chinese",
		"Hungarian",
		"Japanese",
		"Korean",
		"Mongolian",
		"Taiwanese",
		"Vietnamese"];
		var names = [slave.slaveName, slave.slaveSurname || ""];
	if ((1 !== State.variables.surnameOrder) && (surnamesFirstCountries.includes(slave.nationality)))
		names.reverse();
	return names.join(" ").trim();
};

window.getSlaveDevotionClass = 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 = 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) {
	return Math.round(s/2.54) + (Math.round(s/2.54) === 1?" inch":" 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) {
	return (s<9?s*5:(s===9?50: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 centimetres 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 centimetres 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";
};

/* decoration should be passed as "facilityDecoration" in quotes. For example, ValidateFacilityDecoration("brothelDecoration"). The quotes are important, do not pass it as a story variable. */
window.ValidateFacilityDecoration = function ValidateFacilityDecoration(decoration) {
	const V = State.variables;
	switch (V[decoration]) {
		case "standard":
			/*nothing to do */
			break;
		case "Supremacist":
			if (!Number.isFinite(V.arcologies[0].FSSupremacist)) {
				V[decoration] = "standard";
			}
			break;
		case "Subjugationist":
			if (!Number.isFinite(V.arcologies[0].FSSubjugationist)) {
				V[decoration] = "standard";
			}
			break;
		case "Gender Radicalist":
			if (!Number.isFinite(V.arcologies[0].FSGenderRadicalist)) {
				V[decoration] = "standard";
			}
			break;
		case "Gender Fundamentalist":
			if (!Number.isFinite(V.arcologies[0].FSGenderFundamentalist)) {
				V[decoration] = "standard";
			}
			break;
		case "Paternalist":
			if (!Number.isFinite(V.arcologies[0].FSPaternalist)) {
				V[decoration] = "standard";
			}
			break;
		case "Body Purist":
			if (!Number.isFinite(V.arcologies[0].FSBodyPurist)) {
				V[decoration] = "standard";
			}
			break;
		case "Transformation Fetishist":
			if (!Number.isFinite(V.arcologies[0].FSTransformationFetishist)) {
				V[decoration] = "standard";
			}
			break;
		case "Youth Preferentialist":
			if (!Number.isFinite(V.arcologies[0].FSYouthPreferentialist)) {
				V[decoration] = "standard";
			}
			break;
		case "Maturity Preferentialist":
			if (!Number.isFinite(V.arcologies[0].FSMaturityPreferentialist)) {
				V[decoration] = "standard";
			}
			break;
		case "Slimness Enthusiast":
			if (!Number.isFinite(V.arcologies[0].FSSlimnessEnthusiast)) {
				V[decoration] = "standard";
			}
			break;
		case "Asset Expansionist":
			if (!Number.isFinite(V.arcologies[0].FSAssetExpansionist)) {
				V[decoration] = "standard";
			}
			break;
		case "Pastoralist":
			if (!Number.isFinite(V.arcologies[0].FSPastoralist)) {
				V[decoration] = "standard";
			}
			break;
		case "Physical Idealist":
			if (!Number.isFinite(V.arcologies[0].FSPhysicalIdealist)) {
				V[decoration] = "standard";
			}
			break;
		case "Chattel Religionist":
			if (!Number.isFinite(V.arcologies[0].FSChattelReligionist)) {
				V[decoration] = "standard";
			}
			break;
		case "Degradationist":
			if (!Number.isFinite(V.arcologies[0].FSDegradationist)) {
				V[decoration] = "standard";
			}
			break;
		case "Roman Revivalist":
			if (!Number.isFinite(V.arcologies[0].FSRomanRevivalist)) {
				V[decoration] = "standard";
			}
			break;
		case "Egyptian Revivalist":
			if (!Number.isFinite(V.arcologies[0].FSEgyptianRevivalist)) {
				V[decoration] = "standard";
			}
			break;
		case "Edo Revivalist":
			if (!Number.isFinite(V.arcologies[0].FSEdoRevivalist)) {
				V[decoration] = "standard";
			}
			break;
		case "Arabian Revivalist":
			if (!Number.isFinite(V.arcologies[0].FSArabianRevivalist)) {
				V[decoration] = "standard";
			}
			break;
		case "Chinese Revivalist":
			if (!Number.isFinite(V.arcologies[0].FSChineseRevivalist)) {
				V[decoration] = "standard";
			}
			break;
		case "Repopulation Focus":
			if (!Number.isFinite(V.arcologies[0].FSRepopulationFocus)) {
				V[decoration] = "standard";
			}
			break;
		case "Eugenics":
			if (!Number.isFinite(V.arcologies[0].FSRestart)) {
				V[decoration] = "standard";
			}
			break;
		case "Hedonistic":
			if (!Number.isFinite(V.arcologies[0].FSHedonisticDecadence)) {
				V[decoration] = "standard";
			}
			break;
	}
};

window.FSChange = function FSChange(FS, magnitude, bonus_multiplier) {
	"use strict";
	const V = State.variables;
	let errorMessage = "";

	switch (FS) {
		case "Supremacist":
			if (Number.isFinite(V.arcologies[0].FSSupremacist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSSupremacist / V.FSLockinLevel);
				V.arcologies[0].FSSupremacist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "Subjugationist":
			if (Number.isFinite(V.arcologies[0].FSSubjugationist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSSubjugationist / V.FSLockinLevel);
				V.arcologies[0].FSSubjugationist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "GenderRadicalist":
			if (Number.isFinite(V.arcologies[0].FSGenderRadicalist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSGenderRadicalist / V.FSLockinLevel);
				V.arcologies[0].FSGenderRadicalist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "GenderFundamentalist":
			if (Number.isFinite(V.arcologies[0].FSGenderFundamentalist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSGenderFundamentalist / V.FSLockinLevel);
				V.arcologies[0].FSGenderFundamentalist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "Paternalist":
			if (Number.isFinite(V.arcologies[0].FSPaternalist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSPaternalist / V.FSLockinLevel);
				V.arcologies[0].FSPaternalist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "Degradationist":
			if (Number.isFinite(V.arcologies[0].FSDegradationist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSDegradationist / V.FSLockinLevel);
				V.arcologies[0].FSDegradationist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "AssetExpansionist":
			if (Number.isFinite(V.arcologies[0].FSAssetExpansionist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSAssetExpansionist / V.FSLockinLevel);
				V.arcologies[0].FSAssetExpansionist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "SlimnessEnthusiast":
			if (Number.isFinite(V.arcologies[0].FSSlimnessEnthusiast)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSSlimnessEnthusiast / V.FSLockinLevel);
				V.arcologies[0].FSSlimnessEnthusiast += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "TransformationFetishist":
			if (Number.isFinite(V.arcologies[0].FSTransformationFetishist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSTransformationFetishist / V.FSLockinLevel);
				V.arcologies[0].FSTransformationFetishist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "BodyPurist":
			if (Number.isFinite(V.arcologies[0].FSBodyPurist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSBodyPurist / V.FSLockinLevel);
				V.arcologies[0].FSBodyPurist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "MaturityPreferentialist":
			if (Number.isFinite(V.arcologies[0].FSMaturityPreferentialist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSMaturityPreferentialist / V.FSLockinLevel);
				V.arcologies[0].FSMaturityPreferentialist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "YouthPreferentialist":
			if (Number.isFinite(V.arcologies[0].FSYouthPreferentialist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSYouthPreferentialist / V.FSLockinLevel);
				V.arcologies[0].FSYouthPreferentialist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "Pastoralist":
			if (Number.isFinite(V.arcologies[0].FSPastoralist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSPastoralist / V.FSLockinLevel);
				V.arcologies[0].FSPastoralist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "PhysicalIdealist":
			if (Number.isFinite(V.arcologies[0].FSPhysicalIdealist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSPhysicalIdealist / V.FSLockinLevel);
				V.arcologies[0].FSPhysicalIdealist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "ChattelReligionist":
			if (Number.isFinite(V.arcologies[0].FSChattelReligionist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSChattelReligionist / V.FSLockinLevel);
				V.arcologies[0].FSChattelReligionist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "RomanRevivalist":
			if (Number.isFinite(V.arcologies[0].FSRomanRevivalist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSRomanRevivalist / V.FSLockinLevel);
				V.arcologies[0].FSRomanRevivalist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "AztecRevivalist":
			if (Number.isFinite(V.activeArcology.FSAztecRevivalist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSAztecRevivalist / V.FSLockinLevel);
				V.arcologies[0].FSAztecRevivalist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "EgyptianRevivalist":
			if (Number.isFinite(V.arcologies[0].FSEgyptianRevivalist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSEgyptianRevivalist / V.FSLockinLevel);
				V.arcologies[0].FSEgyptianRevivalist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "EdoRevivalist":
			if (Number.isFinite(V.arcologies[0].FSEdoRevivalist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSEdoRevivalist / V.FSLockinLevel);
				V.arcologies[0].FSEdoRevivalist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "ArabianRevivalist":
			if (Number.isFinite(V.arcologies[0].FSArabianRevivalist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSArabianRevivalist / V.FSLockinLevel);
				V.arcologies[0].FSArabianRevivalist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "ChineseRevivalist":
			if (Number.isFinite(V.arcologies[0].FSChineseRevivalist)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSChineseRevivalist / V.FSLockinLevel);
				V.arcologies[0].FSChineseRevivalist += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "Repopulationist":
			if (Number.isFinite(V.arcologies[0].FSRepopulationFocus)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSRepopulationFocus / V.FSLockinLevel);
				V.arcologies[0].FSRepopulationFocus += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "Eugenics":
			if (Number.isFinite(V.arcologies[0].FSRestart)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSRestart / V.FSLockinLevel);
				V.arcologies[0].FSRestart += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		case "Hedonism":
			if (Number.isFinite(V.arcologies[0].FSHedonisticDecadence)) {
				V.rep += magnitude * V.FSSingleSlaveRep * (V.arcologies[0].FSHedonisticDecadence / V.FSLockinLevel);
				V.arcologies[0].FSHedonisticDecadence += 0.05 * magnitude * V.FSSingleSlaveRep * (bonus_multiplier || 1);
			}
			break;
		default:
			errorMessage += "<span class='red'>ERROR: bad FS reference</span>";
	}
	return errorMessage;
};

/* Currently unused, widget version routes directly through FSChange() */
window.FSChangePorn = function FSChangePorn(FS, magnitude) {
	return FSChange(FS, magnitude, State.variables.pornFameBonus);
};

window.ordinalSuffix = function ordinalSuffix(i) {
	var j = i % 10,
		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";
}