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

repo_name: Banyanael/fc

license: mit


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 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}
 */
/* exported normalRandInt */
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}
 */
/* exported jsRandom */
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>}
 */
/* exported jsRandomMany */
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}
 */
/* exported jsEither */
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:
App.RA.Activation.Editor = (function() {
	/**
	 * @type {HTMLDivElement}
	 */
	let editorNode = null;
	/**
	 * @type {Map<string, RulePart>}
	 */
	let rulePartMap = null;
	/**
	 * @type {RuleGroup}
	 */
	let currentRule = null;

	/**
	 * @param {FC.RA.PostFixRule} rule
	 * @returns {HTMLDivElement}
	 */
	function editor(rule) {
		rulePartMap = new Map();
		currentRule = deserializeRule(rule);
		editorNode = document.createElement("div");
		editorNode.append(buildEditor());
		return editorNode;
	}

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

	/**
	 * Save the rule, if it is valid.
	 *
	 * @param {(rule:FC.RA.PostFixRule)=>void} callback
	 */
	function saveEditor(callback) {
		if (currentRule == null) {
			return;
		}
		const error = currentRule.validate([]) === "error";
		if (!error) {
			callback(serializeRule(currentRule));
		}
	}

	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");
		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("string")).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"} RulePairComparators
	 * @type {Map<RulePairComparators, string>}
	 */
	const rulePairComparators = new Map([
		["eq", "="],
		["neq", "≠"],
		["lt", "<"],
		["gt", ">"],
		["lte", "⩽"],
		["gte", "⩾"],
		["sub", "-"],
		["div", "/"],
		["substr", "Contains"],
	]);

	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
			let matchFound = false;
			const select = document.createElement("select");
			for (const [key, name] of rulePairComparators) {
				const el = document.createElement("option");
				el.value = key;
				el.textContent = name;
				if (this.mode === key) {
					el.selected = true;
					matchFound = true;
				}
				select.append(el);
			}
			if (!matchFound) {
				select.selectedIndex = -1;
			}
			select.onchange = () => {
				/** @type {HTMLSelectElement} */
				// @ts-ignore
				const option = select.children.item(select.selectedIndex);
				// @ts-ignore
				this.mode = option.value;
				refreshEditor();
			};
			div.append(select);

			// 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") {
				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"]);
			const button = document.createElement("button");
			button.append(this._stringMode ? "String" : "Number");
			button.onclick = () => {
				this._stringMode = !this._stringMode;
				if (this._stringMode) {
					this.value = String(this.value);
				} else {
					this.value = Number(this.value);
					if (Number.isNaN(this.value)) {
						this.value = 0;
					}
				}
				refreshEditor();
			};
			div.append(button);
			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
			let matchFound = false;
			let select = document.createElement("select");
			for (const [key, value] of this._getterMap) {
				if (value.visible && !value.visible()) {
					continue;
				}
				let el = document.createElement("option");
				el.value = key;
				el.textContent = value.name;
				if (value.enabled) {
					el.disabled = !value.enabled();
				}
				if (this.key === key) {
					el.selected = true;
					matchFound = true;
				}
				select.append(el);
			}
			if (!matchFound) {
				select.selectedIndex = -1;
			}
			select.onchange = () => {
				/** @type {HTMLSelectElement} */
				// @ts-ignore
				const option = select.children.item(select.selectedIndex);
				this.key = option.value;
				refreshEditor();
			};
			span.append(select);
			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 div = App.UI.DOM.makeElement("div", createDragElement(this), ["rule-part"]);
			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();
			};
			div.append(button);
			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();
			};
			div.append(makeTextBoxDragSafe(textArea, this));
			this._markValidationError(div);
			return div;
		}

		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 context = new App.RA.Activation.Context(GenerateNewSlave());
			eval(check)(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();
			ev.dataTransfer.setData("text/plain", rulePart.id);
		};
	}

	/**
	 * @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"} 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")],
			["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,
	};
})();


code:
/* eslint-disable sonarjs/no-identical-expressions */

App.RA.Activation.Context = class {
	/**
	 * @param {App.Entity.SlaveState} slave
	 */
	constructor(slave) {
		this._slave = slave;
	}

	get slave() {
		return this._slave;
	}
};

/**
 * @template {boolean|number|string} T
 * @typedef {object} Getter
 * @property {string} name
 * @property {string} description Should include possible values if applicable.
 * @property {string} [requirements] Plaintext description of requirements to use this getter.
 * @property {()=>boolean} [enabled] Whether the getter can be used.
 * @property {()=>boolean} [visible] Whether the getter should be shown. Mainly intended for disabled mods.
 * @property {(s: App.RA.Activation.Context) =>T} val
 */

App.RA.Activation.getterManager = (function() {
	class GetterManager {
		constructor() {
			/**
			 * @private
			 * @type {Map<string, Getter<boolean>>}
			 */
			this._booleanGetters = new Map();
			/**
			 * @private
			 * @type {Map<string, Getter<boolean>>}
			 */
			this._assignmentGetters = new Map();
			/**
			 * @private
			 * @type {Map<string, Getter<number>>}
			 */
			this._numberGetters = new Map();
			/**
			 * @private
			 * @type {Map<string, Getter<string>>}
			 */
			this._stringGetters = new Map();
		}

		/**
		 * @param {string} key
		 * @private
		 */
		_validateKey(key) {
			if (!/[a-zA-Z]/.test(key.first())) {
				throw new Error(`Invalid Key: ${key}; The first character of a getter key has to be alphabetic`);
			}
		}

		/**
		 * @param {string} key
		 * @param {Getter<boolean>} getter
		 */
		addBoolean(key, getter) {
			this._validateKey(key);
			this._booleanGetters.set(key, getter);
		}

		/**
		 * @param {string} key
		 * @param {Getter<boolean>} getter
		 */
		addAssignment(key, getter) {
			this._validateKey(key);
			this._assignmentGetters.set(key, getter);
		}

		/**
		 * @param {string} key
		 * @param {Getter<number>} getter
		 */
		addNumber(key, getter) {
			this._validateKey(key);
			this._numberGetters.set(key, getter);
		}

		/**
		 * @param {string} key
		 * @param {Getter<string>} getter
		 */
		addString(key, getter) {
			this._validateKey(key);
			this._stringGetters.set(key, getter);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		has(key) {
			return this._booleanGetters.has(key) || this._assignmentGetters.has(key) ||
				this._numberGetters.has(key) || this._stringGetters.has(key);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		isBoolean(key) {
			return this._booleanGetters.has(key);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		isAssignment(key) {
			return this._assignmentGetters.has(key);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		isNumber(key) {
			return this._numberGetters.has(key);
		}

		/**
		 * @param {string} key
		 * @returns {boolean}
		 */
		isString(key) {
			return this._stringGetters.has(key);
		}

		/**
		 * @returns {ReadonlyMap<string, Getter<boolean>>}
		 */
		get booleanGetters() {
			return this._booleanGetters;
		}

		/**
		 * @returns {ReadonlyMap<string, Getter<boolean>>}
		 */
		get assignmentGetters() {
			return this._assignmentGetters;
		}

		/**
		 * @returns {ReadonlyMap<string, Getter<number>>}
		 */
		get numberGetters() {
			return this._numberGetters;
		}

		/**
		 * @returns {ReadonlyMap<string, Getter<string>>}
		 */
		get stringGetters() {
			return this._stringGetters;
		}

		/**
		 * @returns {string}
		 */
		get booleanDefault() {
			return this._booleanGetters.keys().next().value;
		}

		/**
		 * @returns {string}
		 */
		get assignmentDefault() {
			return this._assignmentGetters.keys().next().value;
		}

		/**
		 * @returns {string}
		 */
		get numberDefault() {
			return this._numberGetters.keys().next().value;
		}

		/**
		 * @returns {string}
		 */
		get stringDefault() {
			return this._stringGetters.keys().next().value;
		}

		/**
		 * @param {App.RA.Activation.Stack} stack
		 * @param {App.RA.Activation.Context} context
		 * @param {string} key
		 * @returns {boolean} True, if a getter exists for the given key
		 */
		read(stack, context, key) {
			let getterB = this._booleanGetters.get(key);
			if (getterB !== undefined) {
				stack.pushBoolean(getterB.val(context));
				return true;
			}
			getterB = this._assignmentGetters.get(key);
			if (getterB !== undefined) {
				stack.pushBoolean(getterB.val(context));
				return true;
			}
			const getterN = this._numberGetters.get(key);
			if (getterN !== undefined) {
				stack.pushNumber(getterN.val(context));
				return true;
			}
			const getterS = this._stringGetters.get(key);
			if (getterS !== undefined) {
				stack.pushString(getterS.val(context));
				return true;
			}
			return false;
		}
	}

	return new GetterManager();
})();

App.RA.Activation.populateGetters = function() {
	const gm = App.RA.Activation.getterManager;
	// Note: The first value of each type being added is taken as the default.

	// Booleans
	gm.addBoolean("isfertile", {
		name: "Is Fertile?", description: "Whether or not the slave is fertile.",
		val: c => isFertile(c.slave)
	});
	gm.addBoolean("isamputee", {
		name: "Is Amputee?", description: "Whether or not the slave has no limbs.",
		val: c => isAmputee(c.slave)
	});
	gm.addBoolean("ispregnant", {
		name: "Is Pregnant?", description: "Whether or not the slave is pregnant.",
		val: c => c.slave.preg > 0
	});
	gm.addBoolean("isslim", {
		name: "Is Slim?", description: "If the slave is considered slim or not by arcology standards.",
		val: c => isSlim(c.slave)
	});
	gm.addBoolean("isstacked", {
		name: "Is Stacked?", description: "If the slave is considered stacked (big T&A) or not.",
		val: c => isStacked(c.slave)
	});
	gm.addBoolean("ismodded", {
		name: "Is Modded?", description: "If the slave is considered heavily modded or not.",
		val: c => SlaveStatsChecker.isModded(c.slave)
	});
	gm.addBoolean("isunmodded", {
		name: "Is Unmodded?", description: "If the slave is (relatively) unmodded.",
		val: c => SlaveStatsChecker.isModded(c.slave)
	});

	// Assignments
	// Penthouse Assignments
	gm.addAssignment("rest", {
		name: "Resting", description: "Resting in the penthouse.",
		val: c => c.slave.assignment === Job.REST
	});
	gm.addAssignment("fucktoy", {
		name: "Fucktoy", description: "Pleasing the master.",
		val: c => c.slave.assignment === Job.FUCKTOY
	});
	gm.addAssignment("classes", {
		name: "Taking classes", description: "Taking classes to better serve.",
		val: c => c.slave.assignment === Job.CLASSES
	});
	gm.addAssignment("house", {
		name: "Cleaning", description: "Cleaning the penthouse.",
		val: c => c.slave.assignment === Job.HOUSE
	});
	gm.addAssignment("whore", {
		name: "Whoring", description: "Whoring themself out.",
		val: c => c.slave.assignment === Job.WHORE
	});
	gm.addAssignment("public", {
		name: "Serving public", description: "Serving the public.",
		val: c => c.slave.assignment === Job.PUBLIC
	});
	gm.addAssignment("subordinate", {
		name: "Subordinate", description: "Subordinate to other slaves.",
		val: c => c.slave.assignment === Job.SUBORDINATE
	});
	gm.addAssignment("milked", {
		name: "Milked", description: "Getting milked.",
		val: c => c.slave.assignment === Job.MILKED
	});
	gm.addAssignment("gloryhole", {
		name: "Glory hole", description: "Working as a glory hole.",
		val: c => c.slave.assignment === Job.GLORYHOLE
	});
	gm.addAssignment("confinement", {
		name: "Confined", description: "Confined at the penthouse.",
		val: c => c.slave.assignment === Job.CONFINEMENT
	});
	gm.addAssignment("choice", {
		name: "Choose own", description: "Allowed to choose their own job.",
		val: c => c.slave.assignment === Job.CHOICE
	});
	// Leadership Assignments
	gm.addAssignment("bodyguard", {
		name: "Bodyguard", description: "Serving as Bodyguard.",
		requirements: "Armory is built.", enabled: ()=>App.Entity.facilities.armory.established,
		val: c => c.slave.assignment === Job.BODYGUARD
	});
	gm.addAssignment("headgirl", {
		name: "Head Girl", description: "Serving as Head Girl",
		val: c => c.slave.assignment === Job.HEADGIRL
	});
	gm.addAssignment("recruiter", {
		name: "Recruiter", description: "Recruiting new slaves.",
		val: c => c.slave.assignment === Job.RECRUITER
	});
	gm.addAssignment("agent", {
		name: "Agent", description: "Serving as an Agent in another arcology.",
		val: c => c.slave.assignment === Job.AGENT
	});
	gm.addAssignment("agentpartner", {
		name: "Agent partner", description: "Serving an agent living in another arcology.",
		val: c => c.slave.assignment === Job.AGENTPARTNER
	});
	// Facility Assignments
	gm.addAssignment("arcade", {
		name: "Confined in arcade", description: "Confined in the arcade.",
		requirements: "Arcade is built.", enabled: ()=>App.Entity.facilities.arcade.established,
		val: c => c.slave.assignment === Job.ARCADE
	});
	gm.addAssignment("madam", {
		name: "Madam", description: "Serving as Madam.",
		requirements: "Brothel is built.", enabled: ()=>App.Entity.facilities.brothel.established,
		val: c => c.slave.assignment === Job.MADAM
	});
	gm.addAssignment("brothel", {
		name: "Brothel whoring?", description: "Working in the brothel.",
		requirements: "Brothel is built.", enabled: ()=>App.Entity.facilities.brothel.established,
		val: c => c.slave.assignment === Job.BROTHEL
	});
	gm.addAssignment("warden", {
		name: "Wardeness", description: "Serving as Wardeness.",
		requirements: "Cellblock is built.", enabled: ()=>App.Entity.facilities.cellblock.established,
		val: c => c.slave.assignment === Job.WARDEN
	});
	gm.addAssignment("cellblock", {
		name: "Confined in cellblock?", description: "Confined in the cellblock.",
		requirements: "Cellblock is built.", enabled: ()=>App.Entity.facilities.cellblock.established,
		val: c => c.slave.assignment === Job.CELLBLOCK
	});
	gm.addAssignment("dj", {
		name: "DJ", description: "Serving as DJ.",
		requirements: "Club is built.", enabled: ()=>App.Entity.facilities.club.established,
		val: c => c.slave.assignment === Job.DJ
	});
	gm.addAssignment("club", {
		name: "Serving club", description: "Serving in the club.",
		requirements: "Club is built.", enabled: ()=>App.Entity.facilities.club.established,
		val: c => c.slave.assignment === Job.CLUB
	});
	gm.addAssignment("nurse", {
		name: "Nurse", description: "Serving as Nurse.",
		requirements: "Clinic is built.", enabled: ()=>App.Entity.facilities.clinic.established,
		val: c => c.slave.assignment === Job.NURSE
	});
	gm.addAssignment("clinic", {
		name: "Getting treatment", description: "Getting treatment in the clinic.",
		requirements: "Clinic is built.", enabled: ()=>App.Entity.facilities.clinic.established,
		val: c => c.slave.assignment === Job.CLINIC
	});
	gm.addAssignment("milkmaid", {
		name: "Milkmaid", description: "Serving as Milkmaid",
		requirements: "Dairy is built.", enabled: ()=>App.Entity.facilities.dairy.established,
		val: c => c.slave.assignment === Job.MILKMAID
	});
	gm.addAssignment("dairy", {
		name: "Work dairy", description: "Working in the dairy",
		requirements: "Dairy is built.", enabled: ()=>App.Entity.facilities.dairy.established,
		val: c => c.slave.assignment === Job.DAIRY
	});
	gm.addAssignment("farmer", {
		name: "Farmer", description: "Serving as Farmer",
		requirements: "Farmyard is built.", enabled: ()=>App.Entity.facilities.farmyard.established,
		val: c => c.slave.assignment === Job.FARMER
	});
	gm.addAssignment("farmyard", {
		name: "Farmhand", description: "Working as a farmhand.",
		requirements: "Farmyard is built.", enabled: ()=>App.Entity.facilities.farmyard.established,
		val: c => c.slave.assignment === Job.FARMYARD
	});
	gm.addAssignment("headgirlsuite", {
		name: "Head Girl Servant", description: "Living with the Head Girl.",
		requirements: "Head Girl Suite is built.", enabled: ()=>App.Entity.facilities.headGirlSuite.established,
		val: c => c.slave.assignment === Job.HEADGIRLSUITE
	});
	gm.addAssignment("concubine", {
		name: "Concubine", description: "Serving as Concubine.",
		requirements: "Master suite is built.", enabled: ()=>App.Entity.facilities.masterSuite.established,
		val: c => c.slave.assignment === Job.CONCUBINE
	});
	gm.addAssignment("mastersuite", {
		name: "Master suite servant", description: "Serving in the master suite.",
		requirements: "Master suite is built.", enabled: ()=>App.Entity.facilities.masterSuite.established,
		val: c => c.slave.assignment === Job.MASTERSUITE
	});
	gm.addAssignment("matron", {
		name: "Matron", description: "Serving as Matron.",
		requirements: "Nursery is built.", enabled: ()=>App.Entity.facilities.nursery.established,
		visible: () => V.experimental.nursery > 0,
		val: c => c.slave.assignment === Job.MATRON
	});
	gm.addAssignment("nursery", {
		name: "Nanny", description: "Working as a nanny.",
		requirements: "Nursery is built.", enabled: ()=>App.Entity.facilities.nursery.established,
		visible: () => V.experimental.nursery > 0,
		val: c => c.slave.assignment === Job.NURSERY
	});
	gm.addAssignment("teacher", {
		name: "Schoolteacher", description: "Serving as Schoolteacher.",
		requirements: "Schoolroom is built.", enabled: ()=>App.Entity.facilities.schoolroom.established,
		val: c => c.slave.assignment === Job.TEACHER
	});
	gm.addAssignment("school", {
		name: "Learning", description: "Learning in the schoolroom.",
		requirements: "Schoolroom is built.", enabled: ()=>App.Entity.facilities.schoolroom.established,
		val: c => c.slave.assignment === Job.SCHOOL
	});
	gm.addAssignment("steward", {
		name: "Stewardess", description: "Serving as Stewardess.",
		requirements: "Servants Quarters are built.",
		enabled: ()=>App.Entity.facilities.servantsQuarters.established,
		val: c => c.slave.assignment === Job.STEWARD
	});
	gm.addAssignment("quarter", {
		name: "Servant", description: "Working as a servant in the Servants Quarters.",
		requirements: "Servants Quarters are built.",
		enabled: ()=>App.Entity.facilities.servantsQuarters.established,
		val: c => c.slave.assignment === Job.QUARTER
	});
	gm.addAssignment("attendant", {
		name: "Attendant", description: "Serving as Attendant.",
		requirements: "Spa is built.", enabled: ()=>App.Entity.facilities.spa.established,
		val: c => c.slave.assignment === Job.ATTENDANT
	});
	gm.addAssignment("spa", {
		name: "Spa resting", description: "Resting in the spa.",
		requirements: "Spa is built.", enabled: () => App.Entity.facilities.spa.established,
		val: c => c.slave.assignment === Job.SPA
	});

	// Numbers
	gm.addNumber("devotion", {
		name: "Devotion",
		description: "Very Hateful: (-∞, -95), Hateful: [-95, -50), Resistant: [-50, -20), Ambivalent: [-20, 20], " +
			"Accepting: (20, 50], Devoted: (50, 95], Worshipful: (95, ∞)",
		val: c => c.slave.devotion
	});
	gm.addNumber("trust", {
		name: "Trust",
		description: "Extremely terrified: (-∞, -95), Terrified: [-95, -50), Frightened: [-50, -20), " +
			"Fearful: [-20, 20], Careful: (20, 50], Trusting: (50, 95], Total trust: (95, ∞)",
		val: c => c.slave.trust
	});
	gm.addNumber("health", {
		name: "Health",
		description: "Death: (-∞, -100), Near Death: [-100, -90), Extremely Unhealthy: [-90, -50), " +
			"Unhealthy: [-50, -20), Healthy: [-20, 20], Very Healthy: (20, 50], Extremely Healthy: (50, 90], " +
			"Unnaturally Healthy: (90, ∞)",
		val: c => c.slave.health.condition
	});
	gm.addNumber("fatigue", {
		name: "Fatigue",
		description: "Energetic: (-∞, 0], Rested: (0, 30], Tired: (30, 60], Fatigued: (60, 90], Exhausted: (90, ∞)",
		val: c => c.slave.health.tired
	});
	gm.addNumber("illness", {
		name: "Illness",
		description: "0: Not ill, 1: A little under the weather, 2: Slightly ill, can be treated at the clinic, " +
			"3: Ill, can be treated at the clinic, 4: Very ill, can be treated at the clinic, " +
			"5: Terribly ill, can be treated at the clinic",
		val: c => c.slave.health.illness
	});
	gm.addNumber("energy", {
		name: "Sex drive",
		description: "Frigid: (-∞, 20], Poor: (20, 40], Average: (40, 60], Powerful: (60, 80], " +
			"Sex Addict: (80, 100), Nympho: 100",
		val: c => c.slave.energy
	});
	gm.addNumber("weight", {
		name: "Weight",
		description: "Emaciated: (-∞, -95), Skinny: [-95, -30), Thin: [-30, -10), Average: [-10, 10], " +
			"Plush: (10, 30], Overweight: (30, 95], Fat: (95, 130], Obese: (130, 160], Super Obese: (160, 190], " +
			"Dangerously Obese: (190, ∞)",
		val: c => c.slave.weight
	});
	gm.addNumber("height", {name: "Height", description: "Slave height in cm.", val: c => c.slave.height});
	gm.addNumber("age", {name: "Age", description: "Real slave age", val: c => c.slave.actualAge});
	gm.addNumber("physicalAge", {
		name: "Body Age", description: "Age of the slave's body.",
		val: c => c.slave.physicalAge
	});
	gm.addNumber("visualAge", {
		name: "Visible Age", description: "How old the slave looks.",
		val: c => c.slave.visualAge
	});
	gm.addNumber("muscles", {
		name: "Muscles",
		description: "Frail: (-∞, -96), Very weak: [-96, -31], Weak: [-31, -6), Soft: [-6, 5), Toned: [5, 30), " +
			"Fit: [30, 50), Muscular: [50, 95), Hugely muscular: [95, ∞)",
		val: c => c.slave.muscles
	});
	gm.addNumber("lactation", {
		name: "Lactation", description: "0: None, 1: Natural, 2: Lactation implant",
		val: c => c.slave.lactation
	});
	gm.addNumber("pregType", {
		name: "Pregnancy Multiples", description: "Fetus count, known only after the 10th week of pregnancy",
		val: c => c.slave.pregType
	});
	gm.addNumber("bellyImplant", {
		name: "Belly Implant", description: "Volume in CCs. None: -1",
		val: c => c.slave.bellyImplant
	});
	gm.addNumber("belly", {
		name: "Belly Size", description: "Volume in CCs, any source",
		val: c => c.slave.belly
	});
	gm.addNumber("intelligenceImplant", {
		name: "Education",
		description: "Education level. 0: uneducated, 15: educated, 30: advanced education, " +
			"(0, 15): incomplete education.",
		val: c => c.slave.intelligenceImplant
	});
	gm.addNumber("intelligence", {
		name: "Intelligence", description: "From moronic to brilliant: [-100, 100]",
		val: c => c.slave.intelligence
	});
	gm.addNumber("accent", {
		name: "Accent", description: "No accent: 0, Nice accent: 1, Bad accent: 2, Can't speak language: 3 and above",
		val: c => c.slave.accent
	});
	gm.addNumber("waist", {
		name: "Waist",
		description: "Masculine waist: (95, ∞), Ugly waist: (40, 95], Unattractive waist: (10, 40], " +
			"Average waist: [-10, 10], Feminine waist: [-40, -10), Wasp waist: [-95, -40), Absurdly narrow: (-∞, -95)",
		val: c => c.slave.waist
	});
	gm.addNumber("chem", {
		name: "Carcinogen Buildup",
		description: "Side effects from drug use. If greater than 10 will have negative consequences.",
		val: c => c.slave.chem
	});
	gm.addNumber("boobs", {
		name: "Breasts",
		description: "0-299: Flat, 300-399: A-cup, 400-499: B-cup, 500-649: C-cup, 650-799: D-cup, 00-999: DD-cup, " +
			"1000-1199: F-cup, 1200-1399: G-cup, 1400-1599: H-cup, 1600-1799: I-cup, 1800-2049: J-cup, " +
			"2050-2299: K-cup, 2300-2599: L-cup, 2600-2899: M-cup, 2900-3249: N-cup, 3250-3599: O-cup, " +
			"3600-3949: P-cup, 3950-4299: Q-cup, 4300-4699: R-cup, 4700-5099: S-cup, 5100-10499: Massive",
		val: c => c.slave.boobs
	});
	gm.addNumber("dick", {
		name: "Dick",
		description: "None: 0, Tiny: 1, Little: 2, Normal: 3, Big: 4, Huge: 5, Gigantic: 6, Massive: 7, Titanic: 8, " +
			"Monstrous: 9, Inhuman: 10, Hypertrophied: 11+",
		val: c => c.slave.dick
	});
	gm.addNumber("balls", {
		name: "Balls",
		description: "None: 0, Vestigial: 1, Small: 2, Average: 3, Large: 4, Massive: 5, Huge: 6, Gigantic: 7, " +
			"Enormous: 8, Monstrous: 9, Hypertrophied: 11+",
		val: c => c.slave.balls
	});
	gm.addNumber("lips", {
		name: "Lips",
		description: "Thin: (-∞, 10), Normal: [10, 20), Pretty: [20, 40), Plush: [40, 70), Huge (lisping): [70, 95), " +
			"Facepussy (mute): [95, ∞)",
		val: c => c.slave.lips
	});
	gm.addNumber("butt", {
		name: "Butt",
		description: "Flat: 0, Less flat: 1, Small: 2, Big: 3, Large: 4, Huge: 5, Enormous: 6, Gigantic: 7, " +
			"Ridiculous: 8, Immense: 9-10, Inhuman: 11-20",
		val: c => c.slave.butt
	});
	gm.addNumber("hips", {
		name: "Hips",
		description: "Very narrow: -2, Narrow: -1, Normal: 0, Wide hips: 1, Very wide hips: 2, Inhumanly wide hips: 3",
		val: c => c.slave.hips
	});

	// Strings
	gm.addString("label", {name: "Label", description: "Assigned Label", val: c => c.slave.custom.label});
	gm.addString("genes", {
		name: "Sex", description: "Genetic sex: Male: XX, Female: XY",
		val: c => c.slave.genes
	});
	gm.addString("fetish", {
		name: "Fetish",
		description: "One of 'buttslut', 'cumslut', 'masochist', 'sadist', 'dom', 'submissive', 'boobs', " +
			"'pregnancy', 'none' (AKA vanilla)",
		val: c => c.slave.fetish
	});
};

/**
 * @param {App.Entity.SlaveState} slave
 * @param {FC.RA.PostFixRule} rule
 * @returns {boolean} If the rule should be applied to the given slave
 */
App.RA.Activation.evaluate = function(slave, rule) {
	const gm = App.RA.Activation.getterManager;
	const context = new App.RA.Activation.Context(slave);
	const stack = new App.RA.Activation.Stack();

	/**
	 * @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", () => {
			const length = stack.popNumber();
			let value = 1;
			for (let i = 0; i < length; i++) {
				value &= stack.popAsBoolean();
			}
			stack.pushNumber(value);
		}],
		["or", () => {
			const length = stack.popNumber();
			let value = 0;
			for (let i = 0; i < length; i++) {
				value |= stack.popAsBoolean();
			}
			stack.pushNumber(value);
		}],
		["add", () => {
			const length = stack.popNumber();
			let value = 0;
			for (let i = 0; i < length; i++) {
				value += stack.popNumber();
			}
			stack.pushNumber(value);
		}],
		["mul", () => {
			const length = stack.popNumber();
			let value = 1;
			for (let i = 0; i < length; i++) {
				value *= stack.popNumber();
			}
			stack.pushNumber(value);
		}],
		["max", () => {
			const length = stack.popNumber();
			let value = stack.popNumber();
			for (let i = 1; i < length; i++) {
				value = Math.max(value, stack.popNumber());
			}
			stack.pushNumber(value);
		}],
		["min", () => {
			const length = stack.popNumber();
			let value = stack.popNumber();
			for (let i = 1; i < length; i++) {
				value = Math.min(value, stack.popNumber());
			}
			stack.pushNumber(value);
		}],
		["sub", () => {
			const subtract = stack.popNumber();
			stack.pushNumber(stack.popNumber() - subtract);
		}],
		["div", () => {
			const divisor = stack.popNumber();
			stack.pushNumber(stack.popNumber() / divisor);
		}],
		["eqstr", () => stack.pushBoolean(stack.popString() === stack.popString())],
		["neqstr", () => stack.pushBoolean(stack.popString() !== stack.popString())],
		["eqnum", () => stack.pushBoolean(stack.popNumber() === stack.popNumber())],
		["neqnum", () => stack.pushBoolean(stack.popNumber() === stack.popNumber())],
		["gt", () => stack.pushBoolean(stack.popNumber() < stack.popNumber())],
		["gte", () => stack.pushBoolean(stack.popNumber() <= stack.popNumber())],
		["lt", () => stack.pushBoolean(stack.popNumber() > stack.popNumber())],
		["lte", () => stack.pushBoolean(stack.popNumber() >= stack.popNumber())],
		["substr", () => {
			const value = stack.popString();
			stack.pushBoolean(stack.popString().includes(value));
		}],
		["not", () => stack.pushBoolean(stack.popNumber() === 0)],
		["ternarystr", () => {
			const ifFalse = stack.popString();
			const ifTrue = stack.popString();
			stack.pushString(stack.popNumber() ? ifTrue : ifFalse);
		}],
		["ternarynum", () => {
			const ifFalse = stack.popNumber();
			const ifTrue = stack.popNumber();
			stack.pushNumber(stack.popNumber() ? ifTrue : ifFalse);
		}],
	]);

	/**
	 * Custom getters start with "?" and then "b", "n" or "s" depending on return type
	 * @param {string} rulePart
	 */
	function evalCustom(rulePart) {
		const expectedType = rulePart.charAt(1) === "b" ? "boolean"
			: rulePart.charAt(1) === "n" ? "number"
				: "string";
		try {
			// TODO: This should use a cached Function instead of 'eval'ing
			const value = eval(rulePart.slice(2))(context);
			if (expectedType === "boolean") {
				stack.pushNumber(value ? 1 : 0);
			} else if (expectedType === "number") {
				stack.pushNumber(value);
			} else {
				stack.pushString(value);
			}
		} catch (e) {
			throw new Error(`Custom condition '${rulePart.slice(2)}' failed: '${e.message}'`);
		}
	}

	for (const rulePart of rule) {
		if (typeof rulePart === "string") {
			const operation = operators.get(rulePart);
			if (operation !== undefined) {
				operation();
			} else {
				const result = gm.read(stack, context, rulePart);
				if (!result) {
					if (rulePart.startsWith("?")) {
						evalCustom(rulePart);
					} else {
						stack.pushString(rulePart.slice(1));
					}
				}
			}
		} else if (typeof rulePart === "number") {
			stack.pushNumber(rulePart);
		} else {
			stack.pushBoolean(rulePart);
		}
	}
	return !!stack.popNumber();
};


code:
/* eslint no-unused-vars: [2, { "vars": "local" }] */

/* This file contains only JS functions without dependencies on FC specific variables/conventions and do not rely on
 * custom functions outside this file
 */

/**
 * Returns whether x is defined. Port of SugarCube's def.
 * @param {any} x
 * @returns {boolean}
 */
function jsDef(x) {
	return !(typeof x === "undefined" || x === null || x === undefined);
}

/**
 * @template {PropertyKey} T
 * @param {Object.<T, number>} obj
 * @returns {T}
 */
function hashChoice(obj) {
	let randint = Math.floor(Math.random() * hashSum(obj));
	let ret;
	Object.keys(obj).some((key) => {
		if (randint < obj[key]) {
			ret = key;
			return true;
		} else {
			randint -= obj[key];
			return false;
		}
	});
	return ret;
}

/**
 * @param {Object.<PropertyKey, number>} obj
 * @returns {number}
 */
function hashSum(obj) {
	let sum = 0;
	Object.keys(obj).forEach((key) => {
		sum += obj[key];
	});
	return sum;
}

/**
 * @template {PropertyKey} T
 * @param {T[]} arr
 * @returns {Object.<T, number>}
 */
function arr2obj(arr) {
	/** @type {Object.<T, number>} */
	const obj = {};
	arr.forEach((item) => {
		obj[item] = 1;
	});
	return obj;
}

/**
 * @param {object} object
 * @param {PropertyKey[]} rest
 */
function hashPush(object, ...rest) {
	rest.forEach((item) => {
		if (object[item] === undefined) {
			object[item] = 1;
		} else {
			object[item] += 1;
		}
	});
}

/**
 * @param {Object.<PropertyKey, number>} obj
 * @param {Object.<PropertyKey, number>} other
 */
function hashMerge(obj, other) {
	Object.keys(other).forEach((key) => {
		if (obj[key] === undefined) {
			obj[key] = other[key];
		} else {
			obj[key] += other[key];
		}
	});
}

/**
 * @template {PropertyKey} T
 * @param {T[]} array
 * @returns {Object.<T, number>}
 */
function weightedArray2HashMap(array) {
	/** @type {Object.<T, number>} */
	const obj = {};
	array.forEach((item) => {
		if (obj[item] === undefined) {
			obj[item] = 1;
		} else {
			obj[item] += 1;
		}
	});
	return obj;
}

/**
 * generate a random, almost unique ID that is compliant (possibly) with RFC 4122
 *
 * @returns {string}
 */
function generateNewID() {
	let date = Date.now(); // high-precision timer
	let uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
		let r = (date + Math.random() * 16) % 16 | 0;
		date = Math.floor(date / 16);
		return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
	});
	return uuid;
}

/**
 * Swaps two values in an array.
 * @param {Array} array
 * @param {number} indexA
 * @param {number} indexB
 */
function arraySwap(array, indexA, indexB) {
	const tmp = array[indexA];
	array[indexA] = array[indexB];
	array[indexB] = tmp;
}

/**
 * @param {string} string
 * @returns {string}
 */
function capFirstChar(string) {
	return string.charAt(0).toUpperCase() + string.substr(1);
}

/**
 * @param {string} string
 * @returns {string}
 */
function uncapFirstChar(string) {
	return string.charAt(0).toLowerCase() + string.substr(1);
}

/**
 * @param {string} word
 * @returns {string}
 */
function addA(word) {
	let vocal = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
	if (vocal.includes(word.charAt(0))) {
		return `an ${word}`;
	}
	return `a ${word}`;
}

/**
 * Takes a number and appends the appropriate suffix.
 * Example: `ordinalSuffix(1)` gives `1st`.
 * @param {number} i
 * @returns {string}
 */
function ordinalSuffix(i) {
	let j = i % 10;
	let k = i % 100;
	if (j === 1 && k !== 11) {
		return `${i}st`;
	}
	if (j === 2 && k !== 12) {
		return `${i}nd`;
	}
	if (j === 3 && k !== 13) {
		return `${i}rd`;
	}
	return `${i}th`;
}

/**
 * @param {number} i
 * @returns {string}
 * Takes a number and returns the appropriate ordinal.
 * Example: `ordinalSuffix(1)` gives `first`.
 * For number greater than 19 identical to `ordinalSuffix(i)`
 */
function ordinalSuffixWords(i) {
	const text = ["zeroth", "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth"];
	if (i < text.length) {
		return text[i];
	}
	return ordinalSuffix(i);
}

/**
 * Takes an array and returns a new array without duplicate entries.
 * @template T
 * @param {Iterable<T>} array
 * @returns {T[]}
 */
function removeDuplicates(array) {
	return [...new Set(array)];
}

/**
 * Maps an index from one list onto a matching index on the other.
 * The first and last indexes will be matched to the first and last indexes of the other list,
 * while indexes in between will go to the nearest index.
 * @param {number} index The index in original list to map to new list.
 * @param {*} originalList The original list the index refers into.
 * @param {*} newList The new list which we want an index for
 * @returns {number} The new index into newList
 */
App.Utils.mapIndexBetweenLists = function(index, originalList, newList) {
	if (index === 0) { return 0; }
	if (index === originalList.length - 1) { return newList.length - 1; }
	index--;
	const originalLimitedLength = originalList.length - 2;
	const newLimitedLength = newList.length - 2;
	return Math.round((index / originalLimitedLength) * newLimitedLength) + 1;
};

/**
 * replaces special HTML characters with their '&xxx' forms
 * @param {string} text
 * @returns {string}
 */
App.Utils.escapeHtml = function(text) {
	const map = {
		'&': '&',
		'<': '<',
		'>': '>',
		'"': '"',
		"'": '''
	};
	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.
 *
 * @param {Iterable} list
 * @returns {{}}
 */
function mapIdList(list) {
	let mappedList = {};
	for (const item of list) {
		mappedList[item.id] = item;
	}
	return mappedList;
}

/**
 * Topological sorting algorithm
 * https://gist.github.com/shinout/1232505
 * Added keys parameter since it better suits our needs and updated to project code style.
 *
 * @template {PropertyKey} T
 * @param {T[]} keys
 * @param {T[][]} edges list of edges. each edge forms Array<ID,ID> e.g. [12, 3]
 * @returns {T[]} Array: topological sorted list of IDs
 * @throws Error: in case there is a closed chain.
 **/
App.Utils.topologicalSort = function(keys, edges) {
	class Node {
		/** @param {T} id */
		constructor(id) {
			this.id = id;
			/** @type {T[]} */
			this.afters = [];
		}
	}

	/** @type {Object.<T, Node>} */
	let nodes = {}; // hash: stringified id of the node => { id: id, afters: list of ids }
	let sorted = []; // sorted list of IDs ( returned value )
	/** @type {Object.<T, boolean>} */
	let visited = {}; // hash: id of already visited node => true

	// 1. build data structures
	keys.forEach(key => {
		nodes[key] = new Node(key);
	});

	edges.forEach(edge => {
		const from = edge[0];
		const to = edge[1];
		if (!nodes[from]) { nodes[from] = new Node(from); }
		if (!nodes[to]) { nodes[to] = new Node(to); }
		nodes[from].afters.push(to);
	});

	// 2. topological sort
	Object.keys(nodes).forEach(function visit(idstr, ancestors) {
		let node = nodes[idstr];
		let id = node.id;

		// if already exists, do nothing
		if (visited[idstr]) { return; }

		if (!Array.isArray(ancestors)) { ancestors = []; }

		ancestors.push(id);

		visited[idstr] = true;

		node.afters.forEach((afterID) => {
			if (ancestors.indexOf(afterID) >= 0) { // if already in ancestors, a closed chain exists.
				throw new Error('closed chain : ' + afterID + ' is in ' + id);
			}

			visit(afterID.toString(), ancestors.map(v => v)); // recursive call
		});

		sorted.unshift(id);
	});

	return sorted;
};

/**
 * Sorts an array of objects based on the order of the first array.
 * Sorts by values accessible by unsorted[key]
 * Values of the second array must be a subset of sorted
 *
 * @param {Array<any>} sorted
 * @param {Array<object>} unsorted
 * @param {string} key
 * @returns {Array<object>}
 */
function sortArrayByArray(sorted, unsorted, key) {
	const map = {};
	sorted.forEach((value, index) => {
		map[value] = index;
	});

	return unsorted.sort((a, b) => {
		return map[a[key]] - map[b[key]];
	});
}

/**
 * @param {object} target
 * @param {object} source
 */
function deepAssign(target, source) {
	function isObject(o) {
		return (o !== undefined && o !== null && typeof o === 'object' && !Array.isArray(o));
	}

	if (isObject(target) && isObject(source)) {
		for (const key in source) {
			if (!source.hasOwnProperty(key)) { continue; }
			if (isObject(source[key])) {
				if (!target.hasOwnProperty(key) || !isObject(target[key])) {
					target[key] = {};
				}
				deepAssign(target[key], source[key]);
			} else {
				Object.assign(target, {
					[key]: source[key]
				});
			}
		}
	}
}

/**
 * Returns the median value for an array
 *
 * For more information about mean vs. median see
 * https://www.clinfo.eu/mean-median/
 * @param {number[]} arr Does not need to be sorted
 * @returns {number}
 */
function median(arr = []) {
	const mid = Math.floor(arr.length / 2);
	const nums = [...arr].sort((a, b) => a - b);

	return arr.length % 2 === 0 ? (nums[mid] + nums[mid - 1]) / 2 : nums[mid];
}

/**
 * Returns value for point x, computed assuming linear function defined
 * via two points (x0,y0) and (x1,y1)
 * @param {number} x
 * @param {number} x0
 * @param {number} y0
 * @param {number} x1
 * @param {number} y1
 * @returns {number}
 */
function linearInterpolation(x, x0, y0, x1, y1) {
	return y0 + (x - x0) * ((y1 - y0) / (x1 - x0));
}


free-cities

description: The Free Cities game

type: git

lint: eslint src/**/*.js

compile: sh compile.sh


code:
There are actually two places to add art for FreeCities.  The folder you are currently in, ./resources, is mostly for the git.  Once you compile an HTML, the HTML won't check here for art.

If you are interested in adding the old renders or your own custom art to your local version of FreeCities, then you actually want to have a resources folder in the same folder FC_pregmod.html is in.  By default, that will be ./bin/resources.  If you want any of the art in this folder, make sure to copy it over there.

Two different resources folders, and both connected to art.  But they work very differently.

P.S. make sure you don't stack resources folders, like resources/resources.  That would just be silly.


path: resources/vector/body/addon/boob 0 areola piercing.svg


path: resources/vector/body/addon/boob 0 piercing heavy.svg


path: resources/vector/body/addon/boob 1 areola piercing.svg


path: resources/vector/body/addon/boob 1 piercing heavy.svg


path: resources/vector/body/addon/boob 2 areola piercing.svg


path: resources/vector/body/addon/boob 2 piercing heavy.svg


path: resources/vector/body/addon/boob 3 areola piercing.svg


path: resources/vector/body/addon/boob 3 piercing heavy.svg


path: resources/vector/body/addon/boob 4 areola piercing.svg


path: resources/vector/body/addon/boob 4 piercing heavy.svg


path: resources/vector/body/addon/boob 5 areola piercing.svg


path: resources/vector/body/addon/boob 5 piercing heavy.svg


path: resources/vector/body/addon/boob 6 areola piercing.svg


path: resources/vector/body/addon/boob 6 piercing heavy.svg


path: resources/vector/body/addon/boob 7 areola piercing.svg


path: resources/vector/body/addon/boob 7 piercing heavy.svg


code:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
	 viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
	.st0{fill:#787878;}
</style>
<g id="Clit_Piercing">
	<circle id="XMLID_257_" class="st0" cx="488.3" cy="435.6" r="1.2"/>
	<circle id="XMLID_256_" class="st0" cx="483.6" cy="436.1" r="1.2"/>
</g>
</svg>


path: resources/vector/body/addon/hair aft neat auburn.svg


path: resources/vector/body/addon/hair aft neat black.svg


path: resources/vector/body/addon/hair aft neat blonde.svg


path: resources/vector/body/addon/hair aft neat blue.svg


path: resources/vector/body/addon/hair aft neat brown.svg


path: resources/vector/body/addon/hair aft neat burgundy.svg


path: resources/vector/body/addon/hair aft neat chestnut.svg


path: resources/vector/body/addon/hair aft neat chocolate.svg


path: resources/vector/body/addon/hair aft neat copper.svg


path: resources/vector/body/addon/hair aft neat dark brown.svg


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="hair aft neat darkbrown.svg"
   inkscape:version="0.92.1 r15371"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1920"
	 inkscape:window-height="1001"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="0.236"
	 inkscape:cx="-1508.4746"
	 inkscape:cy="500"
	 inkscape:window-x="-9"
	 inkscape:window-y="-9"
	 inkscape:window-maximized="1"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#3F4040;}
</style><path
	 id="Hair_Aft_7_"
	 class="st0"
	 d="m 568.83729,121.38644 c 13.5,72.4 34.5,66.5 14,103.3 -43.3,47.7 -90.1,7 -98.1,-9.8 -41.1,-23.4 -0.3,-140.799999 48.3,-130.299999 33.3,7.2 33.3,19.499999 35.8,36.799999"
	 inkscape:connector-curvature="0"
	 style="fill:#463325;fill-opacity:1;" /></svg>

path: resources/vector/body/addon/hair aft neat darkbrown.svg


path: resources/vector/body/addon/hair aft neat dirty blonde.svg


path: resources/vector/body/addon/hair aft neat ginger.svg


path: resources/vector/body/addon/hair aft neat golden.svg


path: resources/vector/body/addon/hair aft neat green.svg


path: resources/vector/body/addon/hair aft neat grey.svg


path: resources/vector/body/addon/hair aft neat hazel.svg


path: resources/vector/body/addon/hair aft neat pink.svg


path: resources/vector/body/addon/hair aft neat platinum blonde.svg