code:
App.FindSlave = {};

/**
 * Fragment searching: See if every needle can found somewhere in the field of haystacks
 * @param {string[]} haystacks
 * @param {RegExp[]} needles
 * @returns {boolean}
 */
App.FindSlave._fragmentSearch = function(haystacks, needles) {
	const hs = haystacks.join(" ");
	return needles.every((needle) => { return needle.test(hs); });
};

/**
 * Get slave ids which match a predicate
 * @param {function(App.Entity.SlaveState): boolean} predicate
 * @returns {number[]}
 */
App.FindSlave._slaveIDs = function(predicate) {
	return V.slaves.reduce((acc, slave) => {
		if (predicate(createReadonlyProxy(slave))) {
			acc.push(slave.ID);
		}
		return acc;
	}, []);
};

/**
 * Display a list of results, or text indicating that there were none
 * @param {number[]} ids
 * @param {DocumentFragment} frag
 */
App.FindSlave._appendResultList = function(ids, frag) {
	if (ids.length === 0) {
		App.UI.DOM.appendNewElement("p", frag, "No matching slaves.");
	} else {
		frag.appendChild(App.UI.SlaveList.render(ids, [], App.UI.SlaveList.SlaveInteract.stdInteract));
	}
};

/**
 * Generate a slave list as the result of fragment searching all the name-type fields
 * @param {string} query
 * @returns {DocumentFragment}
 */
App.FindSlave.searchByName = function(query) {
	const frag = document.createDocumentFragment();
	if (query) {
		const resultTitle = App.UI.DOM.appendNewElement("p", frag, "Query results for name: ");
		App.UI.DOM.appendNewElement("code", resultTitle, query);
		const needles = query.split(" ").map((needle) => { return new RegExp(needle, "i"); });
		const ids = this._slaveIDs((slave) => { return this._fragmentSearch([slave.slaveName, slave.slaveSurname, slave.birthName, slave.birthSurname], needles); });
		this._appendResultList(ids, frag);
	}
	return frag;
};

/**
 * Generate a slave list as the result of fragment searching profession and origin
 * @param {string} query
 * @returns {DocumentFragment}
 */
App.FindSlave.searchByBackground = function(query) {
	const frag = document.createDocumentFragment();
	if (query) {
		const resultTitle = App.UI.DOM.appendNewElement("p", frag, "Query results for background: ");
		App.UI.DOM.appendNewElement("code", resultTitle, query);
		const needles = query.split(" ").map((needle) => { return new RegExp(needle, "i"); });
		const ids = this._slaveIDs((slave) => { return this._fragmentSearch([slave.career, slave.origin], needles); });
		this._appendResultList(ids, frag);
	}
	return frag;
};

/**
 * Generate a slave list as the result of evaluating an expression
 * @param {string} query
 * @returns {DocumentFragment}
 */
App.FindSlave.searchByExpression = function(query) {
	const frag = document.createDocumentFragment();
	if (query) {
		const resultTitle = App.UI.DOM.appendNewElement("p", frag, "Query results from expression: ");
		App.UI.DOM.appendNewElement("code", resultTitle, query);
		const pred = new Function("slave", "return (" + query + ");");
		const ids = runWithReadonlyProxy(() => { return this._slaveIDs(pred); });
		this._appendResultList(ids, frag);
	}
	return frag;
};


code:
globalThis.FutureSocieties = (function() {
	"use strict";
	const FSString2Property = { // blame Hedonism and Eugenics for this - TODO: probably can be cleaned up, maybe eliminated
		Supremacist: "FSSupremacist",
		Subjugationist: "FSSubjugationist",
		GenderRadicalist: "FSGenderRadicalist",
		GenderFundamentalist: "FSGenderFundamentalist",
		Degradationist: "FSDegradationist",
		Paternalist: "FSPaternalist",
		BodyPurist: "FSBodyPurist",
		TransformationFetishist: "FSTransformationFetishist",
		YouthPreferentialist: "FSYouthPreferentialist",
		MaturityPreferentialist: "FSMaturityPreferentialist",
		SlimnessEnthusiast: "FSSlimnessEnthusiast",
		AssetExpansionist: "FSAssetExpansionist",
		Pastoralist: "FSPastoralist",
		PhysicalIdealist: "FSPhysicalIdealist",
		Hedonistic: "FSHedonisticDecadence",
		Hedonism: "FSHedonisticDecadence",
		ChattelReligionist: "FSChattelReligionist",
		Multiculturalist: "FSNull",
		RomanRevivalist: "FSRomanRevivalist",
		EgyptianRevivalist: "FSEgyptianRevivalist",
		EdoRevivalist: "FSEdoRevivalist",
		ArabianRevivalist: "FSArabianRevivalist",
		ChineseRevivalist: "FSChineseRevivalist",
		AztecRevivalist: "FSAztecRevivalist",
		RepopulationFocus: "FSRepopulationFocus",
		Repopulationist: "FSRepopulationFocus",
		Eugenics: "FSRestart",
		IntellectualDependency: "FSIntellectualDependency",
		SlaveProfessionalism: "FSSlaveProfessionalism",
		PetiteAdmiration: "FSPetiteAdmiration",
		StatuesqueGlorification: "FSStatuesqueGlorification"
	};
	const SocietyList = [...new Set(Object.keys(FSString2Property).map(key => FSString2Property[key]))]; // This returns an array containing the unique values of FSString2Property. E.g. "FSSupremacist" and "FSSubjugationist"
	const NPCSocietyList = [ "FSCummunism", "FSIncestFetishist" ]; // NPC arcologies may use these FSes, but the PC can't

	const FSMutexGroups = [
		[ "FSSupremacist" ],
		[ "FSSubjugationist" ],
		[ "FSGenderRadicalist", "FSGenderFundamentalist" ],
		[ "FSDegradationist", "FSPaternalist" ],
		[ "FSBodyPurist", "FSTransformationFetishist" ],
		[ "FSYouthPreferentialist", "FSMaturityPreferentialist" ],
		[ "FSSlimnessEnthusiast", "FSAssetExpansionist" ],
		[ "FSPastoralist", "FSCummunism" ],
		[ "FSPhysicalIdealist", "FSHedonisticDecadence" ],
		[ "FSChattelReligionist", "FSNull" ],
		[ "FSIncestFetishist" ],
		[ "FSRomanRevivalist", "FSEgyptianRevivalist", "FSEdoRevivalist", "FSArabianRevivalist", "FSChineseRevivalist", "FSAztecRevivalist" ],
		[ "FSRepopulationFocus", "FSRestart" ],
		[ "FSIntellectualDependency", "FSSlaveProfessionalism" ],
		[ "FSPetiteAdmiration", "FSStatuesqueGlorification"]
	];

	const DisplayName = {
		FSSupremacist: {noun: "Racial Supremacy", adj: "Supremacist"},
		FSSubjugationist: {noun: "Racial Subjugationism", adj: "Subjugationist"},
		FSGenderRadicalist: {noun: "Gender Radicalism", adj: "Gender Radicalist"},
		FSGenderFundamentalist: {noun: "Gender Fundamentalism", adj: "Gender Fundamentalist"},
		FSDegradationist: {noun: "Degradationism", adj: "Degradationist"},
		FSPaternalist: {noun: "Paternalism", adj: "Paternalist"},
		FSBodyPurist: {noun: "Body Purism", adj: "Body Purist"},
		FSTransformationFetishist: {noun: "Transformation Fetishism", adj: "Transformation Fetishist"},
		FSYouthPreferentialist: {noun: "Youth Preferentialism", adj: "Youth Preferentialist"},
		FSMaturityPreferentialist: {noun: "Maturity Preferentialism", adj: "Maturity Preferentialist"},
		FSSlimnessEnthusiast: {noun: "Slimness Enthusiasm", adj: "Slimness Enthusiast"},
		FSAssetExpansionist: {noun: "Asset Expansionism", adj: "Asset Expansionist"},
		FSPastoralist: {noun: "Pastoralism", adj: "Pastoralist"},
		FSCummunism: {noun: "Cummunism", adj: "Cummunist"},
		FSPhysicalIdealist: {noun: "Physical Idealism", adj: "Physical Idealist"},
		FSHedonisticDecadence: {noun: "Decadent Hedonism", adj: "Decadent Hedonist"},
		FSChattelReligionist: {noun: "Chattel Religionism", adj: "Chattel Religionist"},
		FSNull: {noun: "Multiculturalism", adj: "Multiculturalist"},
		FSIncestFetishist: {noun: "Incest Fetishism", adj: "Incest Fetishist"},
		FSRomanRevivalist: {noun: "Roman Revivalism", adj: "Roman Revivalist"},
		FSEgyptianRevivalist: {noun: "Egyptian Revivalism", adj: "Egyptian Revivalist"},
		FSEdoRevivalist: {noun: "Edo Revivalism", adj: "Edo Revivalist"},
		FSArabianRevivalist: {noun: "Arabian Revivalism", adj: "Arabian Revivalist"},
		FSChineseRevivalist: {noun: "Chinese Revivalism", adj: "Chinese Revivalist"},
		FSAztecRevivalist: {noun: "Aztec Revivalism", adj: "Aztec Revivalist"},
		FSRepopulationFocus: {noun: "Repopulation Focus", adj: "Repopulationist"},
		FSRestart: {noun: "Eugenics", adj: "Eugenics"},
		FSIntellectualDependency: {noun: "Intellectual Dependency", adj: "Intellectual Dependency"},
		FSSlaveProfessionalism: {noun: "Slave Professionalism", adj: "Slave Professional"},
		FSPetiteAdmiration: {noun: "Petite Admiration", adj: "Petite Admiration"},
		FSStatuesqueGlorification: {noun: "Statuesque Glorification", adj: "Statuesque Glorification"}
	};

	return {
		activeFSes: activeFSes,
		activeCount: activeCount,
		applyBroadProgress: applyBroadProgress,
		availCredits: calcFSCredits,
		influenceSources: influenceSources,
		diplomaticFSes: diplomaticFSes,
		displayName: displayName,
		displayAdj: displayAdj,
		decay: decayFSes,
		overflowToInfluence: overflowToInfluence,
		remove: removeFS,
		DecorationCleanup: DecorationCleanup,
		DecorationBonus: FSDecorationBonus,
		Change: FSChange,
		ChangePorn: FSChangePorn,
		HighestDecoration: FSHighestDecoration
	};

	/** get the list of FSes active for a particular arcology
	 * helper function, not callable externally
	 * @param {number} arcologyID
	 * @returns {Array<string>}
	 */
	function activeFSes(arcologyID) {
		let isSet = (fs) => V.arcologies[arcologyID][fs] !== "unset";
		const npcFSes = arcologyID !== 0 ? NPCSocietyList.filter(isSet) : [];
		return SocietyList.filter(isSet).concat(npcFSes);
	}

	/** call as FutureSocieties.activeCount(arcologyID)
	 * @param {number} arcologyID
	 * @returns {number}
	 */
	function activeCount(arcologyID) {
		return activeFSes(arcologyID).length;
	}

	/** call as FutureSocieties.applyBroadProgress(arcologyID, progress)
	 * @param {number} arcologyID
	 * @param {number} progress
	 */
	function applyBroadProgress(arcologyID, progress) {
		for (const fs of activeFSes(arcologyID)) {
			if (fs !== "FSNull") { // does not progress this way
				V.arcologies[arcologyID][fs] += progress;
			}
		}
	}

	/** converts excess progress into influence bonus
	 * call as FutureSocieties.overflowToInfluence(arcologyID)
	 * @param {number} arcologyID
	 */
	function overflowToInfluence(arcologyID) {
		const arcology = V.arcologies[arcologyID];
		for (const fs of activeFSes(arcologyID)) {
			if (fs !== "FSNull") { // no conventional progress
				if (arcology[fs] > V.FSLockinLevel) {
					arcology.influenceBonus += arcology[fs] - V.FSLockinLevel;
					arcology[fs] = V.FSLockinLevel;
				}
			}
		}
	}

	/** returns an array of FSes which the arcology has developed enough to influence others
	 * call as FutureSocieties.influenceSources(arcologyID)
	 * @param {number} arcologyID
	 */
	function influenceSources(arcologyID) {
		let fses = [];
		const arcology = V.arcologies[arcologyID];
		for (const fs of activeFSes(arcologyID)) {
			if (fs !== "FSNull") { // no influence from Multiculturalism?
				if (arcology[fs] > 60) {
					fses.push(fs);
				}
			}
		}
		return fses;
	}

	/** determines whether two named FSes are naturally conflicting or not
	 * @param {string} left FS
	 * @param {string} right FS
	 * @returns {boolean}
	 */
	function conflictingFSes(left, right) {
		if (left !== right) { // identical FSes are not opposed
			for (const group of FSMutexGroups) {
				if (group.includesAll(left, right)) {
					return true; // but any other FS in the mutex group is
				}
			}
		}
		return false;
	}

	/** returns the set of shared FSes between two arcologies, and the set of conflicts between pairs of FSes between the arcologies
	 * relatively expensive, try not to call frequently
	 * call as FutureSocieties.diplomaticFSes(arc1ID, arc2ID)
	 * @param {number} arc1ID
	 * @param {number} arc2ID
	 * @returns {{shared: string[], conflicting: string[][]}}
	 */
	function diplomaticFSes(arc1ID, arc2ID) {
		let shared = [], conflicting = [];
		const arc1FSes = activeFSes(arc1ID);
		const arc2FSes = activeFSes(arc2ID);
		// find ordinary shared and conflicting FSes
		for (const fs1 of arc1FSes) {
			for (const fs2 of arc2FSes) {
				if (fs1 === fs2) {
					shared.push(fs1);
				} else if (conflictingFSes(fs1, fs2)) {
					conflicting.push([fs1, fs2]);
				}
			}
		}
		// special cases: racial FSes might be conflicting even when shared
		const arc1 = V.arcologies[arc1ID];
		const arc2 = V.arcologies[arc2ID];
		if (shared.contains("FSSupremacist")) {
			// a different race is supreme
			if (arc1.FSSupremacistRace !== arc2.FSSupremacistRace) {
				shared.delete("FSSupremacist");
				conflicting.push(["FSSupremacist", "FSSupremacist"]);
			}
			// subjugating the supreme race
			if (arc2FSes.contains("FSSubjugationist") && arc2.FSSubjugationistRace === arc1.FSSupremacistRace) {
				shared.delete("FSSupremacist");
				conflicting.push(["FSSupremacist", "FSSubjugationist"]);
			}
		}
		if (shared.contains("FSSubjugationist")) {
			// subjugating a different race
			if (arc1.FSSubjugationistRace !== arc2.FSSubjugationistRace) {
				shared.delete("FSSubjugationist");
				conflicting.push(["FSSubjugationist", "FSSubjugationist"]);
			}
			// believe the subjugated race is supreme
			if (arc2FSes.contains("FSSupremacist") && arc2.FSSupremacistRace === arc1.FSSubjugationistRace) {
				shared.delete("FSSubjugationist");
				conflicting.push(["FSSubjugationist", "FSSupremacist"]);
			}
		}
		return {shared, conflicting};
	}

	/** returns the future society display name (typically an "ism") for the given property
	 * @param {string} FSProp
	 * @returns {string}
	 */
	function displayName(FSProp) {
		return DisplayName[FSProp].noun;
	}

	/** returns the future society adjective (typically an "ist") for the given property
	 * @param {string} FSProp
	 * @returns {string}
	 */
	function displayAdj(FSProp) {
		return DisplayName[FSProp].adj;
	}

	/** decays all the FSes adopted by a particular arcology (for example, because of government instability)
	 * call as FutureSocieties.decay(arcologyID)
	 * @param {number} arcologyID
	 * @returns {Array<string>} FSes which purged completely
	 */
	function decayFSes(arcologyID) {
		const fses = activeFSes(arcologyID);
		const arc = V.arcologies[arcologyID];
		let purged = [];
		for (const fs of fses) {
			if (fs !== "FSNull") { // exempt for some reason?
				if (arc[fs] < jsRandom(10, 150)) {
					purged.push(fs);
				} else {
					arc[fs] -= 10;
				}
			}
		}
		return purged;
	}

	/** Remove an FS and all associated attributes from an arcology
	 * call as FutureSocieties.remove(FS)
	 * @param {string} FS (e.g. "FSPaternalist" or "FSDegradationist")
	 */
	function removeFS(FS) {
		const arcology = V.arcologies[0];
		const FSDecoration = `${FS}Decoration`;
		const FSSMR = `${FS}SMR`;
		let FSLaw = `${FS}Law`;
		if (arcology[FS] === undefined) {
			// eslint-disable-next-line no-console
			console.log(`ERROR: bad FS reference, $arcologies[0].${FS} not defined`);
			return;
		}

		if (FS === "FSSupremacist" || FS === "FSSubjugationist") { FSLaw += "ME"; }
		if (FS !== "FSNull") { arcology[FSDecoration] = 20; }
		arcology[FS] = "unset";
		switch (FS) {
			case "FSPaternalist":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				if (_.get(V, "SecExp.edicts")) {
					V.SecExp.edicts.slaveWatch = 0;
				}
				break;
			case "FSDegradationist":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				if (_.get(V, "SecExp.edicts")) {
					V.SecExp.edicts.defense.liveTargets = 0;
				}
				break;
			case "FSGenderRadicalist":
				arcology.FSGenderRadicalistLawBeauty = 0;
				arcology.FSGenderRadicalistLawFuta = 0;
				break;
			case "FSGenderFundamentalist":
				arcology.FSGenderFundamentalistLawBeauty = 0;
				arcology.FSGenderFundamentalistLawBimbo = 0;
				arcology.FSGenderFundamentalistSMR = 0;
				break;
			case "FSSupremacist":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				if (_.get(V, "SecExp.edicts")) {
					V.SecExp.edicts.defense.noSubhumansInArmy = 0;
				}
				break;
			case "FSTransformationFetishist":
			case "FSAssetExpansionist":
				arcology[FSSMR] = 0;
				break;
			case "FSPhysicalIdealist":
				arcology.FSPhysicalIdealistLaw = 0;
				arcology.FSPhysicalIdealistSMR = 0;
				arcology.FSPhysicalIdealistStrongFat = 0;
				if (_.get(V, "SecExp.edicts")) {
					V.SecExp.edicts.defense.martialSchool = 0;
				}
				break;
			case "FSHedonisticDecadence":
				arcology.FSHedonisticDecadenceLaw = 0;
				arcology.FSHedonisticDecadenceLaw2 = 0;
				arcology.FSHedonisticDecadenceSMR = 0;
				arcology.FSHedonisticDecadenceStrongFat = 0;
				break;
			case "FSChattelReligionist":
				arcology.FSChattelReligionistLaw = 0;
				arcology.FSChattelReligionistSMR = 0;
				arcology.FSChattelReligionistCreed = 0;
				if (_.get(V, "SecExp.edicts")) {
					V.SecExp.edicts.subsidyChurch = 0;
				}
				break;
			case "FSRepopulationFocus":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				V.universalRulesChildrenBecomeBreeders = 0;
				if (_.get(V, "SecExp.edicts")) {
					V.SecExp.edicts.defense.pregExemption = 0;
				}
				break;
			case "FSRestart":
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				if (_.get(V, "SecExp.edicts")) {
					V.SecExp.edicts.defense.eliteOfficers = 0;
				}
				V.propOutcome = 0;
				V.failedElite = 0;
				break;
			case "FSIntellectualDependency":
				arcology.FSIntellectualDependencyLaw = 0;
				arcology.FSIntellectualDependencyLawBeauty = 0;
				arcology.FSIntellectualDependencySMR = 0;
				break;
			case "FSPetiteAdmiration":
				arcology.FSPetiteAdmirationLaw = 0;
				arcology.FSPetiteAdmirationLaw2 = 0;
				arcology.FSPetiteAdmirationSMR = 0;
				break;
			case "FSStatuesqueGlorification":
				arcology.FSStatuesqueGlorificationLaw = 0;
				arcology.FSStatuesqueGlorificationLaw2 = 0;
				arcology.FSStatuesqueGlorificationSMR = 0;
				break;
			case "FSNull":
				break;
			default: // all others have one law and one SMR
				arcology[FSLaw] = 0;
				arcology[FSSMR] = 0;
				break;
		}

		DecorationCleanup();
	}

	/** Calculate the number of FS credits that the player still has available (unspent)
	 * Call as FutureSocieties.availCredits()
	 * @returns {number}
	 */
	function calcFSCredits() {
		const arcology = V.arcologies[0];
		let activeFS = activeCount(0);
		if (arcology.FSNull > 0) { // multiculturalism is accounted for separately
			activeFS -= 1; // already counted once, remove that one and count investments instead
			if (V.FSCreditCount === 4) {
				activeFS += arcology.FSNull / 25;
			} else if (V.FSCreditCount === 6) {
				activeFS += arcology.FSNull / 17;
			} else if (V.FSCreditCount === 7) {
				activeFS += arcology.FSNull / 15;
			} else {
				activeFS += arcology.FSNull / 20;
			}
		}
		return Math.max(Math.trunc(V.FSGotRepCredits - activeFS), 0);
	}

	/* call as FutureSocieties.DecorationCleanup() */
	function DecorationCleanup() {
		ValidateFacilityDecoration("brothelDecoration");
		ValidateFacilityDecoration("dairyDecoration");
		ValidateFacilityDecoration("clubDecoration");
		ValidateFacilityDecoration("servantsQuartersDecoration");
		ValidateFacilityDecoration("schoolroomDecoration");
		ValidateFacilityDecoration("spaDecoration");
		ValidateFacilityDecoration("clinicDecoration");
		ValidateFacilityDecoration("arcadeDecoration");
		ValidateFacilityDecoration("cellblockDecoration");
		ValidateFacilityDecoration("masterSuiteDecoration");
		ValidateFacilityDecoration("nurseryDecoration");
		ValidateFacilityDecoration("farmyardDecoration");
	}

	/* helper function, not callable */
	/* decoration should be passed as "facilityDecoration" in quotes. For example, ValidateFacilityDecoration("brothelDecoration"). The quotes are important, do not pass it as a story variable. */
	function ValidateFacilityDecoration(decoration) {
		const FSString = V[decoration].replace(/ /g, ''); // removes spaces
		const activeFS = FSString2Property[FSString]; // gets the property name

		if (FSString === "standard") {
			// nothing to do
		} else if (activeFS === undefined) {
			// eslint-disable-next-line no-console
			console.log(`Error: $${decoration} is ${V[decoration]}`);
			V[decoration] = "standard";
		} else if (!Number.isFinite(V.arcologies[0][activeFS])) {
			if (V.arcologies[0][activeFS] !== "unset") {
				// eslint-disable-next-line no-console
				console.log(`Error: $arcologies[0].${activeFS} is ${V.arcologies[0][activeFS]}`);
			}
			V[decoration] = "standard";
		}
	}

	/** Apply the decoration bonus for a slave working in a facility to the FS
	 * call as FutureSocieties.DecorationBonus()
	 * @param {string} decoration - not quoted, just pass it straight in
	 * @param {number} magnitude - will be multiplied by V.FSSingleSlaveRep
	 */
	function FSDecorationBonus(decoration, magnitude) {
		if (decoration === "standard") {
			return; // no bonus
		}

		const FSString = decoration.replace(/ /g, ''); // removes spaces
		const FSProp = FSString2Property[FSString]; // gets the property name
		const arc = V.arcologies[0];

		if (FSProp && Number.isFinite(arc[FSProp])) {
			arc[FSProp] = Math.clamp(arc[FSProp] + magnitude * V.FSSingleSlaveRep, 0, 100);
		}
	}

	/* call as FutureSocieties.Change() */
	/* FSString should be in the FSString2Property object above */
	function FSChange(FSString, magnitude, bonusMultiplier = 1) {
		const arcology = V.arcologies[0];
		const activeFS = FSString2Property[FSString];

		if (activeFS === undefined) {
			return "ERROR: bad FS reference";
		} else if (Number.isFinite(arcology[activeFS])) {
			if (magnitude < 0) {
				repX(magnitude * V.FSSingleSlaveRep * (arcology[activeFS] / V.FSLockinLevel) / 3, 'futureSocieties'); // Reducing the reputation impact of slaves that are not adhering to societal ideals properly
			} else {
				repX(magnitude * V.FSSingleSlaveRep * (arcology[activeFS] / V.FSLockinLevel), 'futureSocieties');
			}
			arcology[activeFS] += 0.05 * magnitude * V.FSSingleSlaveRep * bonusMultiplier;
		}
	}

	/* call as FutureSocieties.ChangePorn() */
	/* FSString should be in the FSString2Property object above */
	function FSChangePorn(FSString, magnitude) {
		return FSChange(FSString, magnitude, V.pornFameBonus);
	}

	/**
	 * Returns the highest decoration level of active future societies, call as FutureSocieties.HighestDecoration()
	 * @returns {number}
	 */
	function FSHighestDecoration() {
		const arcology = V.arcologies[0];
		const decorationList = SocietyList.map(FS => `${FS}Decoration`);
		let level = 20; // All decorations start at 20

		for (let i = 0; i < decorationList.length; i++) {
			if (arcology[decorationList[i]] > level) {
				level = arcology[decorationList[i]];
			}
		}
		return level;
	}
})();

repo_name: NovX/fc

license: mit


code:
/*
* <<htag>> macro
* A simple macro which allows to create wrapping html elements with dynamic IDs.
*	idea blatantly robbed from the spanMacroJS.tw but expanded to a more generic
*	case, allowing <div>, <button> or whatever you want. elements, default is for
*	the div though. In addition, you can pass an object in as the first argument
*	instead of an id, and each of the object's attributes will become attributes
*	of the generate tag.
*
* Usage: <<htag id>>...<</htag>>
* Usage: <<htag id tag>>...<</htag>>
* Usage: <<htag attributes>>...<</htag>>
* Usage: <<htag attributes tag>>...<</htag>>
*/
Macro.add('htag', {
	tags: null,
	handler() {
		const payload = this.payload[0].contents.replace(/(^\n+|\n+$)/, '');
		let htag = 'div';
		let attributes;

		function munge(val, key) {
			return `${key}="${val}"`;
		}

		if (this.args.length === 0) { return this.error('invalid syntax, format: <<htag [id [ tag ] | attributes [ tag ] >>'); }
		if (this.args.length > 1) { htag = String(this.args[1]).trim(); }
		if (typeof this.args[0] === "object") {
			attributes = $.map(this.args[0], munge).join(" ");
		} else {
			attributes = `id="${String(this.args[0]).trim()}"`;
		}
		if (Config.debug) {
			this.debugView.modes({
				block: true
			});
		}

		jQuery(`<${htag} ${attributes} />`)
			.wiki(payload)
			.appendTo(this.output);
	}
});


code:
/**
 * checks for illness and reduces the effectiveness of the slave accordingly -- 10, 25, 50, 85, 95% reduced
 * also checks for tiredness and reduces effectiveness from that as well -- 25% reduction at 90+ and 10% at 60-90
 * @param {App.Entity.SlaveState} slave
 * @returns {number}
 */
globalThis.healthPenalty = function(slave) {
	const H = slave.health;
	let penalty = 100;
	if (slave.assignment !== Job.GLORYHOLE) {
		if (H.illness > 0) {
			penalty *= (100 - Math.min((Math.pow(H.illness, 2) * 5 + 5), 95)) / 100;
		}
		if (H.tired > 90) {
			penalty *= 0.75;
		} else if (H.tired > 60) {
			penalty *= 0.90;
		}
	}
	penalty = Math.trunc(penalty) / 100;
	return penalty;
};

/**
 * All things hurting a slave go through this to update short term damage and slave health.
 * @param {App.Entity.SlaveState} slave
 * @param {number} damage
 * @returns {void}
 */
globalThis.healthDamage = function(slave, damage) {
	if (!_.isFinite(damage)) {
		throw "Health damage must be a finite number.";
	}
	const H = slave.health;
	damage = Math.max(Math.trunc(damage), 0);
	H.shortDamage += damage;
	H.condition -= damage;
	updateHealth(slave);
};

/**
 * All things directly curing wounds on a slave go through this to update short term damage and slave health.
 * Use sparingly and for direct medical treatment by qualified professionals only; improveCondition should be used instead for drugs, natural healing, etc.
 * @param {App.Entity.SlaveState} slave
 * @param {number} cure
 * @returns {void}
 */
globalThis.healthCure = function(slave, cure) {
	if (!_.isFinite(cure)) {
		throw "Health cure must be a finite number.";
	}
	const H = slave.health;
	cure = Math.max(Math.trunc(cure), 0);
	if (cure > H.shortDamage) {
		cure = H.shortDamage;
	}
	H.shortDamage -= cure;
	H.condition += cure;
	updateHealth(slave);
};

/**
 * Surgical procedures also depend on the PC's medicine skill
 * @param {App.Entity.SlaveState} slave
 * @param {number} damage
 * @returns {void}
 */
globalThis.surgeryDamage = function(slave, damage) {
	const playerSkillFactor = 1 + Math.clamp(Math.pow(V.PC.skill.medicine / 100, 2), 0, 1);
	healthDamage(slave, Math.trunc(damage / playerSkillFactor));
};

/**
 * All things improving a slave's condition go through this to update condition and slave health.
 * @param {App.Entity.SlaveState} slave
 * @param {number} condition
 * @returns {void}
 */
globalThis.improveCondition = function(slave, condition) {
	if (!_.isFinite(condition)) {
		throw "Health condition change must be a finite number.";
	}
	const H = slave.health;
	H.condition += Math.max(Math.trunc(condition), 0);
	updateHealth(slave);
};

/**
 * Updates slave.health.health
 * @param {App.Entity.SlaveState} slave
 * @returns {void}
 */
globalThis.updateHealth = function(slave) {
	const H = slave.health;
	const condition = H.condition;

	// Converting the other variables to better fit the same scale as .condition
	const damage = (40 - H.shortDamage - H.longDamage) * 2.5; // 100 / -inf
	const tired = (40 - H.tired) * 2.5; // 100 / -150
	const illness = (5 - Math.pow(H.illness, 2)) * 20; // 100 / -400

	// Assigning weights to the different components and aggregating
	H.health = condition * 0.6 + damage * 0.2 + tired * 0.1 - illness * 0.1;
};

/**
 * A function for setting valid health values for new slaves, comes with a set of defaults.
 * Not meant for use with existing slaves (consider healthDamage(), surgeryDamage() and improveCondition() first), but sometimes useful still.
 * @param {App.Entity.SlaveState} slave
 * @param {number} condition
 * @param {number} [shortDamage]
 * @param {number} [longDamage]
 * @param {number} [illness]
 * @param {number} [tired]
 * @returns {void}
 */
globalThis.setHealth = function(slave, condition = -101, shortDamage = -1, longDamage = -1, illness = -1, tired = -1) {
	const H = slave.health;

	// Making sure all values get set to either a default or their desired value
	if (condition < -100) {
		H.condition = jsRandom(-50, 50); // Default value
	} else {
		H.condition = condition;
	}
	if (shortDamage < 0) {
		H.shortDamage = Math.max(normalRandInt(0, 3), 0);
	} else {
		H.shortDamage = shortDamage;
	}
	if (longDamage < 0) {
		H.longDamage = Math.max(normalRandInt(0, 3), 0);
	} else {
		H.longDamage = longDamage;
	}
	if (illness < 0 || illness > 5) {
		H.illness = Math.max(normalRandInt(0, 0.5), 0);
	} else {
		H.illness = illness;
	}
	if (tired < 0 || tired > 100) {
		H.tired = jsRandom(20, 50);
	} else {
		H.tired = tired;
	}

	// Adjusting long term damage for age
	if (slave.physicalAge > 29) {
		for (let i = 1; i < (slave.physicalAge - 29); i++) {
			H.longDamage += Math.trunc((i + 4 + jsRandom(1, 15)) / 20);
		}
	}

	// Making sure their overall health doesn't go too low (and would lead to a very likely death)
	if (H.condition - H.shortDamage - H.longDamage < -100) {
		let excess = -100 + H.condition + H.shortDamage + H.longDamage;
		if (H.shortDamage + H.longDamage < 50) { // As long as the damage doesn't get too crazy we'll compensate condition for it
			H.condition += excess;
		} else if (H.longDamage < 50) { // As long as long damage doesn't get too high we'll compensate by improving shortDamage and condition
			if (H.shortDamage - Math.ceil(excess / 2) > 0) {
				H.shortDamage -= Math.ceil(excess / 2);
				H.condition += Math.ceil(excess / 2);
			} else {
				excess -= H.shortDamage;
				H.shortDamage = 0;
				H.condition += excess;
			}
		} else { // If such high longDamage was intentional, they really should have done better basic math to ensure these slaves wouldn't just die immediately
			if (H.shortDamage - Math.ceil(excess / 3) > 0 && H.longDamage - Math.ceil(excess / 3) > 0) {
				H.longDamage -= Math.ceil(excess / 3);
				H.shortDamage -= Math.ceil(excess / 3);
				H.condition += Math.ceil(excess / 3);
			} else {
				excess -= H.shortDamage;
				H.shortDamage = 0;
				if (H.longDamage - Math.ceil(excess / 2) > 0) {
					H.longDamage -= Math.ceil(excess / 2);
					H.condition += Math.ceil(excess / 2);
				} else {
					excess -= H.longDamage;
					H.longDamage = 0;
					H.condition += excess;
				}
			}
		}
	}

	// Finally giving slaves their overall health value
	H.health = H.condition - H.shortDamage - H.longDamage;
};


code:
globalThis.ibc = (() => {
	// These IDs are considered to be unknown parents
	let or_null = (s) => specificCharacterID(s) ? s : null;

	// Find some sort of state for a slave. Checks first the gene pool, then V.slaves, then the
	// missing table
	let find_gp = (id) => (slaveStateById(id) || V.genePool.find((s) => s.ID === id) || ((id in V.missingTable) ? V.missingTable[id] : null) || null);

	// Create a node for the given ID
	let create_node = (id) => ({
		id: id, // Node ID
		mother: null,
		father: null,
		nodecodes: [], // NodeCodes
		nodecodes_str: [], // String version of the NodeCodes, for comparison
		_coeff: null, // Cached CoI
		child_count: 0 // Number of children of the node, for computing NodeCodes
	});

	// Determine the length of the shared prefix between the two NodeCode parameters
	let prefix_len = (nca, ncb) => {
		let i = 0;
		for (i=0; i<Math.min(nca.length, ncb.length); i++) {
			if (nca[i] !== ncb[i])
				break;
		}
		return i;
	};

	// Determine the set of longest common prefixes for a node pair
	let prefixes = (a, b) => {
		let found = [];
		let found_s = [];
		a.nodecodes.forEach(nca => {
			let match = false;
			b.nodecodes.forEach(ncb => {
				let l = prefix_len(nca, ncb);
				if (l === 0 && match)
					return;

				if (l > 0) {
					match = true;
					let pfx = nca.slice(0, l);
					let pfx_s = pfx.join(';');

					if (!found_s.includes(pfx_s)) {
						found_s.push(pfx_s);
						found.push(pfx);
					}
				}
			});
		});

		return found;
	};

	// Search up the tree to find a given NodeCode, starting at `n`
	let find_nc = (nc, n) => {
		if (n.nodecodes_str.includes(nc.join(';'))) {
			return n;
		}

		let ret = null;
		if (n.mother !== null)
			ret = find_nc(nc, n.mother);
		if (n.father !== null && ret === null)
			ret = find_nc(nc, n.father);

		return ret;
	};

	// Determine the set of common ancestors between a node pair
	let common = (a, b) => {
		let pfx = prefixes(a, b);
		let pfx_s = pfx.map(s => s.join(';'));
		let anc = [];

		while (pfx.length > 0) {
			let p = pfx.pop(0);
			pfx_s.pop(0);
			let ret = find_nc(p, a);

			ret.nodecodes.forEach(nc => {
				let i = pfx_s.indexOf(nc.join(';'));
				if (i === -1)
					return;

				pfx.pop(i);
				pfx_s.pop(i);
			});

			if (anc.findIndex(s => (s[0] == ret)) === -1)
				anc.push([ret, p]);
		}

		return anc;
	};

	// Determine the set of NodeCodes on `n` with prefix `x`
	let mps = (n, x) => {
		let x_s = x.join(';');
		return n.nodecodes.filter(nc => (nc.slice(0, x.length).join(';') === x_s));
	};

	// Compute the set of all paths to the parents of `n` with prefix `x`
	let pp = (mother, father, x) => {
		let m = mps(mother, x);
		let f = mps(father, x);

		let prod = [];
		m.forEach(i => {
			f.forEach(j => {
				prod.push([i, j]);
			});
		});

		return prod;
	};

	let kinship = (mother, father) => {
		let _coeff = 0;
		if (mother === null || father === null)
			_coeff = 0;
		else if (mother == father)
			_coeff = 0.5 * (1 + coeff(mother));
		else {
			let cf = 0;
			let cmn = common(mother, father);

			cmn.forEach(el => {
				let c = el[0];
				let p = el[1];
				let p_s = p.join(';');

				let paths = pp(mother, father, p);
				let paths_s = paths.map(p => [p[0].join(';'), p[1].join(';')].join(','));

				cmn.forEach(el2 => {
					let co = el2[0];
					if (co == c)
						return;
					let m_pp = [];
					let f_pp = [];

					co.nodecodes.forEach(nc => {
						if (nc.slice(0, p.length).join(';') != p_s)
							return;

						m_pp = m_pp.concat(mps(mother, nc));
						f_pp = f_pp.concat(mps(father, nc));
					});

					m_pp.forEach(mp => {
						f_pp.forEach(fp => {
							let mf_s = [mp.join(';'), fp.join(';')].join(',');
							let i = paths_s.indexOf(mf_s);
							if (i === -1)
								return;
							paths_s.pop(i);
							paths.pop(i);
						});
					});
				});
				paths.forEach(p => {
					let pfx = prefix_len(p[0], p[1]);

					cf += 0.5**(p[0].length + p[1].length+1 - 2*pfx) * (1 + coeff(c));
				});
			});

			_coeff = cf;
		}

		return _coeff;
	};

	// Determine the coefficient of inbreeding of a node `n`
	let coeff = n => {
		if (n._coeff === null)
			n._coeff = kinship(n.mother, n.father);
		return n._coeff;
	};

	// Populate the NodeCodes.
	//
	// Each node has a set of NodeCodes, which represent the set of paths from it to its ancestors.
	// NodeCodes here are represented by arrays of integers.
	//
	// NodeCodes are constructed recursively in this fashion:
	//
	// - Assign each of the founders (nodes with both parents === null) an unique ID, starting from
	//   0 and incrementing each time (the order doesn't matter); a founder's set of NodeCodes has
	//   exactly one NodeCode, which is [ID] (an array containing only their ID)
	//
	// - For each other node, let M be its child number w.r.t. its mother and N its child number
	//   w.r.t. its father, i.e. the number of children that the respective parent has had before
	//   this one (the order is not important to the algorithm, it's arbitrary here for
	//   convenience). Its set of NodeCodes is the set of all its mother's NodeCodes with M appended
	//   and all of its father's NodeCodes with N appended. For example, if its mother has the
	//   NodeCodes [[2]] and M = 3 and its father has the NodeCodes [[0,1], [3,1]] and N = 1 then
	//   the set of NodeCodes for this node would be
	//
	//       [[2, 3], [0, 1, 1], [3, 1, 1]]
	//
	// We do this iteratively here, looping over the set of all nodes until each has been assigned
	// a NodeCode. This requires looping through a number of times equal to the number of
	// generations, since as soon as both parents have NodeCodes their children's NodeCodes may be
	// computed.
	let make_nc = nodes => {
		// Generate founder NodeCodes
		let total = Object.keys(nodes).length;
		let seen = [];
		let curid = 0;
		Object.values(nodes).forEach(n => {
			if (n.mother !== null || n.father !== null)
				return;
			n.nodecodes.push([curid]);
			curid += 1;
			seen.push(n.id);
		});

		// Generate the rest of the NodeCodes
		let oldSeenLength = -1;
		while (seen.length !== total) {
			oldSeenLength = seen.length;
			Object.keys(nodes).forEach(s=> {
				let n = nodes[s];
				if (seen.includes(+s)) // We've already done this
					return;
				else if ((n.mother !== null && n.mother.nodecodes.length === 0) || (n.father !== null && n.father.nodecodes.length === 0)) // Too soon, we haven't done its parents
					return;

				seen.push(n.id);
				// Compute the NodeCodes from its parents
				[n.mother, n.father].forEach((a, i) => {
					if (a === null || (n.mother === n.father && i === 1)) // Ignore missing parents/repeated
						return;

					a.nodecodes.forEach(nc => {
						// Copy the NodeCode, push the child number, then add it
						let nnc = nc.slice();
						nnc.push(a.child_count);
						n.nodecodes.push(nnc);
					});
					a.child_count += 1;
				});

				// NodeCodes must be sorted; this suffices
				n.nodecodes.sort();
			});
			// check to make sure we're progressing...if not, there's a cycle in the graph
			// dump all the nodes participating in or descended from the cycle and let the player figure it out
			if (oldSeenLength === seen.length) {
				const badSlaveIDs = Object.keys(nodes).filter(s => !seen.includes(+s)).map(k => nodes[k].id);
				throw(`Inbreeding calculation: heritance cycle detected. Check slave IDs: ${badSlaveIDs}`);
			}
		}

		// Cache the string NodeCodes
		Object.values(nodes).forEach(n => {
			n.nodecodes_str = n.nodecodes.map(nc => nc.join(';'));
		});
	};

	// Make nodes for an array of slaves
	let nodes_slaves = (slaves, ignore_coeffs=false) => {
		let nodes = {};

		// Recursively create the nodes we need, moving upwards from the given slave
		let create_node_rec = s => {
			// Certain parents (e.g. 0, societal elite) are not considered to be related, despite
			// having the same ID; convert them to null
			let m = or_null(s.mother);
			let f = or_null(s.father);

			// Ensure that parent nodes are created
			[m, f].forEach(p => {
				if (p !== null && !(p in nodes)) { // Not created, we have to do something
					if (p === -1) {
						create_node_rec(SugarCube.State.variables.PC);
					} else {
						// Search for a slave state, genePool entry, or missingTable entry
						let gp = find_gp(p);
						if (gp !== null) {
							// If we find one, we might have ancestry information: recurse
							create_node_rec(gp);
						} else {
							// Otherwise, just create a plain node
							nodes[p] = create_node(p);
						}
					}
				}
			});

			if (!(s.ID in nodes)) // Create this node if necessary
				nodes[s.ID] = create_node(s.ID);
			// We created its parents earlier, so set them to the actual nodes
			nodes[s.ID].mother = (m === null) ? m : nodes[m];
			nodes[s.ID].father = (f === null) ? f : nodes[f];

			// Try to use a cached CoI for performance
			let sg = find_gp(s.ID);
			if (!ignore_coeffs && sg !== null && "inbreedingCoeff" in sg && sg.inbreedingCoeff !== -1) {
				nodes[s.ID]._coeff = sg.inbreedingCoeff;
			}
		};

		// Populate the nodes
		slaves.forEach(s=> create_node_rec(s));

		// Populate NodeCodes
		make_nc(nodes);

		return nodes;
	};

	// Determine the coefficients of inbreeding of an array of slaves. Returns a mapping of their
	// ID to their coefficient of inbreeding
	let coeff_slaves = (slaves, ignore_coeffs=false) => {
		let ret = {};
		// First, pull as many existing CoI off the slaves
		slaves.forEach(s => {
			let sg = find_gp(s.ID);
			if (!ignore_coeffs && sg !== null && "inbreedingCoeff" in sg && sg.inbreedingCoeff !== -1) {
				ret[s.ID] = sg.inbreedingCoeff;
			}
		});

		// Now do any we haven't done already
		slaves = slaves.filter(s => (!(s.ID in ret)));
		if (slaves.length > 0) {
			let nodes = nodes_slaves(slaves, ignore_coeffs);

			// Compute coefficients
			slaves.forEach(s => {
				ret[s.ID] = coeff(nodes[s.ID]);
			});
		}

		return ret;
	};

	// Determine the kinship between two slaves `a` and `b`
	let kinship_slaves = (a, b, ignore_coeffs=false) => {
		if (a === 0 || b === 0)
			return 0;

		return kinship_one_many(a, [b], ignore_coeffs)[b.ID];
	};

	// Determine the coefficient of inbreeding of a single slave
	let coeff_slave = (slave, ignore_coeffs=false) => {
		if (!ignore_coeffs && "inbreedingCoeff" in slave && slave.inbreedingCoeff !== -1)
			return slave.inbreedingCoeff;

		let gp = find_gp(slave.ID);
		if (!ignore_coeffs && gp !== null && "inbreedingCoeff" in gp && gp.inbreedingCoeff !== -1)
			return gp.inbreedingCoeff;

		return coeff_slaves([slave], ignore_coeffs)[slave.ID];
	};

	// Determine the kinship between one and many slaves. Returns an mapping from the ID of each of
	// the slaves in `others` to its kinship with slave `a`
	let kinship_one_many = (a, others, ignore_coeffs=false) => {
		if (a === 0) {
			let ks = {};
			others.forEach(s => {
				if (s === 0)
					ks[s] = 0;
				else
					ks[s.ID] = 0;
			});
			return ks;
		}

		let nodes = nodes_slaves(others.concat([a]), ignore_coeffs);

		let ks = {};
		others.forEach(s => {
			if (s === -3) {
				// Fake a slave object for the player's old master
				s = {ID: -3, mother: 0, father: 0, inbreedingCoeff: 0};
			}

			if (s === 0)
				ks[s] = 0;
			else
				ks[s.ID] = kinship(nodes[a.ID], nodes[s.ID]);
		});

		return ks;
	};

	// Recalculate the inbreeding coefficient for all slaves dependent on the passed IDs (e.g. the
	// slaves themselves and all of their children). This will replace the inbreeding coefficients
	// wherever they exist with the computed values, ignoring all cached values.
	//
	// This should be called if parents are changed.
	let recalculate_coeff_ids = (ids) => {
		// These are all the slave-like objects, i.e. they have ID, mother, and father. There will
		// be multiple elements with the same ID: we want this, since we have to replace all
		// occurrences of the COI for the affected slaves
		let all_slave_like = V.slaves.concat(V.genePool).concat(V.cribs).concat(V.tanks).concat(Object.values(V.missingTable));
		if (V.boomerangSlave !== 0)
			all_slave_like.push(V.boomerangSlave);
		if (V.traitor !== 0)
			all_slave_like.push(V.traitor);
		if (V.activeSlave !== 0)
			all_slave_like.push(V.activeSlave);
		all_slave_like.push(V.PC);
		// Add a fake entry for the PC's old master
		all_slave_like.push({ID: -3, mother: 0, father: 0, inbreedingCoeff: 0});

		// Gather the genetics of all current fetuses
		let all_fetuses = V.slaves.filter(s => s.preg > 0).map(s => s.womb.map(i => i.genetics)).reduce((res, cur) => res.concat(cur), []);

		// Recursively find all of the given ID's children, born and unborn
		let find_children_rec = (id, cur_slaves, cur_fetuses, cur_fetus_parents) => {
			// Add fetuses
			all_fetuses.filter(f => (f.father === id || f.mother === id)).forEach(f => {
				// We may have to manually add the parents later
				let actual_f = or_null(f.father);
				let actual_m = or_null(f.mother);
				if (actual_f !== null)
					cur_fetus_parents.add(actual_f);
				if (actual_m !== null)
					cur_fetus_parents.add(actual_m);

				cur_fetuses.add(f);
			});

			// Recursively add slaves
			all_slave_like.filter(s => (s.father === id || s.mother === id)).forEach(s => {
				if (!cur_slaves.has(s.ID)) {
					cur_slaves.add(s.ID);
					find_children_rec(s.ID, cur_slaves, cur_fetuses, cur_fetus_parents);
				}
			});
		};

		// We only need slave IDs, since we have to update all of their entries (including GP)
		let needed_slave_ids = new Set();
		// Since each fetus has a unique record, a set still suffices
		let needed_fetuses = new Set();
		let needed_parent_ids = new Set();

		// Find all the children of the IDs we need to do
		ids.forEach(id => {
			needed_slave_ids.add(id);
			find_children_rec(id, needed_slave_ids, needed_fetuses, needed_parent_ids);
		});

		// Now we assemble the tree from the slaves
		let needed_slaves = [];
		needed_slave_ids.forEach(id => {
			if (typeof id !== "undefined")
				needed_slaves.push(all_slave_like.find(s => s.ID === id));
		});
		needed_parent_ids.forEach(id => {
			if (typeof id !== "undefined" && !needed_slave_ids.has(id))
				needed_slaves.push(all_slave_like.find(s => s.ID === id));
		});
		let nodes = nodes_slaves(needed_slaves, true);

		// Now calculate the inbreeding coefficients (they're cached in the tree once calculated)
		all_slave_like.filter(s => needed_slave_ids.has(s.ID)).forEach(s => {
			s.inbreedingCoeff = coeff(nodes[s.ID]);
		});

		// Finally, handle all of the kinship for the fetuses
		let kinship_cache = new Map(); // Manually cache it
		needed_fetuses.forEach(f => {
			if (or_null(f.mother) === null || or_null(f.father) === null) {
				f.inbreedingCoeff = 0;
				return;
			}

			// Use a string of the form "parent;parent" to store the cache value; since kinship is
			// commutative, the minimum parent ID will be first
			let kinship_str = Math.min(f.mother, f.father) + ';' + Math.max(f.mother, f.father);
			if (!kinship_cache.has(kinship_str))
				kinship_cache.set(kinship_str, kinship(nodes[f.mother], nodes[f.father]));

			f.inbreedingCoeff = kinship_cache.get(kinship_str);
		});
	};

	let recalculate_coeff_id = (id) => {
		return recalculate_coeff_ids([id]);
	};

	return {
		coeff: coeff_slave,
		coeff_slaves,
		kinship: kinship_slaves,
		kinship_one_many,
		recalculate_coeff_ids,
		recalculate_coeff_id
	};
})();


code:
globalThis.isItemAccessible = (function() {
	return {
		array: array,
		entry: entry,
	};

	/**
	 * Checks whether an item is accessible
	 * @param {string} string Name of wearable item
	 * @param {string} [category="clothing"] that item is in clothing, collar, etc
	 * @param {App.Entity.SlaveState} [slave]
	 * @returns {boolean|string} Returns true if item is accessible, and false if it is not. If the slave param is set, it may sometimes return a string instead of false, explaining why the item can't be used with that slave.
	 */
	function entry(string, category = "clothing", slave) {
		if (V.cheatMode === 1) {
			return true;
		}
		let niceDB = [];
		let harshDB = [];
		switch (category) {
			case "clothing":
			case "clothes":
				niceDB = App.Data.misc.niceClothes;
				harshDB = App.Data.misc.harshClothes;
				break;
			case "collar":
				niceDB = App.Data.misc.niceCollars;
				harshDB = App.Data.misc.harshCollars;
				break;
			case "bellyAccessory":
				niceDB = App.Data.misc.bellyAccessories;
				break;
			case "buttplug":
				niceDB = App.Data.misc.buttplugs;
				break;
			case "buttplugAttachment":
				niceDB = App.Data.misc.buttplugAttachments;
				break;
			case "vaginalAccessory":
				niceDB = App.Data.misc.vaginalAccessories;
				break;
			case "vaginalAttachment":
				niceDB = App.Data.misc.vaginalAttachments;
				break;
			case "dickAccessory":
				niceDB = App.Data.misc.vaginalAccessories;
				break;
			case "shoes":
				niceDB = App.Data.misc.shoes;
				break;
			case "chastity":
				niceDB = App.Data.misc.chastityDevices;
				break;
			default:
				console.log(`made a category for ${category} automatically, may need to define this by hand`);
				niceDB = App.Data.misc[category];
				break;
		}
		let item = niceDB.find((i) => i.value === string);
		if (!item && (typeof harshDB !== 'undefined')) {
			item = harshDB.find((i) => i.value === string);
		}
		if (!item) {
			console.log(`${string} is not a registered piece of clothing! Check App.Data.misc.${category}`);
			return false; /* couldn't be found */
		}
		return isAvailable(item, category, slave);
	}
	/**
	 * Returns array of wearable clothing in format [name, value], basically player facing / game data.
	 * @param {Array} db Array to look in (such as App.Data.misc.niceClothes)
	 * @returns {Array}
	 */
	function array(db) {
		const array = [];
		db.forEach((i) => {
			if (V.cheatMode || isAvailable(i)) {
				let name = i.name;
				if (i.fs) {
					name = `${i.name} (FS)`;
				}
				array.push([name, i.value]);
			}
		});
		return array;
	}

	/**
	 * @param {object} item
	 * @param {string} [category="clothing"] that item is in clothing, collar, etc
	 * @param {App.Entity.SlaveState} [slave]
	 * @returns {boolean|string} Returns true if item is accessible, and false if it is not. If the slave param is set, it may sometimes return a string instead of false, explaining why the item can't be used with that slave.
	 */
	function isAvailable(item, category, slave) {
		let slaveResults;
		if (slave) {
			slaveResults = isAvailableForSlave(item, category, slave);
			if (slaveResults === false) {
				return slaveResults;
			}
		}
		if (!(item.hasOwnProperty("fs")) && !(item.hasOwnProperty("requirements"))) {
			// No restriction, this clothing item is available to everyone
			return true;
		}
		if (item.hasOwnProperty("requirements")) {
			if (item.requirements === true) {
				return true;
			}
		}
		if (item.hasOwnProperty("fs")) {
			if (V.arcologies[0][item.fs] > 0) {
				return true;
			}
		}
		if (slaveResults && slaveResults !== true) { // If we still haven't returned true or false, then we display why this particular slave can't use the item.
			return slaveResults;
		}
		return false;
	}

	/**
	 * @param {object} item
	 * @param {string} [category="clothing"] that item is in clothing, collar, etc
	 * @param {App.Entity.SlaveState} [slave]
	 * @returns {boolean|string} Returns true if item is accessible, and false if it is not. If the slave param is set, it may sometimes return a string instead of false, explaining why the item can't be used with that slave.
	 */
	function isAvailableForSlave(item, category, slave) {
		switch (category) {
			case "clothing":
			case "clothes":
				break;
			case "collar":
				break;
			case "bellyAccessory": {
				switch (item.value) {
					case "a support band": {
						if (slave.belly > 10000) {
							return true;
						} else {
							return `Slave belly too small for this`;
						}
					}
					case "a small empathy belly":
					case "a medium empathy belly":
					case "a large empathy belly":
					case "a huge empathy belly": {
						if (slave.belly < 1500 && slave.weight < 130) {
							return true;
						} else {
							return `Slave belly too big for this`;
						}
					}
					default:
						return true;
				}
			}
			case "buttplug": {
				switch (item.value) {
					case "huge plug": {
						if (slave.anus < 2) {
							return `Slave's anus is too small for this right now`;
						} else {
							return true;
						}
					}
					case "long plug":
					case "long, large plug": {
						if (!(slave.breedingMark !== 1 || V.propOutcome === 0 || V.eugenicsFullControl === 1 || V.arcologies[0].FSRestart === "unset")) {
							return "Elites frown on this";
						} else {
							return true;
						}
					}
					case "long, huge plug": {
						if (slave.anus < 2) {
							return `Slave's anus is too small for this right now`;
						} else if (!(slave.breedingMark !== 1 || V.propOutcome === 0 || V.eugenicsFullControl === 1 || V.arcologies[0].FSRestart === "unset")) {
							return "Elites frown on this";
						} else {
							return true;
						}
					}
					default:
						return true;
				}
			}
			case "buttplugAttachment":
				break;
			case "vaginalAccessory": {
				if (slave.vagina < 0) {
					return false;
				}
				switch (item.value) {
					case "huge dildo": {
						if (slave.vagina < 2) {
							return `Slave's vagina is too small for this right now`;
						} else {
							return true;
						}
					}
					case "long dildo":
					case "long, large dildo": {
						if (!(slave.breedingMark !== 1 || V.propOutcome === 0 || V.eugenicsFullControl === 1 || V.arcologies[0].FSRestart === "unset")) {
							return "Elites frown on this";
						} else {
							return true;
						}
					}
					case "long, huge dildo": {
						if (slave.vagina < 2) {
							return `Slave's vagina is too small for this right now`;
						} else if (!(slave.breedingMark !== 1 || V.propOutcome === 0 || V.eugenicsFullControl === 1 || V.arcologies[0].FSRestart === "unset")) {
							return "Elites frown on this";
						} else {
							return true;
						}
					}
					default:
						return true;
				}
			}
			case "vaginalAttachment": {
				if (slave.vagina < 0) {
					return false;
				}
				switch (item.value) {
					case "vibrator":
						if (slave.vaginalAccessory === "none") {
							return "No vaginal accessory to attach it to";
						} else if (slave.vaginalAccessory === "bullet vibrator" || slave.vaginalAccessory === "smart bullet vibrator") {
							return "Vaginal accessory already vibrates";
						} else {
							return true;
						}
				}
				break;
			}
			case "dickAccessory":
				return (slave.dick >= 1);
			case "shoes":
				break;
			case "chastity": {
				switch (item.value) {
					case "chastity belt":
					case "combined chastity belt":
						return (slave.vagina > -1);
					case "chastity cage":
					case "combined chastity cage":
						return (slave.dick > 0);
					case "genital chastity":
					case "full chastity":
						return (slave.vagina > -1 && slave.dick > 0);
					case "choose own chastity":
						return (slave.choosesOwnChastity !== 1 && slave.devotion > 20 && slave.trust > 0);
					case "revoke choosing own chastity":
						return (slave.choosesOwnChastity > 0);
					default:
						return true;
				}
			}
			default:
				break;
		}
		return true;
	}
})();

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} prosthetic
 * @returns {boolean}
 */
globalThis.isProstheticAvailable = function(slave, prosthetic) {
	return slave.readyProsthetics.findIndex(function(p) { return p.id === prosthetic; }) !== -1;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} prosthetic
 */
globalThis.addProsthetic = function(slave, prosthetic) {
	if (!isProstheticAvailable(slave, prosthetic)) {
		let limb = prostheticToLimb(prosthetic);
		if (limb > 0) {
			let p = {
				id: prosthetic,
				arm: {left: new App.Entity.LimbState(), right: new App.Entity.LimbState()},
				leg: {left: new App.Entity.LimbState(), right: new App.Entity.LimbState()}
			};
			p.arm.left.type = limb;
			p.arm.right.type = limb;
			p.leg.left.type = limb;
			p.leg.right.type = limb;
			slave.readyProsthetics.push(p);
		} else {
			slave.readyProsthetics.push({id: prosthetic});
		}
	}
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {string} prosthetic
 * @returns {{}}
 */
globalThis.findProsthetic = function(slave, prosthetic) {
	return slave.readyProsthetics.find(p => p.id === prosthetic);
};

/**
 * @param {string} prosthetic
 * @returns {number}
 */
globalThis.prostheticToLimb = function(prosthetic) {
	switch (prosthetic) {
		case "basicL":
			return 2;
		case "sexL":
			return 3;
		case "beautyL":
			return 4;
		case "combatL":
			return 5;
		case "cyberneticL":
			return 6;
		default:
			return 0;
	}
};

/**
 *
 * @param {number} limb
 * @returns {string}
 */
globalThis.limbToProsthetic = function(limb) {
	switch (limb) {
		case 2:
			return "basicL";
		case 3:
			return "sexL";
		case 4:
			return "beautyL";
		case 5:
			return "combatL";
		case 6:
			return "cyberneticL";
		default:
			return "";
	}
};


code:
/**
 * @returns {DocumentFragment}
 */
App.MainView.errors = function() {
	const fragment = document.createDocumentFragment();

	/**
	 * @returns {HTMLParagraphElement}
	 */
	function newError() {
		const error = document.createElement("p");
		fragment.append(error);
		return error;
	}

	// check for correct version
	if (V.releaseID >= 1000 || ["0.9", "0.8", "0.7", "0.6"].includes(V.ver)) {
		if (V.releaseID < App.Version.release) {
			newError().append(App.UI.DOM.makeElement("span", "INCOMPATIBILITY WARNING:", "major-warning"),
				` Your saved game was created using version: ${V.ver}, build: ${V.releaseID}. You must run `,
				App.UI.DOM.passageLink("Backwards Compatibility", "Backwards Compatibility"));
		}
	} else {
		newError().append(App.UI.DOM.makeElement("span", "INCOMPATIBLE SAVE WARNING:", "major-warning"),
			` Your saved game was created using version: ${V.ver}, and you are using a later version which New Game Plus cannot reconcile. Please start a new game.`);
	}

	// check for correct rules
	if (V.defaultRules.length > 0 && (V.defaultRules[0].condition === undefined || V.defaultRules[0].set === undefined)) {
		const error = newError();
		error.append(App.UI.DOM.makeElement("span", "INCOMPATIBILITY WARNING:", "major-warning"),
			" The rules assistant format has changed. In the Options Menu, please ");
		const ra = document.createElement("strong");
		ra.append("Reset RA Rules");
		error.append(ra);
	}

	// check for NaN
	if (V.NaNArray.length > 0) {
		const error = newError();
		error.id = "NaNArray";
		error.append(App.UI.DOM.makeElement("span", "ERROR: The following variables are NaN! Please report this.", "error"));
		V.NaNArray.forEach(e => {
			const div = document.createElement("div");
			div.append(e);
			error.append(div);
		});
		error.append(App.UI.DOM.link("Hide NaN variables until next week",
			() => {
				error.outerHTML = "";
				V.NaNArray = [];
			})
		);
	}

	// check custom slaves
	if (App.Utils.IsCustomSlaveMutated(V.customSlave)) {
		newError().append(App.UI.DOM.makeElement("span", "ERROR: Your custom slave order has taken on a mutated life of its own and has been summarily shot. Refile your custom slave order, if necessary, and notify the appropriate authorities if you see this message again.", "error"));
		V.customSlave = new App.Entity.CustomSlaveOrder();
	}
	if (App.Utils.IsCustomSlaveMutated(V.huskSlave)) {
		newError().append(App.UI.DOM.makeElement("span", "ERROR: Your husk slave order has taken on a mutated life of its own and has been summarily shot. Refile your husk slave order, if necessary, and notify the appropriate authorities if you see this message again.", "error"));
		V.huskSlave = new App.Entity.CustomSlaveOrder();
	}

	return fragment;
};

/**
 * @returns {Text}
 */
App.MainView.fcnn = function() {
	let text;

	if (V.FCNNstation !== 1 && V.week >= 90) {
		text = "FCNN: FCNN service has been temporarily suspended. Please stand by.";
	} else {
		text = V.fcnn.random();
	}

	return document.createTextNode(`${text} `);
};

App.MainView.useFucktoys = function() {
	const fragment = document.createDocumentFragment();
	for (const slave of V.slaves) {
		if (slave.assignment !== Job.FUCKTOY) {
			continue;
		}
		fragment.append(App.MainView.useFucktoy(slave));
	}
	return fragment;
};

/**
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLDivElement}
 */
App.MainView.useFucktoy = function(slave) {
	const fragment = document.createDocumentFragment();

	function setEnvironment() {
		V.activeSlave = slave;
		V.nextButton = "Back";
		V.nextLink = "AS Dump";
		V.returnTo = passage();
	}

	const {him, his} = getPronouns(slave);

	const div = document.createElement("div");
	div.classList.add("note");

	div.append(App.Interact.ToyChest(slave));

	div.append(" In the coming week you plan to concentrate on ");

	if (slave.toyHole !== "all her holes") {
		div.append(`${his} ${slave.toyHole}`);
	} else {
		div.append(`all of ${his} holes equally`);
	}

	if (slave.fuckdoll === 0) {
		div.append(", but for now:");

		const optionDiv = document.createElement("div");
		optionDiv.classList.add("indent");

		optionDiv.append(App.UI.DOM.passageLink(`Use ${his} mouth`, "FLips", setEnvironment),
			" | ", App.UI.DOM.passageLink("Play with " + his + " tits", "FBoobs", setEnvironment));

		if (canDoVaginal(slave)) {
			optionDiv.append(" | ", App.UI.DOM.passageLink(`Fuck ${him}`, "FVagina", setEnvironment));
			if (canDoAnal(slave)) {
				optionDiv.append(" | ", App.UI.DOM.passageLink(`Use ${his} holes`, "FButt", setEnvironment));
			}
		}
		if (canDoAnal(slave)) {
			optionDiv.append(" | ", App.UI.DOM.passageLink(`Fuck ${his} ass`, "FAnus", setEnvironment));
		}
		if (canDoVaginal(slave) || canDoAnal(slave)) {
			if (slave.belly >= 300000) {
				optionDiv.append(" | ", App.UI.DOM.passageLink(`Fuck ${him} over ${his} belly`, "FBellyFuck", setEnvironment));
			}
		}
		if (canPenetrate(slave)) {
			optionDiv.append(" | ", App.UI.DOM.passageLink(`Ride ${him}`, "FDick", setEnvironment));
		}
		optionDiv.append(" | ", App.UI.DOM.passageLink(`Abuse ${him}`, "FAbuse", setEnvironment));

		div.append(optionDiv);
	} else {
		div.append(".");
	}
	fragment.append(div);

	return div;
};

/**
 * @returns {HTMLDivElement}
 */
App.MainView.useGuard = function() {
	const guard = S.Bodyguard;

	const outerDiv = document.createElement("div");

	if (!guard || guard.assignment !== Job.BODYGUARD) {
		return outerDiv;
	}

	App.UI.DOM.appendNewElement("span", outerDiv, App.Interact.guardPose(guard), "scene-intro");

	function setEnvironment() {
		V.activeSlave = guard;
		V.nextButton = "Back";
		V.nextLink = "AS Dump";
		V.returnTo = passage();
	}

	const {him, his} = getPronouns(guard);
	const optionDiv = document.createElement("div");
	optionDiv.classList.add("indent");

	optionDiv.append(
		App.UI.DOM.passageLink(`Use ${his} mouth`, "FLips", setEnvironment),
		" | ",
		App.UI.DOM.passageLink(`Play with ${his} tits`, "FBoobs", setEnvironment)
	);

	if (canDoVaginal(guard)) {
		optionDiv.append(" | ", App.UI.DOM.passageLink(`Fuck ${him}`, "FVagina", setEnvironment));
		if (canDoAnal(guard)) {
			optionDiv.append(" | ", App.UI.DOM.passageLink(`Use ${his} holes`, "FButt", setEnvironment));
		}
		if (guard.belly >= 300000) {
			optionDiv.append(" | ", App.UI.DOM.passageLink(`Fuck ${him} over ${his} belly`, "FBellyFuck", setEnvironment));
		}
	}

	if (canPenetrate(guard)) {
		optionDiv.append(" | ", App.UI.DOM.passageLink(`Ride ${him}`, "FDick", setEnvironment));
	}
	if (canDoAnal(guard)) {
		optionDiv.append(" | ", App.UI.DOM.passageLink(`Fuck ${his} ass`, "FAnus", setEnvironment));
	}
	optionDiv.append(" | ", App.UI.DOM.passageLink(`Abuse ${him}`, "Gameover", () => { V.gameover = "idiot ball"; }));

	outerDiv.append(optionDiv);

	return outerDiv;
};

App.MainView.full = function() {
	function mainMenu() {
		const div = document.createElement("div");

		div.append(App.UI.DOM.makeElement("span", "MAIN MENU", "name"));

		App.UI.DOM.appendNewElement("span", div, App.UI.DOM.passageLink("Summary Options", "Summary Options"),
			["tab", "note"]);

		if (V.rulesAssistantMain !== 0) {
			div.append(" | ");
			const raLink = document.createElement("span");
			raLink.classList.add("note");
			raLink.id = "RAButton";
			raLink.append(App.UI.DOM.passageLink("Rules Assistant Options", "Rules Assistant"));
			div.append(raLink, " ",
				App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Rules Assistant"), "hotkey"));

			div.append(" | ");
			if (V.rulesAssistantAuto !== 1) {
				App.UI.DOM.appendNewElement("span", div,
					App.UI.DOM.passageLink("Apply Rules Assistant at week end", "Main", () => {
						V.rulesAssistantAuto = 1;
					}), "note");
			} else {
				App.UI.DOM.appendNewElement("span", div,
					App.UI.DOM.passageLink("Stop applying Rules Assistant at week end", "Main", () => {
						V.rulesAssistantAuto = 0;
					}), "note");
			}
			div.append(" | ");
			if (DefaultRulesError()) {
				App.UI.DOM.appendNewElement("span", div, "WARNING: One or more rules' custom conditions has errors!",
					["note", "warning"]);
			}
			App.UI.DOM.appendNewElement("span", div,
				App.UI.DOM.passageLink("Re-apply Rules Assistant now (this will only check slaves in the Penthouse)",
					"Main", () => {
						for (const slave of V.slaves) {
							if (assignmentVisible(slave) && slave.useRulesAssistant === 1) {
								DefaultRules(slave);
							}
						}
					}), "note");
		}
		return div;
	}

	const fragment = document.createDocumentFragment();

	fragment.append(App.Reminders.list({maxFuture: 5, link: true}));

	if (V.seeFCNN === 1) {
		const div = document.createElement("div");
		div.classList.add("main-fcnn");
		div.append(App.MainView.fcnn(),
			App.UI.DOM.passageLink("Hide", passage(), () => { V.seeFCNN = 0; })
		);
		fragment.append(div);
	}

	fragment.append(mainMenu());

	fragment.append(App.UI.SlaveList.penthousePage());

	if (V.fucktoyInteractionsPosition === 0) {
		fragment.append(App.MainView.useFucktoys());
	}
	if (V.useSlaveSummaryOverviewTab === 0) {
		fragment.append(App.MainView.useGuard());
	}

	return fragment;
};


code:
/**
 * Basic application of scar
 * @param {App.Entity.SlaveState} slave
 * @param {string} scar
 * @param {string} design
 * @param {number} [weight]
 */
App.Medicine.Modification.addScar = function(slave, scar, design, weight) {
	/*
	V.scarApplied = 1;
	V.degradation += 10;
	surgeryDamage(slave, 10); // dangerous to uncomment this as sometimes many scars are applied at once.
	cashX(forceNeg(surgery.costs), "slaveSurgery", slave);
	surgeryDamage(slave, (V.PC.skill.medicine >= 100) ? Math.round(surgery.healthCosts / 2) : surgery.healthCosts);*/
	if (!weight) {
		weight = 1;
	}
	if (!slave.scar.hasOwnProperty(scar)) {
		slave.scar[scar] = new App.Entity.scarState();
	}
	if (!slave.scar[scar].hasOwnProperty(design)) {
		slave.scar[scar][design] = weight;
	} else {
		slave.scar[scar][design] += weight;
	}
};
/**
 * Basic application of scar
 * @param {App.Entity.SlaveState} slave
 * @param {string} scar
 * @param {string} design
 */
App.Medicine.Modification.removeScar = function(slave, scar, design) {
	/*
	V.scarApplied = 1;
	V.degradation += 10;
	surgeryDamage(slave, 10); //dangerous to uncomment this as sometimes many scars are applied at once.
	cashX(forceNeg(surgery.costs), "slaveSurgery", slave);
	surgeryDamage(slave, (V.PC.skill.medicine >= 100) ? Math.round(surgery.healthCosts / 2) : surgery.healthCosts);*/
	if (slave.scar.hasOwnProperty(scar)) { // if scar object exists for this body part
		if (slave.scar[scar].hasOwnProperty(design)) { // if object has this kind of scar (might be custom)
			if (["burn", "c-section", "chain", "cutting", "exotic", "generic", "menacing", "surgical", "whip"].includes(design)) {
				slave.scar[scar][design] = 0;
			} else {
				delete slave.scar[scar][design]; // scar was custom
			}
		}
		// remove the scar object entirely if no entry is scarred:
		let weights = Object.values(slave.scar[scar]);
		let total = 0;
		let i;
		for (i = 0; i < weights.length; i++) {
			total += weights[i];
		}
		if (total === 0) {
			delete slave.scar[scar];
		}
	}
};

/**
 * Slave is whipped over the entire body, and strains in manacles so much that the wrists and ankles scar if present.
 * @param {App.Entity.SlaveState} slave
 * @param {number} [weight]
 */
App.Medicine.Modification.addScourged = function(slave, weight) {
	let scarArray = ["left breast", "right breast", "back", "lower back", "left buttock", "right buttock"];
	let i = 0;
	// Whip
	if (getLeftArmID(slave) === 1) {
		scarArray.push("left upper arm");
	}
	if (getRightArmID(slave) === 1) {
		scarArray.push("right upper arm");
	}
	if (getLeftLegID(slave) === 1) {
		scarArray.push("left thigh");
	}
	if (getRightLegID(slave) === 1) {
		scarArray.push("right thigh");
	}

	for (i = 0; i < scarArray.length; i++) {
		App.Medicine.Modification.addScar(slave, scarArray[i], "whip", weight);
	}
	// Manacles
	scarArray = [];
	if (getLeftArmID(slave) === 1) {
		scarArray.push("left wrist");
	}
	if (getRightArmID(slave) === 1) {
		scarArray.push("right wrist");
	}
	if (getLeftLegID(slave) === 1) {
		scarArray.push("left ankle");
	}
	if (getRightLegID(slave) === 1) {
		scarArray.push("right ankle");
	}
	for (i = 0; i < scarArray.length; i++) {
		App.Medicine.Modification.addScar(slave, scarArray[i], "chain", weight);
	}
};

/**
 * Scars a slave over a large section of their body.
 * @param {App.Entity.SlaveState} slave
 * @param {string} location full, upper, lower, left or right
 * @param {string} type whip, burn, surgical, generic
 * @param {number} weight
 */
App.Medicine.Modification.addBulkScars = function(slave, location, type, weight) {
	let scarArray = [];

	/* Divide slave into quarters, and add each quarter as needed. */

	/* Top left */
	if (["full", "left", "upper"].includes(location)) {
		scarArray.push("left breast");
		if (getLeftArmID(slave) === 1) {
			scarArray.push("left upper arm", "left lower arm", "left hand");
		}
	}

	/* Top right */
	if (["full", "right", "upper"].includes(location)) {
		scarArray.push("right breast");
		if (getRightArmID(slave) === 1) {
			scarArray.push("right upper arm", "right lower arm", "right hand");
		}
	}

	/* Lower left */
	if (["full", "left", "lower"].includes(location)) {
		scarArray.push("left buttock");
		if (getLeftLegID(slave) === 1) {
			scarArray.push("left thigh", "left calf", "left foot");
		}
	}

	/* Lower Right */
	if (["full", "lower", "right"].includes(location)) {
		scarArray.push("right buttock");
		if (getRightLegID(slave) === 1) {
			scarArray.push("right thigh", "right calf", "right foot");
		}
	}

	/* Extra */
	if (["full", "upper"].includes(location)) {
		scarArray.push("back", "lower back");
	}

	let i = 0;
	for (i = 0; i < scarArray.length; i++) {
		App.Medicine.Modification.addScar(slave, scarArray[i], type, weight);
	}
};

/**
 * Adds a piercing to a slave.
 * @param {App.Entity.SlaveState} slave
 * @param {string} location
 * @param {number} weight
 * @returns {string} slave reaction
 */
App.Medicine.Modification.setPiercing = function(slave, location, weight) {
	if (weight === 3) {
		// smart piercings cost more
		cashX(forceNeg(V.SPcost), "slaveMod", slave);
	} else {
		cashX(forceNeg(V.modCost), "slaveMod", slave);
	}

	// reaction
	const {He, he, His, his, him} = getPronouns(slave);
	const delta = weight - slave[`${location}Piercing`];
	let r = ` `;
	if (location === "tongue") {
		if (slave.fetishKnown && slave.fetish === "cumslut" && slave.fetishStrength > 10) {
			r += `Since ${he}'s an oral whore, ${he}'s `;
			if (delta > 0) {
				if (weight > 1) {
					r += `<span class="devotion inc">very happy</span> you took the time to personally pierce ${his} tongue so heavily. `;
				} else {
					r += `<span class="devotion inc">happy</span> to get a tongue piercing from you personally. `;
				}
				r += `After all, ${he}'s pretty sure it's a sign there will be plenty of ${V.PC.dick > 0 ? `dick for ${him} to suck` : `pussy for ${him} to eat`} in the future. `;
				slave.devotion += 2 * delta;
			} else if (slave.devotion <= 95) {
				r += `<span class="devotion dec">saddened</span> you decided to take out ${his} tongue piercings. ${He}'s afraid this means something about ${his} future as a suck slut. `;
				slave.devotion += 2 * delta;
			} else {
				r += `disappointed to lose ${his} tongue piercings, but accepts your will for ${his} body. `;
			}
		}
		if (slave.sexualFlaw === "hates oral" && weight > 0) {
			r += `${He} has trouble thinking of ${his} mouth as a warm, wet, playful hole, so ${his} new piercings will be therapeutic. Having something to suck on at all times should force ${him} past ${his} hang-ups. `;
		}
	} else if (location === "nipples" || location === "areolae") {
		if (slave.fetishKnown && slave.fetish === "boobs" && slave.fetishStrength > 10) {
			r += `Since ${he}'s fixated on ${his} boobs, ${he}'s `;
			if (delta > 0) {
				if (weight > 1) {
					r += `<span class="devotion inc">very happy</span> you took the time to personally pierce them so heavily. `;
				} else {
					r += `<span class="devotion inc">happy</span> to get them pierced by you personally. `;
				}
				r += `As far as ${he}'s concerned, more attention on ${his} tits is always good. `;
				slave.devotion += 2 * delta;
			} else if (slave.devotion <= 95) {
				r += `<span class="devotion dec">saddened</span> you decided to take out ${his} piercings. ${He} doesn't like anything that makes ${his} tits less distinctive. `;
				slave.devotion += 2 * delta;
			} else {
				r += `disappointed to lose ${his} the piercings in ${his} ${location}, but accepts your will for ${his} body. `;
			}
		}
	} else if (location === "corset") {
		if (slave.fetishKnown && slave.fetish === "masochist" && slave.fetishStrength > 10) {
			r += `Since ${he}'s a pain whore `;
			if (delta > 0) {
				r += `${he} <span class="devotion inc">got off</span> on you giving ${him} a corset piercing personally. ${He} loved the feeling of the metal entering ${his} flesh. `;
				slave.devotion += 2 * delta;
			} else if (slave.devotion <= 95) {
				r += `<span class="devotion dec">saddened</span> you decided to take out ${his} corset piercings. ${He}'ll miss the constant slight discomfort they gave ${him}. `;
				slave.devotion += 2 * delta;
			} else {
				r += `disappointed to lose ${his} corset piercings, but accepts your will for ${his} body. `;
			}
		}
	} else if (location === "vagina") {
		if (slave.fetishKnown && slave.fetish === "pregnancy" && slave.fetishStrength > 10) {
			r += `Since ${he}'s a whore for impregnation, ${he}'s `;
			if (delta > 0) {
				if (weight > 1) {
					r += `<span class="devotion inc">very happy</span> you took the time to personally pierce ${his} pussylips so heavily. `;
				} else {
					r += `<span class="devotion inc">happy</span> to get pussy piercings from you personally. `;
				}
				r += `Like most pregnancy fetishists, ${he}'s a little desperate for dick, and anything that points towards more bareback sex is appealing to ${him}. `;
				slave.devotion += 2 * delta;
			} else if (slave.devotion <= 95) {
				r += `saddened you decided to take out ${his} pussy piercings. Like most pregnancy fetishists, ${he}'s a little desperate for dick, and ${he}'s afraid of anything that points towards less attention focused on ${his} cunt. `;
				slave.devotion += 2 * delta;
			} else {
				r += `disappointed to lose ${his} pussy piercings, but accepts your will for ${his} body. `;
			}
		}
		if (slave.sexualFlaw === "hates penetration" && weight > 0) {
			r += `${He} has trouble thinking of ${his} vagina as a fuckhole rather than something special to protect and cherish, so ${his} new piercings will be therapeutic. The constant, inescapable stimulation of ${his} labia should force ${him} past ${his} hang-ups. `;
		}
	} else if (location === "dick") {
		if (slave.fetishKnown && slave.fetish === "pregnancy" && slave.fetishStrength > 10) {
			r += `Since ${he}'s fixated on using ${his} cock to impregnate other slaves, ${he}'s `;
			if (delta > 0) {
				if (weight > 1) {
					r += `<span class="devotion inc">very happy</span> you took the time to personally stick so much metal into ${his} dick`;
					if (slave.scrotum > 0) {
						r += ` and ballsack`;
					}
					r += `. `;
				} else {
					r += `<span class="devotion inc">happy</span> to get penis piercings from you personally. `;
				}
				r += `${He}'s extremely proud of ${his} newly decorated member. `;
				slave.devotion += 2 * delta;
			} else if (slave.devotion <= 95) {
				r += `saddened you decided to take out ${his} dick piercings. ${He} passionately loves using ${his} penis, and ${he}'s concerned you're planning to let ${him} do less of that. `;
				slave.devotion += 2 * delta;
			} else {
				r += `disappointed to lose ${his} dick piercings, but accepts your will for ${his} body. `;
			}
		}
	} else if (location === "anus") {
		if (slave.fetishKnown && slave.fetish === "buttslut" && slave.fetishStrength > 10) {
			r += `Since ${he}'s an anal slut, ${he}'s `;
			if (delta > 0) {
				if (weight > 1) {
					r += `<span class="devotion inc">very happy</span> you took the time to personally pierce the entire area around ${his} whorish asspussy. `;
				} else {
					r += `<span class="devotion inc">happy</span> to get a piercing next to ${his} favorite hole from you personally. `;
				}
				r += `${He} can't wait to find out how the metal will feel against ${his} butthole when ${he} moves around. `;
				slave.devotion += 2 * delta;
			} else if (slave.devotion <= 95) {
				r += `saddened you decided to take out ${his} backdoor piercings. ${He} will miss the constant anal titillation, and ${he}'s worried this means you're becoming less interested in ${his} butthole. `;
				slave.devotion += 2 * delta;
			} else {
				r += `disappointed to lose ${his} backdoor piercings, but accepts your will for ${his} body. `;
			}
		}
		if (slave.sexualFlaw === "hates anal" && weight > 0) {
			r += `${He} has trouble thinking of ${his} asshole as a sexy, fuckable hole, so ${his} new piercings will be therapeutic. The constant tickling back there should force ${him} past ${his} hang-ups. `;
		}
	}

	if (slave.genes === "XY" && slave.attrXY <= 35 && ["ear", "eyebrow", "lips", "navel", "nose"].contains(location)) {
		r += `${His} girly new `;
		switch (location) {
			case "ear":
				r += `pierced ears should help ${him} a little with ${his} issues about ${his} self-image as a fuckable slave girl. `;
				break;
			case "lips":
				r += `facial piercing, just below ${his} pretty mouth, should help ${him} get used to the idea of it being a warm, wet hole for cocks. `;
				break;
			case "nose":
				r += `nose piercing should make ${his} reflection in the mirror seem just a bit more fuckably feminine to ${him}. `;
				break;
			case "navel":
				r += `navel piercing should help ${him} see ${his} naked body in a submissively sexual way, especially as ${he} turns to hide it and present ${his} asspussy. `;
				break;
			case "eyebrow":
				r += `facial piercing should help destroy ${his} vestiges of sexual identity outside of ${his} life as a hole for cocks. `;
				break;
			default:
				r += `set of slutty facial piercings should help ${him} get used to the idea of being a girly slave meant to please dicks. `; /* impossible, but I'm leaving the text anyway */
		}
		if (slave.devotion < -20) {
			r += `Unfortunately, that positive effect will have to wait until ${he}'s a little less resistant to the idea of being a sex slave. `;
		}
	}

	slave[`${location}Piercing`] = weight;
	return r;
};

/**
 * Adds a tattoo to a slave.
 * @param {App.Entity.SlaveState} slave
 * @param {string} location
 * @param {string|number} design (0 removes)
 * @returns {string} slave reaction
 */
App.Medicine.Modification.setTattoo = function(slave, location, design) {
	cashX(forceNeg(V.modCost), "slaveMod", slave);

	// reaction
	const {He, he, His, his, him} = getPronouns(slave);
	const {s, ss, x, c, say, title: Master} = getEnunciation(slave);
	let r = ` `;
	if (location === "anus" && design !== 0) {
		if (canSee(slave) && canTalk(slave)) {
			const anus = (function(s) {
				if (s.anus > 3) {
					return "anal gape";
				} else if (s.anus === 3) {
					return "relaxed asspussy";
				} else if (s.anus === 2) {
					return "soft butthole";
				} else {
					return "tight butthole";
				}
			})(slave);

			if (design === "bleached") {
				r += `${He} knew ${he} was getting ${his} ass bleached, of course. The stinging sensation was hard to miss. Even so, ${he} cranes ${his} neck around as soon as possible to look at ${his} ${anus} in the mirror. `;
			} else {
				r += `${He} knew ${he} was getting ${his} asshole tattooed, of course. ${He} cranes ${his} neck around as soon as possible to look at ${his} ${anus} in the mirror. `;
				if (slave.butt > 5) {
					r += `${His} huge buttocks are still held apart by the spreaders used to give the tattooing actuators access to ${his} most intimate area, giving ${him} a good view. `;
				}
			}
			if (slave.fetishKnown && slave.fetish === "buttslut") {
				switch (design) {
					case "bleached":
						r += `${He} giggles at ${his} crinkled fuckhole's clean, fuckable appearance, and starts squeezing ${his} sphincter muscles experimentally. "Thank you, ${Master}," ${he} gasps, flushing.`;
						break;
					case "flowers":
					case "tribal patterns":
						r += `${He} gasps at the beautiful design and then suddenly bursts into tears. "T-thank you, ${Master}," ${he} blubbers. "I'm ${s}-${s}uch an anal whore."`;
						break;
					case "advertisements":
						r += `${He} stares, ${his} mouth working as ${he} spells out `;
						if (slave.vagina > -1) {
							r += `'Fucktoy's Favorite Fuckhole.' ${He} smiles naughtily. "${Master}, it sure i${s}, `;
						} else {
							r += `'Spit First.' ${He} smiles naughtily. "${Master}, that work${s} for me. Lube'${s} good too. Or, um, dry'${s} fine too," `;
						}
						r += `${he} giggles.`;
						break;
					case "rude words":
						r += `${He} stares, ${his} mouth working as ${he} spells out `;
						if (slave.vagina > -1) {
							r += `'Fucktoy's Favorite Fuckhole.' ${He} smiles naughtily. "${Master}, it sure i${s}, `;
						} else {
							r += `'Sissy Slut's Sodomy Slit.' ${He} smiles naughtily. "${Master}, I love it when you fuck my lewd butt ${s}lit," `;
						}
						r += `${he} giggles.`;
						break;
					case "sacrilege":
						r += `${He} stares, ${his} mouth working as ${he} reads the ring of Aramaic blasphemy around ${his} hole. Getting it, ${he} smiles naughtily. "${Master}, I'm a horrible little butthole ${s}uccubu${s}. Plea${s}e, ${Master}, feed me. I need cum in my a${ss} to ${s}urvive."`;
						break;
					case "sacrament":
						r += `${He} stares, ${his} mouth working as ${he} reads the ring of Aramaic around ${his} hole. Getting it, ${he} smiles beatifically. "${Master}, I alway${s} knew butt${s}e${x} was a ${s}acrament."`;
						break;
					case "degradation":
						r += `${He} stares, ${his} mouth working as ${he} spells out '${WrittenMaster(slave)} Made Me An Anal Slut.' ${He} smiles naughtily. "Oh ${Master}, you did," ${he} ${say}s gratefully. "And I love you for it."`;
						break;
					case "counting":
						r += `${He} stares, realizing that the markings denote how many times ${he}'s been sodomized. "${Master}, thank you, and, and, I hope I'll have to come back in here a lot," ${he} ${say}s. "Um, becau${s}e I have to have that updated. Becau${s}e I'm being fucked in the butt a lot," ${he} adds, wanting to be completely clear.`;
						break;
					case "bovine patterns":
						r += `${He} stares at ${his} new cow's anus. Finally, ${he} focuses on the upsides of being permanently transformed like this. "${Master}, I hope thi${s} mean${s} I'll have to be fed up the butt a lot," ${he} ${say}s, referencing the milking systems' anal feeders.`;
						break;
					case "possessive":
						r += `${He} stares, realizing that the ring of numbers around ${his} anus give the date ${he} was enslaved by you. "Be${s}t pla${c}e for that date, ${Master}," ${he} ${say}s. "Being a butt ${s}lut i${s} my life now."`;
						break;
					case "paternalist":
						r += `${He} stares, ${his} mouth working as ${he} spells out '${WrittenMaster(slave)} Helped Me Love Anal.' "Oh ${Master}, you did," ${he} coos. "You did! Thank you ${s}o much for helping me become the anal ${s}lut I wa${s} alway${s} meant to be!"`;
						break;
					default:
						r += `${He} smiles, pleased at the attention ${his} ${anus} is getting.`;
				}
			} else if (slave.devotion > 50) {
				switch (design) {
					case "bleached":
						r += `${He} gasps at how closely the crinkled skin around ${his} anal opening matches the rest of ${his} ${slave.skin} body. "Oh, it'${s} ${s}o beautiful! Thank you, ${Master}, thank you," ${he} exclaims lovingly.`;
						break;
					case "flowers":
					case "tribal patterns":
						r += `${He} gasps at the beautiful design. "It'${s} ${s}o pretty, ${Master}," ${he} ${say}s. "Thank you. I love you ${s}o much!"`;
						break;
					case "advertisements":
					case "degradation":
					case "paternalist":
					case "possessive":
					case "rude words":
					case "sacrament":
					case "sacrilege":
						r += `${He} stares, ${his} mouth working as ${he} spells out the words tattooed in a ring around ${his} asshole. "Thank you, ${Master}," ${he} ${say}s devotedly. "I love you and I'll ${s}erve you with my butthole a${s} be${s}t I can."`;
						break;
					default:
						r += `${He} smiles, pleased at the mark of your attention to ${his} body. "Thank you, ${Master}," ${he} ${say}s devotedly. "I love you and I'll ${s}erve you with my butthole a${s} be${s}t I can."`;
				}
			} else if (slave.sexualFlaw === "hates anal") {
				r += `${He} stares, `;
				switch (design) {
					case "bleached":
						r += `horrified, and then shudders convulsively, but can't look away. `;
						break;
					case "flowers":
					case "tribal patterns":
						r += `torn between fascination at the intricate design and horror at having ${his} anus permanently tattooed. `;
						break;
					case "advertisements":
					case "degradation":
					case "paternalist":
					case "possessive":
					case "rude words":
					case "sacrament":
					case "sacrilege":
						r += `${his} mouth working as ${he} spells out the words tattooed in a ring around ${his} asshole. ${He} starts to cry as their meaning sinks in. `;
						break;
					default:
						r += `lower lip quivering. `;
				}
				r += `${He} hates being assraped, and knows that this means there's lots of it in store for ${him}. "${Master}, p-plea${s}e," ${he} moans halfheartedly. "I d-don't like being b-butt f-fucked."`;
			} else if (slave.devotion > 20) {
				r += `${He} stares, `;
				switch (design) {
					case "bleached":
						r += `fascinated. ${He} doesn't seem sure what to make of this. `;
						break;
					case "flowers":
					case "tribal patterns":
						r += `torn between admiration at the intricate design and realization that ${his} anus has been permanently tattooed. `;
						break;
					case "advertisements":
					case "degradation":
					case "paternalist":
					case "possessive":
					case "rude words":
					case "sacrament":
					case "sacrilege":
						r += `${his} mouth working as ${he} spells out the words tattooed in a ring around ${his} asshole. ${He} shudders a little as they sink in and ${he} internalizes what they say about ${him} and ${his} asshole. `;
						break;
					default:
						r += `realizing that ${his} anus has been permanently tattooed. It's yet another proof that ${he}'s permanently a sex slave, and ${his} anus is for fucking. `;
				}
				r += `"Thank you, ${Master}," ${he} ${say}s, taking refuge in propriety. "Um, d-doe${s} thi${s} mean I'm going to be more of a, um, b-butt${s}lut?" ${he} asks hesitantly, not sounding very enthusiastic.`;
			} else if (slave.trust < -20) {
				r += `${He} stares, `;
				switch (design) {
					case "bleached":
						r += `trying to process the appearance of what must seem like an unnaturally clean-looking hole. `;
						break;
					case "flowers":
					case "tribal patterns":
						r += `struggling to process the fact that one of ${his} most intimate areas has been tattooed. Beautifully tattooed, but still." `;
						break;
					case "advertisements":
					case "degradation":
					case "paternalist":
					case "possessive":
					case "rude words":
					case "sacrament":
					case "sacrilege":
						r += `${his} mouth working as ${he} spells out the words tattooed in a ring around ${his} asshole. ${He} shudders as ${he} does ${his} best to conceal ${his} feelings about such a permanent display of ${his} status as an anal slave. `;
						break;
					default:
						r += `looking sad as the meaning of a permanent display of ${his} status as an anal slave sinks in. `;
				}
				r += `After a short pause ${he} remembers ${his} duties and stammers, "T-thank you, ${Master}." After a while longer ${he} turns ${his} head away, clearly not wanting to dwell on how ${his} anus is obviously being beautified to attract sexual attention.`;
			} else {
				switch (design) {
					case "bleached":
						r += `${His} face darkens as ${he} sees ${his} crinkled fuckhole's clean, fuckable appearance. "Don't you dare fucking a${ss}rape me!" ${he} bursts out.`;
						break;
					case "flowers":
					case "tribal patterns":
						r += `${He}'s momentarily surprised by the pretty design, but then ${his} face darkens. "That'${s} permanent!" ${he} bursts out. "My a${ss}hole i${s} going to look like that forever!"`;
						break;
					case "advertisements":
						r += `${He} stares, ${his} mouth working as ${he} spells out `;
						if (slave.vagina > -1) {
							r += `'Second Pussy.' ${His} face darkens. "No it'${s} not," ${he} bursts out. "Anal is dirty."`;
						} else {
							r += `'Spit First.' ${His} face darkens. "Gro${ss}," ${he} bursts out. "Gro${ss} and wrong!"`;
						}
						break;
					case "rude words":
						r += `${He} stares, ${his} mouth working as ${he} spells out `;
						if (slave.vagina > -1) {
							r += `'Fucktoy's Favorite Fuckhole.' ${His} face darkens. "No it'${s} not," ${he} bursts out. "It'${s} my a${ss}!"`;
						} else {
							r += `'Sissy Slut's Sodomy Slit.' ${His} face darkens. "No it'${s} not," ${he} bursts out. "I'm not a ${s}i${ss}y! Or a ${s}lut! And it'${s} not a ${s}lit!" Those are debatable, but they can all change, of course.`;
						}
						break;
					case "sacrament":
					case "sacrilege":
						r += `${He} stares, ${his} mouth working as ${he} reads the ring of Aramaic around ${his} hole. ${He} doesn't understand it, but ${he}'s not happy about having stuff permanently inscribed around ${his} ass.`;
						break;
					case "degradation":
						r += `${He} stares, ${his} mouth working as ${he} spells out '${WrittenMaster(slave)} Made Me An Anal Slut.' ${His} face darkens. "No I'm not," ${he} bursts out. That remains to be seen.`;
						break;
					case "counting":
						r += `${He} stares, realizing that the markings denote how many times ${he}'s been sodomized. ${His} face darkens. "I ${s}ee there'${s} room for more," ${he} bursts out. "Fucking gro${ss}."`;
						break;
					case "bovine patterns":
						r += `${He} stares at ${his} new cow's anus. "Fucking gro${ss}," ${he} bursts out. "I'm not an animal." That remains to be seen.`;
						break;
					case "possessive":
						r += `${He} stares, realizing that the ring of numbers around ${his} anus give the date ${he} was enslaved by you. ${His} face darkens. "Wor${s}t day of my life," ${he} bursts out.`;
						break;
					case "paternalist":
						r += `${He} stares, ${his} mouth working as ${he} spells out '${WrittenMaster(slave)} Helped Me Love Anal.' "I don't," ${he} bursts out. "It'${s} fucking gro${ss}."`;
						break;
					default:
						r += `${He} stares, horrified that you have defaced ${his} body in this way.`;
				}
			}
		}
	}
	// TODO: maybe some reactions for other kinds of tattoos would be nice too?

	slave[`${location}Tat`] = design;
	return r;
};