From 1faafb97d9518416c561be95588608f52f22c6bc Mon Sep 17 00:00:00 2001
From: humungusluver <82497-humungusluver@users.noreply.gitgud.io>
Date: Tue, 4 Feb 2025 20:21:15 -0500
Subject: [PATCH] add experimental fertility cycles

---
 .../backwardsCompatibility/datatypeCleanup.js |  4 +-
 src/data/verification/verifyHumanState.js     |  2 +-
 src/endWeek/nextWeek/nextWeek.js              | 59 ++++++++++++++++-
 .../nursery/nurseryDatatypeCleanup.js         |  2 +-
 src/facilities/surgery/analyzePregnancy.js    |  2 +
 src/gui/options/options.js                    |  4 ++
 src/interaction/siPhysicalRegimen.js          |  2 +
 src/interaction/siWork.js                     |  2 +-
 src/js/pregJS.js                              | 64 ++++++++++---------
 src/js/slaveSummaryWidgets.js                 |  6 +-
 src/js/states/002-HumanState.js               |  2 +-
 src/js/statsChecker/statsChecker.js           |  6 +-
 src/npc/descriptions/style/collar.js          |  6 ++
 src/npc/generate/slaveGenerationJS.js         |  3 +
 14 files changed, 123 insertions(+), 41 deletions(-)

diff --git a/src/data/backwardsCompatibility/datatypeCleanup.js b/src/data/backwardsCompatibility/datatypeCleanup.js
index 853624c865f..f3661528ffd 100644
--- a/src/data/backwardsCompatibility/datatypeCleanup.js
+++ b/src/data/backwardsCompatibility/datatypeCleanup.js
@@ -741,7 +741,7 @@ App.Update.SlaveDatatypeCleanup = (function SlaveDatatypeCleanup() {
 		if (slave.pubertyXX === 0 && slave.ovaries > 0 && slave.preg === -1) {
 			slave.preg = 0; // no contraceptives for prepubescent slaves
 		}
-		slave.fertPeak = Math.clamp(+slave.fertPeak, 0, 4) || 0;
+		slave.fertPeak = Math.clamp(+slave.fertPeak, -10, 10) || 0;
 		slave.broodmother = Math.clamp(+slave.broodmother, 0, 3) || 0;
 		slave.broodmotherFetuses = Math.max(+slave.broodmotherFetuses, 0) || 0;
 		slave.broodmotherOnHold = Math.clamp(+slave.broodmotherOnHold, 0, 1) || 0;
@@ -1563,7 +1563,7 @@ App.Update.PCDatatypeCleanup = (function PCDatatypeCleanup() {
 		if (PC.pubertyXX === 0 && (PC.ovaries > 0 || PC.mpreg > 0) && PC.preg === -1) {
 			PC.preg = 0; // no contraceptives for prepubescent slaves
 		}
-		PC.fertPeak = Math.clamp(+PC.fertPeak, 0, 4) || 0;
+		PC.fertPeak = Math.clamp(+PC.fertPeak, -10, 10) || 0;
 		PC.pregSource = +PC.pregSource || 0;
 		PC.pregMood = Math.clamp(+PC.pregMood, 0, 2) || 0;
 		if (typeof PC.pregControl !== "string") {
diff --git a/src/data/verification/verifyHumanState.js b/src/data/verification/verifyHumanState.js
index 31f28d85f6e..0ca9b779344 100644
--- a/src/data/verification/verifyHumanState.js
+++ b/src/data/verification/verifyHumanState.js
@@ -192,7 +192,7 @@ App.Verify.I.humanPregnancy = (actor, location) => {
 	if (actor.pubertyXX === 0 && (actor.ovaries > 0 || actor.mpreg > 0) && actor.preg === -1) {
 		actor.preg = 0; // no contraceptives for prepubescent humans
 	}
-	actor.fertPeak = Math.clamp(+actor.fertPeak, 0, 4) ?? 0;
+	actor.fertPeak = Math.clamp(+actor.fertPeak, -10, 10) ?? 0;
 	// @ts-expect-error Type 'number' is not assignable to type ...
 	actor.broodmother = Math.clamp(+actor.broodmother, 0, 3) ?? 0;
 	actor.broodmotherFetuses = Math.max(+actor.broodmotherFetuses, 0) ?? 0;
diff --git a/src/endWeek/nextWeek/nextWeek.js b/src/endWeek/nextWeek/nextWeek.js
index 133ab69c041..aeee16bb5fe 100644
--- a/src/endWeek/nextWeek/nextWeek.js
+++ b/src/endWeek/nextWeek/nextWeek.js
@@ -76,7 +76,32 @@ App.EndWeek.nextWeek = function() {
 		}
 	}
 	if (V.menstruation === 1) {
-		// TODO
+		if (V.PC.preg < 0 || (!V.PC.ovaries && !V.PC.mpreg) || V.PC.pubertyXX === 0) {
+			// Handle contraceptives and sterility.
+			V.PC.fertPeak = 1;
+		} else if (V.PC.geneticQuirks.superfetation === 2) {
+			if (V.PC.womb.length > 0) {
+				if (V.PC.fertPeak === 0) {
+					V.PC.fertPeak = 1;
+				}
+				V.PC.fertPeak--;
+			} else {
+				V.PC.fertPeak = 0;
+			}
+		} else if (V.PC.pregWeek < 0) {
+			V.PC.fertPeak = 2;
+		} else {
+			if (V.PC.fertPeak === 0) {
+				// Assume a standard 4 week cycle, 3 weeks fertile 1 week not.
+				V.PC.fertPeak += 3;
+			} else if (V.PC.fertPeak > 0) {
+				V.PC.fertPeak--;
+			}
+			// Fertility drugs will always make next week ovulate.
+			if (V.PC.drugs === "super fertility drugs" || V.PC.drugs === "fertility drugs" ) {
+				V.PC.fertPeak = 0;
+			}
+		}
 	} else if (V.PC.geneticQuirks.superfetation === 2 && V.PC.womb.length > 0) {
 		if (V.PC.fertPeak === 0) {
 			V.PC.fertPeak = 1;
@@ -200,7 +225,37 @@ App.EndWeek.nextWeek = function() {
 			}
 		}
 		if (V.menstruation === 1) {
-			// TODO
+			if (slave.preg < 0 || (!slave.ovaries && !slave.mpreg) || slave.pubertyXX === 0) {
+				slave.fertPeak = 1;
+			} else if (slave.geneticQuirks.superfetation === 2) {
+				// Handle Superfetation
+				if (slave.womb.length > 0) {
+					if (slave.fertPeak === 0) {
+						slave.fertPeak = 1;
+					}
+					slave.fertPeak--;
+				} else {
+					slave.fertPeak = 0;
+				}
+			} else if (slave.preg > 0) {
+				slave.fertPeak = 2;
+			} else if (slave.pregWeek < 0 || slave.preg > 0) {
+				slave.fertPeak = 2;
+			} else {
+				if (slave.fertPeak === 0) {
+					// Assume a standard 4 week cycle
+					slave.fertPeak += 3;
+				} else if (slave.fertPeak > 0) {
+					slave.fertPeak--;
+				} else if (slave.fertPeak < 0) {
+					// This logics lets you go into the negatives. This allows multiple risky weeks in a row.
+					slave.fertPeak++;
+				}
+				// Fertility drugs and diet will always make next week risky.
+				if (slave.drugs === "super fertility drugs" || slave.drugs === "fertility drugs" || slave.diet === "fertility") {
+					slave.fertPeak = 0;
+				}
+			}
 		} else if (slave.geneticQuirks.superfetation === 2 && slave.womb.length > 0) {
 			if (slave.fertPeak === 0) {
 				slave.fertPeak = 1;
diff --git a/src/facilities/nursery/nurseryDatatypeCleanup.js b/src/facilities/nursery/nurseryDatatypeCleanup.js
index f79310c451e..81c362ba03e 100644
--- a/src/facilities/nursery/nurseryDatatypeCleanup.js
+++ b/src/facilities/nursery/nurseryDatatypeCleanup.js
@@ -175,7 +175,7 @@ App.Facilities.Nursery.ChildDatatypeCleanup = function(child) {
 		if (typeof child.pregControl !== "string") {
 			child.pregControl = "none";
 		}
-		child.fertPeak = Math.clamp(+child.fertPeak, 0, 4) || 0;
+		child.fertPeak = Math.clamp(+child.fertPeak, -10, 10) || 0;
 		WombNormalizePreg(child);
 	}
 
diff --git a/src/facilities/surgery/analyzePregnancy.js b/src/facilities/surgery/analyzePregnancy.js
index 8f29becd3cb..3c5198db724 100644
--- a/src/facilities/surgery/analyzePregnancy.js
+++ b/src/facilities/surgery/analyzePregnancy.js
@@ -391,6 +391,8 @@ App.UI.analyzePregnancy = function() {
 		App.UI.DOM.appendNewElement("div", node, `Subject is in the postpartum period, and will regain fertility in ${num(slave.pregWeek * -1)} ${slave.pregWeek === -1 ? `week` : `weeks`}.`);
 	} else if (slave.preg === -1) { // special states
 		App.UI.DOM.appendNewElement("div", node, `Contraceptive agents detected in subject.`);
+	} else if (V.menstruation === 1 && slave.fertPeak > 0 && slave.preg === 0) {
+		App.UI.DOM.appendNewElement("div", node, `${He} is currently in ${his} safe week and not ovulating. ${His} next risky weak will be in ${num(slave.fertPeak)} week${slave.fertPeak > 1 ? 's' : ''}`);
 	}
 
 	return node;
diff --git a/src/gui/options/options.js b/src/gui/options/options.js
index 2c7278ac5bf..3b79aa9402f 100644
--- a/src/gui/options/options.js
+++ b/src/gui/options/options.js
@@ -815,6 +815,10 @@ App.UI.optionsPassage = function() {
 			.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
 			.addComment("This will sort rule assistant output. You may benefit if you have a lot of rules, but only want to look out for a specific portion of it.");
 
+		options.addOption("Slave Fertility Cycles", "menstruation")
+			.addValue("Enabled", 1).on().addValue("Disabled", 0).off()
+			.addComment("Adds slave fertility cycles. Slaves can only get pregnant on their fertile week. Fertility drugs can force slaves into a fertile week.");
+
 		options.addOption("Random slave events repeat control", "level", V.eventControl)
 			.addValue("No control", 0, () => V.eventControl.RIEPerWeek = Math.min(V.eventControl.RIEPerWeek, 3))
 			.addValue("Soft", 2, () => V.eventControl.RIEPerWeek = Math.min(V.eventControl.RIEPerWeek, 3))
diff --git a/src/interaction/siPhysicalRegimen.js b/src/interaction/siPhysicalRegimen.js
index 5b46d6c884c..1a6ab713ee3 100644
--- a/src/interaction/siPhysicalRegimen.js
+++ b/src/interaction/siPhysicalRegimen.js
@@ -415,6 +415,8 @@ App.UI.SlaveInteract.physicalRegimen = function(slave, refresh) {
 					fertility = "using contraceptives";
 				} else if (slave.pregWeek < 0) {
 					fertility = "postpartum";
+				} else if (V.menstruation === 1 && slave.fertPeak > 0 && slave.preg === 0) {
+					fertility = "safe";
 				} else if (slave.preg === 0) {
 					fertility = "fertile";
 				} else if (slave.preg < 4 && (slave.broodmother === 0 || slave.broodmotherOnHold === 1)) {
diff --git a/src/interaction/siWork.js b/src/interaction/siWork.js
index 7065f960799..09a58af3116 100644
--- a/src/interaction/siWork.js
+++ b/src/interaction/siWork.js
@@ -416,7 +416,7 @@ App.UI.SlaveInteract.work = function(slave, refresh) {
 				sexOptions.push({text: `Get a footjob`, scene: () => App.Interact.fFeet(slave)});
 			}
 
-			if (canGetPregnant(slave) && (slave.geneticQuirks.superfetation !== 2 || V.geneticMappingUpgrade !== 0) && (slave.fuckdoll === 0) && V.seePreg !== 0) {
+			if (canGetPregnant(slave) && (slave.geneticQuirks.superfetation !== 2 || V.geneticMappingUpgrade !== 0 || slave.womb.length < 1) && (slave.fuckdoll === 0) && V.seePreg !== 0) {
 				if (canImpreg(slave, V.PC) || canFemImpreg(slave, V.PC)) {
 					sexOptions.push({
 						text: `Impregnate ${him} yourself`,
diff --git a/src/js/pregJS.js b/src/js/pregJS.js
index 6632fa4362e..51c7c62cb93 100644
--- a/src/js/pregJS.js
+++ b/src/js/pregJS.js
@@ -408,22 +408,22 @@ globalThis.knockMeUp = function(target, chance, hole, fatherID) {
 				target.pregType = setPregType(target);
 				WombImpregnate(target, target.pregType, fatherID, 1);
 
-				if (V.menstruation === 1) {
-					//
+
+				target.pregKnown = 1;
+				if (target.ID === -1) {
+					/* r += "<span class="lime">You have gotten pregnant.</span>"; */
 				} else {
-					target.pregKnown = 1;
-					if (target.ID === -1) {
-						/* r += "<span class="lime">You have gotten pregnant.</span>"; */
+					const {He} = getPronouns(target);
+					r += `<span class="lime">${He} has become pregnant.</span>`;
+				}
+				if (V.menstruation === 1) {
+					target.fertPeak = 1;
+				}
+				if (target.geneticQuirks.superfetation === 2 && target.womb.length > 0) {
+					if (V.seeHyperPreg === 1) {
+						target.fertPeak = 1;
 					} else {
-						const {He} = getPronouns(target);
-						r += `<span class="lime">${He} has become pregnant.</span>`;
-					}
-					if (target.geneticQuirks.superfetation === 2 && target.womb.length > 0) {
-						if (V.seeHyperPreg === 1) {
-							target.fertPeak = 1;
-						} else {
-							target.fertPeak = 4;
-						}
+						target.fertPeak = 4;
 					}
 				}
 			}
@@ -488,26 +488,25 @@ globalThis.tryKnockMeUp = function(mother, chance, hole, father) {
 					mother.pregType = setPregType(mother);
 					WombImpregnate(mother, mother.pregType, father.ID, 1);
 
-					if (V.menstruation === 1) {
-						//
+					mother.pregKnown = 1;
+					if (mother.ID === -1) {
+						/* r += "<span class="lime">You have gotten pregnant.</span>"; */
 					} else {
-						mother.pregKnown = 1;
-						if (mother.ID === -1) {
-							/* r += "<span class="lime">You have gotten pregnant.</span>"; */
+						const {He, him} = getPronouns(mother);
+						if (father.ID === -1) {
+							r += `<span class="lime">${He} has become pregnant.</span>`;
 						} else {
-							const {He, him} = getPronouns(mother);
-							if (father.ID === -1) {
-								r += `<span class="lime">${He} has become pregnant.</span>`;
-							} else {
-								r += `<span class="lime">${father.slaveName} has gotten ${him} pregnant.</span>`;
-							}
+							r += `<span class="lime">${father.slaveName} has gotten ${him} pregnant.</span>`;
 						}
-						if (mother.geneticQuirks.superfetation === 2 && mother.womb.length > 0) {
-							if (V.seeHyperPreg === 1) {
-								mother.fertPeak = 1;
-							} else {
-								mother.fertPeak = 4;
-							}
+					}
+					if (V.menstruation === 1) {
+						mother.fertPeak = 1;
+					}
+					if (mother.geneticQuirks.superfetation === 2 && mother.womb.length > 0) {
+						if (V.seeHyperPreg === 1) {
+							mother.fertPeak = 1;
+						} else {
+							mother.fertPeak = 4;
 						}
 					}
 				}
@@ -580,6 +579,9 @@ globalThis.TerminatePregnancy = function(slave) {
 		// very early
 		slave.pregWeek = -1;
 	}
+	if (V.menstruation === 1) {
+		slave.fertPeak = 2;
+	}
 	WombFlush(slave);
 	SetBellySize(slave);
 };
diff --git a/src/js/slaveSummaryWidgets.js b/src/js/slaveSummaryWidgets.js
index e5a25768243..8313043e1f2 100644
--- a/src/js/slaveSummaryWidgets.js
+++ b/src/js/slaveSummaryWidgets.js
@@ -235,7 +235,9 @@ App.UI.SlaveSummaryRenderers = function() {
 				makeSpan(c, "Postpartum", styles);
 			} else if (slave.preg === -1) {
 				makeSpan(c, "CC", styles);
-			} else if (slave.preg === 0 && (slave.ovaries === 1 || slave.mpreg === 1)) {
+			} else if (V.menstruation === 1 && slave.fertPeak > 0 && slave.preg === 0) {
+				makeSpan(c, "Safe", styles);
+			}  else if (slave.preg === 0 && (slave.ovaries === 1 || slave.mpreg === 1)) {
 				makeSpan(c, "Fert+", styles);
 			} else if (((slave.preg < slave.pregData.normalBirth / 10) && (slave.preg > 0) && slave.pregKnown === 0) || slave.pregWeek === 1) {
 				makeSpan(c, "Preg?", styles);
@@ -581,6 +583,8 @@ App.UI.SlaveSummaryRenderers = function() {
 				}
 			} else if (slave.preg === -1) {
 				makeSpan(c, "On contraceptives.", styles);
+			}  else if (V.menstruation === 1 && slave.fertPeak > 0 && slave.preg === 0) {
+				makeSpan(c, "Safe.", styles);
 			} else if (slave.preg === 0 && (slave.ovaries === 1 || slave.mpreg === 1)) {
 				makeSpan(c, "Fertile.", styles);
 			} else if ((slave.preg >= 36) && (slave.broodmother > 0)) {
diff --git a/src/js/states/002-HumanState.js b/src/js/states/002-HumanState.js
index 55c3a203b0e..24843c91ea5 100644
--- a/src/js/states/002-HumanState.js
+++ b/src/js/states/002-HumanState.js
@@ -658,7 +658,7 @@ App.Entity.HumanState = class HumanState extends App.Entity.GenePoolRecord {
 		/**
 		 * Menstrual cycle control variable.
 		 *
-		 * * 0: Danger week
+		 * * 0-: Danger week
 		 * * 1+: safe week
 		 */
 		this.fertPeak = 0;
diff --git a/src/js/statsChecker/statsChecker.js b/src/js/statsChecker/statsChecker.js
index d4d72366a36..78f31773730 100644
--- a/src/js/statsChecker/statsChecker.js
+++ b/src/js/statsChecker/statsChecker.js
@@ -631,10 +631,14 @@ globalThis.isFertile = function(slave) {
 		return false;
 	} else if (slave.mpreg === 1 || slave.ovaries === 1) {
 		if (slave.womb.length > 0) { // superfetation route
-			if (slave.fertPeak !== 0) {
+			if (slave.fertPeak > 0) {
 				return false;
 			}
 		}
+		// Menstruation route.
+		if (V.menstruation === 1 && slave.fertPeak > 0 ) {
+			return false;
+		}
 		return true;
 	}
 	return false;
diff --git a/src/npc/descriptions/style/collar.js b/src/npc/descriptions/style/collar.js
index 953462bb634..84a5c09801a 100644
--- a/src/npc/descriptions/style/collar.js
+++ b/src/npc/descriptions/style/collar.js
@@ -37,6 +37,8 @@ App.Desc.collar = function(slave) {
 						r.push(`"Womb not detected!"`);
 					} else if ((slave.ovaries > 0 || slave.mpreg > 0) && slave.pubertyXX === 0 && slave.physicalAge < V.fertilityAge) {
 						r.push(`"I'm not old enough to get pregnant yet!"`);
+					} else if (V.menstruation === 1 && slave.fertPeak > 0) {
+						r.push(`"I'm not ovulating right now!"`);
 					} else {
 						r.push(`"I'm infertile!"`);
 					}
@@ -73,6 +75,8 @@ App.Desc.collar = function(slave) {
 						}
 					} else if (isFertile(slave)) {
 						r.push(`"My womb needs filling!"`);
+					} else if (V.menstruation === 1 && slave.fertPeak > 0) {
+						r.push(`"My next risky period is in ${num(slave.fertPeak)} week${slave.fertPeak > 1 ? 's' : ''}!"`);
 					} else if ((slave.ovaries > 0 || slave.mpreg > 0) && slave.pubertyXX === 0) {
 						r.push(`"I should be fertile`);
 						if (slave.pubertyAgeXX - slave.physicalAge > 2) {
@@ -106,6 +110,8 @@ App.Desc.collar = function(slave) {
 						r.push(`"Put a baby in me today!"`);
 					} else if ((slave.ovaries > 0 || slave.mpreg > 0) && slave.pubertyXX === 0 && slave.physicalAge < V.fertilityAge) {
 						r.push(`"I'm too young to get pregnant!"`);
+					} else if (V.menstruation === 1 && slave.fertPeak > 0) {
+						r.push(`"This is a safe week!"`);
 					} else {
 						r.push(`"Try to knock me up some other time!"`);
 					}
diff --git a/src/npc/generate/slaveGenerationJS.js b/src/npc/generate/slaveGenerationJS.js
index 71ca61bbf84..99d1063db4e 100644
--- a/src/npc/generate/slaveGenerationJS.js
+++ b/src/npc/generate/slaveGenerationJS.js
@@ -1443,6 +1443,9 @@ globalThis.generatePronouns = function(slave) {
 globalThis.generatePuberty = function(slave) {
 	if (((slave.ovaries === 1 || slave.mpreg === 1) && slave.physicalAge >= slave.pubertyAgeXX) || slave.pubertyXX === 1) {
 		slave.pubertyXX = 1;
+		if (V.menstruation === 1 && slave.geneticQuirks.superfetation < 2) {
+			slave.fertPeak = jsRandom(0, 3);
+		}
 	} else {
 		if (slave.preg >= -1) {
 			slave.preg = 0;
-- 
GitLab