From f2b421495a39b03f92b23db98476baffefb8ba72 Mon Sep 17 00:00:00 2001
From: ezsh <ezsh.junk@gmail.com>
Date: Sat, 4 May 2024 23:04:03 +0200
Subject: [PATCH] RA: add option to control nipples

---
 devTools/types/FC/RA.d.ts       |  1 +
 devTools/types/FC/human.d.ts    | 10 ++--
 js/003-data/constants.js        |  8 +--
 src/002-config/fc-version.js    |  2 +-
 src/js/DefaultRules.js          | 90 ++++++++++++++++++++++++---------
 src/js/rulesAssistant.js        |  1 +
 src/js/rulesAssistantOptions.js | 17 ++++++-
 7 files changed, 94 insertions(+), 35 deletions(-)

diff --git a/devTools/types/FC/RA.d.ts b/devTools/types/FC/RA.d.ts
index 17c7e6cf48e..ac03fb5f016 100644
--- a/devTools/types/FC/RA.d.ts
+++ b/devTools/types/FC/RA.d.ts
@@ -93,6 +93,7 @@ declare namespace FC {
 			dick: ExpressiveNumericTarget;
 			balls: ExpressiveNumericTarget;
 			clit: ExpressiveNumericTarget;
+			nipples: NippleShape[];
 			intensity: number;
 		}
 
diff --git a/devTools/types/FC/human.d.ts b/devTools/types/FC/human.d.ts
index 91bb4871c77..81593fb6dd0 100644
--- a/devTools/types/FC/human.d.ts
+++ b/devTools/types/FC/human.d.ts
@@ -497,7 +497,7 @@ declare global {
 		type Markings = WithNone<"beauty mark" | "birthmark" | "freckles" | "heavily freckled">;
 		type TailShape = WithNone<"cat" | "leopard" | "tiger" | "jaguar" | "lion" | "dog" | "wolf" | "jackal" | "fox" | "kitsune" | "tanuki" | "raccoon" | "rabbit" | "squirrel" | "horse" | "bird" | "phoenix" | "peacock" | "raven" | "swan" | "sheep" | "cow" | "gazelle" | "deer" | "succubus" | "dragon" >;
 		type WingsShape = WithNone<"angel" | "seraph" | "demon"| "dragon" | "phoenix" | "bird"| "fairy" | "butterfly" | "moth" | "insect" | "evil" >;
-		
+
 		type ToyHole = "all her holes" | "mouth" | "boobs" | "pussy" | "ass" | "dick";
 		interface ToyHoleFreeze extends Record<string, ToyHole> {
 			ALL: "all her holes";
@@ -518,14 +518,14 @@ declare global {
 
 		type NippleShape = "huge" | "puffy" | "inverted" | "tiny" | "cute" | "partially inverted" | "fuckable" | "flat";
 		interface NippleShapeFreeze extends Record<string, NippleShape> {
-			HUGE: "huge";
-			PUFFY: "puffy";
 			INVERTED: "inverted";
+			PARTIAL: "partially inverted";
+			FLAT: "flat";
 			TINY: "tiny";
 			CUTE: "cute";
-			PARTIAL: "partially inverted";
+			PUFFY: "puffy";
+			HUGE: "huge";
 			FUCKABLE: "fuckable";
-			FLAT: "flat";
 		}
 
 		type LactationType = 0 | 1 | 2;
diff --git a/js/003-data/constants.js b/js/003-data/constants.js
index da40d9daac3..c3cace50ab8 100644
--- a/js/003-data/constants.js
+++ b/js/003-data/constants.js
@@ -391,14 +391,14 @@ globalThis.OvaryImplantType = Object.freeze({
  * @enum {string}
  */
 globalThis.NippleShape = Object.freeze({
-	HUGE: "huge",
-	PUFFY: "puffy",
 	INVERTED: "inverted",
+	PARTIAL: "partially inverted",
+	FLAT: "flat",
 	TINY: "tiny",
 	CUTE: "cute",
-	PARTIAL: "partially inverted",
+	PUFFY: "puffy",
+	HUGE: "huge",
 	FUCKABLE: "fuckable",
-	FLAT: "flat",
 });
 
 /**
diff --git a/src/002-config/fc-version.js b/src/002-config/fc-version.js
index 5ef16aebe97..14f201392c5 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.31",
 	commitHash: null,
-	release: 1249, // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js.
+	release: 1250, // 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/js/DefaultRules.js b/src/js/DefaultRules.js
index ebce434bf6a..5e44c3c4a73 100644
--- a/src/js/DefaultRules.js
+++ b/src/js/DefaultRules.js
@@ -1216,61 +1216,62 @@ globalThis.DefaultRules = function(slave, options) {
 			ProcessOtherDrugs(slave, rule);
 			return;
 		} else if (
-			[
+			([
 				rule.growth.boobs,
 				rule.growth.butt,
 				rule.growth.lips,
 				rule.growth.dick,
 				rule.growth.clit,
 				rule.growth.balls
-			].every(r => r === null) // Check if all objects in list equal null
+			].every(r => r === null) && !rule.growth.nipples.length) // Check if all objects in list equal null
 		) {
 			ProcessOtherDrugs(slave, rule);
 			return;
 		}
 
 		// Asset growth/shrink
-		const sizingDrugs = new Set(["breast injections", "breast redistributors", "butt injections", "butt redistributors", "hyper breast injections", "hyper butt injections", "hyper penis enhancement", "hyper testicle enhancement", "intensive breast injections", "intensive butt injections", "intensive penis enhancement", "intensive testicle enhancement", "lip atrophiers", "lip injections", "penis atrophiers", "penis enhancement", "testicle atrophiers", "testicle enhancement", "clitoris enhancement", "intensive clitoris enhancement"]);
+		/** @type {Set<FC.Drug>} */
+		const sizingDrugs = new Set([Drug.GROWBREAST, Drug.REDISTBREAST, Drug.HYPERBREAST, Drug.GROWBUTT, Drug.REDISTBUTT, Drug.HYPERBUTT, Drug.HYPERPENIS, Drug.HYPERTESTICLE, Drug.INTENSIVEBREAST, Drug.INTENSIVEBUTT, Drug.INTENSIVEPENIS, Drug.INTENSIVETESTICLE, Drug.ATROPHYLIP, Drug.GROWLIP, Drug.ATROPHYPENIS, Drug.GROWPENIS, Drug.ATROPHYTESTICLE, Drug.GROWTESTICLE, Drug.GROWCLIT, Drug.INTENSIVECLIT, Drug.GROWNIPPLE, Drug.ATROPHYNIPPLE]);
 
 		// NOTE: property names in growDrugs, and shrinkDrugs must be identical and this fact is used by the drugs() below
 		/** @type {Record<FC.SizableBodyPart, FC.Drug>} */
 		const growDrugs = {
-			lips: "lip injections",
-			boobs: "breast injections",
-			butt: "butt injections",
+			lips: Drug.GROWLIP,
+			boobs: Drug.GROWBREAST,
+			butt: Drug.GROWBUTT,
 			clit: null,
 			dick: null,
 			balls: null
 		};
 
 		if (slave.dick > 0) {
-			growDrugs.dick = "penis enhancement";
+			growDrugs.dick = Drug.GROWPENIS;
 		} else if (slave.vagina >= 0) {
-			growDrugs.clit = "clitoris enhancement";
+			growDrugs.clit = Drug.GROWCLIT;
 		}
 		if (slave.balls > 0) {
-			growDrugs.balls = "testicle enhancement";
+			growDrugs.balls = Drug.GROWTESTICLE;
 		}
 
 		if (V.arcologies[0].FSAssetExpansionistResearch === 1 && rule.hyper_drugs === 1) {
-			growDrugs.boobs = "hyper breast injections";
-			growDrugs.butt = "hyper butt injections";
+			growDrugs.boobs = Drug.HYPERBREAST;
+			growDrugs.butt = Drug.HYPERBUTT;
 			if (slave.dick > 0) {
-				growDrugs.dick = "hyper penis enhancement";
+				growDrugs.dick = Drug.HYPERPENIS;
 			}
 			if (slave.balls > 0) {
-				growDrugs.balls = "hyper testicle enhancement";
+				growDrugs.balls = Drug.HYPERTESTICLE;
 			}
 		} else if (rule.growth.intensity && slave.indentureRestrictions < 2 && slave.health.condition > 0) {
-			growDrugs.boobs = "intensive breast injections";
-			growDrugs.butt = "intensive butt injections";
+			growDrugs.boobs = Drug.INTENSIVEBREAST;
+			growDrugs.butt = Drug.INTENSIVEBUTT;
 			if (slave.dick > 0) {
-				growDrugs.dick = "intensive penis enhancement";
+				growDrugs.dick = Drug.INTENSIVEPENIS;
 			} else if (slave.vagina >= 0) {
-				growDrugs.clit = "intensive clitoris enhancement";
+				growDrugs.clit = Drug.INTENSIVECLIT;
 			}
 			if (slave.balls > 0) {
-				growDrugs.balls = "intensive testicle enhancement";
+				growDrugs.balls = Drug.INTENSIVETESTICLE;
 			}
 		}
 
@@ -1285,18 +1286,18 @@ globalThis.DefaultRules = function(slave, options) {
 		};
 
 		if (V.arcologies[0].FSSlimnessEnthusiastResearch === 1) {
-			shrinkDrugs.lips = "lip atrophiers";
+			shrinkDrugs.lips = Drug.ATROPHYLIP;
 			if (slave.dick > 0) {
-				shrinkDrugs.dick = "penis atrophiers";
+				shrinkDrugs.dick = Drug.ATROPHYPENIS;
 			} else if (slave.vagina >= 0) {
-				shrinkDrugs.clit = "clitoris atrophiers";
+				shrinkDrugs.clit = Drug.ATROPHYCLIT;
 			}
 			if (slave.balls > 0) {
-				shrinkDrugs.balls = "testicle atrophiers";
+				shrinkDrugs.balls = Drug.ATROPHYTESTICLE;
 			}
 			if (slave.weight < 100) {
-				shrinkDrugs.boobs = "breast redistributors";
-				shrinkDrugs.butt = "butt redistributors";
+				shrinkDrugs.boobs = Drug.REDISTBREAST;
+				shrinkDrugs.butt = Drug.REDISTBUTT;
 			}
 		}
 
@@ -1346,6 +1347,46 @@ globalThis.DefaultRules = function(slave, options) {
 			}
 		}
 
+		/**
+		 * @param {FC.SlaveState} slave
+		 * @param {FC.NippleShape[]} target
+		 * @param {{drug: FC.Drug, weight: number, source:string}[]} priorities
+		 * @param {object} source
+		 */
+		function nippleDrugs(slave, target, priorities, source) {
+			if (!target.length || target.includes(slave.nipples)) { return; }
+
+			/** Assign sizes artificially
+			 *  @type {Record<FC.NippleShape, number>} */
+			const sizes = {
+				flat: 0,
+				tiny: 1,
+				cute: 2,
+				puffy: 3,
+				huge: 4,
+				// the next ones are not directly reachable by drugs
+				"partially inverted": NaN,
+				inverted: NaN,
+				fuckable: NaN,
+			};
+
+			if (Number.isNaN(sizes[slave.nipples])) { return; }
+			// target is sorted according to the sizes above, and target does not include slave.nipples
+
+			const curSize = sizes[slave.nipples];
+			for (const tgt of target) {
+				const tgtSize = sizes[tgt];
+				if (curSize < tgtSize) {
+					priorities.push({drug: Drug.GROWNIPPLE, weight: 1.0 - (curSize / tgtSize) , source});
+					return;
+				}
+				if (sizes[slave.nipples] > sizes[tgt]) {
+					priorities.push({drug: Drug.ATROPHYNIPPLE, weight: curSize / tgtSize - 1.0, source});
+					return;
+				}
+			}
+		}
+
 		/** @type {{drug: FC.Drug, weight: number, source:string}[]} */
 		let priorities = [];
 		drugs(slave, "boobs", rule.growth.boobs, priorities, 200, sourceRecord.growth.boobs);
@@ -1354,6 +1395,7 @@ globalThis.DefaultRules = function(slave, options) {
 		drugs(slave, "dick", rule.growth.dick, priorities, 1, sourceRecord.growth.dick);
 		drugs(slave, "clit", rule.growth.clit, priorities, 1, sourceRecord.growth.clit);
 		drugs(slave, "balls", rule.growth.balls, priorities, 1, sourceRecord.growth.balls);
+		nippleDrugs(slave, rule.growth.nipples, priorities, sourceRecord.growth.nipples);
 
 		if (priorities.length > 0) {
 			const action = priorities.reduce((acc, cur) => (acc.weight > cur.weight) ? acc : cur);
diff --git a/src/js/rulesAssistant.js b/src/js/rulesAssistant.js
index 457cf2214b7..073a5de0ac6 100644
--- a/src/js/rulesAssistant.js
+++ b/src/js/rulesAssistant.js
@@ -333,6 +333,7 @@ App.RA.newRule = function() {
 			dick: null,
 			balls: null,
 			clit: null,
+			nipples: [],
 			intensity: 0
 		};
 	}
diff --git a/src/js/rulesAssistantOptions.js b/src/js/rulesAssistantOptions.js
index 49c972d65de..836238debaa 100644
--- a/src/js/rulesAssistantOptions.js
+++ b/src/js/rulesAssistantOptions.js
@@ -2101,7 +2101,7 @@ App.RA.options = (function() {
 				this.lips = new ExprLipGrowthList();
 				this.clits = new ExprClitGrowthList();
 			}
-			this.sublists.push(this.breasts, this.butts, this.lips, this.clits);
+			this.sublists.push(new NippleGrowthList(), this.breasts, this.butts, this.lips, this.clits);
 
 			if (V.seeDicks > 0 || V.makeDicks > 0) {
 				if (!V.experimental.raGrowthExpr) {
@@ -2261,6 +2261,21 @@ App.RA.options = (function() {
 		}
 	}
 
+	class NippleGrowthList extends MultiListSelector {
+		constructor(label, target) {
+			/** @type {Array<[string, FC.NippleShape]>} */
+			const items = [
+				["Tiny", NippleShape.TINY],
+				["Cute", NippleShape.CUTE],
+				["Puffy", NippleShape.PUFFY],
+				["Huge", NippleShape.HUGE]
+			];
+			super("Nipple shape", items);
+			this.setValue(current_rule.set.growth.nipples);
+			this.onchange = (value) => current_rule.set.growth.nipples = value;
+		}
+	}
+
 	class ExprBreastGrowthList extends ExpressiveNumericTargetEditor {
 		constructor() {
 			const pairs = [
-- 
GitLab