diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19bd80bd30a44f93b3f3715fc8cade24e42586a3..6174b56f77ce9c0e34a8cc93e79288d87bc10458 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 
 ## Unreleased
 
+* Cosmetic implant surgeries reworked to be more realistic: implant size that can be fit in is dynamically computed based on asset size, players don't need to begin with the smallest size anymore if a bigger fits. Cosmetic implant surgeries now cost a lot more (beware, autosurgery is also affected!) to compensate for achieving the grow targets quickly. Autosurgery can be tuned to use only specific set of implant types and its note in the end week logs became more detailed and visually detectable.
+
 ## 0.10.7.1-4.0.0-alpha.12 - 2022-01-06
 
 * Sugarcube updated to 2.36.1
diff --git a/css/interaction/slaveInteract.css b/css/interaction/slaveInteract.css
index 8050ac2aea95201e974c5a3c031d8b788c1e719c..403f30d2892fa168071b423359ded448114626f9 100644
--- a/css/interaction/slaveInteract.css
+++ b/css/interaction/slaveInteract.css
@@ -9,3 +9,11 @@
 .si-header {
 	font-size: 1.5em;
 }
+
+/* notification */
+a.with-note::after /*notification for links with notes in tooltips */
+{
+	font-family: "tme-fa-icons";
+	content: " \e80c";
+	color: yellow;
+}
diff --git a/devTools/types/FC/RA.d.ts b/devTools/types/FC/RA.d.ts
index 43fb919675fbddf64b43500873f0b9263fc53932..a771bac04b26912b7ce2018e7672581060b64bab 100644
--- a/devTools/types/FC/RA.d.ts
+++ b/devTools/types/FC/RA.d.ts
@@ -1,19 +1,12 @@
 declare namespace FC {
 	namespace RA {
-		interface NumericTarget {
-			cond: "==" | ">=" | "<=" | ">" | "<";
-			val: number;
-		}
 
-		interface ExpressiveNumericTarget {
-			cond: string;
-			val: string | number;
-		}
-
-		interface NumericRange {
-			min: number;
-			max: number;
+		interface GenericNumericTarget<T> {
+			cond: "==" | ">=" | "<=" | ">" | "<";
+			val: T;
 		}
+		type NumericTarget = GenericNumericTarget<number>;
+		type ExpressiveNumericTarget = GenericNumericTarget<number | string>;
 
 		interface RuleConditions {
 			function: boolean | string;
@@ -38,10 +31,12 @@ declare namespace FC {
 			shouldersImplant: number;
 			boobs: NumericRange;
 			boobsImplantTypes: SizingImplantType[];
+			boobsImplantAllowReplacing: boolean;
 			hips: number;
 			hipsImplant: number;
 			butt: NumericRange;
 			buttImplantTypes: SizingImplantType[];
+			buttImplantAllowReplacing: boolean;
 			faceShape: FaceShape;
 			lips: NumericRange;
 			holes: number;
@@ -142,7 +137,7 @@ declare namespace FC {
 			weight: NumericRange;
 			diet: string;
 			dietCum: number;
-			dietMilk: number;
+			dietMilk: FC.dietMilkType;
 			onDiet: number;
 			muscles: NumericTarget;
 			XY: number;
diff --git a/devTools/types/FC/human.d.ts b/devTools/types/FC/human.d.ts
index 4fef5bce151f3df84590e2a911044ce8fdae2216..8cc3ae8aad6c0a78509c471cac70dbf6f6292dfd 100644
--- a/devTools/types/FC/human.d.ts
+++ b/devTools/types/FC/human.d.ts
@@ -311,8 +311,22 @@ declare global {
 		type Piercing = "ear" | "nose" | "eyebrow" | "lips" | "tongue" | "nipple" | "areola" | "navel" | "corset" | "genitals" | "vagina" | "dick" | "anus";
 		type Race = "amerindian" | "asian" | "black" | "indo-aryan" | "latina" | "malay" | "middle eastern" | "mixed race" |
 			"pacific islander" | "catgirl" | "semitic" | "southern european" | "white";
+
 		type SizingImplantType = "normal" | "string" | "fillable" | "advanced fillable" | "hyper fillable";
+		type LipsImplantType = "normal";
 		type InstalledSizingImplantType = WithNone<SizingImplantType>;
+		type InstalledLipsImplantType = WithNone<LipsImplantType>;
+		type SizableBodyPart = "lips" | "boobs" | "butt" | "dick" | "balls";
+		type SizingImplantTarget = "boobs" | "butt" | "lips";
+
+		interface BodyPartImplantTypeMap {
+			lips: LipsImplantType;
+			boobs: SizingImplantType;
+			butt: SizingImplantType;
+		}
+
+		type BodyPartInstalledImplantType<Target extends SizingImplantTarget> = WithNone<BodyPartImplantTypeMap[Target]>;
+
 		type SmartPiercingSetting = WithNone<"off" | "all" | "no default setting" | "random" | "women" | "men" | "vanilla" | "oral" | "anal" |
 			"boobs" | "submissive" | "humiliation" | "pregnancy" | "dom" | "masochist" | "sadist" | "anti-women" | "anti-men">;
 		type TeethType = "normal" | "crooked" | "gapped" | "straightening braces" | "cosmetic braces" | "removable" | "pointy" |
@@ -479,35 +493,6 @@ declare global {
 
 		type HumanState = SlaveState | PlayerState;
 
-		export namespace Medicine {
-			export namespace Surgery {
-				export interface SizingOptions {
-					/** include possible augmentation procedures */
-					augmentation?: boolean;
-					/** include possible reduction procedures */
-					reduction?: boolean;
-					/** include implant change options */
-					replace?: boolean;
-					/** allowed implant types */
-					types?: InstalledSizingImplantType[];
-					/**
-					 * If true, unlisted in the types array implants will be
-					 * disallowed completely, including fill/drain operations
-					 */
-					strictTypes?: boolean;
-				}
-			}
-			export namespace OrganFarm {
-				interface GrowingOrgan {
-					type: string;
-					weeksToCompletion: number;
-					ID: number;
-				}
-				export namespace Organs {
-				}
-			}
-		}
-
 		type ImageFormat = "png"|"jpg"|"gif"|"webp"|"webm"|"mp4";
 		export interface CustomImage {
 			filename?: string;
diff --git a/devTools/types/FC/medicine.d.ts b/devTools/types/FC/medicine.d.ts
index 8c2bae4ca46436193bafe16eb5993f1b9a904a35..620f6a48e6b1dbe94c39360a934c5c953ade837d 100644
--- a/devTools/types/FC/medicine.d.ts
+++ b/devTools/types/FC/medicine.d.ts
@@ -1,45 +1,165 @@
+
 declare namespace FC {
-    type prostheticID = "interfaceP1" | "interfaceP2" | "interfaceP3" | "basicL" | "sexL" | "beautyL" | "combatL" | "felidaeL" | "canidaeL" | "felidaeCL" | "canidaeCL" | "cyberneticL" 
-    | "ocular" | "cochlear" | "electrolarynx" | "interfaceTail" | "modT" | "sexT" | "combatT" | "combatT2" |/* "erectile" |*/ "interfaceBack" | "modW" | "flightW" 
-    | "sexA" | "combatW" | "combatA1" | "combatA2";
-
-    type prostheticName = "basic prosthetic interface" | "advanced prosthetic interface" | "quadrupedal prosthetic interface" | "set of basic prosthetic limbs" | "set of advanced sex limbs" 
-    | "set of advanced beauty limbs" | "set of advanced combat limbs" | "set of quadruped feline limbs" | "set of quadruped canine limbs" | "set of feline combat limbs" 
-    | "set of canine combat limbs" | "set of cybernetic limbs" | "ocular implant" | "cochlear implant" | "electrolarynx" | "prosthetic tail interface" | "modular tail" 
-    | "pleasure tail" | "combat tail" | `combat tail, type "Stinger"` | "prosthetic back interface" | "modular pair of wings" | "pair of flight capable wings" 
-    | "set of pleasure appendages" | `"set of combat appendages, type "Falcon"` | `set of combat appendages, type "Arachnid"` | `set of combat appendages, type "Kraken"`;
-
-
-    type LimbArgument = "left arm" | "right arm" | "left leg" | "right leg"
-    type LimbArgumentAll = "all" | LimbArgument
-
-    type BodySide = "left" | "right"
-    type BodySideAll = "both" | BodySide
-
-    interface AdjustProsthetics {
-        id: prostheticID;
-        workLeft: number;
-        slaveID: number;
-    }
-
-    type TaskType = "research" | "craft" | "craftFit"; 
-
-    interface Tasks {
-            type: TaskType;
-            id: prostheticID;
-            workLeft: number;
-            craftFit?: number;
-        }
-
-    interface ResearchLab {
-            level: number;
-            aiModule: number;
-            tasks: Tasks;
-            maxSpace: number;
-            hired: number;
-            menials: number;
-    }
-   
-}
+	type prostheticID = "interfaceP1" | "interfaceP2" | "interfaceP3" | "basicL" | "sexL" | "beautyL" | "combatL" | "felidaeL" | "canidaeL" | "felidaeCL" | "canidaeCL" | "cyberneticL"
+		| "ocular" | "cochlear" | "electrolarynx" | "interfaceTail" | "modT" | "sexT" | "combatT" | "combatT2" |/* "erectile" |*/ "interfaceBack" | "modW" | "flightW"
+		| "sexA" | "combatW" | "combatA1" | "combatA2";
+
+	type prostheticName = "basic prosthetic interface" | "advanced prosthetic interface" | "quadrupedal prosthetic interface" | "set of basic prosthetic limbs" | "set of advanced sex limbs"
+		| "set of advanced beauty limbs" | "set of advanced combat limbs" | "set of quadruped feline limbs" | "set of quadruped canine limbs" | "set of feline combat limbs"
+		| "set of canine combat limbs" | "set of cybernetic limbs" | "ocular implant" | "cochlear implant" | "electrolarynx" | "prosthetic tail interface" | "modular tail"
+		| "pleasure tail" | "combat tail" | `combat tail, type "Stinger"` | "prosthetic back interface" | "modular pair of wings" | "pair of flight capable wings"
+		| "set of pleasure appendages" | `"set of combat appendages, type "Falcon"` | `set of combat appendages, type "Arachnid"` | `set of combat appendages, type "Kraken"`;
+
+
+	type LimbArgument = "left arm" | "right arm" | "left leg" | "right leg"
+	type LimbArgumentAll = "all" | LimbArgument
+
+	type BodySide = "left" | "right"
+	type BodySideAll = "both" | BodySide
+
+	interface AdjustProsthetics {
+		id: prostheticID;
+		workLeft: number;
+		slaveID: number;
+	}
+
+	type TaskType = "research" | "craft" | "craftFit";
+
+	interface Tasks {
+		type: TaskType;
+		id: prostheticID;
+		workLeft: number;
+		craftFit?: number;
+	}
+
+	interface ResearchLab {
+		level: number;
+		aiModule: number;
+		tasks: Tasks;
+		maxSpace: number;
+		hired: number;
+		menials: number;
+	}
+
+	namespace Data.Medicine.SizingImplants {
+		type DynamicName = (volume: number) => string;
+		type DynamicCostFactor = (volume: number) => number;
+
+		interface FillDrainData {
+			limit: number;
+			step: number[];
+			healthCost: number;
+			materialCostFactor: number;
+		}
+		interface ImplantType {
+			name: string | DynamicName;
+			specificMaterialCost: number | DynamicCostFactor;
+			workCostFactor: {
+				installation: number;
+				removal: number;
+			}
+			healthCostFactor: {
+				installation: number;
+				removal: number;
+			},
+			availableSizes: number[] | (() => number[]);
+			/** If defined the implant is fillable and can be filled up to the given size */
+			fill?: FillDrainData;
+			/** If defined the implant is drainable and can be drained down to the given size */
+			drain?: FillDrainData;
+		}
 
- 
\ No newline at end of file
+		type SizingImplantDatabase = {
+			[K in SizingImplantTarget]: Record<BodyPartImplantTypeMap[K], FC.Data.Medicine.SizingImplants.ImplantType>;
+		}
+	}
+
+	namespace Medicine {
+
+		type ProcedureInstance = InstanceType<typeof App.Medicine.Surgery.Procedure>;
+		type SizingImplantProcedure = InstanceType<typeof App.Medicine.Surgery.Procedures.SizingImplantProcedure>;
+
+		type SizingImplantProcedureConstructor<T extends SizingImplantProcedure = SizingImplantProcedure> = new (...args: any[]) => T;
+
+		interface ImplantProcedureCreators {
+			install: (slave: FC.SlaveState, implantType: SizingImplantType, volume: number, fleshExcess: number) => ProcedureInstance;
+			remove: (slave: FC.SlaveState) => ProcedureInstance;
+			replace: (slave: FC.SlaveState, implantType: SizingImplantType, volume: number, fleshExcess: number) => ProcedureInstance;
+
+			fill: (slave: FC.SlaveState, amount: number) => ProcedureInstance;
+			drain: (slave: FC.SlaveState, amount: number) => ProcedureInstance;
+		}
+
+		type ImplantInfo<Target extends SizingImplantTarget> = {
+			type: BodyPartInstalledImplantType<Target>;
+			volume: number;
+		}
+
+		interface AssetSizingProcedureSet extends ImplantProcedureCreators {
+			reduce: (slave: SlaveState, name: string, amount: number) => ProcedureInstance;
+		}
+
+		namespace Surgery {
+			/**
+			 * Describes surgical procedure
+			 */
+			interface Procedure {
+				/**
+				 * Type code that identifies this kind of procedure.
+				 * Currently unused, but planned for future use by RA for prioritizing procedures
+				 */
+				typeId: string;
+				/**
+				 * Short label for the procedure. Can be used as a link text.
+				 */
+				label: string;
+				/**
+				 * If procedure is targeted at changing object characteristic, this is the net change (signed)
+				 */
+				targetEffect: number;
+				/**
+				 * Description of the procedure, more or less detailed
+				 */
+				description: string;
+				/**
+				 * Money costs (positive when you pay for it)
+				 */
+				costs: number;
+				/**
+				 * Projected health loss (positive when health decreases)
+				 */
+				healthCosts: number;
+				/**
+				 * Function to perform the procedure
+				 * If action is undefined, the procedure can't be applied (and .description contains the reason)
+				 */
+				action: slaveOperation;
+				/**
+				 * surgery type for passages like "Surgery Degradation"
+				 */
+				surgeryType: string;
+			}
+
+			type SizingOptions<Target extends SizingImplantTarget> = {
+				targetSize?: NumericRange;
+				/**
+				 * Allowed procedures for given implant type
+				 */
+				allowedTypes?: Set<BodyPartInstalledImplantType<Target>>;
+				/** include options with implant change */
+				replace?: boolean;
+			}
+
+			type DefinitiveSizedOptions<Target extends SizingImplantTarget> = Required<SizingOptions<Target>>;
+		}
+		namespace OrganFarm {
+			interface GrowingOrgan {
+				type: string;
+				weeksToCompletion: number;
+				ID: number;
+			}
+			namespace Organs {
+			}
+		}
+	}
+}
diff --git a/devTools/types/FC/util.d.ts b/devTools/types/FC/util.d.ts
index 5dd8757958ccef4ca9823b5f3c38adaa4c9c415c..3ed3029b8ef312d7d6465dbab41f8009f3ada656 100644
--- a/devTools/types/FC/util.d.ts
+++ b/devTools/types/FC/util.d.ts
@@ -12,5 +12,10 @@ declare namespace FC {
         }
 
         type DiffRecorder<T> = T & DiffBase<T>
-    }
+	}
+
+	interface NumericRange {
+		min: number;
+		max: number;
+	}
 }
diff --git a/devTools/types/extensions.d.ts b/devTools/types/extensions.d.ts
index d166c6b86673fd880061c75dd374d1ce86f31126..07c495c194f1cb1f82c91c0135b92ea8b7d30e03 100644
--- a/devTools/types/extensions.d.ts
+++ b/devTools/types/extensions.d.ts
@@ -13,11 +13,11 @@ interface Number {
 	 isBetween(min: number, max: number, inclusive?: boolean): boolean;
 }
 
-type ActualPropertyKey<T extends PropertyKey> = T extends number ? string : T;
+type EnumerablePropertyKey<T extends PropertyKey> = T extends symbol ? never : (T extends number ? string : T);
 
 interface ObjectConstructor {
-	keys<K extends PropertyKey, V>(o: Partial<Record<K, V>>): ActualPropertyKey<K>[];
-	entries<K extends PropertyKey, V>(o: Partial<Record<K, V>>): [ActualPropertyKey<K>, V][];
+	keys<K extends PropertyKey, V>(o: Partial<Record<K, V>>): EnumerablePropertyKey<K>[];
+	entries<K extends PropertyKey, V>(o: Partial<Record<K, V>>): [EnumerablePropertyKey<K>, V][];
 }
 
 // d3-dtree
diff --git a/js/002-config/fc-js-init.js b/js/002-config/fc-js-init.js
index 12eccefaacb2ffe8fe0924da2ff80f5be88e0a43..19f9622b9982a3b0ca38ffcad1a9503e0b88e761 100644
--- a/js/002-config/fc-js-init.js
+++ b/js/002-config/fc-js-init.js
@@ -20,6 +20,7 @@ App.Corporate = {};
 App.Data = {};
 App.Data.FCTV = {};
 App.Data.HeroSlaves = {};
+App.Data.Medicine = {};
 App.Data.Policies = {};
 App.Data.Policies.Selection = {};
 App.Data.SecExp = {};
diff --git a/js/003-data/medicine/implants.js b/js/003-data/medicine/implants.js
new file mode 100644
index 0000000000000000000000000000000000000000..0c0c000f370d7eb952d00a648cd1ddff677b03a2
--- /dev/null
+++ b/js/003-data/medicine/implants.js
@@ -0,0 +1,256 @@
+/** @type {FC.Data.Medicine.SizingImplants.SizingImplantDatabase} */
+App.Data.Medicine.sizingImplants = {
+	boobs: {
+		normal: {
+			name: volume => volume > 500
+				? "large"
+				: (volume < 300 ? "small" : "standard"),
+			specificMaterialCost: () => V.ImplantProductionUpgrade ? 0.1 : 1.3,
+			workCostFactor: {
+				installation: 1,
+				removal: 0.5
+			},
+			healthCostFactor: {
+				installation: 0.1,
+				removal: 0.05
+			},
+			availableSizes: () => _.range(200, V.ImplantProductionUpgrade ? 601 : 401, 200)
+		},
+		fillable: {
+			name: "fillable",
+			availableSizes: [800],
+			specificMaterialCost: () => V.ImplantProductionUpgrade ? 0.15 : 1.5,
+			workCostFactor: {
+				installation: 1.5,
+				removal: 0.7
+			},
+			healthCostFactor: {
+				installation: 0.05,
+				removal: 0.05
+			},
+			fill: {
+				limit: 1800,
+				step: [200],
+				healthCost: 10,
+				materialCostFactor: 0,
+			},
+			drain: {
+				limit: 800,
+				step: [200],
+				healthCost: 10,
+				materialCostFactor: 0,
+			}
+		},
+		"advanced fillable": {
+			name: () => V.ImplantProductionUpgrade > 0 ? "advanced fillable" : "advanced fillable (special order)",
+			availableSizes: [2200],
+			specificMaterialCost: () => V.ImplantProductionUpgrade > 0 ? 0.4 : 4.5,
+			workCostFactor: {
+				installation: 1.5,
+				removal: 0.7
+			},
+			healthCostFactor: {
+				installation: 0.05,
+				removal: 0.03
+			},
+			fill: {
+				limit: 10000,
+				step: [200, 400, 1000],
+				healthCost: 10,
+				materialCostFactor: 0,
+			},
+			drain: {
+				limit: 2200,
+				step: [200, 400, 1000],
+				healthCost: 5,
+				materialCostFactor: 0,
+			}
+		},
+		"hyper fillable": {
+			name: "hyper fillable",
+			availableSizes: () => FutureSocieties.researchAvailable("TransformationFetishist")
+				? [11000] : [],
+			specificMaterialCost: 0.5,
+			workCostFactor: {
+				installation: 0.5,
+				removal: 0.25
+			},
+			healthCostFactor: {
+				installation: 0.01,
+				removal: 0.005
+			},
+			fill: {
+				limit: 50000,
+				step: [1000],
+				healthCost: 10,
+				materialCostFactor: 0.1,
+			},
+			drain: {
+				limit: 11000,
+				step: [1000],
+				healthCost: 5,
+				materialCostFactor: 0,
+			}
+		},
+		string: {
+			name: "string",
+			availableSizes: [400],
+			specificMaterialCost: () => V.ImplantProductionUpgrade > 0 ? 0.4 : 4,
+			workCostFactor: {
+				installation: 2,
+				removal: 5,
+			},
+			healthCostFactor: {
+				installation: 0.2,
+				removal: 0.1
+			},
+			drain: {
+				limit: 600, // yes, can't be drained dry
+				step: [200],
+				healthCost: 5,
+				materialCostFactor: 0,
+			}
+		}
+	},
+	butt: {
+		normal: {
+			name: volume => volume > 1 ? "large" : "standard",
+			specificMaterialCost: () => V.ImplantProductionUpgrade ? 100 : 130,
+			workCostFactor: {
+				installation: 100,
+				removal: 50
+			},
+			healthCostFactor: {
+				installation: 10,
+				removal: 5
+			},
+			availableSizes: () => V.ImplantProductionUpgrade ? [1, 2] : [1]
+		},
+		fillable: {
+			name: "fillable",
+			availableSizes: [3],
+			specificMaterialCost: () => V.ImplantProductionUpgrade ? 150 : 1500,
+			workCostFactor: {
+				installation: 150,
+				removal: 70
+			},
+			healthCostFactor: {
+				installation: 5,
+				removal: 5
+			},
+			fill: {
+				limit: 4,
+				step: [1],
+				healthCost: 10,
+				materialCostFactor: 0,
+			},
+			drain: {
+				limit: 3,
+				step: [1],
+				healthCost: 5,
+				materialCostFactor: 0,
+			}
+		},
+		"advanced fillable": {
+			name: () => V.ImplantProductionUpgrade > 0 ? "advanced fillable" : "advanced fillable (special order)",
+			availableSizes: [5],
+			specificMaterialCost: () => V.ImplantProductionUpgrade > 0 ? 150 : 1500,
+			workCostFactor: {
+				installation: 600,
+				removal: 300
+			},
+			healthCostFactor: {
+				installation: 2.5,
+				removal: 0.3
+			},
+			fill: {
+				limit: 8,
+				step: [1],
+				healthCost: 10,
+				materialCostFactor: 0,
+			},
+			drain: {
+				limit: 5,
+				step: [1],
+				healthCost: 5,
+				materialCostFactor: 0,
+			}
+		},
+		"hyper fillable": {
+			name: "hyper fillable",
+			availableSizes: () => FutureSocieties.researchAvailable("TransformationFetishist")
+				? [9] : [],
+			specificMaterialCost: 400,
+			workCostFactor: {
+				installation: 100,
+				removal: 50
+			},
+			healthCostFactor: {
+				installation: 2,
+				removal: 1
+			},
+			fill: {
+				limit: 20,
+				step: [1],
+				healthCost: 10,
+				materialCostFactor: 100,
+			},
+			drain: {
+				limit: 9,
+				step: [1],
+				healthCost: 5,
+				materialCostFactor: 0,
+			}
+		},
+		string: {
+			name: "string",
+			availableSizes: [1],
+			specificMaterialCost: () => V.ImplantProductionUpgrade > 0 ? 150 : 1500,
+			workCostFactor: {
+				installation: 800,
+				removal: 500,
+			},
+			healthCostFactor: {
+				installation: 80,
+				removal: 10
+			},
+			drain: {
+				limit: 1,
+				step: [1],
+				healthCost: 5,
+				materialCostFactor: 0,
+			}
+		}
+	},
+	lips: {
+		normal: {
+			availableSizes: [10],
+			name: (volume) => {
+				const names = {
+					10: "moderate",
+					20: "large",
+					100: "enormous"
+				};
+				return App.Ratings.numeric(names, volume);
+			},
+			healthCostFactor: {
+				installation: 1,
+				removal: 0.01,
+			},
+			specificMaterialCost: 0,
+			workCostFactor: {
+				installation: 0,
+				removal: 0,
+			},
+			// this is a fake fill data to implement implant sizing
+			// used because there are no names for large implants and they
+			// can be installed in steps only
+			fill: {
+				healthCost: 10,
+				limit: 90,
+				materialCostFactor: 0,
+				step: [10],
+			}
+		}
+	}
+};
diff --git a/js/004-base/SurgeryEffect.js b/js/004-base/SurgeryEffect.js
index e0f299b9ba84bd71983fad36ac4970c473e8756f..09f00ffed574342f67adbec075d061c8d566c135 100644
--- a/js/004-base/SurgeryEffect.js
+++ b/js/004-base/SurgeryEffect.js
@@ -53,8 +53,8 @@ App.Medicine.Surgery.SimpleReaction = class {
 
 	/**
 	 * @typedef {object} reactionResult
-	 * @property {Array<Array<string|Node>>} longReaction can contain HTML, every array is a new paragraph. Shown when manually applied.
-	 * @property {Array<string|Node>} shortReaction can contain HTML. May be empty. Intended for use with RA.
+	 * @property {Array<Array<string|HTMLElement>>} longReaction can contain HTML, every array is a new paragraph. Shown when manually applied.
+	 * @property {Array<string|HTMLElement>} shortReaction can contain HTML. May be empty. Intended for use with RA.
 	 * @property {number} devotion 0 means no change
 	 * @property {number} trust 0 means no change
 	 */
diff --git a/js/004-base/SurgeryProcedure.js b/js/004-base/SurgeryProcedure.js
index bf44c04323103f44ccbfb06925b073fc363c10ea..dc5b65e44a22eb4d1538b5838739f0fa5ceec41d 100644
--- a/js/004-base/SurgeryProcedure.js
+++ b/js/004-base/SurgeryProcedure.js
@@ -16,6 +16,7 @@ App.Medicine.Surgery.Procedure = class {
 
 	// eslint-disable-next-line jsdoc/require-returns-check
 	/**
+	 * @abstract
 	 * @returns {string}
 	 */
 	get name() { throw new Error("Method 'name()' must be implemented."); }
@@ -27,6 +28,12 @@ App.Medicine.Surgery.Procedure = class {
 	 */
 	get description() { return ""; }
 
+	/**
+	 * Additional note to put at the bottom of the tooltip
+	 * @returns {string | DocumentFragment | HTMLElement}
+	 */
+	get note() { return null; }
+
 	/**
 	 * May die, but high player skill also reduces health impact.
 	 *
@@ -39,15 +46,33 @@ App.Medicine.Surgery.Procedure = class {
 
 	/**
 	 * The monetary cost of the surgery.
+	 * @returns {number}
+	 */
+	get cost() {
+		return Math.round(this._materialCost + this._workCost);
+	}
+
+	/**
+	 * Part of the surgery cost related to the required materials
+	 * @protected
+	 * @returns {number}
+	 */
+	get _materialCost() {
+		return 0;
+	}
+
+	/**
+	 * Part of the surgery cost related to the work efforts
 	 * This is the default cost. Subclasses should base their cost on this value if modifying it.
 	 * @example
-	 * get cost() {
+	 * get _workCost() {
 	 *     // Make the surgery 4 times more expensive
-	 *     return super.cost * 4;
+	 *     return super._workCost * 4;
 	 * }
+	 * @protected
 	 * @returns {number}
 	 */
-	get cost() {
+	get _workCost() {
 		// TODO phase out V.surgeryCost and replace with static calculation
 		return V.surgeryCost;
 	}
diff --git a/js/medicine/surgery/assets/01-sizingImplants.js b/js/medicine/surgery/assets/01-sizingImplants.js
new file mode 100644
index 0000000000000000000000000000000000000000..ef25e28d60737b10124f7daa5f22fccfe2ba43ad
--- /dev/null
+++ b/js/medicine/surgery/assets/01-sizingImplants.js
@@ -0,0 +1,381 @@
+App.Medicine.Surgery.Procedures.SizingImplantProcedure = class extends App.Medicine.Surgery.Procedure {
+	/**
+	 * @param {FC.SlaveState} slave
+	 * @param {FC.SizingImplantTarget} target
+	 * @param {FC.SizingImplantType} implantType
+	 * @param {number} size
+	 */
+	constructor(slave, target, implantType, size) {
+		super(slave);
+		this._target = target;
+		this._implantType = implantType;
+		this._size = size;
+	}
+
+	get name() {
+		return `${capFirstChar(this._resolveDynamic(this._implantData.name, this._size))} implants`;
+	}
+
+	get changeValue() {
+		return this._size;
+	}
+
+	apply(cheat) {
+		this._slave[this._target] += this._size;
+		this._slave[`${this._target}Implant`] += this._size;
+		this._setSlaveImplantType(this._implantType);
+
+		if (this.changeValue >= 0) {
+			return this._assemble(this._doTargetGain());
+		} else {
+			if (this._slave[`${this._target}Implant`] < 1) {
+				this._slave[`${this._target}Implant`] = 0;
+				this._setSlaveImplantType("none");
+			}
+			return this._assemble(this._doTargetLoss());
+		}
+	}
+
+	_surgeryWorkPriceFactor() {
+		return V.surgeryCost * V.localEcon / 3e4;
+	}
+
+	/**
+	 * @param {FC.SizingImplantType} type
+	 */
+	_implantDataForType(type) {
+		return App.Data.Medicine.sizingImplants[this._target][type];
+	}
+
+	/**
+	 * @param {FC.SizingImplantType} type
+	 * @param {number} size
+	 */
+	_installationHealthCost(type, size) {
+		return this._implantDataForType(type).healthCostFactor.installation * size;
+	}
+
+	/**
+	 * @param {FC.InstalledSizingImplantType} type
+	 * @param {number} size
+	 */
+	_removalHealthCost(type, size) {
+		return type === "none" ? 0
+			: this._implantDataForType(type).healthCostFactor.removal * size;
+	}
+
+	/**
+	 * @param {FC.InstalledSizingImplantType} type
+	 * @param {number} size
+	 */
+	_installationWorkCost(type, size) {
+		return type === "none" ? 0
+			: this._implantDataForType(type).workCostFactor.installation * size * this._surgeryWorkPriceFactor();
+	}
+
+	/**
+	 * @param {FC.InstalledSizingImplantType} type
+	 * @param {number} size
+	 */
+	_removalWorkCost(type, size) {
+		return type === "none" ? 0
+			: this._implantDataForType(type).workCostFactor.removal * size * this._surgeryWorkPriceFactor();
+	}
+
+	get _implantData() {
+		return this._implantDataForType(this._implantType);
+	}
+
+	get _implantName() {
+		return this._resolveDynamic(this._implantData.name, this._size);
+	}
+
+	get _materialCost() {
+		return this._resolveDynamic(this._implantData.specificMaterialCost, this._size) * this._size;
+	}
+
+	/**
+	 * @param {*} amount Positive for fill, negative for drain
+	 */
+	_fillDrainMaterialCost(amount) {
+		const id = this._implantData;
+		return amount >= 0
+			? (id.fill ? this._implantData.fill.materialCostFactor * amount : 0)
+			: 0; // draining does not impose material cost as of now
+	}
+
+	/**
+	 * @template {string | number} T
+	 * @param {T | ((size: number) => T)} value
+	 * @param {number} size
+	 * @returns {T}
+	 */
+	_resolveDynamic(value, size) {
+		return typeof value !== "function" ? value : value(size);
+	}
+
+	get _targetName() {
+		switch (this._target) {
+			case "boobs":
+				return ["mammaries", "chest", "breasts"].random();
+			case "butt":
+				return ["buttocks", "posterior"].random();
+			case "lips":
+				return "lip";
+			default:
+				return this._target;
+		}
+	}
+
+	/**
+	 * Returns a string containign the name (depends on size) and possible volume of the implant
+	 * @param {FC.SizingImplantType} type
+	 * @param {number} volume
+	 */
+	_describeImplant(type, volume) {
+		let r = this._resolveDynamic(this._implantDataForType(type).name, volume);
+		const volumeDesc = this._volumeStr(volume);
+		if (volumeDesc) {
+			r += ` (${volumeDesc})`;
+		}
+		return r;
+	}
+
+	_setSlaveImplantType(implantType) {
+		if (this.originalSlave[`${this._target}ImplantType`] !== implantType) {
+			this._slave[`${this._target}ImplantType`] = implantType;
+		}
+	}
+
+	/**
+	 * @abstract
+	 * @returns {App.Medicine.Surgery.SimpleReaction}
+	 */
+	_doTargetGain() {
+		throw Error("Abstract method called");
+	}
+	/**
+	 * @abstract
+	 * @returns {App.Medicine.Surgery.SimpleReaction}
+	 */
+	_doTargetLoss() {
+		throw Error("Abstract method called");
+	}
+
+	/**
+	 * @abstract
+	 * @param {number} volume
+	 * @returns {string}
+	 */
+	_volumeStr(volume) {
+		throw Error("Abstract method called");
+	}
+
+	/**
+	 * @abstract
+	 * @param {number} volume
+	 * @returns {string}
+	 */
+	 _fleshExcessStr(volume) {
+		throw Error("Abstract method called");
+	}
+};
+
+App.Medicine.Surgery.Procedures.InstallSizingImplantProcedure = class extends App.Medicine.Surgery.Procedures.SizingImplantProcedure {
+	/**
+	 * @param {FC.SlaveState} slave
+	 * @param {FC.SizingImplantTarget} target
+	 * @param {FC.SizingImplantType} implantType
+	 * @param {number} size how big is the implant
+	 * @param {number} fleshExcess Excess flesh volume to remove for installing this implant
+	 */
+	constructor(slave, target, implantType, size, fleshExcess) {
+		super(slave, target, implantType, size);
+		this._fleshExcess = fleshExcess;
+	}
+
+	get description() {
+		const {his} = getPronouns(this._slave);
+		return `place ${this._describeImplant(this._implantType, this._size)} implants into ${his} ${this._targetName}`;
+	}
+
+	get note() {
+		if (this._fleshExcess > 0) {
+			const {his} = getPronouns(this._slave);
+			return App.UI.DOM.makeElement('span',
+				`Installation requires removing ${this._fleshExcessStr(this._fleshExcess)} of ${his} ${this._targetName} flesh.`, ["warning"]);
+		}
+		return null;
+	}
+
+	get healthCost() {
+		return this._installationHealthCost(this._implantType, this._size);
+	}
+
+	get _workCost() {
+		return super._workCost + super._installationWorkCost(this._implantType, this._size);
+	}
+};
+
+App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase = class extends App.Medicine.Surgery.Procedures.SizingImplantProcedure {
+	/**
+	 * @param {FC.SlaveState} slave
+	 * @param {FC.SizingImplantTarget} target
+	 * @param {number} size
+	 * @param {FC.SizingImplantType} [implantType]
+	 */
+	constructor(slave, target, size, implantType = null) {
+		const oldImplantInfo = App.Medicine.implantInfo(slave, target);
+		if (oldImplantInfo.type === "none") {
+			throw Error(`Slave has no ${target} implant to work on`);
+		}
+		super(slave, target, implantType || oldImplantInfo.type, size);
+		this._oldImplantInfo = oldImplantInfo;
+	}
+
+	/**
+	 * @protected
+	 */
+	get _oldImplantsDescription() {
+		return this._describeImplant(/** @type {FC.SizingImplantType}*/(this._oldImplantInfo.type), this._oldImplantInfo.volume);
+	}
+};
+
+App.Medicine.Surgery.Procedures.RemoveSizingImplantProcedure = class extends App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase {
+	/**
+	 * @param {FC.SlaveState} slave
+	 * @param {FC.SizingImplantTarget} target
+	 */
+	constructor(slave, target) {
+		super(slave, target, -slave[`${target}Implant`]);
+	}
+
+	get name() {
+		return `Remove ${this._targetName} implants`;
+	}
+
+	get description() {
+		const {his} = getPronouns(this._slave);
+		return `remove ${his} ${this._oldImplantsDescription} ${this._targetName} implants`;
+	}
+
+	get _materialCost() {
+		return 0;
+	}
+
+	get _workCost() {
+		return super._workCost + super._removalWorkCost(this._implantType, -this._size);
+	}
+
+	get healthCost() {
+		return this._removalHealthCost(this._implantType, -this._size);
+	}
+};
+
+App.Medicine.Surgery.Procedures.ReplaceSizingImplantProcedure = class extends App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase {
+	/**
+	 * @param {FC.SlaveState} slave
+	 * @param {FC.SizingImplantTarget} target
+	 * @param {FC.SizingImplantType} implantType
+	 * @param {number} size how big is the implant
+	 * @param {number} fleshExcess Excess flesh volume to remove for installing this implant
+	 */
+	constructor(slave, target, implantType, size, fleshExcess) {
+		super(slave, target, size, implantType);
+		this._fleshExcess = fleshExcess;
+	}
+
+	get description() {
+		const {his} = getPronouns(this._slave);
+		return `replace ${his} ${this._oldImplantsDescription} ${this._targetName} implants with ${this._describeImplant(this._implantType, this._size)} ones`;
+	}
+
+	get note() {
+		if (this._fleshExcess > 0) {
+			const {his} = getPronouns(this._slave);
+			return App.UI.DOM.makeElement('span',
+				`Installation of the new implant requires removing ${this._fleshExcessStr(this._fleshExcess)} of ${his} ${this._targetName} flesh.`, ["warning"]);
+		}
+		return null;
+	}
+
+	get healthCost() {
+		return this._removalHealthCost(this._oldImplantInfo.type, this._oldImplantInfo.volume) +
+			this._installationHealthCost(this._implantType, this._size);
+	}
+
+	get _workCost() {
+		const previousImplant = App.Medicine.implantInfo(this.originalSlave, this._target);
+		return super._workCost + super._removalWorkCost(previousImplant.type, previousImplant.volume) +
+			this._installationWorkCost(this._implantType, this._size);
+	}
+
+	get changeValue() {
+		return this._size - this._oldImplantInfo.volume;
+	}
+
+	apply(cheat) {
+		this._slave[this._target] -= this._oldImplantInfo.volume;
+		this._slave[`${this._target}Implant`] = 0;
+
+		return super.apply(cheat);
+	}
+};
+
+App.Medicine.Surgery.Procedures.FillSizingImplantProcedure = class extends  App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase {
+	/**
+	 * @param {FC.SlaveState} slave
+	 * @param {FC.SizingImplantTarget} target
+	 * @param {number} amount
+	 */
+	constructor(slave, target, amount) {
+		super(slave, target, amount);
+	}
+
+	get name() {
+		return `Add inert filler`;
+	}
+
+	get description() {
+		const {his} = getPronouns(this._slave);
+		const volumeStr = this._volumeStr(this._size);
+		return `add ${volumeStr ? `${volumeStr} of` : 'some'} inert filler to each of ${his} ${this._oldImplantsDescription} ${this._targetName} implants`;
+	}
+
+	get _materialCost() {
+		return this._fillDrainMaterialCost(this._size);
+	}
+
+	get healthCost() {
+		return this._implantData.fill.healthCost;
+	}
+};
+
+App.Medicine.Surgery.Procedures.DrainSizingImplantProcedure = class extends  App.Medicine.Surgery.Procedures.ExistingImplantsProcedureBase {
+	/**
+	 * @param {FC.SlaveState} slave
+	 * @param {FC.SizingImplantTarget} target
+	 * @param {number} amount
+	 */
+	constructor(slave, target, amount) {
+		super(slave, target, -amount);
+	}
+
+	get name() {
+		return `Drain inert filler`;
+	}
+
+	get description() {
+		const {his} = getPronouns(this._slave);
+		const volumeStr = this._volumeStr(this._size);
+		return `drain ${volumeStr ? `${volumeStr} of` : 'some'} inert filler from ${his} ${this._oldImplantsDescription} ${this._targetName} implants`;
+	}
+
+	get _materialCost() {
+		return 0;
+	}
+
+	get healthCost() {
+		return this._implantData.drain.healthCost;
+	}
+};
diff --git a/js/medicine/surgery/assets/boobs.js b/js/medicine/surgery/assets/boobs.js
index c7b0d4c4d14c3d609b5304d9828625d46ed79876..99334fa8546ad43459c71beb6c5d40bfd2d12cca 100644
--- a/js/medicine/surgery/assets/boobs.js
+++ b/js/medicine/surgery/assets/boobs.js
@@ -143,11 +143,14 @@ App.Medicine.Surgery.Reactions.BoobsLoss = class extends App.Medicine.Surgery.Si
 	}
 };
 
-{
+/**
+ * @type {FC.Medicine.AssetSizingProcedureSet}
+ */
+App.Medicine.Surgery.Procedures.boobImplantsProcedure = function() {
 	/**
-	 * @param {App.Entity.SlaveState} slave
+	 * @param {FC.SlaveState} slave
 	 */
-	const applyBoobsGain = function(slave) {
+	const applyBoobsGain = (slave) => {
 		if (slave.areolae < 2 && Math.random() > 0.7) {
 			slave.areolae += 1;
 		}
@@ -172,9 +175,9 @@ App.Medicine.Surgery.Reactions.BoobsLoss = class extends App.Medicine.Surgery.Si
 	};
 
 	/**
-	 * @param {App.Entity.SlaveState} slave
+	 * @param {FC.SlaveState} slave
 	 */
-	const applyBoobsLoss = function(slave) {
+	const applyBoobsLoss = (slave) => {
 		if (slave.areolae > 2) {
 			slave.areolae -= 1;
 		}
@@ -190,196 +193,48 @@ App.Medicine.Surgery.Reactions.BoobsLoss = class extends App.Medicine.Surgery.Si
 		}
 	};
 
-	App.Medicine.Surgery.Procedures.InstallBoobImplants = class extends App.Medicine.Surgery.Procedure {
-		/**
-		 * @param {App.Entity.SlaveState} slave
-		 * @param {string} name
-		 * @param {FC.SizingImplantType} implantType
-		 * @param {number} size how big is the implant
-		 */
-		constructor(slave, name, implantType, size) {
-			super(slave);
-			this._size = size;
-			this._implantType = implantType;
-			this._name = name;
-		}
-
-		get name() {
-			return `${capFirstChar(this._name)} implants`;
-		}
-
-		get description() {
-			const {his} = getPronouns(this._slave);
-			return `place ${this._name}${V.showBoobCCs ? ` ${this._size}cc` : ''} implants into ${his} boobs`;
-		}
-
-		get healthCost() { return 10; }
-
-		get changeValue() { return this._size; }
-
-		apply(cheat) {
-			this._slave.boobs += this._size;
-			this._slave.boobsImplant = this._size;
-			this._slave.boobsImplantType = this._implantType;
-
-			applyBoobsGain(this._slave);
-
-			return this._assemble(new App.Medicine.Surgery.Reactions.BoobsGain());
-		}
-	};
-
-	App.Medicine.Surgery.Procedures.RemoveBoobImplants = class extends App.Medicine.Surgery.Procedure {
-		get name() {
-			return "Remove implants";
-		}
-
-		get description() {
-			const {his} = getPronouns(this._slave);
-			return `remove ${his} boob implants`;
-		}
-
-		get healthCost() { return 5; }
-
-		get changeValue() { return -this._slave.boobsImplant; }
-
-		apply(cheat) {
-			this._slave.boobs -= this._slave.boobsImplant;
-			this._slave.boobsImplant = 0;
-			this._slave.boobsImplantType = "none";
-
-			applyBoobsLoss(this._slave);
-
-			return this._assemble(new App.Medicine.Surgery.Reactions.BoobsLoss());
-		}
-	};
-
-
-	App.Medicine.Surgery.Procedures.ReplaceBoobImplants = class extends App.Medicine.Surgery.Procedure {
-		/**
-		 * @param {App.Entity.SlaveState} slave
-		 * @param {string} name
-		 * @param {FC.SizingImplantType} implantType
-		 * @param {number} size how big is the implant
-		 * @param {boolean} [advanced=false] advanced implants may be more expensive
-		 */
-		constructor(slave, name, implantType, size, advanced = false) {
-			super(slave);
-			this._size = size;
-			this._implantType = implantType;
-			this._name = name;
-			this._advanced = advanced;
-		}
-
-		get name() {
-			return `${capFirstChar(this._name)} implants${this._advanced && V.ImplantProductionUpgrade < 1 ? " (special order)" : ""}`;
-		}
-
-		get description() {
-			const {his} = getPronouns(this._slave);
-			const r = [];
-			r.push(`replace ${his} boob implants with ${this._name}`);
-			if (V.showBoobCCs) {
-				r.push(`(${this._size}cc)`);
-			}
-			r.push("ones");
-			if (this._advanced && V.ImplantProductionUpgrade < 1) {
-				r.push("(special order)");
+	/**
+	 * @template {FC.Medicine.SizingImplantProcedureConstructor} T
+	 * @param {T} Procedure
+	 * @param {ConstructorParameters<T>} args
+	 */
+	function makeBoobProcedure(Procedure, args) {
+		return new (class extends Procedure {
+			_doTargetGain() {
+				applyBoobsGain(this._slave);
+				return new App.Medicine.Surgery.Reactions.BoobsGain();
 			}
-			return r.join(" ");
-		}
-
-		get cost() { return super.cost + (this._advanced && V.ImplantProductionUpgrade < 1 ? 10000 : 0); }
-
-		get healthCost() { return 10; }
-
-		get changeValue() { return this._size - this.originalSlave.boobsImplant; }
 
-		apply(cheat) {
-			this._slave.boobs += this._size - this._slave.boobsImplant;
-			this._slave.boobsImplant = this._size;
-			this._slave.boobsImplantType = this._implantType;
-
-			if (this.changeValue >= 0) {
-				applyBoobsGain(this._slave);
-				return this._assemble(new App.Medicine.Surgery.Reactions.BoobsGain());
-			} else {
+			_doTargetLoss() {
 				applyBoobsLoss(this._slave);
-				return this._assemble(new App.Medicine.Surgery.Reactions.BoobsLoss());
+				return new App.Medicine.Surgery.Reactions.BoobsLoss();
 			}
-		}
-	};
-
-
-	App.Medicine.Surgery.Procedures.FillBoobImplants = class extends App.Medicine.Surgery.Procedure {
-		/**
-		 * @param {App.Entity.SlaveState} slave
-		 * @param {number} amount
-		 */
-		constructor(slave, amount) {
-			super(slave);
-			this._amount = amount;
-		}
-
-		get name() {
-			return `Add inert filler`;
-		}
-
-		get description() {
-			const {his} = getPronouns(this._slave);
-			return `add ${V.showBoobCCs ? `${this._amount}cc of` : 'some'} inert filler to each of ${his} boob implants`;
-		}
-
-		get healthCost() { return 10; }
-
-		get changeValue() { return this._amount; }
-
-		apply(cheat) {
-			this._slave.boobs += this._amount;
-			this._slave.boobsImplant += this._amount;
-
-			applyBoobsGain(this._slave);
-
-			return this._assemble(new App.Medicine.Surgery.Reactions.BoobsGain());
-		}
-	};
-
-	App.Medicine.Surgery.Procedures.DrainBoobImplants = class extends App.Medicine.Surgery.Procedure {
-		/**
-		 * @param {App.Entity.SlaveState} slave
-		 * @param {number} amount
-		 */
-		constructor(slave, amount) {
-			super(slave);
-			this._amount = amount;
-		}
-
-		get name() {
-			return `Drain inert filler`;
-		}
-
-		get description() {
-			const {his} = getPronouns(this._slave);
-			return `drain ${V.showBoobCCs ? `${this._amount}cc of` : 'some'} inert filler from ${his} boob implants`;
-		}
-
-		get healthCost() { return 5; }
-
-		get changeValue() { return -this._amount; }
-
-		apply(cheat) {
-			this._slave.boobs -= this._amount;
-			this._slave.boobsImplant -= this._amount;
 
-			applyBoobsLoss(this._slave);
+			/**
+			 * @param {number} volume
+			 * @returns {string}
+			 */
+			_volumeStr(volume) {
+				return V.showBoobCCs ? App.Utils.expandHTML(`${volume}&thinsp;cm³`) : '';
+			}
 
-			return this._assemble(new App.Medicine.Surgery.Reactions.BoobsLoss());
-		}
-	};
+			/**
+			 * @param {number} volume
+			 * @returns {string}
+			 */
+			_fleshExcessStr(volume) {
+				const percentsStr = `${Math.round(100. * volume / App.Medicine.fleshSize(this.originalSlave, "boobs"))}&thinsp;%`;
 
+				return V.showBoobCCs
+					? App.Utils.expandHTML(`${volume}&thinsp;cm³ (${percentsStr})`)
+					: App.Utils.expandHTML(percentsStr);
+			}
+		})(...args);
+	}
 
-	App.Medicine.Surgery.Procedures.ReduceBoobs = class extends App.Medicine.Surgery.Procedure {
+	class Reduce extends App.Medicine.Surgery.Procedure {
 		/**
-		 * @param {App.Entity.SlaveState} slave
+		 * @param {FC.SlaveState} slave
 		 * @param {string} procedureName
 		 * @param {number} amount
 		 */
@@ -409,44 +264,21 @@ App.Medicine.Surgery.Reactions.BoobsLoss = class extends App.Medicine.Surgery.Si
 
 			return this._assemble(new App.Medicine.Surgery.Reactions.BoobsLoss());
 		}
-	};
-}
-
-/**
- * @param {FC.HumanState} slave
- * @returns {number}
- */
-App.Medicine.Surgery.breastPocketVolume = function(slave) {
-	/*
-	The breast is approximated by a spherical cap with the volume of slave.boobs.
-	The radius `a` of the cap is proportional to slave height and shoulders width
-	We assume that the skin has elasticity coefficient that depends on slave physical age.
-	Max pocket volume is therefore the volume of the cap with the same base but the
-	surface stretched by the skin elasticity coefficient
-	V = 1/6*πh(3a²+h²)
-	A = π(a²+h²)
-	*/
-	const a = 0.05 * slave.height + slave.shoulders * 1.1;
-	/* now the eq. for V we rewrite as
-	h³ + 3a³h -6/π*V = 0
-	and replace:
-	p = 3*a³, q = -6/π*V:
-	h³ + ph + q = 0
-	*/
-	const p = 3 * a * a;
-	const q = -6. / Math.PI * slave.boobs;
-	const Q = Math.pow(p / 3, 3) + Math.pow(q / 2, 2);
-	console.assert(Q > 0);
-	const h = Math.cbrt(-q / 2 + Math.sqrt(Q)) + Math.cbrt(-q / 2 - Math.sqrt(Q));
-
-	// now the current skin area:
-	const A = Math.PI * (a * a + h * h);
+	}
 
-	const skinStretchFactor = linearInterpolation(Math.max(slave.physicalAge, 13), 13, 1.5, 45, 1.15);
-	console.log(`skinStretchFactor = ${skinStretchFactor}`);
+	return {
+		install: (slave, type, size, fleshExcess) =>
+			makeBoobProcedure(App.Medicine.Surgery.Procedures.InstallSizingImplantProcedure, [slave, "boobs", type, size, fleshExcess]),
+		replace: (slave, type, size, fleshExcess) =>
+			makeBoobProcedure(App.Medicine.Surgery.Procedures.ReplaceSizingImplantProcedure, [slave, "boobs", type, size, fleshExcess]),
+		remove: (slave) =>
+			makeBoobProcedure(App.Medicine.Surgery.Procedures.RemoveSizingImplantProcedure, [slave, "boobs"]),
 
-	const hMax = Math.sqrt(A * skinStretchFactor / Math.PI - a * a);
-	const VMax = Math.PI / 6 * hMax * (3 * a * a + hMax * hMax);
+		fill: (slave, amount) =>
+			makeBoobProcedure(App.Medicine.Surgery.Procedures.FillSizingImplantProcedure, [slave, "boobs", amount]),
+		drain: (slave, amount) =>
+			makeBoobProcedure(App.Medicine.Surgery.Procedures.DrainSizingImplantProcedure, [slave, "boobs", amount]),
 
-	return VMax;
-};
+		reduce: (slave, name, amount) => new Reduce(slave, name, amount)
+	};
+}();
diff --git a/js/medicine/surgery/assets/breastShapePreservation.js b/js/medicine/surgery/assets/breastShapePreservation.js
index 04beba000f2ff431434c0ec30a7ec926c89016ef..88a08430c494dfce7f4885733553a1e8dff8ed59 100644
--- a/js/medicine/surgery/assets/breastShapePreservation.js
+++ b/js/medicine/surgery/assets/breastShapePreservation.js
@@ -204,7 +204,7 @@ App.Medicine.Surgery.Procedures.BreastShapePreservation = class extends App.Medi
 		return "Implant a supportive mesh to preserve their shape";
 	}
 
-	get cost() {
+	get _workCost() {
 		return this.originalSlave.boobs / 100;
 	}
 
diff --git a/js/medicine/surgery/assets/butt.js b/js/medicine/surgery/assets/butt.js
index ce2673fd4190e1d9495b4aa3c408f50c621b201f..bb850556814b2b9c3d10430975c7dc734582f6b4 100644
--- a/js/medicine/surgery/assets/butt.js
+++ b/js/medicine/surgery/assets/butt.js
@@ -86,194 +86,87 @@ App.Medicine.Surgery.Reactions.ButtLoss = class extends App.Medicine.Surgery.Sim
 	}
 };
 
-App.Medicine.Surgery.Procedures.InstallButtImplants = class extends App.Medicine.Surgery.Procedure {
+/**
+ * @type {FC.Medicine.AssetSizingProcedureSet}
+ */
+App.Medicine.Surgery.Procedures.buttImplantsProcedure = function() {
 	/**
-	 * @param {App.Entity.SlaveState} slave
-	 * @param {string} name
-	 * @param {FC.SizingImplantType} implantType
-	 * @param {number} size how big is the implant
+	 * @template {FC.Medicine.SizingImplantProcedureConstructor} T
+	 * @param {T} Procedure
+	 * @param {ConstructorParameters<T>} args
 	 */
-	constructor(slave, name, implantType, size) {
-		super(slave);
-		this._size = size;
-		this._implantType = implantType;
-		this._name = name;
-	}
-
-	get name() {
-		return `${capFirstChar(this._name)} implants`;
-	}
-
-	get description() {
-		const {his} = getPronouns(this._slave);
-		return `place ${this._name} implants into ${his} butt`;
-	}
-
-	get healthCost() { return 10; }
-
-	get changeValue() { return this._size; }
-
-	apply(cheat) {
-		this._slave.butt += this._size;
-		this._slave.buttImplant = this._size;
-		this._slave.buttImplantType = this._implantType;
-		return this._assemble(new App.Medicine.Surgery.Reactions.ButtGain());
-	}
-};
-
-App.Medicine.Surgery.Procedures.RemoveButtImplants = class extends App.Medicine.Surgery.Procedure {
-	get name() {
-		return "Remove implants";
-	}
-
-	get description() {
-		const {his} = getPronouns(this._slave);
-		return `remove ${his} butt implants`;
-	}
-
-	get healthCost() { return 5; }
-
-	get changeValue() { return -this._slave.buttImplant; }
+	 function makeButtProcedure(Procedure, args) {
+		return new (class extends Procedure {
+			_doTargetGain() {
+				return new App.Medicine.Surgery.Reactions.ButtGain();
+			}
 
-	apply(cheat) {
-		this._slave.butt -= this._slave.buttImplant;
-		this._slave.buttImplant = 0;
-		this._slave.buttImplantType = "none";
-		return this._assemble(new App.Medicine.Surgery.Reactions.ButtLoss());
-	}
-};
+			_doTargetLoss() {
+				return new App.Medicine.Surgery.Reactions.ButtLoss();
+			}
 
-App.Medicine.Surgery.Procedures.ReplaceButtImplants = class extends App.Medicine.Surgery.Procedure {
-	/**
-	 * @param {App.Entity.SlaveState} slave
-	 * @param {string} name
-	 * @param {FC.SizingImplantType} implantType
-	 * @param {number} size how big is the implant
-	 * @param {boolean} [advanced=false] advanced implants may be more expensive
-	 */
-	constructor(slave, name, implantType, size, advanced = false) {
-		super(slave);
-		this._size = size;
-		this._implantType = implantType;
-		this._name = name;
-		this._advanced = advanced;
-	}
+			/**
+			 * @param {number} volume
+			 * @returns {string}
+			 */
+			_volumeStr(volume) {
+				return '';
+			}
 
-	get name() {
-		return `${capFirstChar(this._name)} implants${this._advanced && V.ImplantProductionUpgrade < 1 ? " (special order)" : ""}`;
-	}
+			/**
+			 * @param {number} volume
+			 * @returns {string}
+			 */
+			_fleshExcessStr(volume) {
+				return App.Utils.expandHTML(`${Math.round(100. * volume / App.Medicine.fleshSize(this.originalSlave, "butt"))}&thinsp;%`);
+			}
+		})(...args);
+	}
+
+	class Reduce extends App.Medicine.Surgery.Procedure {
+		/**
+		 * @param {App.Entity.SlaveState} slave
+		 * @param {string} procedureName
+		 * @param {number} amount
+		 */
+		constructor(slave, procedureName, amount) {
+			super(slave);
+			this._procedureName = capFirstChar(procedureName);
+			this._amount = amount;
+		}
 
-	get description() {
-		const {his} = getPronouns(this._slave);
-		return `replace ${his} butt implants with ${this._name} ones${this._advanced && V.ImplantProductionUpgrade < 1 ? " (special order)" : ""}`;
-	}
+		get name() {
+			return `${this._procedureName} butt`;
+		}
 
-	get cost() { return super.cost + (this._advanced && V.ImplantProductionUpgrade < 1 ? 10000 : 0); }
+		get description() {
+			const {his} = getPronouns(this._slave);
+			return `${this._procedureName} ${his} butt`;
+		}
 
-	get healthCost() { return 10; }
+		get healthCost() { return 5; }
 
-	get changeValue() { return this._size - this._slave.buttImplant; }
+		get changeValue() { return -this._amount; }
 
-	apply(cheat) {
-		this._slave.butt += this._size - this._slave.buttImplant;
-		this._slave.buttImplant = this._size;
-		this._slave.buttImplantType = this._implantType;
-		if (this.changeValue >= 0) {
-			return this._assemble(new App.Medicine.Surgery.Reactions.ButtGain());
-		} else {
+		apply(cheat) {
+			this._slave.butt -= this._amount;
 			return this._assemble(new App.Medicine.Surgery.Reactions.ButtLoss());
 		}
 	}
-};
 
+	return {
+		install: (slave, type, size, fleshExcess) =>
+			makeButtProcedure(App.Medicine.Surgery.Procedures.InstallSizingImplantProcedure, [slave, "butt", type, size, fleshExcess]),
+		replace: (slave, type, size, fleshExcess) =>
+			makeButtProcedure(App.Medicine.Surgery.Procedures.ReplaceSizingImplantProcedure, [slave, "butt", type, size, fleshExcess]),
+		remove: (slave) =>
+			makeButtProcedure(App.Medicine.Surgery.Procedures.RemoveSizingImplantProcedure, [slave, "butt"]),
 
-App.Medicine.Surgery.Procedures.FillButtImplants = class extends App.Medicine.Surgery.Procedure {
-	/**
-	 * @param {App.Entity.SlaveState} slave
-	 * @param {number} amount
-	 */
-	constructor(slave, amount) {
-		super(slave);
-		this._amount = amount;
-	}
+		fill: (slave, amount) =>
+			makeButtProcedure(App.Medicine.Surgery.Procedures.FillSizingImplantProcedure, [slave, "butt", amount]),
+		drain: (slave, amount) =>
+			makeButtProcedure(App.Medicine.Surgery.Procedures.DrainSizingImplantProcedure, [slave, "butt", amount]),
 
-	get name() {
-		return `Add inert filler`;
-	}
-
-	get description() {
-		const {his} = getPronouns(this._slave);
-		return `add some inert filler to each of ${his} butt implants`;
-	}
-
-	get healthCost() { return 10; }
-
-	get changeValue() { return this._amount; }
-
-	apply(cheat) {
-		this._slave.butt += this._amount;
-		this._slave.buttImplant += this._amount;
-		return this._assemble(new App.Medicine.Surgery.Reactions.ButtGain());
-	}
-};
-
-App.Medicine.Surgery.Procedures.DrainButtImplants = class extends App.Medicine.Surgery.Procedure {
-	/**
-	 * @param {App.Entity.SlaveState} slave
-	 * @param {number} amount
-	 */
-	constructor(slave, amount) {
-		super(slave);
-		this._amount = amount;
-	}
-
-	get name() {
-		return `Drain inert filler`;
-	}
-
-	get description() {
-		const {his} = getPronouns(this._slave);
-		return `drain some inert filler from ${his} butt implants`;
-	}
-
-	get healthCost() { return 5; }
-
-	get changeValue() { return -this._amount; }
-
-	apply(cheat) {
-		this._slave.butt -= this._amount;
-		this._slave.buttImplant -= this._amount;
-		return this._assemble(new App.Medicine.Surgery.Reactions.ButtLoss());
-	}
-};
-
-App.Medicine.Surgery.Procedures.ReduceButt = class extends App.Medicine.Surgery.Procedure {
-	/**
-	 * @param {App.Entity.SlaveState} slave
-	 * @param {string} procedureName
-	 * @param {number} amount
-	 */
-	constructor(slave, procedureName, amount) {
-		super(slave);
-		this._procedureName = capFirstChar(procedureName);
-		this._amount = amount;
-	}
-
-	get name() {
-		return `${this._procedureName} butt`;
-	}
-
-	get description() {
-		const {his} = getPronouns(this._slave);
-		return `${this._procedureName} ${his} butt`;
-	}
-
-	get healthCost() { return 5; }
-
-	get changeValue() { return -this._amount; }
-
-	apply(cheat) {
-		this._slave.butt -= this._amount;
-		return this._assemble(new App.Medicine.Surgery.Reactions.ButtLoss());
-	}
-};
+		reduce: (slave, name, amount) => new Reduce(slave, name, amount)
+	};
+}();
diff --git a/js/medicine/surgery/assets/fatGraft.js b/js/medicine/surgery/assets/fatGraft.js
index f9a8bc45a38300d9b4547143fdaef06b7125664b..c43e27e5a2c199402954fe815c0bf070102a3e0b 100644
--- a/js/medicine/surgery/assets/fatGraft.js
+++ b/js/medicine/surgery/assets/fatGraft.js
@@ -199,8 +199,8 @@ App.Medicine.Surgery.Procedures.FatGraft = class extends App.Medicine.Surgery.Pr
 		return "Finalize fat transfer";
 	}
 
-	get cost() {
-		return super.cost * 2;
+	get _workCost() {
+		return super._workCost * 2;
 	}
 
 	get healthCost() {
diff --git a/js/medicine/surgery/exotic/retrogradeVirusInjectionNCS.js b/js/medicine/surgery/exotic/retrogradeVirusInjectionNCS.js
index 3eb4be7684844038a27e33fc953c8d19458747d3..9e3581f955b92fb453fb693b34d51162551d34e0 100644
--- a/js/medicine/surgery/exotic/retrogradeVirusInjectionNCS.js
+++ b/js/medicine/surgery/exotic/retrogradeVirusInjectionNCS.js
@@ -248,8 +248,8 @@ App.Medicine.Surgery.Procedures.RetrogradeVirusInjectionNCS = class extends App.
 		return 80;
 	}
 
-	get cost() {
-		return super.cost * 4;
+	get _workCost() {
+		return super._workCost * 4;
 	}
 
 	apply(cheat) {
diff --git a/js/medicine/surgery/exotic/treatment.js b/js/medicine/surgery/exotic/treatment.js
index ff8fe2f8abb5b608d018274d223226bc2afe32e3..765a7d1366954253627b2bbf7fe4f7d27967a369 100644
--- a/js/medicine/surgery/exotic/treatment.js
+++ b/js/medicine/surgery/exotic/treatment.js
@@ -33,8 +33,8 @@ App.Medicine.Surgery.Procedures.ElasticityTreatment = class extends App.Medicine
 		return `this will alter ${his} genetic code to encourage ${his} body to stretch`;
 	}
 
-	get cost() {
-		return super.cost * 4;
+	get _workCost() {
+		return super._workCost * 4;
 	}
 
 	get healthCost() {
@@ -58,8 +58,8 @@ App.Medicine.Surgery.Procedures.ImmortalityTreatment = class extends App.Medicin
 		return `this will alter ${his} genetic code to reverse and prevent aging, effectively thwarting the rigors of old age`;
 	}
 
-	get cost() {
-		return super.cost * 4;
+	get _workCost() {
+		return super._workCost * 4;
 	}
 
 	get healthCost() {
@@ -119,8 +119,8 @@ App.Medicine.Surgery.Procedures.RemoveGene = class extends App.Medicine.Surgery.
 		return `Applying a retro-virus treatment radically increases carcinogen buildup`;
 	}
 
-	get cost() {
-		return super.cost * 4;
+	get _workCost() {
+		return super._workCost * 4;
 	}
 
 	get healthCost() {
@@ -169,8 +169,8 @@ App.Medicine.Surgery.Procedures.AddGene = class extends App.Medicine.Surgery.Pro
 		return r.join(" ");
 	}
 
-	get cost() {
-		return super.cost * (this.activation ? 4 : 10);
+	get _workCost() {
+		return super._workCost * (this.activation ? 4 : 10);
 	}
 
 	get healthCost() {
diff --git a/js/medicine/surgery/extreme/fuckdoll.js b/js/medicine/surgery/extreme/fuckdoll.js
index 026265842337b99fc03ed386d9e19d505825cda8..900913f6882f5768703f80760a19d77c8f0c90b0 100644
--- a/js/medicine/surgery/extreme/fuckdoll.js
+++ b/js/medicine/surgery/extreme/fuckdoll.js
@@ -181,7 +181,7 @@ App.Medicine.Surgery.Procedures.Fuckdoll = class extends App.Medicine.Surgery.Pr
 		return "Encase in a Fuckdoll suit";
 	}
 
-	get cost() {
+	get _workCost() {
 		return 0;
 	}
 
diff --git a/js/medicine/surgery/extreme/fuckdollExtraction.js b/js/medicine/surgery/extreme/fuckdollExtraction.js
index dc966e070868c16cbb57b5b983caec732595e080..a7a6bf621498688217b37e18a377d10e8d075812 100644
--- a/js/medicine/surgery/extreme/fuckdollExtraction.js
+++ b/js/medicine/surgery/extreme/fuckdollExtraction.js
@@ -39,7 +39,7 @@ App.Medicine.Surgery.Procedures.FuckdollExtraction = class extends App.Medicine.
 		return `Extract ${him}`;
 	}
 
-	get cost() {
+	get _workCost() {
 		return 0;
 	}
 
diff --git a/js/medicine/surgery/face/lips.js b/js/medicine/surgery/face/lips.js
index 2ee1174da1dfe84d05c40276c27238a19d153d62..3efebb3a8e59af39919e0424994046015e9d9004 100644
--- a/js/medicine/surgery/face/lips.js
+++ b/js/medicine/surgery/face/lips.js
@@ -51,68 +51,95 @@ App.Medicine.Surgery.Reactions.Lips = class extends App.Medicine.Surgery.SimpleR
 	}
 };
 
-App.Medicine.Surgery.Procedures.IncreaseLipImplant = class extends App.Medicine.Surgery.Procedure {
-	get name() {
-		return "Replace with the next size up";
-	}
+/**
+ * @type {FC.Medicine.AssetSizingProcedureSet}
+ */
+App.Medicine.Surgery.Procedures.lipImplantProcedure = function() {
+	class Fill extends App.Medicine.Surgery.Procedures.FillSizingImplantProcedure {
+		get name() {
+			return "Replace with the next size up";
+		}
 
-	get healthCost() {
-		return 10;
+		get description() {
+			return '';
+		}
 	}
 
-	apply(cheat) {
-		this._slave.lipsImplant += 20;
-		this._slave.lips += 20;
-		return this._assemble(new App.Medicine.Surgery.Reactions.Lips());
-	}
-};
+	/**
+	 * @template {FC.Medicine.SizingImplantProcedureConstructor} T
+	 * @param {T} Procedure
+	 * @param {ConstructorParameters<T>} args
+	 */
+	function makeProcedure(Procedure, args) {
+		return new (class extends Procedure {
+			_doTargetGain() {
+				return new App.Medicine.Surgery.Reactions.Lips();
+			}
 
-App.Medicine.Surgery.Procedures.AddLipImplant = class extends App.Medicine.Surgery.Procedure {
-	get name() {
-		return "Lip implant";
-	}
+			_doTargetLoss() {
+				return new App.Medicine.Surgery.Reactions.Lips();
+			}
 
-	get healthCost() {
-		return 10;
-	}
+			/**
+			 * @param {number} volume
+			 * @returns {string}
+			 */
+			_volumeStr(volume) {
+				return '';
+			}
 
-	apply(cheat) {
-		this._slave.lipsImplant = 20;
-		this._slave.lips += 20;
-		return this._assemble(new App.Medicine.Surgery.Reactions.Lips());
-	}
-};
+			/**
+			 * @param {number} volume
+			 * @returns {string}
+			 */
+			_fleshExcessStr(volume) {
+				return App.Utils.expandHTML(`${Math.round(100. * volume / App.Medicine.fleshSize(this.originalSlave, "lips"))}&thinsp;%`);
+			}
 
-App.Medicine.Surgery.Procedures.RemoveLipImplant = class extends App.Medicine.Surgery.Procedure {
-	get name() {
-		return "Remove lip implants";
+			// overridden because slaves do not have the lipsImplantType property (yet?)
+			_setSlaveImplantType() {}
+		})(...args);
 	}
 
-	get healthCost() {
-		return 10;
-	}
+	class Reduce extends App.Medicine.Surgery.Procedure {
+		/**
+		 * @param {FC.SlaveState} slave
+		 * @param {string} name
+		 * @param {number} amount
+		 */
+		constructor(slave, name, amount) {
+			super(slave);
+			this._name = name;
+			this._amount = amount;
+		}
 
-	apply(cheat) {
-		this._slave.lips -= this._slave.lipsImplant;
-		this._slave.lipsImplant = 0;
-		if (this._slave.skill.oral > 10) {
-			this._slave.skill.oral -= 10;
+		get name() {
+			return `${this._name.toUpperFirst()} lips`;
 		}
-		return this._assemble(new App.Medicine.Surgery.Reactions.Lips());
-	}
-};
 
-App.Medicine.Surgery.Procedures.ReduceLips = class extends App.Medicine.Surgery.Procedure {
-	get name() {
-		return "Reduce lips";
-	}
+		get healthCost() {
+			return 10;
+		}
 
-	get healthCost() {
-		return 10;
+		apply(cheat) {
+			this._slave.lips -= this._amount;
+			return this._assemble(new App.Medicine.Surgery.Reactions.Lips());
+		}
 	}
 
-	apply(cheat) {
-		this._slave.lips -= 10;
-		return this._assemble(new App.Medicine.Surgery.Reactions.Lips());
-	}
-};
+	return {
+		install: (slave, type, size, fleshExcess) =>
+			makeProcedure(App.Medicine.Surgery.Procedures.InstallSizingImplantProcedure, [slave, "lips", type, size, fleshExcess]),
+		replace: (slave, type, size, fleshExcess) =>
+			makeProcedure(App.Medicine.Surgery.Procedures.ReplaceSizingImplantProcedure, [slave, "lips", type, size, fleshExcess]),
+		remove: (slave) =>
+			makeProcedure(App.Medicine.Surgery.Procedures.RemoveSizingImplantProcedure, [slave, "lips"]),
+
+		fill: (slave, amount) =>
+			makeProcedure(Fill, [slave, "lips", amount]),
+		drain: (slave, amount) =>
+			makeProcedure(App.Medicine.Surgery.Procedures.DrainSizingImplantProcedure, [slave, "lips", amount]),
+
+		reduce: (slave, name, amount) => new Reduce(slave, name, amount)
+	};
+}();
diff --git a/js/medicine/surgery/structural/amputation.js b/js/medicine/surgery/structural/amputation.js
index 9a523c9ab505a7fbce1361035e94c262b94296a3..cc058af0d72f337ab8775be238a9aef0b95b2432 100644
--- a/js/medicine/surgery/structural/amputation.js
+++ b/js/medicine/surgery/structural/amputation.js
@@ -270,8 +270,8 @@ App.Medicine.Surgery.Procedures.Amputate = class extends App.Medicine.Surgery.Pr
 		return this.count * 10;
 	}
 
-	get cost() {
-		return super.cost * this.count;
+	get _workCost() {
+		return super._workCost * this.count;
 	}
 
 	apply(cheat) {
diff --git a/js/medicine/utility.js b/js/medicine/utility.js
new file mode 100644
index 0000000000000000000000000000000000000000..a7e2ba4d7cd20a6b08d4fb70db9883b0643661bb
--- /dev/null
+++ b/js/medicine/utility.js
@@ -0,0 +1,141 @@
+App.Medicine.pocketVolume = function() {
+	/**
+	 * @param {FC.HumanState} slave
+	 * @returns {number}
+	 */
+	function boobs(slave) {
+		/*
+		The breast is approximated by a spherical cap with the volume of slave.boobs.
+		The radius `a` of the cap is proportional to slave height and shoulders width
+		We assume that the skin has elasticity coefficient that depends on slave physical age.
+		Max pocket volume is therefore the volume of the cap with the same base but the
+		surface stretched by the skin elasticity coefficient
+		V = 1/6*πh(3a²+h²)
+		A = π(a²+h²)
+		*/
+		const a = 0.05 * slave.height + slave.shoulders * 1.1;
+		/* now the eq. for V we rewrite as
+		h³ + 3a³h -6/π*V = 0
+		and replace:
+		p = 3*a³, q = -6/π*V:
+		h³ + ph + q = 0
+		*/
+		const p = 3 * a * a;
+		const q = -6. / Math.PI * slave.boobs;
+		const Q = Math.pow(p / 3, 3) + Math.pow(q / 2, 2);
+		console.assert(Q > 0);
+		const h = Math.cbrt(-q / 2 + Math.sqrt(Q)) + Math.cbrt(-q / 2 - Math.sqrt(Q));
+
+		// now the current skin area:
+		const A = Math.PI * (a * a + h * h);
+
+		const skinStretchFactor = linearInterpolation(Math.max(slave.physicalAge, 13), 13, 1.5, 45, 1.15);
+
+		const hMax = Math.sqrt(A * skinStretchFactor / Math.PI - a * a);
+		const VMax = Math.PI / 6 * hMax * (3 * a * a + hMax * hMax);
+
+		return VMax;
+	}
+
+	/**
+	 * @param {FC.HumanState} slave
+	 * @returns {number}
+	 */
+	function butt(slave) {
+		return slave.butt + (slave.hips > 0 ? 2 : 1);
+	}
+
+	/**
+	 * @param {FC.HumanState} slave
+	 * @returns {number}
+	 */
+	function lips(slave) {
+		return slave.lips + 10;
+	}
+
+	return {
+		boobs,
+		butt,
+		lips
+	};
+}();
+
+/**
+ * @template {FC.SizingImplantTarget} T
+ * @param {FC.HumanState} slave
+ * @param {T} target
+ * @returns {FC.Medicine.ImplantInfo<T>}
+ */
+App.Medicine.implantInfo = function(slave, target) {
+	/**
+	 * @template {"boobs"|"butt"} Target
+	 * @param {Target} target
+	 * @returns FC.Medicine.ImplantInfo<Target>
+	 */
+	function boobsButt(target) {
+		return {
+			type: slave[`${target}ImplantType`],
+			volume: slave[`${target}Implant`]
+		};
+	}
+
+	switch (target) {
+		case "boobs":
+		case "butt":
+			return /** @type {FC.Medicine.ImplantInfo<T>} */(boobsButt(target));
+		case "lips":
+			return {
+				type: slave.lipsImplant > 0 ? "normal" : "none",
+				volume: slave.lipsImplant
+			};
+	}
+};
+
+/**
+ * @template {FC.SizingImplantTarget} T
+ * @param {T} target
+ * @returns {Array<FC.BodyPartImplantTypeMap[T]>}
+ */
+App.Medicine.implantTypesForTarget = function(target) {
+	return Object.keys(App.Data.Medicine.sizingImplants[target]);
+};
+
+/**
+ * Returns the size of the flesh part of the sizable body part
+ * @param {FC.SlaveState} slave
+ * @param {FC.SizableBodyPart} part
+ * @returns {number}
+ */
+App.Medicine.fleshSize = function(slave, part) {
+	switch (part) {
+		case "balls": return slave.balls;
+		case "boobs": return slave.boobs - slave.boobsImplant - slave.boobsMilk;
+		case "butt": return slave.butt - slave.buttImplant;
+		case "dick": return slave.dick;
+		case "lips": return slave.lips - slave.lipsImplant;
+	}
+};
+
+/**
+ * Returns the size of the flesh part of the sizable body part
+ * @param {FC.SlaveState} slave
+ * @param {FC.SizableBodyPart} part
+ * @returns {number}
+ */
+App.Medicine.assetSize = function(slave, part) {
+	return slave[part];
+};
+
+/**
+ * @param {FC.SizableBodyPart} part
+ * @returns {number}
+ */
+App.Medicine.maxAssetSize = function(part) {
+	switch (part) {
+		case "balls": return 125;
+		case "boobs": return 50000;
+		case "butt": return 20;
+		case "dick": return 31;
+		case "lips": return V.seeExtreme ? 96 : 86;
+	}
+};
diff --git a/js/utils.js b/js/utils.js
index 985320decf2f6bf949ef129334ece9db35936ba7..4ee72ebf6bced819a356a55e31f26f4e3e07c9f9 100644
--- a/js/utils.js
+++ b/js/utils.js
@@ -240,6 +240,21 @@ App.Utils.escapeHtml = function(text) {
 	return text.replace(/[&<>"']/g, m => map[m]);
 };
 
+App.Utils.expandHTML = (function() {
+	const element = document.createElement("div");
+
+	/**
+	 * @param {string} s
+	 * @returns {string}
+	 */
+	function expand(s) {
+		element.innerHTML = s;
+		return element.textContent;
+	}
+
+	return expand;
+})();
+
 /**
  * Creates an object where the items are accessible via their ids.
  *
diff --git a/src/002-config/fc-version.js b/src/002-config/fc-version.js
index e4c90a3508afde45bd8780e4c74898e846dc3805..64f8d2dd005891515096355d71300d65b74441e8 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.12",
 	commitHash: null,
-	release: 1156 // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js.
+	release: 1156, // 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/data/backwardsCompatibility/datatypeCleanup.js b/src/data/backwardsCompatibility/datatypeCleanup.js
index d04c5007c76eb7ef7a17a8dc9575f3a6ff6b0324..0afc936fb02554674492c2c51968b5b924dfa709 100644
--- a/src/data/backwardsCompatibility/datatypeCleanup.js
+++ b/src/data/backwardsCompatibility/datatypeCleanup.js
@@ -2298,20 +2298,20 @@ App.Entity.Utils.RARuleDatatypeCleanup = function() {
 		 * Converts numeric target to range.
 		 * Conversion is valid only for integer targets
 		 * @param {FC.RA.NumericTarget} tgt
-		 * @returns {FC.RA.NumericRange}
+		 * @returns {FC.NumericRange}
 		 */
 		function targetToRange(tgt) {
 			switch (tgt.cond) {
 				case '==':
-					return App.RA.makeRange(tgt.val, tgt.val);
+					return App.Utils.makeRange(tgt.val, tgt.val);
 				case "<=":
-					return App.RA.makeRange(Number.NEGATIVE_INFINITY, tgt.val);
+					return App.Utils.makeRange(Number.NEGATIVE_INFINITY, tgt.val);
 				case "<":
-					return App.RA.makeRange(Number.NEGATIVE_INFINITY, tgt.val - 1);
+					return App.Utils.makeRange(Number.NEGATIVE_INFINITY, tgt.val - 1);
 				case ">=":
-					return App.RA.makeRange(tgt.val, Number.POSITIVE_INFINITY);
+					return App.Utils.makeRange(tgt.val, Number.POSITIVE_INFINITY);
 				case ">":
-					return App.RA.makeRange(tgt.val + 1, Number.POSITIVE_INFINITY);
+					return App.Utils.makeRange(tgt.val + 1, Number.POSITIVE_INFINITY);
 			}
 		}
 
@@ -2337,7 +2337,7 @@ App.Entity.Utils.RARuleDatatypeCleanup = function() {
 
 		// moving numeric diets to the 'weight' attribute
 		if (typeof set.diet === 'number') {
-			set.weight = App.RA.makeRange(set.diet, set.diet);
+			set.weight = App.Utils.makeRange(set.diet, set.diet);
 			set.diet = null;
 		}
 
diff --git a/src/endWeek/saLiveWithHG.js b/src/endWeek/saLiveWithHG.js
index ec905845d3810fc5c9ec7a30e78b389dd4987652..5a45e74790549e956bed8f7c89426f0ca9e96d7a 100644
--- a/src/endWeek/saLiveWithHG.js
+++ b/src/endWeek/saLiveWithHG.js
@@ -1393,56 +1393,56 @@ App.SlaveAssignment.liveWithHG = (function() {
 				thisSurgery.lactation = 0;
 				thisSurgery.cosmetic = 1;
 				thisSurgery.faceShape = "cute";
-				thisSurgery.lips = App.RA.makeRange(10, 10);
+				thisSurgery.lips = App.Utils.makeRange(10, 10);
 				thisSurgery.hips = 0;
 				thisSurgery.hipsImplant = 0;
-				thisSurgery.butt = App.RA.makeRange(0, 0);
+				thisSurgery.butt = App.Utils.makeRange(0, 0);
 				thisSurgery.accent = 0;
 				thisSurgery.shoulders = 0;
 				thisSurgery.shouldersImplant = 0;
-				thisSurgery.boobs = App.RA.makeRange(0, 0);
+				thisSurgery.boobs = App.Utils.makeRange(0, 0);
 				thisSurgery.holes = 0;
 				break;
 			case 2:
 				thisSurgery.lactation = 0;
 				thisSurgery.cosmetic = 1;
 				thisSurgery.faceShape = "cute";
-				thisSurgery.lips = App.RA.makeRange(60, 60);
+				thisSurgery.lips = App.Utils.makeRange(60, 60);
 				thisSurgery.hips = 0;
 				thisSurgery.hipsImplant = 0;
-				thisSurgery.butt = App.RA.makeRange(4, 4);
+				thisSurgery.butt = App.Utils.makeRange(4, 4);
 				thisSurgery.accent = 0;
 				thisSurgery.shoulders = 0;
 				thisSurgery.shouldersImplant = 0;
-				thisSurgery.boobs = App.RA.makeRange(1200, 1200);
+				thisSurgery.boobs = App.Utils.makeRange(1200, 1200);
 				thisSurgery.holes = 0;
 				break;
 			case 3:
 				thisSurgery.lactation = 0;
 				thisSurgery.cosmetic = 1;
 				thisSurgery.faceShape = "cute";
-				thisSurgery.lips = App.RA.makeRange(95, 95);
+				thisSurgery.lips = App.Utils.makeRange(95, 95);
 				thisSurgery.hips = 0;
 				thisSurgery.hipsImplant = 0;
-				thisSurgery.butt = App.RA.makeRange(8, 8);
+				thisSurgery.butt = App.Utils.makeRange(8, 8);
 				thisSurgery.accent = 0;
 				thisSurgery.shoulders = 0;
 				thisSurgery.shouldersImplant = 0;
-				thisSurgery.boobs = App.RA.makeRange(10000, 10000);
+				thisSurgery.boobs = App.Utils.makeRange(10000, 10000);
 				thisSurgery.holes = 2;
 				break;
 			case 4:
 				thisSurgery.lactation = 1;
 				thisSurgery.cosmetic = 1;
 				thisSurgery.faceShape = "cute";
-				thisSurgery.lips = App.RA.makeRange(10, 10);
+				thisSurgery.lips = App.Utils.makeRange(10, 10);
 				thisSurgery.hips = 3;
 				thisSurgery.hipsImplant = 0;
-				thisSurgery.butt = App.RA.makeRange(0, 0);
+				thisSurgery.butt = App.Utils.makeRange(0, 0);
 				thisSurgery.accent = 0;
 				thisSurgery.shoulders = 0;
 				thisSurgery.shouldersImplant = 0;
-				thisSurgery.boobs = App.RA.makeRange(0, 0);
+				thisSurgery.boobs = App.Utils.makeRange(0, 0);
 				thisSurgery.holes = 0;
 				break;
 			default:
diff --git a/src/facilities/surgery/surgeryPassageFaceAndHair.js b/src/facilities/surgery/surgeryPassageFaceAndHair.js
index e5b15ef77d03f44172d3795fff1fad4505838b25..ffe425dfae92be8ac31495c5557e25616daad15c 100644
--- a/src/facilities/surgery/surgeryPassageFaceAndHair.js
+++ b/src/facilities/surgery/surgeryPassageFaceAndHair.js
@@ -536,13 +536,13 @@ App.UI.surgeryPassageHairAndFace = function(slave, refreshParent, cheat = false)
 
 			if (slave.earImplant !== 1) {
 				if (slave.hears === -1) {
-					if (slave.earImplant !== 1 && slave.earShape !== "none") {
+					if (slave.earShape !== "none") {
 						linkArray.push(App.Medicine.Surgery.makeLink(
 							new App.Medicine.Surgery.Procedures.EarFix(slave),
 							refresh, cheat));
 					}
 				} else if (slave.hears === 0) {
-					if (V.seeExtreme === 1 && slave.earImplant !== 1 && slave.indentureRestrictions < 1) {
+					if (V.seeExtreme === 1 && slave.indentureRestrictions < 1) {
 						linkArray.push(App.Medicine.Surgery.makeLink(
 							new App.Medicine.Surgery.Procedures.EarMuffle(slave),
 							refresh, cheat));
@@ -634,36 +634,13 @@ App.UI.surgeryPassageHairAndFace = function(slave, refreshParent, cheat = false)
 				r.push(`${He} has moderate lip implants.`);
 			}
 			r.push(App.UI.DOM.makeElement("span", `New implants will reduce ${his} oral skills`, "note"));
+
 			App.Events.addNode(el, r, "div");
 
-			if (slave.indentureRestrictions >= 2) {
-				App.UI.DOM.appendNewElement("div", el, `${His} indenture forbids elective surgery`, ["choices", "note"]);
-			} else if ((slave.lips <= 75) || ((slave.lips <= 95) && (V.seeExtreme === 1))) {
-				if (slave.lipsImplant > 0) {
-					linkArray.push(App.Medicine.Surgery.makeLink(
-						new App.Medicine.Surgery.Procedures.IncreaseLipImplant(slave),
-						refresh, cheat));
-				} else {
-					linkArray.push(App.Medicine.Surgery.makeLink(
-						new App.Medicine.Surgery.Procedures.AddLipImplant(slave),
-						refresh, cheat));
-				}
-			}
-			if (slave.lipsImplant !== 0) {
-				if (slave.indentureRestrictions < 2) {
-					linkArray.push(App.Medicine.Surgery.makeLink(
-						new App.Medicine.Surgery.Procedures.RemoveLipImplant(slave),
-						refresh, cheat));
-				}
-			}
-			if (slave.lips >= 10 && slave.lipsImplant === 0) {
-				if (slave.indentureRestrictions < 2) {
-					linkArray.push(App.Medicine.Surgery.makeLink(
-						new App.Medicine.Surgery.Procedures.ReduceLips(slave),
-						refresh, cheat));
-				}
-			}
-			App.UI.DOM.appendNewElement("div", el, App.UI.DOM.generateLinksStrip(linkArray), "choices");
+			const surgeries = App.Medicine.Surgery.sizingProcedures.lips(slave, App.Medicine.Surgery.allSizingOptions());
+			const surgeryLinks = surgeries.map(s => App.Medicine.Surgery.makeLink(s, refresh, cheat));
+			App.UI.DOM.appendNewElement("div", el, (App.UI.DOM.generateLinksStrip(surgeryLinks)), ["choices"]);
+
 			return el;
 		}
 
diff --git a/src/futureSocieties/futureSociety.js b/src/futureSocieties/futureSociety.js
index 20967c98c1ce532b7dcb103bfbcb3594c6b0df39..e96e585fdbbb55b5096204f8b96a64e917cea18a 100644
--- a/src/futureSocieties/futureSociety.js
+++ b/src/futureSocieties/futureSociety.js
@@ -26,7 +26,8 @@ globalThis.FutureSocieties = (function() {
 		DecorationBonus: FSDecorationBonus,
 		Change: FSChange,
 		HighestDecoration: FSHighestDecoration,
-		arcSupport: arcSupport
+		arcSupport: arcSupport,
+		researchAvailable,
 	};
 
 	/** get the list of FSes active for a particular arcology
@@ -798,4 +799,14 @@ globalThis.FutureSocieties = (function() {
 			}
 		}
 	}
+
+	/**
+	 *
+	 * @param {FC.FSName<keyof FC.FutureSocietyWithResearchMap>} fs
+	 * @param {FC.ArcologyState}[arcology] Arcology to test, defaults to the PC's arcology
+	 * @returns {boolean}
+	 */
+	function researchAvailable(fs, arcology) {
+		return (arcology || V.arcologies[0])[`FS${fs}Research`] === 1;
+	}
 })();
diff --git a/src/js/DefaultRules.js b/src/js/DefaultRules.js
index c0942163003f466baecbd1d649b2bf2f9a8ff271..627318aad64b6791eaf9e1477ca2852c7c628175 100644
--- a/src/js/DefaultRules.js
+++ b/src/js/DefaultRules.js
@@ -1193,30 +1193,11 @@ globalThis.DefaultRules = (function() {
 			return;
 		}
 
-		/** @typedef {"lips" | "boobs" | "butt" | "dick" | "balls"} DrugTarget */
 		// Asset Growth
 		const growthDrugs = 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"]);
 
-		// WARNING: property names in fleshFunc, growDrugs, and shrinkDrugs must be identical and this fact is used by the drugs() below
-		/** @type {Record<DrugTarget, (s: App.Entity.SlaveState) => number>} */
-		const fleshFunc = {
-			lips: s => s.lips - s.lipsImplant,
-			boobs: s => s.boobs - s.boobsImplant - s.boobsMilk,
-			butt: s => s.butt - s.buttImplant,
-			dick: s => s.dick,
-			balls: s => s.balls,
-		};
-
-		/** @type {Record<DrugTarget, number>} */
-		const maxAssetSize = {
-			lips: V.seeExtreme ? 95 : 85,
-			boobs: 48000,
-			butt: 20,
-			dick: 31,
-			balls: 125
-		};
-
-		/** @type {Record<DrugTarget, FC.Drug>} */
+		// WARNING: 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",
@@ -1252,7 +1233,7 @@ globalThis.DefaultRules = (function() {
 			}
 		}
 
-		/** @type {Record<DrugTarget, FC.Drug>} */
+		/** @type {Record<FC.SizableBodyPart, FC.Drug>} */
 		const shrinkDrugs = {
 			lips: null,
 			boobs: null,
@@ -1277,8 +1258,8 @@ globalThis.DefaultRules = (function() {
 
 		/**
 		 * @param {App.Entity.SlaveState} slave
-		 * @param {DrugTarget} asset
-		 * @param {FC.RA.NumericTarget | FC.RA.ExpressiveNumericTarget} target
+		 * @param {FC.SizableBodyPart} asset
+		 * @param {FC.RA.ExpressiveNumericTarget} target
 		 * @param {{drug: FC.Drug, weight: number}[]} priorities
 		 * @param {number} step
 		 */
@@ -1287,16 +1268,28 @@ globalThis.DefaultRules = (function() {
 				return;
 			}
 
-			if (V.experimental.raGrowthExpr === 1 && typeof target.val === 'string') {
+			if (typeof target.val === 'string') {
 				const interpreter = new Function("slave", "return (" + target.val + ");");
-				target.val = runWithReadonlyProxy(() => interpreter(createReadonlyProxy(slave)));
+				const newVal = runWithReadonlyProxy(() => interpreter(createReadonlyProxy(slave)));
 				if (V.debugMode) {
-					console.log(asset + " expression for '" + slave.slaveName + "' resolves to " + target.val.toString());
+					console.log(asset + " expression for '" + slave.slaveName + "' resolves to " + newVal.toString());
 				}
+				return drugsImpl(slave, asset, {cond: target.cond, val: newVal}, priorities, step);
 			}
 
-			const flesh = fleshFunc[asset](slave);
-			if (growDrugs[asset] !== null && App.RA.shallGrow(flesh, target, step) && maxAssetSize[asset] > slave[asset]) {
+			return drugsImpl(slave, asset, {cond: target.cond, val: target.val}, priorities, step);
+		}
+
+		/**
+		 * @param {App.Entity.SlaveState} slave
+		 * @param {FC.SizableBodyPart} asset
+		 * @param {FC.RA.NumericTarget} target
+		 * @param {{drug: FC.Drug, weight: number}[]} priorities
+		 * @param {number} step
+		 */
+		 function drugsImpl(slave, asset, target, priorities, step) {
+			const flesh = App.Medicine.fleshSize(slave, asset);
+			if (growDrugs[asset] !== null && App.RA.shallGrow(flesh, target, step) && App.Medicine.maxAssetSize(asset) > slave[asset]) {
 				priorities.push({
 					drug: growDrugs[asset],
 					weight: 1.0 - (flesh / target.val)
diff --git a/src/js/rulesAssistant.js b/src/js/rulesAssistant.js
index cfd2061ed5eb7d9fe0587215acc63072bc44cba7..455c1baf15b755021f050c8fdb15cb2b21acbe4d 100644
--- a/src/js/rulesAssistant.js
+++ b/src/js/rulesAssistant.js
@@ -371,10 +371,12 @@ App.RA.newRule = function() {
 			shouldersImplant: null,
 			boobs: null,
 			boobsImplantTypes: null,
+			boobsImplantAllowReplacing: true,
 			hips: null,
 			hipsImplant: null,
 			butt: null,
 			buttImplantTypes: null,
+			buttImplantAllowReplacing: true,
 			faceShape: null,
 			lips: null,
 			holes: null,
@@ -419,33 +421,6 @@ App.RA.makeTarget = function(condition, val) {
 	};
 };
 
-/**
- * Creates RA range object used in rules
- * @param {number} minValue
- * @param {number} maxValue
- * @returns {FC.RA.NumericRange}
- */
-App.RA.makeRange = function(minValue, maxValue) {
-	return {
-		min: minValue, max: maxValue
-	};
-};
-
-/**
- * Compares value to a range
- * @param {number} value
- * @param {FC.RA.NumericRange} range
- * @returns {number} The value which when added to `value` brings it within the range [min:max]
- * `positive` when value is less than range min, `0` when the value is whithin the range or
- * the range is `null`, and `negative` when value is greater than range max
- */
-App.RA.distanceToRange = function(value, range) {
-	if (!range) {
-		return 0;
-	}
-	return value < range.min ? range.min - value : (value > range.max ? range.max - value : 0);
-};
-
 /**
  * Shall the current value be increased according to the target and condition
  * @param {number} current
diff --git a/src/js/rulesAssistantOptions.js b/src/js/rulesAssistantOptions.js
index 24c789b8b14b3546c0092e68e95abba5952c2d5a..8187ca4c2e3a4c9c7e799af158677602361b9b04 100644
--- a/src/js/rulesAssistantOptions.js
+++ b/src/js/rulesAssistantOptions.js
@@ -824,7 +824,7 @@ App.RA.options = (function() {
 	class NumericRangeEditor extends EditorWithShortcuts {
 		/**
 		 * @param {string} prefix
-		 * @param {Array<[string, FC.RA.NumericRange]} [data=[]]
+		 * @param {Array<[string, FC.NumericRange]} [data=[]]
 		 * @param {boolean} [allowNullValue=true]
 		 * @param {number} [min=0]
 		 * @param {number} [max=100]
@@ -891,7 +891,7 @@ App.RA.options = (function() {
 			const vMin = parse(this._minEditor.value);
 			const vMax = parse(this._maxEditor.value);
 			return (vMin === null && vMax === null) ? null
-				: App.RA.makeRange(vMin !== null ? vMin : this._min, vMax !== null ? vMax : this._max);
+				: App.Utils.makeRange(vMin !== null ? vMin : this._min, vMax !== null ? vMax : this._max);
 		}
 
 		setTextValue(what) {
@@ -1937,8 +1937,10 @@ App.RA.options = (function() {
 			this.appendChild(new LipSurgeryList());
 			this.appendChild(new ButtSurgeryList());
 			this.appendChild(new SizingImplantType("butt", "butt"));
+			this.appendChild(new SizingImplantAllowReplacing("butt", "butt"));
 			this.appendChild(new BreastSurgeryList());
 			this.appendChild(new SizingImplantType("breast", "boobs"));
+			this.appendChild(new SizingImplantAllowReplacing("breast", "boobs"));
 			this.appendChild(new HipsAndShoulderSurgeryList("shoulders", "shoulders"));
 			this.appendChild(new HipsAndShoulderSurgeryList("pelvis", "hips"));
 			this.appendChild(new TighteningSurgeryList());
@@ -2698,16 +2700,16 @@ App.RA.options = (function() {
 		constructor() {
 			const hdp = V.arcologies[0].FSHedonisticDecadence !== "unset";
 			const pairs = [
-				["emaciated", App.RA.makeRange(-100, -96)],
-				["very thin", App.RA.makeRange(-95, -31)],
-				["pleasingly thin", App.RA.makeRange(-30, -11)],
-				["healthy", App.RA.makeRange(-10, 10)],
-				["nicely plush", App.RA.makeRange(11, 30)],
-				[hdp ? "quite curvy" : "chubby", App.RA.makeRange(31, 95)],
-				[hdp ? "extremely curvy" : "overweight", App.RA.makeRange(96, 130)],
-				[hdp ? "amazingly curvy" : "very overweight", App.RA.makeRange(131, 160)],
-				[hdp ? "spectacularly curvy" : "extremely overweight", App.RA.makeRange(161, 190)],
-				[hdp ? "perfectly curvy" : "dangerously overweight", App.RA.makeRange(191, 200)]
+				["emaciated", App.Utils.makeRange(-100, -96)],
+				["very thin", App.Utils.makeRange(-95, -31)],
+				["pleasingly thin", App.Utils.makeRange(-30, -11)],
+				["healthy", App.Utils.makeRange(-10, 10)],
+				["nicely plush", App.Utils.makeRange(11, 30)],
+				[hdp ? "quite curvy" : "chubby", App.Utils.makeRange(31, 95)],
+				[hdp ? "extremely curvy" : "overweight", App.Utils.makeRange(96, 130)],
+				[hdp ? "amazingly curvy" : "very overweight", App.Utils.makeRange(131, 160)],
+				[hdp ? "spectacularly curvy" : "extremely overweight", App.Utils.makeRange(161, 190)],
+				[hdp ? "perfectly curvy" : "dangerously overweight", App.Utils.makeRange(191, 200)]
 			];
 			super("Weight", pairs, true, -100, 200);
 			this.setValue(current_rule.set.weight);
@@ -4092,11 +4094,11 @@ App.RA.options = (function() {
 	class LipSurgeryList extends NumericRangeEditor {
 		constructor() {
 			const items = [
-				["removed", App.RA.makeRange(0, 0)],
-				["plush", App.RA.makeRange(20, 20)],
-				["big", App.RA.makeRange(40, 40)],
-				["huge", App.RA.makeRange(70, 70)],
-				["facepussy", App.RA.makeRange(95, 95)],
+				["removed", App.Utils.makeRange(0, 0)],
+				["plush", App.Utils.makeRange(20, 20)],
+				["big", App.Utils.makeRange(40, 40)],
+				["huge", App.Utils.makeRange(70, 70)],
+				["facepussy", App.Utils.makeRange(95, 95)],
 			];
 			super("Lip implants", items, true, 0, 95);
 			this.setValue(current_rule.set.surgery.lips);
@@ -4107,11 +4109,11 @@ App.RA.options = (function() {
 	class ButtSurgeryList extends NumericRangeEditor {
 		constructor() {
 			const items = [
-				["removed", App.RA.makeRange(0, 0)],
-				["slim", App.RA.makeRange(2, 2)],
-				["stacked", App.RA.makeRange(4, 4)],
-				["huge", App.RA.makeRange(6, 6)],
-				["maximized", App.RA.makeRange(9, 9)],
+				["removed", App.Utils.makeRange(0, 0)],
+				["slim", App.Utils.makeRange(2, 2)],
+				["stacked", App.Utils.makeRange(4, 4)],
+				["huge", App.Utils.makeRange(6, 6)],
+				["maximized", App.Utils.makeRange(9, 9)],
 			];
 			super("Buttock implants", items, true, 0, 9);
 			this.setValue(current_rule.set.surgery.butt);
@@ -4122,12 +4124,12 @@ App.RA.options = (function() {
 	class BreastSurgeryList extends NumericRangeEditor {
 		constructor() {
 			const items = [
-				["removed", App.RA.makeRange(0, 0)],
-				["slim", App.RA.makeRange(400, 400)],
-				["stacked", App.RA.makeRange(1000, 1000)],
-				["huge", App.RA.makeRange(2000, 2000)],
-				["barely functional", App.RA.makeRange(9000, 9000)],
-				["maximized", App.RA.makeRange(48000, 48000)]
+				["removed", App.Utils.makeRange(0, 0)],
+				["slim", App.Utils.makeRange(400, 400)],
+				["stacked", App.Utils.makeRange(1000, 1000)],
+				["huge", App.Utils.makeRange(2000, 2000)],
+				["barely functional", App.Utils.makeRange(9000, 9000)],
+				["maximized", App.Utils.makeRange(48000, 48000)]
 			];
 			super("Breast implants", items, true, 0, 48000, true);
 			this.setValue(current_rule.set.surgery.boobs);
@@ -4173,6 +4175,19 @@ App.RA.options = (function() {
 		}
 	}
 
+	class SizingImplantAllowReplacing extends BooleanSwitch {
+		/**
+		 * @param {string} label
+		 * @param {FC.SizingImplantTarget} target
+		 */
+		constructor(label, target) {
+			super(`Replace other types of ${label} implants`);
+
+			this.setValue(current_rule.set.surgery[`${target}ImplantAllowReplacing`]);
+			this.onchange = (value) => current_rule.set.surgery[`${target}ImplantAllowReplacing`] = value;
+		}
+	}
+
 	class TighteningSurgeryList extends RadioSelector {
 		constructor() {
 			const items = [
diff --git a/src/js/rulesAutosurgery.js b/src/js/rulesAutosurgery.js
index 0c890fbf10d74c83c0846b8b3093ad53f891982f..e41e8867b63e1dfcae0441d15808eed2144b699c 100644
--- a/src/js/rulesAutosurgery.js
+++ b/src/js/rulesAutosurgery.js
@@ -48,22 +48,22 @@ globalThis.rulesAutosurgery = (function() {
 				.map(x => x.set));
 		if ((thisSurgery.hips !== null) && (thisSurgery.butt !== null)) {
 			if (slave.hips < -1) {
-				if (App.RA.distanceToRange(2, thisSurgery.butt) > 0) {
-					thisSurgery.butt = App.RA.makeRange(2, 2);
+				if (App.Utils.distanceToRange(2, thisSurgery.butt) > 0) {
+					thisSurgery.butt = App.Utils.makeRange(2, 2);
 				}
 			} else if (slave.hips < 0) {
-				if (App.RA.distanceToRange(4, thisSurgery.butt) > 0) {
-					thisSurgery.butt = App.RA.makeRange(4, 4);
+				if (App.Utils.distanceToRange(4, thisSurgery.butt) > 0) {
+					thisSurgery.butt = App.Utils.makeRange(4, 4);
 				}
 			} else if (slave.hips > 0) {
-				if (App.RA.distanceToRange(8, thisSurgery.butt) > 0) {
-					thisSurgery.butt = App.RA.makeRange(8, 8);
+				if (App.Utils.distanceToRange(8, thisSurgery.butt) > 0) {
+					thisSurgery.butt = App.Utils.makeRange(8, 8);
 				}
 			} else if (slave.hips > 1) {
 				// true
 			} else {
-				if (App.RA.distanceToRange(6, thisSurgery.butt) > 0) {
-					thisSurgery.butt = App.RA.makeRange(8, 6);
+				if (App.Utils.distanceToRange(6, thisSurgery.butt) > 0) {
+					thisSurgery.butt = App.Utils.makeRange(8, 6);
 				}
 			}
 		}
@@ -96,73 +96,49 @@ globalThis.rulesAutosurgery = (function() {
 		 * @param {App.Medicine.Surgery.Procedure} proc
 		 */
 		function procedureEfficiency(proc) {
-			const value = typeof proc.changeValue === "number" ? proc.changeValue : 1.;
-			return (value / proc.cost + value/proc.healthCost);
+			const value = typeof proc.changeValue === "number" ? Math.abs(proc.changeValue) : 1.;
+			return (value / proc.cost + value / proc.healthCost);
 		}
 
 		/**
-		 * @param {"boobs"|"butt"} bodyPart
-		 * @param {!FC.RA.NumericRange} range
+		 * @param {FC.SizingImplantTarget} bodyPart
+		 * @param {!FC.NumericRange} range
 		 * @param {FC.SizingImplantType[]} implantTypes
+		 * @param {boolean} replaceImplants
 		 */
-		function bodyPartSizing(bodyPart, range, implantTypes) {
-			const distance = App.RA.distanceToRange(slave[`${bodyPart}Implant`], range);
+		function bodyPartSizing(bodyPart, range, implantTypes, replaceImplants) {
+			const current = App.Medicine.implantInfo(slave, bodyPart);
+			const distance = App.Utils.distanceToRange(current.volume, range);
 			const shallShrink = distance < 0;
 			const shallGrow = distance > 0;
-			/** @type {FC.Medicine.Surgery.SizingOptions} */
-			const options = {
-				types: implantTypes && implantTypes.length ? implantTypes : null // we can't set null via RA UI :(
-			};
-			options.strictTypes = options.types && options.types.length > 0;
-			const shallReplaceImplantType = slave[`${bodyPart}Implant`] > 0 &&
-				options.types && !options.types.some(v => v === slave[`${bodyPart}ImplantType`]);
-			/** @type {(left: App.Medicine.Surgery.Procedure, right: App.Medicine.Surgery.Procedure) => number} */
-			let sorter;
-			if (shallShrink) {
-				if (range.max === 0) {
-					options.types = options.types ? [...options.types, "none"] : ["none"];
-				}
-				options.reduction = true;
-				options.replace = true;
-				sorter = (left, right) => -procedureEfficiency(right) + procedureEfficiency(left);
-			} else if (shallGrow) {
-				options.augmentation = true;
-				options.replace = true;
-				sorter = (left, right) => procedureEfficiency(right) - procedureEfficiency(left);
-			}
+			const shallReplaceImplantType = replaceImplants && (current.volume > 0) &&
+				(implantTypes !== null && !implantTypes.some(v => v === current.type));
+
 			if (!shallShrink && !shallGrow && !shallReplaceImplantType) {
 				return;
 			}
-			if (shallReplaceImplantType) {
-				options.replace = true;
-				// we allow implant removal because there might be no other way to change its type
-				options.types = options.types ? [...options.types, "none"] : ["none"];
-				options.reduction = true;
-				sorter = (left, right) => procedureEfficiency(right) - procedureEfficiency(left);
-			}
 
-			/**
-			 * test if taking the option does not invalidate sizing condition
-			 * @param {App.Medicine.Surgery.Procedure} opt
-			 * @returns {boolean}
-			 */
-			const testOption = opt => {
-				return shallReplaceImplantType ||
-					(shallShrink && App.RA.distanceToRange(slave[`${bodyPart}Implant`] - opt.changeValue, range) <= 0) ||
-					(shallGrow && App.RA.distanceToRange(slave[`${bodyPart}Implant`] + opt.changeValue, range) >= 0);
-			};
-
-			let surgeryOptions = App.Medicine.Surgery.sizingProcedures.bodyPart(bodyPart, slave, options);
+			let surgeryOptions = App.Medicine.Surgery.sizingProcedures.bodyPart(bodyPart, slave, {
+				allowedTypes: implantTypes ? new Set(implantTypes) : null,
+				replace: shallReplaceImplantType,
+				targetSize: range
+			});
 			surgeryOptions = surgeryOptions
-				.filter(surgery => surgery.disabledReasons.length === 0 && testOption(surgery));
+				.filter(surgery => surgery.disabledReasons.length === 0);
 			if (!surgeryOptions.length) {
 				return;
 			}
 
-			const so = surgeryOptions.sort(sorter)[0];
-			const [diff, reaction] = App.Medicine.Surgery.apply(so, false);
+			surgeryOptions.sort((a, b) => procedureEfficiency(b) - procedureEfficiency(a));
 
-			const result1 = reaction.reaction(slave, diff);
+			const so = surgeryOptions[0];
+			const surgeryResult = App.Medicine.Surgery.apply(so, false);
+			if (!surgeryResult) {
+				return;
+			}
+
+			const [diff, reaction] = surgeryResult;
+			const result1 =  reaction.reaction(slave, diff);
 			const result2 = reaction.outro(slave, diff, result1);
 
 			App.Utils.Diff.applyDiff(slave, diff);
@@ -254,11 +230,15 @@ globalThis.rulesAutosurgery = (function() {
 				}
 			});
 		} else if (thisSurgery.boobs) {
-			bodyPartSizing("boobs", thisSurgery.boobs, thisSurgery.boobsImplantTypes);
+			bodyPartSizing("boobs", thisSurgery.boobs, thisSurgery.boobsImplantTypes, thisSurgery.boobsImplantAllowReplacing);
 		}
 
 		if (thisSurgery.butt !== null) {
-			bodyPartSizing("butt", thisSurgery.butt, thisSurgery.buttImplantTypes);
+			bodyPartSizing("butt", thisSurgery.butt, thisSurgery.buttImplantTypes, thisSurgery.buttImplantAllowReplacing);
+		}
+
+		if (thisSurgery.lips != null) {
+			bodyPartSizing("lips", thisSurgery.lips, null, true);
 		}
 
 		if (slave.anus > 3 && thisSurgery.cosmetic > 0) {
@@ -352,22 +332,6 @@ globalThis.rulesAutosurgery = (function() {
 				slave.voice += 1;
 				slave.voiceImplant += 1;
 			});
-		} else if (App.RA.distanceToRange(slave.lipsImplant, thisSurgery.lips) < 0 && thisSurgery.lips.max === 0) {
-			commitProcedure(`surgery to remove ${his} lip implants`, () => {
-				slave.lips -= slave.lipsImplant;
-				slave.lipsImplant = 0;
-				if (slave.skill.oral > 10) {
-					slave.skill.oral -= 10;
-				}
-			});
-		} else if (App.RA.distanceToRange(slave.lipsImplant, thisSurgery.lips) >= 10 && slave.lips <= 95) {
-			commitProcedure("bigger lips", () => {
-				slave.lipsImplant += 10;
-				slave.lips += 10;
-				if (slave.skill.oral > 10) {
-					slave.skill.oral -= 10;
-				}
-			});
 		} 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) {
diff --git a/src/js/utilsMisc.js b/src/js/utilsMisc.js
index 1a1ad6cbb1d1723c782ad7cb8c327f6bc57218dc..bbbff013ebf80ce5f85f3eb16b29f6790c0af680 100644
--- a/src/js/utilsMisc.js
+++ b/src/js/utilsMisc.js
@@ -343,3 +343,30 @@ globalThis.addTrinket = function(name, object) {
 		V.trinkets.set(name, V.trinkets.get(name) +1);
 	}
 };
+
+/**
+ * Creates range object
+ * @param {number} minValue
+ * @param {number} maxValue
+ * @returns {FC.NumericRange}
+ */
+App.Utils.makeRange = function(minValue, maxValue) {
+	return {
+		min: minValue, max: maxValue
+	};
+};
+
+/**
+ * Compares value to a range
+ * @param {number} value
+ * @param {FC.NumericRange} [range]
+ * @returns {number} The value which when added to `value` brings it within the range [min:max]
+ * `positive` when value is less than range min, `0` when the value is whithin the range or
+ * the range is `null`, and `negative` when value is greater than range max
+ */
+App.Utils.distanceToRange = function(value, range) {
+	if (!range) {
+		return 0;
+	}
+	return value < range.min ? range.min - value : (value > range.max ? range.max - value : 0);
+};
diff --git a/src/npc/surgery/surgery.js b/src/npc/surgery/surgery.js
index d1a6c182002fe6c7c646066f9a0fb29bf66cfffe..1955aba6ca31aadbfb7548bd5db05887faf578af 100644
--- a/src/npc/surgery/surgery.js
+++ b/src/npc/surgery/surgery.js
@@ -10,8 +10,13 @@ App.Medicine.Surgery.makeLink = function(procedure, refresh, cheat, onApply) {
 		return App.UI.DOM.disabledLink(procedure.name, procedure.disabledReasons);
 	}
 
+	const note = procedure.note;
 	const slave = procedure.originalSlave;
-	return App.UI.DOM.link(procedure.name, apply, [], "", tooltip());
+	const res = App.UI.DOM.link(procedure.name, apply, [], "", tooltip());
+	if (note) {
+		res.classList.add("with-note");
+	}
+	return res;
 
 	function healthCosts() {
 		const hc = (V.PC.skill.medicine >= 100) ? Math.round(procedure.healthCost / 2) : procedure.healthCost;
@@ -29,13 +34,17 @@ App.Medicine.Surgery.makeLink = function(procedure, refresh, cheat, onApply) {
 
 	function tooltip() {
 		const tooltip = new DocumentFragment();
-		if (procedure.description !== "") {
-			App.UI.DOM.appendNewElement("div", tooltip, `${capFirstChar(procedure.description)}.`);
+		const desc = procedure.description;
+		if (desc !== "") {
+			App.UI.DOM.appendNewElement("div", tooltip, `${capFirstChar(desc)}.`);
 		}
 		if (!cheat) {
 			App.UI.DOM.appendNewElement("div", tooltip, `Surgery costs: ${cashFormat(procedure.cost)}.`);
 			App.UI.DOM.appendNewElement("div", tooltip, `Projected health damage: ${healthCosts()}.`);
 		}
+		if (note) {
+			App.UI.DOM.appendNewElement("div", tooltip, typeof note === "string" ? `${capFirstChar(note)}.` : note);
+		}
 		return tooltip;
 	}
 
@@ -134,9 +143,7 @@ App.Medicine.Surgery.apply = function(procedure, cheat) {
  */
 App.Medicine.Surgery.allSizingOptions = function() {
 	return {
-		augmentation: true,
-		reduction: true,
-		replace: true
+		replace: true,
 	};
 };
 
@@ -165,192 +172,235 @@ App.Medicine.Surgery.sizingProcedures = function() {
 	return {
 		bodyPart: bodyPart,
 		boobs: boobSizingProcedures,
-		butt: buttSizingProcedures
+		butt: buttSizingProcedures,
+		lips: lipsSizingProcedures,
 	};
 
 	/**
 	 * Returns list of available surgeries targeted at changing size of the given body part
-	 * @param {string} bodyPart
+	 * @template {FC.SizingImplantTarget} T
+	 * @param {FC.SizingImplantTarget} bodyPart
 	 * @param {App.Entity.SlaveState} slave
-	 * @param {FC.Medicine.Surgery.SizingOptions} [options]
+	 * @param {FC.Medicine.Surgery.SizingOptions<T>} [options]
 	 * @returns {App.Medicine.Surgery.Procedure[]}
 	 */
 	function bodyPart(bodyPart, slave, options) {
-		if (bodyPart === "boobs") {
-			return boobSizingProcedures(slave, options);
+		switch (bodyPart) {
+			case "boobs":
+				return boobSizingProcedures(slave, options);
+			case "butt":
+				return buttSizingProcedures(slave, options);
+			case "lips":
+				return lipsSizingProcedures(slave, options);
+			default:
+				throw Error(`No sizing procedures for ${bodyPart}`);
 		}
-		if (bodyPart === "butt") {
-			return buttSizingProcedures(slave, options);
+	}
+
+	/**
+	 * @template {FC.SizingImplantTarget} T
+	 * @param {FC.BodyPartInstalledImplantType<T>} type
+	 * @param {FC.Data.Medicine.SizingImplants.ImplantType} implantData
+	 * @param {number} volume
+	 * @param {FC.Medicine.ImplantInfo<T>} implantInfo
+	 * @returns {boolean}
+	 */
+	function implantsAreSame(type, implantData, volume, implantInfo) {
+		if (implantInfo.type !== type) {
+			return false;
 		}
-		throw Error(`No sizing procedures for ${bodyPart}`);
+		if (!implantData.fill) {
+			return volume === implantInfo.volume;
+		}
+		return implantInfo.volume.isBetween(volume, implantData.fill.limit, true);
 	}
 
 	/**
-	 * @param {FC.Medicine.Surgery.SizingOptions} options
-	 * @param {FC.InstalledSizingImplantType} implantType
+	 * @template {FC.SizingImplantTarget} Target
+	 * @param {FC.Medicine.Surgery.SizingOptions<Target>} options
+	 * @param {FC.SizingImplantTarget} target
+	 * @returns {FC.Medicine.Surgery.DefinitiveSizedOptions<Target>}
 	 */
-	function implantTypeAllowed(options, implantType) {
-		return _.isNil(options.types) || options.types.includes(implantType);
+	function substituteImplicitOptions(options, target) {
+		return {
+			allowedTypes: options.allowedTypes || new Set(["none", ...App.Medicine.implantTypesForTarget(target)]),
+			replace: options.replace || false,
+			// reversed range to include all sizing options
+			targetSize: options.targetSize || App.Utils.makeRange(App.Medicine.maxAssetSize(target), 0),
+		};
 	}
 
 	/**
-	 * @param {FC.Medicine.Surgery.SizingOptions} options
-	 * @param {FC.InstalledSizingImplantType} implantType
+	 * @param {number} value
+	 * @param {FC.NumericRange} range
+	 * @returns {number}
 	 */
-	function fillOrDrainAllowed(options, implantType) {
-		return !options.strictTypes || implantTypeAllowed(options, implantType);
+	function distanceToRange(value, range) {
+		if (range.max >= range.min) {
+			return App.Utils.distanceToRange(value, range);
+		} else { // reversed range
+			return App.Utils.distanceToRange(value, App.Utils.makeRange(range.max, range.min));
+		}
 	}
 
 	/**
-	 * @param {FC.Medicine.Surgery.SizingOptions} options
+	 * @param {number} d1
+	 * @param {number} d2
+	 * @param {FC.NumericRange} range
+	 * @returns {boolean}
 	 */
-	function fleshReductionAllowed(options) {
-		return fillOrDrainAllowed(options, "none");
+	function distanceToRangeDecreased(d1, d2, range) {
+		return range.max >= range.min ? d2 < d1 : d2 <= d1;
 	}
 
 	/**
-	 * @param {App.Entity.SlaveState} slave
-	 * @param {FC.Medicine.Surgery.SizingOptions} [options]
+	 * @param {number} p1
+	 * @param {number} p2
+	 * @param {FC.NumericRange} range
+	 * @returns {boolean}
+	 */
+	function pointIsCloserToRange(p1, p2, range) {
+		return distanceToRangeDecreased(distanceToRange(p1, range), distanceToRange(p2, range), range);
+	}
+
+	/**
+	 * @template {FC.SizingImplantTarget} T
+	 * @param {FC.SlaveState} slave
+	 * @param {T} target
+	 * @param {FC.Medicine.Surgery.DefinitiveSizedOptions<T>} options
+	 * @param {FC.Medicine.ImplantProcedureCreators} creators
 	 * @returns {App.Medicine.Surgery.Procedure[]}
 	 */
-	function boobSizingProcedures(slave, options = {}) {
-		const thisArcology = V.arcologies[0];
-		const largeImplantsAvailable = thisArcology.FSTransformationFetishistResearch === 1;
-		const advancedSurgeryAvailable = V.ImplantProductionUpgrade === 1;
-		const {he, His} = getPronouns(slave);
+	function implantSizingProcedures(slave, target, options, creators) {
+		/** @type {App.Medicine.Surgery.Procedure[]} */
+		const procedures = [];
+
+		const current = App.Medicine.implantInfo(slave, target);
+		const currentDistance = distanceToRange(current.volume, options.targetSize);
+		const curImplantData = current.type !== "none" ? App.Data.Medicine.sizingImplants[target][current.type] : null;
+		const curFleshSize = App.Medicine.fleshSize(slave, target);
+		const maxVolume = App.Medicine.pocketVolume[target](slave);
+		const maxPureImplantVolume = maxVolume - curFleshSize;
+		const maxAssetVolume = App.Medicine.maxAssetSize(target);
+
+		const addMaxAssetSizeReachedDummy = (name) => {
+			const {His} = getPronouns(slave);
+			procedures.push(new ForbiddenDummy(slave, name, `${His} ${target} size reached the limit.`));
+		};
+
+		const distanceDecreased = (newValue) => {
+			return distanceToRangeDecreased(currentDistance, distanceToRange(newValue, options.targetSize), options.targetSize);
+		};
 
-		const areStringsInstalled = slave.boobsImplantType === "string";
-		const areFillablesInstalled = ["fillable", "advanced fillable", "hyper fillable"].includes(slave.boobsImplantType);
-		const curSize = slave.boobsImplant;
-		const implantType = slave.boobsImplantType;
+		/**
+		 * @param {FC.Data.Medicine.SizingImplants.ImplantType} data
+		 * @returns {number[]}
+		 */
+		const availableSizes = (data) => {
+			const sizes = data.availableSizes;
+			return typeof sizes === "function" ? sizes() : sizes;
+		};
 
 		/**
-		 * @type {Array<App.Medicine.Surgery.Procedure>}
+		 * @param {FC.Data.Medicine.SizingImplants.FillDrainData} fd
 		 */
-		let procedures = [];
-		if (options.augmentation) {
-			if (slave.indentureRestrictions >= 2) {
-				procedures.push(new ForbiddenDummy(slave, "Change boob size", `${His} indenture forbids elective surgery.`));
-			} else if (slave.breastMesh === 1) {
-				procedures.push(new ForbiddenDummy(slave, "Put implants", `${His} supportive mesh implant blocks implantation.`));
-			} else if (curSize === 0) {
-				if (implantTypeAllowed(options, "string")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.InstallBoobImplants(slave, "string", "string", 400));
-				}
-				if (implantTypeAllowed(options, "normal")) {
-					if (advancedSurgeryAvailable) {
-						procedures.push(new App.Medicine.Surgery.Procedures.InstallBoobImplants(slave, "large", "normal", 600));
+		const indenturePermitsFillDrain = (fd) => {
+			return slave.indentureRestrictions < 2 || fd.healthCost === 0;
+		};
+
+		// filling/draining the current implants go first
+		if (curImplantData && options.allowedTypes.has(current.type)) {
+			if (curImplantData.fill && current.volume < options.targetSize.min && indenturePermitsFillDrain(curImplantData.fill)) {
+				let maxVolumeReached = true;
+				let fillingPossible = false;
+				for (const fillVolume of curImplantData.fill.step) {
+					const newVolume = current.volume + fillVolume;
+					if (curImplantData.fill.limit >= newVolume && distanceDecreased(newVolume)) {
+						if (slave[target] + fillVolume <= maxAssetVolume) {
+							maxVolumeReached = false;
+							if (newVolume <= maxPureImplantVolume) {
+								procedures.push(creators.fill(slave, fillVolume));
+								fillingPossible = true;
+							}
+						}
 					}
-					procedures.push(new App.Medicine.Surgery.Procedures.InstallBoobImplants(slave, "standard", "normal", 400));
-					procedures.push(new App.Medicine.Surgery.Procedures.InstallBoobImplants(slave, "small", "normal", 200));
 				}
-			} else if (implantType === "normal") {
-				if (curSize > 400 && implantTypeAllowed(options, "fillable")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "fillable", "fillable", 800));
-				} else if (curSize > 200 && implantTypeAllowed(options, "normal")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "large", "normal", 600));
-				} else if (implantTypeAllowed(options, "normal")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "standard", "normal", 400));
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "large", "normal", 600));
+				if (!fillingPossible) {
+					const {His} = getPronouns(slave);
+					procedures.push(new ForbiddenDummy(slave, "Fill up", `${His} ${target} implants are filled to capacity.`));
+				} else if (maxVolumeReached) {
+					addMaxAssetSizeReachedDummy("Fill up");
 				}
-			} else if (implantType === "hyper fillable") {
-				if (slave.boobs >= 50000) {
-					procedures.push(new ForbiddenDummy(slave, "Increase boobs", `${His} breasts are as large as ${he} can physically support.`));
-				} else if (fillOrDrainAllowed(options, implantType)){
-					procedures.push(new App.Medicine.Surgery.Procedures.FillBoobImplants(slave, 1000));
-				}
-			} else if (implantType === "advanced fillable") {
-				if (curSize >= 10000) {
-					procedures.push(new ForbiddenDummy(slave, "Increase boobs", `${His} implants are filled to capacity.`));
-					if (largeImplantsAvailable && implantTypeAllowed(options, "hyper fillable")) {
-						procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "hyper fillable", "hyper fillable", 11000));
-					}
-				} else if (fillOrDrainAllowed(options, implantType)){
-					procedures.push(new App.Medicine.Surgery.Procedures.FillBoobImplants(slave, 400));
-				}
-			} else if (implantType === "fillable") {
-				if (curSize >= 1800) {
-					procedures.push(new ForbiddenDummy(slave, "Add inert filler", `${His} implants are filled to capacity.`));
-					if (implantTypeAllowed(options, "advanced fillable")) {
-						procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "advanced fillable", "advanced fillable", 2200, true));
+			}
+			if (curImplantData.drain && indenturePermitsFillDrain(curImplantData.drain)) {
+				for (const drainVolume of curImplantData.drain.step) {
+					if (curImplantData.drain.limit <= current.volume - drainVolume && distanceDecreased(current.volume - drainVolume)) {
+						procedures.push(creators.drain(slave, drainVolume));
 					}
-				} else if (fillOrDrainAllowed(options, implantType)){
-					procedures.push(new App.Medicine.Surgery.Procedures.FillBoobImplants(slave, 200));
 				}
 			}
 		}
 
-		if (options.replace && slave.indentureRestrictions < 2 && curSize > 0) {
-			if (!areStringsInstalled && curSize < 600 && implantTypeAllowed(options, "string")) {
-				procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "string", "string", 400));
-			} else if (areStringsInstalled) {
-				// we have engorged string implants, suggest replacing with normal implants of similar size
-				if (curSize > 10000) {
-					if (largeImplantsAvailable && implantTypeAllowed(options, "hyper fillable")) {
-						if (slave.boobs < 50000) {
-							procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "hyper fillable", "hyper fillable", Math.round(curSize / 1000) * 1000));
-						}
-					}
-				} else if (curSize > 2200 && implantTypeAllowed(options, "advanced fillable")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "advanced fillable", "advanced fillable", Math.round(curSize / 400) * 400, true));
-				} else if (curSize > 400 && implantTypeAllowed(options, "fillable")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "fillable", "fillable", Math.round(curSize / 200) * 200));
-				} else if (implantTypeAllowed(options, "normal")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceBoobImplants(slave, "standard", "normal", 400));
-				}
-			}
+		// all the other procedures require indentureRestrictions < 2
+		if (slave.indentureRestrictions >= 2) {
+			const {His} = getPronouns(slave);
+			procedures.push(new ForbiddenDummy(slave, "Implantation", `${His} indenture forbids invasive surgery.`));
+			return procedures;
 		}
 
-		if (options.reduction && (slave.boobs > 300 || curSize > 0)) {
-			if (curSize > 0) {
-				if (fillOrDrainAllowed(options, implantType)) {
-					if (areStringsInstalled && curSize > 400) {
-						if (curSize > 8000) {
-							procedures.push(new App.Medicine.Surgery.Procedures.DrainBoobImplants(slave, 1000));
-						} else if (curSize > 5000) {
-							procedures.push(new App.Medicine.Surgery.Procedures.DrainBoobImplants(slave, 750));
-						} else if (curSize > 2000) {
-							procedures.push(new App.Medicine.Surgery.Procedures.DrainBoobImplants(slave, 500));
-						} else if (curSize > 1000) {
-							procedures.push(new App.Medicine.Surgery.Procedures.DrainBoobImplants(slave, 250));
-						} else if (curSize > 500) {
-							procedures.push(new App.Medicine.Surgery.Procedures.DrainBoobImplants(slave, 100));
-						}
-					} else if (areFillablesInstalled) {
-						if (implantType === "hyper fillable") {
-							if (curSize <= 10000) {
-								procedures.push(new ForbiddenDummy(slave, "Remove filler", `${His} implants are empty.`));
-							} else {
-								procedures.push(new App.Medicine.Surgery.Procedures.DrainBoobImplants(slave, 1000));
-							}
-						} else if (implantType === "advanced fillable") {
-							if (curSize <= 1000) {
-								procedures.push(new ForbiddenDummy(slave, "Remove filler", `${His} implants are empty.`));
-							} else {
-								procedures.push(new App.Medicine.Surgery.Procedures.DrainBoobImplants(slave, 500));
-							}
-						} else if (implantType === "fillable") {
-							if (curSize <= 500) {
-								procedures.push(new ForbiddenDummy(slave, "Remove filler", `${His} implants are empty.`));
-							} else {
-								procedures.push(new App.Medicine.Surgery.Procedures.DrainBoobImplants(slave, 100));
+		// install an implant or replace with a bigger one
+		if (distanceDecreased(current.volume + 1)) {
+			let maxVolumeReached = true;
+			const curAssetSize = App.Medicine.assetSize(slave, target);
+			const currentCanBeReplaced = options.replace || options.allowedTypes.has(current.type);
+			for (const [type, data] of Object.entries(App.Data.Medicine.sizingImplants[target])) {
+				if (!options.allowedTypes.has(type)) {
+					continue;
+				}
+				for (const volume of availableSizes(data)) {
+					if (!distanceDecreased(volume)) {
+						continue;
+					}
+					if (curAssetSize - current.volume + volume <= maxVolume) {
+						maxVolumeReached = false;
+					} else {
+						continue;
+					}
+					const fleshExcess = Math.ceil(volume - maxPureImplantVolume);
+					if (volume >= current.volume && distanceDecreased(volume) && (volume <= maxPureImplantVolume ||
+						(options.allowedTypes.has("none")))) {
+						maxVolumeReached = false;
+						if (current.volume > 0) {
+							if (currentCanBeReplaced && !implantsAreSame(type, data, volume, current)) {
+								procedures.push(creators.replace(slave, type, volume, fleshExcess));
 							}
+						} else {
+							procedures.push(creators.install(slave, type, volume, fleshExcess));
 						}
 					}
 				}
-				if (slave.indentureRestrictions < 2 && implantTypeAllowed(options, "none")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.RemoveBoobImplants(slave));
-				}
 			}
-			if ((slave.boobs > 300) && (curSize === 0) && slave.indentureRestrictions < 2 && fleshReductionAllowed(options)) {
-				procedures.push(new App.Medicine.Surgery.Procedures.ReduceBoobs(slave, "reduce", 200));
-				if (slave.boobs < 675) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReduceBoobs(slave, "slightly reduce", 25));
-				}
+			if (maxVolumeReached) {
+				addMaxAssetSizeReachedDummy("Bigger implants");
 			}
-			if ((curSize === 0) && slave.indentureRestrictions < 2 && (slave.breedingMark !== 1 || V.propOutcome !== 1 || V.eugenicsFullControl === 1 || V.arcologies[0].FSRestart === "unset") && fleshReductionAllowed(options)) {
-				if (slave.boobs >= 2000) {
-					procedures.push(new App.Medicine.Surgery.Procedures.Mastectomy(slave));
+		}
+
+		// remove the current implant or replace with a smaller one
+		if (current.volume > 0 && distanceDecreased(current.volume - 1)) {
+			if (options.targetSize.max <= 0) {
+				procedures.push(creators.remove(slave));
+			}
+
+			if (options.replace) {
+				for (const [type, data] of Object.entries(App.Data.Medicine.sizingImplants[target])) {
+					if (!options.allowedTypes.has(type)) {
+						continue;
+					}
+					for (const volume of availableSizes(data).filter(s => s < current.volume && distanceDecreased(s))) {
+						if (!implantsAreSame(type, data, volume, current)) {
+							procedures.push(creators.replace(slave, type, volume, 0));
+						}
+					}
 				}
 			}
 		}
@@ -359,132 +409,73 @@ App.Medicine.Surgery.sizingProcedures = function() {
 
 	/**
 	 * @param {App.Entity.SlaveState} slave
-	 * @param {FC.Medicine.Surgery.SizingOptions} [options]
+	 * @param {FC.Medicine.Surgery.SizingOptions<"boobs">} [options]
 	 * @returns {App.Medicine.Surgery.Procedure[]}
 	 */
-	function buttSizingProcedures(slave, options = {}) {
-		const thisArcology = V.arcologies[0];
-		const largeImplantsAvailable = thisArcology.FSTransformationFetishistResearch === 1;
-		const advancedSurgeryAvailable = V.ImplantProductionUpgrade === 1;
-		const {His} = getPronouns(slave);
+	function boobSizingProcedures(slave, options = {}) {
+		// check if implant-related surgeries are allowed for this slave
+		if (slave.breastMesh === 1) {
+			const {his, His} = getPronouns(slave);
+			return [new ForbiddenDummy(slave, "Implants", `${His} supportive mesh implant blocks resizing ${his} boobs.`)];
+		}
 
-		const areStringsInstalled = slave.buttImplantType === "string";
-		const areFillablesInstalled = ["fillable", "advanced fillable", "hyper fillable"].includes(slave.buttImplantType);
-		const curSize = slave.buttImplant;
-		const implantType = slave.buttImplantType;
+		const p = App.Medicine.Surgery.Procedures.boobImplantsProcedure;
+		const dOptions = substituteImplicitOptions(options, "boobs");
 
-		/**
-		 * @type {Array<App.Medicine.Surgery.Procedure>}
-		 */
-		let procedures = [];
-		if (options.augmentation) {
-			if (slave.indentureRestrictions >= 2) {
-				procedures.push(new ForbiddenDummy(slave, "Change butt size", `${His} indenture forbids elective surgery.`));
-			} else if (slave.butt > 19 && areFillablesInstalled) {
-				procedures.push(new ForbiddenDummy(slave, "Increase butt", `${His} butt is as large as it can possibly get.`));
-			} else if (curSize === 0) {
-				if (implantTypeAllowed(options, "string")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.InstallButtImplants(slave, "string", "string", 1));
-				}
-				if (implantTypeAllowed(options, "normal")) {
-					if (advancedSurgeryAvailable) {
-						procedures.push(new App.Medicine.Surgery.Procedures.InstallButtImplants(slave, "big", "normal", 2));
-					}
-					procedures.push(new App.Medicine.Surgery.Procedures.InstallButtImplants(slave, "standard", "normal", 1));
-				}
-			} else if (implantType === "normal") {
-				if (curSize >= 5 && implantTypeAllowed(options, "advanced fillable")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "advanced fillable", "advanced fillable", curSize, true));
-				} else if (curSize >= 2 && implantTypeAllowed(options, "fillable")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "fillable", "fillable", 3));
-				} else if (curSize === 1 && implantTypeAllowed(options, "normal")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "bigger", "normal", 2));
-				}
-			} else if (implantType === "hyper fillable" && fillOrDrainAllowed(options, implantType)) {
-				if (curSize > 19) {
-					procedures.push(new ForbiddenDummy(slave, "Increase butt", `${His} butt implants are filled to capacity.`));
-				} else {
-					procedures.push(new App.Medicine.Surgery.Procedures.FillButtImplants(slave, 1));
+		const procedures = implantSizingProcedures(slave, "boobs", dOptions, p);
+
+		// boob-specific reduction procedures
+		if (slave.indentureRestrictions < 2 && slave.boobsImplant === 0 && dOptions.allowedTypes.has("none")) {
+			if (App.Medicine.fleshSize(slave, "boobs") > 300) {
+				if (pointIsCloserToRange(slave.boobs, slave.boobs - 200, dOptions.targetSize)) {
+					procedures.push(p.reduce(slave, "reduce", 200));
 				}
-			} else if (implantType === "advanced fillable") {
-				if (curSize > 7) {
-					procedures.push(new ForbiddenDummy(slave, "Increase butt", `${His} butt implants are filled to capacity.`));
-					if (largeImplantsAvailable && implantTypeAllowed(options, "hyper fillable")) {
-						procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "hyper fillable", "hyper fillable", 9));
+				if (App.Medicine.fleshSize(slave, "boobs") < 675) {
+					if (pointIsCloserToRange(slave.boobs, slave.boobs - 25, dOptions.targetSize)) {
+						procedures.push(p.reduce(slave, "slightly reduce", 25));
 					}
-				} else if (fillOrDrainAllowed(options, implantType)) {
-					procedures.push(new App.Medicine.Surgery.Procedures.FillButtImplants(slave, 1));
 				}
-			} else if (implantType === "fillable") {
-				if (curSize >= 4) {
-					procedures.push(new ForbiddenDummy(slave, "Increase size", `${His} implants are filled to capacity.`));
-					if (implantTypeAllowed(options, "advanced fillable")) {
-						procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "advanced fillable", "advanced fillable", 5, true));
-					}
-				} else if (fillOrDrainAllowed(options, implantType)) {
-					procedures.push(new App.Medicine.Surgery.Procedures.FillButtImplants(slave, 1));
+			}
+			if ((slave.breedingMark !== 1 || V.propOutcome !== 1 || V.eugenicsFullControl === 1 || V.arcologies[0].FSRestart === "unset")) {
+				if (App.Medicine.fleshSize(slave, "boobs") >= 2000 && pointIsCloserToRange(slave.boobs, 0, dOptions.targetSize)) {
+					procedures.push(new App.Medicine.Surgery.Procedures.Mastectomy(slave));
 				}
 			}
 		}
+		return procedures;
+	}
 
-		if (options.replace && slave.indentureRestrictions < 2 && curSize > 0) {
-			if (!areStringsInstalled && curSize === 1) {
-				procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "string", "string", 1));
-			} else if (areStringsInstalled) {
-				// we have engorged string implants, suggest replacing with normal implants of similar size
-				if (curSize >= 9) {
-					if (largeImplantsAvailable && implantTypeAllowed(options, "hyper fillable")) {
-						procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "hyper fillable", "hyper fillable", curSize));
-					}
-				} else if (curSize >= 5 && implantTypeAllowed(options, "advanced fillable")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "advanced fillable", "advanced fillable", curSize, true));
-				} else if (curSize >= 3 && implantTypeAllowed(options, "fillable")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "fillable", "fillable", curSize));
-				} else if (curSize === 2 && implantTypeAllowed(options, "normal")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "big", "normal", curSize));
-				} else if (implantTypeAllowed(options, "normal")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReplaceButtImplants(slave, "standard", "normal", curSize));
-				}
-			}
+	/**
+	 * @param {App.Entity.SlaveState} slave
+	 * @param {FC.Medicine.Surgery.SizingOptions<"butt">} [options]
+	 * @returns {App.Medicine.Surgery.Procedure[]}
+	 */
+	function buttSizingProcedures(slave, options = {}) {
+		const dOptions = substituteImplicitOptions(options, "butt");
+		const p = App.Medicine.Surgery.Procedures.buttImplantsProcedure;
+		const procedures = implantSizingProcedures(slave, "butt", dOptions, p);
+
+		if (slave.buttImplant === 0 && slave.butt > 1 && dOptions.allowedTypes.has("none") &&
+			slave.indentureRestrictions < 2 && pointIsCloserToRange(slave.butt, slave.butt - 1, dOptions.targetSize)) {
+			procedures.push(p.reduce(slave, "reduce", 1));
 		}
+		return procedures;
+	}
 
-		if (options.reduction) {
-			if (curSize > 0) {
-				if (fillOrDrainAllowed(options, implantType)) {
-					if (areStringsInstalled && curSize > 1) {
-						procedures.push(new App.Medicine.Surgery.Procedures.DrainButtImplants(slave, 1));
-					} else if (areFillablesInstalled) {
-						if (implantType === "hyper fillable") {
-							if (curSize <= 5) {
-								procedures.push(new ForbiddenDummy(slave, "Remove filler", `${His} implants are empty.`));
-							} else {
-								procedures.push(new App.Medicine.Surgery.Procedures.DrainButtImplants(slave, 1));
-							}
-						} else if (implantType === "advanced fillable") {
-							if (curSize <= 3) {
-								procedures.push(new ForbiddenDummy(slave, "Remove filler", `${His} implants are empty.`));
-							} else {
-								procedures.push(new App.Medicine.Surgery.Procedures.DrainButtImplants(slave, 1));
-							}
-						} else if (implantType === "fillable") {
-							if (curSize <= 1) {
-								procedures.push(new ForbiddenDummy(slave, "Remove filler", `${His} implants are empty.`));
-							} else {
-								procedures.push(new App.Medicine.Surgery.Procedures.DrainButtImplants(slave, 1));
-							}
-						}
-					}
-				}
-				if (slave.indentureRestrictions < 2 && implantTypeAllowed(options, "none")) {
-					procedures.push(new App.Medicine.Surgery.Procedures.RemoveButtImplants(slave));
-				}
-			}
-			if ((slave.butt > 1) && (curSize === 0) && fleshReductionAllowed(options)) {
-				if (slave.indentureRestrictions < 2) {
-					procedures.push(new App.Medicine.Surgery.Procedures.ReduceButt(slave, "reduce", 1));
-				}
-			}
+	/**
+	 * @param {App.Entity.SlaveState} slave
+	 * @param {FC.Medicine.Surgery.SizingOptions<"lips">} [options]
+	 * @returns {App.Medicine.Surgery.Procedure[]}
+	 */
+	function lipsSizingProcedures(slave, options) {
+		const dOptions = substituteImplicitOptions(options, "lips");
+		const p = App.Medicine.Surgery.Procedures.lipImplantProcedure;
+		const procedures = implantSizingProcedures(slave, "lips", dOptions, p);
+
+		if (slave.lips >= 10 && slave.lipsImplant === 0 && slave.indentureRestrictions < 2) {
+			procedures.push(p.reduce(slave, "reduce", 10));
 		}
+
 		return procedures;
 	}
 }();