diff --git a/cspell.json b/cspell.json
index f47a083b498d626708cd9890b78f5081806daa01..2634d48e0ad7fbfa2a6e2f8b5c63ed5cb5bc6775 100644
--- a/cspell.json
+++ b/cspell.json
@@ -36,8 +36,6 @@
         "*.min.js",
         "*.json",
         "*.svg",
-        "*.sh",
-        "*.bat",
         ".gitignore",
         "# Below ignored until properly cleaned up #",
         "src/npc/children",
@@ -61,15 +59,15 @@
         "css",
         "filetypes",
         "npm",
-        "countries",
+        "countries_and_people_groups",
         "names",
         "japanese_custom",
         "languages"
     ],
     "dictionaryDefinitions": [
         {
-            "name": "countries",
-            "path": "./devTools/dictionaries/countries.txt"
+            "name": "countries_and_people_groups",
+            "path": "./devTools/dictionaries/countries_and_people_groups.txt"
         },
         {
             "name": "names",
@@ -90,6 +88,7 @@
         "javascript->JavaScript",
         "non-lethal->nonlethal",
         "randomise->randomize",
+        "seperator->separator",
         "slave-owner->slaveowner",
         "slave-owners->slaveowners",
         "slave-ownership->slaveownership",
@@ -105,6 +104,7 @@
         "breastflesh",
         "coeff",
         "coeffs",
+        "detaste",
         "documentjs",
         "elohiem's",
         "eqnum",
diff --git a/css/art/genAI.css b/css/art/genAI.css
index cbd87eee3fbf2b147f4f0c774aab0e77ded6ebce..a8165246498f8b9d8b24f12eb03b06ab06b7988a 100644
--- a/css/art/genAI.css
+++ b/css/art/genAI.css
@@ -1,10 +1,10 @@
 .ai-art-image {
     transition: filter 0.5s ease-in-out;
     position: relative;
-    float:right; 
-    border:3px hidden; 
-    object-fit:contain; 
-    height:100%; 
+    float:right;
+    border:3px hidden;
+    object-fit:contain;
+    height:100%;
     width:100%;
 }
 
@@ -12,6 +12,55 @@
     filter: blur(5px);
 }
 
+.ai-art-progress {
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+    height: 5%;
+    border-radius: 0;
+    border: none;
+    background: rgba(0, 0, 0, 0.5);
+    transition: 300ms opacity ease-in-out;
+}
+.ai-art-progress::-webkit-progress-bar {
+    transition: width 1s linear;
+}
+.ai-art-progress::-webkit-progress-value {
+    background: white;
+    transition: width 1s linear;
+}
+.ai-art-progress::-moz-progress-bar {
+    background: white;
+    transition: width 1s linear;
+}
+.ai-art-progress[value="0"],
+.ai-art-progress[value="1"] {
+    opacity: 0;
+}
+.ai-art-progress[value="1"] {
+    transition: opacity 300ms 200ms ease-in-out;
+}
+.ai-art-progress[value="1"]::-webkit-progress-bar {
+    transition: width 200ms linear;
+}
+.ai-art-progress[value="1"]::-webkit-progress-value {
+    background: white;
+    transition: width 200ms linear;
+}
+.ai-art-progress[value="1"]::-moz-progress-bar {
+    background: white;
+    transition: width 200ms linear;
+}
+.ai-art-progress[value="0"]::-webkit-progress-bar {
+    transition: none;
+}
+.ai-art-progress[value="0"]::-webkit-progress-value {
+    transition: none;
+}
+.ai-art-progress[value="0"]::-moz-progress-bar {
+    transition: none;
+}
+
 .spinner {
     display: none;
     position: absolute;
@@ -80,7 +129,10 @@
     min-height: 2rem;
     cursor: pointer;
     border: none;
-    background: none;
+}
+
+.ai-art-container button {
+    background: rgba(0, 0, 0, 0.5);
 }
 
 .zoom-in::after {
diff --git a/devTools/dictionaries/countries.txt b/devTools/dictionaries/countries_and_people_groups.txt
similarity index 90%
rename from devTools/dictionaries/countries.txt
rename to devTools/dictionaries/countries_and_people_groups.txt
index ba9220f5e488ba8bb3c7e3e5b2aba1ff3e0a2fbe..1d1dbaa737295d2c351190389f6010647bc2814c 100644
--- a/devTools/dictionaries/countries.txt
+++ b/devTools/dictionaries/countries_and_people_groups.txt
@@ -1,4 +1,5 @@
 Afghan
+African
 Albanian
 Algerian
 American
@@ -31,19 +32,18 @@ Bulgarian
 Burkinabé
 Burmese
 Burundian
+Caledonian
 Cambodian
 Cameroonian
 Canadian
 Cape Verdean
 Catalan
-Central African
 Chadian
 Chilean
 Chinese
 Colombian
 Comorian
 Congolese
-a Cook Islander
 Costa Rican
 Croatian
 Cuban
@@ -55,7 +55,6 @@ Djiboutian
 Dominican
 Dominiquais
 Dutch
-East Timorese
 Ecuadorian
 Egyptian
 Emirati
@@ -66,9 +65,6 @@ Ethiopian
 Fijian
 Filipina
 Finnish
-French
-French Guianan
-French Polynesian
 Gabonese
 Gambian
 Georgian
@@ -79,6 +75,7 @@ Greenlandic
 Grenadian
 Guamanian
 Guatemalan
+Guianan
 Guinean
 Guyanese
 Haitian
@@ -110,8 +107,8 @@ Latvian
 Lebanese
 Liberian
 Libyan
-a Liechtensteiner
 Lithuanian
+Lusitanic
 Luxembourgian
 Macedonian
 Malagasy
@@ -136,8 +133,6 @@ Mozambican
 Namibian
 Nauruan
 Nepalese
-New Caledonian
-a New Zealander
 Ni-Vanuatu
 Nicaraguan
 Nigerian
@@ -153,6 +148,7 @@ Papua New Guinean
 Paraguayan
 Peruvian
 Polish
+Polynesian
 Portuguese
 Puerto Rican
 Qatari
@@ -174,10 +170,7 @@ Sierra Leonean
 Singaporean
 Slovak
 Slovene
-a Solomon Islander
 Somali
-South African
-South Sudanese
 Spanish
 Sri Lankan
 Sudanese
@@ -191,6 +184,7 @@ Tajik
 Tanzanian
 Thai
 Tibetan
+Timorese
 Togolese
 Tongan
 Trinidadian
diff --git a/devTools/scripts/advancedCompiler.js b/devTools/scripts/advancedCompiler.js
index a888e473cb8fdc6ac152ab5d038e71edacb924e6..c16d95165c7ce3a2ece85be611f9afd0343ea8a9 100644
--- a/devTools/scripts/advancedCompiler.js
+++ b/devTools/scripts/advancedCompiler.js
@@ -89,6 +89,9 @@ if (settings.compilerMode === "simple") {
 	if (settings.compilerFilenamePmodVersion === true) {
 		command += ` --pmodversion`;
 	}
+	if (settings.WatcherLiveReload === true) {
+		command += ` --inject-live-reload`;
+	}
 	command += ` --filename=${args.filename}`;
 }
 
diff --git a/devTools/scripts/customChecks.js b/devTools/scripts/customChecks.js
index 2a71b9e00c052c9d631c689a1cf1ca8796a9c0f2..0ad6b3b787560ec5abec7ef45ea63394a22c689d 100644
--- a/devTools/scripts/customChecks.js
+++ b/devTools/scripts/customChecks.js
@@ -46,6 +46,9 @@ const customArticles = [
 	"a MILF",
 	"a SHIT",
 	"a MUCH",
+	"a span",
+	"a html",
+	"a HTML",
 ]
 	.map((entry) => entry.slice(0, 1).toLowerCase() + entry.slice(1));
 
diff --git a/devTools/scripts/dependencyCheck.bat b/devTools/scripts/dependencyCheck.bat
index 82e3799890707f7d439eea41cc80daa90758eaf7..7c8743c6b008b5a7c9627408e338fbe4b76563bd 100644
--- a/devTools/scripts/dependencyCheck.bat
+++ b/devTools/scripts/dependencyCheck.bat
@@ -75,13 +75,14 @@ GOTO :eof
 ECHO   Node.js, https://nodejs.org/, enables all of the new sanity checks and the advanced compiler.
 ECHO     Allows for things like:
 ECHO       Source maps for easier debugging: https://dzone.com/articles/what-are-source-maps-and-how-to-properly-use-them
-ECHO       Javascript linting to catch bugs early using ESLint: https://eslint.org/
-ECHO       Javascript type checking to catch bugs early using the Typescript compiler: https://www.typescriptlang.org/
+ECHO       JavaScript linting to catch bugs early using ESLint: https://eslint.org/
+ECHO       JavaScript type checking to catch bugs early using the Typescript compiler: https://www.typescriptlang.org/
 ECHO       Custom sanity checks to catch common errors.
 ECHO       Spell checking using the cSpell project: https://cspell.org/
 ECHO       Tweaking of compiler and sanity check settings using 'setup.bat'
 ECHO       Manage and run FCHost using 'FCHost.bat'
 ECHO       Copy FC to FCHost (if installed) after each successful compile
+ECHO       Automatic rebuilding and hot reloading of FC when file changes occur using 'watcher.bat'
 :: TODO: @franklygeorge: update as we add the rest of the features
 GOTO :eof
 
diff --git a/devTools/scripts/dependencyCheck.js b/devTools/scripts/dependencyCheck.js
index e4397c253a1cb1a4a2142f3459234d48b971a046..3881e63d06b0156b571556ce829d951834ab7f12 100644
--- a/devTools/scripts/dependencyCheck.js
+++ b/devTools/scripts/dependencyCheck.js
@@ -126,7 +126,7 @@ async function main() {
 	console.log("");
 	problems.forEach(problem => console.log(problem));
 	console.log("");
-	console.log("The command(s) that need ran to fix this problem are:");
+	console.log("The command(s) that need to be run to fix this problem are:");
 	console.log("");
 	if (devDependencyCommand !== "npm install --save-dev") {
 		console.log(devDependencyCommand);
diff --git a/devTools/scripts/dependencyCheck.sh b/devTools/scripts/dependencyCheck.sh
index 16cc2f08e03f0c49a4e5f45010ce228aefc03e02..aa5f5ee0afdd8ded0e3d94797450e034249d15c8 100755
--- a/devTools/scripts/dependencyCheck.sh
+++ b/devTools/scripts/dependencyCheck.sh
@@ -158,13 +158,14 @@ if [[ ! "$node" ]]; then
     echo "  Node.js, https://nodejs.org/, enables all of the new sanity checks and the advanced compiler."
     echo "    Allows for things like:"
     echo "      Source maps for easier debugging: https://dzone.com/articles/what-are-source-maps-and-how-to-properly-use-them"
-    echo "      Javascript linting to catch bugs early using ESLint: https://eslint.org/"
-    echo "      Javascript type checking to catch bugs early using the Typescript compiler: https://www.typescriptlang.org/"
+    echo "      JavaScript linting to catch bugs early using ESLint: https://eslint.org/"
+    echo "      JavaScript type checking to catch bugs early using the Typescript compiler: https://www.typescriptlang.org/"
     echo "      Custom sanity checks to catch common errors."
     echo "      Spell checking using the cSpell project: https://cspell.org/"
     echo "      Tweaking of compiler and sanity check settings using 'setup.sh'"
     echo "      Manage and run FCHost using 'FCHost.sh'"
     echo "      Copy FC to FCHost (if installed) after each successful compile"
+    echo "      Automatic rebuilding and hot reloading of FC when file changes occur using 'watcher.sh'"
     # TODO: @franklygeorge: update as we add the rest of the features
 fi
 
diff --git a/devTools/scripts/setup.js b/devTools/scripts/setup.js
index 43d52ff6cc2da6e8f1b1c281cc23556f5c94aa91..bddee5c8f6c64f68a9c253ca53d4ebc14c7b01fb 100644
--- a/devTools/scripts/setup.js
+++ b/devTools/scripts/setup.js
@@ -20,6 +20,8 @@ const args = yargs(hideBin(process.argv))
 	})
 	.parse();
 
+const separatorString = "-".repeat(78);
+
 // default settings
 /**
  * @typedef {object} Settings
@@ -47,6 +49,7 @@ const args = yargs(hideBin(process.argv))
  * @property {boolean} checksOnlyChangedTypescript If true then we will only check changed lines
  * @property {-1|0|1} precommitHookEnabled 0 = Disabled, 1 = Enabled, -1 = temporarily disabled
  * @property {string} FCHostPath Path to FCHost's directory
+ * @property {boolean} WatcherLiveReload If true then the watcher will trigger a live reload of FC each time it gets re-compiled
  */
 
 // TODO:@franklygeorge Do we want an extensions.json file for VSCode?
@@ -79,6 +82,7 @@ const settings = {
 	checksOnlyChangedTypescript: true,
 	precommitHookEnabled: 1,
 	FCHostPath: "FCHost/fchost/Release",
+	WatcherLiveReload: false,
 };
 
 // create settings.json if it doesn't exist
@@ -131,63 +135,189 @@ if (args.settings === true) {
 /** @type {Settings} */
 let originalSettings = JSON.parse(JSON.stringify(settings));
 
-let compilerMenuChoice;
-
-async function compilerSettings() {
-	let choices = [];
-	if (settings.compilerMode === "advanced") {
-		choices.push("Using the advanced compiler");
-		choices.push(settings.compileThemes
+const strings = {
+	compilerMode: () => {
+		if (settings.compilerMode === "advanced") {
+			return "Using the advanced compiler";
+		} else {
+			return "Using the simple compiler, change to the advanced compiler for more options";
+		}
+	},
+	compileThemes: () => {
+		return (settings.compileThemes
 			? "Themes are compiled"
 			: "Themes are not compiled"
 		);
-		choices.push(settings.compilerSourcemaps
+	},
+	compilerSourcemaps: () => {
+		return (settings.compilerSourcemaps
 			? "Source maps are added, minification is disabled"
 			: "Source maps are not added"
 		);
-		if (settings.compilerSourcemaps === false) {
-			choices.push(settings.compilerMinify
-				? "Build is minified"
-				: "Build is not minified"
-			);
-		}
-		choices.push(settings.compilerAddDebugFiles
+	},
+	compilerMinify: () => {
+		return (settings.compilerMinify
+			? "Build is minified"
+			: "Build is not minified"
+		);
+	},
+	compilerAddDebugFiles: () => {
+		return (settings.compilerAddDebugFiles
 			? "Adding *.debug.* files to the build"
 			: "Ignoring *.debug.* files"
 		);
-		if (settings.compilerRunSanityChecks === 0) {
-			choices.push("Not running sanity checks when compiling");
-		} else if (settings.compilerRunSanityChecks === 1) {
-			choices.push("Running sanity checks before compiling");
-		} else {
-			choices.push("Running sanity checks after compiling");
-		}
-		choices.push(settings.compilerFilenameHash
+	},
+	compilerFilenameHash: () => {
+		return (settings.compilerFilenameHash
 			? "Adding the current Git commit hash to the final filename"
 			: "Not adding the current Git commit hash to the final filename"
 		);
-		choices.push(settings.compilerFilenameEpoch
+	},
+	compilerFilenameEpoch: () => {
+		return (settings.compilerFilenameEpoch
 			? "Adding the current time to the final filename"
 			: "Not adding the current time to the final filename"
 		);
-		choices.push(settings.compilerFilenamePmodVersion
+	},
+	compilerFilenamePmodVersion: () => {
+		return (settings.compilerFilenamePmodVersion
 			? "Adding the current Pmod version to the final filename"
 			: "Not adding the current Pmod version to the final filename"
 		);
-		choices.push(`Verbosity level: ${settings.compilerVerbosity}`);
-		choices.push(settings.compilerCopyToFCHost
-			? "Copying compiled files to FCHost's directory"
-			: "Not copying compiled files to FCHost's directory"
+	},
+	compilerVerbosity: () => {
+		return `Verbosity level: ${settings.compilerVerbosity}`;
+	},
+	compilerRunSanityChecks: () => {
+		if (settings.compilerRunSanityChecks === 0) {
+			return "Not running sanity checks when compiling";
+		} else if (settings.compilerRunSanityChecks === 1) {
+			return "Running sanity checks before compiling";
+		} else {
+			return "Running sanity checks after compiling";
+		}
+	},
+	compilerWaitOnWindows: () => {
+		return (settings.compilerWaitOnWindows
+			? "Waiting for user input before exiting compiler"
+			: "Exiting compiler without user input"
 		);
-	} else if (settings.compilerMode === "simple") {
-		choices.push("Using the simple compiler, change to the advanced compiler for more options");
-		choices.push(settings.compileThemes
-			? "Themes are compiled"
-			: "Themes are not compiled"
+	},
+	precommitHookEnabled: () => {
+		if (settings.precommitHookEnabled === 0) {
+			return "Not running sanity checks before commiting";
+		} else if (settings.precommitHookEnabled === 1) {
+			return "Running sanity checks before commiting";
+		} else {
+			return "Sanity checks are temporarily disabled and will be re-enabled after the next commit";
+		}
+	},
+	checksEnableCustom: () => {
+		return (settings.checksEnableCustom
+			? "Custom sanity checks are enabled"
+			: "Custom sanity checks are disabled"
+		);
+	},
+	checksEnableSpelling: () => {
+		return (settings.checksEnableSpelling
+			? "Spelling checks are enabled"
+			: "Spelling checks are disabled"
+		);
+	},
+	checksEnableESLint: () => {
+		return (settings.checksEnableESLint
+			? "JavaScript linting is enabled"
+			: "JavaScript linting is disabled"
+		);
+	},
+	checksEnableTypescript: () => {
+		return (settings.checksEnableTypescript
+			? "JavaScript type checking is enabled"
+			: "JavaScript type checking is disabled"
+		);
+	},
+	checksOnlyChangedCustom: () => {
+		return (settings.checksOnlyChangedCustom
+			? "Custom sanity checks are only reporting problems on changed lines"
+			: "Custom sanity checks are reporting all problems"
+		);
+	},
+	checksOnlyChangedSpelling: () => {
+		return (settings.checksOnlyChangedSpelling
+			? "Spelling checks are only reporting problems on changed lines"
+			: "Spelling checks are reporting all problems"
+		);
+	},
+	checksOnlyChangedESLint: () => {
+		return (settings.checksOnlyChangedESLint
+			? "JavaScript linting is only reporting problems on changed lines"
+			: "JavaScript linting is reporting all problems"
 		);
+	},
+	checksOnlyChangedTypescript: () => {
+		return (settings.checksOnlyChangedTypescript
+			? "JavaScript type checking is only reporting problems on changed lines"
+			: "JavaScript type checking is reporting all problems"
+		);
+	},
+	manageNodePackages: () => {
+		if (settings.manageNodePackages === -1) {
+			return "Ignoring incorrect Node packages";
+		} else if (settings.manageNodePackages === 0) {
+			return "Asking about incorrect Node packages";
+		} else {
+			return "Automatically fixing incorrect Node packages";
+		}
+	},
+	fetchUpstreamBranch: () => {
+		if (settings.fetchUpstreamBranch === -1) {
+			return "Not fetching upstream pregmod-master branch. Sanity checks will report all errors";
+		} else if (settings.fetchUpstreamBranch === 0) {
+			return "Asking before fetching upstream pregmod-master branch";
+		} else {
+			return "Automatically pulling upstream pregmod-master branch. Sanity checks can report changed lines";
+		}
+	},
+	WatcherLiveReload: () => {
+		return (settings.WatcherLiveReload
+			? "Watcher is triggering a live reload on each successful build"
+			: "Watcher is not triggering a live reload on each successful build"
+		);
+	},
+	compilerCopyToFCHost: () => {
+		return (settings.compilerCopyToFCHost
+			? "Copying FC to FCHost's directory after a successful build"
+			: "Not copying FC to FCHost's directory"
+		);
+	},
+};
+
+let compilerMenuChoice;
+
+async function compilerSettings() {
+	let choices = [];
+	if (settings.compilerMode === "advanced") {
+		choices.push(strings.compilerMode());
+		choices.push(new inquirer.Separator(separatorString));
+		choices.push(strings.compileThemes());
+		choices.push(strings.compilerSourcemaps());
+		if (settings.compilerSourcemaps === false) {
+			choices.push(strings.compilerMinify());
+		}
+		choices.push(strings.compilerAddDebugFiles());
+		choices.push(strings.compilerFilenameHash());
+		choices.push(strings.compilerFilenameEpoch());
+		choices.push(strings.compilerFilenamePmodVersion());
+		choices.push(strings.compilerVerbosity());
+		choices.push(new inquirer.Separator(separatorString));
+		choices.push(strings.compilerRunSanityChecks());
+	} else {
+		choices.push(strings.compilerMode());
+		choices.push(new inquirer.Separator(separatorString));
+		choices.push(strings.compileThemes());
 	}
 	if (process.platform === "win32") {
-		choices.push(settings.compilerWaitOnWindows ? "Waiting for user input before exiting compiler" : "Exiting compiler without user input");
+		choices.push(strings.compilerWaitOnWindows());
 	}
 
 	choices.push("Back");
@@ -206,86 +336,61 @@ async function compilerSettings() {
 			compilerMenuChoice = answers.choice;
 		});
 
-	if (
-		compilerMenuChoice === "Using the advanced compiler" ||
-		compilerMenuChoice === "Using the simple compiler, change to the advanced compiler for more options"
-	) {
+	if (compilerMenuChoice === strings.compilerMode()) {
 		if (settings.compilerMode === "simple") {
 			settings.compilerMode = "advanced";
 		} else {
 			settings.compilerMode = "simple";
 		}
-	} else if (
-		compilerMenuChoice === "Themes are compiled" ||
-		compilerMenuChoice === "Themes are not compiled"
-	) {
+		compilerMenuChoice = strings.compilerMode();
+	} else if (compilerMenuChoice === strings.compileThemes()) {
 		settings.compileThemes = !settings.compileThemes;
-	} else if (
-		compilerMenuChoice === "Source maps are added, minification is disabled" ||
-		compilerMenuChoice === "Source maps are not added"
-	) {
+		compilerMenuChoice = strings.compileThemes();
+	} else if (compilerMenuChoice === strings.compilerSourcemaps()) {
 		settings.compilerSourcemaps = !settings.compilerSourcemaps;
 		if (settings.compilerSourcemaps === true) {
 			settings.compilerMinify = false;
 		}
-	} else if (
-		compilerMenuChoice === "Build is minified" ||
-		compilerMenuChoice === "Build is not minified"
-	) {
+		compilerMenuChoice = strings.compilerSourcemaps();
+	} else if (compilerMenuChoice === strings.compilerMinify()) {
 		settings.compilerMinify = !settings.compilerMinify;
-	} else if (
-		compilerMenuChoice === "Adding *.debug.* files to the build" ||
-		compilerMenuChoice === "Ignoring *.debug.* files"
-	) {
+		compilerMenuChoice = strings.compilerMinify();
+	} else if (compilerMenuChoice === strings.compilerAddDebugFiles()) {
 		settings.compilerAddDebugFiles = !settings.compilerAddDebugFiles;
-	} else if (
-		compilerMenuChoice === "Not running sanity checks when compiling" ||
-		compilerMenuChoice === "Running sanity checks before compiling" ||
-		compilerMenuChoice === "Running sanity checks after compiling"
-	) {
-		if (settings.compilerRunSanityChecks === 2) {
-			settings.compilerRunSanityChecks = 0;
-		} else {
-			settings.compilerRunSanityChecks += 1;
-		}
-	} else if (
-		compilerMenuChoice === "Adding the current Git commit hash to the final filename" ||
-		compilerMenuChoice === "Not adding the current Git commit hash to the final filename"
-	) {
+		compilerMenuChoice = strings.compilerAddDebugFiles();
+	} else if (compilerMenuChoice === strings.compilerFilenameHash()) {
 		settings.compilerFilenameHash = !settings.compilerFilenameHash;
-	} else if (
-		compilerMenuChoice === "Adding the current time to the final filename" ||
-		compilerMenuChoice === "Not adding the current time to the final filename"
-	) {
+		compilerMenuChoice = strings.compilerFilenameHash();
+	} else if (compilerMenuChoice === strings.compilerFilenameEpoch()) {
 		settings.compilerFilenameEpoch = !settings.compilerFilenameEpoch;
-	} else if (
-		compilerMenuChoice === "Adding the current Pmod version to the final filename" ||
-		compilerMenuChoice === "Not adding the current Pmod version to the final filename"
-	) {
+		compilerMenuChoice = strings.compilerFilenameEpoch();
+	} else if (compilerMenuChoice === strings.compilerFilenamePmodVersion()) {
 		settings.compilerFilenamePmodVersion = !settings.compilerFilenamePmodVersion;
-	} else if (compilerMenuChoice === `Verbosity level: ${settings.compilerVerbosity}`) {
+		compilerMenuChoice = strings.compilerFilenamePmodVersion();
+	} else if (compilerMenuChoice === strings.compilerVerbosity()) {
 		if (settings.compilerVerbosity === 6) {
 			settings.compilerVerbosity = 1;
 		} else {
 			settings.compilerVerbosity += 1;
 		}
-	} else if (
-		compilerMenuChoice === "Copying compiled files to FCHost's directory" ||
-		compilerMenuChoice === "Not copying compiled files to FCHost's directory"
-	) {
-		settings.compilerCopyToFCHost = !settings.compilerCopyToFCHost;
-	} else if (
-		compilerMenuChoice === "Waiting for user input before exiting compiler" ||
-		compilerMenuChoice === "Exiting compiler without user input"
-	) {
+		compilerMenuChoice = strings.compilerVerbosity();
+	} else if (compilerMenuChoice === strings.compilerRunSanityChecks()) {
+		if (settings.compilerRunSanityChecks === 2) {
+			settings.compilerRunSanityChecks = 0;
+		} else {
+			settings.compilerRunSanityChecks += 1;
+		}
+		compilerMenuChoice = strings.compilerRunSanityChecks();
+	} else if (compilerMenuChoice === strings.compilerWaitOnWindows()) {
 		settings.compilerWaitOnWindows = !settings.compilerWaitOnWindows;
+		compilerMenuChoice = strings.compilerWaitOnWindows();
 	} else if (compilerMenuChoice === "Back") {
 		compilerMenuChoice = 0;
 		return;
+	} else {
+		throw new Error("Invalid compiler menu choice: " + compilerMenuChoice);
 	}
 
-	compilerMenuChoice = choices.indexOf(compilerMenuChoice);
-
 	await compilerSettings();
 }
 
@@ -293,59 +398,25 @@ let sanityCheckMenuChoice;
 
 async function sanityCheckSettings() {
 	let choices = [];
-	if (settings.precommitHookEnabled === 0) {
-		choices.push("Not running sanity checks before commiting");
-	} else if (settings.precommitHookEnabled === 1) {
-		choices.push("Running sanity checks before commiting");
-	} else {
-		choices.push("Sanity checks are temporarily disabled and will be re-enabled after the next commit");
-	}
-	if (settings.compilerRunSanityChecks === 0) {
-		choices.push("Not running sanity checks when compiling");
-	} else if (settings.compilerRunSanityChecks === 1) {
-		choices.push("Running sanity checks before compiling");
-	} else {
-		choices.push("Running sanity checks after compiling");
-	}
-	choices.push(settings.checksEnableCustom
-		? "Custom sanity checks are enabled"
-		: "Custom sanity checks are disabled"
-	);
-	choices.push(settings.checksEnableSpelling
-		? "Spelling checks are enabled"
-		: "Spelling checks are disabled"
-	);
-	choices.push(settings.checksEnableESLint
-		? "JavaScript linting is enabled"
-		: "JavaScript linting is disabled"
-	);
-	choices.push(settings.checksEnableTypescript
-		? "JavaScript type checking is enabled"
-		: "JavaScript type checking is disabled"
-	);
+	choices.push(strings.precommitHookEnabled());
+	choices.push(strings.compilerRunSanityChecks());
+	choices.push(new inquirer.Separator(separatorString));
+	choices.push(strings.checksEnableCustom());
+	choices.push(strings.checksEnableSpelling());
+	choices.push(strings.checksEnableESLint());
+	choices.push(strings.checksEnableTypescript());
+	choices.push(new inquirer.Separator(separatorString));
 	if (settings.checksEnableCustom === true) {
-		choices.push(settings.checksOnlyChangedCustom
-			? "Custom sanity checks are only reporting problems on changed lines"
-			: "Custom sanity checks are reporting all problems"
-		);
+		choices.push(strings.checksOnlyChangedCustom());
 	}
 	if (settings.checksEnableSpelling === true) {
-		choices.push(settings.checksOnlyChangedSpelling
-			? "Spelling checks are only reporting problems on changed lines"
-			: "Spelling checks are reporting all problems"
-		);
+		choices.push(strings.checksOnlyChangedSpelling());
 	}
 	if (settings.checksEnableESLint === true) {
-		choices.push(settings.checksOnlyChangedESLint
-			? "JavaScript linting is only reporting problems on changed lines"
-			: "JavaScript linting is reporting all problems"
-		);
+		choices.push(strings.checksOnlyChangedESLint());
 	}
 	if (settings.checksEnableTypescript === true) {
-		choices.push(settings.checksOnlyChangedTypescript
-			? "JavaScript type checking is only reporting problems on changed lines"
-			: "JavaScript type checking is reporting all problems"
-		);
+		choices.push(strings.checksOnlyChangedTypescript());
 	}
 
 	choices.push("Back");
@@ -363,11 +434,7 @@ async function sanityCheckSettings() {
 		.then((answers) => {
 			sanityCheckMenuChoice = answers.choice;
 		});
-	if (
-		sanityCheckMenuChoice === "Running sanity checks before commiting" ||
-		sanityCheckMenuChoice === "Not running sanity checks before commiting" ||
-		sanityCheckMenuChoice === "Sanity checks are temporarily disabled and will be re-enabled after the next commit"
-	) {
+	if (sanityCheckMenuChoice === strings.precommitHookEnabled()) {
 		if (settings.precommitHookEnabled === -1) {
 			settings.precommitHookEnabled = 0;
 		} else if (settings.precommitHookEnabled === 0) {
@@ -375,87 +442,59 @@ async function sanityCheckSettings() {
 		} else {
 			settings.precommitHookEnabled = -1;
 		}
-	} else if (
-		sanityCheckMenuChoice === "Not running sanity checks when compiling" ||
-		sanityCheckMenuChoice === "Running sanity checks before compiling" ||
-		sanityCheckMenuChoice === "Running sanity checks after compiling"
-	) {
+		sanityCheckMenuChoice = strings.precommitHookEnabled();
+	} else if (sanityCheckMenuChoice === strings.compilerRunSanityChecks()) {
 		if (settings.compilerRunSanityChecks === 2) {
 			settings.compilerRunSanityChecks = 0;
 		} else {
 			settings.compilerRunSanityChecks += 1;
 		}
-	} else if (
-		sanityCheckMenuChoice === "Custom sanity checks are enabled" ||
-		sanityCheckMenuChoice === "Custom sanity checks are disabled"
-	) {
+		sanityCheckMenuChoice = strings.compilerRunSanityChecks();
+	} else if (sanityCheckMenuChoice === strings.checksEnableCustom()) {
 		settings.checksEnableCustom = !settings.checksEnableCustom;
-	} else if (
-		sanityCheckMenuChoice === "Spelling checks are enabled" ||
-		sanityCheckMenuChoice === "Spelling checks are disabled"
-	) {
+		sanityCheckMenuChoice = strings.checksEnableCustom();
+	} else if (sanityCheckMenuChoice === strings.checksEnableSpelling()) {
 		settings.checksEnableSpelling = !settings.checksEnableSpelling;
-	} else if (
-		sanityCheckMenuChoice === "JavaScript linting is enabled" ||
-		sanityCheckMenuChoice === "JavaScript linting is disabled"
-	) {
+		sanityCheckMenuChoice = strings.checksEnableSpelling();
+	} else if (sanityCheckMenuChoice === strings.checksEnableESLint()) {
 		settings.checksEnableESLint = !settings.checksEnableESLint;
-	} else if (
-		sanityCheckMenuChoice === "JavaScript type checking is enabled" ||
-		sanityCheckMenuChoice === "JavaScript type checking is disabled"
-	) {
+		sanityCheckMenuChoice = strings.checksEnableESLint();
+	} else if (sanityCheckMenuChoice === strings.checksEnableTypescript()) {
 		settings.checksEnableTypescript = !settings.checksEnableTypescript;
-	} else if (
-		sanityCheckMenuChoice === "Custom sanity checks are only reporting problems on changed lines" ||
-		sanityCheckMenuChoice === "Custom sanity checks are reporting all problems"
-	) {
+		sanityCheckMenuChoice = strings.checksEnableTypescript();
+	} else if (sanityCheckMenuChoice === strings.checksOnlyChangedCustom()) {
 		settings.checksOnlyChangedCustom = !settings.checksOnlyChangedCustom;
-	} else if (
-		sanityCheckMenuChoice === "Spelling checks are only reporting problems on changed lines" ||
-		sanityCheckMenuChoice === "Spelling checks are reporting all problems"
-	) {
+		sanityCheckMenuChoice = strings.checksOnlyChangedCustom();
+	} else if (sanityCheckMenuChoice === strings.checksOnlyChangedSpelling()) {
 		settings.checksOnlyChangedSpelling = !settings.checksOnlyChangedSpelling;
-	} else if (
-		sanityCheckMenuChoice === "JavaScript linting is only reporting problems on changed lines" ||
-		sanityCheckMenuChoice === "JavaScript linting is reporting all problems"
-	) {
+		sanityCheckMenuChoice = strings.checksOnlyChangedSpelling();
+	} else if (sanityCheckMenuChoice === strings.checksOnlyChangedESLint()) {
 		settings.checksOnlyChangedESLint = !settings.checksOnlyChangedESLint;
-	} else if (
-		sanityCheckMenuChoice === "JavaScript type checking is only reporting problems on changed lines" ||
-		sanityCheckMenuChoice === "JavaScript type checking is reporting all problems"
-	) {
+		sanityCheckMenuChoice = strings.checksOnlyChangedESLint();
+	} else if (sanityCheckMenuChoice === strings.checksOnlyChangedTypescript()) {
 		settings.checksOnlyChangedTypescript = !settings.checksOnlyChangedTypescript;
+		sanityCheckMenuChoice = strings.checksOnlyChangedTypescript();
 	} else if (
 		sanityCheckMenuChoice === "Back"
 	) {
 		sanityCheckMenuChoice = 0;
 		return;
+	} else {
+		throw new Error("Invalid sanity check menu choice: " + sanityCheckMenuChoice);
 	}
 
-	sanityCheckMenuChoice = choices.indexOf(sanityCheckMenuChoice);
-
 	await sanityCheckSettings();
 }
 
-let MiscMenuChoice;
+let miscMenuChoice;
 
 async function MiscSettings() {
 	let choices = [];
-	if (settings.manageNodePackages === -1) {
-		choices.push("Ignoring incorrect Node packages");
-	} else if (settings.manageNodePackages === 0) {
-		choices.push("Asking about incorrect Node packages");
-	} else {
-		choices.push("Automatically fixing incorrect Node packages");
-	}
-	if (settings.fetchUpstreamBranch === -1) {
-		choices.push("Not fetching upstream pregmod-master branch. Sanity checks will report all errors");
-	} else if (settings.fetchUpstreamBranch === 0) {
-		choices.push("Asking before fetching upstream pregmod-master branch");
-	} else {
-		choices.push("Automatically pulling upstream pregmod-master branch. Sanity checks can report changed lines");
-	}
-
+	choices.push(strings.manageNodePackages());
+	choices.push(strings.fetchUpstreamBranch());
+	choices.push(new inquirer.Separator(separatorString));
+	choices.push(strings.WatcherLiveReload());
+	choices.push(strings.compilerCopyToFCHost());
 	choices.push("Back");
 
 	await inquirer
@@ -464,40 +503,41 @@ async function MiscSettings() {
 			name: "choice",
 			message: "Miscellaneous Settings",
 			choices: choices,
-			default: MiscMenuChoice,
+			default: miscMenuChoice,
 			loop: false,
 			pageSize: 11
 		}])
 		.then((answers) => {
-			MiscMenuChoice = answers.choice;
+			miscMenuChoice = answers.choice;
 		});
 
-	if (
-		MiscMenuChoice === "Ignoring incorrect Node packages" ||
-		MiscMenuChoice === "Asking about incorrect Node packages" ||
-		MiscMenuChoice === "Automatically fixing incorrect Node packages"
-	) {
+	if (miscMenuChoice === strings.manageNodePackages()) {
 		if (settings.manageNodePackages === 1) {
 			settings.manageNodePackages = -1;
 		} else {
 			settings.manageNodePackages += 1;
 		}
-	} else if (
-		MiscMenuChoice === "Not fetching upstream pregmod-master branch. Sanity checks will report all errors" ||
-		MiscMenuChoice === "Asking before fetching upstream pregmod-master branch" ||
-        MiscMenuChoice === "Automatically pulling upstream pregmod-master branch. Sanity checks can report changed lines"
-	) {
+		miscMenuChoice = strings.manageNodePackages();
+	} else if (miscMenuChoice === strings.fetchUpstreamBranch()) {
 		if (settings.fetchUpstreamBranch === 1) {
 			settings.fetchUpstreamBranch = -1;
 		} else {
 			settings.fetchUpstreamBranch += 1;
 		}
-	} else if (MiscMenuChoice === "Back") {
+		miscMenuChoice = strings.fetchUpstreamBranch();
+	} else if (miscMenuChoice === strings.WatcherLiveReload()) {
+		settings.WatcherLiveReload = !settings.WatcherLiveReload;
+		miscMenuChoice = strings.WatcherLiveReload();
+	} else if (miscMenuChoice === strings.compilerCopyToFCHost()) {
+		settings.compilerCopyToFCHost = !settings.compilerCopyToFCHost;
+		miscMenuChoice = strings.compilerCopyToFCHost();
+	} else if (miscMenuChoice === "Back") {
+		miscMenuChoice = 0;
 		return;
+	} else {
+		throw new Error("Invalid misc menu choice: " + miscMenuChoice);
 	}
 
-	MiscMenuChoice = choices.indexOf(MiscMenuChoice);
-
 	await MiscSettings();
 }
 
@@ -544,6 +584,8 @@ async function mainMenu() {
         mainMenuChoice === "Exit"
 	) {
 		process.exit(0);
+	} else {
+		throw new Error("Invalid main menu choice: " + mainMenuChoice);
 	}
 
 	mainMenuChoice = choices.indexOf(mainMenuChoice);
diff --git a/devTools/scripts/watcher.js b/devTools/scripts/watcher.js
index 3395f3429702edd74882dff2ddb47fecddeb09ae..784e8b59c686dd6a6e50b3d722227f264087602d 100644
--- a/devTools/scripts/watcher.js
+++ b/devTools/scripts/watcher.js
@@ -7,61 +7,180 @@ import jetpack from "fs-jetpack";
 import watch from "node-watch";
 import {execSync, spawn} from "child_process";
 import * as path from "path";
+import * as http from "http";
+import {createHash} from "crypto";
+// @ts-ignore
+import colors from "ansi-colors";
 
 const batSh = (process.platform === "win32") ? "bat" : "sh";
 
 /** @type {import("child_process").ChildProcessWithoutNullStreams} */
 let buildProcess;
+/** @type {import("child_process").ChildProcessWithoutNullStreams} */
+let sanityCheckProcess;
 
 // make sure settings.json exists and has all the required properties
 execSync("node devTools/scripts/setup.js --settings");
 
 // load settings.json
 /** @type {import("./setup.js").Settings} */
-const settings = jetpack.read("settings.json", "json");
+let settings = jetpack.read("settings.json", "json");
 
 /**
- * Builds FC using the advanced compiler
+ * @param {string} fPath path to check
+ * @returns {boolean} true if a path is allowed, false otherwise
  */
-function build() {
-	console.log("");
-
-	if (buildProcess !== undefined) {
-		buildProcess.kill();
+function allowed(fPath) {
+	if (fPath.endsWith("fc-version.js.commitHash.js")) {
+		return false;
+	} else if (
+		fPath.startsWith("src") ||
+		fPath.startsWith("js") ||
+		fPath.startsWith("css") ||
+		fPath.startsWith("mods") ||
+		fPath.startsWith("themes") ||
+		fPath.startsWith("tests") ||
+		fPath.startsWith("resources") ||
+		fPath.startsWith("settings.json") ||
+		fPath.startsWith(`devTools${path.sep}scripts`) ||
+		fPath.startsWith("gulpfile.js")
+	) {
+		return true;
 	}
-	buildProcess = spawn("node", ["devTools/scripts/advancedCompiler.js", "--filename=FC_pregmod.watcher.html", "--no-interaction"], {
-		stdio: ['inherit', 'inherit', 'inherit'],
-	  });
+	return false;
+}
+
+let hashes = {};
+
+let needsReloaded = false;
+
+if (settings.WatcherLiveReload === true) {
+	// start http server for live reloading
+	let app = http.createServer((req, res) => {
+		res.setHeader("Content-Type", "application/json");
+		res.setHeader("Access-Control-Allow-Origin", "*");
+		res.end(JSON.stringify({needsReloaded: needsReloaded}, null, 2));
+		needsReloaded = false;
+	});
+	app.listen(16969);
+	console.log("Live reload server listening on port 16969");
+} else {
+	console.log("Live reloading is disabled");
+}
 
-	buildProcess.on('exit', function (code) {
+/**
+ * runs the compiler
+ * @param {boolean} andSanity if true then we run sanity checks after compiling
+ */
+function runCompiler(andSanity = false) {
+	console.log("Running the compiler...");
+	buildProcess = spawn(
+		"node",
+		[
+			"devTools/scripts/advancedCompiler.js", "--filename=FC_pregmod.watcher.html",
+			"--no-interaction", "--skip-sanity-checks"
+		],
+		{
+			stdio: ['inherit', 'inherit', 'inherit'],
+		}
+	);
+	buildProcess.on('exit', function(code) {
 		if (code === null) {
 			return;
 		} else if (code === 0) {
-			console.log(`Saving changes in "setup.${batSh}" will toggle a rebuild`);
+			console.log(colors.blue(`Saving changes in "setup.${batSh}" will toggle a rebuild using the new settings`));
+			if (settings.WatcherLiveReload === true) {
+				// trigger a reload
+				needsReloaded = true;
+			}
+			if (andSanity === true) {
+				runSanity();
+			}
 		} else {
-			console.log('Compiler exited with code:', code);
+			console.log(colors.red(`Compiler exited with code: ${code.toString()}`));
 		}
 	});
 }
 
+/**
+ * runs the sanity checker
+ * @param {boolean} andCompiler if true then we run compiler after doing sanity checks
+ */
+function runSanity(andCompiler = false) {
+	console.log("Running sanity checks...");
+	sanityCheckProcess = spawn(
+		"node",
+		[ "devTools/scripts/sanityCheck.js", "--no-interaction"],
+		{
+			stdio: ['inherit', 'inherit', 'inherit'],
+		}
+	);
+	sanityCheckProcess.on('exit', function(code) {
+		if (code === null) {
+			return;
+		} else {
+			if (code !== 0) {
+				console.log(colors.red(`Sanity checker exited with code: ${code.toString()}`));
+			}
+			if (andCompiler === true) {
+				runCompiler();
+			}
+		}
+	});
+}
+
+/**
+ * Builds FC using the advanced compiler
+ */
+function build() {
+	console.log("");
+
+	if (buildProcess !== undefined) {
+		buildProcess.kill();
+	}
+	if (sanityCheckProcess !== undefined) {
+		sanityCheckProcess.kill();
+	}
+
+	// set needsReloaded to false to stop unnecessary reloads
+	needsReloaded = false;
+
+	if (settings.compilerRunSanityChecks === 1) {
+		// sanity checks first then compile
+		runSanity(true);
+	} else if (settings.compilerRunSanityChecks === 2) {
+		// compile first then sanity checks
+		runCompiler(true);
+	} else {
+		// compile without sanity checks
+		runCompiler(false);
+	}
+}
+
+console.log(colors.blue(`The watcher will save its output to "bin/FC_pregmod.watcher.html"`));
+if (settings.compilerCopyToFCHost === true && jetpack.exists(settings.FCHostPath) === "dir") {
+	console.log(colors.blue(`The watcher's output will be copied to "${settings.FCHostPath}"`));
+}
+console.log("Watcher is starting, please wait...");
+
+// hash all files
+jetpack.find(".").forEach(file => {
+	if (!allowed(file)) { return; }
+	// console.log(`Hashing "${file}"`);
+	try {
+		hashes[file] = createHash("sha256").update(jetpack.read(file, "utf8")).digest("base64");
+	} catch (e) {
+		// fail silently
+	}
+});
+
 const watcher = watch(".", {
 	recursive: true,
+	// eslint-disable-next-line jsdoc/require-jsdoc
 	filter(f, skip) {
 		if (
-			f.startsWith("src") ||
-			f.startsWith("js") ||
-			f.startsWith("css") ||
-			f.startsWith("mods") ||
-			f.startsWith("themes") ||
-			f.startsWith("tests") ||
-			f.startsWith("resources") ||
-			f.startsWith("settings.json") ||
-			f.startsWith(`devTools${path.sep}scripts`) ||
-			f.startsWith("gulpfile.js")
+			allowed(f)
 		) {
-			if (f.endsWith("fc-version.js.commitHash.js")) {
-				return false;
-			}
 			return true;
 		} else if (jetpack.exists(f) === "dir") {
 			return skip;
@@ -71,15 +190,27 @@ const watcher = watch(".", {
 	}
 });
 
-// TODO:@franklygeorge optional launching of a live reloading web server
-
 watcher.on("change", function(event, filename) {
 	filename = filename.toString();
 
 	if (event === "update" && filename && jetpack.exists(filename) === "file") {
-		console.log("");
-		console.log(filename + " changed");
-		build();
+		try {
+			let hash = createHash("sha256").update(jetpack.read(filename, "utf8")).digest("base64");
+			if (!(filename in hashes) || hash !== hashes[filename]) {
+				console.log("");
+				console.log(filename + " changed");
+				if (filename === "settings.json") {
+					// reload settings.json
+					console.log("Reloading settings");
+					/** @type {import("./setup.js").Settings} */
+					settings = jetpack.read("settings.json", "json");
+				}
+				hashes[filename] = hash;
+				build();
+			}
+		} catch (e) {
+			console.log(e);
+		}
 	}
 });
 
@@ -87,4 +218,4 @@ console.log("Watching for changes");
 console.log("");
 
 // run build when we first startup
-build()
+build();
diff --git a/devTools/scripts/watcherLiveReload.js b/devTools/scripts/watcherLiveReload.js
new file mode 100644
index 0000000000000000000000000000000000000000..a6c8ada97e5a19d0091ea0f5037d03f20e0cc61b
--- /dev/null
+++ b/devTools/scripts/watcherLiveReload.js
@@ -0,0 +1,20 @@
+/** @file This is injected into FC to allow the watchers live reload functionality */
+
+const liveReloadUrl = "http://localhost:16969";
+
+function checkForLiveReload() {
+	try {
+		$.getJSON(liveReloadUrl, function(data) {
+			if (data !== undefined && "needsReloaded" in data && data["needsReloaded"] === true) {
+				// reload
+				location.reload();
+			}
+		});
+	} catch (e) {
+		// fail silently
+		// this can happen for many reasons. The main one being that the server has been closed while FC has been left open
+	}
+}
+
+// query server every 2 seconds and see if we need to reload
+window.setInterval(checkForLiveReload, 2000);
diff --git a/devTools/types/FC/RA.d.ts b/devTools/types/FC/RA.d.ts
index 12ad19b8c71ef048104472041705fe3b60b5fe1f..d4070eacddc4720a6d9f29a4b532270ca67ce01f 100644
--- a/devTools/types/FC/RA.d.ts
+++ b/devTools/types/FC/RA.d.ts
@@ -22,6 +22,7 @@ declare namespace FC {
 		interface RuleSurgerySettings {
 			voice: number;
 			eyes: number;
+			heels: number;
 			hears: number;
 			smells: number;
 			tastes: number;
@@ -217,6 +218,8 @@ declare namespace FC {
 			choosesOwnClothes: 0 | 1;
 			pronoun: number;
 			posePrompt: string;
+			expressionPositivePrompt: string;
+			expressionNegativePrompt: string;
 			positivePrompt: string;
 			negativePrompt: string;
 			overridePrompts: boolean;
diff --git a/devTools/types/FC/util.d.ts b/devTools/types/FC/util.d.ts
index 3ed3029b8ef312d7d6465dbab41f8009f3ada656..8123985964b9bac8c4856555f616c230430f0e20 100644
--- a/devTools/types/FC/util.d.ts
+++ b/devTools/types/FC/util.d.ts
@@ -18,4 +18,8 @@ declare namespace FC {
 		min: number;
 		max: number;
 	}
+
+	type PromiseWithProgress<T> = Promise<T> & {
+		onProgress: (fn: (progress: number) => void) => PromiseWithProgress<T>
+	}
 }
diff --git a/gulpfile.js b/gulpfile.js
index d63dee4f59cae3a39c5f6e4fae46b33652186531..2e167e4b33a6f381910d8121013f6a74cbe1367f 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -92,6 +92,11 @@ const args = yargs(hideBin(process.argv))
 		description: 'The filename to save the compiled HTML file as',
 		default: cfg.output,
 	})
+	.option('inject-live-reload', {
+		type: 'boolean',
+		description: 'Injects code used by the watcher to live reload FC',
+		default: false,
+	})
 	// commands should exist as exported gulp tasks
 	.command('html', "Build FC")
 	.command('themes', "Build themes")
@@ -157,7 +162,7 @@ function tweeCompilerExecutable() {
  *
  * Combines paths to tweego and options defined in the build.config.json file to
  * return a full tweego launch command, which will combine all story elements, pick up modules,
- * and generate an HTML file in the intermediate directory
+ * and generate a HTML file in the intermediate directory
  * @returns {string} Full tweego command string
  */
 function tweeCompileCommand() {
@@ -215,6 +220,19 @@ function processScripts(srcGlob, destDir, destFileName) {
 	const addSourcemaps = !args.release;
 	const prefix = path.relative(destDir, srcGlob.substr(0, srcGlob.indexOf("*")));
 
+	if (args.injectLiveReload === true && destFileName === "module-script.js") {
+		const liveReloadScriptPath = "devTools/scripts/watcherLiveReload.js";
+		if (jetpack.exists(liveReloadScriptPath) === "file") {
+			log.info("Injecting live reload code");
+			if (typeof srcGlob === "string") {
+				srcGlob = [srcGlob];
+			}
+			srcGlob.push(liveReloadScriptPath);
+		} else {
+			log.error(`Live reload script is missing from "${liveReloadScriptPath}"!`);
+		}
+	}
+
 	return gulp.src(srcGlob)
 		.pipe(args.debug
 			? noop()
diff --git a/js/003-data/constants.js b/js/003-data/constants.js
index 5021c30201e41d5d269daf0f07cc7c147b74d090..fc9592ceb0dc1686b0c2dfe487b1620fb6ffd313 100644
--- a/js/003-data/constants.js
+++ b/js/003-data/constants.js
@@ -438,7 +438,7 @@ globalThis.SmartPiercingSetting = Object.freeze({
 });
 
 /**
- * @type {Readonly<{MASTERED_XP: number}>}
+ * @type {Readonly<{MASTERED_XP: number, SEX_SLAVE_CONTRACT_COST: number}>}
  */
 globalThis.Constant = Object.freeze({
 	MASTERED_XP: 200,
diff --git a/js/003-data/gameVariableData.js b/js/003-data/gameVariableData.js
index 077dae71d1052e9666b1ad01eaf9f75cfc4557c0..f94bc6b94860509aff8c9ecf83498f96fd9968cc 100644
--- a/js/003-data/gameVariableData.js
+++ b/js/003-data/gameVariableData.js
@@ -179,6 +179,7 @@ App.Data.defaultGameStateVariables = {
 	aiApiUrl: "http://localhost:7860",
 	aiAutoGen: true,
 	aiAutoGenFrequency: 10,
+	aiUseRAForEvents: false,
 	aiCfgScale: 5,
 	aiTimeoutPerStep: 2.5,
 	/** @type {'static' | 'reactive'} */
@@ -313,6 +314,17 @@ App.Data.defaultGameStateVariables = {
 		vaginalAccessory: new Map([]),
 	},
 
+	// pregnancy notice data
+	// This is used by App.Events.PregnancyNotice
+	pregnancyNotice: {
+		/** @type {boolean} if false then pregnancy notice events will not happen */
+		enabled: true,
+		/** @type {boolean} if true then the pregnancy notice event will render a visual representation of the ova */
+		renderFetus: true,
+		/** @type {number[]} FC.HumanState.ID: list of humans that have already been processed this week */
+		processedSlaves: [],
+	},
+
 	// Mods
 	mods: {
 		/** @type {FC.Mods.Food} */
@@ -559,6 +571,7 @@ App.Data.resetOnNGPlus = {
 	/** @type {{[key: string]: number[]}} */
 	rulesToApplyOnce: {},
 	raDefaultMode: 0,
+	addButtonsToSlaveLinks: true,
 
 	RECheckInIDs: [],
 
@@ -668,7 +681,19 @@ App.Data.resetOnNGPlus = {
 	spaAggroSpermBan: 1,
 	spaName: "the Spa",
 
-	incubator: {capacity: 0, tanks: []},
+	incubator: {
+		// Everything in here is overwritten by App.Facilities.Incubator.init()
+		capacity: 0,
+		tanks: [],
+		maleSetting: {
+			imprint: "trust",
+			targetAge: 18,
+		},
+		femaleSetting: {
+			imprint: "trust",
+			targetAge: 18,
+		},
+	},
 
 	/** @type {FC.FutureSocietyDeco} */
 	clinicDecoration: (/** @type {FC.FutureSocietyDeco} */ "standard"),
@@ -1309,8 +1334,9 @@ App.Data.resetOnNGPlus = {
 		reorder: 0
 	},
 
-	/** @type {FC.Bool} */
+	/** @type {FC.Bool} toggles cheats */
 	cheatMode: 0,
+	/** @type {FC.Bool} toggles sidebar cheats */
 	cheatModeM: 1,
 	slaveBotGeneration: 0,
 	experimental: {
@@ -1518,6 +1544,7 @@ App.Data.defaultGameOptions = {
 	/** @type {'link'|'button'} */
 	purchaseStyle: 'link',
 	raDefaultMode: 0,
+	addButtonsToSlaveLinks: true,
 
 	sideBarOptions: {
 		/** @type {'expanded'|'compact'} */
@@ -1545,6 +1572,7 @@ App.Data.defaultGameOptions = {
 	aiNationality: 2,
 	aiAutoGen: true,
 	aiAutoGenFrequency: 10,
+	aiUseRAForEvents: false,
 	aiSamplingMethod: "DPM++ 2M SDE Karras",
 	aiCfgScale: 5,
 	aiTimeoutPerStep: 2.5,
diff --git a/js/003-data/playerData.js b/js/003-data/playerData.js
index 096abdf69305d0737ccd9b22172e2fe2b1837262..12e5cf6d2668d8fd92c47daffcb578c6eac80125 100644
--- a/js/003-data/playerData.js
+++ b/js/003-data/playerData.js
@@ -8,7 +8,7 @@ App.Data.player = {
 		],
 		[1,
 			{
-				name: `Drank`,
+				name: `Drunk`,
 				suggestions: new Set(["whiskey", "rum", "wine", "sake", "vodka", "beer", "bourbon", "scotch"])
 			}
 		],
diff --git a/js/rulesAssistant/conditionEvaluation.js b/js/rulesAssistant/conditionEvaluation.js
index be47fbf365ae8bd47cd4357f034389041184e18a..b8697c139319466e177ebb49743f4b05486a9164 100644
--- a/js/rulesAssistant/conditionEvaluation.js
+++ b/js/rulesAssistant/conditionEvaluation.js
@@ -277,9 +277,13 @@ App.RA.Activation.populateGetters = function() {
 		val: c => canWalk(c.slave)
 	});
 	gm.addBoolean("hasinternalballs", {
-		name: "Has Internal Balls?", description: "If the slaves has internal balls. False, if the slave has no balls",
+		name: "Has Internal Balls?", description: "If the slave has internal balls. False, if the slave has no balls",
 		val: c => c.slave.balls > 0 && c.slave.scrotum === 0
 	});
+	gm.addBoolean("ismindbroken", {
+		name: "Is Mindbroken?", description: "If the slave is mindbroken",
+		val: c => isMindbroken(c.slave)
+	});
 
 	// Assignments
 	// Groups
diff --git a/src/002-config/fc-version.js b/src/002-config/fc-version.js
index 74e870346784ea4bc0f31df5b395289e3b9178b9..bc58cebdb0754068835cdbeeabadf20ef0403dda 100644
--- a/src/002-config/fc-version.js
+++ b/src/002-config/fc-version.js
@@ -2,5 +2,5 @@ App.Version = {
 	base: "0.10.7.1", // The vanilla version the mod is based off of, this should never be changed.
 	pmod: "4.0.0-alpha.30",
 	commitHash: null,
-	release: 1225, // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js.
+	release: 1229, // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js.
 };
diff --git a/src/004-base/basePrompt.js b/src/004-base/basePrompt.js
index 87e6ba05cc631d01de478b8816151e84a60b778b..8c76b433ff8eed95dd9f2fc521feb81feee6fd2f 100644
--- a/src/004-base/basePrompt.js
+++ b/src/004-base/basePrompt.js
@@ -1,7 +1,7 @@
 /** base class for prompt parts */
 App.Art.GenAI.PromptPart = class PromptPart {
 	/**
-	 * @param {FC.SlaveState} slave
+	 * @param {FC.HumanState} slave
 	 */
 	constructor(slave) {
 		this.slave = slave;
diff --git a/src/Mods/SecExp/events/conflictReport.js b/src/Mods/SecExp/events/conflictReport.js
index 626b19775d2fc612b5a99979a1d5a8e9d74f78a1..e23f1a1198bdd28c90f213bbed0781678bc07da4 100644
--- a/src/Mods/SecExp/events/conflictReport.js
+++ b/src/Mods/SecExp/events/conflictReport.js
@@ -356,7 +356,7 @@ App.Events.conflictReport = function() {
 				r = [];
 			}
 			if (V.maximumRep < 30000) {
-				result === 3 ? V.maximumRep += 1000 : V.maximumRep += 500;
+				V.maximumRep += (result === 3 ? 1000 : 500);
 			}
 		} else if (result === -3 || result === -2) {
 			r.push(` Thanks to your defeat, your `, App.UI.DOM.makeElement("span", `reputation`, ["red"]), ` and `, App.UI.DOM.makeElement("span", `authority`, ["red"]), ` decreased.`);
@@ -1029,43 +1029,50 @@ App.Events.conflictReport = function() {
 			App.Events.addParagraph(node, r);
 			r = [];
 
-			let menialPrice = Math.trunc((V.slaveCostFactor * 1000) / 100) * 100;
-			menialPrice = Math.clamp(menialPrice, 500, 1500);
 			captives = Math.trunc(captives);
 			if (captives > 0) {
-				let candidates = 0;
+				let candidates = [];
 				r.push(`During the battle ${captives} attackers were captured.`);
+				let totalPrice = 0;
 				if (random(1, 100) <= 25) {
-					candidates = Math.min(captives, random(1, 3));
-					r.push(`${capFirstChar(num(candidates, true))} of them have the potential to be sex slaves.`);
+					const numCandidates = Math.min(captives, random(1, 3));
+					for (let i = 0; i < numCandidates; i++) {
+						const generateFemale = random(0, 99) < V.seeDicks;
+						let slave = GenerateNewSlave((generateFemale ? "XY" : "XX"), {minAge: 16, maxAge: 32, disableDisability: 1});
+						slave.weight = (generateFemale ? random(-20, 30) : random(0, 30));
+						slave.muscles = (generateFemale ? random(15, 80) : random(25, 80));
+						slave.waist = (generateFemale ? random(10, 80) : random(-20, 20));
+						slave.skill.combat = 70;
+						slave.origin = `$He is an enslaved ${V.SecExp.war.attacker.type} soldier captured during a battle.`;
+						candidates.push(slave);
+						totalPrice += slaveCost(slave) - sexSlaveContractCost();
+					}
+					r.push(`${capFirstChar(num(numCandidates, true))} of them have the potential to be sex slaves.`);
 				}
+				const menialValue = (captives - candidates.length) * menialSlaveCost(-(captives - candidates.length));
+				totalPrice += menialValue;
 				App.Events.addParagraph(node, r);
 				r = [];
 
 				const sell = function() {
-					cashX((menialPrice * captives), "menialTransfer");
-					return `You have the all of the captives sold, gaining ${cashFormatColor(menialPrice * captives)}.`;
+					for (const slave of candidates) {
+						cashX(slaveCost(slave) - sexSlaveContractCost(), "slaveTransfer", slave);
+					}
+					cashX(menialValue, "menialTransfer");
+					V.menialSupplyFactor += (captives - candidates.length);
+					return `You have the all of the captives sold, gaining ${cashFormatColor(totalPrice)}.`;
 				};
 				const keep = function() {
 					let t = [];
-					V.menials += (captives - candidates);
-					let names = [];
-					for (let i = 0; i < candidates; i++) {
-						const generateFemale = random(0, 99) < V.seeDicks;
-						let slave = GenerateNewSlave((generateFemale ? "XY" : "XX"), {minAge: 16, maxAge: 32, disableDisability: 1});
-						slave.weight = (generateFemale ? random(-20, 30) : random(0, 30));
-						slave.muscles = (generateFemale ? random(15, 80) : random(25, 80));
-						slave.waist = (generateFemale ? random(10, 80) : random(-20, 20));
-						slave.skill.combat = 70;
-						slave.origin = `$He is an enslaved ${V.SecExp.war.attacker.type} soldier captured during a battle.`;
-						names.push(SlaveFullName(slave));
+					V.menials += (captives - candidates.length);
+					for (const slave of candidates) {
 						newSlave(slave); // skip New Slave Intro
 					}
-					if (candidates > 0) {
-						t.push(`${toSentence(names)} ${names.length > 1 ? "have" : "has"} been added to your roster of sex slaves.`);
+					if (candidates.length > 0) {
+						t.push(`${toSentence(candidates.map(s => SlaveFullName(s)))} ${candidates.length > 1 ? "have" : "has"} been added to your roster of sex slaves.`);
 					}
-					if (captives > candidates) {
-						t.push(`${capFirstChar(numberWithPluralOne(captives - candidates, "menial"))} slaves acquired.`);
+					if (captives > candidates.length) {
+						t.push(`${capFirstChar(numberWithPluralOne(captives - candidates.length, "menial slave"))} acquired.`);
 					}
 					return t;
 				};
@@ -1075,8 +1082,8 @@ App.Events.conflictReport = function() {
 				};
 
 				App.Events.addResponses(node, [
-					new App.Events.Result(`Sell them all immediately`, sell, `They are worth ${cashFormat(menialPrice * captives)}`),
-					new App.Events.Result(`Keep them as slaves`, keep, `${capFirstChar(numberWithPluralOne(captives - candidates, "menial"))} and ${numberWithPluralOne(candidates, "sex slave")}`),
+					new App.Events.Result(`Sell them all immediately`, sell, `They are worth ${cashFormat(totalPrice)}`),
+					new App.Events.Result(`Keep them as slaves`, keep, `${capFirstChar(numberWithPluralOne(captives - candidates.length, "menial"))} and ${numberWithPluralOne(candidates.length, "sex slave")}`),
 					new App.Events.Result(`Execute them on the spot`, execute, `Improves your authority`),
 				]);
 			}
diff --git a/src/art/artJS.js b/src/art/artJS.js
index 9ffc890707d8cebc900ca7dc98005388814784ca..858e057ed8d4e76f8d0de9e7caff3199591883ac 100644
--- a/src/art/artJS.js
+++ b/src/art/artJS.js
@@ -497,6 +497,9 @@ async function renderAIArt(slave, imageSize, imageNum = null) {
 App.Art.aiArtElement = function(slave, imageSize, isEventImage = null) {
 	const container = App.UI.DOM.makeElement('div', null, ['ai-art-container']);
 	App.UI.DOM.appendNewElement('img', container, null, ['ai-art-image']);
+	const progress = App.UI.DOM.appendNewElement('progress', container, null, ['ai-art-progress']);
+	progress.value = 0;
+	progress.max = 1;
 	const toolbar = App.UI.DOM.appendNewElement('div', container, null, ['ai-toolbar']);
 
 	/** @type {HTMLButtonElement} */
@@ -523,19 +526,43 @@ App.Art.aiArtElement = function(slave, imageSize, isEventImage = null) {
 				const lightbox = App.UI.DOM.appendNewElement('div', document.body, null, ['lightbox', 'ui-front']);
 				// make a seperate background element so that the user can click on the image without lightbox closing
 				const lightboxBackground = App.UI.DOM.appendNewElement('div', lightbox, null, ['lightbox-background']);
-				lightboxBackground.addEventListener('click', () => {
-					console.log('background clicked');
-					lightbox.remove();
+				lightboxBackground.addEventListener('click', (ev) => {
+					if (ev.target === lightboxBackground) {
+						console.log('background clicked');
+						lightbox.remove();
+					}
 				});
 				// Visible button for exiting, but clicking outside of image should automatically close it anyways
 				App.UI.DOM.appendNewElement('button', lightboxBackground, '✕', ['close']);
 				const lightboxImg = App.UI.DOM.appendNewElement('img', lightboxBackground);
 				lightboxImg.src = imageElement.getAttribute('src');
+
+				const list = App.UI.DOM.appendNewElement('ul', lightboxBackground);
+				Object.assign(list.style, {
+					display: 'flex',
+					position: 'absolute',
+					height: '9%',
+					bottom: '0',
+					margin: '0'
+				});
+				const images = Promise.allSettled(slave.custom.aiImageIds.map((aiImageId, idx) => renderAIArt(slave, 1, idx)));
+				images.then(images => images.filter(image => image.status === "fulfilled").map(image => {
+					list.append(image.value);
+					image.value.onclick = () => lightboxImg.src = image.value.src;
+				}));
 			} else {
 				console.error('No image element found to lightbox');
 			}
 		};
 		zoomIn.addEventListener("click", onZoomInClick);
+
+		const onContainerClick = (ev) => {
+			const imageElement = container.querySelector('.ai-art-image');
+			if (ev.target === imageElement) {
+				onZoomInClick();
+			}
+		};
+		container.addEventListener("click", onContainerClick);
 	};
 
 
@@ -578,7 +605,13 @@ App.Art.aiArtElement = function(slave, imageSize, isEventImage = null) {
 				...options
 			};
 			container.classList.add("refreshing");
+			progress.value = 0;
 			App.Art.GenAI.reactiveImageDB.getImage([slave], effectiveOptions)
+				.onProgress((progressNum) => {
+					if (slave.ID === App.Art.GenAI.sdQueue.workingOnID || progressNum === 1) {
+						progress.value = progressNum;
+					}
+				})
 				.then((imageData) => {
 					reactiveSpecific.setImage(imageData?.data?.images?.lowRes, imageData?.id?.toString() || `unknownId-${Math.random()}`);
 				})
@@ -590,14 +623,17 @@ App.Art.aiArtElement = function(slave, imageSize, isEventImage = null) {
 	const staticSpecific = {
 		updateAndRefresh: (index = null) => {
 			container.classList.add("refreshing");
-
-			App.Art.GenAI.staticCache.updateSlave(slave, index, isEventImage).then(() => {
-				staticSpecific.refresh();
-			}).catch(error => {
-				console.log(error.message || error);
-			}).finally(() => {
-				container.classList.remove("refreshing");
-			});
+			progress.value = 0;
+
+			App.Art.GenAI.staticCache.updateSlave(slave, index, isEventImage)
+				.onProgress((progressNum) => progress.value = progressNum)
+				.then(() => {
+					staticSpecific.refresh();
+				}).catch(error => {
+					console.log(error.message || error);
+				}).finally(() => {
+					container.classList.remove("refreshing");
+				});
 		},
 		refresh: () => {
 			renderAIArt(slave, imageSize, slave.custom.aiDisplayImageIdx)
@@ -638,7 +674,6 @@ App.Art.aiArtElement = function(slave, imageSize, isEventImage = null) {
 		replaceButton.addEventListener("click", () => {
 			if (!container.classList.contains("refreshing")) {
 				if (V.aiCachingStrategy === 'reactive') {
-					container.classList.add("refreshing");
 					reactiveSpecific.refresh({forceRegenerate: true});
 				} else { // static
 					if (slave.custom.aiImageIds.length === 0) {
diff --git a/src/art/genAI/buildPrompt.js b/src/art/genAI/buildPrompt.js
index e7669cfc805d7713f006d4bbac7938bf4fbe77fd..d34b66e7b2063ff90d9e249ff70e09211a965977 100644
--- a/src/art/genAI/buildPrompt.js
+++ b/src/art/genAI/buildPrompt.js
@@ -20,6 +20,7 @@ function buildPrompt(slave) {
 		new App.Art.GenAI.ClothesPromptPart(slave),
 		new App.Art.GenAI.CollarPromptPart(slave),
 		new App.Art.GenAI.BreastsPromptPart(slave),
+		new App.Art.GenAI.FakeBoobsPromptPart(slave),
 		new App.Art.GenAI.WaistPromptPart(slave),
 		new App.Art.GenAI.HipsPromptPart(slave),
 		new App.Art.GenAI.HairPromptPart(slave),
diff --git a/src/art/genAI/openPose.js b/src/art/genAI/openPose.js
index df339675474ce40336740472a8d5981cc73b7b7d..354f08677932405432bf68e6db1b4c58327edda9 100644
--- a/src/art/genAI/openPose.js
+++ b/src/art/genAI/openPose.js
@@ -31,9 +31,9 @@ App.Art.GenAI.getOpenPoseData = (function() {
 				}
 			} else {
 				// TODO: pick a pose programmatically. should align with the prompts in PosturePromptPart, otherwise weirdness will ensue.
-				return null; // for now, bail out here
-				let pose = "Standing, Neutral";
-				return poseFromLibrary(pose);
+				return null; // for now, bail out here - lines below dummied out to prevent "unreachable code after return" debugger complaints
+				// let pose = "Standing, Neutral";
+				// return poseFromLibrary(pose);
 			}
 		}
 		return null;
diff --git a/src/art/genAI/prompts/androidPromptPart.js b/src/art/genAI/prompts/androidPromptPart.js
index eed5641b2308b2c2f85dd4b8006a0c569264764c..ed4556ab9dba212c9cc4a248cfa49251f097fca6 100644
--- a/src/art/genAI/prompts/androidPromptPart.js
+++ b/src/art/genAI/prompts/androidPromptPart.js
@@ -3,7 +3,7 @@ App.Art.GenAI.AndroidPromptPart = class AndroidPromptPart extends App.Art.GenAI.
 	 * @override
 	 */
 	positive() {
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			return; // limbs covered by fuckdoll suit
 		}
 		if (V.aiLoraPack) {
@@ -21,7 +21,7 @@ App.Art.GenAI.AndroidPromptPart = class AndroidPromptPart extends App.Art.GenAI.
 	 * @override
 	 */
 	negative() {
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			return; // limbs covered by fuckdoll suit
 		}
 		if (V.aiLoraPack) {
diff --git a/src/art/genAI/prompts/arousalPromptPart.js b/src/art/genAI/prompts/arousalPromptPart.js
index 1d2dd000c1140c191eea5513356e2938df29368a..7b133238092353b518bbae021f492db66ce4c569 100644
--- a/src/art/genAI/prompts/arousalPromptPart.js
+++ b/src/art/genAI/prompts/arousalPromptPart.js
@@ -4,7 +4,7 @@ App.Art.GenAI.ArousalPromptPart = class ArousalPromptPart extends App.Art.GenAI.
 	 */
 	positive() {
 		let prompt = {terms: [], weight: 1};
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			// fuckdolls are kept in a state of permanent arousal, with genitals exposed
 			if (this.slave.vagina >= 0) {
 				prompt.terms.push("pussy juice");
diff --git a/src/art/genAI/prompts/beautyPromptPart.js b/src/art/genAI/prompts/beautyPromptPart.js
index 17198643ee4bbc7eba84e5f424298818c8db1f7f..29f1142229b9528fedc404d04a45b47760b0aa6b 100644
--- a/src/art/genAI/prompts/beautyPromptPart.js
+++ b/src/art/genAI/prompts/beautyPromptPart.js
@@ -3,7 +3,7 @@ App.Art.GenAI.BeautyPromptPart = class BeautyPromptPart extends App.Art.GenAI.Pr
 	 * @override
 	 */
 	positive() {
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			return undefined; // face not visible
 		}
 
@@ -24,7 +24,7 @@ App.Art.GenAI.BeautyPromptPart = class BeautyPromptPart extends App.Art.GenAI.Pr
 	 * @override
 	 */
 	negative() {
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			return undefined; // face not visible
 		}
 
diff --git a/src/art/genAI/prompts/clothesPromptPart.js b/src/art/genAI/prompts/clothesPromptPart.js
index c67b9195301d9c4b2cdf76af0406119f3c49d127..234b8d46357e7d0caade6c2f036c7b9b04c049f4 100644
--- a/src/art/genAI/prompts/clothesPromptPart.js
+++ b/src/art/genAI/prompts/clothesPromptPart.js
@@ -510,7 +510,7 @@ App.Art.GenAI.ClothesPromptPart = class ClothesPromptPart extends App.Art.GenAI.
 	 */
 	colorReplacer(prompt, colors) {
 		if (colors && prompt.includes('$color')) {
-			const color = colors[Math.floor(Math.random() * colors.length)];
+			const color = colors[this.slave.natural.artSeed % colors.length];
 			return prompt.replaceAll('$color', color);
 		}
 		return prompt;
diff --git a/src/art/genAI/prompts/collarPromptPart.js b/src/art/genAI/prompts/collarPromptPart.js
index bbc21126f91f345c1db4ba1a4ce23fa630c831eb..787fdbf6e32f42a382f757f09bd3833ebb5ff9f1 100644
--- a/src/art/genAI/prompts/collarPromptPart.js
+++ b/src/art/genAI/prompts/collarPromptPart.js
@@ -3,10 +3,23 @@ App.Art.GenAI.CollarPromptPart = class CollarPromptPart extends App.Art.GenAI.Pr
 	 * @override
 	 */
 	positive() {
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			return undefined; // fuckdolls can't wear collars
 		}
-		if (this.slave.collar !== "none") {
+
+		if (this.slave.collar === "bell collar") {  // Doesn't work well, better than "bell collar collar"
+			return "bell collar";
+		} else if (this.slave.collar === "bowtie") {
+			return "bowtie, collar";
+		} else if (this.slave.collar === "leather with cowbell") {  // Doesn't work well, better than "leather with cowbell collar"
+			return "leather collar, cowbell around neck";
+		} else if (this.slave.collar === "neck corset") { // Doesn't work well, but doesn't add real corsets
+			return "tall leather collar, tight collar";
+		} else if (this.slave.collar === "neck tie") {
+			return "(necktie:1.2), collar";
+		} else if (this.slave.collar === "satin choker") {
+			return "satin choker";
+		} else if (this.slave.collar !== "none") {
 			return `${this.slave.collar} collar`;
 		}
 	}
diff --git a/src/art/genAI/prompts/customPromptPart.js b/src/art/genAI/prompts/customPromptPart.js
index f1d098ef92bf6c615f7bf4527c1e137d0de23f5d..b080d950b4be54d7d0b9ee97427d4a60b7836bb6 100644
--- a/src/art/genAI/prompts/customPromptPart.js
+++ b/src/art/genAI/prompts/customPromptPart.js
@@ -3,8 +3,9 @@ App.Art.GenAI.CustomPromptPart = class CustomPromptPart extends App.Art.GenAI.Pr
 	 * @override
 	 */
 	positive() {
-		if (this.slave.custom.aiPrompts?.positive) {
-			return this.slave.custom.aiPrompts.positive;
+		const customPrompt = asSlave(this.slave)?.custom.aiPrompts?.positive;
+		if (customPrompt) {
+			return customPrompt;
 		}
 		return undefined;
 	}
@@ -13,8 +14,9 @@ App.Art.GenAI.CustomPromptPart = class CustomPromptPart extends App.Art.GenAI.Pr
 	 * @override
 	 */
 	negative() {
-		if (this.slave.custom.aiPrompts?.negative) {
-			return this.slave.custom.aiPrompts.negative;
+		const customPrompt = asSlave(this.slave)?.custom.aiPrompts?.negative;
+		if (customPrompt) {
+			return customPrompt;
 		}
 		return undefined;
 	}
diff --git a/src/art/genAI/prompts/expressionPromptPart.js b/src/art/genAI/prompts/expressionPromptPart.js
index a4718891e34bfd62273f52c9f00138fd640b26ed..930b8a9d4850a9100980c893b5882eff1fcbefdc 100644
--- a/src/art/genAI/prompts/expressionPromptPart.js
+++ b/src/art/genAI/prompts/expressionPromptPart.js
@@ -3,8 +3,13 @@ App.Art.GenAI.ExpressionPromptPart = class ExpressionPromptPart extends App.Art.
 	 * @override
 	 */
 	positive() {
-		if (V.aiLoraPack && this.slave.fuckdoll !== 0) {
-			if (this.slave.fuckdoll < 50) {
+		const customPrompt = asSlave(this.slave)?.custom.aiPrompts?.expressionPositive;
+		if (customPrompt) {
+			return customPrompt;
+		}
+
+		if (V.aiLoraPack && asSlave(this.slave)?.fuckdoll !== 0) {
+			if (asSlave(this.slave)?.fuckdoll < 50) {
 				return `open mouth, clenched fists`; // NG proxy for terrified for early adaptation
 			} else {
 				return undefined;
@@ -26,22 +31,25 @@ App.Art.GenAI.ExpressionPromptPart = class ExpressionPromptPart extends App.Art.
 			}
 
 			let trustPart;
-			if (this.slave.trust < -90) {
-				trustPart = `(scared expression:1.2), looking down, crying, tears`;
-			}
-			if (this.slave.trust < -50) {
-				trustPart = `(scared expression:1.1), looking down, crying`;
-			} else if (this.slave.trust < -20) {
-				trustPart = `scared expression, looking down`;
-			} else if (this.slave.trust < 51) {
-				trustPart = `looking at viewer`;
-				if (!devotionPart) {
-					trustPart += `, neutral expression`;
+			const slaveWithTrust = asSlave(this.slave);
+			if (slaveWithTrust) {
+				if (slaveWithTrust.trust < -90) {
+					trustPart = `(scared expression:1.2), looking down, crying, tears`;
+				}
+				if (slaveWithTrust.trust < -50) {
+					trustPart = `(scared expression:1.1), looking down, crying`;
+				} else if (slaveWithTrust.trust < -20) {
+					trustPart = `scared expression, looking down`;
+				} else if (slaveWithTrust.trust < 51) {
+					trustPart = `looking at viewer`;
+					if (!devotionPart) {
+						trustPart += `, neutral expression`;
+					}
+				} else if (slaveWithTrust.trust < 95) {
+					trustPart = `looking at viewer, confident`;
+				} else {
+					trustPart = `looking at viewer, confident, smirk`;
 				}
-			} else if (this.slave.trust < 95) {
-				trustPart = `looking at viewer, confident`;
-			} else {
-				trustPart = `looking at viewer, confident, smirk`;
 			}
 
 			if (devotionPart && trustPart) {
@@ -58,7 +66,12 @@ App.Art.GenAI.ExpressionPromptPart = class ExpressionPromptPart extends App.Art.
 	 * @override
 	 */
 	negative() {
-		if (V.aiLoraPack && this.slave.fuckdoll !== 0) {
+		const customPrompt = asSlave(this.slave)?.custom.aiPrompts?.expressionNegative;
+		if (customPrompt) {
+			return customPrompt;
+		}
+
+		if (V.aiLoraPack && asSlave(this.slave)?.fuckdoll !== 0) {
 			 return `smile, angry, confident`;
 		} else if (V.aiLoraPack && this.slave.fetish === Fetish.MINDBROKEN) {
 			 return `smile, angry, looking at viewer, confident`;
@@ -75,12 +88,15 @@ App.Art.GenAI.ExpressionPromptPart = class ExpressionPromptPart extends App.Art.
 			}
 
 			let trustPart;
-			if (this.slave.trust < -50) {
-				trustPart = `looking at viewer, confident`;
-			} else if (this.slave.trust < -20) {
-				trustPart = null;
-			} else {
-				trustPart = `looking away`;
+			const slaveWithTrust = asSlave(this.slave);
+			if (slaveWithTrust) {
+				if (slaveWithTrust.trust < -50) {
+					trustPart = `looking at viewer, confident`;
+				} else if (slaveWithTrust.trust < -20) {
+					trustPart = null;
+				} else {
+					trustPart = `looking away`;
+				}
 			}
 
 			if (devotionPart && trustPart) {
diff --git a/src/art/genAI/prompts/eyePromptPart.js b/src/art/genAI/prompts/eyePromptPart.js
index 4bd49617ea1bf602b6c35f1afaee96d7bcc39cfc..2a670c673d997b1981ace8510f16c400317afa62 100644
--- a/src/art/genAI/prompts/eyePromptPart.js
+++ b/src/art/genAI/prompts/eyePromptPart.js
@@ -3,7 +3,7 @@ App.Art.GenAI.EyePromptPart = class EyePromptPart extends App.Art.GenAI.PromptPa
 	 * @override
 	 */
 	positive() {
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			return undefined; // eyes are not visible behind fuckdoll mask
 		}
 		if (hasBothEyes(this.slave)) {
diff --git a/src/art/genAI/prompts/eyebrowPromptPart.js b/src/art/genAI/prompts/eyebrowPromptPart.js
index 2ce796e2e364220639048f999d3047cf6bd5463f..c37eba4e7e1c0e7278ecafa5d3180008dfe2fbf7 100644
--- a/src/art/genAI/prompts/eyebrowPromptPart.js
+++ b/src/art/genAI/prompts/eyebrowPromptPart.js
@@ -3,7 +3,7 @@ App.Art.GenAI.EyebrowPromptPart = class EyebrowPromptPart extends App.Art.GenAI.
 	 * @override
 	 */
 	positive() {
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			return; // covered by fuckdoll mask
 		}
 		if (this.slave.eyebrowHStyle === "shaved" || this.slave.eyebrowHStyle === "bald" || this.slave.eyebrowHStyle === "hairless") {
diff --git a/src/art/genAI/prompts/fakeBoobsPromptPart.js b/src/art/genAI/prompts/fakeBoobsPromptPart.js
new file mode 100644
index 0000000000000000000000000000000000000000..9fc1e7b09d276e9aba132534f39663da8acebd34
--- /dev/null
+++ b/src/art/genAI/prompts/fakeBoobsPromptPart.js
@@ -0,0 +1,49 @@
+App.Art.GenAI.FakeBoobsPromptPart = class FakeBoobsPromptPart extends App.Art.GenAI.PromptPart {
+	/**
+	 * @override
+	 */
+	positive() {
+		if (V.aiLoraPack) {
+			if(this.slave.boobsImplant >= 1000)
+			{
+				return `fake tits, <lora:hugefaketits1:1>`;
+			} else if(this.slave.boobsImplant >= 900)
+			{
+				return `fake tits, <lora:hugefaketits1:0.9>`;
+			} else if(this.slave.boobsImplant >= 800)
+			{
+				return `fake tits, <lora:hugefaketits1:0.8>`;
+			} else if(this.slave.boobsImplant >= 700)
+			{
+				return `fake tits, <lora:hugefaketits1:0.7>`;
+			} else if(this.slave.boobsImplant >= 600)
+			{
+				return `fake tits, <lora:hugefaketits1:0.6>`;
+			} else if(this.slave.boobsImplant >= 500)
+			{
+				return `fake tits, <lora:hugefaketits1:0.5>`;
+			} else if(this.slave.boobsImplant >= 400)
+			{
+				return `fake tits, <lora:hugefaketits1:0.4>`;
+			} else if(this.slave.boobsImplant >= 300)
+			{
+				return `fake tits, <lora:hugefaketits1:0.3>`;
+			} else if(this.slave.boobsImplant >= 200)
+			{
+				return `fake tits, <lora:hugefaketits1:0.2>`;
+			}
+		}
+	}
+
+	/**
+	 * @override
+	 */
+	negative() {
+		if (V.aiLoraPack) {
+			if (!this.slave.boobsImplant == false) {
+				return `fake tits`; // Space for negative prompt if needed NG
+			}
+		}
+		return;
+	}
+};
diff --git a/src/art/genAI/prompts/healthPromptPart.js b/src/art/genAI/prompts/healthPromptPart.js
index 3d9a9d179582bbd8616348965f5a4a85a76a3137..a30768ef2f8814d0c602f32ff82b5da5b0389499 100644
--- a/src/art/genAI/prompts/healthPromptPart.js
+++ b/src/art/genAI/prompts/healthPromptPart.js
@@ -3,7 +3,7 @@ App.Art.GenAI.HealthPromptPart = class HealthPromptPart extends App.Art.GenAI.Pr
 	 * @override
 	 */
 	positive() {
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			return undefined;
 		}
 		if (this.slave.health.condition < -90) {
@@ -23,7 +23,7 @@ App.Art.GenAI.HealthPromptPart = class HealthPromptPart extends App.Art.GenAI.Pr
 	 * @override
 	 */
 	negative() {
-		if (this.slave.fuckdoll > 0) {
+		if (asSlave(this.slave)?.fuckdoll > 0) {
 			return undefined;
 		}
 		if (this.slave.health.condition > 50) {
diff --git a/src/art/genAI/prompts/nationalityPromptPart.js b/src/art/genAI/prompts/nationalityPromptPart.js
index fb14e9bafa2071c168e1e70e50a89a50fcc72862..0c29470968351141857585f0ce9febd3a8e1ce75 100644
--- a/src/art/genAI/prompts/nationalityPromptPart.js
+++ b/src/art/genAI/prompts/nationalityPromptPart.js
@@ -29,7 +29,7 @@ App.Art.GenAI.NationalityPromptPart = class NationalityPromptPart extends App.Ar
 	 * @override
 	 */
 	positive() {
-		if (["Stateless", "none", "slave", ""].includes(this.slave.nationality) || this.slave.fuckdoll > 0) {
+		if (["Stateless", "none", "slave", ""].includes(this.slave.nationality) || asSlave(this.slave)?.fuckdoll > 0) {
 			return;
 		}
 		if (this.slave.nationality.endsWith("Revivalist")) {
diff --git a/src/art/genAI/prompts/piercingsPromptPart.js b/src/art/genAI/prompts/piercingsPromptPart.js
index a66c57f065d74557081cafbfc8f23c4b3df59345..cbe0d13c567c2f29f28292cc167b3392b237bbbb 100644
--- a/src/art/genAI/prompts/piercingsPromptPart.js
+++ b/src/art/genAI/prompts/piercingsPromptPart.js
@@ -3,21 +3,23 @@ App.Art.GenAI.PiercingsPromptPart = class PiercingsPromptPart extends App.Art.Ge
 	 * @override
 	 */
 	positive() {
+		const isFuckdoll = asSlave(this.slave)?.fuckdoll !== 0;
+
 		let piercingParts = [];
 		if (this.slave.piercing.areola.weight > 0) {
-			if (this.slave.fuckdoll === 0 || this.slave.race === "catgirl") { // TODO: needs exposure check
+			if (!isFuckdoll || this.slave.race === "catgirl") { // TODO: needs exposure check
 				let desc = this.slave.piercing.areola.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.areola.desc) + ` `) : ``;
 				piercingParts.push(`${desc}areola piercing`);
 			}
 		}
 		if (this.slave.piercing.ear.weight > 0) {
-			if (this.slave.fuckdoll === 0 || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur
+			if (!isFuckdoll || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur
 				let desc = this.slave.piercing.ear.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.ear.desc) + ` `) : ``;
 				piercingParts.push(`${desc}earrings`);
 			}
 		}
 		if (this.slave.piercing.eyebrow.weight > 0) {
-			if (this.slave.fuckdoll === 0 || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur
+			if (!isFuckdoll || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur
 				let desc = this.slave.piercing.eyebrow.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.eyebrow.desc) + ` `) : ``;
 				piercingParts.push(`${desc}eyebrow piercing`);
 			}
@@ -27,19 +29,19 @@ App.Art.GenAI.PiercingsPromptPart = class PiercingsPromptPart extends App.Art.Ge
 			piercingParts.push(`${desc}lip piercing`);
 		}
 		if (this.slave.piercing.navel.weight > 0) {
-			if (this.slave.fuckdoll === 0 || this.slave.race === "catgirl") { // covered by fuckdoll suit or fur
+			if (!isFuckdoll || this.slave.race === "catgirl") { // covered by fuckdoll suit or fur
 				let desc = this.slave.piercing.navel.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.navel.desc) + ` `) : ``;
 				piercingParts.push(`${desc}navel piercing`);
 			}
 		}
 		if (this.slave.piercing.nipple.weight > 0) {
-			if (this.slave.fuckdoll === 0) { // TODO: needs exposure check
+			if (!isFuckdoll) { // TODO: needs exposure check
 				let desc = this.slave.piercing.nipple.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.nipple.desc) + ` `) : ``;
 				piercingParts.push(`${desc}nipple piercing`);
 			}
 		}
 		if (this.slave.piercing.nose.weight > 0) {
-			if (this.slave.fuckdoll === 0 || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur
+			if (!isFuckdoll || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur
 				let desc = this.slave.piercing.nose.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.nose.desc) + ` `) : ``;
 				piercingParts.push(`${desc}nose piercing`);
 			}
diff --git a/src/art/genAI/prompts/posturePromptPart.js b/src/art/genAI/prompts/posturePromptPart.js
index d3f77551e924374d803d6f5025a297110b473f40..45f74e8e19e3eca0f3ce080919cdd46f3bbfdeb9 100644
--- a/src/art/genAI/prompts/posturePromptPart.js
+++ b/src/art/genAI/prompts/posturePromptPart.js
@@ -3,53 +3,59 @@ App.Art.GenAI.PosturePromptPart = class PosturePromptPart extends App.Art.GenAI.
 	 * @override
 	 */
 	positive() {
-		if (this.slave.custom.aiPrompts?.pose) {
-			return this.slave.custom.aiPrompts.pose;
+		const customPrompt = asSlave(this.slave)?.custom.aiPrompts?.pose;
+		if (customPrompt) {
+			return customPrompt;
 		}
 
-		let devotionPart;
-		if (this.slave.fuckdoll !== 0) {
-			let lora = ``;
+		const parts = [];
+
+		if (isAmputee(this.slave)) {
+			parts.push(`sitting in chair`); // posture change prevents genning arms/legs, looks more natural
+		} else if (asSlave(this.slave)?.fuckdoll !== 0) {
 			if (V.aiLoraPack && !V.aiOpenPose) { // always prefer OpenPose over lora; less side effects
-				lora = `<lora:Standing straight  - arms at sides - legs together - v1 - locon 32dim:1>`;
+				parts.push(`<lora:Standing straight  - arms at sides - legs together - v1 - locon 32dim:1>`);
 			}
-			devotionPart = `${lora} standing straight`;
-		} else if (this.slave.devotion < -50) {
-			devotionPart = `standing, from side, arms crossed`;
-		} else if (this.slave.devotion < -20) {
-			devotionPart = `standing, arms crossed`;
-		} else if (this.slave.devotion < 21) {
-			devotionPart = `standing`;
+			parts.push(`standing straight`);
+		} else if (canStand(this.slave)) {
+			parts.push(`standing`);
 		} else {
-			devotionPart = `standing, arms behind back`;
+			parts.push(`kneeling`);
 		}
 
-		if (isAmputee(this.slave)) {
-			return devotionPart.replace(/( *)standing(,)*/g, "sitting in chair,"); // posture change prevents genning arms/legs, looks more natural
+		if (this.slave.devotion < -50) {
+			parts.push(`from side, arms crossed`);
+		} else if (this.slave.devotion < -20) {
+			parts.push(`arms crossed`);
+		} else if (this.slave.devotion < 21) {
+			// parts.push(`standing`);
+		} else {
+			parts.push(`arms behind back, from front`);
 		}
 
-		let trustPart;
-		if (this.slave.fuckdoll !== 0) {
-			trustPart = ``;
-		} else if (this.slave.trust < -50) {
-			trustPart = `trembling, head down`;
-		} else if (this.slave.trust < -20) {
-			trustPart = `trembling`;
+		if (asSlave(this.slave)?.fuckdoll !== 0) {
+			// trustPart = ``;
+		} else if (asSlave(this.slave)?.trust < -50) {
+			parts.push(`trembling, head down`);
+		} else if (asSlave(this.slave)?.trust < -20) {
+			parts.push(`trembling`);
 		}
 
-		if (devotionPart && trustPart) {
-			return `${devotionPart}, ${trustPart}`;
-		} else if (devotionPart) {
-			return devotionPart;
-		} else if (trustPart) {
-			return trustPart;
-		}
+		return parts.join(`, `);
 	}
 
 	/**
 	 * @override
 	 */
 	negative() {
+		if (asSlave(this.slave)?.custom.aiPrompts?.pose) {
+			return undefined;
+		}
+
+		if (!isAmputee(this.slave) && !canWalk(this.slave)) {
+			return 'from above';
+		}
+
 		return undefined;
 	}
 };
diff --git a/src/art/genAI/prompts/pregPromptPart.js b/src/art/genAI/prompts/pregPromptPart.js
index f62cb9b90b61ab0853b2e684836b61d7cc5795cc..7c7a6daba374342f24fac2936b12c217ab047f61 100644
--- a/src/art/genAI/prompts/pregPromptPart.js
+++ b/src/art/genAI/prompts/pregPromptPart.js
@@ -6,9 +6,11 @@ App.Art.GenAI.PregPromptPart = class PregPromptPart extends App.Art.GenAI.Prompt
 		if (this.slave.belly >= 10000) {
 			return "pregnant, full term";
 		} else if (this.slave.belly >= 5000) {
-			return "pregnant";
+			return "[pregnant:0.3]";
 		} else if (this.slave.belly >= 1500) {
-			return "baby bump";
+			return "[pregnant:0.5]";
+		} else if (this.slave.belly >= 100) {
+			return "bloated, [pregnant:0.8]";
 		}
 	}
 
diff --git a/src/art/genAI/prompts/pubicHairPromptPart.js b/src/art/genAI/prompts/pubicHairPromptPart.js
index 14144446b5d1da4004b99a298f761ca1c75009cb..9603f2190bb0d2b9fd9c2eaf916ffc034742b974 100644
--- a/src/art/genAI/prompts/pubicHairPromptPart.js
+++ b/src/art/genAI/prompts/pubicHairPromptPart.js
@@ -6,7 +6,7 @@ App.Art.GenAI.PubicHairPromptPart = class PubicHairPromptPart extends App.Art.Ge
 		if (this.slave.pubicHStyle === "waxed" || this.slave.pubicHStyle === "bald" || this.slave.pubicHStyle === "hairless" || this.slave.physicalAge < Math.min(this.slave.pubertyAgeXX, this.slave.pubertyAgeXY)) {
 			return;
 		}
-		if (App.Data.clothes.get(this.slave.clothes).exposure < 3 || this.slave.fuckdoll > 0) {
+		if (App.Data.clothes.get(this.slave.clothes).exposure < 3 || asSlave(this.slave)?.fuckdoll > 0) {
 			return; // pubic region should be covered by clothes
 		}
 		const style = (this.slave.pubicHStyle === "bushy in the front and neat in the rear" ? "bushy" : this.slave.pubicHStyle); // less complicated prompt works better for the long style
diff --git a/src/art/genAI/prompts/tattoosPromptPart.js b/src/art/genAI/prompts/tattoosPromptPart.js
index e7cbf0fd7bcd73f7c949dacabb4b5d3b563d9a92..925c0401d5bd795fa3e6d8fbfb8b890158ff7bf0 100644
--- a/src/art/genAI/prompts/tattoosPromptPart.js
+++ b/src/art/genAI/prompts/tattoosPromptPart.js
@@ -3,7 +3,7 @@ App.Art.GenAI.TattoosPromptPart = class TattoosPromptPart extends App.Art.GenAI.
 	 * @override
 	 */
 	positive() {
-		if (this.slave.fuckdoll > 0 || this.slave.race === "catgirl") {
+		if (asSlave(this.slave)?.fuckdoll > 0 || this.slave.race === "catgirl") {
 			return undefined; // fuckdoll suit covers all possible tattoo locations, catgirl covered with fur
 		}
 		// TODO: clothes can cover limbs/belly/boobs.
diff --git a/src/art/genAI/reactiveImageDB.js b/src/art/genAI/reactiveImageDB.js
index 01aec0eb734a3999ab7cfda036c0b2855bdc8b93..c22321ef547fee7bbd611620ce18d0107722a3a4 100644
--- a/src/art/genAI/reactiveImageDB.js
+++ b/src/art/genAI/reactiveImageDB.js
@@ -121,8 +121,14 @@ App.Art.GenAI.reactiveImageDB = (function() {
 	 */
 	async function generateNewImage(slaves, options) {
 		// {isEventImage: options.isEventImage, action: options.action}
+		const slave = {
+			// This can un-proxy a slave so it can be stored in IndexedDB
+			...slaves[0],
+			// This gets turned into a function sometimes and that breaks storing in IndexedDB
+			clone: typeof slaves[0].clone === "function" ? 0 : slaves[0].clone
+		};
 		/** @type {string} */
-		const base64Image = await App.Art.GenAI.reactiveCache.fetchImageForSlave(slaves[0], options.isEventImage);
+		const base64Image = await App.Art.GenAI.reactiveCache.fetchImageForSlave(slave, options.isEventImage);
 		return getImageData(base64Image);
 	}
 
@@ -306,8 +312,8 @@ App.Art.GenAI.reactiveImageDB = (function() {
 					};
 				} else if (currentRecord.averageDifference === prevRecord.averageDifference) {
 					prevRecord.matches.push(currentRecord.entry);
-					return prevRecord;
 				}
+				return prevRecord;
 			}, {matches: [], averageDifference: Number.MAX_SAFE_INTEGER});
 		// to regenerate, we need an exact match.
 		if (options.forceRegenerate && fuzzyResults.averageDifference > 0) {
@@ -326,73 +332,115 @@ App.Art.GenAI.reactiveImageDB = (function() {
 	 * @param {Partial<App.Art.GenAI.GetImageOptions>} [options] Misc options.
 	 * Defaults: action='overview', size=App.Art.ArtSizes.SMALL, forceRegenerate: false, isEventImage: false
 	 *
-	 * @returns {Promise<App.Art.GenAI.EventStore.Entry>} Promise object that resolves with the retrieved image data
+	 * @returns {FC.PromiseWithProgress<App.Art.GenAI.EventStore.Entry>} Promise object that resolves with the retrieved image data
 	 */
-	async function getImage(slaves, options = {}) {
-		await waitForInit();
-
-		/** @type {App.Art.GenAI.GetImageOptions} */
-		const effectiveOptions = {
-			/** @type {App.Art.GenAI.Action} */
-			action: 'overview',
-			size: App.Art.ArtSizes.SMALL,
-			forceRegenerate: false,
-			isEventImage: false,
-			...options
-		};
-
-		// Data is optional
-		/** @type {Omit<App.Art.GenAI.EventStore.Entry, "data"> & { data?: App.Art.GenAI.EventStore.DataType}} */
-		let event = {
-			slaveIds: slaves.map(s => s.ID).sort(),
-			seed: slaves[0].natural.artSeed,
-			slaveStates: JSON.parse(JSON.stringify(slaves)),
-			action: effectiveOptions.action,
-		};
-
-		// look for identical event
-		/** @type {App.Art.GenAI.EventStore.Entry[]} */
-		const eventEntries = await db.getAllFromIndex(EVENT_STORE.path, EVENT_STORE.indicies.bySlaveIdsActions, IDBKeyRange.only([event.slaveIds, event.action]));
-
-		const {matches, averageDifference} = findClosestEvents(slaves, eventEntries, effectiveOptions);
-		const shouldUseCache = (averageDifference <= SIGNIFICANTLY_DIFFERENT_THRESHOLD) && !effectiveOptions.forceRegenerate;
-		const isExactMatch = averageDifference === 0;
-		const chosenEvent = matches[Math.floor(Math.random() * matches.length)];
-
-		// Use the cached value
-		if (matches?.length > 0 && shouldUseCache) {
-			// Any of the allowed entries will work. Return a random one.
-			return matches[Math.floor(Math.random() * matches.length)];
-		}
-
-		const base64Image = await generateNewImage(slaves, effectiveOptions);
+	function getImage(slaves, options = {}) {
+		const progressFns = [];
+		const result = Object.assign(
+			new Promise((resolve, reject) => {
+				(async () => {
+					await waitForInit();
+
+					/** @type {App.Art.GenAI.GetImageOptions} */
+					const effectiveOptions = {
+						/** @type {App.Art.GenAI.Action} */
+						action: 'overview',
+						size: App.Art.ArtSizes.SMALL,
+						forceRegenerate: false,
+						isEventImage: false,
+						...options
+					};
 
-		/** @type {App.Art.GenAI.EventStore.Entry} */
-		// @ts-expect-error
-		let fullEvent = event;
-		if (isExactMatch) {
-			// fill in with previous data
-			fullEvent = {
-				...event,
-				...chosenEvent
-			};
-		}
+					// Data is optional
+					/** @type {Omit<App.Art.GenAI.EventStore.Entry, "data"> & { data?: App.Art.GenAI.EventStore.DataType}} */
+					let event = {
+						slaveIds: slaves.map(s => s.ID).sort(),
+						seed: slaves[0].natural.artSeed,
+						slaveStates: slaves.map(slave => ({
+							// This can un-proxy a slave so it can be stored in IndexedDB
+							...slave,
+							// This gets turned into a function sometimes and that breaks storing in IndexedDB
+							clone: typeof slave.clone === "function" ? 0 : slave.clone,
+						})),
+						action: effectiveOptions.action,
+					};
 
-		if (event.action === 'overview') {
-			fullEvent.data = {
-				images: {
-					lowRes: base64Image
+					// look for identical event
+					/** @type {App.Art.GenAI.EventStore.Entry[]} */
+					const eventEntries = await db.getAllFromIndex(EVENT_STORE.path, EVENT_STORE.indicies.bySlaveIdsActions, IDBKeyRange.only([event.slaveIds, event.action]));
+
+					const {matches, averageDifference} = findClosestEvents(slaves, eventEntries, effectiveOptions);
+					const shouldUseCache = (averageDifference <= SIGNIFICANTLY_DIFFERENT_THRESHOLD) && !effectiveOptions.forceRegenerate;
+					const isExactMatch = averageDifference === 0;
+					const chosenEvent = matches[Math.floor(Math.random() * matches.length)];
+
+					// Use the cached value
+					if (matches?.length > 0 && shouldUseCache) {
+						// Any of the allowed entries will work. Return a random one.
+						return matches[Math.floor(Math.random() * matches.length)];
+					}
+
+					const base64Image = await generateNewImage(slaves, effectiveOptions);
+
+					/** @type {App.Art.GenAI.EventStore.Entry} */
+					// @ts-expect-error
+					let fullEvent = event;
+					if (isExactMatch) {
+						// fill in with previous data
+						fullEvent = {
+							...event,
+							...chosenEvent
+						};
+					}
+
+					if (event.action === 'overview') {
+						fullEvent.data = {
+							images: {
+								lowRes: base64Image
+							}
+						};
+					}
+
+					// save it to the DB, unless it's temporary
+					if (!fullEvent.slaveIds.includes(0)) {
+						await db.put(EVENT_STORE.path, fullEvent);
+					}
+
+					return fullEvent;
+				})().then(resolve).catch(reject);
+			}),
+			{
+				/**
+				 * Do something when there's progress on generating an image
+				 * @param {(progress: number) => void} fn A function to call when there's progress
+				 * @returns {FC.PromiseWithProgress<App.Art.GenAI.EventStore.Entry>}
+				 */
+				onProgress(fn) {
+					progressFns.push(fn);
+					return result;
 				}
-			};
-		}
-
-		// save it to the DB, unless it's temporary
-		if (!fullEvent.slaveIds.includes(0)) {
-			await db.put(EVENT_STORE.path, fullEvent);
-		}
-
+			}
+		);
+
+		const interval = setInterval(async () => {
+			// Sometimes the same slave has multiple images being generated, like in the dressing room
+			if (slaves.map((slave) => slave.ID).includes(App.Art.GenAI.sdQueue.workingOnID)) {
+				const response = await fetch('https://home.local:9728/sdapi/v1/progress?skip_current_image=true', {
+					method: 'GET',
+					headers: [
+						['accept', 'application/json'],
+					],
+				});
+				const progress = (await response.json()).progress;
+				progressFns.forEach((fn) => fn(progress));
+			}
+		}, 1000);
+		result.finally(() => {
+			clearInterval(interval);
+			progressFns.forEach((fn) => fn(1));
+		});
 
-		return fullEvent;
+		return result;
 	}
 
 	/**
diff --git a/src/art/genAI/stableDiffusion.js b/src/art/genAI/stableDiffusion.js
index b2bc39b4bb01f7d2e680ae8a4979e6ef9e220f9d..c9e90ddbf55c578ac53cee5395fb16d4c2e36a93 100644
--- a/src/art/genAI/stableDiffusion.js
+++ b/src/art/genAI/stableDiffusion.js
@@ -343,6 +343,8 @@ App.Art.GenAI.StableDiffusionClient = class {
 			// API Docs: https://github.com/Bing-su/adetailer/wiki/API
 			alwaysOnScripts.ADetailer = {
 				"args": [
+					true, // ad_enable
+					true, // skip_img2img
 					{
 						"ad_model": "face_yolov8s.pt"
 					}
@@ -592,7 +594,12 @@ App.Art.GenAI.StaticCaching = class {
 			steps = V.aiSamplingStepsEvent;
 		}
 
-		const settings = await App.Art.GenAI.sdClient.buildStableDiffusionSettings(slave, steps);
+		let settingsSlave = slave;
+		if (V.aiUseRAForEvents && isEventImage) {
+			settingsSlave = structuredClone(slave);
+			DefaultRules(settingsSlave, {aiPromptOnly: true});
+		}
+		const settings = await App.Art.GenAI.sdClient.buildStableDiffusionSettings(settingsSlave, steps);
 		const body = JSON.stringify(settings);
 		// set up a passage switch handler to clear queued generation of event and temporary images upon passage change
 		const oldHandler = App.Utils.PassageSwitchHandler.get();
@@ -634,31 +641,71 @@ App.Art.GenAI.StaticCaching = class {
 	 * @param {FC.SlaveState} slave - The slave to update
 	 * @param {number | null} replacementImageIndex - If provided, replace the image at this index
 	 * @param {boolean | null} isEventImage - Whether request is canceled on passage change and which step setting to use. true => V.aiSamplingStepsEvent, false => V.aiSamplingSteps, null => chosen based on passage tags
+	 * @returns {FC.PromiseWithProgress<void>}
 	 */
-	async updateSlave(slave, replacementImageIndex = null, isEventImage = null) {
-		const base64Image = await this.fetchImageForSlave(slave, isEventImage);
-		const imageData = getImageData(base64Image);
-		const imagePreexisting = await compareExistingImages(slave, imageData);
-		let vSlave = globalThis.getSlave(slave.ID);
-		// if `slave` is owned but the variable has become detached from V.slaves, save the image changes to V.slaves instead
-		if (vSlave && slave !== vSlave) {
-			slave = vSlave;
-		}
-		// If new image, add or replace it in
-		if (imagePreexisting === -1) {
-			const imageId = await App.Art.GenAI.staticImageDB.putImage({data: imageData});
-			if (replacementImageIndex !== null) {
-				await App.Art.GenAI.staticImageDB.removeImage(slave.custom.aiImageIds[replacementImageIndex]);
-				slave.custom.aiImageIds[replacementImageIndex] = imageId;
-			} else {
-				slave.custom.aiImageIds.push(imageId);
-				slave.custom.aiDisplayImageIdx = slave.custom.aiImageIds.indexOf(imageId);
+	updateSlave(slave, replacementImageIndex = null, isEventImage = null) {
+		const progressFns = [];
+		const result = Object.assign(
+			new Promise((resolve, reject) => {
+				(async () => {
+					const base64Image = await this.fetchImageForSlave(slave, isEventImage);
+					const imageData = getImageData(base64Image);
+					const imagePreexisting = await compareExistingImages(slave, imageData);
+					if (!isEventImage) {
+						let vSlave = globalThis.getSlave(slave.ID);
+						// if `slave` is owned but the variable has become detached from V.slaves, save the image changes to V.slaves instead
+						// but don't do it for temporary images because they might be intentionally using a copy of a slave for temporary changes
+						if (vSlave && slave !== vSlave) {
+							slave = vSlave;
+						}
+					}
+					// If new image, add or replace it in
+					if (imagePreexisting === -1) {
+						const imageId = await App.Art.GenAI.staticImageDB.putImage({data: imageData});
+						if (replacementImageIndex !== null) {
+							await App.Art.GenAI.staticImageDB.removeImage(slave.custom.aiImageIds[replacementImageIndex]);
+							slave.custom.aiImageIds[replacementImageIndex] = imageId;
+						} else {
+							slave.custom.aiImageIds.push(imageId);
+							slave.custom.aiDisplayImageIdx = slave.custom.aiImageIds.indexOf(imageId);
+						}
+						// If image already exists, just update the display idx to it
+					} else {
+						console.log('Generated redundant image, no image stored');
+						slave.custom.aiDisplayImageIdx = imagePreexisting;
+					}
+				})().then(resolve).catch(reject);
+			}), {
+				/**
+				 * Do something when there's progress on generating an image
+				 * @param {(progress: number) => void} fn A function to call when there's progress
+				 * @returns {FC.PromiseWithProgress<void>}
+				 */
+				onProgress(fn) {
+					progressFns.push(fn);
+					return result;
+				}
 			}
-			// If image already exists, just update the display idx to it
-		} else {
-			console.log('Generated redundant image, no image stored');
-			slave.custom.aiDisplayImageIdx = imagePreexisting;
-		}
+		);
+
+		const interval = setInterval(async () => {
+			if (App.Art.GenAI.sdQueue.workingOnID === slave.ID) {
+				const response = await fetch('https://home.local:9728/sdapi/v1/progress?skip_current_image=true', {
+					method: 'GET',
+					headers: [
+						['accept', 'application/json'],
+					],
+				});
+				const progress = (await response.json()).progress;
+				progressFns.forEach((fn) => fn(progress));
+			}
+		}, 1000);
+		result.finally(() => {
+			clearInterval(interval);
+			progressFns.forEach((fn) => fn(1));
+		});
+
+		return result;
 	}
 };
 
@@ -685,7 +732,12 @@ App.Art.GenAI.ReactiveCaching = class {
 			steps = V.aiSamplingStepsEvent;
 		}
 
-		const settings = await App.Art.GenAI.sdClient.buildStableDiffusionSettings(slave, steps);
+		let settingsSlave = slave;
+		if (V.aiUseRAForEvents && isEventImage) {
+			settingsSlave = structuredClone(slave);
+			DefaultRules(settingsSlave, {aiPromptOnly: true});
+		}
+		const settings = await App.Art.GenAI.sdClient.buildStableDiffusionSettings(settingsSlave, steps);
 		const body = JSON.stringify(settings);
 		// set up a passage switch handler to clear queued generation of event and temporary images upon passage change
 		const oldHandler = App.Utils.PassageSwitchHandler.get();
diff --git a/src/data/backwardsCompatibility/backwardsCompatibility.js b/src/data/backwardsCompatibility/backwardsCompatibility.js
index f372f3fb1b60bd55d446bcbe8cc56b3a0f68391f..929277aa191ad95d029c27662a0523453bf8e0a8 100644
--- a/src/data/backwardsCompatibility/backwardsCompatibility.js
+++ b/src/data/backwardsCompatibility/backwardsCompatibility.js
@@ -269,6 +269,32 @@ App.Update.globalVariables = function(node) {
 			}
 		}
 
+		// variables related to the App.Events.PregnancyNotice event
+		V.pregnancyNotice = V.pregnancyNotice || {};
+		V.pregnancyNotice.enabled = V.pregnancyNotice.enabled || true;
+		V.pregnancyNotice.renderFetus = V.pregnancyNotice.renderFetus || true;
+		V.pregnancyNotice.processedSlaves = V.pregnancyNotice.processedSlaves || [];
+
+		// fix anything that has to do with fetuses
+		V.slaves.concat([V.PC]).forEach(human => {
+			human.womb.forEach(fetus => {
+				// variables releated to App.Events.PregnancyNotice
+				fetus.noticeData = fetus.noticeData || {};
+				fetus.noticeData.fate = fetus.noticeData.fate || (() => {
+					if (fetus.age < 2) {
+						return (fetus.reserve !== "") ? fetus.reserve : "undecided";
+					} else if (fetus.age < 6) {
+						return "wait";
+					} else {
+						return (fetus.reserve !== "") ? fetus.reserve : "nothing";
+					}
+				})();
+				fetus.noticeData.transplantReceptrix = fetus.noticeData.transplantReceptrix || 0;
+				fetus.noticeData.cheatAccordionCollapsed = fetus.noticeData.cheatAccordionCollapsed || true;
+				fetus.noticeData.child = fetus.noticeData.child || undefined;
+			});
+		});
+
 		// remove old settings or BC temporary
 		delete V.incubator.settings;
 	}
@@ -371,6 +397,7 @@ App.Update.globalVariables = function(node) {
 			V.UI.compressSocialEffects = 0;
 		}
 	}
+	V.addButtonsToSlaveLinks = V.addButtonsToSlaveLinks || true;
 
 	if (typeof V.taitorWeeks !== "undefined") {
 		V.traitorWeeks = V.taitorWeeks;
diff --git a/src/data/backwardsCompatibility/datatypeCleanup.js b/src/data/backwardsCompatibility/datatypeCleanup.js
index debc303dd9aadef0a4fa7228ac8117c2527d2098..2d5d0fb1517c51b0d52a9d2ca83b15c33ffd89c9 100644
--- a/src/data/backwardsCompatibility/datatypeCleanup.js
+++ b/src/data/backwardsCompatibility/datatypeCleanup.js
@@ -276,6 +276,11 @@ App.Entity.Utils.SlaveDataSchemeCleanup = (function() {
 				slave.custom.aiDisplayImageIdx = -1;
 			}
 		}
+
+		if (slave.custom.aiPrompts?.aiAutoRegenExclude) {
+			slave.custom.aiAutoRegenExclude = 1;
+			delete slave.custom.aiPrompts.aiAutoRegenExclude;
+		}
 	}
 
 	/**
@@ -1076,6 +1081,14 @@ globalThis.SlaveDatatypeCleanup = (function SlaveDatatypeCleanup() {
 				slave.custom.image = null;
 			}
 		}
+		if (slave.custom.aiPrompts) {
+			if (typeof slave.custom.aiPrompts.expressionPositive !== "string") {
+				slave.custom.aiPrompts.expressionPositive = "";
+			}
+			if (typeof slave.custom.aiPrompts.expressionNegative !== "string") {
+				slave.custom.aiPrompts.expressionNegative = "";
+			}
+		}
 	}
 
 	/**
diff --git a/src/data/backwardsCompatibility/updateCustomSlaveOrder.js b/src/data/backwardsCompatibility/updateCustomSlaveOrder.js
index fb3c0ac2b4d7b38061a3bebee205764b7c5a4266..b2b884b13ea0946545980a20ef6529149a60419a 100644
--- a/src/data/backwardsCompatibility/updateCustomSlaveOrder.js
+++ b/src/data/backwardsCompatibility/updateCustomSlaveOrder.js
@@ -22,6 +22,9 @@ App.Update.CustomSlaveOrder = function(customSlaveOrder) {
 
 	App.Update.setNonexistentProperties(customSlaveOrder, {
 		skill: {whore: 15, combat: 0},
+		hairColor: "hair color is unimportant",
+		eyesColor: "eye color is unimportant"
+
 	});
 
 	App.Update.moveProperties(customSlaveOrder.skill, customSlaveOrder, {
diff --git a/src/endWeek/nextWeek/nextWeek.js b/src/endWeek/nextWeek/nextWeek.js
index b635f109ac52aee3c36b566879d0d59f45ba8608..e74104c98dd78ba286751084744e5dfab6c307d8 100644
--- a/src/endWeek/nextWeek/nextWeek.js
+++ b/src/endWeek/nextWeek/nextWeek.js
@@ -358,6 +358,11 @@ App.EndWeek.nextWeek = function() {
 		}
 	}
 
+	// resets processed slaves for the pregnancy notice event
+	if (V.pregnancyNotice) {
+		V.pregnancyNotice.processedSlaves = [];
+	}
+
 	V.week++;
 	V.arcologies[0].weeks++;
 
@@ -452,7 +457,7 @@ App.EndWeek.nextWeek = function() {
 				await sleep();
 			}
 			V.slaves.forEach(s => {
-				if ((V.week - s.weekAcquired) % V.aiAutoGenFrequency === 0 && !s.custom.aiPrompts?.aiAutoRegenExclude){
+				if ((V.week - s.weekAcquired) % V.aiAutoGenFrequency === 0 && !s.custom.aiAutoRegenExclude){
 					App.Art.GenAI.staticCache.updateSlave(s, null, false)
 						.catch(error => {
 							console.log(error.message || error);
diff --git a/src/endWeek/player/prInflation.js b/src/endWeek/player/prInflation.js
index 2bf3a8b4dcc5a72765644757f9ff4533853a6237..e7bac54a07f1004e0d3ba02696905d0bd5f97918 100644
--- a/src/endWeek/player/prInflation.js
+++ b/src/endWeek/player/prInflation.js
@@ -163,7 +163,7 @@ App.EndWeek.Player.inflation = function(PC = V.PC) {
 				} else if (PC.inflationMethod === 2) {
 					r.push(`You fill your rear with nearly`);
 				} else if (PC.inflationMethod === 3) {
-					r.push(`You suckle from ${cow.slaveName} until you've drank nearly`);
+					r.push(`You suckle from ${cow.slaveName} until you've drunk nearly`);
 					cow.lactationDuration = 2;
 					cow.boobs -= cow.boobsMilk;
 					cow.boobsMilk = 0;
diff --git a/src/endWeek/reports/incubatorReport.js b/src/endWeek/reports/incubatorReport.js
index 012c785ea68c02d0760f45d65da9e991748f3628..163e363524b74e4df1b1105b9fd2e75a46e83074 100644
--- a/src/endWeek/reports/incubatorReport.js
+++ b/src/endWeek/reports/incubatorReport.js
@@ -30,6 +30,7 @@ App.EndWeek.incubatorReport = function() {
 			r.push(`<span class="pink">${tank.slaveName}'s</span> growth is currently being accelerated. ${He}`);
 			if (Math.round(tank.incubatorSettings.growTime/V.incubator.upgrade.speed) <= 0) {
 				r.push(`is <span class="lime">ready for release.</span> ${He} will be ejected from ${his} tank upon your approach.`);
+				V.incubator.readySlaves = 1;
 			} else {
 				r.push(`will be ready for release in about ${Math.round(tank.incubatorSettings.growTime/V.incubator.upgrade.speed)} weeks.`);
 			}
diff --git a/src/endWeek/reports/penthouseReport.js b/src/endWeek/reports/penthouseReport.js
index 4b10b0420a298c0aeaf4e29b7b8d3d540eaa8b8a..0da3b8bde239a8b43923195bf92f21384eb68f51 100644
--- a/src/endWeek/reports/penthouseReport.js
+++ b/src/endWeek/reports/penthouseReport.js
@@ -227,8 +227,14 @@ App.EndWeek.penthouseReport = function() {
 		} = getPronouns(S.HeadGirl);
 		const {he2, His2, his2, him2, himself2, girl2} = getPronouns(slave).appendSuffix("2");
 		let r = [];
-		const popup = App.UI.DOM.slaveDescriptionDialog(slave, SlaveFullName(S.HeadGirl));
-		popup.classList.add("slave-name", "bold");
+		const popup = App.UI.DOM.slaveDescriptionDialog(
+			S.HeadGirl,
+			SlaveFullName(S.HeadGirl),
+			undefined,
+			{
+				linkClasses: ["slave-name", "bold"]
+			}
+		);
 
 		slave.training = Math.clamp(slave.training, 0, 150);
 		let effectiveness = S.HeadGirl.actualAge + ((S.HeadGirl.intelligence + S.HeadGirl.intelligenceImplant) / 3) - (S.HeadGirl.accent * 5) + (V.HGSeverity * 10) + ((slave.intelligence + slave.intelligenceImplant) / 4) - (slave.accent * 5);
diff --git a/src/endWeek/reports/personalAttention.js b/src/endWeek/reports/personalAttention.js
index e1e92792237a10048299a381e661ff286ac283cd..8059cc8bfb0b369406c145bd2c23bae44157b106 100644
--- a/src/endWeek/reports/personalAttention.js
+++ b/src/endWeek/reports/personalAttention.js
@@ -1656,7 +1656,7 @@ App.PersonalAttention.slaveReport = function(slave) {
 							} else {
 								r.push(`forces you back to the ground`);
 								if (slave.lactation > 0) {
-									r.push(`before pushing a milky nipple into your mouth. Soon your libido has you sucking away, oblivious to ${him} playing with ${himself} while enjoying the release. ${He} sneaks off once ${he} feels you've drank as much of ${his} milk as you can handle.`);
+									r.push(`before pushing a milky nipple into your mouth. Soon your libido has you sucking away, oblivious to ${him} playing with ${himself} while enjoying the release. ${He} sneaks off once ${he} feels you've drunk as much of ${his} milk as you can handle.`);
 									slave.boobs -= slave.boobsMilk;
 									slave.boobsMilk = 0;
 									slave.lactationDuration = 2;
diff --git a/src/endWeek/saPleaseYou.js b/src/endWeek/saPleaseYou.js
index 705ec1cc46a8953a87ef9bb590480627c911ff2f..e808e193af8146d22b4a4fc0893118e9bf837ccf 100644
--- a/src/endWeek/saPleaseYou.js
+++ b/src/endWeek/saPleaseYou.js
@@ -175,15 +175,13 @@ App.SlaveAssignment.pleaseYou = function saPleaseYou(slave) {
 	 * @param {FC.SlaveActs|"penetrativeTease"} target
 	 */
 	function evaluateSexQuality(slave, target) {
-		let quality;
+		let quality = 1;
 		if (target === "mammary") {
-			if (slave.nipples === NippleShape.FUCKABLE) {
-				if (canPenetrate(V.PC) || V.PC.clit >= 3) {
-					if (isHorny(slave)) {
-						quality = 1.5;
-					} else {
-						quality = 1.2;
-					}
+			if (slave.nipples === NippleShape.FUCKABLE && (canPenetrate(V.PC) || V.PC.clit >= 3)) {
+				if (isHorny(slave)) {
+					quality = 1.5;
+				} else {
+					quality = 1.2;
 				}
 			/*
 			} else if (slave.nipples === NippleShape.DICKNIPS && V.PC.vagina > 0 && isPlayerReceptive()) {
@@ -298,7 +296,7 @@ App.SlaveAssignment.pleaseYou = function saPleaseYou(slave) {
 	 * @param {FC.SlaveActs|"penetrativeTease"} target
 	 */
 	function evaluateSlavePleasure(slave, target) {
-		let pleasure;
+		let pleasure = 1;
 		if (target === "mammary") {
 			// slaves do not gain equivalent release to a dicked PC, so have their own calcs.
 			pleasure = Math.max((slave.fetish === Fetish.BOOBS ? slave.fetishStrength / 50 : 0.3), 0.3);
@@ -1747,7 +1745,7 @@ App.SlaveAssignment.pleaseYou = function saPleaseYou(slave) {
 	 */
 	function useBoobs(slave) {
 		let pcBoobsException = false;
-		let playerMammaryPleasure;
+		let playerMammaryPleasure = 1;
 		mammaryQuality = evaluateSexQuality(slave, "mammary");
 		mammaryPleasure = evaluateSlavePleasure(slave, "mammary");
 		acts = libidoToActs;
@@ -3212,15 +3210,15 @@ App.SlaveAssignment.pleaseYou = function saPleaseYou(slave) {
 	 */
 	function useAllHoles(slave) {
 		let showExcessReport = false;
-		let oralUseQuality;
-		let analUseQuality;
-		let vaginalUseQuality;
-		let mammaryUseQuality;
+		let oralUseQuality = 1;
+		let analUseQuality = 1;
+		let vaginalUseQuality = 1;
+		let mammaryUseQuality = 1;
 		let sumQuality = 0;
-		let oralUseExcess;
-		let analUseExcess;
-		let vaginalUseExcess;
-		let mammaryUseExcess;
+		let oralUseExcess = 0;
+		let analUseExcess = 0;
+		let vaginalUseExcess = 0;
+		let mammaryUseExcess = 0;
 
 		const totalActs = libidoToActs;
 		if (totalActs > 200) {
diff --git a/src/endWeek/standardSlaveReport.js b/src/endWeek/standardSlaveReport.js
index 94c2fd5f5b41dacd19918d9b182c251699db1335..7606d53a1719392b7c8e309a34857ed885ec62e3 100644
--- a/src/endWeek/standardSlaveReport.js
+++ b/src/endWeek/standardSlaveReport.js
@@ -57,8 +57,13 @@ App.SlaveAssignment.appendSlaveArt = function(node, slave) {
  */
 App.SlaveAssignment.saSlaveName = function(slave) {
 	const frag = new DocumentFragment();
-	const popup = App.UI.DOM.slaveDescriptionDialog(slave, SlaveFullName(slave));
-	popup.classList.add("slave-name", "bold");
+	const popup = App.UI.DOM.slaveDescriptionDialog(
+		slave, SlaveFullName(slave), undefined,
+		{
+			noButtons: true,
+			linkClasses: ["slave-name", "bold"],
+		}
+	);
 	frag.append(
 		App.UI.favoriteToggle(slave), " ",
 		App.Reminders.slaveLink(slave.ID), " ",
diff --git a/src/events/PE/foodplay.js b/src/events/PE/foodplay.js
index 81327917d103f621956729eb9ff5aec65fa2ffe1..8a82876937ee68fd0484b24738b8aded42347c22 100644
--- a/src/events/PE/foodplay.js
+++ b/src/events/PE/foodplay.js
@@ -6,39 +6,50 @@ App.Events.PEFoodplay = class PEFoodplay extends App.Events.BaseEvent {
 		];
 	}
 
+	/**
+	 * @param {Node} node
+	 */
 	execute(node) {
-		let eventSlave = undefined;
+		const artDiv = App.UI.DOM.makeElement("div");
+		node.appendChild(artDiv);
 		App.Events.addParagraph(node, [`You are relaxing in the Penthouse after a day of hard work when your PA alerts you to an incoming shipment. One of the aristocrats in your arcology was able to get their hands on a shipment of fresh, wild-caught fish and has sent you a few prime cuts as a sign of goodwill. Such a delicacy is rare to come across, given the rapidly deteriorating global climate.`]);
 		App.Events.addParagraph(node, [`As the leader of a well-established and renowned arcology, it isn't uncommon for you to receive these gestures of friendship and goodwill on a semi-regular basis. Perhaps it wouldn't be such a bad idea to enjoy this latest gift with one of your slaves.`]);
 
 		App.Events.addResponses(node, [
 			(V.HeadGirlID !== 0)
-				? new App.Events.Result(`Accept the gift and invite your Head Girl to join you `, () => scene(S.HeadGirl))
+				? new App.Events.Result(`Accept the gift and invite your Head Girl to join you `, () => scene(S.HeadGirl, artDiv))
 				: new App.Events.Result(),
 			(V.ConcubineID !== 0)
-				? new App.Events.Result(`Accept the gift and share it with your Concubine`, () => scene(S.Concubine))
+				? new App.Events.Result(`Accept the gift and share it with your Concubine`, () => scene(S.Concubine, artDiv))
 				: new App.Events.Result(),
 			(V.BodyguardID !== 0)
-				? new App.Events.Result(`Accept the gift, and have your Bodyguard join you`, () => scene(S.Bodyguard))
+				? new App.Events.Result(`Accept the gift, and have your Bodyguard join you`, () => scene(S.Bodyguard, artDiv))
 				: new App.Events.Result(),
 			new App.Events.Result(`Decline the gift`, decline)
 		]);
 
+		/**
+		 * @returns {ContainerT}
+		 */
 		function decline() {
 			const r = new SpacedTextAccumulator();
 			r.push(`You politely decline the shipment of seafood. You receive plenty of gifts, and can't spend time entertaining each individual that wishes to gain your favor.`);
 			return r.container();
 		}
 
-		function scene(slave){
+		/**
+		 * @param {App.Entity.SlaveState} eventSlave
+		 * @param {HTMLDivElement} artDiv
+		 * @returns {ContainerT}
+		 */
+		function scene(eventSlave, artDiv){
 			const r = new SpacedTextAccumulator();
-			eventSlave = slave;
 			const {
 				He, he, his, him
 			} = getPronouns(eventSlave);
 			const {title: Master} = getEnunciation(eventSlave);
 
-			App.Events.drawEventArt(node, eventSlave, "no clothing");
+			App.Events.drawEventArt(artDiv, eventSlave, "no clothing");
 
 			r.push(`You summon`);
 			r.push(contextualIntro(V.PC, eventSlave, true));
diff --git a/src/events/RE/reRoyalBlood.js b/src/events/RE/reRoyalBlood.js
index 55d42ec380073c0819e681eb2c9e12a16a0e1b25..f7826ade1acd8cab16f9a7220c897ba5eababd2f 100644
--- a/src/events/RE/reRoyalBlood.js
+++ b/src/events/RE/reRoyalBlood.js
@@ -346,6 +346,11 @@ App.Events.RERoyalBlood = class RERoyalBlood extends App.Events.BaseEvent {
 				newSlave(prince); // skip New Slave Intro
 			}
 
+			// the queen is missing
+			princess.mother = 0;
+			setMissingParents(princess);
+			prince.mother = princess.mother;
+
 			return text;
 		}
 
diff --git a/src/events/RE/reStaffedMorning.js b/src/events/RE/reStaffedMorning.js
index 906506660e9fdbb58e4db1c3ca827a9626fc67a3..3cfbbabd6803f059e7235f20ca8136117478d6fa 100644
--- a/src/events/RE/reStaffedMorning.js
+++ b/src/events/RE/reStaffedMorning.js
@@ -23,6 +23,9 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 		];
 	}
 
+	/**
+	 * @param {Node} node
+	 */
 	execute(node) {
 		let bedSlaves = this.actors.map(getSlave);
 
@@ -45,9 +48,16 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 
 		App.Events.drawEventArt(node, bedSlaves.slice(0, 2), "no clothing");
 
-		let t = [
-			`Sleep leaves you quickly one morning to the sensation of two of your fucktoys performing human alarm clock duty. You open your eyes and look down: it's ${bedSlaves[0].slaveName} and ${contextualIntro(bedSlaves[0], bedSlaves[1])} today.`
-		];
+		let t = new SpacedTextAccumulator();
+
+		t.push(
+			`Sleep leaves you quickly one morning to the sensation of two of your fucktoys performing human alarm clock duty.`,
+			`You open your eyes and look down: it's`,
+			App.UI.DOM.slaveDescriptionDialog(bedSlaves[0]),
+			`and`,
+			contextualIntro(bedSlaves[0], bedSlaves[1], true),
+			`today.`
+		);
 
 		if (V.PC.dick !== 0) {
 			t.push(`${bedSlaves[0].slaveName} is ${(bedSlaves[0].fetish === "cumslut") ? "rapturously" : "industriously"} sucking your dick as it rapidly hardens in ${his} `);
@@ -114,9 +124,17 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 			t.push(`You feel absolutely buried in attentive slave.`);
 		}
 
-		t.push(`The bathroom door is open and the shower is running. Though the steam is beginning to fill the glass-walled shower, you can see a pair of naked bodies in there; that would be ${bedSlaves[2].slaveName} and ${contextualIntro(bedSlaves[2], bedSlaves[3])}, ready to attend you as you bathe.`);
+		t.push(
+			`The bathroom door is open and the shower is running.`,
+			`Though the steam is beginning to fill the glass-walled shower, you can see a pair of naked bodies in there; that would be`,
+			App.UI.DOM.slaveDescriptionDialog(bedSlaves[2]),
+			`and`,
+			contextualIntro(bedSlaves[2], bedSlaves[3], true),
+			`, ready to attend you as you bathe.`
+		);
 
-		App.Events.addParagraph(node, t);
+		t.toParagraph();
+		node.appendChild(t.container());
 
 		App.Events.addResponses(node, [
 			new App.Events.Result("Leave them satisfied", satisfied),
@@ -126,7 +144,7 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 		function satisfied() {
 			const frag = document.createDocumentFragment();
 			let sexTarget = "";
-			t = [];
+			t = new SpacedTextAccumulator();
 
 			if (V.PC.dick !== 0) {
 				t.push(`You begin to thrust gently into ${bedSlaves[0].slaveName}'s mouth. ${girl === girl2 ? `The ${girl}` : "Your bedmate"}s moan and giggle into you at the signal that you're not going to get up right this instant, and`);
@@ -146,9 +164,11 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 				seX(bedSlaves[0], "oral", V.PC, "vaginal");
 				seX(bedSlaves[1], "oral", V.PC, "vaginal");
 			}
-			App.Events.addParagraph(frag, t);
 
-			t = [];
+			t.toParagraph();
+
+			App.Events.refreshEventArt(bedSlaves, "no clothing");
+
 			t.push(`By now, the shower is an impenetrable fog of steam. The wet, soapy bodies inside are easy to find, though. ${bedSlaves[2].slaveName} happens to be closest, so you kiss ${his3} laughing mouth`);
 			if (V.PC.dick !== 0) {
 				if ((canDoVaginal(bedSlaves[2]) && bedSlaves[2].vagina > 0) || (canDoAnal(bedSlaves[2]) && bedSlaves[2].anus > 0)) {
@@ -288,9 +308,9 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 				}
 				t.push(`straddle one of ${his4} legs. ${bedSlaves[2].slaveName} does ${his3} best to get ${his3} wits back and take over washing duty. They towel you together, and you head back out of the bathroom.`);
 			}
-			App.Events.addParagraph(frag, t);
 
-			t = [];
+			t.toParagraph();
+
 			t.push(`Your clothes have been laid out, ready for ${bedSlaves[0].slaveName} and ${bedSlaves[1].slaveName} to dress you, but`);
 			if (V.PC.dick !== 0) {
 				t.push(`next to the neat stack of clothes, the two slaves are bent over the bed with their buttocks spread. You select ${bedSlaves[1].slaveName}`);
@@ -351,9 +371,12 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 			if (App.Utils.sexAllowed(bedSlaves[0], bedSlaves[1])) {
 				SimpleSexAct.Slaves(bedSlaves[0], bedSlaves[1]);
 			}
-			bedSlaves.forEach(s => (s.trust += 4));
 
-			App.Events.addParagraph(frag, t);
+			t.toParagraph();
+
+			frag.appendChild(t.container());
+
+			bedSlaves.forEach(s => (s.trust += 4));
 
 			return frag;
 		}
@@ -361,7 +384,7 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 		function exhausted() {
 			const frag = document.createDocumentFragment();
 			let noShowerSex = 0;
-			t = [];
+			t = new SpacedTextAccumulator();
 
 			if (V.PC.dick !== 0) {
 				t.push(`${bedSlaves[0].slaveName} feels a hand snake behind ${his} head and relaxes ${his} throat, knowing what's coming. You fuck the bitch's mouth hard, and since the pounding pulls your balls out of ${bedSlaves[1].slaveName}'s mouth,`);
@@ -393,9 +416,11 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 				seX(bedSlaves[0], "oral", V.PC, "vaginal");
 			}
 			t.push(`and bounce up to fuck bitches in the shower, knocking ${bedSlaves[0].slaveName} aside and sending ${bedSlaves[1].slaveName} sprawling. As you go, you tell them they've got ten minutes to get your clothes laid out and their bodies ready for more. They nod furiously and scramble.`);
-			App.Events.addParagraph(frag, t);
 
-			t = [];
+			t.toParagraph();
+
+			App.Events.refreshEventArt(bedSlaves, "no clothing");
+
 			t.push(`By now, the shower is an impenetrable fog of steam. The wet, soapy bodies inside are easy to find, though. ${bedSlaves[2].slaveName} happens to be closest, so you`);
 			t.push(`grab ${him3} and shove ${him3} into a corner of the`); // strength
 			if (bedSlaves[2].belly >= 10000) {
@@ -478,9 +503,9 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 				}
 				t.push(`experimentally, nodding in satisfaction when the big phallus forces a pained gasp out of ${him4}.`);
 			}
-			App.Events.addParagraph(frag, t);
 
-			t = [];
+			t.toParagraph();
+
 			t.push(`Back in the bedroom, your clothes have been laid out, ready for ${bedSlaves[0].slaveName} and ${bedSlaves[1].slaveName} to dress you.`);
 
 			let dick = V.PC.dick !== 0 ? "dick" : "strap-on";
@@ -533,7 +558,10 @@ App.Events.REStaffedMorning = class REStaffedMorning extends App.Events.BaseEven
 			SimpleSexAct.Player(bedSlaves[3]);
 			t.push(`When you finally leave the suite, all four slaves are lying like discarded tissues on the bed, face-down and spread-eagled, carefully avoiding resting on any sensitive parts. <span class="devotion inc">Your fucktoys are reminded of who you are.</span>`);
 
-			App.Events.addParagraph(frag, t);
+			t.toParagraph();
+
+			frag.appendChild(t.container());
+
 			bedSlaves.forEach(s => (s.devotion += 4));
 
 			return frag;
diff --git a/src/events/REFS/refsNeoImperialistFeast.js b/src/events/REFS/refsNeoImperialistFeast.js
index 32bfcf9acf284bcb57fd99b81e824cc53cb89f80..84367c24397dd2a95ea7516e54733bdf83a60bb8 100644
--- a/src/events/REFS/refsNeoImperialistFeast.js
+++ b/src/events/REFS/refsNeoImperialistFeast.js
@@ -66,7 +66,7 @@ App.Events.refsNeoImperialistFeast = class refsNeoImperialistFeast extends App.E
 		function moderate() {
 			cashX(-moderateCash, "event");
 			repX(2000, "event");
-			return `There's no special cause for celebration this time around, but that doesn't mean you can't have a great time. You lay out platters of expensive food and drink, served by gorgeous maids, for the arriving Barons and Knights, who seem universally pleased to have an opportunity to forget the troubles of rulership and power for a day – not that that's particularly difficult in the Free Cities, anyway. Among the roaring company of ${V.arcologies[0].name}'s wealthiest and most influential citizens, you tear into fresh-cooked meats and the rounded asses of the servants alike, in a glorious celebration where nothing and no one is off-limits. At the end of the evening, when the crowd has finally had their fill and you've fucked and drank enough that you can barely stand to wave them goodbye, nearly each and every Baron leaves the celebration with a <span class="reputation inc">stupid, drunken smile across their face.</span>`;
+			return `There's no special cause for celebration this time around, but that doesn't mean you can't have a great time. You lay out platters of expensive food and drink, served by gorgeous maids, for the arriving Barons and Knights, who seem universally pleased to have an opportunity to forget the troubles of rulership and power for a day – not that that's particularly difficult in the Free Cities, anyway. Among the roaring company of ${V.arcologies[0].name}'s wealthiest and most influential citizens, you tear into fresh-cooked meats and the rounded asses of the servants alike, in a glorious celebration where nothing and no one is off-limits. At the end of the evening, when the crowd has finally had their fill and you've fucked and drunk enough that you can barely stand to wave them goodbye, nearly each and every Baron leaves the celebration with a <span class="reputation inc">stupid, drunken smile across their face.</span>`;
 		}
 
 		function refuse() {
diff --git a/src/events/nonRandom/pregnancyNotice.js b/src/events/nonRandom/pregnancyNotice.js
new file mode 100644
index 0000000000000000000000000000000000000000..749995269fe57e579d101a18c0579a7d6e89e7b2
--- /dev/null
+++ b/src/events/nonRandom/pregnancyNotice.js
@@ -0,0 +1,811 @@
+App.Events.PregnancyNotice = {};
+
+/**
+ * Notifies the player of a slave pregnancy at 2 weeks of gestation
+ * This event is controlled by the v.pregnancyNotice.enabled variable, which must be true
+ * This event is also requires V.pregnancyMonitoringUpgrade to be 1
+ */
+App.Events.PregnancyNotice.SlavePregnant = class PregnancyNotice extends App.Events.BaseEvent {
+	constructor(actors, params) {
+		super(actors, params);
+	}
+
+	actorPrerequisites() {
+		return [[(s) => {
+			return App.Events.PregnancyNotice.ValidActor(s);
+		}]];
+	}
+
+	eventPrerequisites() {
+		return [
+			() => App.Events.PregnancyNotice.CanShow(),
+		];
+	}
+
+	execute(node) {
+		return App.Events.PregnancyNotice.Event(node, getSlave(this.actors[0]));
+	}
+};
+
+/**
+ * Notifies the player of their own pregnancy at 2 weeks of gestation
+ * This event is controlled by the v.pregnancyNotice.enabled variable, which must be true
+ * This event is also requires V.pregnancyMonitoringUpgrade to be 1
+ */
+App.Events.PregnancyNotice.PlayerPregnant = class PlayerPregnancyNotice extends App.Events.BaseEvent {
+	constructor(actors, params) {
+		super(actors, params);
+	}
+
+	/** we cast the player as an actor if they meet the requirements */
+	castActors() {
+		if (App.Events.PregnancyNotice.ValidActor(V.PC) === true) {
+			this.actors = [V.PC.ID];
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+	eventPrerequisites() {
+		return [
+			() => App.Events.PregnancyNotice.CanShow(),
+		];
+	}
+
+	execute(node) {
+		return App.Events.PregnancyNotice.Event(node, V.PC);
+	}
+};
+
+/**
+ * @returns {boolean} true if the pregnancy notice events can happen
+ */
+App.Events.PregnancyNotice.CanShow = () => {
+	if (
+		V.pregnancyMonitoringUpgrade === 1 &&
+		V.pregnancyNotice.enabled === true
+	) {
+		return true;
+	}
+	return false;
+};
+
+/**
+ * @param {FC.HumanState} actor
+ * @returns {boolean} true if the given actor is valid for the pregnancy notice event
+ */
+App.Events.PregnancyNotice.ValidActor = (actor) => {
+	// return false if we have already processed this actor this week
+	if (V.pregnancyNotice.processedSlaves.includes(actor.ID)) { return false; }
+	// return false if the actor doesn't have any fetuses
+	if (actor.womb.length === 0) { return false; }
+	// return true if one or more of the fetuses are valid
+	let validFetus = false;
+	actor.womb.forEach(fetus => {
+		if (fetus.age === 2 || (fetus.age === 6 && ["wait", "undecided"].includes(fetus.noticeData.fate))) {
+			validFetus = true;
+		}
+
+		// slaves that we have acquired this week with a pregnancy over 6 weeks are also valid
+		// @ts-expect-error weekAcquired doesn't exist on PlayerState
+		if (actor.ID !== -1 && actor.weekAcquired === V.week && fetus.age > 6) {
+			validFetus = true;
+		}
+	});
+	return validFetus;
+};
+
+/**
+ * @typedef {object} App.Events.PregnancyNotice.Accordion
+ * @property {HTMLDivElement} contentDiv the content of the accordion
+ * @property {HTMLSpanElement} titleSpan the title
+ * @property {HTMLSpanElement} noteSpan the note to the right of the title
+ * @property {DocumentFragment} accordion the accordion. This should be attached to the dom
+ */
+
+/**
+ * @param {string|Node} titleContent
+ * @param {string|Node} [titleNote=undefined] shown to the right of the title
+ * @param {boolean} [collapsed=true] if true the accordion starts in a collapsed state
+ * @returns {App.Events.PregnancyNotice.Accordion}
+ */
+App.Events.PregnancyNotice.createAccordion = (titleContent, titleNote = undefined, collapsed = true) => {
+	const title = document.createDocumentFragment();
+	const titleSpan = App.UI.DOM.makeElement("span", titleContent, ["title"]);
+	title.appendChild(titleSpan);
+	const noteSpan = App.UI.DOM.makeElement("span", titleNote, ["info"]); // shows amount of slaves there
+	title.appendChild(noteSpan);
+	const contentDiv = App.UI.DOM.makeElement("div");
+	return {
+		contentDiv: contentDiv,
+		titleSpan: titleSpan,
+		noteSpan: noteSpan,
+		accordion: App.UI.DOM.accordion(title, contentDiv, collapsed)
+	};
+};
+
+/**
+ * @param {number} slaveID
+ * @returns {FC.HumanState | string}
+ */
+App.Events.PregnancyNotice.getFather = (slaveID) => {
+	const societalElite = V.arcologies[0].FSNeoImperialistLaw2 === 1 ? "Barons" : "Societal Elite";
+	if (slaveID === 0) {
+		return "unknown";
+	} else if (slaveID === -1) {
+		return V.PC;
+	} else if (slaveID === -2) {
+		return "an arcology citizen";
+	} else if (slaveID === -3) {
+		return "your former master";
+	} else if (slaveID === -4) {
+		return "another arcology owner";
+	} else if (slaveID === -5) {
+		return "one of your clientele";
+	} else if (slaveID === -6) {
+		return `the ${societalElite}`;
+	} else if (slaveID === -7) {
+		return "you via the magic of science"; // designer baby
+	} else if (slaveID === -8) {
+		return "an animal";
+	} else if (slaveID === -9) {
+		return "a Futanari Sister";
+	} else if (slaveID === -10) {
+		return "a rapist";
+	} else {
+		let father = findFather(slaveID);
+		if (father) {
+			return father;
+		} else {
+			return "Unknown";
+		}
+	}
+};
+
+/**
+ * The actual event
+ * @param {ParentNode} node
+ * @param {FC.HumanState} mother
+ * @returns {ParentNode}
+ */
+App.Events.PregnancyNotice.Event = (node, mother) => {
+	// mark the slave as processed
+	V.pregnancyNotice.processedSlaves.push(mother.ID);
+
+	const cheating = (V.cheatMode || V.debugMode);
+
+	const motherIsPC = (mother.ID === -1);
+	// @ts-expect-error weekAcquired doesn't exist on PlayerState
+	const motherIsNew = (mother.ID === -1) ? false : (mother.weekAcquired === V.week);
+	const totalBabyCount = mother.womb.length;
+	const fetuses = mother.womb.filter((fetus) => (
+		fetus.age === 2 ||
+		(fetus.age === 6 && ["wait", "undecided"].includes(fetus.noticeData.fate)) ||
+		// @ts-expect-error weekAcquired doesn't exist on PlayerState
+		(mother.ID !== -1 && mother.weekAcquired === V.week && fetus.age > 6)
+	));
+	const hasIncubator = (V.incubator.capacity > 0);
+	const nurseryName = App.Entity.facilities.nursery.name.replace(/^the /i, "").trim();
+	const incubatorName = App.Entity.facilities.incubator.name.replace(/^the /i, "").trim();
+	const hasNursery = (V.nurseryCribs > 0);
+	const unreservedTanks = () => { return (V.incubator.capacity - V.incubator.tanks.length) - FetusGlobalReserveCount("incubator"); };
+	const unreservedCribs = () => { return (V.nurseryCribs - V.cribs.length) - FetusGlobalReserveCount("nursery"); };
+
+	if (motherIsPC) {
+		node.append(App.UI.DOM.makeElement(
+			"div",
+			`You are pregnant with ${num(totalBabyCount)} ${(totalBabyCount === 1) ? "baby": "babies"}.`),
+		);
+		if (totalBabyCount !== fetuses.length) {
+			node.append(App.UI.DOM.makeElement(
+				"div",
+				` ${num(fetuses.length)} of them are listed below.`),
+			);
+		}
+	} else {
+		let motherDiv = App.UI.DOM.makeElement("div");
+		if (!motherIsPC) {
+			// @ts-expect-error FC.HumanState is not accepted by drawEventArt
+			App.Events.drawEventArt(motherDiv, mother);
+		}
+		// @ts-expect-error saSlaveName will not accept FC.HumanState
+		motherDiv.append(App.SlaveAssignment.saSlaveName(mother));
+		if (cheating) {
+			motherDiv.append(` (ID: ${mother.ID})`);
+		}
+		if (motherIsNew) {
+			motherDiv.append(` was acquired this week and`);
+		}
+		motherDiv.append(` is pregnant with ${num(totalBabyCount)} ${(totalBabyCount === 1) ? "baby": "babies"}.`);
+		if (totalBabyCount !== fetuses.length) {
+			motherDiv.append(` ${num(fetuses.length)} of them are listed below.`);
+		}
+		node.append(motherDiv);
+	}
+	const transplantedBabyCount = mother.womb.filter((fetus) => (fetus.motherID !== mother.ID)).length;
+	if (transplantedBabyCount !== 0) {
+		node.append(App.UI.DOM.makeElement(
+			"div",
+			`${num(transplantedBabyCount)} of them ${(transplantedBabyCount === 1) ? "was": "are"} transplanted.`),
+		);
+	}
+	// TODO:@franklygeorge mother's (If not PC) info (Same as main screen).
+	// TODO:@franklygeorge list out each group of babies and how old they are (See how the slave description does it for superfetation)
+
+	let unprocessedDiv = App.UI.DOM.makeElement("div");
+
+	function updateProcessed() {
+		// clear unprocessedDiv's contents
+		unprocessedDiv.innerHTML = "";
+		let unprocessedFetuses = fetuses.filter((fetus) => (
+			(
+				fetus.noticeData.fate === "undecided" ||
+				(fetus.age >= 6 && fetus.noticeData.fate === "wait")
+			) &&
+			mother.womb.indexOf(fetus) !== -1 // transplanted fetuses are processed
+		));
+		if (unprocessedFetuses.length === 0) {
+			unprocessedDiv.append(`There are no unprocessed children.`);
+			V.nextButton = "Apply";
+			App.Utils.updateUserButton();
+		} else {
+			let unprocessedFetusNames = unprocessedFetuses.map((fetus) => (fetus.genetics.name));
+			unprocessedDiv.append(`${toSentence(unprocessedFetusNames, ", ", " and ")} ${(unprocessedFetuses.length === 1) ? "has": "have"} not been processed yet.`);
+			V.nextButton = " ";
+			App.Utils.updateUserButton();
+
+			let bulkOptions = new App.UI.OptionsGroup;
+			let bulkChoice = {
+				/** @type {"incubator"|"nursery"|"nothing"|"wait"|"undecided"} */
+				choice: "undecided"
+			};
+			// TODO:@franklygeorge option to show only unprocessed (collapse all processed and expand all unprocessed accordions);
+			bulkOptions.customRefresh(() => {
+				unprocessedFetuses.forEach((fetus) => {
+					fetus.noticeData.fate = bulkChoice.choice;
+					if (["incubator", "nursery"].includes(bulkChoice.choice)) {
+						// @ts-ignore
+						fetus.reserve = bulkChoice.choice;
+					}
+					let noteSpan = $(`#${fetus.ID}-note-span`)[0];
+					fetusInfo(fetus, noteSpan);
+				});
+			});
+			let bulkOption = bulkOptions.addOption("Change all unprocessed to: ", "choice", bulkChoice);
+			if (hasIncubator && unreservedTanks() >= unprocessedFetuses.length) {
+				bulkOption.addValue(`Reserve ${incubatorName} tanks`, "incubator");
+			}
+			if (hasNursery && unreservedCribs() >= unprocessedFetuses.length) {
+				bulkOption.addValue(`Reserve ${nurseryName} cribs`, "nursery");
+			}
+			if (unprocessedFetuses.filter((fetus) => fetus.age === 2).length !== 0) {
+				bulkOption.addValue(`Ask me again in ${num(4)} more weeks.`, "wait");
+			}
+			bulkOption.addValue("Do nothing", "nothing");
+
+			unprocessedDiv.append(bulkOptions.render());
+		}
+	}
+
+	let fetusesDiv = App.UI.DOM.makeElement("div");
+	_.forEachRight(fetuses, fetus => {
+		// we use forEachRight to do this in reverse order. If the user is using AI art generation this puts the first fetus on the top of the queue instead of the bottom
+		let fetusAccordion = App.Events.PregnancyNotice.createAccordion(
+			fetus.genetics.name,
+			``,
+			V.useAccordion > 0,
+		);
+		fetusAccordion.noteSpan.id = `${fetus.ID}-note-span`;
+		fetusAccordion.contentDiv.append(fetusInfo(fetus, fetusAccordion.noteSpan));
+		fetusesDiv.prepend(fetusAccordion.accordion);
+	});
+
+	node.append(fetusesDiv);
+	node.append(unprocessedDiv);
+
+	return node;
+
+	/**
+	 * @param {App.Entity.Fetus} fetus
+	 * @param {HTMLSpanElement} noteSpan
+	 * @returns {DocumentFragment}
+	 */
+	function fetusInfo(fetus, noteSpan) {
+		let noteSpanContent = `Place Reserved: ${(fetus.reserve !== "") ? (fetus.reserve === "incubator") ? incubatorName + " tank": nurseryName + " crib": "None"}`;
+		const frag = new DocumentFragment();
+		const cheating = (V.cheatMode === 1);
+
+		const father = App.Events.PregnancyNotice.getFather(fetus.fatherID);
+		const fatherIsPC = (typeof father !== "string" && father.ID === -1);
+		const fatherName = (typeof father === "string") ? father: SlaveFullName(father);
+
+		const canTerminate = canTerminateFetus(mother, fetus);
+		const canTransplant = canTransplantFetus(mother, fetus);
+
+		const twins = getFetusTwins(fetus);
+
+		if (fetus.noticeData.child === undefined) {
+			// @ts-expect-error As long as generateChild's code is not changed incubator = true will return a SlaveState object
+			fetus.noticeData.child = generateChild(mother, fetus, true); // used for descriptions and art
+		}
+
+		// make fake child a clone of fetus.noticeData.child
+		const fakeChild = clone(fetus.noticeData.child);
+		// Age fake child up to the ideal age
+		while (fakeChild.actualAge < V.idealAge) {
+			ageSlave(fakeChild, true);
+		}
+
+		const {
+			His, He, his, he
+		} = getPronouns(fakeChild);
+
+		let intro = new SpacedTextAccumulator();
+		if (V.geneticMappingUpgrade < 1 && cheating === false) {
+			intro.push(`You do not have the equipment needed to detect genetic details.`);
+			intro.push("You need a basic genetic sequencer to view more info.");
+		} else {
+			// add child image to intro
+			if (V.pregnancyNotice.renderFetus === true && (V.geneticMappingUpgrade >= 1 || cheating)) {
+				let childArtDiv = App.UI.DOM.makeElement("div");
+				App.Events.drawEventArt(childArtDiv, fakeChild, "no clothing");
+				intro.push(childArtDiv);
+			}
+
+			intro.push(`The child${cheating ? " (wombIndex: " + mother.womb.indexOf(fetus) + ")": ""} is ${geneToGender(fetus.genetics.gender, {keepKaryotype: false, lowercase: true})} and`);
+			if (fetus.age === 2) {
+				intro.push(`it is too early to know who the father${(cheating === true) ? " (" + fatherName + ")" : ""} is.`);
+			} else {
+				if (fatherIsPC) {
+					intro.push(`you are the father.`);
+					noteSpanContent += "; Father: You";
+				} else if (father === "unknown") {
+					intro.push(" the father is unknown.");
+					noteSpanContent += "; Father: Unknown";
+				} else {
+					intro.push(
+						// @ts-ignore
+						(typeof father === "string") ? father: App.SlaveAssignment.saSlaveName(father),
+						(fetus.fatherID === -6 || fetus.fatherID === -7) ? "are": "is",
+						`the father.`
+					);
+					noteSpanContent += `; Father ${(typeof father === "string") ? father: SlaveFullName(father)}`;
+				}
+			}
+		}
+
+		noteSpan.textContent = noteSpanContent;
+
+		if (canTransplant === -1) {
+			if (fetus.motherID === -1) {
+				intro.push(`The child was transplanted from your womb.`);
+			} else {
+				intro.push(`The child was transplanted from`, App.SlaveAssignment.saSlaveName(getSlave(fetus.motherID)), `'s womb.`);
+			}
+		}
+
+		if (twins) {
+			intro.push(`The child is twins with ${toSentence(twins.map((tFetus) => tFetus.genetics.name), ", ", ", and ")}.`);
+		}
+
+		intro.toParagraph();
+
+		if (V.geneticMappingUpgrade >= 1 || cheating) {
+			intro.push(App.Desc.geneticQuirkAssessment(fakeChild));
+			intro.toParagraph();
+
+			intro.push(`<b>Below describes what ${he} will likely look like at age ${num(fakeChild.actualAge)}.</b>`);
+			intro.toParagraph();
+
+			if (cheating) {
+				const rerollChild = new App.UI.OptionsGroup();
+				rerollChild.customRefresh(() => {
+					// @ts-expect-error As long as generateChild's code is not changed incubator = true will return a SlaveState object
+					fetus.noticeData.child = generateChild(mother, fetus, true); // rebuild the child object
+					fetusInfo(fetus, noteSpan);
+				});
+				rerollChild.addOption("", "do", {do: false})
+					.addValue("Reroll child traits", true);
+				intro.push(rerollChild.render());
+				intro.toParagraph();
+			}
+
+			if (fetus.age >= 6) {
+				intro.push(App.Desc.family(fakeChild, false));
+				intro.toParagraph();
+			}
+
+			if (V.showScores !== 0) {
+				intro.push(`Currently, ${he} has an`);
+				intro.push(App.UI.DOM.makeElement("span", `attractiveness score`, ["pink", "bold"]));
+				intro.push(App.UI.DOM.makeElement("span", `of`, ["pink"]));
+				intro.push(BeautyTooltip(fakeChild));
+				intro.toParagraph();
+			}
+
+			const descType =  DescType.NORMAL;
+			intro.push(App.Desc.arms(fakeChild));
+			intro.push(App.Desc.legs(fakeChild));
+			intro.push(App.Desc.skin(fakeChild, descType));
+			// birthmark
+			if (fakeChild.markings === "birthmark" && fakeChild.prestige === 0 && fakeChild.porn.prestige < 2) {
+				intro.push(`${He} has a large, liver-colored birthmark, detracting from ${his} beauty.`);
+			}
+
+
+			intro.push(App.Desc.ears(fakeChild));
+			// hair
+			intro.push(`${His} hair is`);
+			if (fakeChild.hColor !== fakeChild.eyebrowHColor) {
+				intro.push(`${fakeChild.hColor}, with ${fakeChild.eyebrowHColor} eyebrows.`);
+			} else {
+				intro.push(`${fakeChild.hColor}.`);
+			}
+
+			// freckled redhead
+			if (App.Data.misc.redheadColors.includes(fakeChild.hColor)) {
+				if (fakeChild.hLength >= 10) {
+					if (fakeChild.markings === "freckles" || fakeChild.markings === "heavily freckled") {
+						if (App.Medicine.Modification.naturalSkins.includes(fakeChild.skin) && skinToneLevel(fakeChild.skin).isBetween(5, 10)) {
+							intro.push(`It goes perfectly with ${his} ${fakeChild.skin} skin and freckles.`);
+						}
+					}
+				}
+			}
+			intro.push(App.Desc.armpitHair(fakeChild));
+			intro.push(App.Desc.horns(fakeChild)); // is it even possible for a slave to have horns without surgery? Leaving this here for potential future support
+			intro.push(App.Desc.face(fakeChild, false));
+			intro.push(App.Desc.mouth(fakeChild));
+			intro.push(App.Desc.eyes(fakeChild));
+			intro.toParagraph();
+
+			intro.push(App.Desc.boobs(fakeChild, descType));
+			intro.push(App.Desc.boobsShape(fakeChild));
+			intro.push(App.Desc.shoulders(fakeChild));
+			if (fakeChild.appendages !== "none" || fakeChild.wingsShape !== "none") {
+				intro.push(App.Desc.upperBack(fakeChild));
+			}
+			intro.push(App.Desc.nipples(fakeChild, descType));
+			intro.push(App.Desc.areola(fakeChild, descType));
+			intro.push(App.Desc.belly(fakeChild, descType));
+			intro.push(App.Desc.butt(fakeChild, descType));
+			intro.toParagraph();
+
+			intro.push(App.Desc.crotch(fakeChild, descType));
+			intro.push(App.Desc.dick(fakeChild, descType, false));
+			intro.push(App.Desc.vagina(fakeChild, false));
+			intro.push(App.Desc.anus(fakeChild, descType, false));
+			intro.toParagraph();
+		}
+
+		/** @type {FC.FetusGenetics} */
+		const genes = fetus.genetics;
+
+		let cheatOption;
+		const cheatOptions = new App.UI.OptionsGroup();
+		cheatOptions.customRefresh(() => {
+			// @ts-expect-error As long as generateChild's code is not changed incubator = true will return a SlaveState object
+			fetus.noticeData.child = generateChild(mother, fetus, true); // rebuild the child object with the new fetus data
+			fetusInfo(fetus, noteSpan);
+		});
+		// let the cheaters change things to their liking
+		cheatOption = cheatOptions.addOption(`Name: ${genes.name}`, "name", genes);
+		cheatOption.showTextBox();
+		cheatOption = cheatOptions.addOption(`Surname: ${genes.surname}`, "surname", genes);
+		cheatOption.showTextBox();
+		cheatOption = cheatOptions.addOption(`Gender: ${geneToGender(genes.gender, {keepKaryotype: true, lowercase: false})}`, "gender", genes);
+		cheatOption.addValue("Female", "XX");
+		cheatOption.addValue("Male", "XY");
+		cheatOption = cheatOptions.addOption(`Father name: ${(genes.fatherName) ? genes.fatherName : `name not registered`}; ID: ${genes.father}`, "father", genes);
+		cheatOption.showTextBox(); // TODO:@franklygeorge dropdown slave selectors (Also apply this to analyzePregnancy and the cheat genetic editor)
+		cheatOption = cheatOptions.addOption(`Mother name: ${(genes.motherName) ? genes.motherName : `name not registered`}; ID: ${genes.mother}`, "mother", genes);
+		cheatOption.showTextBox(); // TODO:@franklygeorge dropdown slave selectors (Also apply this to analyzePregnancy and the cheat genetic editor)
+		cheatOption = cheatOptions.addOption(`Nationality: ${genes.nationality}`, "nationality", genes);
+		cheatOption.showTextBox();
+		if (V.seeRace === 1) {
+			cheatOption = cheatOptions.addOption(`Race: ${capFirstChar(genes.race)}`, "race", genes);
+			cheatOption.showTextBox().pulldown().addValueList(Array.from(App.Data.misc.filterRaces, (k => [k[1], k[0]])));
+		}
+		cheatOption = cheatOptions.addOption(`Skin tone: ${capFirstChar(genes.skin)}`, "skin", genes);
+		cheatOption.showTextBox().pulldown().addValueList(genes.race === "catgirl" ? App.Medicine.Modification.catgirlNaturalSkins : App.Medicine.Modification.naturalSkins);
+		cheatOption = cheatOptions.addOption(`Intelligence index: ${genes.intelligence} out of 100`, "intelligence", genes);
+		cheatOption.showTextBox();
+		cheatOption = cheatOptions.addOption(`Face index: ${genes.face} out of 100`, "face", genes);
+		cheatOption.showTextBox();
+		cheatOption = cheatOptions.addOption(`Expected adult height: ${heightToEitherUnit(genes.adultHeight)}`, "adultHeight", genes);
+		cheatOption.showTextBox();
+		cheatOption = cheatOptions.addOption(`Estimated potential breast size: ${genes.boobPotential}cc`, "boobPotential", genes);
+		cheatOption.showTextBox();
+		cheatOption = cheatOptions.addOption(`Eye Color: ${capFirstChar(genes.eyeColor)}`, "eyeColor", genes);
+		cheatOption.showTextBox().pulldown();
+		for (const color of App.Medicine.Modification.eyeColor.map(color => color.value)) {
+			cheatOption.addValue(capFirstChar(color), color);
+		}
+		cheatOption = cheatOptions.addOption(`Hair Color: ${capFirstChar(genes.hColor)}`, "hColor", genes);
+		cheatOption.showTextBox().pulldown();
+		for (const color of App.Medicine.Modification.Color.Primary.map(color => color.value)) {
+			cheatOption.addValue(capFirstChar(color), color);
+		}
+		cheatOption = cheatOptions.addOption(`Pubic hair: ${capFirstChar(genes.pubicHStyle)}`, "pubicHStyle", genes);
+		cheatOption.showTextBox().pulldown()
+			.addValue("hairless")
+			.addValue("bushy");
+		cheatOption = cheatOptions.addOption(`Armpit hair: ${capFirstChar(genes.underArmHStyle)}`, "underArmHStyle", genes);
+		cheatOption.showTextBox().pulldown()
+			.addValue("hairless")
+			.addValue("bushy");
+		cheatOption = cheatOptions.addOption(`Markings: ${capFirstChar(genes.markings)}`, "markings", genes);
+		cheatOption.addValueList([
+			["None", "none"],
+			["Freckles", "freckles"],
+			["Heavily freckled", "heavily freckled"],
+			["Beauty mark", "beauty mark"],
+			["Birthmark", "birthmark"],
+		]);
+		cheatOption = cheatOptions.addOption(`Inbreeding coefficient: ${genes.inbreedingCoeff}`, "inbreedingCoeff", genes);
+		cheatOption.showTextBox();
+
+		let fetusOption;
+		const fetusOptions = new App.UI.OptionsGroup();
+		fetusOptions.customRefresh(() => {
+			fetusInfo(fetus, noteSpan);
+		});
+		if (fetus.reserve !== "") {
+			fetus.noticeData.fate = fetus.reserve;
+		}
+		fetusOption = fetusOptions.addOption("What do you want to do?", "fate", fetus.noticeData);
+		if (unreservedTanks() > 0 || fetus.reserve === "incubator") {
+			fetusOption.addValue(`Reserve a ${incubatorName} tank`, "incubator", () => {
+				fetus.reserve = "incubator";
+			});
+		}
+		if (unreservedCribs() > 0 || fetus.reserve === "nursery") {
+			fetusOption.addValue(`Reserve a ${nurseryName} crib`, "nursery", () => {
+				fetus.reserve = "nursery";
+			});
+		}
+		if (fetus.age === 2) {
+			fetusOption.addValue(`Ask me again in ${num(4)} more weeks`, "wait", () => {
+				fetus.reserve = "";
+			});
+		}
+		fetusOption.addValue("Do nothing", "nothing", () => {
+			fetus.reserve = "";
+		});
+		if (canTransplant !== 0) {
+			fetusOption.addValue("Transplant Fetus", "transplant", () => {
+				fetus.reserve = "";
+			});
+		}
+		if (canTerminate) {
+			fetusOption.addValue("Terminate Fetus", "terminate", () => {
+				fetus.reserve = "";
+			});
+		}
+
+		// create div
+		let fetusDiv = App.UI.DOM.makeElement("div");
+		fetusDiv.id = `fetus-id-${fetus.ID}`;
+		const oldDiv = $(`#fetus-id-${fetus.ID}`);
+		if (oldDiv.length) {
+		// if old div exists replace it
+			oldDiv.replaceWith(fetusDiv);
+		} else {
+		// otherwise add div to frag
+			frag.append(fetusDiv);
+		}
+		// add intro to div
+		fetusDiv.append(intro.container());
+
+		// add main choice to div
+		fetusDiv.append(fetusOptions.render());
+
+		if (!["terminate", "transplant"].includes(fetus.noticeData.fate)) {
+			if (hasIncubator) {
+				const countString = `There are ${num(unreservedTanks())} unreserved tanks in the ${incubatorName}.`;
+				let freeCountDiv = App.UI.DOM.makeElement(
+					"div",
+					countString,
+					["note"]
+				);
+				freeCountDiv.classList.add(`incubator-free-count`);
+				fetusDiv.append(freeCountDiv);
+				// update all counts
+				$(".incubator-free-count").each((index, element) => {
+					element.innerHTML = countString;
+				});
+			}
+			if (hasNursery) {
+				const countString = `There are ${num(unreservedCribs())} unreserved cribs in the ${nurseryName}.`;
+				let freeCountDiv = App.UI.DOM.makeElement(
+					"div",
+					countString,
+					["note"]
+				);
+				freeCountDiv.classList.add(`nursery-free-count`);
+				fetusDiv.append(freeCountDiv);
+				// update all counts
+				$(".nursery-free-count").each((index, element) => {
+					element.innerHTML = countString;
+				});
+			}
+		}
+
+		if (fetus.noticeData.fate === "wait") {
+			fetusDiv.append(App.UI.DOM.makeElement(
+				"div",
+				`You will not be able to terminate this fetus later.`,
+				["note"]
+			));
+		}
+
+		if (["incubator", "nursery"].includes(fetus.noticeData.fate)) {
+			fetusDiv.append(App.UI.DOM.makeElement(
+				"div",
+				`A ${(fetus.reserve === "incubator") ? "tank": "crib"} is being reserved for the child in the ${(fetus.reserve === "incubator") ? incubatorName : nurseryName}.`,
+				["note"]
+			));
+			if (fetus.noticeData.fate === "nursery") {
+				fetusDiv.append(App.UI.DOM.makeElement(
+					"div",
+					`If they are sent to the nursery then the description above will not match when born is born.`,
+					["note"]
+					// TODO:@franklygeorge fix this. Probably need to make InfantState an extension of SlaveState or just convert it to SlaveState instead
+				));
+			}
+		}
+
+		if (fetus.noticeData.fate === "terminate") {
+			const options = new App.UI.OptionsGroup();
+			options.customRefresh(() => {
+				WombRemoveFetus(mother, mother.womb.indexOf(fetus));
+				if (mother.preg === 0) {
+					mother.pregWeek = -1;
+				}
+				let jDiv = $(`#fetus-id-${fetus.ID}`);
+				jDiv.empty().append("Fetus terminated");
+			});
+			options.addOption("", "do", {do: false})
+				.addValue("Terminate", true);
+			fetusDiv.append(options.render());
+		}
+
+		if (fetus.noticeData.fate === "transplant") {
+			if (canTransplant === -1) {
+				let originalMother = (fetus.motherID === -1) ? V.PC : getSlave(fetus.motherID);
+				// @ts-expect-error PlayerState not assignable to SlaveState
+				const popup = (originalMother.ID === -1) ? App.UI.DOM.makeElement("div", "yourself"): App.UI.DOM.slaveDescriptionDialog(originalMother, SlaveFullName(originalMother));
+				popup.classList.add("slave-name", "bold");
+				App.UI.DOM.appendNewElement("span", fetusDiv, `Fetus cannot be transplanted because it has already been transplanted. The original mother was `);
+				App.UI.DOM.appendNewElement("span", fetusDiv, popup);
+			} else {
+				let transplantingCost = V.surgeryCost * 2;
+				// Slave selector
+				let eligibility = 0;
+				let options = new App.UI.OptionsGroup();
+				options.customRefresh(() => {
+					fetusInfo(fetus, noteSpan);
+				});
+				let option = options.addOption(`Select a host`, "transplantReceptrix", fetus.noticeData);
+
+				App.UI.DOM.appendNewElement("h4", fetusDiv, "Slave details");
+				for (const slave of V.slaves) {
+					if ((mother.ID !== slave.ID && slave.ovaries > 0 || slave.mpreg > 0) &&
+						isSlaveAvailable(slave) && slave.preg >= 0 && slave.preg < slave.pregData.normalBirth / 10 &&
+						slave.pregWeek >= 0 && slave.pubertyXX === 1 && slave.pregType < 12 &&
+						slave.bellyImplant === -1 && slave.broodmother === 0 && slave.inflation <= 2 && slave.physicalAge < 70
+					) {
+						const slaveView = App.UI.DOM.appendNewElement("div", fetusDiv, App.SlaveAssignment.saSlaveName(slave));
+						if (slave.pregType === 0) {
+							App.UI.DOM.appendNewElement("span", slaveView, "  Their womb is empty", ["note", "green"]);
+						} else if (slave.pregType >= 4) {
+							App.UI.DOM.appendNewElement("span", slaveView, `  Using a slave carrying multiples is inadvisable; Their womb already has ${num(slave.pregType)} fetuses in it`, ["note", "red"]);
+						} else {
+							App.UI.DOM.appendNewElement("span", slaveView, `  Their womb already has ${num(slave.pregType)} fetuses in it`, ["note", "yellow"]);
+						}
+						option.addValue(SlaveFullName(slave), slave.ID);
+						eligibility = 1;
+					}
+				}
+				if (eligibility === 0) {
+					App.UI.DOM.appendNewElement("div", fetusDiv, "You have no slaves capable of acting as a surrogate.");
+				}
+
+				if (V.PC.vagina !== -1 && mother.ID !== -1 && V.PC.preg >= 0 && V.PC.preg < 4 && V.PC.pregType < 8 && V.PC.physicalAge < 70) {
+					if (V.PC.womb.length === 0) {
+						App.UI.DOM.appendNewElement("h4", fetusDiv, "Your womb is empty.", ["green"]);
+					} else if (V.PC.pregType >= 4) {
+						App.UI.DOM.appendNewElement("span", fetusDiv, `Putting another child in your womb is inadvisable; Your womb already has ${num(V.PC.pregType)} fetuses in it.`, ["red"]);
+					} else {
+						App.UI.DOM.appendNewElement("h4", fetusDiv, "Your womb has enough room for another child.", ["yellow"]);
+					}
+					option.addValue("Use your own womb", -1);
+				}
+				option.addValue("Undecided", 0);
+				fetusDiv.append(options.render());
+
+				// finalize button
+				if (fetus.noticeData.transplantReceptrix !== 0) {
+					let transplantCommitButton = new App.UI.OptionsGroup();
+					transplantCommitButton.customRefresh(() => {
+						// Set fate to "undecided" so that the event will come up for the new mother. Also stops an infinite recursion loop, so don't remove it unless you know what you are doing
+						fetus.noticeData.fate = "undecided";
+						// If new mother is in the list of already processed slaves remove them from the list
+						V.pregnancyNotice.processedSlaves = V.pregnancyNotice.processedSlaves.filter((id) => {
+							return (id !== fetus.noticeData.transplantReceptrix);
+						});
+						// @ts-expect-error this is not defined in GameVariables
+						V.donatrix = mother;
+						// @ts-expect-error this is not defined in GameVariables
+						V.receptrix = (fetus.noticeData.transplantReceptrix === -1) ? V.PC : getSlave(fetus.noticeData.transplantReceptrix);
+						// @ts-expect-error this is not defined in GameVariables
+						V.wombIndex = mother.womb.indexOf(fetus);
+						cashX(forceNeg(transplantingCost), (fetus.noticeData.transplantReceptrix === -1) ? "PCmedical": "slaveSurgery");
+						V.surgeryType = "transplant";
+						Dialog.setup("Transplant Fetus");
+						Dialog.append(App.UI.surrogacy());
+						Dialog.open();
+
+						let jDiv = $(`#fetus-id-${fetus.ID}`);
+
+						// change the id of div
+						// this fixes a bug where the old div is still on the dom but not visible anymore (SugarCube's history functionality)
+						// and so it gets the new contents that we don't want it to get
+						jDiv.attr('id', `#fetus-id-${fetus.ID}-transplanted`);
+
+						// notify the user that the transplanting was successful
+						jDiv.empty().append(
+							"Fetus has been transplanted into ",
+							(fetus.noticeData.transplantReceptrix === -1) ? "your": App.SlaveAssignment.saSlaveName(getSlave(fetus.noticeData.transplantReceptrix)),
+							(fetus.noticeData.transplantReceptrix === -1) ? "": "'s",
+							" womb."
+						);
+					});
+					transplantCommitButton.addOption("", "do", {do: false})
+						.addValue(`Transplant fetus into ${(fetus.noticeData.transplantReceptrix === -1) ? "your womb" : SlaveFullName(getSlave(fetus.noticeData.transplantReceptrix))}`, true);
+					transplantCommitButton.addComment(`This will cost ${cashFormat(transplantingCost)}`);
+					fetusDiv.append(transplantCommitButton.render());
+				}
+			}
+		}
+
+		if (cheating) {
+			let cheatingAccordion = App.Events.PregnancyNotice.createAccordion(
+				"Cheat Menu",
+				"",
+				fetus.noticeData.cheatAccordionCollapsed,
+			);
+
+			// change fetus.noticeData.cheatAccordionCollapsed when the cheat menu is toggled
+			const attrObserver = new MutationObserver((mutations) => {
+				mutations.forEach(mu => {
+					if (mu.type !== "attributes" && mu.attributeName !== "class") { return; }
+					// @ts-expect-error mu.target returns an element not a node
+					if (mu.target.classList.contains("closed")) {
+						fetus.noticeData.cheatAccordionCollapsed = true;
+					} else {
+						fetus.noticeData.cheatAccordionCollapsed = false;
+					}
+				});
+			});
+			attrObserver.observe(cheatingAccordion.noteSpan.parentElement, {attributes: true});
+
+			cheatingAccordion.contentDiv.append(cheatOptions.render());
+			App.UI.DOM.appendNewElement("h4", cheatingAccordion.contentDiv, "Genetic quirks");
+			cheatingAccordion.contentDiv.append(App.UI.SlaveInteract.geneticQuirks(
+				fetus.genetics,
+				true,
+				undefined,
+				false,
+				{
+					function: fetusInfo,
+					variables: [fetus, noteSpan],
+				}
+			));
+			fetusDiv.append(cheatingAccordion.accordion);
+		}
+		updateProcessed();
+		return frag;
+	}
+};
diff --git a/src/events/nonRandomEvent.js b/src/events/nonRandomEvent.js
index 2d4112195b9fde8c7a87bb67d5a99924e57d794f..6edc274e3fabc3c40b734ab8b1c22bea4f603360 100644
--- a/src/events/nonRandomEvent.js
+++ b/src/events/nonRandomEvent.js
@@ -94,7 +94,10 @@ App.Events.getNonrandomEvents = function() {
 		// rivalry events
 		new App.Events.PRivalInitiation(),
 		new App.Events.PRivalryDispatch(),
-		new App.Events.pHostageAcquisition()
+		new App.Events.pHostageAcquisition(),
+
+		new App.Events.PregnancyNotice.PlayerPregnant(),
+		new App.Events.PregnancyNotice.SlavePregnant(),
 	].concat(App.Mods.events.nonRandom);
 };
 
diff --git a/src/events/scheduled/seCustomSlaveDelivery.js b/src/events/scheduled/seCustomSlaveDelivery.js
index 485157f8cb0e9b309bb4a10f1681985d95aaa629..82b8c9a29151b13f2c03f872362bb58713a6d819 100644
--- a/src/events/scheduled/seCustomSlaveDelivery.js
+++ b/src/events/scheduled/seCustomSlaveDelivery.js
@@ -237,6 +237,18 @@ App.Events.SEcustomSlaveDelivery = class SEcustomSlaveDelivery extends App.Event
 				delivery.origSkin = V.customSlave.skin;
 				delivery.skin = getGeneticSkinColor(delivery);
 			}
+			if (V.customSlave.hairColor !== "hair color is unimportant") {
+				delivery.origHColor = V.customSlave.hairColor;
+				delivery.hColor = getGeneticHairColor(delivery);
+				delivery.eyebrowHColor = getGeneticHairColor(delivery);
+				delivery.pubicHColor = getGeneticHairColor(delivery);
+				delivery.underArmHColor = getGeneticHairColor(delivery);
+			}
+			if (V.customSlave.eyesColor !== "eye color is unimportant") {
+				delivery.eye.origColor = V.customSlave.eyesColor;
+				delivery.eye.left.iris = getGeneticEyeColor(delivery);
+				delivery.eye.right.iris = getGeneticEyeColor(delivery);
+			}
 			delivery.boobs = V.customSlave.boobs;
 			delivery.natural.boobs = delivery.boobs;
 			delivery.butt = V.customSlave.butt;
diff --git a/src/events/scheduled/sePlayerBirth.js b/src/events/scheduled/sePlayerBirth.js
index 9db48d27a0ec4f65999f53b90c40f745cb251e87..054de537201166dff4097109644ded22208756d4 100644
--- a/src/events/scheduled/sePlayerBirth.js
+++ b/src/events/scheduled/sePlayerBirth.js
@@ -806,7 +806,11 @@ App.Events.SEPlayerBirth = class SEPlayerBirth extends App.Events.BaseEvent {
 						}
 						r.push(`aside for incubation.</span>`);
 						if (V.incubator.tanks.length < V.incubator.capacity) {
-							App.Facilities.Incubator.newChild(generateChild(V.PC, birthed[0], true), birthed[0].tankSetting);
+							if (birthed[0].noticeData.child !== undefined) {
+								App.Facilities.Incubator.newChild(birthed[0].noticeData.child, birthed[0].tankSetting);
+							} else {
+								App.Facilities.Incubator.newChild(generateChild(V.PC, birthed[0], true), birthed[0].tankSetting);
+							}
 						}
 					} else if (birthed[0].reserve === "nursery") {
 						r.push(`<span class="pink">You set`);
@@ -817,6 +821,7 @@ App.Events.SEPlayerBirth = class SEPlayerBirth extends App.Events.BaseEvent {
 						}
 						r.push(`aside for incubation.</span>`);
 						if (V.cribs.length < V.nurseryCribs) {
+							// TODO:@franklygeorge handling for birthed[0].noticeData.child. Long term we probably just want to convert InfantState into an extension of SlaveState, or maybe just convert it to SlaveState
 							App.Facilities.Nursery.newChild(generateChild(V.PC, birthed[0]));
 						}
 					}
diff --git a/src/facilities/dressingRoom/dressingRoom.js b/src/facilities/dressingRoom/dressingRoom.js
index 09338a386ab848cc536372b048363cb7b4eaa620..adb9562de53f362b67da0605db9ce9fdab801960 100644
--- a/src/facilities/dressingRoom/dressingRoom.js
+++ b/src/facilities/dressingRoom/dressingRoom.js
@@ -241,13 +241,14 @@ App.UI.DressingRoom.render = function() {
 				 * @returns {string} prompts
 				 */
 				function createComment(type) {
+					let comment = 'no prompts';
 					if (customClothesPrompts[clothingName] && customClothesPrompts[clothingName][type] !== '') {
-						return customClothesPrompts[clothingName][type];
+						comment = customClothesPrompts[clothingName][type];
 					} else if (clothesPrompts[clothingName]) {
-						return clothesPrompts[clothingName][type];
-					} else {
-						return 'no prompts';
+						comment = clothesPrompts[clothingName][type];
 					}
+					comment = comment.replace(/</g, '&lt;');
+					return comment;
 				}
 			}
 		}
@@ -445,7 +446,8 @@ App.UI.DressingRoom.render = function() {
 						};
 
 						cont.appendChild(element);
-						cont.append(element);
+						cont.appendChild(document.createElement("br"));
+						cont.appendChild(submitBtn);
 					}
 				}
 			)
diff --git a/src/facilities/farmyard/shows/saFarmyardShows.js b/src/facilities/farmyard/shows/saFarmyardShows.js
index 8b8be9195bdbfeaee6c3cbd425e01ae7cfe9ae9c..4a7d8298cc81524f4aaec0993da31fb12197f5b2 100644
--- a/src/facilities/farmyard/shows/saFarmyardShows.js
+++ b/src/facilities/farmyard/shows/saFarmyardShows.js
@@ -369,7 +369,7 @@ App.Facilities.Farmyard.putOnShows = function(slave) {
 
 	function pregnancy() {
 		if (isPreg(slave)) {
-			return `${His}${slave.bellyPreg > 100000 ? ` advanced` : ``} pregnancy makes it more difficult for him to effectively put on a good show.`;
+			return `${His}${slave.bellyPreg > 100000 ? ` advanced` : ``} pregnancy makes it more difficult for ${him} to effectively put on a good show.`;
 		}
 	}
 
diff --git a/src/facilities/penthouse/managePenthouse.js b/src/facilities/penthouse/managePenthouse.js
index 2a0999b160a39b5ef2f3a1bc290fc5a489868c30..f5d1c06f5c61d7fd8c3c7e25a639962dfdc3aae2 100644
--- a/src/facilities/penthouse/managePenthouse.js
+++ b/src/facilities/penthouse/managePenthouse.js
@@ -141,7 +141,7 @@ App.UI.managePenthouse = function() {
 				} else {
 					App.UI.DOM.appendNewElement("div", el, `The penthouse has a specialized facility dedicated to rapidly aging children.`);
 				}
-			} else if (V.arcologyUpgrade.hydro === 1 || V.arcologyUpgrade.apron === 1) {
+			} else {
 				App.UI.DOM.appendNewElement("div", el, App.UI.DOM.makeElement("span", "Installation of a child aging facility will require the arcology's electrical infrastructure to be overhauled.", ["note"]));
 			}
 		}
diff --git a/src/facilities/surgery/analyzePregnancy.js b/src/facilities/surgery/analyzePregnancy.js
index 3011cce08d34c1d3a21d1148b243c52ece447179..a7e61453585edd36bb77d55ed48cbdf3f09dc9d4 100644
--- a/src/facilities/surgery/analyzePregnancy.js
+++ b/src/facilities/surgery/analyzePregnancy.js
@@ -15,6 +15,10 @@ globalThis.analyzePregnancies = function(mother, cheat) {
 		const el = new DocumentFragment();
 		const fetus = mother.womb[i];
 		const genes = fetus.genetics;
+
+		const canTerminate = canTerminateFetus(mother, fetus);
+		const canTransplant = canTransplantFetus(mother, fetus);
+
 		let option;
 		const options = new App.UI.OptionsGroup();
 		if (fetus.age >= 2 || cheat) {
@@ -27,7 +31,7 @@ globalThis.analyzePregnancies = function(mother, cheat) {
 				option.showTextBox();
 			}
 			if (V.geneticMappingUpgrade >= 1 || cheat) {
-				option = options.addOption(`Gender: ${genes.gender}`, "gender", genes);
+				option = options.addOption(`Gender: ${geneToGender(genes.gender, {keepKaryotype: true, lowercase: false})}`, "gender", genes);
 				if (cheat) {
 					option.addValue("Female", "XX");
 					option.addValue("Male", "XY");
@@ -137,20 +141,8 @@ globalThis.analyzePregnancies = function(mother, cheat) {
 				App.UI.DOM.appendNewElement("div", el, `Reserved: ${fetus.reserve}`);
 			}
 
-			if (fetus.age < 4 && (!FutureSocieties.isActive('FSRestart') || V.eugenicsFullControl === 1 || mother.breedingMark === 0 || V.propOutcome === 0 || (fetus.fatherID !== -1 && fetus.fatherID !== -6)) || cheat) {
-				option = terminateOvum();
-				if (V.surgeryUpgrade === 1) {
-					option.addButton(
-						"Transplant ovum",
-						() => {
-							V.donatrix = mother;
-							V.wombIndex = i;
-							V.nextLink = passage();
-						},
-						"Ova Transplant Workaround"
-					);
-				}
-			}
+			ovumSurgery();
+
 			if (V.incubator.capacity > 0) {
 				if (fetus.reserve === "incubator") {
 					App.UI.DOM.appendNewElement("div", el, App.UI.DOM.link(
@@ -233,19 +225,8 @@ globalThis.analyzePregnancies = function(mother, cheat) {
 		} else {
 			App.UI.DOM.appendNewElement("div", el, `Unidentified ova found, no detailed data available.`);
 			App.UI.DOM.appendNewElement("div", el, `Age: too early for scan.`);
-			option = terminateOvum();
 
-			if (V.surgeryUpgrade === 1) {
-				option.addButton(
-					`Transplant ovum`,
-					() => {
-						V.donatrix = mother;
-						V.wombIndex = i;
-						V.nextLink = "Analyze Pregnancy";
-					},
-					`Ova Transplant Workaround`
-				);
-			}
+			ovumSurgery();
 		}
 		el.append(options.render());
 
@@ -272,18 +253,38 @@ globalThis.analyzePregnancies = function(mother, cheat) {
 			return div;
 		}
 
-		function terminateOvum() {
-			return options.addCustomOption(`Surgical options`)
-				.addButton(
-					`Terminate ovum`,
-					() => {
-						WombRemoveFetus(mother, i);
-						if (mother.preg === 0) {
-							mother.pregWeek = -1;
-						}
-					},
-					passage()
-				);
+		/**
+		 * Adds buttons for transplanting and/or termination if they are allowable
+		 */
+		function ovumSurgery() {
+			if (canTerminate || canTransplant === 1) {
+				const row = options.addCustomOption(`Surgical options`);
+				if (canTerminate) {
+					row.addButton(
+						`Terminate ovum`,
+						() => {
+							WombRemoveFetus(mother, i);
+							if (mother.preg === 0) {
+								mother.pregWeek = -1;
+							}
+						},
+						passage()
+					);
+				}
+				if (canTransplant === 1) {
+					row.addButton(
+						"Transplant ovum",
+						() => {
+							// @ts-expect-error donatrix doesn't exist in V
+							V.donatrix = mother;
+							// @ts-expect-error wombIndex doesn't exist in V
+							V.wombIndex = i;
+							V.nextLink = passage();
+						},
+						"Ova Transplant Workaround"
+					);
+				}
+			}
 		}
 	}
 };
diff --git a/src/facilities/surgery/geneticQuirks.js b/src/facilities/surgery/geneticQuirks.js
index 5ef2961d4bdbb7ce4cd4e1dfca2d3fc7bacf5f49..a159d482734b9d6c75cd4dd058de77d44e3a82f9 100644
--- a/src/facilities/surgery/geneticQuirks.js
+++ b/src/facilities/surgery/geneticQuirks.js
@@ -1,13 +1,38 @@
+/**
+ * @typedef {object} App.UI.SlaveInteract.geneticQuirks.ReloadData
+ * @property {Function} function the function to be called when a page reload is requested
+ * @property {any[]|undefined} variables a list of variables to pass to the function or undefined to pass nothing
+ */
+
 /**
  * @param {App.Entity.SlaveState|FC.FetusGenetics} slave
- * @param {boolean} allInactive
+ * @param {boolean} allInactive If false then we only show the active genetic quirks
  * @param {function(keyof FC.GeneticQuirks):boolean} [filter]
- * @param {boolean} [onlyGenetics=false]
+ * @param {boolean} [changePhysicalTraitsToMatch=false] If true then we change the slaves physical traits to match what you would expect from their genetics.
+ * @param {App.UI.SlaveInteract.geneticQuirks.ReloadData} reloadData used to supply an alterative to reloading the page
  * @returns {DocumentFragment}
  */
-App.UI.SlaveInteract.geneticQuirks = function(slave, allInactive, filter, onlyGenetics = false) {
+App.UI.SlaveInteract.geneticQuirks = function(
+	slave,
+	allInactive,
+	filter,
+	changePhysicalTraitsToMatch = false,
+	reloadData = {
+		function: undefined,
+		variables: undefined,
+	}
+) {
 	const el = new DocumentFragment();
 	const options = new App.UI.OptionsGroup();
+	if (reloadData.function !== undefined) {
+		options.customRefresh(() => {
+			if (reloadData.variables === undefined) {
+				reloadData.function();
+			} else {
+				reloadData.function(...reloadData.variables);
+			}
+		});
+	}
 	for (const [key, obj] of App.Data.geneticQuirks) {
 		if (obj.hasOwnProperty("requirements") && !obj.requirements) {
 			continue;
@@ -24,7 +49,7 @@ App.UI.SlaveInteract.geneticQuirks = function(slave, allInactive, filter, onlyGe
 			for (const color of App.Medicine.Modification.eyeColor.map(color => color.value)) {
 				option.addValue(capFirstChar(color), color);
 			}
-			if (!onlyGenetics) {
+			if (!changePhysicalTraitsToMatch) {
 				// @ts-ignore
 				option.addGlobalCallback(() => resetEyeColor(slave));
 			}
diff --git a/src/facilities/wardrobe/wardrobeShopping.js b/src/facilities/wardrobe/wardrobeShopping.js
index 11b2660a8097e599159030c10d7b21a53dc39a35..bb15849b2975788be931401b8a77fa7766292ab5 100644
--- a/src/facilities/wardrobe/wardrobeShopping.js
+++ b/src/facilities/wardrobe/wardrobeShopping.js
@@ -76,7 +76,7 @@ App.UI.WardrobeShopping = function() {
 		}
 
 		if (modelChoices.length > 1) {
-			model = modelChoices.map(slave => {
+			model = structuredClone(modelChoices.map(slave => {
 				// calculate beauty score for all slaves
 				return {
 					slave,
@@ -89,9 +89,9 @@ App.UI.WardrobeShopping = function() {
 				} else {
 					return slave;
 				}
-			}).slave;
+			}).slave);
 		} else if (modelChoices.length === 1) {
-			model = modelChoices[0];
+			model = structuredClone(modelChoices[0]);
 		} else {
 			model = (V.seeDicks === 100) ? GenerateNewSlave("XY") : GenerateNewSlave("XX");
 		}
@@ -116,15 +116,15 @@ App.UI.WardrobeShopping = function() {
 		function createCell(clothing, oldOutfit = "") {
 			const el = document.createElement("div");
 			el.classList.add("wardrobe-shopping-cell");
-			// Not AI
-			if (V.imageChoice !== 6) {
-				el.onclick = () => {
+			el.onclick = () => {
+				// Not AI
+				if (V.imageChoice !== 6) {
 					// Randomize devotion and trust a bit, so the model moves their arms and "poses" for the player.
 					model.devotion = random(-10, 70);
 					model.trust = random(30, 100);
-					jQuery(`#${clothing}`).empty().append(createCell(clothing, model.clothes));
-				};
-			}
+				}
+				jQuery(`#${clothing}`).empty().append(createCell(clothing, model.clothes));
+			};
 
 			/** @type {wardrobeItem} */
 			const clothingObj = App.Data.WardrobeShopping.Clothing[category][clothing];
@@ -148,7 +148,11 @@ App.UI.WardrobeShopping = function() {
 
 				// AI deals with stuff async so we would run into a race condition.
 				if (V.imageChoice === 6) {
-					App.UI.DOM.appendNewElement("div", el, App.Art.aiArtElement(structuredClone(model), 1), ["imageRef", "smlImg"]);
+					// For reactive, all images are saved anyways. Persist them to save processing power in future.
+					const isTempImage = V.aiCachingStrategy !== 'reactive';
+					const cellModel = structuredClone(model);
+					const aiArtElem = App.UI.DOM.appendNewElement("div", el, App.Art.aiArtElement(cellModel, App.Art.ArtSizes.SMALL, isTempImage), ["imageRef", "smlImg"]);
+					if (isTempImage) { aiArtElem.querySelector('[title*="Replace"]').remove(); }
 				} else {
 					App.UI.DOM.appendNewElement("div", el, App.Art.SlaveArtElement(model, 1, 0), ["imageRef", "smlImg"]);
 				}
diff --git a/src/gui/favorite.js b/src/gui/favorite.js
index 97204b9bfee948bfe41e1ca34b93cc7e60ef0036..61c3a1eec8c0614d4a55d3425f9ca3392d1d239d 100644
--- a/src/gui/favorite.js
+++ b/src/gui/favorite.js
@@ -4,6 +4,9 @@
  * @returns {HTMLAnchorElement}
  */
 App.UI.favoriteToggle = function(slave, handler) {
+	/**
+	 * @returns {HTMLAnchorElement}
+	 */
 	function favLink() {
 		const linkID = `fav-link-${slave.ID}`;
 		if (V.favorites.includes(slave.ID)) {
diff --git a/src/gui/options/options.js b/src/gui/options/options.js
index fb5d1a1e7abd7882e7e6da668d05de0828952a80..c295d7915cf35c937590385c91ae95779a489b62 100644
--- a/src/gui/options/options.js
+++ b/src/gui/options/options.js
@@ -928,6 +928,22 @@ App.Intro.display = function(isIntro) {
 	options.addOption("Default Rules Assistant mode is", "raDefaultMode")
 		.addValue("Simple", 0).addValue("Advanced", 1);
 
+	options.addOption(
+		"Favorite and reminder buttons in front of some slave description links are",
+		"addButtonsToSlaveLinks"
+	)
+		.addValue("Enabled", true).on().addValue("Disabled", false).off();
+
+	options.addOption("Pregnancy reports are", "enabled", V.pregnancyNotice)
+		.addValue("Enabled", true).addValue("Disabled", false)
+		.addComment("You have to buy the 'pregnancy monitoring system upgrade' for these reports to happen.");
+
+	if (V.pregnancyNotice.enabled === true) {
+		options.addOption("Rendering of the child in pregnancy reports is", "renderFetus", V.pregnancyNotice)
+			.addValue("Enabled", true).addValue("Disabled", false)
+			.addComment("Requires the 'basic genetic sequencer' to render");
+	}
+
 	el.append(options.render());
 
 	r = [];
@@ -1373,6 +1389,10 @@ App.UI.artOptions = function() {
 					}
 				}
 
+				options.addOption("Apply RA prompt changes for event images", "aiUseRAForEvents")
+					.addValue("Enabled", true).on().addValue("Disabled", false).off()
+					.addComment("Apply image generation prompt changes from Rules Assistant for event images, including slave marketplace images. Useful for customizing prompts of non-owned slaves.");
+
 				const samplerListSpan = App.UI.DOM.makeElement('span', `Fetching options, please wait...`);
 				App.Art.GenAI.sdClient.getSamplerList().then(list => {
 					if (list.length === 0) {
diff --git a/src/gui/options/stableDiffusionInstallationGuide.js b/src/gui/options/stableDiffusionInstallationGuide.js
index a7748aa98865b52e9484e6f6b7ced320f999d406..8918b70c71438f9a14306a5c9a2a09167d9d1b75 100644
--- a/src/gui/options/stableDiffusionInstallationGuide.js
+++ b/src/gui/options/stableDiffusionInstallationGuide.js
@@ -97,6 +97,7 @@ You'll need to download any or all of the relevant LoRAs:
 	<li><a href="https://huggingface.co/NGBot/ampuLora/blob/main/Standing%20Straight%20%20v1%20-%20locon%2032dim.safetensors">Fuckdoll Posture</a></li>
 	<li><a href="https://huggingface.co/NGBot/ampuLora/blob/main/OnlyCocksV1LORA.safetensors">Improved Average Male Assets</a></li>
 	<li><a href="https://huggingface.co/NGBot/ampuLora/blob/main/CatgirlLoraV7.safetensors">Catperson Lora</a></li>
+	<li><a href="https://civitai.com/api/download/models/131998?type=Model&format=SafeTensor">High Profile Implants</a></li>
 </ul>
 
 Copy any that you've chosen to use into your <code>stable-diffusion-webui/models/Lora</code> folder (see the Stable Diffusion Installation instructions for details).
diff --git a/src/interaction/siCustom.js b/src/interaction/siCustom.js
index 74dd91fd7d5b70610745be3142d9934117d606aa..f494c4ec940a36c7789af34c4ff980f046679ab0 100644
--- a/src/interaction/siCustom.js
+++ b/src/interaction/siCustom.js
@@ -722,16 +722,12 @@ App.UI.SlaveInteract.custom = function(slave, refresh) {
 		let el = document.createElement('div');
 		let label = document.createElement('div');
 
-		if (slave.custom.aiPrompts == null) {
-			slave.custom.aiPrompts = new App.Entity.SlaveCustomAIPrompts();
-		}
-
 		const links = [];
 		links.push(
 			App.UI.DOM.link(
 				`Exclude`,
 				() => {
-					slave.custom.aiPrompts.aiAutoRegenExclude = 1;
+					slave.custom.aiAutoRegenExclude = 1;
 					refresh();
 				},
 			)
@@ -741,14 +737,14 @@ App.UI.SlaveInteract.custom = function(slave, refresh) {
 			App.UI.DOM.link(
 				`Include`,
 				() => {
-					slave.custom.aiPrompts.aiAutoRegenExclude = 0;
+					slave.custom.aiAutoRegenExclude = 0;
 					refresh();
 				},
 			)
 		);
 
 		label.append(`Exclude ${him} from automatic image generation: `);
-		App.UI.DOM.appendNewElement("span", label, slave.custom.aiPrompts.aiAutoRegenExclude ? "Excluded" : "Included", ["bold"]);
+		App.UI.DOM.appendNewElement("span", label, slave.custom.aiAutoRegenExclude ? "Excluded" : "Included", ["bold"]);
 
 		el.appendChild(label);
 		el.appendChild(App.UI.DOM.generateLinksStrip(links));
@@ -777,6 +773,48 @@ App.UI.SlaveInteract.custom = function(slave, refresh) {
 			return el;
 		}
 
+		/** Add HTML for overriding positive expression prompt */
+		function expressionPositivePrompt() {
+			let el = document.createElement('p');
+			el.append(`Override ${his} positive expression prompt: `);
+			el.appendChild(
+				App.UI.DOM.makeTextBox(
+					slave.custom.aiPrompts.expressionPositive,
+					v => {
+						slave.custom.aiPrompts.expressionPositive = v;
+						$(promptDiv).empty().append(genAIPrompt());
+					}
+				)
+			);
+
+			let choices = document.createElement('div');
+			choices.className = "choices";
+			choices.appendChild(App.UI.DOM.makeElement('span', ` This prompt will replace the default positive facial expression prompts. Example: 'smile, grin, loving expression'.`, 'note'));
+			el.appendChild(choices);
+			return el;
+		}
+
+		/** Add HTML for overriding negative expression prompt */
+		function expressionNegativePrompt() {
+			let el = document.createElement('p');
+			el.append(`Override ${his} negative expression prompt: `);
+			el.appendChild(
+				App.UI.DOM.makeTextBox(
+					slave.custom.aiPrompts.expressionNegative,
+					v => {
+						slave.custom.aiPrompts.expressionNegative = v;
+						$(promptDiv).empty().append(genAIPrompt());
+					}
+				)
+			);
+
+			let choices = document.createElement('div');
+			choices.className = "choices";
+			choices.appendChild(App.UI.DOM.makeElement('span', ` This prompt will replace the default negative facial expression prompts. Example: 'angry'.`, 'note'));
+			el.appendChild(choices);
+			return el;
+		}
+
 		function positivePrompt() {
 			let el = document.createElement('p');
 			el.append(`Add positive prompts: `);
@@ -836,6 +874,8 @@ App.UI.SlaveInteract.custom = function(slave, refresh) {
 			if (slave.custom.aiPrompts) {
 				customDiv.append(
 					posePrompt(),
+					expressionPositivePrompt(),
+					expressionNegativePrompt(),
 					positivePrompt(),
 					negativePrompt(),
 				);
diff --git a/src/js/CustomSlave.js b/src/js/CustomSlave.js
index 4f417ad6d4884541e98d6ef0c6e7eed690791c28..b22ea05e8e2b8dd63a759c7f9bfea4453d7cf20e 100644
--- a/src/js/CustomSlave.js
+++ b/src/js/CustomSlave.js
@@ -65,6 +65,18 @@ App.Entity.CustomSlaveOrder = class CustomSlaveOrder {
 		 */
 		this.skin = "left natural";
 
+		/** desired hair color
+		 * "hair color is unimportant" or other values as in SlaveState
+		 * @type {string}
+		 */
+		this.hairColor = "hair color is unimportant";
+
+		/** desired eye color
+		 * "eye color is unimportant" or other values as in SlaveState
+		 * @type {string}
+		 */
+		this.eyesColor = "eye color is unimportant";
+
 		/** desired boob size
 		 * Values as in SlaveState.
 		 * @type {number}
diff --git a/src/js/DefaultRules.js b/src/js/DefaultRules.js
index dd3fcec25fe7610011213c5dc024aeff1f3c435a..9916752d5a6e3f590206f4d9c49cfb6833542a36 100644
--- a/src/js/DefaultRules.js
+++ b/src/js/DefaultRules.js
@@ -3,7 +3,7 @@
  * @param {App.Entity.SlaveState} slave
  * @returns {string}
  */
-globalThis.DefaultRules = function(slave) {
+globalThis.DefaultRules = function(slave, options) {
 	if (slave.useRulesAssistant === 0) {
 		return ""; // exempted
 	}
@@ -19,68 +19,70 @@ globalThis.DefaultRules = function(slave) {
 	const pronouns = getPronouns(slave);
 	const {he, him, his} = pronouns;
 
-	AssignJobToSlave(slave, rule);
-	if (slave.fuckdoll === 0) {
-		ProcessClothing(slave, rule);
-		ProcessCollar(slave, rule);
-		ProcessMask(slave, rule);
-		ProcessGag(slave, rule);
-		ProcessEyewear(slave, rule);
-		ProcessEarwear(slave, rule);
-		ProcessDildos(slave, rule);
-		ProcessDickAccessories(slave, rule);
-		ProcessAnalAccessories(slave, rule);
-		ProcessChastity(slave, rule);
-		ProcessShoes(slave, rule);
-		ProcessBellyAccessories(slave, rule);
-		ProcessArmAccessory(slave, rule);
-		ProcessLegAccessory(slave, rule);
-	}
-	ProcessPit(slave, rule);
-	ProcessBellyImplant(slave, rule);
-	if (isFertile(slave) || slave.pregWeek < 0) {
-		ProcessContraceptives(slave, rule);
-	}
-	if (slave.preg > 0 && slave.pregKnown === 1 && slave.broodmother === 0) {
-		ProcessAbortions(slave, rule);
-	}
-	ProcessDrugs(slave, rule);
-	ProcessEnema(slave, rule);
-	ProcessDiet(slave, rule);
-	ProcessCuratives(slave, rule);
-	ProcessAphrodisiacs(slave, rule);
-	ProcessPenisHormones(slave, rule);
-	ProcessFemaleHormones(slave, rule);
-	ProcessPregnancyDrugs(slave, rule);
-	if (slave.fuckdoll === 0) {
-		ProcessLivingStandard(slave, rule);
-		ProcessRest(slave, rule);
-		ProcessSpeech(slave, rule);
-		ProcessRelationship(slave, rule);
-		ProcessRelease(slave, rule);
-		ProcessLactation(slave, rule);
-		if (!canWalk(slave) && canMove(slave)) {
-			ProcessMobility(slave, rule);
-		}
-		ProcessPunishment(slave, rule);
-		ProcessReward(slave, rule);
-	}
-	ProcessToyHole(slave, rule);
-	ProcessDietCum(slave, rule);
-	ProcessDietMilk(slave, rule);
-	if (V.arcologies[0].FSHedonisticDecadenceResearch === 1) {
-		ProcessSolidFood(slave, rule);
-	}
-	ProcessTeeth(slave, rule);
-	ProcessStyle(slave, rule);
-	ProcessPiercings(slave, rule);
-	ProcessSmartPiercings(slave, rule);
-	ProcessTattoos(slave, rule);
-	ProcessBrands(slave, rule);
-	ProcessPornFeedEnabled(slave, rule);
-	ProcessPorn(slave, rule);
-	ProcessLabel(slave, rule);
-	ProcessOther(slave, rule);
+	if (!options?.aiRulesOnly) {
+		AssignJobToSlave(slave, rule);
+		if (slave.fuckdoll === 0) {
+			ProcessClothing(slave, rule);
+			ProcessCollar(slave, rule);
+			ProcessMask(slave, rule);
+			ProcessGag(slave, rule);
+			ProcessEyewear(slave, rule);
+			ProcessEarwear(slave, rule);
+			ProcessDildos(slave, rule);
+			ProcessDickAccessories(slave, rule);
+			ProcessAnalAccessories(slave, rule);
+			ProcessChastity(slave, rule);
+			ProcessShoes(slave, rule);
+			ProcessBellyAccessories(slave, rule);
+			ProcessArmAccessory(slave, rule);
+			ProcessLegAccessory(slave, rule);
+		}
+		ProcessPit(slave, rule);
+		ProcessBellyImplant(slave, rule);
+		if (isFertile(slave) || slave.pregWeek < 0) {
+			ProcessContraceptives(slave, rule);
+		}
+		if (slave.preg > 0 && slave.pregKnown === 1 && slave.broodmother === 0) {
+			ProcessAbortions(slave, rule);
+		}
+		ProcessDrugs(slave, rule);
+		ProcessEnema(slave, rule);
+		ProcessDiet(slave, rule);
+		ProcessCuratives(slave, rule);
+		ProcessAphrodisiacs(slave, rule);
+		ProcessPenisHormones(slave, rule);
+		ProcessFemaleHormones(slave, rule);
+		ProcessPregnancyDrugs(slave, rule);
+		if (slave.fuckdoll === 0) {
+			ProcessLivingStandard(slave, rule);
+			ProcessRest(slave, rule);
+			ProcessSpeech(slave, rule);
+			ProcessRelationship(slave, rule);
+			ProcessRelease(slave, rule);
+			ProcessLactation(slave, rule);
+			if (!canWalk(slave) && canMove(slave)) {
+				ProcessMobility(slave, rule);
+			}
+			ProcessPunishment(slave, rule);
+			ProcessReward(slave, rule);
+		}
+		ProcessToyHole(slave, rule);
+		ProcessDietCum(slave, rule);
+		ProcessDietMilk(slave, rule);
+		if (V.arcologies[0].FSHedonisticDecadenceResearch === 1) {
+			ProcessSolidFood(slave, rule);
+		}
+		ProcessTeeth(slave, rule);
+		ProcessStyle(slave, rule);
+		ProcessPiercings(slave, rule);
+		ProcessSmartPiercings(slave, rule);
+		ProcessTattoos(slave, rule);
+		ProcessBrands(slave, rule);
+		ProcessPornFeedEnabled(slave, rule);
+		ProcessPorn(slave, rule);
+		ProcessLabel(slave, rule);
+		ProcessOther(slave, rule);
+	}
 	if (V.imageChoice === 6) {
 		ProcessPrompts(slave, rule);
 	}
@@ -3261,7 +3263,7 @@ globalThis.DefaultRules = function(slave) {
 		}
 
 		/**
-		 * @param {string} promptField "positive","negative"
+		 * @param {"positive"|"negative"} promptField
 		 */
 		function assignPrompts(promptField) {
 			let newPrompts;
@@ -3284,7 +3286,7 @@ globalThis.DefaultRules = function(slave) {
 			if (promptsEqual(newPrompts, oldPrompts)) {
 				return;
 			}
-			if (!rule.overridePrompts){
+			if (!rule.overridePrompts) {
 				if (oldPrompts.includesAny(newPrompts)) { // if appending and old prompts have overlap with new prompts
 					oldPrompts = oldPrompts.filter(value => !newPrompts.includes(value));
 				}
@@ -3302,12 +3304,9 @@ globalThis.DefaultRules = function(slave) {
 		}
 
 		// custom prompts setup
-		if (slave.custom.aiPrompts == null) {
+		const hasPrompt = !!(rule.posePrompt || rule.expressionNegativePrompt || rule.expressionPositivePrompt || rule.positivePrompt || rule.negativePrompt);
+		if (hasPrompt && slave.custom.aiPrompts == null) {
 			slave.custom.aiPrompts = new App.Entity.SlaveCustomAIPrompts();
-			// if the player has yet to set custom values for the slave a null type error is passed
-			slave.custom.aiPrompts.pose = '';
-			slave.custom.aiPrompts.positive = '';
-			slave.custom.aiPrompts.negative = '';
 		}
 
 		// custom pose prompt
@@ -3318,6 +3317,22 @@ globalThis.DefaultRules = function(slave) {
 			}
 		}
 
+		// custom expression positive prompt
+		if (rule.expressionPositivePrompt != null && rule.expressionPositivePrompt !== '') {
+			if (slave.custom.aiPrompts.expressionPositive !== rule.expressionPositivePrompt) {
+				slave.custom.aiPrompts.expressionPositive = rule.expressionPositivePrompt;
+				message(`${slave.slaveName} has had ${his} positive expression prompt set.`, sourceRecord.expressionPositivePromptPrompt);
+			}
+		}
+
+		// custom expression negative prompt
+		if (rule.expressionNegativePrompt != null && rule.expressionNegativePrompt !== '') {
+			if (slave.custom.aiPrompts.expressionNegative !== rule.expressionNegativePrompt) {
+				slave.custom.aiPrompts.expressionNegative = rule.expressionNegativePrompt;
+				message(`${slave.slaveName} has had ${his} negative expression prompt set.`, sourceRecord.expressionNegativePromptPrompt);
+			}
+		}
+
 		// custom positive prompts
 		if (rule.positivePrompt != null && rule.positivePrompt !== '') {
 			assignPrompts("positive");
@@ -3377,12 +3392,12 @@ globalThis.DefaultRules = function(slave) {
 		}
 
 		// auto generation exclusion set
-		if (rule.aiAutoRegenExclude != null && slave.custom.aiPrompts.aiAutoRegenExclude !== rule.aiAutoRegenExclude) {
+		if (rule.aiAutoRegenExclude != null && slave.custom.aiAutoRegenExclude !== rule.aiAutoRegenExclude) {
 			if (rule.aiAutoRegenExclude) {
-				slave.custom.aiPrompts.aiAutoRegenExclude = 1;
+				slave.custom.aiAutoRegenExclude = 1;
 				message(`${slave.slaveName} will not have new images auto generated`, sourceRecord.aiAutoRegenExclude);
 			} else {
-				slave.custom.aiPrompts.aiAutoRegenExclude = 0;
+				slave.custom.aiAutoRegenExclude = 0;
 				message(`${slave.slaveName} will have new images auto generated`, sourceRecord.aiAutoRegenExclude);
 			}
 		}
diff --git a/src/js/SlaveState.js b/src/js/SlaveState.js
index 308cc32300da834ef1a69b9ae546f4342feda0d0..6d9749800310e4f81615980bf55bb8ab598b9559 100644
--- a/src/js/SlaveState.js
+++ b/src/js/SlaveState.js
@@ -367,14 +367,14 @@ App.Entity.SlaveCustomAIPrompts = class SlaveCustomAIPrompts {
 	constructor() {
 		/** replaces the slave's posture prompts with a custom string for user-specified poses */
 		this.pose = "";
+		/** replaces the slave's expression positive prompt with a custom string for user-specified expressions */
+		this.expressionPositive = "";
+		/** replaces the slave's expression negative prompt with a custom string for user-specified expressions */
+		this.expressionNegative = "";
 		/** manually adds to the dynamic positive prompt string */
 		this.positive = "";
 		/** manually adds to the dynamic negative prompt string */
 		this.negative = "";
-		/** skips this slave's image in the weekly ai auto regeneration
-		 * @type {FC.Bool}
-		 * 0: no; 1: yes */
-		this.aiAutoRegenExclude = 0;
 	}
 };
 
@@ -429,6 +429,10 @@ App.Entity.SlaveCustomAddonsState = class SlaveCustomAddonsState {
 		 * @type {FC.Zeroable<string>}
 		 */
 		this.hairVector = 0;
+		/** skips this slave's image in the weekly ai auto regeneration
+		 * @type {FC.Bool}
+		 * 0: no; 1: yes */
+		this.aiAutoRegenExclude = 0;
 		/**
 		 * holds the ai image ID
 		 *
diff --git a/src/js/birth/birth.js b/src/js/birth/birth.js
index 259d56e27f4b5a403fda3df9526d536babc87d7e..326f23f25aa560f145192e8096d6f05fe0073b66 100644
--- a/src/js/birth/birth.js
+++ b/src/js/birth/birth.js
@@ -9376,15 +9376,20 @@ globalThis.birth = function(slave, {birthStorm = false, cSection = false, artRen
 /**
  * Sends newborns to incubator or nursery
  * @param {App.Entity.SlaveState} mom
- * @param {object[]} babiesBeingBorn ovum objects (FIXME: need defined type)
- * @returns {object[]} remaining ova
+ * @param {App.Entity.Fetus[]} babiesBeingBorn ovum objects
+ * @returns {App.Entity.Fetus[]} remaining ova
  */
 globalThis.sendNewbornsToFacility = function(mom, babiesBeingBorn, sendAll) {
 	const remainingBabies = [];
 	for (const ovum of babiesBeingBorn) {
 		if ((ovum.reserve === "incubator" || sendAll) && V.incubator.tanks.length < V.incubator.capacity) {
-			App.Facilities.Incubator.newChild(generateChild(mom, ovum, true), "tankSetting" in ovum ? ovum.tankSetting : null);
+			if (ovum.noticeData.child !== undefined) {
+				App.Facilities.Incubator.newChild(ovum.noticeData.child, "tankSetting" in ovum ? ovum.tankSetting : null);
+			} else {
+				App.Facilities.Incubator.newChild(generateChild(mom, ovum, true), "tankSetting" in ovum ? ovum.tankSetting : null);
+			}
 		} else if ((ovum.reserve === "nursery" || sendAll) && V.cribs.length < V.nurseryCribs) {
+			// TODO:@franklygeorge handling for ovum.noticeData.child. Long term we probably just want to convert InfantState into an extension of SlaveState, or maybe just convert it to SlaveState
 			App.Facilities.Nursery.newChild(generateChild(mom, ovum));
 		} else {
 			remainingBabies.push(ovum);
diff --git a/src/js/pronouns.js b/src/js/pronouns.js
index 504e64c0780d68e2cabcf35c317688b257669800..ca219eb8b5a67e3facc78ce314c831f9f5027f37 100644
--- a/src/js/pronouns.js
+++ b/src/js/pronouns.js
@@ -136,7 +136,7 @@ globalThis.getNonlocalPronouns = function(dickRatio) {
 };
 
 /** Get a property for a given slave, with the correct pronouns.
- * @param {App.Entity.SlaveState} slave
+ * @param {FC.HumanState} slave
  * @param {string} prop
  * @returns {string}
  */
diff --git a/src/js/reminder.js b/src/js/reminder.js
index 667ff39ddb02a511f80dbb67076bf84b63fa60ed..b79f77b8658a001d3325108541e476c3bbb5b5f4 100644
--- a/src/js/reminder.js
+++ b/src/js/reminder.js
@@ -219,9 +219,11 @@ App.Reminders = (function() {
 	 * @returns {HTMLElement}
 	 */
 	function slaveLink(slaveID) {
-		return App.UI.DOM.link(String.fromCharCode(0x23f0), () => {
+		const link = App.UI.DOM.link(String.fromCharCode(0x23f0), () => {
 			dialog(slaveID);
 		});
+		link.style.textDecoration = "none";
+		return link;
 	}
 
 	/**
diff --git a/src/js/rulesAssistant.js b/src/js/rulesAssistant.js
index 99b90cde54d071330861c3cdf0307fbf5aaab602..30a93a68ced1ae54ffdf2950855a80bfe34fec80 100644
--- a/src/js/rulesAssistant.js
+++ b/src/js/rulesAssistant.js
@@ -301,6 +301,8 @@ App.RA.newRule = function() {
 			labelTagsClear: null,
 			pronoun: null,
 			posePrompt: null,
+			expressionPositivePrompt: null,
+			expressionNegativePrompt: null,
 			positivePrompt: null,
 			negativePrompt: null,
 			overridePrompts: null,
diff --git a/src/js/rulesAssistantOptions.js b/src/js/rulesAssistantOptions.js
index 80ea5f0ac6823941c3c322ff4b90dccc31aa0900..9f0f3b993b7aadd56a3d547df8714a319bf20c74 100644
--- a/src/js/rulesAssistantOptions.js
+++ b/src/js/rulesAssistantOptions.js
@@ -1560,6 +1560,8 @@ App.RA.options = (function() {
 			}
 			if (V.imageChoice === 6) { // If using AI generated images
 				this.appendChild(new OverridePosePrompt());
+				this.appendChild(new OverrideExpressionPositivePrompt());
+				this.appendChild(new OverrideExpressionNegativePrompt());
 				this.appendChild(new OverridePromptSwitch());
 				this.appendChild(new AddCustomPosPrompt());
 				this.appendChild(new AddCustomNegPrompt());
@@ -1681,6 +1683,7 @@ App.RA.options = (function() {
 			super();
 			this.appendChild(new VoiceSurgeryList());
 			this.appendChild(new VisionSurgeryList());
+			this.appendChild(new HeelsSurgeryList());
 			this.appendChild(new HearingSurgeryList());
 			this.appendChild(new SmellSurgeryList());
 			this.appendChild(new TasteSurgeryList());
@@ -3856,6 +3859,18 @@ App.RA.options = (function() {
 		}
 	}
 
+	class HeelsSurgeryList extends RadioSelector {
+		constructor() {
+			const items = [
+				["fixed", 1],
+				["clipped", -1],
+			];
+			super("Heels correction", items, true);
+			this.setValue(current_rule.set.surgery.heels);
+			this.onchange = (value) => current_rule.set.surgery.heels = value;
+		}
+	}
+
 	class HearingSurgeryList extends RadioSelector {
 		constructor() {
 			const items = [
@@ -4223,6 +4238,22 @@ App.RA.options = (function() {
 		}
 	}
 
+	class OverrideExpressionPositivePrompt extends StringEditor {
+		constructor() {
+			super("Override positive expression prompt", [], true, false);
+			this.setValue(current_rule.set.expressionPositivePrompt);
+			this.onchange = (value) => current_rule.set.expressionPositivePrompt = value;
+		}
+	}
+
+	class OverrideExpressionNegativePrompt extends StringEditor {
+		constructor() {
+			super("Override negative expression prompt", [], true, false);
+			this.setValue(current_rule.set.expressionNegativePrompt);
+			this.onchange = (value) => current_rule.set.expressionNegativePrompt = value;
+		}
+	}
+
 	class AddCustomPosPrompt extends StringEditor {
 		constructor() {
 			super("Add custom positive prompt(s) to slave", [], true, false);
diff --git a/src/js/rulesAutosurgery.js b/src/js/rulesAutosurgery.js
index 5f2cebcbbc8e807df9d4e360bb0a143d2733cf8c..e8c24de1c650b75d76d90813b6d3239401548d0a 100644
--- a/src/js/rulesAutosurgery.js
+++ b/src/js/rulesAutosurgery.js
@@ -81,13 +81,22 @@ globalThis.rulesAutosurgery = (function() {
 		/**
 		 * Performs an individual surgery procedure
 		 * @param {string} desc
-		 * @param {slaveOperation} proc
+		 * @param {slaveOperation | App.Medicine.Surgery.Procedure} proc
 		 * @param {number} [healthCost=10] normal health cost
 		 */
 		function commitProcedure(desc, proc, healthCost = 10) {
 			if (slave.health.health >= -20) {
 				surgeries.push(desc);
-				proc(slave);
+				if ( (typeof proc) === "function" ) {
+					// @ts-ignore
+					proc(slave);
+				} else {
+					// @ts-ignore
+					const result = App.Medicine.Surgery.apply(proc, V.cheatMode);
+					const [diff, ] = result;
+					App.Utils.Diff.applyDiff(slave, diff);
+				}
+
 				cashX(forceNeg(V.surgeryCost), "slaveSurgery", slave);
 				surgeryDamage(slave, healthCost);
 			}
@@ -101,7 +110,7 @@ globalThis.rulesAutosurgery = (function() {
 		function geneModProcedure(desc, procedure) {
 			if (slave.health.health >= 0) {
 				surgeries.push(desc);
-				const [diff, reaction] = App.Medicine.Surgery.apply(procedure, V.cheatMode === 1);
+				const [diff, ] = App.Medicine.Surgery.apply(procedure, V.cheatMode === 1);
 				App.Utils.Diff.applyDiff(slave, diff);
 			}
 		}
@@ -251,18 +260,22 @@ globalThis.rulesAutosurgery = (function() {
 			if (getRightEyeVision(slave) === 2) {
 				commitProcedure(`surgery to blur ${his} right vision`, s => { eyeSurgery(s, "right", "blur"); }, 5);
 			}
+		} else if (slave.heels === 1 && thisSurgery.heels === 1) {
+			commitProcedure(`surgery to repair ${his} tendons`, new App.Medicine.Surgery.Procedures.ReplaceTendons(slave));
+		} else if (slave.heels === 0 && thisSurgery.heels === -1) {
+			commitProcedure(`surgery to shorten ${his} tendons`, new App.Medicine.Surgery.Procedures.ShortenTendons(slave));
 		} else if (slave.hears === -1 && thisSurgery.hears === 0) {
-			commitProcedure(`surgery to correct ${his} hearing`, s => { s.hears = 0; });
+			commitProcedure(`surgery to correct ${his} hearing`, new App.Medicine.Surgery.Procedures.EarFix(slave));
 		} else if (slave.hears === 0 && thisSurgery.hears === -1) {
-			commitProcedure(`surgery to muffle ${his} hearing`, s => { s.hears = -1; });
+			commitProcedure(`surgery to muffle ${his} hearing`, new App.Medicine.Surgery.Procedures.EarMuffle(slave));
 		} else if (slave.smells === -1 && thisSurgery.smells === 0) {
-			commitProcedure(`surgery to correct ${his} sense of smell`, s => { s.smells = 0; });
+			commitProcedure(`surgery to correct ${his} sense of smell`, new App.Medicine.Surgery.Procedures.Resmell(slave));
 		} else if (slave.smells === 0 && thisSurgery.smells === -1) {
-			commitProcedure(`surgery to muffle ${his} sense of smell`, s => { s.smells = -1; });
+			commitProcedure(`surgery to muffle ${his} sense of smell`, new App.Medicine.Surgery.Procedures.Desmell(slave));
 		} else if (slave.tastes === -1 && thisSurgery.tastes === 0) {
-			commitProcedure(`surgery to correct ${his} sense of taste`, s => { s.tastes = 0; });
+			commitProcedure(`surgery to correct ${his} sense of taste`, new App.Medicine.Surgery.Procedures.Retaste(slave));
 		} else if (slave.tastes === 0 && thisSurgery.tastes === -1) {
-			commitProcedure(`surgery to muffle ${his} sense of taste`, s => { s.tastes = -1; });
+			commitProcedure(`surgery to muffle ${his} sense of taste`, new App.Medicine.Surgery.Procedures.Detaste(slave));
 		} else if (_.isNumber(thisSurgery.voice) && slave.voice !== thisSurgery.voice) {
 			const voiceDifference = thisSurgery.voice - slave.voice;
 			commitProcedure(`surgery to ${(voiceDifference < 0) ? "lower" : "raise"} ${his} voice`, s => {
@@ -301,26 +314,11 @@ globalThis.rulesAutosurgery = (function() {
 		}
 
 		if (slave.anus > 3 && thisSurgery.cosmetic > 0) {
-			commitProcedure("a restored anus", () => {
-				slave.anus = 3;
-				if (slave.skill.anal > 10) {
-					slave.skill.anal -= 10;
-				}
-			});
+			commitProcedure("a restored anus", new App.Medicine.Surgery.Procedures.RepairAnus(slave));
 		} else if (slave.vagina > 3 && thisSurgery.cosmetic > 0) {
-			commitProcedure("a restored pussy", () => {
-				slave.vagina = 3;
-				if (slave.skill.vaginal > 10) {
-					slave.skill.vaginal -= 10;
-				}
-			});
+			commitProcedure("a restored pussy", new App.Medicine.Surgery.Procedures.RepairVagina(slave));
 		} else if (slave.anus > 0 && V.surgeryUpgrade === 1 && thisSurgery.holes === 2) {
-			commitProcedure("a virgin anus", () => {
-				slave.anus = 0;
-				if (slave.skill.anal > 10) {
-					slave.skill.anal -= 10;
-				}
-			});
+			commitProcedure("a virgin anus", new App.Medicine.Surgery.Procedures.RestoreAnalVirginity(slave));
 		} else if (slave.vagina > 0 && V.surgeryUpgrade === 1 && thisSurgery.holes === 2) {
 			commitProcedure("a virgin pussy", () => {
 				slave.vagina = 0;
@@ -347,13 +345,13 @@ globalThis.rulesAutosurgery = (function() {
 		}
 
 		if (slave.prostate === 2 && thisSurgery.prostate === 1) {
-			commitProcedure(`surgery to remove ${his} prostate implant`, s => { s.prostate = 1; });
+			commitProcedure(`surgery to remove ${his} prostate implant`, new App.Medicine.Surgery.Procedures.RemoveProstate(slave));
 		} else if (slave.prostate === 1 && thisSurgery.prostate === 2) {
-			commitProcedure("a precum production enhancing drug implant", s => { s.prostate = 2; });
+			commitProcedure("a precum production enhancing drug implant", new App.Medicine.Surgery.Procedures.Precum(slave));
 		} else if (slave.balls > 0 && slave.vasectomy === 0 && thisSurgery.vasectomy === true) {
 			commitProcedure("vasectomy", s => { s.vasectomy = 1; });
 		} else if (slave.balls > 0 && slave.vasectomy === 1 && thisSurgery.vasectomy === false) {
-			commitProcedure("undo vasectomy", s => { s.vasectomy = 0; });
+			commitProcedure("undo vasectomy", new App.Medicine.Surgery.Procedures.VasectomyUndo(slave));
 		}
 
 		// Since currently there's no way of changing the autosurgery cost, when replacing, the cost is 300 instead of 600
@@ -405,17 +403,11 @@ globalThis.rulesAutosurgery = (function() {
 				slave.bald = 1;
 			}, 0);
 		} else if (slave.weight >= 60 && thisSurgery.cosmetic > 0) {
-			commitProcedure("liposuction", s => { s.weight = 10; });
+			commitProcedure("liposuction", new App.Medicine.Surgery.Procedures.Liposuction(slave));
 		} else if ((slave.bellySagPreg > 0 || slave.bellySag > 0) && (thisSurgery.cosmetic > 0 || thisSurgery.tummy > 0 )) {
-			commitProcedure("a tummy tuck", () => {
-				slave.bellySag = 0;
-				slave.bellySagPreg = 0;
-			}, 20);
+			commitProcedure("a tummy tuck", new App.Medicine.Surgery.Procedures.TummyTuck(slave), 20);
 		} else if (slave.voice === 1 && slave.voiceImplant === 0 && thisSurgery.cosmetic > 0) {
-			commitProcedure("a feminine voice", () => {
-				slave.voice += 1;
-				slave.voiceImplant += 1;
-			});
+			commitProcedure("a feminine voice", new App.Medicine.Surgery.Procedures.VoiceRaise(slave));
 		} else if (slave.scar.hasOwnProperty("belly") && slave.scar.belly["c-section"] > 0 && thisSurgery.cosmetic > 0) {
 			commitProcedure("surgery to remove a c-section scar", s => { App.Medicine.Modification.removeScar(s, "belly", "c-section"); });
 		} else if (slave.faceImplant <= 45 && slave.face <= 95 && thisSurgery.cosmetic === 2) {
@@ -430,21 +422,15 @@ globalThis.rulesAutosurgery = (function() {
 				slave.faceImplant += 25 - 5 * Math.trunc(V.PC.skill.medicine / 50) - 5 * V.surgeryUpgrade;
 			});
 		} else if (slave.voice < 3 && slave.voiceImplant === 0 && thisSurgery.cosmetic === 2) {
-			commitProcedure("a bimbo's voice", () => {
-				slave.voice += 1;
-				slave.voiceImplant += 1;
-			});
+			commitProcedure("a bimbo's voice", new App.Medicine.Surgery.Procedures.VoiceRaise(slave));
 		}
 
 		if (slave.waist >= -10 && thisSurgery.cosmetic > 0) {
-			commitProcedure("a narrower waist", s => { s.waist -= 20; });
+			commitProcedure("a narrower waist", new App.Medicine.Surgery.Procedures.WaistReduction(slave));
 		} else if (slave.waist >= -95 && V.seeExtreme === 1 && thisSurgery.cosmetic === 2) {
 			commitProcedure("a narrower waist", s => { s.waist = Math.clamp(s.waist - 20, -100, 100); });
 		} else if (thisSurgery.hips !== null && slave.hips < 3 && V.surgeryUpgrade === 1 && (slave.hips < thisSurgery.hips)) {
-			commitProcedure("wider hips", () => {
-				slave.hips++;
-				slave.hipsImplant++;
-			});
+			commitProcedure("wider hips", new App.Medicine.Surgery.Procedures.BroadenPelvis(slave));
 		}
 
 		if (slave.bellyImplant < 0 && V.bellyImplants > 0 && thisSurgery.bellyImplant === "install" && slave.womb.length === 0 && slave.broodmother === 0) {
@@ -466,30 +452,27 @@ globalThis.rulesAutosurgery = (function() {
 			});
 		}
 
-		if (slave.horn !== "none" && thisSurgery.horn === 1) {
-			commitProcedure(`surgery to remove ${his} implanted horns`, s => { s.horn = "none"; });
-		} else if (slave.horn !== "curved succubus horns" && thisSurgery.horn === 2) {
-			commitProcedure(`surgery to implant ${him} with curved succubus horns`, s => { s.horn = "curved succubus horns"; s.hornColor = "white"; });
-		} else if (slave.horn !== "backswept horns" && thisSurgery.horn === 3) {
-			commitProcedure(`surgery to implant ${him} with backswept horns`, s => { s.horn = "backswept horns"; s.hornColor = "white"; });
-		} else if (slave.horn !== "cow horns" && thisSurgery.horn === 4) {
-			commitProcedure(`surgery to implant ${him} with cow horns`, s => { s.horn = "cow horns"; s.hornColor = "white"; });
-		} else if (slave.horn !== "one long oni horn" && thisSurgery.horn === 5) {
-			commitProcedure(`surgery to implant ${him} with one long oni horn`, s => { s.horn = "one long oni horn"; s.hornColor = "white"; });
-		} else if (slave.horn !== "two long oni horns" && thisSurgery.horn === 6) {
-			commitProcedure(`surgery to implant ${him} with two long oni horns`, s => { s.horn = "two long oni horns"; s.hornColor = "white"; });
-		} else if (slave.horn !== "small horns" && thisSurgery.horn === 7) {
-			commitProcedure(`surgery to implant ${him} with small horns`, s => { s.horn = "small horns"; s.hornColor = "white"; });
+		/**
+		 * @type {FC.HornType[]}
+		 */
+		const hornTypes = [ "none", "curved succubus horns", "backswept horns", "cow horns", "one long oni horn", "two long oni horns", "small horns" ];
+		const hornType = hornTypes[thisSurgery.horn - 1];
+		if (hornType && slave.horn !== hornType) {
+			if (hornType === "none") {
+				commitProcedure(`surgery to remove ${his} implanted horns`, new App.Medicine.Surgery.Procedures.HornGone(slave));
+			} else {
+				commitProcedure(`surgery to implant ${him} with ${hornType}`, new App.Medicine.Surgery.Procedures.Horn(slave, hornType, hornType, "white"));
+			}
 		}
 
 		if (slave.earShape !== "normal" && thisSurgery.earShape === 1) {
-			commitProcedure(`surgery to restore ${his} modified ears`, s => { s.earShape = "normal"; });
+			commitProcedure(`surgery to restore ${his} modified ears`, new App.Medicine.Surgery.Procedures.EarRestore(slave));
 		} else if (slave.earShape !== "pointy" && thisSurgery.earShape === 2) {
-			commitProcedure(`surgery to modify ${his} ears into a pair of small pointy ears`, s => { s.earShape = "pointy"; });
+			commitProcedure(`surgery to modify ${his} ears into a pair of small pointy ears`, new App.Medicine.Surgery.Procedures.EarMinorReshape(slave, "pointy", "pointy"));
 		} else if (slave.earShape !== "elven" && thisSurgery.earShape === 3) {
-			commitProcedure(`surgery to modify ${his} ears into a pair of elven ears`, s => { s.earShape = "elven"; });
+			commitProcedure(`surgery to modify ${his} ears into a pair of elven ears`, new App.Medicine.Surgery.Procedures.EarMajorReshape(slave, "elven", "elven"));
 		} else if (slave.earShape !== "cow" && thisSurgery.earShape === 4) {
-			commitProcedure(`surgery to modify ${his} ears into a pair of bovine-like ears`, s => { s.earShape = "cow"; });
+			commitProcedure(`surgery to modify ${his} ears into a pair of bovine-like ears`, new App.Medicine.Surgery.Procedures.EarMajorReshape(slave, "cow", "cow"));
 		}
 	}
 
diff --git a/src/js/sexActsJS.js b/src/js/sexActsJS.js
index 363cc80c2086f8fc7f2d7243fa3c3a704aff3de9..14d45b345f9889be21251edb46d0d87b914efb6b 100644
--- a/src/js/sexActsJS.js
+++ b/src/js/sexActsJS.js
@@ -358,8 +358,11 @@ globalThis.SimpleSexAct = (function() {
 					seX(slave, "mammary", V.PC, "penetrative");
 				} else if (canDoVaginal(slave) && (slave.vagina > 0 || (slave.vagina >= 0 && playerSex === "vaginal")) && fuckTarget > 33) {
 					seX(slave, "vaginal", V.PC, playerSex);
-					if (playerSex === "penetrative" || playerSex === "vaginal") {
+					if (playerSex === "penetrative") {
+						tryKnockMeUp(slave, 10, 0, V.PC);
+					} else if (playerSex === "vaginal") {
 						tryKnockMeUp(slave, 10, 0, V.PC);
+						tryKnockMeUp(V.PC, 10, 0, slave);
 					}
 				} else if (canDoAnal(slave) && slave.anus > 0 && fuckTarget > 10) {
 					seX(slave, "anal", V.PC, "penetrative");
diff --git a/src/js/utilsArcology.js b/src/js/utilsArcology.js
index 422b493e6202fc982d683fc8b9964d845fa9de5c..0fcb67ead098cde7572d0360887de6e06db2a76a 100644
--- a/src/js/utilsArcology.js
+++ b/src/js/utilsArcology.js
@@ -19,9 +19,9 @@ globalThis.getRevivalistNationality = function() {
  */
 App.Utils.economicUncertainty = function(arcologyID) {
 	let uncertainty = arcologyID === 0 ? 5 : 10;
-	if (assistant.power === 1) {
+	if (V.assistant.power === 1) {
 		uncertainty -= Math.max(Math.trunc(uncertainty / 2), 0);
-	} else if (assistant.power > 1) {
+	} else if (V.assistant.power > 1) {
 		uncertainty = 0;
 	}
 	return jsRandom(100 - uncertainty, 100 + uncertainty) / 100;
diff --git a/src/js/utilsDOM.js b/src/js/utilsDOM.js
index e7a2bc319ecc3a6eed18bf73ed479745caeb5fbe..3c7f64d2be2ba39111496ba0035d901911bc5d5d 100644
--- a/src/js/utilsDOM.js
+++ b/src/js/utilsDOM.js
@@ -379,15 +379,15 @@ App.UI.DOM.generateLinksStrip = function(links) {
 /**
  * @param {Node|string} head
  * @param {HTMLDivElement} [content]
- * @param {boolean} [hidden]
+ * @param {boolean} [collapsed]
  * @returns {DocumentFragment}
  */
-App.UI.DOM.accordion = function(head, content, hidden = true) {
+App.UI.DOM.accordion = function(head, content, collapsed = true) {
 	const fragment = document.createDocumentFragment();
 	const button = App.UI.DOM.appendNewElement("button", fragment, head, ["accordion"]);
 
 	if (content) {
-		App.UI.DOM.elementToggle(button, [content], hidden);
+		App.UI.DOM.elementToggle(button, [content], collapsed);
 		fragment.append(content);
 	} else {
 		button.classList.add("empty");
diff --git a/src/js/utilsMisc.js b/src/js/utilsMisc.js
index 668b3b745e936fd4cc3d02fd3ff6d3147584b221..88ece588a91ba23c850a91db18a1837eb00f5299 100644
--- a/src/js/utilsMisc.js
+++ b/src/js/utilsMisc.js
@@ -394,3 +394,38 @@ globalThis.weightedRandom = function(values) {
 	// Array was empty or all weights were 0
 	return null;
 };
+
+/**
+ * @typedef {object} geneToGenderOptions
+ * @property {boolean} keepKaryotype if true then we will keep the karyotype. `XX` = `Female (XX)
+ * @property {boolean} lowercase if true then we will make the output lower case. the karyotype is exempt from this.
+ */
+
+/**
+ * Takes a karyotype (XX, XY, X, etc) and converts it to a gender (Female, Male, Turner Syndrome Female, etc)
+ * @param {FC.GenderGenes} karyotype the karyotype to convert
+ * @param {geneToGenderOptions} [options] {keepKaryotype: false, lowercase: true}
+ * @returns {string} the gender that matches the karyotype
+ */
+globalThis.geneToGender = (karyotype, options = {
+	keepKaryotype: false,
+	lowercase: true,
+}) => {
+	/** @type {string} */
+	let gender = {
+		XX: 'Female',
+		XY: 'Male',
+		X: 'Turner Syndrome Female',
+		X0: 'Turner Syndrome Female',
+		XYY: 'XYY Syndrome Male',
+		XXY: 'Klinefelter Syndrome Male',
+		XXX: 'triple X Syndrome Female'
+	}[String(karyotype).toUpperCase()] || `Unknown Gender: ${String(karyotype)}`;
+	if (options.lowercase === true) {
+		gender = gender.toLowerCase();
+	}
+	if (options.keepKaryotype === true && !gender.toLowerCase().startsWith("unknown gender")) {
+		gender = `${gender} (${String(karyotype).toUpperCase()})`;
+	}
+	return gender;
+};
diff --git a/src/js/utilsSC.js b/src/js/utilsSC.js
index e3c510a499d8e06d32c25d4168868680418e23eb..b28f9bf963dfbf6a8f12312e90944a7ce8917956 100644
--- a/src/js/utilsSC.js
+++ b/src/js/utilsSC.js
@@ -101,21 +101,48 @@ App.UI.replace = function(selector, newContent) {
 	target.append(ins);
 };
 
+/**
+ * @typedef {object} App.UI.DOM.slaveDescriptionDialogOptions
+ * @property {boolean} [noButtons] if true then we won't add the favorite toggle or the reminder button
+ * @property {string[]} [linkClasses] a list of classes to add to the link element
+ */
+
 /**
  * Generates a link which shows a slave description dialog for a specified slave.
  * Do not call from within another dialog.
  * @param {App.Entity.SlaveState} slave
  * @param {string} [text] link text to use instead of slave name
- * @param {FC.Desc.LongSlaveOptions} options
+ * @param {FC.Desc.LongSlaveOptions} [longSlaveOptions]
+ * @param {App.UI.DOM.slaveDescriptionDialogOptions} [options]
  * @returns {HTMLElement} link
  */
-App.UI.DOM.slaveDescriptionDialog = function(slave, text, options = {descType: DescType.EVENT, noArt: true}) {
-	return App.UI.DOM.link(text ? text : SlaveFullName(slave), () => {
+App.UI.DOM.slaveDescriptionDialog = function(
+	slave, text,
+	longSlaveOptions = {descType: DescType.EVENT, noArt: true},
+	options = {
+		noButtons: false,
+		linkClasses: [],
+	}
+) {
+	const span = App.UI.DOM.makeElement("span");
+	if ((!text || text === SlaveFullName(slave)) && !options.noButtons && V.addButtonsToSlaveLinks) {
+		// text = [favorite button] [reminder button] [slave name]
+		span.append(
+			App.UI.favoriteToggle(slave),
+			App.Reminders.slaveLink(slave.ID),
+		);
+	}
+	const link = App.UI.DOM.link(text ? text: SlaveFullName(slave), () => {
 		Dialog.setup(SlaveFullName(slave));
 		App.UI.DOM.drawOneSlaveRight(Dialog.body(), slave);
-		Dialog.append(App.Desc.longSlave(slave, options));
+		Dialog.append(App.Desc.longSlave(slave, longSlaveOptions));
 		Dialog.open();
 	});
+	if (options.linkClasses && Array.isArray(options.linkClasses)) {
+		link.classList.add(...options.linkClasses);
+	}
+	span.append(link);
+	return span;
 };
 
 /**
diff --git a/src/js/wombJS.js b/src/js/wombJS.js
index 0d923bfd934b8f666d919ac74d052aea67b7d57b..67ccfa2c8f2f18dd320f925eb64560c0a13a7d2d 100644
--- a/src/js/wombJS.js
+++ b/src/js/wombJS.js
@@ -121,9 +121,8 @@ App.Entity.Fetus = class {
 	 * @param {number} age - initial age, after conception, in weeks
 	 * @param {number} fatherID
 	 * @param {FC.HumanState} mother
-	 * @param {string} name - name of ovum (generally ovumNN where NN is the number in the batch)
 	 */
-	constructor(age, fatherID, mother, name) {
+	constructor(age, fatherID, mother) {
 		/** Unique identifier for this fetus */
 		this.ID = generateNewID();
 		/** Week since conception */
@@ -132,10 +131,24 @@ App.Entity.Fetus = class {
 		this.realAge = 1;
 		this.fatherID = fatherID;
 		this.volume = 1;
+		/** @type {(""|"incubator"|"nursery")} */
 		this.reserve = "";
+		// used by App.Events.PregnancyNotice.Event
+		this.noticeData = {
+			/** @type {("undecided"|"nothing"|"terminate"|"transplant"|"incubator"|"nursery"|"wait")} */
+			fate: "undecided",
+			/** @type {number} The id of the human that will receive the fetus during transplanting or 0 */
+			transplantReceptrix: 0,
+			/** If true then the fetus' cheat menu accordion is collapsed */
+			cheatAccordionCollapsed: true,
+			/** @type {App.Entity.SlaveState} This is used by the descriptors and is used by generateChild() when the slave is born */
+			child: undefined,
+		};
 		/** All identical multiples share the same twinID */
 		this.twinID = "";
 		this.motherID = mother.ID;
+		const childNumber = ordinalSuffix(mother.counter.birthsTotal + mother.womb.length + 1);
+		const name = `${SlaveFullName(mother)}'s ${childNumber} child`;
 		this.genetics = generateGenetics(mother, fatherID, name);
 	}
 };
@@ -149,7 +162,7 @@ App.Entity.Fetus = class {
  */
 globalThis.WombImpregnate = function(actor, fCount, fatherID, age, surrogate) {
 	for (let i = 0; i < fCount; i++) {
-		const tf = new App.Entity.Fetus(age, fatherID, surrogate || actor, `ovum${i}`);
+		const tf = new App.Entity.Fetus(age, fatherID, surrogate || actor);
 		try {
 			if (actor.womb.length === 0) {
 				actor.pregWeek = age;
@@ -188,7 +201,7 @@ globalThis.WombImpregnateClone = function(actor, fCount, mother, age) {
 	const motherOriginal = V.genePool.find(s => s.ID === mother.ID) || mother;
 
 	for (let i = 0; i < fCount; i++) {
-		const tf = new App.Entity.Fetus(age, mother.ID, mother, `ovum${i}`);
+		const tf = new App.Entity.Fetus(age, mother.ID, mother);
 
 		// gene corrections
 		tf.fatherID = -7;
@@ -227,7 +240,13 @@ globalThis.WombImpregnateClone = function(actor, fCount, mother, age) {
 	WombUpdatePregVars(actor);
 };
 
-// Should be used to set biological age for fetus (ageToAdd), AND chronological (realAgeToAdd). Speed up or slow down gestation drugs should affect ONLY biological.
+/**
+ * Should be used to set biological age for fetus (ageToAdd), AND chronological (realAgeToAdd).
+ * Speed up or slow down gestation drugs should affect ONLY biological.
+ * @param {FC.HumanState} actor
+ * @param {number} ageToAdd
+ * @param {number} realAgeToAdd
+ */
 globalThis.WombProgress = function(actor, ageToAdd, realAgeToAdd = ageToAdd) {
 	ageToAdd = Math.ceil(ageToAdd * 10) / 10;
 	realAgeToAdd = Math.ceil(realAgeToAdd * 10) / 10;
@@ -602,8 +621,9 @@ globalThis.WombSort = function(actor) {
 globalThis.fetalSplit = function(actor, chance) {
 	for (const s of actor.womb) {
 		if (jsRandom(1, chance) >= chance || (actor.geneticQuirks.twinning === 2 && (actor.womb.length < Math.floor(actor.pregAdaptation / 32) || actor.womb.length === 1) && (s.twinID === "" || s.twinID === undefined))) {
+			let twinsAlreadyExist = (s.twinID !== undefined && s.twinID !== "");
 			// if this fetus is not already an identical, generate a new twin ID before cloning it
-			if (s.twinID === "" || s.twinID === undefined) {
+			if (!twinsAlreadyExist) {
 				s.twinID = generateNewID();
 			}
 
@@ -612,10 +632,51 @@ globalThis.fetalSplit = function(actor, chance) {
 			nft.ID = generateNewID();
 			nft.reserve = ""; // new fetus does not inherit reserve status
 
+			// add cloned fetus to the womb
 			actor.womb.push(nft);
+
+			// rename twinned fetuses to be `${fetus.name} (twin ${letter})`
+			if (twinsAlreadyExist) {
+				let count = actor.womb.filter((fetus) => (fetus.twinID === s.twinID)).length;
+				// check if original twin has lettering
+				if (s.genetics.name.match(/ \(twin [A-Z]\)$/) === null) {
+					// if they don't then add it
+					s.genetics.name = `${s.genetics.name} (twin ${getLetterFromNumber(count -1, false)})`;
+				}
+				// rename new twin
+				nft.genetics.name = `${nft.genetics.name.replace(/ \(twin [A-Z]\)$/, "")} (twin ${getLetterFromNumber(count, false)})`;
+			} else {
+				actor.womb
+					.filter((fetus) => (fetus.twinID === s.twinID))
+					.forEach((fetus, index) => {
+						fetus.genetics.name = `${fetus.genetics.name} (twin ${getLetterFromNumber(index)})`;
+					});
+			}
 		}
 	}
 	WombNormalizePreg(actor);
+
+	/**
+	 * Returns the letter of the alphabet that matches the number
+	 * with startingAtZero = false: 1 = A, 26 = Z, etc
+	 * with startingAtZero = true: 0 = A, 25 = Z, etc
+	 * // Going over the amount of letters in the alphabet will throw an error
+	 * @param {number} number
+	 * @param {boolean} startingAtZero
+	 * @returns {string}
+	 */
+	function getLetterFromNumber(number, startingAtZero = true) {
+		// TODO:@franklygeorge move this to somewhere in the global space
+		if (startingAtZero === false) {
+			number -= 1;
+		}
+		if (number >= 26) {
+			throw new Error(`Number cannot be greater than ${startingAtZero ? "25": "26"}`);
+		} else if (number < 0) {
+			throw new Error(`Number cannot be less than ${startingAtZero ? "0": "1"}`);
+		}
+		return (number + 10).toString(36).toUpperCase();
+	}
 };
 
 // safe alternative to .womb.length.
@@ -666,6 +727,69 @@ globalThis.WombChangeGene = function(actor, geneName, newValue) {
 	actor.womb.forEach(ft => ft.genetics[geneName] = newValue);
 };
 
+/**
+ * Returns a list of twins that this fetus has or null if the fetus has no twins
+ * @param {App.Entity.Fetus} fetus
+ * @returns {App.Entity.Fetus[]|null}
+ */
+globalThis.getFetusTwins = function(fetus) {
+	if (fetus.twinID && fetus.twinID !== "") {
+		return getSlave(fetus.motherID).womb.filter((tFetus) => (tFetus.twinID === fetus.twinID && tFetus !== fetus));
+	}
+	return null;
+};
+
+/**
+ * @param {FC.HumanState} mother the mother of the fetus
+ * @param {App.Entity.Fetus} fetus the fetus to check
+ * @returns {boolean} true if the fetus can be terminated, false otherwise
+ */
+globalThis.canTerminateFetus = (mother, fetus) => {
+	if (mother.womb.indexOf(fetus) === -1) { throw new Error("mother.womb must contain fetus"); }
+	return (
+		(
+			fetus.age <= 4 &&
+			(
+				!FutureSocieties.isActive('FSRestart') ||
+				V.eugenicsFullControl === 1 ||
+				mother.breedingMark === 0 ||
+				V.propOutcome === 0 ||
+				fetus.fatherID !== -6
+			)
+		) ||
+		V.cheatMode === 1
+	);
+};
+
+/**
+ * @param {FC.HumanState} mother the mother of the fetus
+ * @param {App.Entity.Fetus} fetus the fetus to check
+ * @returns {-1|0|1} -1 = has already been transpanted, 0 = cannot be transpanted, 1 = can be transplanted
+ */
+globalThis.canTransplantFetus = (mother, fetus) => {
+	if (mother.womb.indexOf(fetus) === -1) { throw new Error("mother.womb must contain fetus"); }
+
+	if (fetus.motherID !== mother.ID) {
+		return -1;
+	} else if (
+		(
+			fetus.age <= 6 &&
+			V.surgeryUpgrade > 0 &&
+			(
+				!FutureSocieties.isActive('FSRestart') ||
+				V.eugenicsFullControl === 1 ||
+				mother.breedingMark === 0 ||
+				V.propOutcome === 0 ||
+				fetus.fatherID !== -6
+			)
+		) ||
+		V.cheatMode === 1
+	) {
+		return 1;
+	}
+	return 0;
+};
+
 // change genetic property of all fetuses based on race
 globalThis.WombFatherRace = function(actor, raceName) {
 	let skinColor = randomRaceSkin(raceName);
@@ -707,6 +831,11 @@ globalThis.WombCleanYYFetuses = function(actor) {
 	return reserved;
 };
 
+/**
+ * Returns the amount of fetuses currently destined (reserved) for the given location
+ * @param {"incubator"|"nursery"} reserveType
+ * @returns {number}
+ */
 globalThis.FetusGlobalReserveCount = function(reserveType) {
 	let cnt = 0;
 
diff --git a/src/markets/specificMarkets/customSlaveMarket.js b/src/markets/specificMarkets/customSlaveMarket.js
index d9059f30937457877c18b5fbe6cf518636a0d042..1b8c0df35f590fef69f807ba653a77d892639910 100644
--- a/src/markets/specificMarkets/customSlaveMarket.js
+++ b/src/markets/specificMarkets/customSlaveMarket.js
@@ -12,6 +12,8 @@ App.Markets["Custom Slave"] = function() {
 	el.append(face());
 	el.append(race());
 	el.append(skin());
+	el.append(hairColor());
+	el.append(eyesColor());
 	el.append(boobs());
 	el.append(butt());
 	el.append(sex());
@@ -353,6 +355,76 @@ App.Markets["Custom Slave"] = function() {
 	}
 
 
+	function hairColor() {
+		const el = document.createElement("div");
+		const slaveProperty = "hairColor";
+		const choices = [{key: "hair color is unimportant", name: "Hair color is unimportant"}];
+		for (const hair of App.Medicine.Modification.Color.Primary) {
+			choices.push({key: hair.value, name: capFirstChar(hair.value)});
+		}
+
+		createDescription(el, description, slaveProperty);
+
+		// Choices
+		el.append(App.UI.DOM.makeSelect(choices, slave.hairColor, h => {
+			slave.hairColor	= h;
+			jQuery("#hairColor-text").empty().append(description());
+		}));
+
+		function description() {
+			const el = new DocumentFragment();
+			el.append("Natural hair color: ");
+			el.append(
+				App.UI.DOM.makeTextBox(
+					slave.hairColor,
+					(v) => {
+						slave.hairColor = v;
+						jQuery("#hairColor-text").empty().append(description());
+					}
+				)
+			);
+			return el;
+		}
+
+		return el;
+	}
+
+
+	function eyesColor() {
+		const el = document.createElement("div");
+		const slaveProperty = "eyesColor";
+		const choices = [{key: "eye color is unimportant", name: "Eye color is unimportant"}];
+		for (const eyes of App.Medicine.Modification.eyeColor) {
+			choices.push({key: eyes.value, name: capFirstChar(eyes.value)});
+		}
+
+		createDescription(el, description, slaveProperty);
+
+		// Choices
+		el.append(App.UI.DOM.makeSelect(choices, slave.eyesColor, e => {
+			slave.eyesColor = e;
+			jQuery("#eyesColor-text").empty().append(description());
+		}));
+
+		function description() {
+			const el = new DocumentFragment();
+			el.append("Natural eye color: ");
+			el.append(
+				App.UI.DOM.makeTextBox(
+					slave.eyesColor,
+					(v) => {
+						slave.eyesColor = v;
+						jQuery("#eyesColor-text").empty().append(description());
+					}
+				)
+			);
+			return el;
+		}
+
+		return el;
+	}
+
+
 	function boobs() {
 		const el = document.createElement("div");
 		const slaveProperty = "boobs";
diff --git a/src/npc/descriptions/armpitHair.js b/src/npc/descriptions/armpitHair.js
new file mode 100644
index 0000000000000000000000000000000000000000..4e3f28b243c3f17e09e690e62b81984e2f92568d
--- /dev/null
+++ b/src/npc/descriptions/armpitHair.js
@@ -0,0 +1,66 @@
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @returns {string}
+ */
+App.Desc.armpitHair = function(slave) {
+	const r = [];
+	const {
+		his, His, he, He
+	} = getPronouns(slave);
+	const pubertyAge = Math.min(slave.pubertyAgeXX, slave.pubertyAgeXY);
+	if (slave.race === "catgirl") {
+		if (slave.underArmHStyle === "bushy") {
+			r.push(`${His} underarm fur is especially bushy and untamed.`);
+		} else if (slave.underArmHStyle === "waxed" || slave.underArmHStyle === "hairless" || slave.underArmHStyle === "shaved" || slave.underArmHStyle === "bald") {
+			r.push(`${His} underarm fur is trim and smooth.`);
+		} else {
+			r.push(`${His} ${slave.skin} underarm fur is quite regular along ${his} fuzzy body.`);
+		}
+	} else if (slave.physicalAge < pubertyAge - 2) {
+		r.push(`${He} is too sexually immature to have armpit hair.`);
+	} else if (slave.underArmHStyle === "hairless") {
+		r.push(`${His} armpits are perfectly smooth and naturally hairless.`);
+	} else if (slave.underArmHStyle === "bald") {
+		r.push(`${His} armpits no longer grow hair, leaving them smooth and hairless.`);
+	} else if (slave.underArmHStyle === "waxed") {
+		if (slave.assignment === Job.DAIRY && V.dairyRestraintsSetting > 1) {
+			r.push(`${His} armpit hair has been removed to prevent chafing.`);
+		} else {
+			r.push(`${His} armpits are waxed and smooth.`);
+		}
+	} else if (slave.physicalAge < pubertyAge - 1) {
+		r.push(`${He} has a few ${slave.underArmHColor} wisps of armpit hair.`);
+	} else if (slave.physicalAge < pubertyAge) {
+		r.push(`${He} is on the verge of puberty and has a small patch of ${slave.underArmHColor} armpit hair.`);
+	} else if (slave.underArmHStyle === "shaved") {
+		r.push(`${His} armpits appear hairless, but closer inspection reveals light, ${slave.underArmHColor} stubble.`);
+	} else if (slave.underArmHStyle === "neat") {
+		r.push(`${His} armpit hair is neatly trimmed`);
+		if (!hasBothArms(slave)) {
+			r.push(`since`);
+			if (hasAnyArms(slave)) {
+				r.push(`at least half`);
+			} else {
+				r.push(`it`);
+			}
+			r.push(`is always in full view.`);
+		} else {
+			r.push(`to not be visible unless ${he} lifts ${his} arms.`);
+		}
+	} else if (slave.underArmHStyle === "bushy") {
+		r.push(`${His} ${slave.underArmHColor} armpit hair has been allowed to grow freely,`);
+		if (!hasAnyArms(slave)) {
+			r.push(`creating two bushy patches under where ${his} arms used to be.`);
+		} else {
+			r.push(`so it can be seen poking out from under ${his}`);
+			if (hasBothArms(slave)) {
+				r.push(`arms`);
+			} else {
+				r.push(`arm`);
+			}
+			r.push(`at all times.`);
+		}
+	}
+
+	return r.join(" ");
+};
diff --git a/src/npc/descriptions/butt/anus.js b/src/npc/descriptions/butt/anus.js
index 0442f75adc847be6d5c52ab1bb46302dbf048d9f..d39f1d20ff8aba3da758cc02370bf42c6e403909 100644
--- a/src/npc/descriptions/butt/anus.js
+++ b/src/npc/descriptions/butt/anus.js
@@ -1,9 +1,10 @@
 /**
  * @param {FC.GingeredSlave} slave
  * @param {DescType} [descType=DescType.NORMAL]
+ * @param {boolean} [skills=true] If true then we describe how skilled the slave is, otherwise we omit it.
  * @returns {string}
  */
-App.Desc.anus = function(slave, descType = DescType.NORMAL) {
+App.Desc.anus = function(slave, descType = DescType.NORMAL, skills = true) {
 	const r = [];
 	const {
 		he, his, He, His
@@ -105,33 +106,35 @@ App.Desc.anus = function(slave, descType = DescType.NORMAL) {
 
 	r.push(App.Desc.mods(slave, "anus"));
 
-	if (slave.fuckdoll > 0) {
-		r.push(`As a Fuckdoll,`);
-		if (slave.fuckdoll <= 45) {
-			r.push(`${he} is only fit to be locked in place so ${his} rear hole can be raped.`);
-		} else {
-			r.push(`${he} can be instructed to rhythmically squeeze`);
-			if (V.PC.dick !== 0) {
-				r.push(`cocks`);
+	if (skills) {
+		if (slave.fuckdoll > 0) {
+			r.push(`As a Fuckdoll,`);
+			if (slave.fuckdoll <= 45) {
+				r.push(`${he} is only fit to be locked in place so ${his} rear hole can be raped.`);
 			} else {
-				r.push(`anything`);
-			}
-			r.push(`inserted into ${his} rear hole.`);
-			if (slave.fuckdoll <= 85) {
-				r.push(`${He} can also be ordered to bounce atop objects in ${his} anus.`);
+				r.push(`${he} can be instructed to rhythmically squeeze`);
+				if (V.PC.dick !== 0) {
+					r.push(`cocks`);
+				} else {
+					r.push(`anything`);
+				}
+				r.push(`inserted into ${his} rear hole.`);
+				if (slave.fuckdoll <= 85) {
+					r.push(`${He} can also be ordered to bounce atop objects in ${his} anus.`);
+				}
 			}
-		}
-	} else {
-		if (slave.skill.anal >= 100) {
-			r.push(`${He} is a <span class="skill">masterful anal slut.</span>`);
-		} else if (slave.skill.anal > 60) {
-			r.push(`${He} is an <span class="skill">expert anal slut.</span>`);
-		} else if (slave.skill.anal > 30) {
-			r.push(`${He} is a <span class="skill">skilled anal slut.</span>`);
-		} else if (slave.skill.anal > 10) {
-			r.push(`${He} has <span class="skill">basic knowledge about anal.</span>`);
 		} else {
-			r.push(`${He} is unskilled at taking anal.`);
+			if (slave.skill.anal >= 100) {
+				r.push(`${He} is a <span class="skill">masterful anal slut.</span>`);
+			} else if (slave.skill.anal > 60) {
+				r.push(`${He} is an <span class="skill">expert anal slut.</span>`);
+			} else if (slave.skill.anal > 30) {
+				r.push(`${He} is a <span class="skill">skilled anal slut.</span>`);
+			} else if (slave.skill.anal > 10) {
+				r.push(`${He} has <span class="skill">basic knowledge about anal.</span>`);
+			} else {
+				r.push(`${He} is unskilled at taking anal.`);
+			}
 		}
 	}
 	return r.join(" ");
diff --git a/src/npc/descriptions/crotch/dick.js b/src/npc/descriptions/crotch/dick.js
index f8bdeeeb57046fbc3998a1aa221a487f8aacb281..042cf1e565beab4476c71b19748059c5b04f397c 100644
--- a/src/npc/descriptions/crotch/dick.js
+++ b/src/npc/descriptions/crotch/dick.js
@@ -1,9 +1,10 @@
 /**
  * @param {FC.GingeredSlave} slave
  * @param {DescType} [descType=DescType.NORMAL]
+ * @param {boolean} [skills=true] If true then we describe how skilled the slave is, otherwise we omit it.
  * @returns {string}
  */
-App.Desc.dick = function(slave, descType = DescType.NORMAL) {
+App.Desc.dick = function(slave, descType = DescType.NORMAL, skills = true) {
 	const r = [];
 	const {
 		he, him, his, himself, girl, He, His
@@ -1631,7 +1632,9 @@ App.Desc.dick = function(slave, descType = DescType.NORMAL) {
 	}
 
 	r.push(App.Desc.mods(slave, "dick"));
-	penetrativeSkillDesc();
+	if (skills) {
+		penetrativeSkillDesc();
+	}
 
 	return r.join(" ");
 
diff --git a/src/npc/descriptions/crotch/vagina.js b/src/npc/descriptions/crotch/vagina.js
index 7583611173fe072762220dd246258f76d1a3da83..8cbe6d99ceffe61b27db6e2df6200643398ef60b 100644
--- a/src/npc/descriptions/crotch/vagina.js
+++ b/src/npc/descriptions/crotch/vagina.js
@@ -1,8 +1,9 @@
 /**
  * @param {FC.GingeredSlave} slave
+ * @param {boolean} [skills=true] If true then we describe how skilled the slave is, otherwise we omit it.
  * @returns {string}
  */
-App.Desc.vagina = function(slave) {
+App.Desc.vagina = function(slave, skills = true) {
 	const r = [];
 	const {
 		he, him, his, himself, He, His
@@ -601,82 +602,84 @@ App.Desc.vagina = function(slave) {
 		}
 	}
 
-	if (slave.fuckdoll > 0) {
-		if (slave.vagina > 0) {
-			r.push(`${His} front hole`);
-			if (slave.fuckdoll <= 45) {
-				r.push(`is mostly useful when ${he}'s restrained for rape.`);
-			} else {
-				r.push(`will massage`);
-				if (V.PC.dick !== 0) {
-					r.push(`cocks`);
+	if (skills) {
+		if (slave.fuckdoll > 0) {
+			if (slave.vagina > 0) {
+				r.push(`${His} front hole`);
+				if (slave.fuckdoll <= 45) {
+					r.push(`is mostly useful when ${he}'s restrained for rape.`);
 				} else {
-					r.push(`anything`);
-				}
-				r.push(`placed inside it on command.`);
-				if (slave.fuckdoll <= 85) {
-					r.push(`${He} is even capable of riding`);
+					r.push(`will massage`);
 					if (V.PC.dick !== 0) {
-						r.push(`dick.`);
+						r.push(`cocks`);
 					} else {
-						r.push(`a strap-on.`);
+						r.push(`anything`);
+					}
+					r.push(`placed inside it on command.`);
+					if (slave.fuckdoll <= 85) {
+						r.push(`${He} is even capable of riding`);
+						if (V.PC.dick !== 0) {
+							r.push(`dick.`);
+						} else {
+							r.push(`a strap-on.`);
+						}
 					}
 				}
 			}
-		}
-	} else {
-		let skillBoth = slave.vagina >= 0 && slave.dick === 0 && (canPenetrate(slave) || penetrativeSocialUse(slave) >= 40) ? `</span> ${(slave.skill.vaginal > 10 && slave.skill.penetrative > 10) || (slave.skill.vaginal <= 10 && slave.skill.penetrative <= 10) ? "and" : "but"} ${he} is` : `.</span>`;
-		if (slave.vagina === -1) {
-			if (V.seeDicks < 100 && slave.anus !== 0) {
-				r.push(`Since ${he} lacks a vagina, ${he} takes it up`);
-				if (V.seeRace === 1) {
-					r.push(`${his} ${slave.race}`);
-				} else {
-					r.push(`the`);
-				}
-				r.push(`ass instead.`);
-			}
-		} else if (slave.skill.vaginal >= 100) {
-			r.push(`${He} is a <span class="skill">vanilla sex master${skillBoth}`);
-		} else if (slave.skill.vaginal > 60) {
-			r.push(`${He} is a <span class="skill">vanilla sex expert${skillBoth}`);
-		} else if (slave.skill.vaginal > 30) {
-			r.push(`${He} is <span class="skill">skilled at vanilla sex${skillBoth}`);
-		} else if (slave.skill.vaginal > 10) {
-			r.push(`${He} has <span class="skill">basic knowledge about vanilla sex${skillBoth}`);
 		} else {
-			r.push(`${He} is unskilled at vaginal sex${skillBoth}`);
-		}
-		if (slave.dick === 0) {
-			if (canPenetrate(slave)) {
-				if (slave.skill.penetrative >= 100) {
-					r.push(`a <span class="skill">penetrative sex master.</span>`);
-				} else if (slave.skill.penetrative > 60) {
-					r.push(`an <span class="skill">expert at penetrative sex.</span>`);
-				} else if (slave.skill.penetrative > 30) {
-					r.push(`<span class="skill">skilled at penetrating others.</span>`);
-				} else if (slave.skill.penetrative > 10) {
-					r.push(`<span class="skill">capable of basic penetrative sex.</span>`);
-				} else {
-					if (penetrativeSocialUse(slave) >= 40) {
-						r.push(`clueless at how to penetrate others.`);
+			let skillBoth = slave.vagina >= 0 && slave.dick === 0 && (canPenetrate(slave) || penetrativeSocialUse(slave) >= 40) ? `</span> ${(slave.skill.vaginal > 10 && slave.skill.penetrative > 10) || (slave.skill.vaginal <= 10 && slave.skill.penetrative <= 10) ? "and" : "but"} ${he} is` : `.</span>`;
+			if (slave.vagina === -1) {
+				if (V.seeDicks < 100 && slave.anus !== 0) {
+					r.push(`Since ${he} lacks a vagina, ${he} takes it up`);
+					if (V.seeRace === 1) {
+						r.push(`${his} ${slave.race}`);
 					} else {
-						r.push(`unskilled at using ${his} ${clitDesc(slave)} for penetration.`);
+						r.push(`the`);
 					}
+					r.push(`ass instead.`);
 				}
-			} else if (penetrativeSocialUse(slave) >= 40 && slave.vagina >= 0) {
-				if (slave.skill.penetrative >= 100) {
-					r.push(`a <span class="skill">penetrative sex master</span>`);
-				} else if (slave.skill.penetrative > 60) {
-					r.push(`an <span class="skill">expert at penetrative sex</span>`);
-				} else if (slave.skill.penetrative > 30) {
-					r.push(`<span class="skill">skilled at penetrating others</span>`);
-				} else if (slave.skill.penetrative > 10) {
-					r.push(`<span class="skill">capable of basic penetrative sex</span>`);
-				} else {
-					r.push(`clueless at how to penetrate others`);
+			} else if (slave.skill.vaginal >= 100) {
+				r.push(`${He} is a <span class="skill">vanilla sex master${skillBoth}`);
+			} else if (slave.skill.vaginal > 60) {
+				r.push(`${He} is a <span class="skill">vanilla sex expert${skillBoth}`);
+			} else if (slave.skill.vaginal > 30) {
+				r.push(`${He} is <span class="skill">skilled at vanilla sex${skillBoth}`);
+			} else if (slave.skill.vaginal > 10) {
+				r.push(`${He} has <span class="skill">basic knowledge about vanilla sex${skillBoth}`);
+			} else {
+				r.push(`${He} is unskilled at vaginal sex${skillBoth}`);
+			}
+			if (slave.dick === 0) {
+				if (canPenetrate(slave)) {
+					if (slave.skill.penetrative >= 100) {
+						r.push(`a <span class="skill">penetrative sex master.</span>`);
+					} else if (slave.skill.penetrative > 60) {
+						r.push(`an <span class="skill">expert at penetrative sex.</span>`);
+					} else if (slave.skill.penetrative > 30) {
+						r.push(`<span class="skill">skilled at penetrating others.</span>`);
+					} else if (slave.skill.penetrative > 10) {
+						r.push(`<span class="skill">capable of basic penetrative sex.</span>`);
+					} else {
+						if (penetrativeSocialUse(slave) >= 40) {
+							r.push(`clueless at how to penetrate others.`);
+						} else {
+							r.push(`unskilled at using ${his} ${clitDesc(slave)} for penetration.`);
+						}
+					}
+				} else if (penetrativeSocialUse(slave) >= 40 && slave.vagina >= 0) {
+					if (slave.skill.penetrative >= 100) {
+						r.push(`a <span class="skill">penetrative sex master</span>`);
+					} else if (slave.skill.penetrative > 60) {
+						r.push(`an <span class="skill">expert at penetrative sex</span>`);
+					} else if (slave.skill.penetrative > 30) {
+						r.push(`<span class="skill">skilled at penetrating others</span>`);
+					} else if (slave.skill.penetrative > 10) {
+						r.push(`<span class="skill">capable of basic penetrative sex</span>`);
+					} else {
+						r.push(`clueless at how to penetrate others`);
+					}
+					r.push(`using toys${hasAnyArms(slave) ? ` or ${his} fingers` : ""}.`);
 				}
-				r.push(`using toys${hasAnyArms(slave) ? ` or ${his} fingers` : ""}.`);
 			}
 		}
 	}
diff --git a/src/npc/descriptions/descriptionWidgets.js b/src/npc/descriptions/descriptionWidgets.js
index 1ec0360435e2dadd15b58c7c054111be23302ed8..31fd2d79957ee735779eb56c0fe23d9e2f3ab199 100644
--- a/src/npc/descriptions/descriptionWidgets.js
+++ b/src/npc/descriptions/descriptionWidgets.js
@@ -1460,207 +1460,244 @@ App.Desc.mouthAccessory = function(slave) {
 
 /**
  * @param {App.Entity.SlaveState} slave
- * @returns {string} Description of slave's limbs
+ * @returns {ContainerT} Description of slave's genetic quirks
  */
 App.Desc.geneticQuirkAssessment = function(slave) {
-	const r = [];
+	const r = new SpacedTextAccumulator();
 	const {
 		he, him, his, He, His
 	} = getPronouns(slave);
+
+	/**
+	 * colors the text dodgerblue if the slave has the quirk
+	 * colors the text deepskyblue if the slave carries the quirk
+	 * adds a tooltip showing the quirk's title and description from App.Data.geneticQuirks
+	 * @param {string} text the text to be changed
+	 * @param {keyof FC.GeneticQuirks} quirk must be a valid key in App.Data.geneticQuirks
+	 * @param {boolean} [carrier=false] if true then we color text to show that the slave carries that quirk, otherwise we color it to show that the slave has that quirk
+	 * @returns {HTMLSpanElement}
+	 */
+	function showQuirk(text, quirk, carrier=false) {
+		if (App.Data.geneticQuirks.get(quirk) === undefined) {
+			throw new Error(`Unknown quirk "${quirk}"! The quirk must exist in App.Data.geneticQuirks to be valid`);
+		}
+		let span = App.UI.DOM.makeElement("span", text);
+
+		// color the text
+		if (carrier) {
+			span.classList.add("deepskyblue");
+		} else {
+			span.classList.add("dodgerblue");
+		}
+
+		// add tooltip
+		span.tabIndex = 0;
+		span.classList.add("has-tooltip");
+		tippy(span, {
+			content: App.UI.DOM.makeElement(
+				"span",
+				App.Data.geneticQuirks.get(quirk).title + "; " + App.Data.geneticQuirks.get(quirk).description
+			),
+		});
+
+		return span;
+	}
+
 	if (V.geneticMappingUpgrade >= 1) {
 		if (slave.geneticQuirks.albinism === 2) {
-			r.push(`${He} is an albino.`);
+			r.push(`${He} is an `, showQuirk("albino", "albinism"), `.`);
 		} else if (slave.geneticQuirks.albinism === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of the albinism gene.`);
+			r.push(`${He} is a carrier of the `, showQuirk("albinism", "albinism", true), ` gene.`);
 		}
 		if (slave.geneticQuirks.dwarfism === 2 && slave.geneticQuirks.gigantism === 2) {
-			r.push(`${He} has both dwarfism and gigantism.`);
+			r.push(`${He} has both`, showQuirk("dwarfism", "dwarfism"), `and`, showQuirk("gigantism", "gigantism"), `.`);
 		} else if (slave.geneticQuirks.dwarfism === 2) {
-			r.push(`${He} has dwarfism.`);
+			r.push(`${He} has`, showQuirk("dwarfism", "dwarfism"), `.`);
 		} else if (slave.geneticQuirks.gigantism === 2) {
-			r.push(`${He} has gigantism.`);
+			r.push(`${He} has`, showQuirk("gigantism", "gigantism"), `.`);
 		}
 		if (slave.geneticQuirks.dwarfism === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of the dwarfism gene.`);
+			r.push(`${He} is a carrier of the`, showQuirk("dwarfism", "dwarfism", true), `gene.`);
 		}
 		if (slave.geneticQuirks.gigantism === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of the gigantism gene.`);
+			r.push(`${He} is a carrier of the`, showQuirk("gigantism", "gigantism", true), `gene.`);
 		}
 		if (slave.geneticQuirks.progeria >= 2) {
-			r.push(`${He} has progeria${slave.geneticQuirks.neoteny === 3 ? ", but it hasn't become a problem yet" : ""}.`);
+			r.push(`${He} has`, showQuirk("progeria", "progeria"), `${slave.geneticQuirks.neoteny === 3 ? ", but it hasn't become a problem yet" : ""}.`);
 			if (slave.geneticQuirks.neoteny >= 2) {
-				r.push(`Oddly enough, ${he} also possesses a neotenic traits, but they won't get the chance to express.`);
+				r.push(`Oddly enough, ${he} also possesses`, showQuirk("neotenic", "neoteny"), `traits, but they won't get the chance to express.`);
 			}
 		} else if (slave.geneticQuirks.neoteny >= 2) {
-			r.push(`${He} has a genetic makeup that ${slave.geneticQuirks.neoteny === 2 ? "renders" : "will render"} ${him} neotenic.`);
+			r.push(`${He} has a genetic makeup that ${slave.geneticQuirks.neoteny === 2 ? "renders" : "will render"} ${him} `, showQuirk("neotenic", "neoteny"), `.`);
 		}
 		if (slave.geneticQuirks.progeria === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of the progeria gene.`);
+			r.push(`${He} is a carrier of the`, showQuirk("progeria", "progeria", true), `gene.`);
 		}
 		if (slave.geneticQuirks.neoteny === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of traits that can result in neotenous development if expressed.`);
+			r.push(`${He} is a carrier of traits that can result in`, showQuirk("neotenous", "neoteny", true), `development if expressed.`);
 		}
 		if (typeof slave.geneticQuirks.heterochromia === "string") {
-			r.push(`${He} carries a gene that allows ${his} eyes to be two different colors.`);
+			r.push(`${He} has a gene that makes ${his}`, showQuirk("eyes two different colors", "heterochromia"), `.`);
 		} else if (slave.geneticQuirks.heterochromia === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of the heterochromia gene.`);
+			r.push(`${He} is a carrier of the`, showQuirk("heterochromia", "heterochromia", true), `gene.`);
 		}
 		if (slave.geneticQuirks.androgyny === 2) {
-			r.push(`${He} has a hormonal condition resulting in androgyny.`);
+			r.push(`${He} has a hormonal condition resulting in`, showQuirk("androgyny", "androgyny"), `.`);
 		} else if (slave.geneticQuirks.androgyny === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a gene that results in androgyny.`);
+			r.push(`${He} is a carrier of a gene that results in`, showQuirk("androgyny", "androgyny", true), `.`);
 		}
 		if (slave.geneticQuirks.pFace === 2) {
-			r.push(`${He} has an exceedingly rare trait associated with perfect facial beauty.`);
+			r.push(`${He} has an exceedingly rare trait associated with`, showQuirk("perfect facial beauty", "pFace"), `.`);
 			if (slave.geneticQuirks.uFace === 2) {
-				r.push(`Oddly enough, ${he} also possesses a conflicting trait for raw ugliness; the two average each other out.`);
+				r.push(`Oddly enough, ${he} also possesses a conflicting trait for`, showQuirk("raw facial ugliness", "uFace"), `; the two average each other out.`);
 			}
 		} else if (slave.geneticQuirks.uFace === 2) {
-			r.push(`${He} has an exceedingly rare trait associated with some of the ugliest mugs in history.`);
+			r.push(`${He} has an exceedingly rare trait associated with some of the`, showQuirk("ugliest mugs", "uFace"), `in history.`);
 		}
 		if (slave.geneticQuirks.pFace === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a combination of traits that can result in perfect facial beauty.`);
+			r.push(`${He} is a carrier of a combination of traits that can result in`, showQuirk("perfect facial beauty", "pFace", true), `.`);
 		}
 		if (slave.geneticQuirks.uFace === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a combination of traits that can result in raw ugliness.`);
+			r.push(`${He} is a carrier of a combination of traits that can result in`, showQuirk("raw facial ugliness", "uFace", true), `.`);
 		}
 		if (slave.geneticQuirks.potent === 2) {
-			r.push(`${He} is naturally potent${isVirile(slave) ? " and excels at impregnation" : ""}.`);
+			r.push(`${He} is naturally`, showQuirk("potent", "potent"), `${isVirile(slave) ? " and excels at impregnation" : ""}.`);
 		} else if (slave.geneticQuirks.potent === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a genetic condition resulting in increased potency.`);
+			r.push(`${He} is a carrier of a genetic condition resulting in increased`, showQuirk("potency", "potent", true), `.`);
 		}
 		if (slave.geneticQuirks.fertility === 2 && slave.geneticQuirks.hyperFertility === 2) {
-			r.push(`${He} has a unique genetic condition resulting in inhumanly high`);
+			r.push(`${He} has a unique genetic condition resulting in`, showQuirk("inhumanly high", "hyperFertility"));
 			if (slave.ovaries === 1 || slave.mpreg === 1) {
-				r.push(`fertility; risky intercourse will result in multiple pregnancy.`);
+				r.push(showQuirk("fertility", "fertility"), `; risky intercourse will result in multiple pregnancies.`);
 			} else {
-				r.push(`fertility.`);
+				r.push(showQuirk("fertility", "fertility"), `.`);
 			}
 		} else if (slave.geneticQuirks.hyperFertility === 2) {
-			r.push(`${He} is prone to extreme`);
+			r.push(`${He} is prone to`);
 			if (slave.ovaries === 1 || slave.mpreg === 1) {
-				r.push(`fertility and will likely undergo multiple pregnancy.`);
+				r.push(showQuirk("extreme fertility", "hyperFertility"), `and will likely undergo multiple pregnancy.`);
 			} else {
-				r.push(`fertility.`);
+				r.push(showQuirk("extreme fertility", "hyperFertility"), `.`);
 			}
 		} else if (slave.geneticQuirks.fertility === 2) {
 			r.push(`${He} is naturally`);
 			if (slave.ovaries === 1 || slave.mpreg === 1) {
-				r.push(`fertile and prone to having twins.`);
+				r.push(showQuirk("fertile", "fertility"), `and prone to having twins.`);
 			} else {
-				r.push(`fertile.`);
+				r.push(showQuirk("fertile", "fertility"), `.`);
 			}
 		}
 		if (slave.geneticQuirks.hyperFertility === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a genetic condition resulting in hyper-fertility.`);
+			r.push(`${He} is a carrier of a genetic condition resulting in`, showQuirk("hyper-fertility", "hyperFertility", true), `.`);
 		}
 		if (slave.geneticQuirks.fertility === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a genetic condition resulting in increased fertility.`);
+			r.push(`${He} is a carrier of a genetic condition resulting in increased`, showQuirk("fertility", "fertility", true), `.`);
 		}
 		if (slave.geneticQuirks.superfetation === 2) {
 			if (slave.broodmother !== 0) {
-				r.push(`${He} possesses a rare genetic flaw that causes pregnancy to not block ovulation; not that it matters with ${his} broodmother implant superseding it.`);
+				r.push(`${He} possesses a rare genetic flaw that causes`, showQuirk("pregnancy to not block ovulation", "superfetation"), `; not that it matters with ${his} broodmother implant superseding it.`);
 			} else if (isFertile(slave)) {
-				r.push(`${He} possesses a rare genetic flaw that causes pregnancy to not block ovulation. ${He} is fully capable of getting pregnant while already pregnant.`);
+				r.push(`${He} possesses a rare genetic flaw that causes`, showQuirk("pregnancy to not block ovulation", "superfetation"), `. ${He} is fully capable of getting pregnant while already pregnant.`);
 			} else {
-				r.push(`${He} possesses a rare genetic flaw that causes pregnancy to not block ovulation; not that it matters when ${he} can't get pregnant.`);
+				r.push(`${He} possesses a rare genetic flaw that causes`, showQuirk("pregnancy to not block ovulation", "superfetation"), `; not that it matters when ${he} can't get pregnant.`);
 			}
 		} else if (slave.geneticQuirks.superfetation === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a genetic flaw that causes superfetation.`);
+			r.push(`${He} is a carrier of a genetic flaw that causes`, showQuirk("superfetation", "superfetation", true), `.`);
 		}
 		if (slave.geneticQuirks.polyhydramnios === 2 || (slave.geneticQuirks.polyhydramnios === 1 && V.geneticMappingUpgrade >= 3)) {
-			r.push(`Polyhydramnios runs in ${his} family.`);
+			r.push(showQuirk("Polyhydramnios", "polyhydramnios", true), `runs in ${his} family.`);
 		}
 		if (slave.geneticQuirks.uterineHypersensitivity === 2) {
-			r.push(`${He} possesses a rare genetic trait that causes uterine hypersensitivity;`);
+			r.push(`${He} possesses a rare genetic trait that causes`, showQuirk("uterine hypersensitivity", "uterineHypersensitivity"), `;`);
 			if (slave.ovaries === 1 || slave.mpreg === 1) {
 				r.push(`pregnancy and birth will be extremely pleasurable for ${him}.`);
 			} else {
 				r.push(`it has little effect on those unable to bear children.`);
 			}
 		} else if (slave.geneticQuirks.uterineHypersensitivity === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a genetic trait that causes uterine hypersensitivity.`);
+			r.push(`${He} is a carrier of a genetic trait that causes`, showQuirk("uterine hypersensitivity", "uterineHypersensitivity", true), `.`);
 		}
 		if (slave.geneticQuirks.macromastia === 2 && slave.geneticQuirks.gigantomastia === 2) {
-			r.push(`${He} has an abnormal strain of gigantomastia and will experience constant excessive breast growth.`);
+			r.push(`${He} has an abnormal strain of`, showQuirk("gigantomastia", "gigantomastia"), `and will experience constant`, showQuirk("excessive breast growth", "macromastia"), `.`);
 		} else if (slave.geneticQuirks.gigantomastia >= 2) {
 			r.push(`${He} has`);
 			if (slave.geneticQuirks.gigantomastia === 3) {
-				r.push(`dormant gigantomastia. Hormonal effects may cause it to become active.`);
+				r.push(`dormant`, showQuirk("gigantomastia", "gigantomastia"), `. Hormonal effects may cause it to become active.`);
 			} else {
-				r.push(`gigantomastia and will experience excessive breast growth.`);
+				r.push(showQuirk("gigantomastia", "gigantomastia"), `and will experience excessive breast growth.`);
 			}
 		} else if (slave.geneticQuirks.macromastia >= 2) {
 			r.push(`${He} has`);
 			if (slave.geneticQuirks.macromastia === 3) {
-				r.push(`dormant macromastia. Hormonal effects may cause it to become active.`);
+				r.push(`dormant`, showQuirk("macromastia", "macromastia"), `. Hormonal effects may cause it to become active.`);
 			} else {
-				r.push(`macromastia and will experience excess development of breast tissue.`);
+				r.push(showQuirk("macromastia", "macromastia"), `and will experience excess development of breast tissue.`);
 			}
 		}
 		if (slave.geneticQuirks.gigantomastia === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a genetic flaw that causes gigantomastia.`);
+			r.push(`${He} is a carrier of a genetic flaw that causes`, showQuirk("gigantomastia", "gigantomastia", true), `.`);
 		}
 		if (slave.geneticQuirks.macromastia === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a genetic flaw that causes macromastia.`);
+			r.push(`${He} is a carrier of a genetic flaw that causes`, showQuirk("macromastia", "macromastia", true), `.`);
 		}
 		if (slave.geneticQuirks.galactorrhea >= 2) {
 			r.push(`${He} is predisposed to`);
 			if (slave.geneticQuirks.galactorrhea === 2 && slave.lactation > 0) {
-				r.push(`galactorrhea, not that it matters when ${he} is already lactating.`);
+				r.push(showQuirk("galactorrhea", "galactorrhea"), `, not that it matters when ${he} is already lactating.`);
 			} else {
-				r.push(`galactorrhea and will likely begin lactating inappropriately ${slave.geneticQuirks.galactorrhea === 2 ? "sooner or later" : "later in life"}.`);
+				r.push(showQuirk("galactorrhea", "galactorrhea"), `and will likely begin lactating inappropriately ${slave.geneticQuirks.galactorrhea === 2 ? "sooner or later" : "later in life"}.`);
 			}
 		} else if (slave.geneticQuirks.galactorrhea === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a gene that leads to galactorrhea.`);
+			r.push(`${He} is a carrier of a gene that leads to`, showQuirk("galactorrhea", "galactorrhea", true), `.`);
 		}
 		if (slave.geneticQuirks.wellHung === 2) {
 			if (slave.physicalAge <= 16 && slave.hormoneBalance < 100 && slave.dick > 0) {
-				r.push(`${He} is likely to experience an inordinate amount of penile growth during ${his} physical development.`);
+				r.push(`${He} is likely to experience an`, showQuirk("inordinate amount of penile growth", "wellHung"), `during ${his} physical development.`);
 			} else if (slave.dick > 0) {
-				r.push(`${He} is predisposed to having an enormous dick, though it is unlikely to naturally grow any larger than it currently is.`);
+				r.push(`${He} is`, showQuirk("predisposed to having an enormous dick", "wellHung"), `, though it is unlikely to naturally grow any larger than it currently is.`);
 			} else {
-				r.push(`${He} is predisposed to having an enormous dick, or would be, if ${he} had one.`);
+				r.push(`${He} is`, showQuirk("predisposed to having an enormous dick", "wellHung"), `, or would be, if ${he} had one.`);
 			}
 		} else if (slave.geneticQuirks.wellHung === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a gene that causes enhanced penile development.`);
+			r.push(`${He} is a carrier of a gene that causes`, showQuirk("enhanced penile development", "wellHung", true), `.`);
 		}
 		if (slave.geneticQuirks.rearLipedema === 2) {
-			r.push(`${His} body uncontrollably builds fat on ${his} rear resulting in constant growth.`);
+			r.push(`${His} body uncontrollably`, showQuirk(`builds fat on ${his} rear`, "rearLipedema"), `resulting in constant growth.`);
 		} else if (slave.geneticQuirks.rearLipedema === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a carrier of a genetic flaw that causes lipedema.`);
+			r.push(`${He} is a carrier of a genetic flaw that causes`, showQuirk(`lipedema`, "rearLipedema", true), `.`);
 		}
 		if (slave.geneticQuirks.wGain === 2 && slave.geneticQuirks.wLoss === 2) {
-			r.push(`${He} has irregular leptin production and will undergo shifts in weight.`);
+			r.push(`${He} has irregular`, showQuirk("leptin production", "wGain"), `and will undergo shifts in weight.`);
 		} else if (slave.geneticQuirks.wGain === 2) {
-			r.push(`${He} has hyperleptinemia and will easily gain weight.`);
+			r.push(`${He} has`, showQuirk("hyperleptinemia", "wGain"), `and will easily gain weight.`);
 		} else if (slave.geneticQuirks.wLoss === 2) {
-			r.push(`${He} has hypoleptinemia and will easily lose weight.`);
+			r.push(`${He} has`, showQuirk("hypoleptinemia", "wLoss"), `and will easily lose weight.`);
 		}
 		if (slave.geneticQuirks.wGain === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a hyperleptinemia carrier.`);
+			r.push(`${He} is a`, showQuirk("hyperleptinemia", "wGain", true), `carrier.`);
 		}
 		if (slave.geneticQuirks.wLoss === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a hypoleptinemia carrier.`);
+			r.push(`${He} is a`, showQuirk("hypoleptinemia", "wLoss", true), `carrier.`);
 		}
 		if (slave.geneticQuirks.mGain === 2 && slave.geneticQuirks.mLoss === 2) {
-			r.push(`${He} has severe genetic flaw resulting in easily replaced, rapidly lost muscle mass.`);
+			r.push(`${He} has severe genetic flaw resulting in`, showQuirk("easily replaced", "mGain"), `,`, showQuirk("rapidly lost muscle mass", "mLoss"), `.`);
 		} else if (slave.geneticQuirks.mGain === 2) {
-			r.push(`${He} has myotonic hypertrophy and will easily gain muscle mass.`);
+			r.push(`${He} has`, showQuirk("myotonic hypertrophy", "mGain"), `and will easily gain muscle mass.`);
 		} else if (slave.geneticQuirks.mLoss === 2) {
-			r.push(`${He} has myotonic dystrophy and will rapidly lose muscle mass.`);
+			r.push(`${He} has`, showQuirk("myotonic dystrophy", "mLoss"), `and will rapidly lose muscle mass.`);
 		}
 		if (slave.geneticQuirks.mGain === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a myotonic hypertrophy carrier.`);
+			r.push(`${He} is a`, showQuirk("myotonic hypertrophy", "mGain", true), `carrier.`);
 		}
 		if (slave.geneticQuirks.mLoss === 1 && V.geneticMappingUpgrade >= 3) {
-			r.push(`${He} is a myotonic dystrophy carrier.`);
+			r.push(`${He} is a`, showQuirk("myotonic dystrophy", "mLoss", true), `carrier.`);
 		}
 		if (slave.genes === "XY" && !V.seeDicksAffectsPregnancy) {
 			r.push(`Analysis of ${his} sperm shows that ${he} has a ${slave.spermY}% chance of fathering a son.`);
 		}
 	}
-	return r.join(` `);
+	r.toParagraph();
+	return r.container();
 };
 
 /**
diff --git a/src/npc/descriptions/face.js b/src/npc/descriptions/face.js
index d01d31e35efae92d4219eb198d0b276082f1c680..b9f4e51620c984be71ffb857e7057d35db680df1 100644
--- a/src/npc/descriptions/face.js
+++ b/src/npc/descriptions/face.js
@@ -1,8 +1,9 @@
 /**
  * @param {App.Entity.SlaveState} slave
+ * @param {boolean} [describeMakeup=true] if true then we will describe the slaves makeup, otherwise we will omit it
  * @returns {string}
  */
-App.Desc.face = function(slave) {
+App.Desc.face = function(slave, describeMakeup = true) {
 	const r = [];
 	const {
 		he, him, his, He, His, girl,
@@ -214,7 +215,7 @@ App.Desc.face = function(slave) {
 		r.push(`${He} has no sense of smell, but this isn't immediately obvious just by looking at ${his} nose.`);
 	}
 
-	if (V.showBodyMods === 1) {
+	if (V.showBodyMods === 1 && describeMakeup) {
 		if (slave.fuckdoll === 0) {
 			r.push(App.Desc.makeup(slave));
 		}
diff --git a/src/npc/descriptions/longSlave.js b/src/npc/descriptions/longSlave.js
index 49e84eff943784b606269de8d5e57616b644be4a..320e25b71924ca377925992b11b1bc7acd3a2019 100644
--- a/src/npc/descriptions/longSlave.js
+++ b/src/npc/descriptions/longSlave.js
@@ -411,7 +411,9 @@ App.Desc.longSlave = function(slave, {descType, market = 0, marketText, noArt, l
 
 	if (slave.fuckdoll === 0) {
 		if (slave.markings === "birthmark" && slave.prestige === 0 && slave.porn.prestige < 2) {
-			r.push(`${He} has a large, liver-colored birthmark, detracting from ${his} beauty.`);
+			const birthmarkSpan = App.UI.DOM.makeElement("span", `large, liver-colored birthmark,`);
+			birthmarkSpan.classList.add("red");
+			r.push(`${He} has a`, birthmarkSpan, `detracting from ${his} beauty.`);
 		}
 		if (slave.skin === "sun tanned") {
 			if ((slave.rules.release.slaves === 1) || App.Utils.hasFamilySex(slave)) {
@@ -466,60 +468,7 @@ App.Desc.longSlave = function(slave, {descType, market = 0, marketText, noArt, l
 			}
 		}
 
-		const pubertyAge = Math.min(slave.pubertyAgeXX, slave.pubertyAgeXY);
-		if (slave.race === "catgirl") {
-			if (slave.underArmHStyle === "bushy") {
-				r.push(`${His} underarm fur is especially bushy and untamed.`);
-			} else if (slave.underArmHStyle === "waxed" || slave.underArmHStyle === "hairless" || slave.underArmHStyle === "shaved" || slave.underArmHStyle === "bald") {
-				r.push(`${His} underarm fur is trim and smooth.`);
-			} else {
-				r.push(`${His} ${slave.skin} underarm fur is quite regular along ${his} fuzzy body.`);
-			}
-		} else if (slave.physicalAge < pubertyAge - 2) {
-			r.push(`${He} is too sexually immature to have armpit hair.`);
-		} else if (slave.underArmHStyle === "hairless") {
-			r.push(`${His} armpits are perfectly smooth and naturally hairless.`);
-		} else if (slave.underArmHStyle === "bald") {
-			r.push(`${His} armpits no longer grow hair, leaving them smooth and hairless.`);
-		} else if (slave.underArmHStyle === "waxed") {
-			if (slave.assignment === Job.DAIRY && V.dairyRestraintsSetting > 1) {
-				r.push(`${His} armpit hair has been removed to prevent chafing.`);
-			} else {
-				r.push(`${His} armpits are waxed and smooth.`);
-			}
-		} else if (slave.physicalAge < pubertyAge - 1) {
-			r.push(`${He} has a few ${slave.underArmHColor} wisps of armpit hair.`);
-		} else if (slave.physicalAge < pubertyAge) {
-			r.push(`${He} is on the verge of puberty and has a small patch of ${slave.underArmHColor} armpit hair.`);
-		} else if (slave.underArmHStyle === "shaved") {
-			r.push(`${His} armpits appear hairless, but closer inspection reveals light, ${slave.underArmHColor} stubble.`);
-		} else if (slave.underArmHStyle === "neat") {
-			r.push(`${His} armpit hair is neatly trimmed`);
-			if (!hasBothArms(slave)) {
-				r.push(`since`);
-				if (hasAnyArms(slave)) {
-					r.push(`at least half`);
-				} else {
-					r.push(`it`);
-				}
-				r.push(`is always in full view.`);
-			} else {
-				r.push(`to not be visible unless ${he} lifts ${his} arms.`);
-			}
-		} else if (slave.underArmHStyle === "bushy") {
-			r.push(`${His} ${slave.underArmHColor} armpit hair has been allowed to grow freely,`);
-			if (!hasAnyArms(slave)) {
-				r.push(`creating two bushy patches under where ${his} arms used to be.`);
-			} else {
-				r.push(`so it can be seen poking out from under ${his}`);
-				if (hasBothArms(slave)) {
-					r.push(`arms`);
-				} else {
-					r.push(`arm`);
-				}
-				r.push(`at all times.`);
-			}
-		}
+		r.push(App.Desc.armpitHair(slave));
 	}
 
 	if (slave.voice === 0) {
diff --git a/src/npc/generate/generateGenetics.js b/src/npc/generate/generateGenetics.js
index f6cd6c5787ae29c78922b151b765b5ab5c2185bd..79cf9ffc6cbdc2acf32c5d10d5c9d3a79835c5b8 100644
--- a/src/npc/generate/generateGenetics.js
+++ b/src/npc/generate/generateGenetics.js
@@ -1141,8 +1141,8 @@ globalThis.generateGenetics = (function() {
 
 /**
  * Creates a new child object based on its mother and father and whether or not it is destined for the Incubator
- * @param {App.Entity.SlaveState|App.Entity.PlayerState} mother The slave object carrying the child source
- * @param {object} ovum The source for the child, comes from the mother's womb array
+ * @param {FC.HumanState} mother The slave object carrying the child source
+ * @param {App.Entity.Fetus} ovum The source for the child, comes from the mother's womb array
  * @param {boolean} [incubator=false] True if the child is destined for the incubator; false if it's destined for the nursery
  * @returns {App.Entity.SlaveState|App.Facilities.Nursery.InfantState}
  */
diff --git a/src/npc/interaction/slaveOnSlaveFeeding/fSlaveFeed.js b/src/npc/interaction/slaveOnSlaveFeeding/fSlaveFeed.js
index 46b4f9e4227f87169bbe7ff3a35799a3869b2740..77c8f9d35dc071a2fd86e373fac473c80a34fe62 100644
--- a/src/npc/interaction/slaveOnSlaveFeeding/fSlaveFeed.js
+++ b/src/npc/interaction/slaveOnSlaveFeeding/fSlaveFeed.js
@@ -185,7 +185,7 @@ globalThis.FSlaveFeed = function(slave, milkTap) {
 		} else if (milkTap.devotion >= -20) {
 			r.push(`Since ${milkTap.slaveName} does not resist your will, ${he2} should comply reasonably well. If anything, ${he}'ll at least be thankful to be relieved of some pressure.`);
 		} else {
-			r.push(`Since ${milkTap.slaveName} is unlikely to comply willingly, you simply restrain ${him2} with ${his2} tits exposed and ready to be drank from.`);
+			r.push(`Since ${milkTap.slaveName} is unlikely to comply willingly, you simply restrain ${him2} with ${his2} tits exposed and ready to be drunk from.`);
 			if (milkTap.lactation > 1) {
 				r.push(`You affix nipple clamps to ${his2} ${milkTap.nipples} nipples and step back to watch ${his2} breasts back up with milk. When ${he2} is unclamped, the flow should certainly be strong enough for your desires.`);
 			} else {
diff --git a/src/player/js/PlayerState.js b/src/player/js/PlayerState.js
index e5f0c8ca097b4a0f176b20cf03af61ee0cfeb440..924d2cab91d3a6fc2b10069e37b7d56b68d7a779 100644
--- a/src/player/js/PlayerState.js
+++ b/src/player/js/PlayerState.js
@@ -255,7 +255,7 @@ App.Entity.PlayerState = class PlayerState {
 		/**
 		 * * The method of consumption of .refreshment
 		 * * 0: smoked
-		 * * 1: drank
+		 * * 1: drunk
 		 * * 2: eaten
 		 * * 3: snorted
 		 * * 4: injected
@@ -1402,7 +1402,7 @@ App.Entity.PlayerState = class PlayerState {
 		 * * "a slutty outfit"
 		 * * "nice business attire"
 		 * * "no clothing"
-		 */
+		 * @type {FC.Clothes} */
 		this.clothes = "nice business attire";
 		/**
 		 * may accept strings, use at own risk
diff --git a/src/pregmod/editGenetics.js b/src/pregmod/editGenetics.js
index 913dcfa107fd509d0962885f95e595238db5373f..8bb54e62e31e464bea29adebbcf22a9a2c3eb4b4 100644
--- a/src/pregmod/editGenetics.js
+++ b/src/pregmod/editGenetics.js
@@ -129,14 +129,14 @@ App.UI.editGenetics = function() {
 	 */
 	function geneDetailsFunction(s) {
 		/**
-		 * Makes an html table row
+		 * Makes a html table row
 		 */
 		function makeRow() {
 			row = App.UI.DOM.appendNewElement("tr", tbody);
 		}
 
 		/**
-		 * Makes an html table cell
+		 * Makes a html table cell
 		 * @param {string|DocumentFragment|HTMLSpanElement} text
 		 * @param {number} [colSpan] the number of columns in the table that the cell should span
 		 */
@@ -148,7 +148,7 @@ App.UI.editGenetics = function() {
 		}
 
 		/**
-		 * Makes an html header with the given text
+		 * Makes a html header with the given text
 		 * @param {string} text
 		 */
 		function makeHeader(text) {
@@ -178,7 +178,7 @@ App.UI.editGenetics = function() {
 		makeCell(s.slaveSurname || '', 2);
 
 		makeHeader(`Karyotype`);
-		cell = App.UI.DOM.appendNewElement("td", row, `${s.genes} (${toSex(s.genes)})`, ["editor", "choice-editor"]);
+		cell = App.UI.DOM.appendNewElement("td", row, `${s.genes} (${geneToGender(s.genes, {keepKaryotype: true, lowercase: false})})`, ["editor", "choice-editor"]);
 		cell.setAttribute("data-param", "genes");
 		cell.setAttribute("data-choices", "XX, XY");
 
@@ -455,22 +455,6 @@ App.UI.editGenetics = function() {
 		return feet + "\u2032" + inches + '\u2033';
 	}
 
-	/**
-	 * @param {FC.GenderGenes} karyotype the karyotype to convert
-	 * @returns {string} the gender that matches the karyotype
-	 */
-	function toSex(karyotype) {
-		return {
-			XX: 'female',
-			XY: 'male',
-			X: 'Turner syndrome female',
-			X0: 'Turner syndrome female',
-			XYY: 'XYY syndrome male',
-			XXY: 'Klinefelter syndrome male',
-			XXX: 'triple X syndrome female'
-		}[String(karyotype).toUpperCase()] || 'unknown/not viable';
-	}
-
 	/**
 	 * @param {App.Entity.SlaveState} slave the slave to describe
 	 * @returns {string} their age description
diff --git a/src/pregmod/surrogacy.js b/src/pregmod/surrogacy.js
index bf561b26c5c0c0ff65621d530de41ca62301538e..b08d89fd63dd8f86780997b0d3171dcb1bd6dd77 100644
--- a/src/pregmod/surrogacy.js
+++ b/src/pregmod/surrogacy.js
@@ -4,7 +4,7 @@ App.UI.surrogacy = function() {
 	const donatrix = getDonatrix();
 	const impregnatrix = getImpregnatrix();
 	const wombIndex = V.wombIndex;
-	const slave = getSlave(V.AS);
+	const slave = getSlave(V.AS) || V.surgeryType.startsWith("transplant") ? donatrix: undefined;
 	let r = [];
 
 	function getReceptrix() {