code:
App.Update.Slave = function(slave, genepool = false) {
	const quirks = {};
	App.Data.geneticQuirks.forEach((value, q) => quirks[q] = 0);
	slave.geneticQuirks = Object.assign(clone(quirks), slave.geneticQuirks);

	if (slave.earShape === undefined) { slave.earShape = "normal"; }
	if (slave.earT === undefined) { slave.earT = "none"; }
	if (slave.earTColor === undefined) { slave.earTColor = "hairless"; }
	if (slave.horn === undefined) { slave.horn = "none"; }
	if (slave.hornColor === undefined) { slave.hornColor = "none"; }
	if (slave.tail === undefined) { slave.tail = "none"; }
	if (slave.tailShape === undefined) { slave.tailShape = "none"; }
	if (slave.tailColor === undefined) { slave.tailColor = "none"; }
	if (slave.appendages === undefined) { slave.appendages = "none"; }
	if (slave.wingsShape === undefined) { slave.wingsShape = "none"; }
	if (slave.appendagesColor === undefined) { slave.appendagesColor = "none"; }
	if (slave.hEffect === undefined) { slave.hEffect = "none"; }
	if (slave.hEffectColor === undefined) { slave.hEffectColor = "none"; }
	if (slave.tailEffect === undefined) { slave.tailEffect = "none"; }
	if (slave.tailEffectColor === undefined) { slave.tailEffectColor = "none"; }
	if (slave.appendagesEffect === undefined) { slave.appendagesEffect = "none"; }
	if (slave.appendagesEffectColor === undefined) { slave.appendagesEffectColor = "none"; }
	if (slave.prostateImplant !== undefined) {
		if (slave.prostateImplant === 1) {
			slave.prostate = 3;
		}
		delete slave.prostateImplant;
	}
	if (slave.daughters === undefined) { slave.daughters = 0; }
	if (slave.sisters === undefined) { slave.sisters = 0; }
	if (slave.pregGenerator !== undefined) { delete slave.pregGenerator; }
	if (slave.pregAdaptation === undefined) {
		if (slave.physicalAge <= 3) {
			slave.pregAdaptation = 10;
		} else if (slave.physicalAge <= 12 || slave.genes === "XY") {
			slave.pregAdaptation = 20;
		} else if (slave.physicalAge <= 17) {
			slave.pregAdaptation = 30;
		} else {
			slave.pregAdaptation = 50;
		}
	}
	if (slave.pregKnown === undefined) {
		if (slave.preg > 0) {
			slave.pregKnown = 1;
		} else {
			slave.pregKnown = 0;
		}
	}
	if (slave.pregWeek === undefined) {
		if (slave.preg > 0) {
			slave.pregWeek = slave.preg;
		} else {
			slave.pregWeek = 0;
		}
	}
	if (slave.pubertyXX === undefined) {
		if (slave.physicalAge >= slave.pubertyAgeXX) {
			slave.pubertyXX = 1;
			slave.fertKnown = 1;
		} else {
			slave.pubertyXX = 0;
			slave.fertKnown = 0;
		}
	}
	if (slave.pubertyXY === undefined) {
		if (slave.physicalAge >= slave.pubertyAgeXY) {
			slave.pubertyXY = 1;
		} else {
			slave.pubertyXY = 0;
		}
	}
	if (slave.genetics !== undefined) { delete slave.genetics; }
	slave.geneMods = Object.assign({NCS: 0, rapidCellGrowth: 0, immortality: 0}, slave.geneMods);
	if (slave.inducedNCS !== undefined) {
		slave.geneMods.NCS = slave.inducedNCS;
		delete slave.inducedNCS;
	}
	if (slave.PCSlutContacts !== undefined) { delete slave.PCSlutContacts; }
	if (slave.wombImplant === undefined) { slave.wombImplant = "none"; }
	if (slave.superfetation !== undefined) { delete slave.superfetation; }
	if (slave.lactationDuration === undefined) {
		if (slave.lactation === 0) {
			slave.lactationDuration = 0;
		} else {
			slave.lactationDuration = 2;
		}
	}
	if (slave.induceLactation === undefined) { slave.induceLactation = 0; }
	if (slave.weightDirection === undefined) { slave.weightDirection = 0; }
	if (slave.clone === undefined) { slave.clone = 0; }
	if (slave.abortionTat === undefined) { slave.abortionTat = -1; }
	if (slave.birthsTat === undefined) { slave.birthsTat = -1; }
	if (slave.reservedChildren !== undefined) { delete slave.reservedChildren; }
	if (slave.origin !== undefined && slave.origin !== 0) { slave.origin = pronounReplacer(slave.origin); }
	if (slave.custom !== undefined) {
		if (slave.custom.desc !== undefined && slave.custom.desc !== "") {
			slave.custom.desc = pronounReplacer(slave.custom.desc);
		}
		if (slave.custom.tattoo !== undefined && slave.custom.tattoo !== "") {
			slave.custom.tattoo = pronounReplacer(slave.custom.tattoo);
		}
	}
	if (slave.prestigeDesc !== undefined && slave.prestigeDesc !== 0) { slave.prestigeDesc = pronounReplacer(slave.prestigeDesc); }
	if (slave.pornPrestigeDesc !== undefined && slave.pornPrestigeDesc !== 0) { // This must be defined first, hence the previous line.
		if (V.releaseID < 1050 && slave.prestigeDesc !== undefined && slave.prestigeDesc !== 0) { /* BC absolutely FUCKED this */
			const genre = App.Porn.getGenreByFameName(slave.pornFameType);
			if (jsDef(genre)) {
				if (slave.pornPrestige === 1) {
					slave.pornPrestigeDesc = `$He has a following in slave pornography. ${genre.prestigeDesc1}.`;
				} else if (slave.pornPrestige === 2) {
					slave.pornPrestigeDesc = `$He is well known from $his career in slave pornography. ${genre.prestigeDesc2}.`;
				} else if (slave.pornPrestige === 3) {
					slave.pornPrestigeDesc = `$He is world famous for $his career in slave pornography. ${genre.prestigeDesc3}.`;
				} else {
					slave.pornPrestigeDesc = 0;
				}
			} else {
				slave.pornPrestigeDesc = 0;
			}
		} else {
			slave.pornPrestigeDesc = pronounReplacer(slave.pornPrestigeDesc);
		}
	}

	if (slave.amp !== undefined) {
		if (slave.amp === 1) {
			slave.arm = {left: null, right: null};
			slave.leg = {left: null, right: null};
		} else {
			const newID = ((slave.amp * -1) + 1);
			slave.arm = {
				left: new App.Entity.LimbState(),
				right: new App.Entity.LimbState()
			};
			slave.leg = {
				left: new App.Entity.LimbState(),
				right: new App.Entity.LimbState()
			};
			slave.arm.left.type = newID;
			slave.arm.right.type = newID;
			slave.leg.left.type = newID;
			slave.leg.right.type = newID;
			/* no need to check partial amputation, since it is not possible to create prior to this */
		}
		delete slave.amp;
		delete slave.missingLegs;
		delete slave.missingArms;
	} else if (slave.arm === undefined) {
		slave.arm = {
			left: new App.Entity.LimbState(),
			right: new App.Entity.LimbState()
		};
		slave.leg = {
			left: new App.Entity.LimbState(),
			right: new App.Entity.LimbState()
		};
	}

	if (hasAnyProstheticLimbs(slave)) {
		slave.PLimb = 1;
		if (getLimbCount(slave, 6) > 0) {
			slave.PLimb = 2;
		}
	}

	if (slave.eyeball !== undefined) { delete slave.eyeball; }

	if (slave.auricle !== undefined) { delete slave.auricle; }

	if (slave.readyProsthetics === undefined) { slave.readyProsthetics = []; }
	if (slave.readyLimbs !== undefined) {
		for (let k = 0; k < slave.readyLimbs.length; k++) {
			switch (slave.readyLimbs[k].type) {
				case -1:
					addProsthetic(slave, "basicL");
					break;
				case -2:
					addProsthetic(slave, "sexL");
					break;
				case -3:
					addProsthetic(slave, "beautyL");
					break;
				case -4:
					addProsthetic(slave, "combatL");
					break;
				case -5:
					addProsthetic(slave, "cyberneticL");
					break;
			}
		}
	}

	if (slave.hStyle === "Salon") { slave.hStyle = "trimmed"; }

	if (!App.Medicine.Modification.Color.Primary.find(c => c.value === slave.hColor)) {
		const colorExtract = (/with (.*?) highlights/g).exec(slave.hColor);
		slave.hEffectColor = colorExtract ? colorExtract[1] : "none";
		slave.hEffect = `${slave.hEffectColor} highlights`;
		slave.hColor = slave.hColor.replace(/ with.*/g, "");
	}

	if (V.releaseID < 1052) {
		const prosthetics = slave.readyProsthetics;
		slave.readyProsthetics = [];
		for (const p of prosthetics) {
			addProsthetic(slave, p.id);
		}
	}

	if (V.releaseID < 1058) {
		if (slave.albinism === 2) {
			let temp;
			temp = slave.origSkin;
			slave.origSkin = slave.albinismOverride.skin;
			slave.albinismOverride.skin = temp;
			temp = slave.origEye;
			slave.origEye = slave.albinismOverride.eyeColor;
			slave.albinismOverride.eyeColor = temp;
			temp = slave.origHColor;
			slave.origHColor = slave.albinismOverride.hColor;
			slave.albinismOverride.hColor = temp;
		}
	}

	if (V.releaseID < 1059 || !(slave.eye)) {
		slave.eye = new App.Entity.EyeState();
		const origEye = slave.origEye || "brown";
		setGeneticEyeColor(slave, origEye);
		if (slave.eyes === -4) {
			eyeSurgery(slave, "both", "remove");
		} else {
			if (slave.eyesImplant === 1) {
				eyeSurgery(slave, "both", "cybernetic");
			}
			if (slave.eyes === -3) {
				eyeSurgery(slave, "both", "glass");
			} else if (slave.eyes === -2) {
				eyeSurgery(slave, "both", "blind");
			} else if (slave.eyes === -1) {
				eyeSurgery(slave, "both", "blur");
			}
			const eyeColor = slave.eyeColor || "brown";
			const pupil = slave.pupil || "circular";
			const sclerae = slave.sclerae || "white";
			setEyeColorFull(slave, eyeColor, pupil, sclerae, "both");
			if (typeof slave.geneticQuirks.heterochromia === "string") {
				setEyeColor(slave.geneticQuirks.heterochromia, "left");
			}
		}
	}

	if (slave.eyes !== undefined) { delete slave.eyes; }
	if (slave.eyeColor !== undefined) { delete slave.eyeColor; }
	if (slave.eyesImplant !== undefined) { delete slave.eyesImplant; }
	if (slave.origEye !== undefined) { delete slave.origEye; }
	if (slave.pupil !== undefined) { delete slave.pupil; }
	if (slave.sclerae !== undefined) { delete slave.sclerae; }

	if (slave.origin === "Shortly after birth, $he was sealed in an aging tank until $he was of age. $He knows only of the terror that awaits $him should $he not obey $his master.") {
		slave.tankBaby = 2;
	} else if (slave.origin === "Shortly after birth, $he was sealed in an aging tank until $he was of age. $He knows nothing of the world outside of what the tank imprinted $him with.") {
		slave.tankBaby = 1;
	} else if (slave.tankBaby === undefined) {
		slave.tankBaby = 0;
	}

	if (slave.origin === "$He sold $himself into slavery to feed $himself and $his growing brood.") {
		if (slave.pregAdaptation < 750) {
			slave.pregAdaptation = 750;
		}
	}

	let backwardsCompatibility;
	if (slave.rivalry !== 0) {
		backwardsCompatibility = V.slaveIndices[slave.rivalryTarget];
		if (backwardsCompatibility === undefined) {
			slave.rivalry = 0;
			slave.rivalryTarget = 0;
		}
	}
	if (slave.relationship > 0) {
		backwardsCompatibility = V.slaveIndices[slave.relationshipTarget];
		if (backwardsCompatibility === undefined) {
			slave.relationship = 0;
			slave.relationshipTarget = 0;
		}
	}

	if (slave.race === "surgically altered to look amerindian") {
		slave.race = "amerindian";
	} else if (slave.race === "surgically altered to look asian") {
		slave.race = "asian";
	} else if (slave.race === "surgically altered to look black") {
		slave.race = "black";
	} else if (slave.race === "surgically altered to look indo-aryan") {
		slave.race = "indo-aryan";
	} else if (slave.race === "surgically altered to look latina") {
		slave.race = "latina";
	} else if (slave.race === "surgically altered to look malay") {
		slave.race = "malay";
	} else if (slave.race === "surgically altered to look middle eastern") {
		slave.race = "middle eastern";
	} else if (slave.race === "surgically altered to look mixed race" || slave.race === "surgically altered to look mixed") {
		slave.race = "mixed race";
	} else if (slave.race === "surgically altered to look pacific islander" || slave.race === "surgically altered to look like a pacific islander") {
		slave.race = "pacific islander";
	} else if (slave.race === "surgically altered to look southern european" || slave.race === "southern European") {
		slave.race = "southern european";
	} else if (slave.race === "surgically altered to look semitic") {
		slave.race = "semitic";
	} else if (slave.race === "surgically altered to look white") {
		slave.race = "white";
	}

	/* eslint-disable camelcase */
	if (slave.override_Race === undefined) { slave.override_Race = 0; }
	if (slave.override_Skin === undefined) { slave.override_Skin = 0; }
	if (slave.override_Eye_Color === undefined) { slave.override_Eye_Color = 0; }
	if (slave.override_H_Color === undefined) { slave.override_H_Color = 0; }
	if (slave.override_Pubic_H_Color === undefined) { slave.override_Pubic_H_Color = 0; }
	if (slave.override_Arm_H_Color === undefined) { slave.override_Arm_H_Color = 0; }
	/* eslint-enable camelcase */

	switch (slave.skin) {
		case "red dyed":
			slave.skin = "dyed red";
			break;
		case "green dyed":
			slave.skin = "dyed green";
			break;
		case "blue dyed":
			slave.skin = "dyed blue";
			break;
		case "tanned":
			slave.skin = "tan";
			break;
		case "lightened":
			slave.skin = "light";
			break;
		case "bronzed":
			slave.skin = "bronze";
			break;
		case "darkened":
			slave.skin = "dark";
			break;
	}

	switch (slave.origSkin) {
		case "blue dyed":
		case "dyed blue":
		case "dyed green":
		case "dyed red":
		case "green dyed":
		case "red dyed":
			slave.origSkin = randomRaceSkin(slave.origRace);
			break;
		case "tanned":
			slave.origSkin = "tan";
			break;
		case "lightened":
			slave.origSkin = "light";
			break;
		case "bronzed":
			slave.origSkin = "bronze";
			break;
		case "darkened":
			slave.origSkin = "dark";
			break;
	}

	if (slave.markings === "heavily") {
		slave.markings = "heavily freckled";
	} else if (slave.markings === "beauty") {
		slave.markings = "beauty mark";
	}

	if (slave.underArmHColor === "slave.hColor") {
		slave.underArmHColor = slave.hColor;
	}
	if (slave.eyebrowHColor === "slave.hColor") {
		slave.eyebrowHColor = slave.hColor;
	}

	if (slave.genes === undefined) {
		if (slave.ovaries === 1) {
			slave.genes = "XX";
		} else {
			slave.genes = "XY";
		}
	}

	if (V.releaseID < 1000) {
		if (slave.face === -3) {
			slave.face = -100;
		} else if (slave.face === -2) {
			slave.face = -50;
		} else if (slave.face === -1) {
			slave.face = -20;
		} else if (slave.face === 0) {
			slave.face = 0;
		} else if (slave.face === 1) {
			slave.face = 20;
		} else if (slave.face === 2) {
			slave.face = 50;
		} else {
			slave.face = 100;
		}
	}
	if (V.releaseID < 1031) {
		if (slave.intelligence === -3) {
			slave.intelligence = -100;
		} else if (slave.intelligence === -2) {
			slave.intelligence = -60;
		} else if (slave.intelligence === -1) {
			slave.intelligence = -30;
		} else if (slave.intelligence === 0) {
			slave.intelligence = 0;
		} else if (slave.intelligence === 1) {
			slave.intelligence = 30;
		} else if (slave.intelligence === 2) {
			slave.intelligence = 60;
		} else {
			slave.intelligence = 99;
		}
		if (slave.intelligenceImplant === 1) {
			slave.intelligenceImplant = 30;
		}
	}

	if (slave.teeth === 0) {
		slave.teeth = "normal";
	} else if (slave.teeth === "straightening") {
		slave.teeth = "straightening braces";
	} else if (slave.teeth === "cosmetic") {
		slave.teeth = "cosmetic braces";
	}

	if (slave.areolaeShape === undefined) {
		if (slave.areolae === 4) {
			slave.areolaeShape = "heart";
			slave.areolae = 3;
		} else if (slave.areolae === 5) {
			slave.areolaeShape = "star";
			slave.areolae = 3;
		} else {
			slave.areolaeShape = "circle";
		}
	}

	if (V.releaseID < 1061) {
		if (slave.boobsImplantType === 1) {
			slave.boobsImplantType = "string";
		} else if (slave.boobsImplant >= 10000) {
			slave.boobsImplantType = "hyper fillable";
		} else if (slave.boobsImplant >= 2000) {
			slave.boobsImplantType = "advanced fillable";
		} else if (slave.boobsImplant >= 800) {
			slave.boobsImplantType = "fillable";
		} else if (slave.boobsImplant > 0) {
			slave.boobsImplantType = "normal";
		} else {
			slave.boobsImplantType = "none";
		}
		if (slave.buttImplantType === 1) {
			slave.buttImplantType = "string";
		} else if (slave.buttImplant > 7) {
			slave.buttImplantType = "hyper fillable";
		} else if (slave.buttImplant >= 5) {
			slave.buttImplantType = "advanced fillable";
		} else if (slave.buttImplant >= 3) {
			slave.buttImplantType = "fillable";
		} else if (slave.buttImplant > 0) {
			slave.buttImplantType = "normal";
		} else {
			slave.buttImplantType = "none";
		}
	}

	if (V.releaseID < 1059) {
		if (slave.eyeColor === undefined) {
			slave.eyeColor = slave.eyes;
			slave.eyes = 1;
			if (slave.eyeColor === undefined) {
				slave.eyeColor = "brown";
			}
		}

		if (slave.pupil === undefined) {
			if (slave.eyeColor === "catlike") {
				slave.pupil = "catlike";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "serpent-like") {
				slave.pupil = "serpent-like";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "devilish") {
				slave.pupil = "devilish";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "demonic") {
				slave.pupil = "demonic";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "hypnotic") {
				slave.pupil = "hypnotic";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "heart-shaped") {
				slave.pupil = "heart-shaped";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "wide-eyed") {
				slave.pupil = "wide-eyed";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "almond-shaped") {
				slave.pupil = "almond-shaped";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "bright") {
				slave.pupil = "bright";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "teary") {
				slave.pupil = "teary";
				slave.eyeColor = "brown";
			} else if (slave.eyeColor === "vacant") {
				slave.pupil = "vacant";
				slave.eyeColor = "brown";
			} else {
				slave.pupil = "circular";
			}
		}
	}

	if (slave.pitkills !== undefined) { delete slave.pitkills; }
	if (slave.penetrationCount !== undefined) { delete slave.penetrationCount; }
	if (slave.oralTotal !== undefined) { delete slave.oralTotal; }
	if (slave.vaginaCount !== undefined) { delete slave.vaginaCount; }

	if (((V.ver.startsWith("0.6") && !V.ver.startsWith("10.6")) || (V.ver.startsWith("0.7")) || (V.ver.startsWith("0.8"))) && (!V.ver.startsWith("0.8.9")) && (!V.ver.startsWith("0.8.10")) && (!V.ver.startsWith("0.8.11")) && (!V.ver.startsWith("0.8.12"))) {
		if (slave.attrXX === 2) {
			slave.attrXX = 90;
		} else if (slave.attrXX === 1) {
			slave.attrXX = 70;
		} else if (slave.attrXX === 0) {
			slave.attrXX = 50;
		} else if (slave.attrXX === -1) {
			slave.attrXX = 30;
		} else {
			slave.attrXX = 10;
		}
		if (slave.attrXY === 2) {
			slave.attrXY = 90;
		} else if (slave.attrXY === 1) {
			slave.attrXY = 70;
		} else if (slave.attrXY === 0) {
			slave.attrXY = 50;
		} else if (slave.attrXY === -1) {
			slave.attrXY = 30;
		} else {
			slave.attrXY = 10;
		}
	}

	if ((V.ver.startsWith("0.6") && !V.ver.startsWith("10.6")) || (V.ver.startsWith("0.7")) || (V.ver.startsWith("0.8"))) {
		if (slave.health <= -9) {
			slave.health = -90;
		} else if (slave.health <= -7) {
			slave.health = jsRandom(-89, -70);
		} else if (slave.health <= -5) {
			slave.health = jsRandom(-69, -50);
		} else if (slave.health <= -3) {
			slave.health = jsRandom(-49, -30);
		} else if (slave.health <= -1) {
			slave.health = jsRandom(-29, -10);
		} else if (slave.health <= 1) {
			slave.health = jsRandom(-9, 10);
		} else if (slave.health <= 3) {
			slave.health = jsRandom(11, 30);
		} else if (slave.health <= 5) {
			slave.health = jsRandom(31, 50);
		} else if (slave.health <= 7) {
			slave.health = jsRandom(51, 70);
		} else if (slave.health <= 8) {
			slave.health = jsRandom(71, 80);
		} else if (slave.health <= 9) {
			slave.health = jsRandom(81, 90);
		} else if (slave.health <= 10) {
			slave.health = jsRandom(91, 100);
		} else if (slave.health <= 15) {
			slave.health = jsRandom(101, 150);
		} else if (slave.health <= 20) {
			slave.health = jsRandom(151, 200);
		} else if (slave.health >= 50) {
			slave.health = 500;
		} else if (slave.health > 20) {
			slave.health = 205;
		}

		slave.devotion = (slave.devotion * 5);
		slave.oldDevotion = (slave.oldDevotion * 5);
		slave.trust = (slave.trust * 5);
		slave.oldTrust = (slave.oldTrust * 5);

		if (slave.fetishStrength === 0) {
			slave.fetishStrength = jsRandom(0, 60);
		} else if (slave.fetishStrength === 1) {
			slave.fetishStrength = jsRandom(61, 80);
		} else if (slave.fetishStrength === 2) {
			slave.fetishStrength = jsRandom(96, 100);
		}

		if (slave.weight === -3) {
			slave.weight = jsRandom(-110, -96);
		} else if (slave.weight === -2) {
			slave.weight = jsRandom(-95, -31);
		} else if (slave.weight === -1) {
			slave.weight = jsRandom(-30, -11);
		} else if (slave.weight === 0) {
			slave.weight = jsRandom(-11, 10);
		} else if (slave.weight === 1) {
			slave.weight = jsRandom(11, 30);
		} else if (slave.weight === 2) {
			slave.weight = jsRandom(31, 95);
		} else if (slave.weight === 3) {
			slave.weight = jsRandom(96, 110);
		}

		if (slave.lips !== 0) {
			if (slave.lips === 3) {
				slave.lips = 85;
			} else if (slave.lips === 2) {
				slave.lips = 55;
			} else if (slave.lips === 1) {
				slave.lips = 35;
			}
		}
	}

	if (((V.ver.startsWith("0.6") && !V.ver.startsWith("10.6")) || (V.ver.startsWith("0.7")) || (V.ver.startsWith("0.8")) || (V.ver.startsWith("0.9"))) && (!V.ver.startsWith("0.9.5")) && (!V.ver.startsWith("0.9.6")) && (!V.ver.startsWith("0.9.7")) && (!V.ver.startsWith("0.9.8")) && (!V.ver.startsWith("0.9.9")) && (!V.ver.startsWith("0.9.10"))) {
		if (slave.skill === undefined) {
			if (slave.oralSkill > 0) {
				if (slave.oralSkill === 3) {
					slave.oralSkill = 100;
				} else if (slave.oralSkill === 2) {
					slave.oralSkill = 65;
				} else {
					slave.oralSkill = 35;
				}
			}
			if (slave.vaginalSkill > 0) {
				if (slave.vaginalSkill === 3) {
					slave.vaginalSkill = 100;
				} else if (slave.vaginalSkill === 2) {
					slave.vaginalSkill = 65;
				} else {
					slave.vaginalSkill = 35;
				}
			}
			if (slave.analSkill > 0) {
				if (slave.analSkill === 3) {
					slave.analSkill = 100;
				} else if (slave.analSkill === 2) {
					slave.analSkill = 65;
				} else {
					slave.analSkill = 35;
				}
			}
			if (slave.whoreSkill > 0) {
				if (slave.whoreSkill === 3) {
					slave.whoreSkill = 100;
				} else if (slave.whoreSkill === 2) {
					slave.whoreSkill = 65;
				} else {
					slave.whoreSkill = 35;
				}
			}
			if (slave.entertainSkill > 0) {
				if (slave.entertainSkill === 3) {
					slave.entertainSkill = 100;
				} else if (slave.entertainSkill === 2) {
					slave.entertainSkill = 65;
				} else {
					slave.entertainSkill = 35;
				}
			}
		}

		if (V.ver !== "0.9.4") {
			slave.aphrodisiacs = 0;
			if (slave.drugs === "curatives") {
				slave.curatives = 2;
				slave.drugs = "no drugs";
			} else if (slave.drugs === "preventatives") {
				slave.curatives = 1;
				slave.drugs = "no drugs";
			} else if (slave.drugs === "aphrodisiacs") {
				slave.aphrodisiacs = 1;
				slave.drugs = "no drugs";
			} else if (slave.drugs === "extreme aphrodisiacs") {
				slave.aphrodisiacs = 2;
				slave.drugs = "no drugs";
			}
			if (slave.muscles >= 3) {
				slave.muscles = 100;
			} else if (slave.muscles >= 2) {
				slave.muscles = 50;
			} else if (slave.muscles >= 1) {
				slave.muscles = 20;
			} else {
				slave.muscles = 0;
			}
		}
	}

	switch (slave.boobsTat) {
		case "floral designs":
			slave.boobsTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.boobsTat = "rude words";
			break;
		case "lewd scenes":
			slave.boobsTat = "scenes";
			break;
		case "degrading language":
			slave.boobsTat = "degradation";
			break;
		case "slutty advertisements":
			slave.boobsTat = "advertisements";
			break;
	}

	switch (slave.buttTat) {
		case "floral designs":
			slave.buttTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.buttTat = "rude words";
			break;
		case "lewd scenes":
			slave.buttTat = "scenes";
			break;
		case "degrading language":
			slave.buttTat = "degradation";
			break;
		case "slutty advertisements":
			slave.buttTat = "advertisements";
			break;
	}

	switch (slave.vaginaTat) {
		case "floral designs":
			slave.vaginaTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.vaginaTat = "rude words";
			break;
		case "lewd scenes":
			slave.vaginaTat = "scenes";
			break;
		case "degrading language":
			slave.vaginaTat = "degradation";
			break;
		case "slutty advertisements":
			slave.vaginaTat = "advertisements";
			break;
	}

	switch (slave.dickTat) {
		case "floral designs":
			slave.dickTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.dickTat = "rude words";
			break;
		case "lewd scenes":
			slave.dickTat = "scenes";
			break;
		case "degrading language":
			slave.dickTat = "degradation";
			break;
		case "slutty advertisements":
			slave.dickTat = "advertisements";
			break;
	}

	switch (slave.anusTat) {
		case "floral designs":
			slave.anusTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.anusTat = "rude words";
			break;
		case "lewd scenes":
			slave.anusTat = "scenes";
			break;
		case "degrading language":
			slave.anusTat = "degradation";
			break;
		case "slutty advertisements":
			slave.anusTat = "advertisements";
			break;
	}

	switch (slave.backTat) {
		case "floral designs":
			slave.backTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.backTat = "rude words";
			break;
		case "lewd scenes":
			slave.backTat = "scenes";
			break;
		case "degrading language":
			slave.backTat = "degradation";
			break;
		case "slutty advertisements":
			slave.backTat = "advertisements";
			break;
	}

	switch (slave.shouldersTat) {
		case "floral designs":
			slave.shouldersTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.shouldersTat = "rude words";
			break;
		case "lewd scenes":
			slave.shouldersTat = "scenes";
			break;
		case "degrading language":
			slave.shouldersTat = "degradation";
			break;
		case "slutty advertisements":
			slave.shouldersTat = "advertisements";
			break;
	}

	switch (slave.armsTat) {
		case "floral designs":
			slave.armsTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.armsTat = "rude words";
			break;
		case "lewd scenes":
			slave.armsTat = "scenes";
			break;
		case "degrading language":
			slave.armsTat = "degradation";
			break;
		case "slutty advertisements":
			slave.armsTat = "advertisements";
			break;
	}

	switch (slave.legsTat) {
		case "floral designs":
			slave.legsTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.legsTat = "rude words";
			break;
		case "lewd scenes":
			slave.legsTat = "scenes";
			break;
		case "degrading language":
			slave.legsTat = "degradation";
			break;
		case "slutty advertisements":
			slave.legsTat = "advertisements";
			break;
	}

	switch (slave.stampTat) {
		case "floral designs":
			slave.stampTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.stampTat = "rude words";
			break;
		case "lewd scenes":
			slave.stampTat = "scenes";
			break;
		case "degrading language":
			slave.stampTat = "degradation";
			break;
		case "slutty advertisements":
			slave.stampTat = "advertisements";
			break;
	}

	switch (slave.lipsTat) {
		case "floral designs":
			slave.lipsTat = "flowers";
			break;
		case "demeaning inscriptions":
			slave.lipsTat = "rude words";
			break;
		case "lewd scenes":
			slave.lipsTat = "scenes";
			break;
		case "degrading language":
			slave.lipsTat = "degradation";
			break;
		case "slutty advertisements":
			slave.lipsTat = "advertisements";
			break;
	}

	if (slave.currentRules === undefined || slave.currentRules.length < 1) {
		slave.currentRules = [];
	}

	if (slave.height < -1) {
		slave.height = jsRandom(140, 149);
	} else if (slave.height < 0) {
		slave.height = jsRandom(150, 159);
	} else if (slave.height < 1) {
		slave.height = jsRandom(160, 169);
	} else if (slave.height < 2) {
		slave.height = jsRandom(170, 184);
	} else if (slave.height <= 3) {
		slave.height = jsRandom(185, 200);
	}

	if (V.releaseID < 1059) {
		if (slave.eyeColor === "no default value") {
			slave.eyeColor = slave.origEye;
		}
	}

	if (slave.birthSurname === undefined) { slave.birthSurname = 0; }
	if (slave.slaveSurname === undefined) { slave.slaveSurname = 0; }

	if (slave.faceImplant === 1) {
		slave.faceImplant = 15;
	} else if (slave.faceImplant === 2) {
		slave.faceImplant = 65;
	}

	if (slave.areoleaPiercing !== undefined) { delete slave.areoleaPiercing; }

	if (slave.pregControl === undefined) { slave.pregControl = "none"; }
	if (slave.pregControl === "labor supressors") {
		slave.pregControl = "labor suppressors";
	}

	// Fix dickAccessory
	if (slave.dickAccessory === "combined chastity") {
		slave.chastityAnus = 1;
		slave.chastityPenis = 1;
		slave.dickAccessory = "none";
	} else if (slave.dickAccessory === "chastity") {
		slave.chastityPenis = 1;
		slave.dickAccessory = "none";
	} else if (slave.dickAccessory === "anal chastity") {
		slave.chastityAnus = 1;
		slave.dickAccessory = "none";
	}
	// Fix vaginalAccessory
	if (slave.vaginalAccessory === "combined chastity") {
		slave.chastityAnus = 1;
		slave.chastityVagina = 1;
		slave.vaginalAccessory = "none";
	} else if (slave.vaginalAccessory === "chastity belt") {
		slave.chastityVagina = 1;
		slave.vaginalAccessory = "none";
	} else if (slave.vaginalAccessory === "anal chastity") {
		slave.chastityAnus = 1;
		slave.vaginalAccessory = "none";
	}

	if (slave.rules !== undefined && slave.rules.rest === undefined) {
		slave.rules.rest = "restrictive";
	}

	// migrate to extended family mode if we detected it was needed
	if (V.relationLinks !== undefined) {
		let link = V.relationLinks[slave.ID];
		if (link) {
			// we already know who your parents are
			slave.mother = link.mother;
			slave.father = link.father;
		} else {
			if (slave.relationTarget > 0) {
				switch (slave.relation) {
					case "sister":
					case "twin": {
						const otherLink = V.relationLinks[slave.relationTarget];
						if (otherLink) {
							// we don't know your parents, but we DO know your sister's parents
							// this shouldn't happen but might sometimes, and there's an obviously correct thing to do - use your sister's parents for you too
							slave.mother = otherLink.mother;
							slave.father = otherLink.father;
						} else {
							// don't know your parents, generate new IDs for them
							setMissingParents(slave);
						}
						V.relationLinks[slave.ID] = {mother: slave.mother, father: slave.father};
						// your sister's parents are the same as your parents
						if (!V.relationLinks[slave.relationTarget]) {
							V.relationLinks[slave.relationTarget] = V.relationLinks[slave.ID];
						}
						break;
					}
					case "mother":
						// we know your mother. that's easy.
						slave.mother = slave.relationTarget;
						V.relationLinks[slave.ID] = {mother: slave.mother, father: 0};
						break;
					case "daughter":
						// we know you are your daughter's mother. keep track of that in case she's forgotten somehow.
						if (!V.relationLinks[slave.relationTarget]) {
							V.relationLinks[slave.relationTarget] = {mother: slave.ID, father: 0};
						}
						break;
					default:
						throw Error(`Unrecognized relation for ${SlaveFullName(slave)}.`);
				}
			}
		}

		// if the slave still had a valid recruitment target, allow her to recruit, otherwise don't
		slave.canRecruit = (slave.recruiter === 0) ? 0 : 1;
	}
	delete slave.relation;
	delete slave.relationTarget;
	delete slave.recruiter;

	if (slave.spermY === undefined) {
		slave.spermY = normalRandInt(50, 3); // narrower range to avoid surprises
	}

	if (slave.geneticQuirks.albinism === 2 && !slave.albinismOverride) {
		induceAlbinism(slave, 2);
	}
	if (genepool) {
		slave.womb = [];
	} else {
		WombInit(slave);
	}
	if (V.releaseID <= 1141) {
		const animalMap = new Map([
			["neko", "cat"],
			["inu", "dog"],
			["kit", "fox"],
			["ushi", "cow"],
			["tanuki", "raccoon"],
			["usagi", "rabbit"],
			["risu", "squirrel"],
			["uma", "horse"],
		]);
		const tail = animalMap.get(slave.tailShape);
		if (tail) {
			slave.tailShape = tail;
		}
		const earShape = animalMap.get(slave.earShape);
		if (earShape) {
			slave.earShape = earShape;
		}
		const earT = animalMap.get(slave.earT);
		if (earT) {
			slave.earT = earT;
		}
	}
	if (V.releaseID < 1036) {
		for (let pmw = 0; pmw < slave.womb.length; pmw++) {
			if (slave.womb[pmw].genetics.mother !== slave.womb[pmw].motherID || slave.womb[pmw].genetics.father !== slave.womb[pmw].fatherID) {
				slave.womb[pmw].genetics = generateGenetics(slave, slave.womb[pmw].fatherID, pmw);
			}
		}
	}

	if (!jsDef(slave.inbreedingCoeff)) {
		slave.inbreedingCoeff = ibc.coeff(slave);
		slave.womb.forEach(f => {
			// Use null as the ID, since fetuses are missing it
			f.genetics.inbreedingCoeff = ibc.coeff(
				{ID: null, mother: f.genetics.mother, father: f.genetics.father}
			);
		});
	}
	slave.womb.forEach(f => {
		f.genetics.geneticQuirks = Object.assign(clone(quirks), f.genetics.geneticQuirks);
	});
};


code:
App.Data.NewGamePlus = (function() {
	const NGPOffset = 1200000;

	function ngpSlaveID(id, preserveSpecial=false) {
		const minID = preserveSpecial ? -20 : -1;
		if (id > 0) {
			id += NGPOffset;
		} else if (V.freshPC === 1 && id === -1) {
			id = -NGPOffset;
		} else if (id < minID) {
			id -= NGPOffset;
		}
		return id;
	}

	function slaveOrZero(id) {
		if (id > 0 && !getSlave(id)) {
			return 0;
		}
		return id;
	}

	function PCInit() {
		if (V.freshPC === 0) {
			cashX(V.ngpParams.prosperity, "personalBusiness");
			const oldCash = V.cash;
			V.cash = 0;
			V.cashLastWeek = 0;
			cashX((Math.clamp(1000*Math.trunc(oldCash/100000), 5000, 1000000)), "personalBusiness");
			if (V.retainCareer === 0) {
				V.PC.career = "arcology owner";
				V.PC.skill.trading = 100;
				V.PC.skill.warfare = 100;
				V.PC.skill.hacking = 100;
				V.PC.skill.slaving = 100;
				V.PC.skill.engineering = 100;
				V.PC.skill.medicine = 100;
			}
			V.PC.mother = ngpSlaveID(V.PC.mother);
			V.PC.father = ngpSlaveID(V.PC.father);
			V.PC.pregSource = slaveOrZero(ngpSlaveID(V.PC.pregSource, true));
			for (let fetus of V.PC.womb) {
				fetus.fatherID = ngpSlaveID(fetus.fatherID, true);
				fetus.genetics.father = ngpSlaveID(fetus.genetics.father, true);
				fetus.genetics.mother = ngpSlaveID(fetus.genetics.mother, true);
			}
		} else {
			V.PC = basePlayer();
			WombInit(V.PC);
			V.cheater = 0;
			V.cash = 0;
			cashX(10000, "personalBusiness");
		}
	}

	function slaveLoopInit() {
		const ngUpdateGenePool = function(genePool = []) {
			const transferredSlaveIds = (V.slaves || [])
				.filter(s => s.ID >= NGPOffset)
				.map(s => s.ID - NGPOffset);
			return genePool
				.filter(s => (transferredSlaveIds.includes(s.ID)))
				.map(s => {
					const result = jQuery.extend(true, {}, s);
					result.ID += NGPOffset;
					result.mother = ngpSlaveID(result.mother);
					result.father = ngpSlaveID(result.father);
					result.cloneID = ngpSlaveID(result.cloneID);
					return result;
				});
		};

		const ngUpdateMissingTable = function(missingTable) {
			const newTable = {};
			let needed = [];

			(V.slaves || [])
				.forEach(s => ([s.pregSource + NGPOffset, s.mother + NGPOffset, s.father + NGPOffset]
					.filter(i => (i in missingTable))
					.forEach(i => {
						if (needed.indexOf(i) === -1) {
							needed.push(i);
						}
					})));
			(V.slaves || []).forEach(s => (s.womb
				.forEach(f => ([f.fatherID, f.genetics.father, f.genetics.mother]
					.filter(i => (i in missingTable))
					.forEach(i => {
						if (needed.indexOf(i) === -1) {
							needed.push(i);
						}
					})))));

			while (needed.length > 0) {
				let i = needed.shift();
				let s = missingTable[i];
				newTable[i - NGPOffset] = s;
				s.ID -= NGPOffset;
				if (s.mother in missingTable) {
					s.mother -= NGPOffset;
					if (!(s.mother in newTable) && needed.indexOf(s.mother + NGPOffset) === -1) {
						needed.push(s.mother + NGPOffset);
					}
				}
				if (s.father in missingTable) {
					s.father -= NGPOffset;
					if (!(s.father in newTable) && needed.indexOf(s.father + NGPOffset) === -1) {
						needed.push(s.father + NGPOffset);
					}
				}
			}

			return newTable;
		};

		const ngUpdatePartners = function(slave) {
			const partners = [...slave.partners]
				.map(k => ngpSlaveID(k, true));

			return new Set(partners);
		};

		let oldMissingParentID = Math.min(-10000, ...Object.keys(V.missingTable)) - 1;
		V.slaves.filter(s => (s.assignment !== Job.IMPORTED)).forEach(s => {
			V.missingTable[oldMissingParentID] = {
				slaveName: s.slaveName,
				slaveSurname: s.slaveSurname,
				fullName: SlaveFullName(s),
				dick: s.dick,
				vagina: s.vagina,
				ID: oldMissingParentID,
				mother: s.mother,
				father: s.father,
				inbreedingCoeff: s.inbreedingCoeff
			};
			Object.values(V.missingTable).forEach(so => {
				if (so.mother === s.ID) {
					so.mother = oldMissingParentID;
				}
				if (so.father === s.ID) {
					so.father = oldMissingParentID;
				}
			});
			V.slaves.concat([V.PC]).forEach(so => {
				if (so.mother === s.ID) {
					so.mother = oldMissingParentID;
				}
				if (so.father === s.ID) {
					so.father = oldMissingParentID;
				}
				if (so.assignment === Job.IMPORTED || so.ID === -1) {
					WombChangeID(so, s.ID, oldMissingParentID);
					WombChangeGeneID(so, s.ID, oldMissingParentID);
				}
			});
			oldMissingParentID--;
		});

		V.slaves.deleteWith((s) => s.assignment !== Job.IMPORTED);

		for (let slave of V.slaves) {
			slave.ID += NGPOffset;
			slave.assignment = Job.REST;
			slave.weekAcquired = 0;
			slave.newGamePlus = 1;
			slave.mother = ngpSlaveID(slave.mother);
			slave.father = ngpSlaveID(slave.father);
			slave.canRecruit = 0;
			slave.breedingMark = 0;
			if (typeof V.ngpParams.nationality === 'string') {
				slave.nationality = V.ngpParams.nationality;
			}
			slave.relationshipTarget = ngpSlaveID(slave.relationshipTarget);
			slave.cloneID = ngpSlaveID(slave.cloneID);
			slave.pregSource = ngpSlaveID(slave.pregSource, true);
			for (let fetus of slave.womb) {
				fetus.fatherID = ngpSlaveID(fetus.fatherID, true);
				fetus.genetics.father = ngpSlaveID(fetus.genetics.father, true);
				fetus.genetics.mother = ngpSlaveID(fetus.genetics.mother, true);
			}
			slave.rivalry = 0;
			slave.rivalryTarget = 0;
			slave.subTarget = 0;
			slave.drugs = "no drugs";
			slave.porn.spending = 0;
			slave.rules.living = "spare";
			slave.diet = "healthy";
			slave.pregControl = "none";
		}
		V.slaveIndices = slaves2indices();
		for (let slave of V.slaves) {
			slave.pregSource = slaveOrZero(slave.pregSource);
			slave.cloneID = slaveOrZero(slave.cloneID);
			slave.relationshipTarget = slaveOrZero(slave.relationshipTarget);
		}
		V.genePool = ngUpdateGenePool(V.genePool);
		V.missingTable = ngUpdateMissingTable(V.missingTable);
		let validRelationship = (s) => (s.relationshipTarget !== 0 && getSlave(s.relationshipTarget).relationshipTarget === s.ID);
		for (let slave of V.slaves) {
			if ((slave.relationship < 0 && V.freshPC === 1) || (slave.relationship > 0 && !validRelationship(slave))) {
				slave.relationship = 0;
				slave.relationshipTarget = 0;
			}
			slave.counter.milk = 0;
			slave.counter.cum = 0;
			slave.counter.births = 0;
			slave.counter.mammary = 0;
			slave.counter.penetrative = 0;
			slave.counter.oral = 0;
			slave.counter.anal = 0;
			slave.counter.vaginal = 0;
			slave.partners = ngUpdatePartners(slave);
			slave.lifetimeCashExpenses = 0;
			slave.lifetimeCashIncome = 0;
			slave.lastWeeksCashIncome = 0;
			slave.lifetimeRepExpenses = 0;
			slave.lifetimeRepIncome = 0;
			slave.lastWeeksRepExpenses = 0;
			slave.lastWeeksRepIncome = 0;
		}
	}

	function doNGPSetup() {
		slaveLoopInit();
		PCInit();
		resetFamilyCounters();
		V.ngpParams = {};
	}

	return doNGPSetup;
})();

repo_name: CloudyCoffee/fc

license: mit


code:
App.Data.Pronouns = {};

/**
 * @readonly
 * @enum {number}
 */
App.Data.Pronouns.Kind = Object.freeze({
	female: 0,
	male: 1,
	neutral: 2,
	toy: 2,
	epicene: 3,
	plural: 3,
	ai: 4,
	custom: 1000
});

/**
 * @param {string} pronoun
 * @param {string} possessive
 * @param {string} possessivePronoun
 * @param {string} object
 * @param {string} objectReflexive
 * @param {string} noun
 * @returns {FC.Data.Pronouns.Definition}
 */
App.Data.Pronouns.makePronounsData = function(
	pronoun, possessive, possessivePronoun, object, objectReflexive, noun) {
	return {
		pronoun: pronoun,
		possessive: possessive,
		possessivePronoun: possessivePronoun,
		object: object,
		objectReflexive: objectReflexive,
		noun: noun
	};
};

App.Data.Pronouns.Std = {
	0: App.Data.Pronouns.makePronounsData("she", "her", "hers", "her", "herself", "girl"),
	1: App.Data.Pronouns.makePronounsData("he", "his", "his", "him", "himself", "boy"),
	2: App.Data.Pronouns.makePronounsData("it", "its", "its", "it", "itself", "toy"),
	3: App.Data.Pronouns.makePronounsData("they", "their", "theirs", "them", "themselves", "toys"),
	4: App.Data.Pronouns.makePronounsData("it", "its", "its", "it", "itself", "program"),
};


code:
/* eslint-disable no-empty */
/* eslint-disable no-var */
/*
Given an object, this will return an array where for each property of the original object, we include the object
{variable: property, oldVal: oldDiff.property, newVal: newDiff.property}
*/
globalThis.generateDiffArray = function(obj) {
	let diffArray = Object.keys(obj).map(function(key) {
		return {variable: key, oldVal: State.temporary.oldDiff[key], newVal: State.temporary.newDiff[key]};
	});
	return diffArray;
};

/*
Shamelessly copied from https://codereview.stackexchange.com/a/11580
Finds and returns the difference between two objects. Potentially will have arbitrary nestings of objects.
*/
globalThis.difference = function(o1, o2) {
	let k;
	let kDiff;
	let diff = {};
	for (k in o1) {
		if (!o1.hasOwnProperty(k)) {
		} else if (typeof o1[k] !== 'object' || typeof o2[k] !== 'object') {
			if (!(k in o2) || o1[k] !== o2[k]) {
				diff[k] = o2[k];
			}
		} else {
			kDiff = difference(o1[k], o2[k]);
			if (kDiff) {
				diff[k] = kDiff;
			}
		}
	}
	for (k in o2) {
		if (o2.hasOwnProperty(k) && !(k in o1)) {
			diff[k] = o2[k];
		}
	}
	for (k in diff) {
		if (diff.hasOwnProperty(k)) {
			return diff;
		}
	}
	return false;
};

/*
Shamelessly copied from https://stackoverflow.com/a/19101235
Flattens an object while concatenating property names.
For example {id: {number: 4, name: "A"}} --> {id.number: 4, id.name: "A"}
*/
globalThis.diffFlatten = function(data) {
	let result = {};
	function recurse(cur, prop) {
		if (Object(cur) !== cur) {
			result[prop] = cur;
		} else if (Array.isArray(cur)) {
			// eslint-disable-next-line no-var
			for (var i=0, l=cur.length; i<l; i++) { // don't change the "var" to "let"
				recurse(cur[i], prop + "[" + i + "]");
			}
			// eslint-disable-next-line block-scoped-var
			if (l === 0) { result[prop] = []; }
		} else {
			let isEmpty = true;
			for (let p in cur) {
				isEmpty = false;
				recurse(cur[p], prop ? prop+"."+p : p);
			}
			if (isEmpty && prop) { result[prop] = {}; }
		}
	}
	recurse(data, "");
	return result;
};

/*
Finds all NaN values anywhere in the State.variables object. Returns an array with the names of the NaNed variables.
*/
globalThis.findNaN = function() {
	const flatV = diffFlatten(State.variables);
	let result = [];
	for (let key in flatV) {
		if (Number.isNaN(flatV[key])) {
			result.push('$$'+key); /* double dollar signs to escape SugarCube markup */
		}
	}
	return result;
};

/* no-usedOnce*/
/**
 * Dumps game save as a readable JSON to the browser for saving in a file
 */
App.Debug.dumpGameState = function() {
	// helper to download a blob
	// borrowed from stackexchange
	function downloadToFile(content, fileName, contentType) {
		let a = document.createElement("a");
		let file = new Blob([content], {
			type: contentType
		});
		a.href = URL.createObjectURL(file);
		a.download = fileName;
		a.click();
	}

	// we will replace SugarCube onSave handler
	let oldHandler = SugarCube.Config.saves.onSave;
	try {
		SugarCube.Config.saves.onSave = function(save, details) {
			if (oldHandler) {
				oldHandler(save, details);
			}
			downloadToFile(JSON.stringify(save, null, 2), save.id + ".json", "text/plain");
		};
		SugarCube.Save.serialize();
	} finally {
		SugarCube.Config.saves.onSave = oldHandler;
	}
};

App.Debug.slaveSummaryText = function(idx) {
	let span = document.createElement("span");
	span.appendChild(App.UI.SlaveSummary.render(V.slaves[idx]));
	return span.outerHTML;
};


code:
globalThis.profileEvents = (function() {
	let passageinit = 0;
	let passagestart = 0;
	let passagerender = 0;
	let passagedisplay = 0;
	let passageend = 0;

	/**
	 * @param {HTMLElement} container
	 */
	function render(container) {
		App.UI.DOM.appendNewElement("h2", container, "Passage Events Profiler");
		const p = document.createElement("p");
		p.classList.add("profile-events");
		row(p, ":passageinit", ":passagestart", "Copy State", passagestart - passageinit);
		row(p, ":passagestart", ":passagerender", "Render", passagerender - passagestart);
		row(p, ":passagerender", ":passagedisplay", "Display", passagedisplay - passagerender);
		row(p, ":passagedisplay", ":passageend", "Cleanup / Auto Save", passageend - passagedisplay);
		container.append(p);
	}

	function row(container, start, stop, desc, value) {
		App.UI.DOM.appendNewElement("div", container, `From ${start}`);
		App.UI.DOM.appendNewElement("div", container, `To ${stop}`);
		App.UI.DOM.appendNewElement("div", container, `(${desc})`);
		App.UI.DOM.appendNewElement("div", container, `${value}ms`);
	}

	return {
		passageinit: () => {
			if (V.profiler) {
				passageinit = performance.now();
			}
		},
		passagestart: () => {
			if (V.profiler) {
				passagestart = performance.now();
			}
		},
		passagerender: () => {
			if (V.profiler) {
				passagerender = performance.now();
			}
		},
		passagedisplay: () => {
			if (V.profiler) {
				passagedisplay = performance.now();
			}
		},
		/**
		 * @param {HTMLElement} content
		 */
		passageend: (content) => {
			if (V.profiler) {
				passageend = performance.now();
				render(content);
			}
		},
	};
})();


code:
/* This is a terrible hack, but it actually seems to yield decent results, so...yay? */

globalThis.ProfileInclude = (function() {
	let stack = [];
	let tally = new Map();
	let builtinIncludeHandler = null;

	class TallyElement {
		constructor(passage) {
			this.passage = passage;
			this.cumulativeTime = 0.0;
			this.calls = 0;
			this.children = new Map();
		}
		get totalChildTime() {
			return Array.from(this.children.values(), (x) => x.cumulativeTime).reduce((sum, current) => { return sum + current; }, 0.0);
		}
		outputBreakdown() {
			if (this.children.size === 0) {
				// no breakdown needed if there are no children, so return an empty fragment
				return document.createDocumentFragment();
			}

			const selfTime = this.cumulativeTime - this.totalChildTime;
			const selfTimePercent = (selfTime / this.cumulativeTime) * 100;
			const selfTimePerCall = selfTime / this.calls;

			let childList = document.createElement("ul");
			let selfListItem = document.createElement("li");
			childList.appendChild(selfListItem);
			selfListItem.appendChild(document.createTextNode(`<self>: ${selfTime.toFixed(0)}ms (${selfTimePercent.toFixed(2)}%); ${selfTimePerCall.toFixed(0)}ms per call over ${this.calls.toFixed(0)} calls`));
			for (const [, child] of this.children) {
				const childTimePercent = (child.cumulativeTime / this.cumulativeTime) * 100;
				const childTimePerCall = child.cumulativeTime / child.calls;
				let childListItem = document.createElement("li");
				childListItem.appendChild(document.createTextNode(`${child.passage}: ${child.cumulativeTime.toFixed(0)}ms (${childTimePercent.toFixed(2)}%); ${childTimePerCall.toFixed(0)}ms per call over ${child.calls.toFixed(0)} calls`));
				childListItem.appendChild(child.outputBreakdown());
				childList.appendChild(childListItem);
			}
			return childList;
		}
	}

	class StackElement {
		constructor(passage) {
			this.passage = passage;
			this.startTime = Date.now();
		}
	}

	function Push(passage) {
		stack.push(new StackElement(passage));
	}

	function PopAndTally() {
		const el = stack.pop();
		const stopTime = Date.now();

		/* traverse the tally tree, ensuring that it has nodes for every frame still in the stack */
		let parentMap = tally;
		for (const child of stack) {
			if (!parentMap.has(child.passage)) {
				parentMap.set(child.passage, new TallyElement(child.passage));
			}
			parentMap = parentMap.get(child.passage).children;
		}

		/* add the popped stack frame's time to the correct child of the tally tree node corresponding with the top of the stack */
		if (!parentMap.has(el.passage)) {
			parentMap.set(el.passage, new TallyElement(el.passage));
		}
		let tallyEl = parentMap.get(el.passage);
		tallyEl.cumulativeTime += stopTime - el.startTime;
		tallyEl.calls++;
	}

	function IncludeBegins(passage) {
		Push(passage);
	}

	function IncludeEnds() {
		PopAndTally();
	}

	$(document).on(':passagestart', function() {
		if (V.profiler && !ProfileInclude.IsEnabled()) {
			ProfileInclude.Start();
		} else if (!V.profiler && ProfileInclude.IsEnabled()) {
			ProfileInclude.Stop();
		}

		tally.clear();

		if (ProfileInclude.IsEnabled()) {
			Push(passage()); // put the "host" passage at the bottom of the stack
		}
	});

	$(document).on(':passagedisplay', function() {
		if (ProfileInclude.IsEnabled()) {
			PopAndTally(); // tally the "host" passage too

			let header = document.createElement("h2");
			header.appendChild(document.createTextNode("Profile Data:"));
			let rootList = document.createElement("ul");
			let rootItem = document.createElement("li");
			let rootData = tally.values().next().value; // should be just the one item in the map at this level
			rootItem.appendChild(document.createTextNode(`${rootData.passage}: ${rootData.cumulativeTime.toFixed(0)}ms (100.00%)`));
			rootItem.appendChild(rootData.outputBreakdown());
			rootList.appendChild(rootItem);
			$("#passages").append(header).append(rootList);
		}
	});

	return {
		Start: function() {
			/* save off the builtin handler for the include macro, delete said macro, then re-register it with instrumentation */
			if (!this.IsEnabled() && Macro.has('include')) {
				builtinIncludeHandler = Macro.get('include').handler;
				Macro.delete('include');
				Macro.add('include', {handler() {
					IncludeBegins(this.args[0]);
					builtinIncludeHandler.call(this);
					IncludeEnds();
				}});
			}
		},

		IsEnabled: function() {
			return (builtinIncludeHandler !== null);
		},

		Stop: function() {
			/* restore the original include macro handler */
			if (this.IsEnabled())	{
				Macro.delete('include');
				Macro.add('include', {handler: builtinIncludeHandler});
				builtinIncludeHandler = null;
			}
		},

		IncludeBegins: IncludeBegins,
		IncludeEnds: IncludeEnds
	};
})();


code:
/**
 * This widget will find which properties were changed in a specific passage (State.bottom.title to be precise), and display their original values and current values.
 * It's somewhat awkward if a stored variable changes types to/from an object or array.
 * The included "Go Back" link is intended to replace the usual NextButton.
 * @returns {DocumentFragment}
 */
App.UI.variableDifference = function() {
	const node = new DocumentFragment();
	const T = State.temporary;
	T.oldVariables = clone(State.bottom.variables);
	T.newVariables = clone(State.variables);
	T.newDiff = diffFlatten(difference(T.oldVariables, T.newVariables)); // Returns a flattened object containing the names of the changed variables, and the new values of those variables.
	T.oldDiff = diffFlatten(difference(T.newVariables, T.oldVariables)); // Returns a flattened object containing the names of the changed variables, and the old values of those variables.
	T.diffArrayFromNew = generateDiffArray(T.newDiff); // This function requires the existence of the specific variables T.newDiff AND T.oldDiff to work.
	T.diffArrayFromOld = generateDiffArray(T.oldDiff);
	App.UI.DOM.appendNewElement("div", node, App.UI.DOM.link(
		"Go Back",
		() => {
			Engine.backward(); // Moves the save state one state backwards.
			State.history.splice(-1, 1); // Removes the latest save state. Necessary to allow the option of displaying the changed variables again immediately without screwing everything up.
			Config.history.maxStates = 1; // Reset the max number of states so as not to explode save file sizes.
		}
	));

	const testSet = new Set(["nextButton", "nextLink", "args"]);

	App.UI.DOM.appendNewElement("div", node, `Differences:`);
	if (T.diffArrayFromNew.length > 0) {
		for (const dif of T.diffArrayFromNew) { // Print variable names, and changed values. Will output the new values correctly, may not output old values correctly
			if (!testSet.has(dif.variable)) {
				App.UI.DOM.appendNewElement("div", node, `Variable: ${dif.variable}, Original Value: ${dif.oldVal}, New Value: ${dif.newVal}`, "indent");
			}
		}

		App.Events.addResponses(node, [
			new App.Events.Result(`Show more`, showMore, `This should only be necessary if a variable changes type to/from an object or array. In that case this will display the correct original value, but incorrect current value.`)
		]);
	} else {
		App.UI.DOM.appendNewElement("div", node, `Nothing changed!`, "indent");
	}
	return node;

	function showMore() {
		const frag = new DocumentFragment();
		App.UI.DOM.appendNewElement("div", frag, `Alternate display:`);
		for (const dif of T.diffArrayFromOld) { // Print variable names, and changed values. Will output the old values correctly, may not output new values correctly
			if (!testSet.has(dif.variable)) {
				App.UI.DOM.appendNewElement("div", frag, `Variable: ${dif.variable}, Original Value: ${dif.oldVal}, New Value: ${dif.newVal}`, "indent");
			}
		}
		return frag;
	}
};