code:
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 = [SID.PC];
			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;
};

App.Events.PregnancyNotice.unreservedTanks = () => {
	if (!V.incubator.capacity) { return 0; }
	return (V.incubator.capacity - tankSlaveCount()) - FetusGlobalReserveCount("incubator");
};

App.Events.PregnancyNotice.unreservedCribs = () => {
	if (!V.nurseryCribs) { return 0; }
	return (V.nurseryCribs - infantCount()) - FetusGlobalReserveCount("nursery");
};

/**
 * @param {FC.HumanState} mother
 * @param {App.Entity.Fetus} fetus
 * @returns {boolean}
 */
App.Events.PregnancyNotice.validFetus = (mother, fetus) => {
	return (
		fetus.age === 2 && fetus.noticeData.fate === "undecided" ||
		(fetus.age === 6 && ["wait", "undecided"].includes(fetus.noticeData.fate)) ||
		// Week 35 is the last one before the first labor check
		(fetus.age === 35 && ["lateWait", "undecided"].includes(fetus.noticeData.fate)) ||
		// 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
		(mother.ID !== SID.PC && mother.weekAcquired === V.week && fetus.age > 6 && fetus.noticeData.fate === "undecided")
	);
};

/**
 * sets the default fate and reserved place for a fetus based off the mothers `pregNoticeDefault` property
 * @param {FC.HumanState} mother
 */
App.Events.PregnancyNotice.setFetusDefaults = (mother) => {
	if (mother.pregNoticeDefault !== "none") {
		mother.womb.filter((fetus) => App.Events.PregnancyNotice.validFetus(mother, fetus)).forEach(fetus => {
			if (mother.pregNoticeDefault === "incubator") {
				// if there is space in the incubator
				if (App.Events.PregnancyNotice.unreservedTanks() > 0) {
					// reserve incubator space
					fetus.noticeData.fate = "incubator";
					fetus.reserve = "incubator";
				// else if they are 2 weeks old
				} else if (fetus.age === 2) {
					// wait and try again at 6 weeks
					fetus.noticeData.fate = "wait";
				} else {
					// set them to nothing
					fetus.noticeData.fate = "nothing";
				}
			} else if (mother.pregNoticeDefault === "nursery") {
				// if there is space in the nursery
				if (App.Events.PregnancyNotice.unreservedCribs() > 0) {
					// reserve nursery space
					fetus.noticeData.fate = "nursery";
					fetus.reserve = "nursery";
				// else if they are 2 weeks old
				} else if (fetus.age === 2) {
					// wait and try again at 6 weeks
					fetus.noticeData.fate = "wait";
				} else {
					// set them to nothing
					fetus.noticeData.fate = "nothing";
				}
			} else if (mother.pregNoticeDefault === "nothing") {
				fetus.noticeData.fate = "nothing";
			} else {
				console.error(`Unhandled slave.pregNoticeDefault of "${mother.pregNoticeDefault}"`);
			}
		});
	}
};

/**
 * @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 the actor is set to bypass the pregnancy notice
	if (actor.pregNoticeBypass === true) {
		// but make sure to set the fetuses defaults firsts
		App.Events.PregnancyNotice.setFetusDefaults(actor);
		return false;
	}
	// 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 (App.Events.PregnancyNotice.validFetus(actor, fetus) === true) {
			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)
	};
};

/**
 * Creates a button for the pregnancy notices options
 * @param {FC.HumanState} mother The slave to show options for
 * @param {boolean} [inPregnancyNotice=false] should be true if this is being called from the pregnancy notice, false otherwise
 * @param {HTMLDivElement} [existingDiv=undefined] used internally
 * @param {object} [state=undefined] used internally
 * @returns {HTMLDivElement}
 */
App.Events.PregnancyNotice.options = (mother, inPregnancyNotice = false, existingDiv = undefined, state = undefined) => {
	let text = inPregnancyNotice ? "Options" : "Pregnancy Notice Options";
	let mainDiv = existingDiv ?? App.UI.DOM.makeElement("div");
	mainDiv.innerHTML = ""; // clear it if it is an existing div
	state = state ?? {
		open: false,
	};
	const options = new App.UI.OptionsGroup();
	options.customRefresh(() => {
		App.Events.PregnancyNotice.options(mother, inPregnancyNotice, mainDiv, state);
	});
	let option = options.addOption("", "open", state);
	if (inPregnancyNotice === true) {
		option.addComment("These options are also available in each slave's Rules tab.");
	}
	if (state.open === true) {
		option.addValue(`Close ${text}`, false)
			.addCallback(() => {
				state.open = false;
				App.Events.PregnancyNotice.options(mother, inPregnancyNotice, mainDiv, state);
			});
		option = options.addOption("Default action for this slaves children is", "pregNoticeDefault", mother)
			.addValue("No default", "none")
			.addValue("Do Nothing", "nothing")
			.addValue("Send to the incubator", "incubator")
			.addValue("Send to the nursery", "nursery");
		if (["incubator", "nursery"].includes(mother.pregNoticeDefault)) {
			option.addComment("If there isn't enough room then this will fall back to doing nothing.");
		}
		option = options.addOption("End of week pregnancy notice for this slave is", "pregNoticeBypass", mother)
			.addValue("Enabled", false).on()
			.addValue("Disabled", true).off();
		if (mother.pregNoticeBypass === true) {
			option.addComment("The default action above will still happen, but the notice will not be shown.");
		}
		if (inPregnancyNotice === true) {
			// TODO:@franklygeorge: auto collapse processed option
		}
	} else {
		option.addValue(text, true)
			.addCallback(() => {
				state.open = true;
				App.Events.PregnancyNotice.options(mother, inPregnancyNotice, mainDiv, state);
			});
	}
	mainDiv.append(options.render());
	return mainDiv;
};

/**
 * 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);

	// set default fetus fates
	App.Events.PregnancyNotice.setFetusDefaults(mother);

	const cheating = (V.cheatMode || V.debugMode);

	const motherIsPC = (mother.ID === SID.PC);
	// @ts-expect-error weekAcquired doesn't exist on PlayerState
	const motherIsNew = (mother.ID === SID.PC) ? false : (mother.weekAcquired === V.week);
	const totalBabyCount = mother.womb.length;
	const fetuses = mother.womb.filter((fetus) => App.Events.PregnancyNotice.validFetus(mother, fetus));
	const nurseryName = App.Entity.facilities.nursery.name.replace(/^the /i, "").trim();
	const incubatorName = App.Entity.facilities.incubator.name.replace(/^the /i, "").trim();
	const hasIncubator = (V.incubator.capacity > 0);
	const hasNursery = (V.nurseryCribs > 0);
	let motherDiv = App.UI.DOM.makeElement("div");
	App.Events.drawEventArt(motherDiv, mother, undefined, undefined, true);
	if (motherIsPC) {
		motherDiv.append(App.UI.DOM.makeElement(
			"div",
			`You are pregnant with ${num(totalBabyCount)} ${(totalBabyCount === 1) ? "baby": "babies"}.`),
		);
		if (totalBabyCount !== fetuses.length) {
			motherDiv.append(App.UI.DOM.makeElement(
				"div",
				` ${num(fetuses.length)} of them are listed below.`),
			);
		}
	} else {
		// @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.`);
		}
		motherDiv.append(App.UI.DOM.makeElement("div", `${pronounsForSlaveProp(mother, mother.origin)}`))
	}
	const transplantedBabyCount = mother.womb.filter((fetus) => (fetus.motherID !== mother.ID)).length;
	if (transplantedBabyCount !== 0) {
		motherDiv.append(App.UI.DOM.makeElement(
			"div",
			`${num(transplantedBabyCount)} of them ${(transplantedBabyCount === 1) ? "was": "are"} transplanted.`),
		);
	}
	// 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 =  mother.womb.filter((fetus) => (App.Events.PregnancyNotice.validFetus(mother, fetus)));
		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));
			let terminatableFetuses = unprocessedFetuses.filter((fetus) => (mother.womb.indexOf(fetus) !== -1) && canTerminateFetus(mother, fetus));
			let transplantableFetuses = unprocessedFetuses.filter((fetus) => canTransplantFetus(mother, fetus) === 1);
			unprocessedDiv.append(`${toSentence(unprocessedFetusNames, ", ", " and ")} ${(unprocessedFetuses.length === 1) ? "has": "have"} not been processed yet.`);
			if (V.pregnancyNotice.nextLockout === true) {
				V.nextButton = " ";
			} else {
				V.nextButton = "Apply";
			}
			App.Utils.updateUserButton();

			let bulkOptions = new App.UI.OptionsGroup;
			let bulkChoice = {
				/** @type {"incubator"|"nursery"|"nothing"|"wait"|"lateWait"|"undecided"|"terminate"|"transplant"} */
				choice: "undecided"
			};
			bulkOptions.customRefresh(() => {
				unprocessedFetuses.forEach((fetus) => {
					if (bulkChoice.choice === "incubator") {
						if (App.Events.PregnancyNotice.unreservedTanks() > 0) {
							fetus.noticeData.fate = bulkChoice.choice;
							// @ts-ignore
							fetus.reserve = bulkChoice.choice;
						}
					} else if (bulkChoice.choice === "nursery") {
						if (App.Events.PregnancyNotice.unreservedCribs() > 0) {
							fetus.noticeData.fate = bulkChoice.choice;
							// @ts-ignore
							fetus.reserve = bulkChoice.choice;
						}
					} else {
						fetus.noticeData.fate = bulkChoice.choice;
					}
					let noteSpan = $(`#${fetus.ID}-note-span`)[0];
					fetusInfo(fetus, noteSpan);
				});
				if (bulkChoice.choice === "transplant") {
					let bulkTransplantDiv = App.UI.DOM.makeElement("div");
					unprocessedDiv.append(bulkTransplantDiv);
					transplantingTool(mother, unprocessedFetuses, bulkTransplantDiv, transplantableFetuses.length, updateProcessed, undefined, true);
				} else if (bulkChoice.choice === "terminate") {
					let bulkTerminateDiv = App.UI.DOM.makeElement("div");
					unprocessedDiv.append(bulkTerminateDiv);
					BulkTerminateTool(mother, unprocessedFetuses, bulkTerminateDiv, terminatableFetuses.length, updateProcessed);
				}
			});
			let bulkOption = bulkOptions.addOption("Change all unprocessed to: ", "choice", bulkChoice);
			if (App.Events.PregnancyNotice.unreservedTanks() >= unprocessedFetuses.length) {
				bulkOption.addValue(`Reserve ${incubatorName} tanks`, "incubator");
			}
			if (App.Events.PregnancyNotice.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");
			}
			if (unprocessedFetuses.filter((fetus) => fetus.age < 35).length !== 0) {
				bulkOption.addValue("Ask me again in the 35th week", "lateWait");
			}
			if (transplantableFetuses.length !== 0) {
				bulkOption.addValue("Transplant fetuses", "transplant");
			}
			if (terminatableFetuses.length !== 0) {
				bulkOption.addValue("Terminate fetuses", "terminate");
			}
			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.pregnancyNotice.accordionCollapsed === -1) ? (V.useAccordion > 0) : (V.pregnancyNotice.accordionCollapsed > 0),
		);
		fetusAccordion.noteSpan.id = `${fetus.ID}-note-span`;
		fetusAccordion.contentDiv.append(fetusInfo(fetus, fetusAccordion.noteSpan));
		fetusesDiv.prepend(fetusAccordion.accordion);
	});

	node.append(motherDiv);
	const hr = App.UI.DOM.makeElement("hr");
	hr.style.clear = "both";
	node.append(hr);
	node.append(unprocessedDiv);
	node.append(App.Events.PregnancyNotice.options(mother, true));
	node.append(App.UI.DOM.makeElement("hr"));
	node.append(fetusesDiv);
	// TODO:@franklygeorge also show unprocessedDiv here (below fetusesDiv)

	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 = getParent(fetus.fatherID, false);
		const fatherIsPC = (typeof father !== "string" && father.ID === SID.PC);
		const fatherName = (typeof father === "string") ? father: SlaveFullName(father);

		const canTerminate = (mother.womb.indexOf(fetus) !== -1) && canTerminateFetus(mother, fetus);
		const canTransplant = canTransplantFetus(mother, fetus);

		const twins = getFetusTwins(fetus);

		/** @type {FC.SlaveState} */
		const fakeChild = generateChild(mother, fetus, true);
		// Age fake child up to the ideal age
		while (fakeChild.actualAge < V.idealAge) {
			ageSlave(fakeChild);
			// TODO:@franklygeorge handle puberty and teeth
		}
		// set some properties that would usually be modified based on diet
		fakeChild.weight = 0;
		fakeChild.muscles = 5;
		fakeChild.waist = -10;
		const buttLookup = {
			"-2": 0,
			"-1": 1,
			"0": 3,
			"1": 4,
			"2": 6,
			"3": 11,
		};
		fakeChild.butt = buttLookup[String(fakeChild.hips)];

		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", undefined, true);
				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 === SID.ELITE || fetus.fatherID === SID.LAB) ? "are": "is",
						`the father.`
					);
					noteSpanContent += `; Father ${(typeof father === "string") ? father: SlaveFullName(father)}`;
				}
			}
		}

		noteSpan.textContent = noteSpanContent;

		if (canTransplant === -1) {
			if (fetus.motherID === SID.PC) {
				intro.push(`The child was transplanted from your womb.`);
			} else {
				let origMom = getSlave(fetus.motherID);
				if (origMom) {
					intro.push(`The child was transplanted from`, App.SlaveAssignment.saSlaveName(origMom), `'s womb.`);
				} else {
					let momRecord = getGenePoolRecord(fetus.motherID);
					if (momRecord) {
						intro.push(`The child was transplanted from`, SlaveFullName(momRecord), `'s womb.`);
					} else {
						intro.push(`The child was transplanted from someone else'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(() => {
					fetus.genetics.artSeed = jsRandom(0, 10 ** 14); // give the fetus a new seed for generation
					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();
			} else if (V.inbreeding && fakeChild.inbreedingCoeff > 0) {
				intro.push(`${He} is`);
				if (fakeChild.inbreedingCoeff >= 0.5) {
					intro.push("extremely");
				} else if (fakeChild.inbreedingCoeff >= 0.25) {
					intro.push("very");
				} else if (fakeChild.inbreedingCoeff >= 0.125) {
					// no adjective here
				} else if (fakeChild.inbreedingCoeff >= 0.0625) {
					intro.push("somewhat");
				} else {
					intro.push("slightly");
				}
				intro.push(`inbred, with a CoI of ${fakeChild.inbreedingCoeff}.`);
				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(He);
			intro.push(App.Desc.dimensions(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 (App.Events.PregnancyNotice.unreservedTanks() > 0 || fetus.reserve === "incubator") {
			fetusOption.addValue(`Reserve a ${incubatorName} tank`, "incubator", () => {
				fetus.reserve = "incubator";
			});
		}
		if (App.Events.PregnancyNotice.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 = "";
			});
		}
		if (fetus.age < 35) {
			fetusOption.addValue("Ask me again in the 35th week", "lateWait", () => {
				fetus.reserve = "";
			});
		}
		if (canTransplant !== 0) {
			fetusOption.addValue("Transplant Fetus", "transplant", () => {
				fetus.reserve = "";
			});
		}
		if (canTerminate) {
			fetusOption.addValue("Terminate Fetus", "terminate", () => {
				fetus.reserve = "";
			});
		}
		fetusOption.addValue("Do nothing", "nothing", () => {
			fetus.reserve = "";
		});
		fetusOption.addValue("Undecided", "undecided", () => {
			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(App.Events.PregnancyNotice.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(App.Events.PregnancyNotice.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 === "terminate") {
			const options = new App.UI.OptionsGroup();
			options.customRefresh(() => {
				terminateFetus(mother, fetus);
				let jDiv = $(`#fetus-id-${fetus.ID}`);
				jDiv.empty().append("Fetus terminated");
				updateProcessed();
			});
			options.addOption("", "do", {do: false})
				.addValue("Terminate", true);
			fetusDiv.append(options.render());
		}

		if (fetus.noticeData.fate === "transplant") {
			if (canTransplant === -1) {
				let originalMother = (fetus.motherID === SID.PC) ? V.PC : getSlave(fetus.motherID);
				// @ts-expect-error PlayerState not assignable to SlaveState
				const popup = (originalMother.ID === SID.PC) ? 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 transplantDiv = App.UI.DOM.makeElement("div");
				fetusDiv.append(transplantDiv);
				transplantingTool(mother, fetus, transplantDiv, 1, fetusInfo, [fetus, noteSpan], true);
			}
		}

		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;
	}
};


code:
App.Events.PRivalInitiation = class PRivalInitiation extends App.Events.BaseEvent {
	eventPrerequisites() {
		return [
			() => V.rival.state === 3,
			() => !!getSlave(V.rival.ID)
		];
	}

	execute(node) {
		V.rival.state = 5;
		const slave = getSlave(V.rival.ID);
		const {
			He, His,
			he, him, his, himself
		} = getPronouns(slave);
		App.Events.addParagraph(node, [
			`This is a special week, the week of your victory.`,
			App.UI.DOM.slaveDescriptionDialog(slave),
			`awaits your pleasure. You could certainly do to ${him} anything and everything you usually do to your chattel. You could also do something special to mark the occasion.`
		]);
		const choices = [];
		choices.push(new App.Events.Result(`Force ${him} to do a public relations tour with you`, tour));
		choices.push(new App.Events.Result(`Make ${him} orally service your other slaves in public`, serveSlaves));
		if (slave.anus === 0) {
			choices.push(new App.Events.Result(`Break in ${his} asshole and then let the public use it`, breakAss));
		}
		if (slave.balls > 0 && slave.boobs > 300 && V.seeExtreme === 1) {
			choices.push(new App.Events.Result(`Publicly geld ${him}`, geld));
		}
		if (isFertile(slave, !fertKnown(slave)) && !isMenstruating(slave) && V.seePreg === 1) {
			choices.push(new App.Events.Result(`Let the public impregnate ${him}`, publicImpreg));
		}
		App.Events.addResponses(node, choices);

		delete V.rival.ID;

		/** @returns {DocumentFragment} */
		function tour() {
			const frag = new DocumentFragment();
			let r = [];
			r.push(`Though ${he} hates you with all ${his} heart, ${he} knows better than most what happens to slaves who disobey. So, when you describe your public relations plans to ${him}, ${he} promises to obey before you even get to the threats. ${He} finds ${himself} accompanying you to the arcology's finest establishment in a lovely evening dress. The two of you share an understandably quiet meal, with a growing crowd coming to leer at the defeated slaveowner-cum-slave. At a prearranged signal from you, ${he} stands, quickly strips naked, gets down on`);
			if (hasBothLegs(slave)) {
				r.push(`${his} knees,`);
			} else {
				r.push(`the ground,`);
			}
			r.push(`and`);
			if (V.PC.dick !== 0) {
				r.push(`sucks you off`);
				if (V.PC.vagina !== -1) {
					r.push(`and`);
				}
			}
			if (V.PC.vagina !== -1) {
				r.push(`eats you out`);
			}
			r.push(
				r.pop() + ".",
				`Such public humiliation starts ${him} down the path of <span class="hotpink">obedience,</span> and is the <span class="green">talk of the Free Cities.</span>`
			);
			slave.devotion += 4;
			seX(slave, "oral", V.PC);
			repX(2500, "event", slave);
			App.Events.addParagraph(frag, r);
			return frag;
		}

		/** @returns {DocumentFragment} */
		function serveSlaves() {
			const frag = new DocumentFragment();
			let r = [];
			const {
				his2, him2,
			} = getPronouns(getSlaves().atIndex(1).value).appendSuffix("2");
			r.push(`${getSlaves().atIndex(0).value.slaveName} leads ${him} out into the arcology's largest atrium, forces ${him} to`);
			if (hasBothLegs(slave)) {
				r.push(`${his} knees,`);
			} else {
				r.push(`the ground,`);
			}
			r.push(`and, in full view of the whole arcology, orally rapes someone who was until this week a slaveowner ${himself}. Behind them, ${getSlaves().atIndex(1).value.slaveName} is standing ready for ${his2} turn, and all your other slaves stand behind ${him2}. Public opinion is divided; the precedent is universally agreed to be bad, but the punishment is generally thought to be terrible and deserved. Your slaves, however, are almost insufferably pleased with you for forcing ${slave.slaveName}, whom they still view as a slaveowner, to pleasure them.`);
			getSlaves().forEach(s => {
				seX(slave, "oral", s);
				s.devotion += 10;
			});
			App.Events.addParagraph(frag, r);
			return frag;
		}

		/** @returns {DocumentFragment} */
		function breakAss() {
			const frag = new DocumentFragment();
			let r = [];
			r.push(`${slave.slaveName}, who has been fairly dignified up to this point, breaks down when ${he}'s placed in stocks with ${his} ass in the air. ${His} sobs become screams when, for the first time in ${his} life, ${he} feels the burning sensation of a well-lubricated`);
			if (V.PC.dick !== 0) {
				r.push(`cockhead`);
			} else {
				r.push(`strap-on`);
			}
			r.push(`forcing its way past ${his} virgin sphincter. Raping a virgin anus is not a new pleasure for you, but the usual shrieking, struggling and spasming is all the sweeter this time. Half the arcology has used ${his} poor injured butthole by the end of the day; ${he} is learning to fear you, and hates you even more if possible.`);
			slave.devotion -= 4;
			slave.trust -= 5;
			slave.anus = 3;
			seX(slave, "anal", "public", "penetrative", 47);
			repX(2500, "event", slave);
			healthDamage(slave, 10);
			App.Events.addParagraph(frag, r);
			return frag;
		}

		/** @returns {string} */
		function geld() {
			slave.devotion -= 50;
			slave.trust -= 50;
			slave.balls = 0;
			repX(2500, "event", slave);
			healthDamage(slave, 10);
			return `You announce that since ${slave.slaveName} has spent so much money and effort turning ${himself} into a girl with expensive hormones, you'll take a lower-tech step to bring ${him} further in that regard. An autosurgery is set up in public and the populace is treated to the edifying spectacle of a very large pair of testicles being efficiently removed by the modern surgical art. Unusually, ${he} was not given general anesthesia, but instead given local painkillers and made to watch on a monitor, to ${his} rage and horror. There is applause as the cauterizer seals the surgical site where ${his} massive scrotum used to hang. ${His} cock looks softer already.`;
		}

		/** @returns {string} */
		function publicImpreg() {
			if (isFertile(slave)) {
				knockMeUp(slave, 100, 2, SID.CITIZEN);
			}
			slave.devotion -= 15;
			repX(2500, "event", slave);
			const r = [];
			if (slave.mpreg === 1) {
				slave.anus = 3;
				seX(slave, "anal", "public", "penetrative", 47);
			} else {
				slave.vagina = 3;
				seX(slave, "vaginal", "public", "penetrative", 47);
			}
			r.push(`You announce that since ${slave.slaveName} damaged the arcology, ${he} will be taking a leading role in the reconstruction. ${He} will be doing this by replacing one of the residents killed in the violence — by bearing a new slave, to be conceived collectively. The shame and horror of ${his} future as breeding stock comes home to ${him} as ${he}'s restrained in a chair with ${his} legs spread. Soon, the stream of fluids is running down ${his} thoroughly-fucked ${slave.mpreg === 1 ? `ass` : `pussy`} to pool on the floor beneath ${him}.`);
			if (fertKnown(slave) && V.pregnancyKnown === 0) {
				r.push(`Modern medical imaging reveals ${his} fertile ovum's last, losing battle against a legion of sperm in real time, and the images are projected on large screens.`);
			}
			return r.join(` `);
		}
	}
};

repo_name: Nithhogg/fc

license: mit