From 141313472f8e4823eb8ec94e59b5dfc2b35a7c66 Mon Sep 17 00:00:00 2001
From: Frankly George <54015-franklygeorge@users.noreply.gitgud.io>
Date: Fri, 11 Oct 2024 20:10:44 +0000
Subject: [PATCH] Fix #5440

---
 src/002-config/fc-version.js                  |   2 +-
 src/data/dataUtils.js                         |  52 ++++-
 .../releases/1267_fixEyeColorsInFetuses.js    |  12 ++
 src/events/RE/reRelativeRecruiter.js          |   4 +-
 src/events/intro/acquisition.js               |   4 +-
 src/events/nonRandom/pregnancyNotice.js       |   1 -
 src/interaction/siCustom.js                   |   5 +-
 src/js/eventHandlers.js                       |   9 +
 src/js/extendedFamilyModeJS.js                |   4 +-
 src/js/ibcJS.js                               |   2 +-
 src/js/states/001-GenePoolRecord.js           | 197 +++++++++---------
 src/npc/generate/generateGenetics.js          |  24 ++-
 src/npc/surgery/bodySwap/huskSlaveSwap.js     |   2 +-
 src/npc/surgery/bodySwap/slaveSlaveSwap.js    |   4 +-
 src/pregmod/editGenetics.js                   |   5 +-
 15 files changed, 190 insertions(+), 137 deletions(-)
 create mode 100644 src/data/patches/releases/1267_fixEyeColorsInFetuses.js

diff --git a/src/002-config/fc-version.js b/src/002-config/fc-version.js
index 7676d245b18..e87e0c2765c 100644
--- a/src/002-config/fc-version.js
+++ b/src/002-config/fc-version.js
@@ -10,5 +10,5 @@ App.Version = {
 	 * The release numbers got messed up, this is corrected in `src/js/eventHandlers.js` and `/src/data/patches/patch.js`.
 	 * The two line above and this line should be safe to remove after release 2001.
 	 */
-	release: 1266,
+	release: 1267,
 };
diff --git a/src/data/dataUtils.js b/src/data/dataUtils.js
index ebe05fbaacf..c9c5bcb8f7f 100644
--- a/src/data/dataUtils.js
+++ b/src/data/dataUtils.js
@@ -25,7 +25,30 @@ App.Utils.setNonexistentProperties = function(obj, props) {
  * @param  {...object} defaultObjs one or more objects to add the properties from.
  */
 App.Utils.assignMissingDefaults = (obj, ...defaultObjs) => {
-	_.defaultsDeep(obj, ...defaultObjs);
+	if (obj === undefined || obj == null) {
+		throw new Error(`assignMissingDefaults() expects param in position 1 to be an object`);
+	}
+	/** @type {object[]} */
+	const defaults = [...defaultObjs];
+	for (const index in defaults) {
+		if (defaults[index] === undefined || defaults[index] == null) {
+			throw new Error(`assignMissingDefaults() expects param in position ${Number(index) +2} to be an object`);
+		}
+	}
+	for (const defaultObj of defaults) {
+		for (const key of Object.keys(defaultObj)) {
+			if (obj[key] === undefined) {
+				obj[key] = clone(defaultObj[key]);
+			} else if (
+				obj[key] != null && typeof obj[key] === "object" &&
+				defaultObj[key] != null &&  typeof defaultObj[key] === "object" &&
+				!Array.isArray(defaultObj[key])
+			) {
+				App.Utils.assignMissingDefaults(obj[key], defaultObj[key]);
+			}
+		}
+	}
+	return obj; // we return the obj so that is some idiot (like me) tries to assign the result of this call to something it will work as they expect
 };
 
 /**
@@ -59,17 +82,32 @@ App.Utils.setExistentProperties = function(obj, props) {
  * @param  {...object} defaultObjs one or more objects to use the properties and values from
  */
 App.Utils.overwriteWithDefaults = (obj, ...defaultObjs) => {
+	if (obj === undefined || obj == null) {
+		throw new Error(`overwriteWithDefaults() expects and object`);
+	}
 	/** @type {object[]} */
 	const defaults = [...defaultObjs];
-	defaults.forEach((defaultObj) => {
-		Object.keys(defaultObj).forEach((key) => {
-			if (typeof obj[key] === "object" && typeof defaultObj[key] === "object" && !Array.isArray(defaultObj[key])) {
-				obj[key] = App.Utils.overwriteWithDefaults(obj[key] ?? {}, defaultObj[key]);
+	for (const index in defaults) {
+		if (defaults[index] === undefined || defaults[index] == null) {
+			throw new Error(`overwriteWithDefaults() expects param in position ${Number(index) +2} to be an object`);
+		}
+	}
+	for (const defaultObj of defaults) {
+		if (defaultObj === undefined || obj == null) { continue; }
+		for (const key of Object.keys(defaultObj)) {
+			if (obj[key] != null && typeof obj[key] === "object" &&
+				defaultObj[key] != null && typeof defaultObj[key] === "object" &&
+				!Array.isArray(defaultObj[key])
+			) {
+				App.Utils.overwriteWithDefaults(obj[key] ?? {}, defaultObj[key]);
+			} else if (Array.isArray(obj[key]) && Array.isArray(defaultObj[key])) {
+				obj[key] = Array.from(clone(defaultObj[key]));
 			} else {
 				obj[key] = defaultObj[key];
 			}
-		});
-	});
+		}
+	}
+	return obj; // we return the obj so that is some idiot (like me) tries to assign the result of this call to something it will work as they expect
 };
 
 /**
diff --git a/src/data/patches/releases/1267_fixEyeColorsInFetuses.js b/src/data/patches/releases/1267_fixEyeColorsInFetuses.js
new file mode 100644
index 00000000000..2be2431286a
--- /dev/null
+++ b/src/data/patches/releases/1267_fixEyeColorsInFetuses.js
@@ -0,0 +1,12 @@
+App.Patch.register({
+	releaseID: 1267,
+	descriptionOfChanges: "Adds eye color to fetuses if it is missing due to bugs in getGenePoolRecord()",
+	fetus: (div, fetus, mother) => {
+		if (fetus.genetics.eyeColor === undefined) {
+			const motherColor = getGenePoolRecord(mother).eye.origColor;
+			fetus.genetics.eyeColor = motherColor;
+			App.Patch.log(`Setting undefined eye color to '${motherColor}'`);
+		}
+		return fetus;
+	}
+});
diff --git a/src/events/RE/reRelativeRecruiter.js b/src/events/RE/reRelativeRecruiter.js
index 23e3ded0fad..2101790d96a 100644
--- a/src/events/RE/reRelativeRecruiter.js
+++ b/src/events/RE/reRelativeRecruiter.js
@@ -41,7 +41,7 @@ App.Events.RERelativeRecruiter = class RERelativeRecruiter extends App.Events.Ba
 
 	/** find all eligible target relatives for a slave; one will be selected randomly (or by cheating)
 	 * @param {FC.SlaveState} slave who's doing the recruiting (should be first actor)
-	 * @param {FC.GenePoolRecord} [gp] genepool entry for this slave, if known
+	 * @param {ReadonlyDeep<FC.GenePoolRecord>} [gp] genepool entry for this slave, if known
 	 * @returns {string[]}
 	 */
 	_getTargetRelativeChoices(slave, gp) {
@@ -621,7 +621,7 @@ App.Events.RERelativeRecruiter = class RERelativeRecruiter extends App.Events.Ba
 		 * @param {number} [parentIDs.father]
 		 */
 		function setUnknownParents(slave, parentIDs = {}) {
-			const gp = isInGenePool(slave) ? getGenePoolRecord(slave, false, true) : undefined;
+			const gp = isInGenePool(slave) ? getGenePoolRecordWriteMode(slave) : undefined;
 
 			if (parentIDs.mother) {
 				slave.mother = parentIDs.mother;
diff --git a/src/events/intro/acquisition.js b/src/events/intro/acquisition.js
index dae2e7c7cb3..422d51c57d4 100644
--- a/src/events/intro/acquisition.js
+++ b/src/events/intro/acquisition.js
@@ -605,14 +605,14 @@ App.Intro.acquisition = function() {
 		for (const slave of getSlaves()) {
 			if (slave.newGamePlus === 0) {
 				slave.inbreedingCoeff = -1;
-				getGenePoolRecord(slave, false, true).inbreedingCoeff = -1;
+				getGenePoolRecordWriteMode(slave).inbreedingCoeff = -1;
 				coeffSlaves.push(slave);
 			}
 		}
 		const ibcoeffs = ibc.coeff_slaves(coeffSlaves);
 		for (const slave of coeffSlaves) {
 			slave.inbreedingCoeff = ibcoeffs[slave.ID];
-			getGenePoolRecord(slave, false, true).inbreedingCoeff = ibcoeffs[slave.ID];
+			getGenePoolRecordWriteMode(slave).inbreedingCoeff = ibcoeffs[slave.ID];
 		}
 	}
 
diff --git a/src/events/nonRandom/pregnancyNotice.js b/src/events/nonRandom/pregnancyNotice.js
index b0edbfa4230..8ec311ad2fc 100644
--- a/src/events/nonRandom/pregnancyNotice.js
+++ b/src/events/nonRandom/pregnancyNotice.js
@@ -443,7 +443,6 @@ App.Events.PregnancyNotice.Event = (node, mother) => {
 		const twins = getFetusTwins(fetus);
 
 		/** @type {FC.SlaveState} */
-		// @ts-expect-error As long as generateChild's code is not changed incubator = true will return a SlaveState object
 		const fakeChild = generateChild(mother, fetus, true);
 		// Age fake child up to the ideal age
 		while (fakeChild.actualAge < V.idealAge) {
diff --git a/src/interaction/siCustom.js b/src/interaction/siCustom.js
index 1d8ebe98feb..e71995b5517 100644
--- a/src/interaction/siCustom.js
+++ b/src/interaction/siCustom.js
@@ -548,14 +548,11 @@ App.UI.SlaveInteract.custom = function(slave, refresh) {
 		}
 
 		function updateName(slave, {oldName: oldName, oldSurname: oldSurname}) {
-			// @ts-ignore you shouldn't access V.genePool directly because it isn't supposed to change; this is an exception; For normal use use getGenePoolRecord()
-			let genepoolRec = isInGenePool(slave) ? getGenePoolRecord(slave, false, true) : undefined;
+			let genepoolRec = isInGenePool(slave) ? getGenePoolRecordWriteMode(slave) : undefined;
 			if (genepoolRec) {
 				genepoolRec.slaveName = slave.slaveName;
 				genepoolRec.slaveSurname = slave.slaveSurname;
 				App.UI.SlaveInteract.rename(slave, {oldName: oldName, oldSurname: oldSurname});
-			} else {
-				console.error(`slave with ID ${slave.ID} is missing its gene pool record`);
 			}
 			refresh();
 		}
diff --git a/src/js/eventHandlers.js b/src/js/eventHandlers.js
index f5834988d59..4ecc7fafc64 100644
--- a/src/js/eventHandlers.js
+++ b/src/js/eventHandlers.js
@@ -24,6 +24,15 @@ App.EventHandlers = function() {
 	 * @param {TwineSugarCube.SaveObject} save
 	 */
 	function onSave(save) {
+		try {
+			// make sure all genePool records are sparse before saving
+			// this saves storage space; in some cases by a crazy amount
+			for (const record of Object.values(V.genePool)) {
+				makeSparseGeneRecord(/** @type {FC.GenePoolRecord} */ (record));
+			}
+		} catch (ex) {
+			console.error(ex);
+		}
 		if (App.Utils.isEndWeek() && V.endweekSaveWarning && Dialog.isOpen()) {
 			$(document).one(':dialogclosed', () => {
 				Dialog.create("Saving during End Week");
diff --git a/src/js/extendedFamilyModeJS.js b/src/js/extendedFamilyModeJS.js
index 03ffdf47b60..54c21bf727c 100644
--- a/src/js/extendedFamilyModeJS.js
+++ b/src/js/extendedFamilyModeJS.js
@@ -32,7 +32,7 @@ globalThis.getRelative = function(ID) {
 		return slave;
 	}
 	// ex-slave
-	const genePool = getGenePoolRecord(ID);
+	const genePool = getGenePoolRecordWriteMode(ID);
 	if (genePool) {
 		return genePool;
 	}
@@ -512,7 +512,7 @@ globalThis.resetFamilyCounters = function() {
  * @param {Relative} slave
  */
 globalThis.setMissingParents = function(slave) {
-	const gp = isInGenePool(slave.ID) ? getGenePoolRecord(slave.ID, false, true) : undefined;
+	const gp = isInGenePool(slave.ID) ? getGenePoolRecordWriteMode(slave.ID) : undefined;
 	if (!specificMom(slave)) {
 		slave.mother = V.missingParentID;
 		if (gp) {
diff --git a/src/js/ibcJS.js b/src/js/ibcJS.js
index 03530d9a74a..d0541022825 100644
--- a/src/js/ibcJS.js
+++ b/src/js/ibcJS.js
@@ -32,7 +32,7 @@ globalThis.ibc = (() => {
 				if (Array.isArray(V.genePool)) {
 					record = V.genePool.find((s) => { return s.ID === id; });
 				} else {
-					record = getGenePoolRecord(id, true);
+					record = getGenePoolRecordWriteMode(id);
 				}
 			}
 			record = record || ((id in V.missingTable) ? V.missingTable[id] : null) || null;
diff --git a/src/js/states/001-GenePoolRecord.js b/src/js/states/001-GenePoolRecord.js
index b09d57dbb79..b1e1494d7ba 100644
--- a/src/js/states/001-GenePoolRecord.js
+++ b/src/js/states/001-GenePoolRecord.js
@@ -3,6 +3,7 @@
  * This houses App.Entity.GenePoolRecord and the functions that you should use to manipulate the gene pool.
  * The gene pool is read only and should not be accessed directly. Use the functions below to access it.
  * @see getGenePoolRecord use this to get a record from the gene pool.
+ * @see getGenePoolRecordWriteMode use this to edit a record in the gene pool.
  * @see isInGenePool use this to check if a record is in the gene pool already; gene pool records must have unique slave IDs
  * @see addToGenePool use this to add a HumanState object to the gene pool; this will throw an error if a record already exist for the HumanState
  * @see deleteGenePoolRecord use this to remove the record from the gene pool; by default this will keep records that are still needed
@@ -10,75 +11,24 @@
  * @see App.Entity.GenePoolRecord the class that defines what a GenePoolRecord is. Handle with care
  */
 
-
 /**
  * Returns a read only record from the gene pool, will return undefined if there is no valid record.
+ * Use `getGenePoolRecordWriteMode()` if you need to edit records.
+ * @see getGenePoolRecordWriteMode
+ * @param {FC.HumanState|number} key The HumanState object or ID to get the record for.
+ * @returns {ReadonlyDeep<FC.GenePoolRecord>}
+ */
+globalThis.getGenePoolRecord = (key) => {
+	return _getGenePoolRecord(key, false, false);
+};
+
+/**
+ * Returns a read/write record from the gene pool, will return undefined if there is no valid record.
  * @param {FC.HumanState|number} key The HumanState object or ID to get the record for.
- * @param {boolean} [missingOkay=false] if true then we won't warn if the record is missing, use sparingly.
- * @param {boolean} [write=false] if true allow writing to the record, is this sparingly and with caution.
  * @returns {FC.GenePoolRecord}
  */
-globalThis.getGenePoolRecord = (key, missingOkay=false, write=false) => {
-	/** @type {string} */
-	let ID;
-	if (typeof key === "number") {
-		ID = String(key);
-	} else if (typeof key === "object" && "ID" in key) {
-		ID = String(key.ID);
-	} else {
-		throw new Error(`key must be an FC.HumanState object or valid HumanState.ID! Got: ${Serial.stringify(key)}`);
-	}
-	if (ID === "0") {
-		console.warn(new Error("getGenePoolRecord: actors with an ID equal to 0 cannot exist in the gene pool"));
-		return undefined;
-	}
-	if (ID in V.genePool) {
-		const handler = {
-			/**
-			 * @param {Partial<FC.GenePoolRecord> & {_canWrite: boolean}} target
-			 * @param {string} key
-			 * @param {*} value
-			 * @returns {boolean}
-			 */
-			set(target, key, value) {
-				if (write === true) {
-					target[key] = value;
-					return true;
-				}
-				throw new Error(`Attempt to set '${String(key)}' in read only gene pool record with ID '${ID}' to '${value}' was blocked`);
-			},
-			/**
-			 * @param {Partial<FC.GenePoolRecord>} target
-			 * @param {keyof FC.GenePoolRecord} key
-			 * @returns {*}
-			 */
-			get(target, key) {
-				const handle = (obj) => {
-					if (typeof obj === 'object' && obj !== null) {
-						return new Proxy(obj, handler);
-					  } else {
-						return obj;
-					  }
-				};
-				if (key in target) {
-					return handle(target[key]);
-				} else if (key in V.genePoolDefaults) {
-					return handle(V.genePoolDefaults[key]);
-				} else {
-					const newRecord = new App.Entity.GenePoolRecord();
-					if (key in newRecord) {
-						return handle(newRecord[key]);
-					}
-				}
-			}
-		};
-		return /** @type {FC.GenePoolRecord} */ (new Proxy(V.genePool[ID], handler));
-	} else {
-		if (!missingOkay) {
-			console.warn(`Gene pool record missing for ID '${ID}'`);
-		}
-		return undefined;
-	}
+globalThis.getGenePoolRecordWriteMode = (key) => {
+	return _getGenePoolRecord(key, false, true);
 };
 
 /**
@@ -103,40 +53,12 @@ globalThis.isInGenePool = (key) => {
 };
 
 /**
- * Adds a clone of the given human to the genePool as a valid FC.GenePoolRecord.
- * Throws an error if they are already in the pool.
- * If unsure, use `isInGenePool()` first to check.
- * @see isInGenePool
- * @param {FC.HumanState} actor
+ * Makes the given GenePoolRecord sparse saving storage space.
+ * If record is undefined then we do nothing.
+ * @param {FC.GenePoolRecord} record
  */
-globalThis.addToGenePool = (actor) => {
-	const record = _.cloneDeep(actor);
-	const player = asPlayer(record);
-	const slave = asSlave(record);
-	if (player) {
-		App.Verify.playerState(
-			player,
-			`<Player with ID '${record.ID}' passed to addToGenePool()>`,
-			undefined,
-			"V.genePool",
-		);
-	} else if (slave) {
-		App.Verify.slaveState(
-			`<Slave with ID '${record.ID}' passed to addToGenePool()>`,
-			slave,
-			"V.genePool"
-		);
-	} else {
-		console.error(`addToGenePool: actor with ID '${record.ID}' is not a valid SlaveState or PlayerState object`);
-		// TODO:@franklygeorge double check that all the keys from new App.Entity.GenePoolRecord() exist on actor
-		// TODO:@franklygeorge if not then throw an error; once you write that code change the console.error above to console.warn
-	}
-
-	if (record.ID === 0) {
-		console.error(new Error("addToGenePool: actors with an ID equal to 0 cannot be added to the gene pool"));
-		return;
-	}
-
+globalThis.makeSparseGeneRecord = (record) => {
+	if (record === undefined) { return; }
 	/**
 	 * DANGER
 	 * strip keys from obj that aren't in template, recursively
@@ -182,6 +104,47 @@ globalThis.addToGenePool = (actor) => {
 		}
 	};
 
+	// strip the record down to a valid GenePoolRecord
+	stripKeys(record, new App.Entity.GenePoolRecord());
+	// remove all default keys from GenePoolRecord
+	removeDefaults(record, V.genePoolDefaults);
+};
+
+/**
+ * Adds a clone of the given human to the genePool as a valid FC.GenePoolRecord.
+ * Throws an error if they are already in the pool.
+ * If unsure, use `isInGenePool()` first to check.
+ * @see isInGenePool
+ * @param {FC.HumanState} actor
+ */
+globalThis.addToGenePool = (actor) => {
+	const record = _.cloneDeep(actor);
+	const player = asPlayer(record);
+	const slave = asSlave(record);
+	if (player) {
+		App.Verify.playerState(
+			player,
+			`<Player with ID '${record.ID}' passed to addToGenePool()>`,
+			undefined,
+			"V.genePool",
+		);
+	} else if (slave) {
+		App.Verify.slaveState(
+			`<Slave with ID '${record.ID}' passed to addToGenePool()>`,
+			slave,
+			"V.genePool"
+		);
+	} else {
+		console.error(`addToGenePool: actor with ID '${record.ID}' is not a valid SlaveState or PlayerState object`);
+		// TODO:@franklygeorge double check that all the keys from new App.Entity.GenePoolRecord() exist on actor
+		// TODO:@franklygeorge if not then throw an error; once you write that code change the console.error above to console.warn
+	}
+
+	if (record.ID === 0) {
+		console.error(new Error("addToGenePool: actors with an ID equal to 0 cannot be added to the gene pool"));
+		return;
+	}
+
 	if (record.ID in V.genePool) {
 		throw new Error(`There is already a genePool record for ID '${record.ID}'`);
 	}
@@ -189,10 +152,7 @@ globalThis.addToGenePool = (actor) => {
 	// cull the contents of womb from the record
 	record.womb = [];
 
-	// strip the record down to a valid GenePoolRecord
-	stripKeys(record, new App.Entity.GenePoolRecord());
-	// remove all default keys from GenePoolRecord
-	removeDefaults(record, V.genePoolDefaults);
+	makeSparseGeneRecord(record);
 
 	// @ts-expect-error V.genePool is set to read only. This is the one of the few lines that should be writing to it
 	V.genePool[String(record.ID)] = record;
@@ -249,6 +209,41 @@ globalThis.deleteGenePoolRecord = (actor, force=false) => {
 	}
 };
 
+/**
+ * Use `getGenePoolRecord()` or `getGenePoolRecordWriteMode()` instead of calling this directly
+ * @see getGenePoolRecord
+ * @see getGenePoolRecordWriteMode
+ * @param {FC.HumanState|number} key The HumanState object or ID to get the record for.
+ * @param {boolean} [missingOkay=false] if true then we won't warn if the record is missing, use sparingly.
+ * @param {boolean} [write=false] if true allow writing to the record.
+ * @returns {FC.GenePoolRecord}
+ */
+globalThis._getGenePoolRecord = (key, missingOkay=false, write=false) => {
+	/** @type {string} */
+	let ID;
+	if (typeof key === "number") {
+		ID = String(key);
+	} else if (typeof key === "object" && "ID" in key) {
+		ID = String(key.ID);
+	} else {
+		throw new Error(`key must be an FC.HumanState object or valid HumanState.ID! Got: ${Serial.stringify(key)}`);
+	}
+	if (ID in V.genePool) {
+		// this implementation means that write=true will convert the record from a sparse object into a full one.
+		// this should be okay since validation will reconvert it to sparse later, but it is not ideal
+		// the reason for doing this is that proxies are an absolute pain if you need to fill in values with defaults recursively.
+		// this also means that if someone tries to write to this with write=false there will be no error or warning to indindcate that something is wrong
+		const record = write ? V.genePool[ID] : clone(V.genePool[ID]);
+		App.Utils.assignMissingDefaults(record, V.genePoolDefaults, new App.Entity.GenePoolRecord());
+		return /** @type {FC.GenePoolRecord} */ (record);
+	} else {
+		if (!missingOkay) {
+			console.warn(`Gene pool record missing for ID '${ID}'`);
+		}
+		return undefined;
+	}
+};
+
 /**
  * This defines properties that are shared between the gene pool and all HumanState objects.
  * The values here should be the default for SlaveState objects.
diff --git a/src/npc/generate/generateGenetics.js b/src/npc/generate/generateGenetics.js
index e4af097d88d..3d58a778ca2 100644
--- a/src/npc/generate/generateGenetics.js
+++ b/src/npc/generate/generateGenetics.js
@@ -2,11 +2,11 @@
 // Generates a child's genetics based off mother and father and returns it as an object to be attached to an ovum
 
 globalThis.generateGenetics = (function() {
-	/** @type {FC.GenePoolRecord} */
+	/** @type {ReadonlyDeep<FC.GenePoolRecord>} */
 	let mother;
 	/** @type {FC.Zeroable<FC.GenePoolRecord>} */
 	let activeMother;
-	/** @type {FC.Zeroable<FC.GenePoolRecord>} */
+	/** @type {FC.Zeroable<ReadonlyDeep<FC.GenePoolRecord>>} */
 	let father;
 	/** @type {FC.Zeroable<FC.GenePoolRecord>} */
 	let activeFather;
@@ -122,8 +122,8 @@ globalThis.generateGenetics = (function() {
 	}
 
 	/** set expected adult height for the fetus
-	 * @param {FC.Zeroable<FC.GenePoolRecord>} father
-	 * @param {FC.GenePoolRecord} mother
+	 * @param {FC.Zeroable<ReadonlyDeep<FC.GenePoolRecord>>} father
+	 * @param {ReadonlyDeep<FC.GenePoolRecord>} mother
 	 * @param {string} gender
 	 * @param {FC.Race} race
 	 * @param {string} nationality
@@ -153,8 +153,8 @@ globalThis.generateGenetics = (function() {
 	}
 
 	/** set breast size potential for the fetus
-	 * @param {FC.GenePoolRecord} mother
-	 * @param {FC.Zeroable<FC.GenePoolRecord>} father
+	 * @param {ReadonlyDeep<FC.GenePoolRecord>} mother
+	 * @param {FC.Zeroable<ReadonlyDeep<FC.GenePoolRecord>>} father
 	 * @returns {number}
 	 */
 	function setBoobPotential(mother, father) {
@@ -338,8 +338,8 @@ globalThis.generateGenetics = (function() {
 	}
 
 	/**
-	 * @param {FC.Zeroable<FC.GenePoolRecord>} father
-	 * @param {FC.GenePoolRecord} mother
+	 * @param {FC.Zeroable<ReadonlyDeep<FC.GenePoolRecord>>} father
+	 * @param {ReadonlyDeep<FC.GenePoolRecord>} mother
 	 * @param {FC.Race} race
 	 */
 	function setSkin(father, mother, race) {
@@ -429,6 +429,10 @@ globalThis.generateGenetics = (function() {
 	 */
 	function validGeneticEyeColor(eyeColor) {
 		switch (eyeColor) {
+			case undefined:
+				console.error(new Error(`validGeneticEyeColor was given 'undefined' instead of a string! Setting the eye color to 'brown'`));
+				eyeColor = "brown";
+				break;
 			case "blind blue":
 				eyeColor = "deep blue";
 				break;
@@ -443,8 +447,8 @@ globalThis.generateGenetics = (function() {
 	// eyeColor
 	function setEyeColor(father, mother) {
 		let eyeColor;
-		/** @type {FC.Zeroable<string>} */
-		let fatherEye = 0;
+		/** @type {string} */
+		let fatherEye;
 
 		// during BC WombInit, the mother has been updated but the father might not have been yet.
 		// if the father is defined but doesn't have eyes, see if maybe he has an old eye color
diff --git a/src/npc/surgery/bodySwap/huskSlaveSwap.js b/src/npc/surgery/bodySwap/huskSlaveSwap.js
index 0e00486cd02..59d85bce329 100644
--- a/src/npc/surgery/bodySwap/huskSlaveSwap.js
+++ b/src/npc/surgery/bodySwap/huskSlaveSwap.js
@@ -9,7 +9,7 @@ App.UI.SlaveInteract.huskSlaveSwap = function() {
 
 	App.UI.DOM.appendNewElement("p", node, `You strap ${target.slaveName}, and the body to which ${he} will be transferred, into the remote surgery and stand back as it goes to work.`);
 	bodySwap(target, asSlave(V.activeSlave), false);
-	const gps = isInGenePool(V.activeSlave) ? getGenePoolRecord(V.activeSlave, false, true) : undefined;
+	const gps = isInGenePool(V.activeSlave) ? getGenePoolRecordWriteMode(V.activeSlave) : undefined;
 	// special exception to swap genePool since the temporary body lacks an entry. Otherwise we could just call bodySwap using the genePool entries
 	gps.race = target.race;
 	gps.origRace = target.origRace;
diff --git a/src/npc/surgery/bodySwap/slaveSlaveSwap.js b/src/npc/surgery/bodySwap/slaveSlaveSwap.js
index 629dc291687..306d20a7c10 100644
--- a/src/npc/surgery/bodySwap/slaveSlaveSwap.js
+++ b/src/npc/surgery/bodySwap/slaveSlaveSwap.js
@@ -7,11 +7,11 @@ App.UI.SlaveInteract.slaveSlaveSwap = function() {
 	const ss2 = getSlave(V.swappingSlave);
 	const ss2Clone = clone(ss2);
 
-	const gps1 = /** @type {FC.SlaveState} */ (getGenePoolRecord(ss1));
+	const gps1 = /** @type {FC.SlaveState} */ (getGenePoolRecordWriteMode(ss1));
 	App.Utils.assignMissingDefaults(gps1, clone(ss1));
 	const gps1Clone = clone(gps1);
 
-	const gps2 = /** @type {FC.SlaveState} */ (getGenePoolRecord(ss2));
+	const gps2 = /** @type {FC.SlaveState} */ (getGenePoolRecordWriteMode(ss2));
 	App.Utils.assignMissingDefaults(gps2, clone(ss2));
 	const gps2Clone = clone(gps2);
 
diff --git a/src/pregmod/editGenetics.js b/src/pregmod/editGenetics.js
index a1ebe570402..2751d1f0856 100644
--- a/src/pregmod/editGenetics.js
+++ b/src/pregmod/editGenetics.js
@@ -646,7 +646,7 @@ App.UI.editGenetics = function() {
 			/* The PC */
 			return birthFullName(V.PC) + ' (PC)';
 		} else {
-			let parent = getGenePoolRecord(id, undefined, true);
+			const parent = getGenePoolRecordWriteMode(id);
 			return parent
 				? birthFullName(parent)
 				: App.Events.makeNode([
@@ -715,8 +715,7 @@ App.UI.editGenetics = function() {
 			el.on('click', function() {
 				jQuery('button.selectedslave').removeClass('selectedslave');
 				el.addClass('selectedslave');
-				// @ts-ignore you shouldn't access V.genePool directly because it isn't supposed to change; this is an exception; For normal use use getGenePoolRecord()
-				let slave = getGenePoolRecord(id, undefined, true);
+				let slave = getGenePoolRecordWriteMode(id);
 				geneDetails.html(geneDetailsFunction(slave));
 
 				let numberEditorOpen = function() {
-- 
GitLab