code:
App.Medicine.Surgery.Reactions.VoiceRaise = class extends App.Medicine.Surgery.SimpleReaction {
	reaction(slave, diff) {
		const reaction = super.reaction(slave, diff);
		const {He, he, his, him, himself, hers} = getPronouns(slave);
		const r = [];

		if (slave.fetish === "mindbroken") {
			r.push(`The autosurgery instructed ${him} in no uncertain terms not to speak during recovery, but ${he} fails to recall that. ${He} doesn't notice anything different. As with all surgery ${his} health has been slightly affected.`);
		} else if (slave.devotion > 50) {
			r.push(`Before surgery, ${he} was warned repeatedly not to try talking for a while, and ${he} obeys. When ${he} finally does, ${his} voice is raspy and weak, but it gains strength gradually. It comes out`);
			if (slave.voice === 3) {
				r.push(`far`);
			}
			r.push(`higher and more girly than it was before, and ${he} laughs at ${himself} as ${he} gets used to it. ${He} has become more submissive, since this helps ${him} to think of ${himself} as a`);
			if (slave.voice === 3) {
				r.push(`bimbo slut.`);
			} else {
				r.push(`real woman.`);
			}
			r.push(`As with all surgery <span class="health dec">${his} health has been slightly affected.</span>`);
			reaction.devotion += 4;
		} else if (slave.devotion > 20) {
			r.push(`Before surgery, ${he} was warned repeatedly not to try talking for a while, and ${he} obeys. When ${he} finally does, ${his} voice is raspy and weak, but it gains strength gradually. It comes out`);
			if (slave.voice === 3) {
				r.push(`far`);
			}
			r.push(`higher and more girly than it was before, and ${he} laughs grimly at ${himself} as ${he} gets used to it. ${He} carries on regardless, accepting it as one more thing to learn to accept. As with all surgery ${his} health has been slightly affected.`);
		} else {
			r.push(`Before surgery, ${he} was warned repeatedly not to try talking for a while, and ${he} obeys. When ${he} finally does, ${his} voice is raspy and weak, but it gains strength gradually. It comes out`);
			if (slave.voice === 3) {
				r.push(`far`);
			}
			r.push(`higher and more girly than it was before. For now, <span class="devotion dec">${he} feels this`);
			if (slave.voice === 3) {
				r.push(`bimbo`);
			} else {
				r.push(`feminine`);
			}
			r.push(`voice is not ${hers}, a cruel mockery. As with all surgery ${his} health has been slightly affected. ${He} is now terribly afraid of your total power over ${his} body.`);
			reaction.trust -= 10;
			reaction.devotion -= 5;
		}

		reaction.longReaction.push(r);
		return reaction;
	}
};

App.Medicine.Surgery.Procedures.VoiceRaise = class extends App.Medicine.Surgery.Procedure {
	get name() {
		return "Perform surgery to raise voice";
	}

	get healthCost() {
		return 10;
	}

	apply(cheat) {
		this._slave.voice += 1;
		this._slave.voiceImplant += 1;
		return this._assemble(new App.Medicine.Surgery.Reactions.VoiceRaise());
	}
};


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

license: mit


code:
App.Medicine.OrganFarm.Organ = class {
	/**
	 * @param {Object} params
	 * @param {string} params.name - display name
	 * @param {string|function(App.Entity.SlaveState):string} [params.tooltip] - full sentence, uncapitalized and unpunctuated
	 * @param {number} params.cost - how much it costs to grow the organ
	 * @param {number} params.time - how long it takes to grow the organ (without upgrades)
	 * @param {function(App.Entity.SlaveState):boolean} [params.canGrow]
	 * @param {string[]} [params.dependencies] - organs that are implanted first if possible, use type of other organs as values
	 * @param {App.Medicine.OrganFarm.OrganImplantAction[]} [params.actions]
	 * @param {boolean} [params.displayMultipleActions=false] allow multiple implant links to be displayed
	 */
	constructor({
		name, tooltip = "", cost, time, canGrow = () => true, dependencies = [],
		displayMultipleActions = false, actions = []
	}) {
		this.name = name;
		this.tooltip = tooltip;
		this.cost = cost;
		this.time = time;
		/** @type {function(App.Entity.SlaveState):boolean} */
		this.canGrow = canGrow;
		/** @type {string[]} */
		this.dependencies = dependencies;
		this.displayMultipleActions = displayMultipleActions;
		/** @type {App.Medicine.OrganFarm.OrganImplantAction[]} */
		this.implantActions = actions;
	}
};

App.Medicine.OrganFarm.OrganImplantAction = class {
	/**
	 * @param {object} params
	 * @param {string} params.name - display name, shown when showing error (otherwise shows procedure name)
	 * @param {typeof App.Medicine.Surgery.Procedure} params.surgeryProcedure - surgery reaction to show
	 * @param {Array} [params.surgeryProcedureParams] - parameters for surgeryProcedure, the slave argument will be added to the front automatically
	 * @param {boolean} [params.autoImplant]
	 * @param {function(App.Entity.SlaveState):boolean} params.canImplant
	 * @param {function(App.Entity.SlaveState):string} params.implantError - message to show if this action cannot be used
	 */
	constructor({
		name,
		surgeryProcedure,
		surgeryProcedureParams = [],
		autoImplant = true,
		canImplant,
		implantError
	}) {
		this.name = name;
		this.surgeryProcedure = surgeryProcedure;
		this.surgeryProcedureParams = surgeryProcedureParams;
		this.autoImplant = autoImplant;
		/**
		 * True if this action can implant the organ
		 * @type {function(App.Entity.SlaveState):boolean}
		 */
		this.canImplant = canImplant;
		/**
		 * Error message if the organ cannot be implanted.
		 * @type {function(App.Entity.SlaveState):string}
		 */
		this.implantError = implantError;
	}
};


code:
App.Medicine.OrganFarm.TesticlesImplantAction = class extends App.Medicine.OrganFarm.OrganImplantAction {
	/**
	 * @param {object} params
	 * @param {string} params.name
	 * @param {boolean} params.animal
	 * @param {typeof App.Medicine.Surgery.Procedure} params.surgeryProcedure
	 * @param {Array} [params.surgeryProcedureParams]
	 * @param {boolean} [params.autoImplant]
	 * @param {function(App.Entity.SlaveState):boolean} params.canImplant
	 * @param {function(App.Entity.SlaveState):string} params.implantError
	 */
	constructor({
		name,
		animal,
		surgeryProcedure,
		surgeryProcedureParams,
		autoImplant = true,
		canImplant,
		implantError,
	}) {
		super({
			name: name,
			surgeryProcedure: surgeryProcedure,
			surgeryProcedureParams: surgeryProcedureParams,
			autoImplant: autoImplant,
			canImplant: s => (!this.animal || V.animalTesticles > 0) && canImplant(s),
			implantError: s => (this.animal && V.animalTesticles === 0) ? "" : implantError(s),
		});
		this.animal = animal;
	}
};

App.Medicine.OrganFarm.Testicles = class extends App.Medicine.OrganFarm.Organ {
	/**
	 * @param {Object} params
	 * @param {string} params.name
	 * @param {FC.AnimalType} params.ballType
	 */
	constructor({name, ballType}) {
		super({
			name: name,
			tooltip: "will add a prostate if one is not already present; requires a penis for successful implantation",
			cost: 5000, time: 10,
			canGrow: () => (this.ballType === "human" || V.animalTesticles > 0), dependencies: ["penis"],
			actions: [
				new App.Medicine.OrganFarm.TesticlesImplantAction({
					name: "Implant", animal: ballType !== "human",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFAddBalls,
					surgeryProcedureParams: [ballType],
					canImplant: slave => (slave.balls <= 0 && slave.dick > 0),
					implantError: slave => {
						if (slave.dick === 0) {
							return "This slave lacks the penis necessary to accept testicles.";
						} else {
							return "This slave already has testicles.";
						}
					},
				}),
				new App.Medicine.OrganFarm.TesticlesImplantAction({
					name: "Implant",
					animal: ballType !== "human", autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFAddTesticles,
					surgeryProcedureParams: [ballType],
					canImplant: slave => (slave.dick === 0 && slave.balls <= 0),
					implantError: slave => ((slave.balls > 0) ? "This slave already has testicles." : ""),
				}),
				new App.Medicine.OrganFarm.TesticlesImplantAction({
					name: "Replace",
					animal: ballType !== "human", autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFReplaceTesticles,
					surgeryProcedureParams: [ballType],
					canImplant: slave => (slave.balls > 0 && slave.ballType !== this.ballType),
					implantError: slave => (slave.balls > 0 ? `This slave already has ${this.ballType} testicles.` : ""),

				})
			]
		});
		this.ballType = ballType;
	}
};

App.Medicine.OrganFarm.OvariesImplantAction = class extends App.Medicine.OrganFarm.OrganImplantAction {
	/**
	 * @param {object} params
	 * @param {string} params.name
	 * @param {boolean} params.animal
	 * @param {typeof App.Medicine.Surgery.Procedure} params.surgeryProcedure
	 * @param {Array} [params.surgeryProcedureParams]
	 * @param {boolean} [params.autoImplant]
	 * @param {function(App.Entity.SlaveState):boolean} params.canImplant
	 * @param {function(App.Entity.SlaveState):string} params.implantError
	 */
	constructor({
		name,
		animal,
		surgeryProcedure,
		surgeryProcedureParams,
		autoImplant = true,
		canImplant,
		implantError
	}) {
		super({
			name: name, autoImplant: autoImplant,
			surgeryProcedure: surgeryProcedure,
			surgeryProcedureParams: surgeryProcedureParams,
			canImplant: s => (!this.animal || V.animalTesticles > 0) && canImplant(s),
			implantError: s => (this.animal && V.animalTesticles === 0) ? "" : implantError(s),
		});
		this.animal = animal;
	}
};

App.Medicine.OrganFarm.Ovaries = class extends App.Medicine.OrganFarm.Organ {
	/**
	 * @param {object} params
	 * @param {string} params.name
	 * @param {FC.AnimalType} params.eggType
	 * @param {string} params.pregData
	 */
	constructor({name, eggType, pregData}) {
		super({
			name: name, tooltip: "requires a vagina for successful implantation",
			cost: 10000, time: 10,
			canGrow: () => (this.eggType === "human" || V.animalOvaries > 0),
			actions: [
				new App.Medicine.OrganFarm.OvariesImplantAction({
					name: "Implant", animal: eggType !== "human",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFAddOvaries,
					surgeryProcedureParams: [eggType, pregData],
					canImplant: s => (s.vagina >= 0 && s.ovaries <= 0 && s.mpreg === 0 && s.bellyImplant === -1),
					implantError: s => {
						if (s.vagina < 0) {
							return "This slave lacks the vagina necessary to accept ovaries.";
						}
						if (s.ovaries > 0 || s.mpreg > 0) {
							return "This slave already has ovaries.";
						}
						return "This slave's body cavity is filled with another organ.";
					},
				}),
				new App.Medicine.OrganFarm.OvariesImplantAction({
					name: "Replace", animal: eggType !== "human",
					autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFReplaceOvaries,
					surgeryProcedureParams: [eggType, pregData],
					canImplant: s => (s.vagina >= 0 && s.mpreg === 0 && s.bellyImplant === -1 && (s.eggType !== this.eggType || s.preg === -3)),
					implantError: s => (s.eggType === this.eggType) ? `This slave already has ${s.eggType} ovaries.` : "",
				})
			]
		});
		this.eggType = eggType;
		this.pregData = pregData;
	}
};

App.Medicine.OrganFarm.AnalWombImplantAction = class extends App.Medicine.OrganFarm.OrganImplantAction {
	/**
	 * @param {object} params
	 * @param {FC.AnimalType} params.eggType
	 * @param {string} params.pregData
	 */
	constructor({eggType, pregData}) {
		super({
			name: "Implant",
			surgeryProcedure: App.Medicine.Surgery.Procedures.OFAddMPreg,
			surgeryProcedureParams: [],
			canImplant: s => ((this.eggType === "human" || V.animalTesticles > 0) && s.ovaries === 0 && s.vagina <= -1 && s.mpreg === 0 && s.bellyImplant < 0),
			implantError: s => {
				if (this.eggType !== "human" && V.animalTesticles === 0) {
					return "";
				}
				if (s.bellyImplant >= 0) {
					return "This slave has a fillable abdominal implant.";
				}
				return "This slave has existing reproductive Organs.";
			},
		});
		this.eggType = eggType;
		this.pregData = pregData;
	}
};

App.Medicine.OrganFarm.AnalWomb = class extends App.Medicine.OrganFarm.Organ {
	/**
	 * @param {object} params
	 * @param {string} params.name
	 * @param {FC.AnimalType} params.eggType
	 * @param {string} params.pregData
	 */
	constructor({name, eggType, pregData}) {
		super({
			name: name,
			tooltip: "the slave must not have female reproductive organs for successful implantation",
			cost: 20000, time: 10,
			canGrow: () => (V.arcologies[0].FSGenderRadicalistResearch === 1 && (eggType === "human" || V.animalMpreg > 0)),
			actions: [
				new App.Medicine.OrganFarm.AnalWombImplantAction({
					eggType: eggType,
					pregData: pregData
				})
			]
		});
	}
};


code:
/**
 * Organs will usually be displayed in this order.
 *
/** @type {Map<string, App.Medicine.OrganFarm.Organ>} */
App.Medicine.OrganFarm.Organs = new Map([
	["penis",
		new App.Medicine.OrganFarm.Organ({
			name: "Penis", cost: 5000, time: 5,
			canGrow: () => V.seeDicks !== 0 || V.makeDicks === 1,
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFAddDick,
					canImplant: slave => slave.dick <= 0,
					implantError: () => "this slave already has a penis",
				})
			]
		})],
	["testicles",
		new App.Medicine.OrganFarm.Testicles({
			name: "Testicles", ballType: "human"
		})],
	["pigTesticles",
		new App.Medicine.OrganFarm.Testicles({
			name: "Pig testicles", ballType: "pig"
		})],
	["dogTesticles",
		new App.Medicine.OrganFarm.Testicles({
			name: "Dog testicles", ballType: "dog"
		})],
	["horseTesticles",
		new App.Medicine.OrganFarm.Testicles({
			name: "Horse testicles", ballType: "horse"
		})],
	["cowTesticles",
		new App.Medicine.OrganFarm.Testicles({
			name: "Cow testicles", ballType: "cow"
		})],
	["scrotum",
		new App.Medicine.OrganFarm.Organ({
			name: "Scrotum", tooltip: "requires balls for successful implantation", cost: 2500, time: 5,
			dependencies: ["testicles", "pigTesticles", "dogTesticles", "horseTesticles", "cowTesticles"],
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Graft on",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFAddScrotum,
					canImplant: slave => slave.scrotum <= 0 && slave.balls > 0,
					implantError: slave => slave.scrotum > 0 ? "This slave already has a scrotum." : "This slave lacks the balls necessary to accept a scrotum.",
				})
			]
		})],
	["foreskin",
		new App.Medicine.OrganFarm.Organ({
			name: "Foreskin", cost: 2500, time: 5,
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Graft on",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFAddForeskin,
					canImplant: slave => slave.foreskin <= 0,
					implantError: slave => `This slave already has a ${slave.dick > 0 ? "foreskin" : "clitoral hood"}.`,
				})
			]
		})],
	["prostate",
		new App.Medicine.OrganFarm.Organ({
			name: "Prostate", cost: 5000, time: 5,
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFAddProstate,
					canImplant: s => s.prostate === 0,
					implantError: () => "This slave already has a prostate.",
				})
			]
		})],
	["ovaries",
		new App.Medicine.OrganFarm.Ovaries({
			name: "Ovaries", eggType: "human", pregData: "human"
		})],
	["freshOvaries",
		new App.Medicine.OrganFarm.Organ({
			name: "Younger Ovaries",
			tooltip: "requires a womb for successful implantation",
			cost: 10000, time: 10,
			canGrow: () => V.youngerOvaries === 1,
			dependencies: ["ovaries", "pigOvaries", "dogOvaries", "horseOvaries", "cowOvaries", "mpreg", "mpregPig", "mpregDog", "mpregHorse", "mpregCow"],
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFFreshOvaries,
					canImplant: s => (s.mpreg !== 0 || s.ovaries !== 0) && s.bellyImplant === -1 && s.physicalAge < 70,
					implantError: s => s.physicalAge >= 70 ? "This slave's body is too old to handle pregnancy." : "This slave lacks a viable womb.",
				})
			]
		})],
	["asexualReproOvaries",
		new App.Medicine.OrganFarm.Organ({
			name: "Asexual reproduction modification",
			tooltip: "requires existing ovaries for successful implantation",
			cost: 10000, time: 10,
			canGrow: () => V.asexualReproduction === 1,
			dependencies: ["ovaries", "pigOvaries", "dogOvaries", "horseOvaries", "cowOvaries", "mpreg", "mpregPig", "mpregDog", "mpregHorse", "mpregCow"],
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFAsexualReproOvaries,
					canImplant: s => s.mpreg !== 0 || s.ovaries !== 0,
					implantError: () => "This slave lacks ovaries.",
				})
			]
		})],
	["pigOvaries",
		new App.Medicine.OrganFarm.Ovaries({
			name: "Pig ovaries", eggType: "pig", pregData: "pig"
		})],
	["dogOvaries",
		new App.Medicine.OrganFarm.Ovaries({
			name: "Dog ovaries", eggType: "dog", pregData: "canineM",
		})],
	["horseOvaries",
		new App.Medicine.OrganFarm.Ovaries({
			name: "Horse ovaries", eggType: "horse", pregData: "equine",
		})],
	["cowOvaries",
		new App.Medicine.OrganFarm.Ovaries({
			name: "Cow ovaries", eggType: "cow", pregData: "cow"
		})],
	["leftEye",
		new App.Medicine.OrganFarm.Organ({
			name: "Left Eye", cost: 5000, time: 10,
			tooltip: s => getLeftEyeVision(s) === 2 ? "would not improve this slave" : "",
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFNewEyes,
					surgeryProcedureParams: ["left"],
					canImplant: s => getLeftEyeVision(s) === 0 && getBestVision(s) !== 0 && getLeftEyeType(s) !== 2,
					implantError: s => getLeftEyeVision(s) !== 0 ? "Slave has a working left eye." : "",
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFUnblind,
					surgeryProcedureParams: ["left"],
					canImplant: s => getBestVision(s) === 0 && getLeftEyeType(s) !== 2,
					implantError: () => "",
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Replace", autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFNewEyes,
					surgeryProcedureParams: ["left"],
					canImplant: s => getLeftEyeType(s) === 2,
					implantError: () => "",
				})
			]
		})],
	["rightEye",
		new App.Medicine.OrganFarm.Organ({
			name: "Right Eye", cost: 5000, time: 10,
			tooltip: s => getRightEyeVision(s) === 2 ? "would not improve this slave" : "",
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFNewEyes,
					surgeryProcedureParams: ["right"],
					canImplant: s => getRightEyeVision(s) === 0 && getBestVision(s) !== 0 && getRightEyeType(s) !== 2,
					implantError: s => getRightEyeVision(s) !== 0 ? "Slave has a working right eye." : "",
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFUnblind,
					surgeryProcedureParams: ["right"],
					canImplant: s => getBestVision(s) === 0 && getRightEyeType(s) !== 2,
					implantError: () => "",
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Replace", autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFNewEyes,
					surgeryProcedureParams: ["right"],
					canImplant: s => getRightEyeType(s) === 2,
					implantError: () => "",
				})
			]
		})],
	["ears",
		new App.Medicine.OrganFarm.Organ({
			name: "Normal Ears", cost: 1000, time: 2,
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Attach",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFImplantEars,
					canImplant: s => s.earShape === "none",
					implantError: () => "This slave already has ears."
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Replace",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFImplantEars,
					autoImplant: false,
					canImplant: s => s.earShape !== "normal",
					implantError: () => "This slave already has normal ears."
				})
			]
		})],
	["topEars",
		new App.Medicine.OrganFarm.Organ({
			name: "Top Ears", cost: 1000, time: 2,
			canGrow: () => V.surgeryUpgrade >= 1,
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Attach",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFImplantTopEars,
					canImplant: s => s.earT === "none",
					implantError: () => "This slave already has ears at the top of the head."
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Replace",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFImplantTopEars,
					autoImplant: false,
					canImplant: s => s.earT !== "normal",
					implantError: () => "This slave already has normal ears at the top of the head."
				})
			]
		})],
	["cochleae",
		new App.Medicine.OrganFarm.Organ({
			name: "Cochleae", cost: 8000, time: 6,
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFUndeafen,
					canImplant: s => s.hears <= -2 && s.earImplant === 0,
					implantError: () => "This slave already has working ears.",
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Replace", autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFNewEars,
					canImplant: s => s.earImplant === 1,
					implantError: () => "",
				})
			]
		})],
	["voicebox",
		new App.Medicine.OrganFarm.Organ({
			name: "Vocal Cords", cost: 5000, time: 5,
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Implant",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFRestoreVoice,
					canImplant: s => s.voice === 0 && s.electrolarynx === 0,
					implantError: () => "This slave is not mute.",
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Replace", autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFNewVoice,
					canImplant: s => s.electrolarynx === 1,
					implantError: () => "",
				})
			]
		})],
	["hair",
		new App.Medicine.OrganFarm.Organ({
			name: "Hair Follicles", cost: 500, time: 2, displayMultipleActions: true,
			actions: [
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Scalp",
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFRestoreHairHead,
					canImplant: s => s.bald !== 0,
					implantError: () => "This slave already has hair.",
				}),
				/* So apparently hair is tracked via the .earTColor variable with no differential between possible hair origin. Not worth the variable, currently.
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Ears", healthImpact: 5,
					surgeryType: "addHairEarsH", autoImplant: false,
					canImplant: s => (s.earTShape !== "normal"),
					implantError: "",
					implant: s => {
						s.earTColor = getGeneticHairColor(s);
					}
				}),
				*/
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Brow", autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFRestoreHairBrow,
					canImplant: s => s.eyebrowHStyle === "bald",
					implantError: () => "",
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Axillary", autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFRestoreHairPits,
					canImplant: s => s.underArmHStyle === "bald" || s.underArmHStyle === "hairless",
					implantError: () => "",
				}),
				new App.Medicine.OrganFarm.OrganImplantAction({
					name: "Pubic", autoImplant: false,
					surgeryProcedure: App.Medicine.Surgery.Procedures.OFRestoreHairPubes,
					canImplant: s => s.pubicHStyle === "bald" || s.pubicHStyle === "hairless",
					implantError: () => "",
				})
			]
		})],
	["mpreg",
		new App.Medicine.OrganFarm.AnalWomb({
			name: "Anal womb and ovaries", eggType: "human", pregData: "human",
		})],
	["mpregPig",
		new App.Medicine.OrganFarm.AnalWomb({
			name: "Anal pig womb and ovaries", eggType: "pig", pregData: "pig",
		})],
	["mpregDog",
		new App.Medicine.OrganFarm.AnalWomb({
			name: "Anal dog womb and ovaries", eggType: "dog", pregData: "canineM",
		})],
	["mpregHorse",
		new App.Medicine.OrganFarm.AnalWomb({
			name: "Anal horse womb and ovaries", eggType: "horse", pregData: "equine",
		})],
	["mpregCow",
		new App.Medicine.OrganFarm.AnalWomb({
			name: "Anal cow womb and ovaries", eggType: "cow", pregData: "cow",
		})],
]);


code:
/**
 * generate two independent Gaussian numbers using Box-Muller transform.
 * mean and deviation specify the desired mean and standard deviation.
 * @param {number} [mean]
 * @param {number} [deviation]
 * @returns {number[]}
 */
function gaussianPair(mean = 0, deviation = 1) {
	const r = Math.sqrt(-2.0 * Math.log(1 - Math.random()));
	const sigma = 2.0 * Math.PI * (1 - Math.random());
	return [r * Math.cos(sigma), r * Math.sin(sigma)].map(val => val * deviation + mean);
}

/**
 * Generate a skewed normal random variable
 * Reference: http://azzalini.stat.unipd.it/SN/faq-r.html
 * @param {number} skew
 * @returns {number}
 */
App.Utils.Math.skewedGaussian = function(skew) {
	let randoms = gaussianPair();
	if (skew === 0) {
		// Don't bother, return an unskewed normal distribution
		return randoms[0];
	}
	let delta = skew / Math.sqrt(1 + skew * skew);
	let result = delta * randoms[0] + Math.sqrt(1 - delta * delta) * randoms[1];
	return randoms[0] >= 0 ? result : -result;
};

/**
 * Generate a skewed normal random variable between max and min
 * @param {number} max
 * @param {number} min
 * @param {number} skew
 * @returns {number}
 */
App.Utils.Math.limitedSkewedGaussian = function(max, min, skew) {
	let result = App.Utils.Math.skewedGaussian(skew);
	while (result < min || result > max) {
		result = App.Utils.Math.skewedGaussian(skew);
	}
	return result;
};

/**
 * Generate a random integer with a normal distribution between min and max (both inclusive).
 * Default parameters result in truncating the standard normal distribution between -3 and +3.
 * Not specifying min/max results in rerolling val approximately 0.3% of the time.
 * @param {number} [mean]
 * @param {number} [deviation]
 * @param {number} [min]
 * @param {number} [max]
 * @returns {number}
 */
function normalRandInt(mean = 0, deviation = 1, min = mean - 3 * deviation, max = mean + 3 * deviation) {
	let val = gaussianPair(mean, deviation)[0];
	while (val < min || val > max) {
		val = gaussianPair(mean, deviation)[0];
	}
	return Math.round(val);
}

/**
 * Returns a random integer between min and max (both inclusive).
 * If count is defined, chooses that many random numbers between min and max and returns the average. This is an approximation of a normal distribution.
 * @param {number} min
 * @param {number} max
 * @param {number} [count]
 * @returns {number}
 */
function jsRandom(min, max, count = 1) {
	function rand() {
		return Math.random() * (max - min + 1) + min;
	}

	if (count === 1) {
		return Math.floor(rand());
	}

	let total = 0;
	for (let i = 0; i < count; i++) {
		total += rand();
	}
	return Math.floor(total / count);
}

/**
 * Chooses multiple random elements of an array.
 * @template {*} T
 * @param {Array<T>} arr
 * @param {number} count
 * @returns {Array<T>}
 */
function jsRandomMany(arr, count) {
	let result = [];
	let tmp = arr.slice();
	for (let i = 0; i < count; i++) {
		let index = Math.floor(Math.random() * tmp.length);
		result.push(tmp.splice(index, 1)[0]);
	}
	return result;
}

/**
 * Accepts both an array and a list, returns undefined if nothing is passed.
 * @template {*} T
 * @param {Array<T>} choices
 * @param {...T} [otherChoices]
 * @returns {T}
 */
function jsEither(choices, ...otherChoices) {
	if (otherChoices.length === 0 && Array.isArray(choices)) {
		return choices[Math.floor(Math.random() * choices.length)];
	}
	const allChoices = otherChoices;
	allChoices.push(...choices);
	return allChoices[Math.floor(Math.random() * allChoices.length)];
}


code:
/**
 * @template {PropertyKey} K
 * @template {*} T
 * @param {Partial<Record<K, T>>} dict
 * @param {K} value
 * @param {T} [defaultValue]
 * @returns {T|null}
 */
App.Ratings.exact = function(dict, value, defaultValue = null) {
	const res = dict[value];
	return res ? res : defaultValue;
};

/**
 * @template {*} T
 * @param {Record<number, T>} ratings
 * @param {number} value
 * @returns {T|null}
 */
App.Ratings.numeric = function(ratings, value) {
	for (const key in ratings) {
		if (parseInt(key) >= value) {
			return ratings[key];
		}
	}
	return null;
};

/**
 * @param {object} ratings
 * @param {number[]} values
 * @returns {*|null}
 */
App.Ratings.multiNumeric = function(ratings, values) {
	const firstRating = App.Ratings.numeric(ratings, values[0]);
	if (firstRating === null || typeof firstRating === "string" || firstRating.hasOwnProperty("desc")) {
		return firstRating;
	}
	return App.Ratings.multiNumeric(firstRating, values.slice(1));
};


code:
App.RA.Activation.Stack = class Stack {
	constructor() {
		/**
		 * @private
		 * @type {number[]}
		 */
		this._numbers = [];
		/**
		 * @private
		 * @type {string[]}
		 */
		this._strings = [];
	}

	/**
	 * @param {boolean} v
	 */
	pushBoolean(v) {
		this._numbers.push(v ? 1 : 0);
	}

	/**
	 * @returns {FC.Bool}
	 */
	popAsBoolean() {
		return this._numbers.pop() ? 1 : 0;
	}

	/**
	 * @param {number} v
	 */
	pushNumber(v) {
		this._numbers.push(v);
	}

	/**
	 * @returns {number}
	 */
	popNumber() {
		return this._numbers.pop();
	}

	/**
	 * @param {string} v
	 */
	pushString(v) {
		this._strings.push(v);
	}

	/**
	 * @returns {string}
	 */
	popString() {
		return this._strings.pop();
	}
};


code:
/**
 * All functions should only be called from z1-conditionEditorController.js
 */
App.RA.Activation.SimpleEditor = (function() {
	/**
	 * @typedef {object} RuleState
	 * @property {"always"|"never"|"boolean"|"number"|"string"} activeRuleType
	 * @property {string} boolGetter
	 * @property {boolean} negateBool
	 * @property {string} stringGetter
	 * @property {string} stringComparator
	 * @property {string} stringValue
	 * @property {string} numberGetter
	 * @property {number} numberUpperValue
	 * @property {"lt"|"lte"|""} numberUpperComparator
	 * @property {number} numberLowerValue
	 * @property {"gt"|"gte"|""} numberLowerComparator
	 * @property {string[]} assignments
	 * @property {"include"|"exclude"|"ignore"} assignmentMode
	 */

	/**
	 * @type {HTMLDivElement}
	 */
	let editorNode = null;
	/**
	 * @type {RuleState}
	 */
	let currentRule = null;

	/**
	 * @param {FC.RA.PostFixRule} rule
	 * @param {HTMLDivElement}parent
	 */
	function editor(rule, parent) {
		currentRule = deserializeRule(rule);
		editorNode = parent;
		editorNode.append(buildEditor());
	}

	function refreshEditor() {
		if (editorNode !== null) {
			$(editorNode).empty().append(buildEditor());
		}
	}

	/**
	 * If the rule is valid, returns the serialized rule, otherwise null.
	 *
	 * @returns {FC.RA.PostFixRule}
	 */
	function saveEditor() {
		if (currentRule == null) {
			return null;
		}
		return serializeRule(currentRule);
	}

	function resetEditor() {
		currentRule = null;
		editorNode = null;
	}

	/**
	 * @returns {HTMLElement}
	 */
	function buildEditor() {
		const outerDiv = document.createElement("div");
		// selector
		const selectorDiv = document.createElement("div");
		selectorDiv.classList.add("button-group");
		outerDiv.append(selectorDiv);
		App.UI.DOM.appendNewElement("button", selectorDiv, "Always", currentRule.activeRuleType === "always" ? ["selected", "disabled"] : []).onclick = () => {
			currentRule.activeRuleType = "always";
			refreshEditor();
		};
		App.UI.DOM.appendNewElement("button", selectorDiv, "Never", currentRule.activeRuleType === "never" ? ["selected", "disabled"] : []).onclick = () => {
			currentRule.activeRuleType = "never";
			refreshEditor();
		};
		App.UI.DOM.appendNewElement("button", selectorDiv, "Boolean", currentRule.activeRuleType === "boolean" ? ["selected", "disabled"] : []).onclick = () => {
			currentRule.activeRuleType = "boolean";
			refreshEditor();
		};
		App.UI.DOM.appendNewElement("button", selectorDiv, "Number", currentRule.activeRuleType === "number" ? ["selected", "disabled"] : []).onclick = () => {
			currentRule.activeRuleType = "number";
			refreshEditor();
		};
		App.UI.DOM.appendNewElement("button", selectorDiv, "String", currentRule.activeRuleType === "string" ? ["selected", "disabled"] : []).onclick = () => {
			currentRule.activeRuleType = "string";
			refreshEditor();
		};

		// add bool
		if (currentRule.activeRuleType === "boolean") {
			const boolDiv = document.createElement("div");
			boolDiv.classList.add("button-group");
			outerDiv.append(boolDiv);
			/**
			 * @type {selectOption[]}
			 */
			const options = [];
			App.RA.Activation.getterManager.booleanGetters.forEach((getter, key) => {
				if (!getter.visible || getter.visible()) {
					options.push({
						key: key, name: getter.name, enabled: !getter.enabled || getter.enabled()
					});
				}
			});
			boolDiv.append(App.UI.DOM.makeSelect(options, currentRule.boolGetter, key => {
				currentRule.boolGetter = key;
				refreshEditor();
			}));
			boolDiv.append(" should be ");
			App.UI.DOM.appendNewElement("button", boolDiv, "True", currentRule.negateBool ? [] : ["selected", "disabled"]).onclick = () => {
				currentRule.negateBool = false;
				refreshEditor();
			};
			App.UI.DOM.appendNewElement("button", boolDiv, "False", currentRule.negateBool ? ["selected", "disabled"] : []).onclick = () => {
				currentRule.negateBool = true;
				refreshEditor();
			};
		} else if (currentRule.activeRuleType === "number") {
			const numberDiv = document.createElement("div");
			outerDiv.append(numberDiv);
			/**
			 * @type {selectOption[]}
			 */
			const options = [];
			App.RA.Activation.getterManager.numberGetters.forEach((getter, key) => {
				if (!getter.visible || getter.visible()) {
					options.push({
						key: key, name: getter.name, enabled: !getter.enabled || getter.enabled()
					});
				}
			});
			numberDiv.append(App.UI.DOM.makeSelect(options, currentRule.numberGetter, key => {
				currentRule.numberGetter = key;
				refreshEditor();
			}));
			numberDiv.append(" should be ");
			numberDiv.append(App.UI.DOM.makeSelect(
				[{key: "gt", name: "greater than"}, {key: "gte", name: "greater than or equal to"},
					{key: "", name: "ignored"}],
				currentRule.numberLowerComparator, key => {
					currentRule.numberLowerComparator = key;
					refreshEditor();
				}));
			if (currentRule.numberLowerComparator !== "") {
				numberDiv.append(" ", App.UI.DOM.makeTextBox(currentRule.numberLowerValue, val => {
					currentRule.numberLowerValue = val;
				}, true));
			}
			numberDiv.append(" and ");
			numberDiv.append(App.UI.DOM.makeSelect(
				[{key: "lt", name: "less than"}, {key: "lte", name: "less than or equal to"},
					{key: "", name: "ignored"}],
				currentRule.numberUpperComparator, key => {
					currentRule.numberUpperComparator = key;
					refreshEditor();
				}));
			if (currentRule.numberUpperComparator !== "") {
				numberDiv.append(" ", App.UI.DOM.makeTextBox(currentRule.numberUpperValue, val => {
					currentRule.numberUpperValue = val;
				}, true));
			}
		} else if (currentRule.activeRuleType === "string") {
			const stringDiv = document.createElement("div");
			outerDiv.append(stringDiv);
			/**
			 * @type {selectOption[]}
			 */
			const options = [];
			App.RA.Activation.getterManager.stringGetters.forEach((getter, key) => {
				if (!getter.visible || getter.visible()) {
					options.push({
						key: key, name: getter.name, enabled: !getter.enabled || getter.enabled()
					});
				}
			});
			stringDiv.append(App.UI.DOM.makeSelect(options, currentRule.stringGetter, key => {
				currentRule.stringGetter = key;
				refreshEditor();
			}));
			stringDiv.append(" should ");
			stringDiv.append(App.UI.DOM.makeSelect(
				[{key: "eqstr", name: "equal"}, {key: "substr", name: "contain"}, {key: "match", name: "match"}],
				currentRule.stringComparator, key => {
					currentRule.stringComparator = key;
					refreshEditor();
				}));
			stringDiv.append(" ", App.UI.DOM.makeTextBox(currentRule.stringValue, val => {
				currentRule.stringValue = val;
			}));
		}

		// Assignments
		outerDiv.append("Assignments: ");
		outerDiv.append(App.UI.DOM.makeSelect(
			[{key: "ignore", name: "Ignored"}, {key: "include", name: "Include"}, {key: "exclude", name: "Exclude"}],
			currentRule.assignmentMode, key => {
				currentRule.assignmentMode = key;
				refreshEditor();
			}
		));
		if (currentRule.assignmentMode !== "ignore") {
			for (const [key, getter] of App.RA.Activation.getterManager.assignmentGetters) {
				if (getter.enabled && !getter.enabled()) {
					continue;
				}
				const checkbox = document.createElement("input");
				checkbox.setAttribute("type", "checkbox");
				checkbox.checked = currentRule.assignments.includes(key);
				checkbox.onchange = () => {
					if (!currentRule.assignments.includes(key)) {
						currentRule.assignments.push(key);
					} else {
						const index = currentRule.assignments.indexOf(key);
						currentRule.assignments.splice(index, 1);
					}
				};
				outerDiv.append(` ${getter.name}: `, checkbox);
			}
		}

		return outerDiv;
	}

	/**
	 * @param {FC.RA.PostFixRule} rule
	 * @returns {RuleState}
	 */
	function deserializeRule(rule) {
		// About the TS errors in this function: we can assume a lot about the rule composition because we know it's in
		// the simple format. The rule itself is still a normal FC.RA.PostFixRule which would allow a lot more.
		// Therefore, TS is not happy even though we now everything's fine.
		/**
		 * @type {RuleState}
		 */
		const ruleState = {
			activeRuleType: "always",
			boolGetter: "isfertile",
			negateBool: false,
			stringGetter: "label",
			stringValue: "",
			stringComparator: "eqstr",
			numberGetter: "devotion",
			numberUpperValue: 100,
			numberUpperComparator: "",
			numberLowerValue: -100,
			numberLowerComparator: "",
			assignments: [],
			assignmentMode: "ignore",
		};
		// we know there is only main one rule.
		let i = 0;
		const rulePart = rule[i];
		if (rulePart === true) {
			ruleState.activeRuleType = "always";
		} else if (rulePart === false) {
			ruleState.activeRuleType = "never";
		} else if (App.RA.Activation.getterManager.isBoolean(rulePart)) {
			ruleState.activeRuleType = "boolean";
			ruleState.boolGetter = rulePart;
			if (rule[i + 1] === "not") {
				ruleState.negateBool = true;
				i++;
			}
		} else if (App.RA.Activation.getterManager.isNumber(rulePart)) {
			ruleState.activeRuleType = "number";
			ruleState.numberGetter = rulePart;
			// check if there is a lower rule:
			if (rule[i + 2].startsWith("g")) {
				ruleState.numberLowerValue = rule[i + 1];
				ruleState.numberLowerComparator = rule[i + 2];
				// check if there is also an upper value:
				if (rule[i + 3] === ruleState.numberGetter) {
					ruleState.numberUpperValue = rule[i + 4];
					ruleState.numberUpperComparator = rule[i + 5];
					i += 3;
				}
			} else {
				ruleState.numberUpperValue = rule[i + 1];
				ruleState.numberUpperComparator = rule[i + 2];
			}
			i += 2;
		} else if (App.RA.Activation.getterManager.isString(rulePart)) {
			ruleState.activeRuleType = "string";
			ruleState.stringGetter = rulePart;
			ruleState.stringValue = rule[i + 1].slice(1);
			ruleState.stringComparator = rule[i + 2];
			i += 2;
		} else {
			throw new Error("Rule is not in simple mode format!");
		}
		i++;

		// check for assignment rules
		let any = false;
		while (App.RA.Activation.getterManager.isAssignment(rule[i])) {
			any = true;
			ruleState.assignments.push(rule[i]);
			i++;
		}
		if (any) {
			i += 2;
			if (rule[i] === "not") {
				ruleState.assignmentMode = "exclude";
				i++;
			} else {
				ruleState.assignmentMode = "include";
			}
		}

		return ruleState;
	}

	/**
	 * Expects a valid RulePart structure
	 *
	 * @param {RuleState} ruleState
	 * @returns {FC.RA.PostFixRule}
	 */
	function serializeRule(ruleState) {
		/**
		 * @type {FC.RA.PostFixRule}
		 */
		const rule = [];
		let counter = 0;

		switch (ruleState.activeRuleType) {
			case "always":
				rule.push(true);
				counter++;
				break;
			case "never":
				rule.push(false);
				counter++;
				break;
			case "boolean":
				rule.push(ruleState.boolGetter);
				if (ruleState.negateBool) {
					rule.push("not");
				}
				counter++;
				break;
			case "number":
				// eslint-disable-next-line no-case-declarations
				let any = false;
				if (ruleState.numberLowerComparator !== "") {
					any = true;
					rule.push(ruleState.numberGetter);
					rule.push(ruleState.numberLowerValue);
					rule.push(ruleState.numberLowerComparator);
					counter++;
				}
				if (ruleState.numberUpperComparator !== "") {
					any = true;
					rule.push(ruleState.numberGetter);
					rule.push(ruleState.numberUpperValue);
					rule.push(ruleState.numberUpperComparator);
					counter++;
				}
				if (!any) {
					rule.push(true);
					counter++;
				}
				break;
			case "string":
				rule.push(ruleState.stringGetter);
				rule.push("!" + ruleState.stringValue);
				rule.push(ruleState.stringComparator);
				counter++;
				break;
		}

		if (ruleState.assignmentMode !== "ignore" && ruleState.assignments.length > 0) {
			rule.push(...ruleState.assignments);
			rule.push(ruleState.assignments.length, "or");
			counter++;
		}

		rule.push(counter, "and");
		return rule;
	}

	return {
		build: editor,
		save: saveEditor,
		reset: resetEditor,
	};
})();


code:
/**
 * All functions should only be called from z1-conditionEditorController.js
 */
App.RA.Activation.TreeEditor = (function() {
	/**
	 * @type {HTMLDivElement}
	 */
	let editorNode = null;
	/**
	 * @type {Map<string, RulePart>}
	 */
	let rulePartMap = null;
	/**
	 * @type {RuleGroup}
	 */
	let currentRule = null;

	/**
	 * @param {FC.RA.PostFixRule} rule
	 * @param {HTMLDivElement}parent
	 */
	function editor(rule, parent) {
		rulePartMap = new Map();
		currentRule = deserializeRule(rule);
		editorNode = parent;
		editorNode.append(buildEditor());
	}

	function refreshEditor() {
		if (editorNode !== null) {
			$(editorNode).empty().append(buildEditor());
		}
	}

	/**
	 * If the rule is valid, returns the serialized rule, otherwise null.
	 *
	 * @returns {FC.RA.PostFixRule}
	 */
	function saveEditor() {
		if (currentRule == null) {
			return null;
		}
		const error = currentRule.validate([]) === "error";
		if (!error) {
			return serializeRule(currentRule);
		}
		return null;
	}

	function resetEditor() {
		rulePartMap = null;
		currentRule = null;
		editorNode = null;
	}

	/**
	 * @returns {HTMLElement}
	 */
	function buildEditor() {
		const outerDiv = document.createElement("div");
		outerDiv.classList.add("rule-builder");

		const ruleDiv = document.createElement("div");
		ruleDiv.classList.add("condition-viewer");
		const errors = [];
		if (currentRule.validate(errors) === "error") {
			ruleDiv.append("Condition has errors:");
			for (const error of errors) {
				ruleDiv.append(" ", error);
			}
			ruleDiv.append(" Changes have NOT been saved!");
		} else {
			ruleDiv.append("Condition saved.");
		}
		ruleDiv.append(" ", App.Encyclopedia.Dialog.linkDOM("Help", "RA Condition Editor"));
		ruleDiv.append(currentRule.render());
		outerDiv.append(ruleDiv);

		outerDiv.append(buildPartBrowser());

		return outerDiv;
	}

	/**
	 * @returns {HTMLDivElement}
	 */
	function buildPartBrowser() {
		const container = document.createElement("div");
		App.UI.DOM.appendNewElement("h3", container, "Part Browser");
		const div = document.createElement("div");
		div.classList.add("rule-part-browser");
		div.append(new RulePartProvider(() => new RuleGroup("and")).render());
		div.append(new RulePartProvider(() => new RuleGroup("add")).render());
		div.append(new RulePartProvider(() => new RuleNegate()).render());
		div.append(new RulePartProvider(() => new RulePair("eq")).render());
		div.append(new RulePartProvider(() => new RuleTernary()).render());
		div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.booleanDefault)).render());
		div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.numberDefault)).render());
		div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.stringDefault)).render());
		div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.assignmentDefault)).render());
		div.append(new RulePartProvider(() => new RuleConstant(0)).render());
		div.append(new RulePartProvider(() => new RuleBooleanConstant(true)).render());
		div.append(new RulePartProvider(() => new RuleConstant("some text")).render());
		div.append(new RulePartProvider(() => new RuleCustomCheck("bcontext => false")).render());
		div.append(new RulePartTrash().render());
		container.append(div);
		return container;
	}

	/**
	 * @abstract
	 */
	class RulePart {
		constructor() {
			this.id = generateNewID();
			rulePartMap.set(this.id, this);

			/**
			 * @type {?RuleContainer}
			 */
			this.parent = null;

			/**
			 * @type {HTMLElement}
			 * @protected
			 */
			this._dragElement = null;

			/**
			 * @type {boolean}
			 * @private
			 */
			this._showValidationError = true;
		}

		/**
		 * @abstract
		 * @returns {HTMLElement}
		 */
		render() {
			throw new Error("Method 'render()' must be implemented.");
		}

		/**
		 * Validate the rule, gives the expected return type or "error".
		 * @abstract
		 * @param {Array<string>} errorList
		 * @returns {"number"|"string"|"error"}
		 */
		validate(errorList) {
			throw new Error("Method 'validate()' must be implemented.");
		}

		/**
		 * Makes the element not draggable
		 */
		disableDragging() {
			if (this._dragElement != null) {
				this._dragElement.draggable = false;
			}
			if (this.parent != null) {
				this.parent.disableDragging();
			}
		}

		/**
		 * Makes the element draggable again.
		 */
		enableDragging() {
			if (this.parent != null) {
				if (this._dragElement != null) {
					this._dragElement.draggable = true;
				}
				this.parent.enableDragging();
			}
		}

		/**
		 * @protected
		 * @param {HTMLElement} element
		 */
		_markValidationError(element) {
			if (this._showValidationError && this.validate([]) === "error") {
				element.classList.add("validation-error");
			}
		}

		/**
		 * @param {boolean} value
		 */
		set showValidationError(value) {
			this._showValidationError = value;
		}
	}

	/**
	 * @abstract
	 */
	class RuleContainer extends RulePart {
		/**
		 * @abstract
		 * @param {RulePart} rulePart
		 */
		removeChild(rulePart) {
			throw new Error("Method 'removeChild()' must be implemented.");
		}

		/**
		 * @abstract
		 * @param {RuleContainer} maybeChild
		 * @returns {boolean}
		 */
		isParent(maybeChild) {
			throw new Error("Method 'isParent()' must be implemented.");
		}

		/**
		 * @param {HTMLElement} element
		 * @param {(child:RulePart)=>void} setChild
		 * @protected
		 */
		_allowDroppingOn(element, setChild) {
			element.ondragover = ev => {
				if (canDrop(ev, this)) {
					// show that it can be dropped
					ev.preventDefault();
				}
				// stop containers further out from capturing the event.
				ev.stopPropagation();
			};
			element.ondrop = ev => {
				ev.preventDefault();
				// stop groups further out from capturing the event.
				ev.stopPropagation();
				const rulePart = rulePartMap.get(ev.dataTransfer.getData("text/plain"));
				setChild(rulePart);
				refreshEditor();
			};
		}

		/**
		 * @param {HTMLElement} container
		 * @param {?RulePart} child
		 * @param {(child:RulePart)=>void} setChild
		 * @protected
		 */
		_conditionalDropLocation(container, child, setChild) {
			if (child == null) {
				const span = document.createElement("span");
				span.classList.add("rule-drop-location");
				this._allowDroppingOn(span, setChild);
				container.append(span);
			} else {
				container.append(child.render());
			}
		}
	}

	class RulePartProvider extends RuleContainer {
		/**
		 * @param {()=>RulePart} partFactory
		 */
		constructor(partFactory) {
			super();
			this._partFactory = partFactory;
		}

		render() {
			const part = this._partFactory();
			part.parent = this;
			part.showValidationError = false;
			const element = part.render();
			part.showValidationError = true;
			return element;
		}

		/**
		 * @returns {"error"}
		 */
		validate(errorList) {
			return "error";
		}

		removeChild(rulePart) {
		}

		isParent(maybeChild) {
			return false;
		}
	}

	class RulePartTrash extends RuleContainer {
		render() {
			const div = document.createElement("div");
			div.classList.add("rule-trash");
			div.ondragover = ev => {
				// show that it can be dropped
				ev.preventDefault();
				// stop groups further out from capturing the event.
				ev.stopPropagation();
			};
			div.ondrop = ev => {
				ev.preventDefault();
				// stop groups further out from capturing the event.
				ev.stopPropagation();
				const rulePartID = ev.dataTransfer.getData("text/plain");
				const rulePart = rulePartMap.get(rulePartID);
				rulePart.parent.removeChild(rulePart);
				refreshEditor();
			};
			return div;
		}

		/**
		 * @returns {"error"}
		 */
		validate(errorList) {
			return "error";
		}

		removeChild(rulePart) {
		}

		isParent(maybeChild) {
			return false;
		}
	}

	/**
	 * @typedef {"and"|"or"|"add"|"mul"|"max"|"min"} RuleGroupAggregators
	 * @type {Map<RuleGroupAggregators, string>}
	 */
	const ruleGroupAggregators = new Map([
		["and", "And"],
		["or", "Or"],
		["add", "Sum all"],
		["mul", "Multiply all"],
		["max", "Maximum"],
		["min", "Minimum"],
	]);

	class RuleGroup extends RuleContainer {
		/**
		 * @param {RuleGroupAggregators} mode
		 */
		constructor(mode) {
			super();
			this.mode = mode;
			/**
			 * @type {RulePart[]}
			 * @private
			 */
			this._children = [];
		}

		render() {
			const div = document.createElement("div");
			div.classList.add("rule-part");
			div.append(ruleGroupAggregators.get(this.mode));
			const button = App.UI.DOM.appendNewElement("button", div, "<->");
			button.onclick = () => {
				if (this.mode === "and") {
					this.mode = "or";
				} else if (this.mode === "or") {
					this.mode = "and";
				} else if (this.mode === "add") {
					this.mode = "mul";
				} else if (this.mode === "mul") {
					this.mode = "max";
				} else if (this.mode === "max") {
					this.mode = "min";
				} else {
					this.mode = "add";
				}
				refreshEditor();
			};
			for (const rulePart of this._children) {
				div.append(rulePart.render());
			}
			let span = document.createElement("span");
			span.classList.add("rule-drop-location");
			div.append(span);
			// interactions
			this._allowDroppingOn(div, (child => this.addChild(child)));
			if (this.parent !== null) {
				// if null, it's the outermost and that can't be draggable
				makeDraggable(div, this);
				this._dragElement = div;
			}
			this._markValidationError(div);
			return div;
		}

		validate(errorList) {
			if (this._children.length === 0) {
				errorList.push("Condition group needs at least 1 condition.");
				return "error";
			}
			const expectedType = "number";
			for (const rulePart of this.children) {
				if (rulePart.validate(errorList) !== expectedType) {
					errorList.push("Condition group only accepts boolean and number conditions.");
					return "error";
				}
			}
			return expectedType;
		}

		/**
		 * @param {RulePart} rulePart
		 */
		addChild(rulePart) {
			if (rulePart.parent != null) {
				rulePart.parent.removeChild(rulePart);
			}
			this._children.push(rulePart);
			rulePart.parent = this;
		}

		/**
		 * @override
		 * @param {RulePart} rulePart
		 */
		removeChild(rulePart) {
			this._children.delete(rulePart);
			rulePart.parent = null;
		}

		/**
		 * @override
		 * @param {RuleContainer} maybeChild
		 */
		isParent(maybeChild) {
			for (const child of this._children) {
				if (isSameOrParent(child, maybeChild)) {
					return true;
				}
			}
			return false;
		}

		/**
		 * @returns {ReadonlyArray<RulePart>}
		 */
		get children() {
			return this._children;
		}
	}

	class RuleNegate extends RuleContainer {
		constructor() {
			super();
			/**
			 * @type {?RulePart}
			 * @private
			 */
			this._child = null;
		}

		render() {
			const div = document.createElement("div");
			div.classList.add("rule-part");
			div.append("Not");

			if (this.child == null) {
				let span = document.createElement("span");
				span.classList.add("rule-drop-location");
				div.append(span);
				this._allowDroppingOn(div, child => this.child = child);
			} else {
				div.append(this.child.render());
				div.ondragover = ev => {
					// stop groups further out from capturing the event.
					ev.stopPropagation();
				};
			}

			makeDraggable(div, this);
			this._dragElement = div;
			this._markValidationError(div);

			return div;
		}

		validate(errorList) {
			if (this._child == null) {
				errorList.push("Negation needs a condition to negate.");
				return "error";
			}
			if (this._child.validate(errorList) === "number") {
				return "number";
			} else {
				errorList.push("Negation accepts only boolean and number conditions.");
				return "error";
			}
		}

		/**
		 * @param {RulePart} rulePart
		 */
		set child(rulePart) {
			if (rulePart.parent != null) {
				rulePart.parent.removeChild(rulePart);
			}
			this._child = rulePart;
			rulePart.parent = this;
		}

		/**
		 * @returns {RulePart}
		 */
		get child() {
			return this._child;
		}

		/**
		 * @override
		 * @param {RulePart} rulePart
		 */
		removeChild(rulePart) {
			this._child = null;
			rulePart.parent = null;
		}

		/**
		 * @override
		 * @param {RuleContainer} maybeChild
		 */
		isParent(maybeChild) {
			if (this._child == null) {
				return false;
			}
			return (isSameOrParent(this._child, maybeChild));
		}
	}

	/**
	 * @typedef {"sub" | "div" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte"| "substr" | "match"} RulePairComparators
	 * @typedef {object} RulePairComparatorDisplay
	 * @property {RulePairComparators} key
	 * @property {string} name
	 * @type {RulePairComparatorDisplay[]}
	 */
	const rulePairComparators = [
		{key: "eq", name: "="},
		{key: "neq", name: "≠"},
		{key: "lt", name: "<"},
		{key: "gt", name: ">"},
		{key: "lte", name: "⩽"},
		{key: "gte", name: "⩾"},
		{key: "sub", name: "-"},
		{key: "div", name: "/"},
		{key: "substr", name: "Contains"},
		{key: "match", name: "Matches"},
	];

	class RulePair extends RuleContainer {
		/**
		 * @param {RulePairComparators} mode
		 */
		constructor(mode) {
			super();
			this.mode = mode;
			/**
			 * @type {RulePart}
			 */
			this._child1 = null;
			/**
			 * @type {RulePart}
			 */
			this._child2 = null;
		}

		render() {
			const div = document.createElement("div");
			div.classList.add("rule-part");
			// drag element
			makeNotDraggable(div);
			div.append(createDragElement(this));
			// element 1
			this._conditionalDropLocation(div, this._child1, child => this.child1 = child);

			// operator
			div.append(App.UI.DOM.makeSelect(rulePairComparators, this.mode, mode => {
				// @ts-ignore
				this.mode = mode;
				refreshEditor();
			}));

			// element 2
			this._conditionalDropLocation(div, this._child2, child => this.child2 = child);

			this._markValidationError(div);
			return div;
		}

		validate(errorList) {
			if (this._child1 == null || this._child2 == null) {
				errorList.push("Comparator conditions need a condition on both sides.");
				return "error";
			}
			if (this.mode === "eq" || this.mode === "neq") {
				const expectedType = this._child1.validate(errorList);
				if (expectedType === this._child2.validate(errorList)) {
					return "number";
				} else {
					errorList.push("Both sides need to return the same type.");
					return "error";
				}
			} else if (this.mode === "substr" || this.mode === "match") {
				if (this._child1.validate(errorList) === "string" && this._child2.validate(errorList) === "string") {
					return "number";
				} else {
					errorList.push("Both sides need to return string.");
					return "error";
				}
			} else {
				if (this._child1.validate(errorList) === "number" && this._child2.validate(errorList) === "number") {
					return "number";
				} else {
					errorList.push("Both sides need to return number.");
					return "error";
				}
			}
		}

		/**
		 * @param {RulePart} child
		 */
		set child1(child) {
			if (child.parent != null) {
				child.parent.removeChild(child);
			}
			child.parent = this;
			this._child1 = child;
		}

		/**
		 * @returns {RulePart}
		 */
		get child1() {
			return this._child1;
		}

		/**
		 * @param {RulePart} child
		 */
		set child2(child) {
			if (child.parent != null) {
				child.parent.removeChild(child);
			}
			child.parent = this;
			this._child2 = child;
		}

		/**
		 * @returns {RulePart}
		 */
		get child2() {
			return this._child2;
		}

		/**
		 * @override
		 * @param {RulePart} rulePart
		 */
		removeChild(rulePart) {
			if (this._child1 === rulePart) {
				this._child1 = null;
			} else if (this._child2 === rulePart) {
				this._child2 = null;
			}
			rulePart.parent = null;
		}

		/**
		 * @override
		 * @param {RuleContainer} maybeChild
		 */
		isParent(maybeChild) {
			return isSameOrParent(this._child1, maybeChild) || isSameOrParent(this._child2, maybeChild);
		}
	}

	class RuleTernary extends RuleContainer {
		constructor() {
			super();
			/**
			 * @type {RulePart}
			 */
			this._condition = null;
			/**
			 * @type {RulePart}
			 */
			this._ifTrue = null;
			/**
			 * @type {RulePart}
			 */
			this._ifFalse = null;
		}

		render() {
			const div = document.createElement("div");
			div.classList.add("rule-part");
			// drag element
			makeDraggable(div, this);
			div.append(createDragElement(this));

			// condition
			App.UI.DOM.appendNewElement("span", div, "If", ["rule-left-margin"]);
			this._conditionalDropLocation(div, this.condition, child => this.condition = child);
			// ifTrue
			div.append("Then");
			this._conditionalDropLocation(div, this.ifTrue, child => this.ifTrue = child);
			// ifFalse
			div.append("Else");
			this._conditionalDropLocation(div, this.ifFalse, child => this.ifFalse = child);

			this._markValidationError(div);
			return div;
		}

		validate(errorList) {
			if (this._condition === null) {
				errorList.push("Ternaries need a condition.");
				return "error";
			}
			if (this._condition.validate(errorList) !== "number") {
				errorList.push("Ternaries conditions accepts only booleans or numbers.");
				return "error";
			}
			if (this._ifTrue == null || this._ifFalse == null) {
				errorList.push("Ternaries need values on both sides.");
				return "error";
			}
			const expectedType = this._ifTrue.validate(errorList);
			if (expectedType === this._ifFalse.validate(errorList)) {
				return "number";
			} else {
				errorList.push("Both sides need to return the same type.");
				return "error";
			}
		}

		/**
		 * @param {RulePart} child
		 */
		set condition(child) {
			if (child.parent != null) {
				child.parent.removeChild(child);
			}
			child.parent = this;
			this._condition = child;
		}

		/**
		 * @returns {RulePart}
		 */
		get condition() {
			return this._condition;
		}

		/**
		 * @param {RulePart} child
		 */
		set ifTrue(child) {
			if (child.parent != null) {
				child.parent.removeChild(child);
			}
			child.parent = this;
			this._ifTrue = child;
		}

		/**
		 * @returns {RulePart}
		 */
		get ifTrue() {
			return this._ifTrue;
		}

		/**
		 * @param {RulePart} child
		 */
		set ifFalse(child) {
			if (child.parent != null) {
				child.parent.removeChild(child);
			}
			child.parent = this;
			this._ifFalse = child;
		}

		/**
		 * @returns {RulePart}
		 */
		get ifFalse() {
			return this._ifFalse;
		}

		/**
		 * @override
		 * @param {RulePart} rulePart
		 */
		removeChild(rulePart) {
			if (this._condition === rulePart) {
				this._condition = null;
			} else if (this._ifTrue === rulePart) {
				this._ifTrue = null;
			} else if (this._ifFalse === rulePart) {
				this._ifFalse = null;
			}
			rulePart.parent = null;
		}

		/**
		 * @override
		 * @param {RuleContainer} maybeChild
		 */
		isParent(maybeChild) {
			return isSameOrParent(this._condition, maybeChild) ||
				isSameOrParent(this._ifTrue, maybeChild) ||
				isSameOrParent(this._ifFalse, maybeChild);
		}
	}

	class RuleBooleanConstant extends RulePart {
		/**
		 * @param {boolean} mode
		 */
		constructor(mode) {
			super();
			this.mode = mode;
		}

		render() {
			const b = App.UI.DOM.makeElement("button", this.mode ? "Always" : "Never", ["rule-part"]);
			b.onclick = () => {
				this.mode = !this.mode;
				refreshEditor();
			};
			makeDraggable(b, this);
			return b;
		}

		/**
		 * @returns {"number"}
		 */
		validate() {
			return "number";
		}
	}

	class RuleConstant extends RulePart {
		/**
		 * @param {number|string} value
		 */
		constructor(value) {
			super();
			this.value = value;
			this._stringMode = typeof value === "string";
		}

		render() {
			const div = App.UI.DOM.makeElement("div", createDragElement(this), ["rule-part"]);
			div.append(this._stringMode ? " String" : " Number");
			div.append(makeTextBoxDragSafe(App.UI.DOM.makeTextBox(this.value, (v) => {
				this.value = v;
				refreshEditor();
			}, !this._stringMode), this));
			return div;
		}

		/**
		 * @returns {"number"|"string"}
		 */
		validate() {
			return this._stringMode ? "string" : "number";
		}
	}

	class RuleMapCheck extends RulePart {
		/**
		 * @param {string} key
		 */
		constructor(key) {
			super();
			/**
			 * @type {"boolean"|"assignment"|"number"|"string"}
			 */
			this.mode = App.RA.Activation.getterManager.isBoolean(key) ? "boolean"
				: App.RA.Activation.getterManager.isAssignment(key) ? "assignment"
					: App.RA.Activation.getterManager.isNumber(key) ? "number"
						: "string";
			this.key = key;
		}

		render() {
			// make container
			const span = document.createElement("span");
			span.classList.add("rule-part");
			makeDraggable(span, this);

			// fill container
			// name
			App.UI.DOM.appendNewElement("span", span, this.mode === "assignment" ? "Assignment" : "Slave", ["rule-right-margin"]);

			// values
			/**
			 * @type {selectOption[]}
			 */
			const options = [];
			this._getterMap.forEach((getter, key) => {
				if (!getter.visible || getter.visible()) {
					options.push({
						key: key, name: getter.name, enabled: !getter.enabled || getter.enabled()
					});
				}
			});
			span.append(App.UI.DOM.makeSelect(options, this.key, key => {
				this.key = key;
				refreshEditor();
			}));
			return span;
		}

		validate() {
			if (this.mode === "boolean" || this.mode === "assignment") {
				return "number";
			}
			return this.mode;
		}

		/**
		 * @returns {ReadonlyMap<string, Getter<*>>}
		 * @private
		 */
		get _getterMap() {
			return this.mode === "boolean" ? App.RA.Activation.getterManager.booleanGetters
				: this.mode === "assignment" ? App.RA.Activation.getterManager.assignmentGetters
					: this.mode === "number" ? App.RA.Activation.getterManager.numberGetters
						: App.RA.Activation.getterManager.stringGetters;
		}
	}

	class RuleCustomCheck extends RulePart {
		/**
		 * @param {string} check
		 */
		constructor(check) {
			super();
			this.check = check;
			/**
			 * @type {"boolean"|"number"|"string"}
			 * @private
			 */
			this._expectedType = check.first() === "b" ? "boolean"
				: check.first() === "n" ? "number"
					: "string";
		}

		render() {
			const outerDiv = App.UI.DOM.makeElement("div", null, ["rule-part"]);
			const leftDiv = document.createElement("div");
			leftDiv.classList.add("rule-custom-controls");

			const upperDiv = document.createElement("div");
			upperDiv.append(createDragElement(this), " Custom Getter");
			leftDiv.append(upperDiv);

			const lowerDiv = document.createElement("div");
			lowerDiv.classList.add("rule-custom-mode");
			lowerDiv.append("Mode:", this._makeToggleButton());
			leftDiv.append(lowerDiv);

			outerDiv.append(leftDiv);

			const textArea = document.createElement("textarea");
			textArea.append(this.check.slice(1));
			textArea.onchange = ev => {
				// @ts-ignore
				this.check = this._expectedType.first() + ev.target.value;
				refreshEditor();
			};
			outerDiv.append(makeTextBoxDragSafe(textArea, this));
			this._markValidationError(outerDiv);
			return outerDiv;
		}

		_makeToggleButton() {
			const button = document.createElement("button");
			button.append(capFirstChar(this._expectedType));
			button.onclick = () => {
				if (this._expectedType === "boolean") {
					this._expectedType = "number";
				} else if (this._expectedType === "number") {
					this._expectedType = "string";
				} else {
					this._expectedType = "boolean";
				}
				this.check = this._expectedType.first() + this.check.slice(1);
				refreshEditor();
			};
			return button;
		}

		validate(errorList) {
			try {
				runWithReadonlyProxy(() => this._validateRule(this.check.slice(1)));
			} catch (e) {
				errorList.push(e.message + ".");
				return "error";
			}
			return this._expectedType === "boolean" ? "number" : this._expectedType;
		}

		_validateRule(check) {
			const slave = createReadonlyProxy(V.slaves[0]);
			const context = new App.RA.Activation.Context(slave);
			(new Function("c", `return (${check})(c)`))(context);
			return true;
		}
	}

	/**
	 * @param {HTMLInputElement|HTMLTextAreaElement} textBox
	 * @param {RulePart} rulePart
	 * @returns {HTMLSpanElement}
	 */
	function makeTextBoxDragSafe(textBox, rulePart) {
		textBox.onfocus = () => rulePart.disableDragging();
		textBox.onmouseover = () => rulePart.disableDragging();
		textBox.onblur = () => rulePart.enableDragging();
		textBox.onmouseout = () => rulePart.enableDragging();
		return textBox;
	}

	/**
	 * @param {RulePart} rulePart
	 */
	function createDragElement(rulePart) {
		const element = document.createElement("div");
		element.classList.add("rule-drag-element");
		makeDraggable(element, rulePart);
		return element;
	}

	/**
	 * @param {HTMLElement} node
	 * @param {RulePart} rulePart
	 */
	function makeDraggable(node, rulePart) {
		node.draggable = true;
		node.classList.add("rule-draggable");
		node.ondragstart = ev => {
			ev.stopPropagation();
			editorNode.classList.add("part-dragging");
			ev.dataTransfer.setData("text/plain", rulePart.id);
		};
		node.ondragend = _ => {
			editorNode.classList.remove("part-dragging");
		};
	}

	/**
	 * @param {HTMLElement} node
	 */
	function makeNotDraggable(node) {
		node.ondragstart = ev => {
			ev.stopPropagation();
		};
	}

	/**
	 * @param {DragEvent} event
	 * @param {RuleContainer} targetPart
	 * @returns {boolean}
	 * @private
	 */
	function canDrop(event, targetPart) {
		const movedPartID = event.dataTransfer.getData("text/plain");
		const movedPart = rulePartMap.get(movedPartID);
		// if it can't have children, any place is valid
		if (!(movedPart instanceof RuleContainer)) {
			return true;
		}
		// don't allow dragging onto itself
		if (movedPart === targetPart) {
			return false;
		}
		// don't allow dragging onto children
		return !movedPart.isParent(targetPart);
	}

	/**
	 * @param {RulePart} parent
	 * @param {RuleContainer} maybeChild
	 * @returns {boolean}
	 */
	function isSameOrParent(parent, maybeChild) {
		if (parent === maybeChild) {
			return true;
		}
		return (parent instanceof RuleContainer) && parent.isParent(maybeChild);
	}

	/**
	 * @param {FC.RA.PostFixRule} rule
	 * @returns {RuleGroup}
	 */
	function deserializeRule(rule) {
		let stack = new RuleFactoryStack();

		/**
		 * @param {"and"|"or"|"add"|"mul"|"max"|"min"} mode
		 */
		function makeGroup(mode) {
			const length = stack.popNumber();
			const group = new RuleGroup(mode);
			const children = [];
			for (let i = 0; i < length; i++) {
				children.unshift(stack.popRulePart());
			}
			for (const child of children) {
				group.addChild(child);
			}
			stack.pushRulePart(group);
		}

		/**
		 * @param {"sub" | "div" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "substr" | "match"} mode
		 */
		function makePair(mode) {
			const pair = new RulePair(mode);
			pair.child2 = stack.popRulePart();
			pair.child1 = stack.popRulePart();
			stack.pushRulePart(pair);
		}

		function makeTernary() {
			const ternary = new RuleTernary();
			ternary.ifFalse = stack.popRulePart();
			ternary.ifTrue = stack.popRulePart();
			ternary.condition = stack.popRulePart();
			stack.pushRulePart(ternary);
		}

		/**
		 * @type {Map<string, function(): void>}
		 */
		const operators = new Map([
			// and, or, +, * can take arbitrarily many arguments, so the first one describes the argument count
			["and", () => makeGroup("and")],
			["or", () => makeGroup("or")],
			["add", () => makeGroup("add")],
			["mul", () => makeGroup("mul")],
			["max", () => makeGroup("max")],
			["min", () => makeGroup("min")],
			["sub", () => makePair("sub")],
			["div", () => makePair("div")],
			["eqstr", () => makePair("eq")],
			["neqstr", () => makePair("neq")],
			["eqnum", () => makePair("eq")],
			["neqnum", () => makePair("neq")],
			["gt", () => makePair("gt")],
			["gte", () => makePair("gte")],
			["lt", () => makePair("lt")],
			["lte", () => makePair("lte")],
			["substr", () => makePair("substr")],
			["match", () => makePair("match")],
			["not", () => {
				const negate = new RuleNegate();
				negate.child = stack.popRulePart();
				stack.pushRulePart(negate);
			}],
			["ternarystr", makeTernary],
			["ternarynum", makeTernary],
		]);

		for (let i = 0; i < rule.length; i++) {
			const rulePart = rule[i];
			if (typeof rulePart === "string") {
				const operation = operators.get(rulePart);
				if (operation !== undefined) {
					operation();
				} else if (App.RA.Activation.getterManager.has(rulePart)) {
					stack.pushRulePart(new RuleMapCheck(rulePart));
				} else if (rulePart.startsWith("?")) {
					stack.pushRulePart(new RuleCustomCheck(rulePart.slice(1)));
				} else {
					stack.pushRulePart(new RuleConstant(rulePart.slice(1)));
				}
			} else if (typeof rulePart === "number") {
				// check if this is a length counter
				const next = rule[i + 1];
				if (["and", "or", "add", "mul"].includes(next)) {
					stack.pushNumber(rulePart);
				} else {
					stack.pushRulePart(new RuleConstant(rulePart));
				}
			} else {
				stack.pushRulePart(new RuleBooleanConstant(rulePart));
			}
		}

		// @ts-ignore
		return stack.popRulePart();
	}

	/**
	 * Expects a valid RulePart structure
	 *
	 * @param {RuleGroup} rulePart
	 * @returns {FC.RA.PostFixRule}
	 */
	function serializeRule(rulePart) {
		/**
		 * @type {FC.RA.PostFixRule}
		 */
		const rule = [];
		_serializeRulePart(rulePart);
		return rule;

		/**
		 * @param {RulePart} rulePart
		 */
		function _serializeRulePart(rulePart) {
			if (rulePart instanceof RuleGroup) {
				for (const ruleChild of rulePart.children) {
					_serializeRulePart(ruleChild);
				}
				rule.push(rulePart.children.length, rulePart.mode);
			} else if (rulePart instanceof RulePair) {
				_serializeRulePart(rulePart.child1);
				_serializeRulePart(rulePart.child2);
				let mode = rulePart.mode;
				if (rulePart.mode === "eq" || rulePart.mode === "neq") {
					if (rulePart.child1.validate([]) === "string") {
						mode += "str";
					} else {
						mode += "num";
					}
				}
				rule.push(mode);
			} else if (rulePart instanceof RuleBooleanConstant) {
				rule.push(rulePart.mode);
			} else if (rulePart instanceof RuleMapCheck) {
				rule.push(rulePart.key);
			} else if (rulePart instanceof RuleConstant) {
				rule.push(typeof rulePart.value === "string" ? "!" + rulePart.value : rulePart.value);
			} else if (rulePart instanceof RuleNegate) {
				_serializeRulePart(rulePart.child);
				rule.push("not");
			} else if (rulePart instanceof RuleTernary) {
				_serializeRulePart(rulePart.condition);
				_serializeRulePart(rulePart.ifTrue);
				_serializeRulePart(rulePart.ifFalse);
				let mode = "ternary";
				if (rulePart.ifTrue.validate([]) === "string") {
					mode += "str";
				} else {
					mode += "num";
				}
				rule.push(mode);
			} else if (rulePart instanceof RuleCustomCheck) {
				rule.push("?" + rulePart.check);
			}
		}
	}

	class RuleFactoryStack extends App.RA.Activation.Stack {
		constructor() {
			super();
			/**
			 * @private
			 * @type {RulePart[]}
			 */
			this._ruleParts = [];
		}

		/**
		 * @param {RulePart} v
		 */
		pushRulePart(v) {
			this._ruleParts.push(v);
		}

		/**
		 * @returns {RulePart}
		 */
		popRulePart() {
			return this._ruleParts.pop();
		}
	}

	/**
	 * @param {FC.RA.PostFixRule} rule
	 */
	function validate(rule) {
		let mapExists = rulePartMap !== null;
		if (!mapExists) {
			rulePartMap = new Map();
		}
		const rulePart = deserializeRule(rule);
		if (!(rulePart instanceof RuleGroup)) {
			console.log("validation error", rule, rulePart, "Outermost is not RuleGroup!");
		}
		if (rulePart.mode !== "and" && rulePart.mode !== "or") {
			console.log("validation error", rule, rulePart, "Outermost has to be \"and\" or \"or\" mode RuleGroup!");
		}
		const errors = [];
		const result = rulePart.validate(errors);
		if (result === "error") {
			console.log("validation error", rule, rulePart, errors);
		}
		if (!mapExists) {
			rulePartMap = null;
		}
		return result !== "error";
	}

	return {
		build: editor,
		save: saveEditor,
		reset: resetEditor,
		validateRule: validate,
	};
})();