diff --git a/css/general/layout.css b/css/general/layout.css
index bb4e6b735f624bbd8da265a606f032626f19f57e..877cf5d1015c1a6a5190e8868800b94b8ac93e54 100644
--- a/css/general/layout.css
+++ b/css/general/layout.css
@@ -127,3 +127,8 @@ div.cheat-menu {
 	position: absolute;
 	right: 50px;
 }
+
+input[type="text"].number {
+	min-width: 6em;
+	max-width: 6em;
+}
diff --git a/css/rulesAssistant/conditionEditor.css b/css/rulesAssistant/conditionEditor.css
new file mode 100644
index 0000000000000000000000000000000000000000..3ab3b7cfc3b11ec8b829111b326d7dfa61541346
--- /dev/null
+++ b/css/rulesAssistant/conditionEditor.css
@@ -0,0 +1,116 @@
+@media only screen and (min-width: 1000px) {
+    .rule-builder {
+        display: grid;
+        grid-template-columns: 60% 40%;
+        grid-column-gap: 1em;
+    }
+}
+
+.rule-builder button {
+    color: var(--link-color);
+    background-color: var(--button-color);
+    border: solid 2px var(--button-border-color);
+    border-radius: 4px;
+    margin-left: 4px;
+}
+
+.rule-builder button:hover {
+    background-color: var(--button-hover-color);
+    color: var(--link-hover-color);
+}
+
+.rule-part-browser {
+}
+
+.rule-part {
+    display: inline flow-root;
+    border-radius: 8px;
+    border: 2px solid #333333;
+    background-color: #1a1a1a;
+    padding: 4px;
+    margin: 4px;
+}
+
+.validation-error::before {
+    font-family: "tme-fa-icons";
+    content: "\e80d";
+    color: red;
+    margin-right: 4px;
+}
+
+.rule-draggable {
+    cursor: grab;
+}
+
+.rule-drag-element {
+    display: inline flow-root;
+    background-image: repeating-linear-gradient(0, #1a1a1a, transparent 0.2em, transparent 0.2em, #1a1a1a 0.4em),
+    repeating-linear-gradient(90deg, #1a1a1a, #777 0.2em, #777 0.2em, #1a1a1a 0.4em);
+    width: 1.6em;
+    height: 2em;
+    vertical-align: middle;
+}
+
+.rule-drop-location {
+    border: #999 dashed 1px;
+    border-radius: 4px;
+    display: inline flow-root;
+    background-color: #555;
+    width: 5em;
+    height: 1.5em;
+    vertical-align: middle;
+    margin: 0 4px
+}
+
+
+.rule-builder input[type="text"] {
+    margin-left: 0.2em;
+}
+
+.rule-right-margin {
+    margin-right: 0.2em;
+}
+
+.rule-left-margin {
+    margin-left: 0.2em;
+}
+
+.rule-trash {
+    border: #F55 1px;
+    border-radius: 4px;
+    background-color: palevioletred;
+    width: 12em;
+    height: 2em;
+    text-align: center;
+    margin: 4px;
+    color: black;
+}
+
+.rule-trash::before {
+    font-family: "tme-fa-icons";
+    content: "\e828";
+}
+
+/* Encyclopedia help entry */
+
+.rule-help-table {
+    display: grid;
+    grid-template-columns: max-content auto auto;
+}
+
+.rule-help-table .head {
+    font-weight: bold;
+    position: sticky;
+    top: -1em;
+    background-color: #111;
+}
+
+.rule-help-table > div {
+    padding: 0 0.5em
+}
+
+.rule-help-table > :nth-child(6n+4),
+.rule-help-table > :nth-child(6n+5),
+.rule-help-table > :nth-child(6n+6) {
+    background-color: #222;
+}
diff --git a/devTools/types/FC/RA.d.ts b/devTools/types/FC/RA.d.ts
index a771bac04b26912b7ce2018e7672581060b64bab..d9c75fff29ffea78e0b574b757ee501e71f33834 100644
--- a/devTools/types/FC/RA.d.ts
+++ b/devTools/types/FC/RA.d.ts
@@ -9,9 +9,7 @@ declare namespace FC {
 		type ExpressiveNumericTarget = GenericNumericTarget<number | string>;
 
 		interface RuleConditions {
-			function: boolean | string;
-			data: any;
-			assignment: Assignment[];
+			activation: PostFixRule;
 			selectedSlaves: number[];
 			excludedSlaves: number[];
 			applyRuleOnce: boolean;
@@ -175,7 +173,7 @@ declare namespace FC {
 			scarDesign: string;
 			hornColor: string;
 			labelTagsClear: boolean;
-			choosesOwnClothes: 0|1;
+			choosesOwnClothes: 0 | 1;
 			pronoun: number;
 		}
 
@@ -185,5 +183,7 @@ declare namespace FC {
 			condition: RuleConditions;
 			set: RuleSetters;
 		}
+
+		type PostFixRule = Array<string | number | boolean>
 	}
 }
diff --git a/js/002-config/fc-js-init.js b/js/002-config/fc-js-init.js
index 19f9622b9982a3b0ca38ffcad1a9503e0b88e761..07aae1b15b699bd263856df1ab215e35658b660a 100644
--- a/js/002-config/fc-js-init.js
+++ b/js/002-config/fc-js-init.js
@@ -68,6 +68,7 @@ App.Medicine.Surgery = {};
 App.Medicine.Surgery.Procedures = {};
 App.Medicine.Surgery.Reactions = {};
 App.RA = {};
+App.RA.Activation = {};
 App.Ratings = {};
 App.SF = {};
 App.SecExp = {};
diff --git a/js/rulesAssistant/01-stack.js b/js/rulesAssistant/01-stack.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0924afc86c6eac6a45b5686edfea7041d96ad86
--- /dev/null
+++ b/js/rulesAssistant/01-stack.js
@@ -0,0 +1,56 @@
+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();
+	}
+};
diff --git a/js/rulesAssistant/conditionEditor.js b/js/rulesAssistant/conditionEditor.js
new file mode 100644
index 0000000000000000000000000000000000000000..580105e2bd20c1e4fe761662c93f3f543b2c28e8
--- /dev/null
+++ b/js/rulesAssistant/conditionEditor.js
@@ -0,0 +1,1336 @@
+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) {
+		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);
+			div.append(makeTextBoxDragSafe(App.UI.DOM.makeTextBox(this.check.slice(1), (v) => {
+				this.check = this._expectedType.first() + v;
+				refreshEditor();
+			}), 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} 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,
+	};
+})();
diff --git a/js/rulesAssistant/conditionEvaluation.js b/js/rulesAssistant/conditionEvaluation.js
new file mode 100644
index 0000000000000000000000000000000000000000..938f17432209674528971a86f70e66dcb6874489
--- /dev/null
+++ b/js/rulesAssistant/conditionEvaluation.js
@@ -0,0 +1,752 @@
+/* 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 = 1;
+			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();
+};
diff --git a/src/002-config/fc-version.js b/src/002-config/fc-version.js
index b4cf149a1bf198d68d6b525754d638288db47950..8081e88f01bad0ca123c8dc1334131837a361f40 100644
--- a/src/002-config/fc-version.js
+++ b/src/002-config/fc-version.js
@@ -2,5 +2,5 @@ App.Version = {
 	base: "0.10.7.1", // The vanilla version the mod is based off of, this should never be changed.
 	pmod: "4.0.0-alpha.12",
 	commitHash: null,
-	release: 1157, // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js.
+	release: 1158, // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js.
 };
diff --git a/src/data/backwardsCompatibility/datatypeCleanup.js b/src/data/backwardsCompatibility/datatypeCleanup.js
index 0afc936fb02554674492c2c51968b5b924dfa709..b6d9489e6973cd518550456ab26c3aba242d0d59 100644
--- a/src/data/backwardsCompatibility/datatypeCleanup.js
+++ b/src/data/backwardsCompatibility/datatypeCleanup.js
@@ -2142,6 +2142,168 @@ App.Entity.Utils.RARuleDatatypeCleanup = function() {
 			}
 			delete cond.specialSlaves;
 		}
+
+		if (cond.function !== undefined) {
+			try {
+				if (typeof cond.function === "boolean") {
+					cond.activation = [cond.function, 1, "and"];
+				} else if (cond.function === "custom") {
+					cond.activation = ["?bc=>" + cond.data + "(c.slave)", 1, "and"];
+				} else if (cond.function === "belongs") {
+					switch (cond.data.attribute) {
+						case "amp":
+							cond.activation = cond.data.value[0] === 1
+								? ["visamputee", 1, "and"]
+								: ["visamputee", "not", 1, "and"];
+							break;
+						case "genes":
+							cond.activation = ["vgenes", cond.data.value[0], "eqstr", 1, "and"];
+							break;
+						case "fetish":
+							cond.activation = [];
+							// eslint-disable-next-line no-case-declarations
+							let count = 0;
+							for (const fetish of cond.data.value) {
+								count++;
+								cond.activation.push("vfetish", "!" + fetish, "eqstr");
+							}
+							cond.activation.push(count, "or", 1, "and");
+					}
+				} else if (cond.function === "between") {
+					let count = convertBetween();
+					if (count === 0) {
+						count++;
+						cond.activation = [false];
+						console.log("no match", JSON.parse(JSON.stringify(cond)));
+					}
+					cond.activation.push(count, "and");
+				}
+
+				if (!App.RA.Activation.Editor.validateRule(cond.activation)) {
+					cond.activation = [false, 1, "and"];
+				}
+			} catch (e) {
+				console.log("condition broke", e.message, JSON.parse(JSON.stringify(cond)));
+				cond.activation = [false, 1, "and"];
+			} finally {
+				delete cond.function;
+				delete cond.data;
+			}
+
+			// assignments
+			try {
+				if (cond.assignment.length > 0) {
+					const rule = [];
+					for (const assignment of cond.assignment) {
+						rule.push(assigmentToKey(assignment));
+					}
+					rule.push(rule.length, "or");
+
+					if (!App.RA.Activation.Editor.validateRule(rule)) {
+						rule = [false, 1, "or"];
+					}
+
+					cond.activation.pop();
+					const length = cond.activation.pop();
+					cond.activation.push(...rule);
+					cond.activation.push(length + 1, "and");
+					if (!App.RA.Activation.Editor.validateRule(cond.activation)) {
+						cond.activation = [false, 1, "and"];
+					}
+				}
+			} catch (e) {
+				console.log("assignments broke", e.message, JSON.parse(JSON.stringify(cond)));
+			} finally {
+				delete cond.assignment;
+			}
+		}
+
+		function convertBetween() {
+			//switch (cond.data.attribute) {
+			const values = {
+				"devotion": "vdevotion",
+				"trust": "vtrust",
+				"health.condition": "vhealth",
+				"health.tired": "vfatigue",
+				"energy": "venergy",
+				"height": "vheight",
+				"weight": "vweight",
+				"actualAge": "vage",
+				"physicalAge": "vphysicalAge",
+				"visualAge": "vvisualAge",
+				"muscles": "vmuscles",
+				"pregType": "vpregType",
+				"bellyImplant": "vbellyImplant",
+				"belly": "vbelly",
+				"intelligenceImplant": "vintelligenceImplant",
+				"intelligence": "vintelligence",
+				"accent": "vaccent",
+				"waist": "vwaist",
+				"chem": "vchem",
+				"lactation": "vlactation",
+			};
+			if (values.hasOwnProperty(cond.data.attribute)) {
+				return addBetween(values[cond.data.attribute]);
+			}
+			return 0;
+		}
+
+		function addBetween(key) {
+			let count = 0;
+			if (cond.data.value[0] !== null) {
+				cond.activation.push(key, cond.data.value[0], "gte");
+				count++;
+			}
+			if (cond.data.value[1] !== null) {
+				cond.activation.push(key, cond.data.value[1], "lte");
+				count++;
+			}
+			return count;
+		}
+
+		function assigmentToKey(assignment) {
+			const jobs = {
+				[Job.REST]: "vrest",
+				[Job.CHOICE]: "vchoice",
+				[Job.FUCKTOY]: "vfucktoy",
+				[Job.CLASSES]: "vclasses",
+				[Job.HOUSE]: "vhouse",
+				[Job.WHORE]: "vwhore",
+				[Job.PUBLIC]: "vpublic",
+				[Job.SUBORDINATE]: "vsubordinate",
+				[Job.MILKED]: "vmilked",
+				[Job.GLORYHOLE]: "vgloryhole",
+				[Job.CONFINEMENT]: "vconfinement",
+				[Job.BODYGUARD]: "vbodyguard",
+				[Job.RECRUITER]: "vrecruiter",
+				[Job.HEADGIRL]: "vheadgirl",
+				[Job.ARCADE]: "varcade",
+				[Job.MADAM]: "vmadam",
+				[Job.BROTHEL]: "vbrothel",
+				[Job.WARDEN]: "vwarden",
+				[Job.CELLBLOCK]: "vcellblock",
+				[Job.DJ]: "vdj",
+				[Job.CLUB]: "vclub",
+				[Job.NURSE]: "vnurse",
+				[Job.CLINIC]: "vclinic",
+				[Job.MILKMAID]: "vmilkmaid",
+				[Job.DAIRY]: "vdairy",
+				[Job.FARMER]: "vfarmer",
+				[Job.FARMYARD]: "vfarmyard",
+				[Job.HEADGIRLSUITE]: "vheadgirlsuite",
+				[Job.CONCUBINE]: "vconcubine",
+				[Job.MASTERSUITE]: "vmastersuite",
+				[Job.MATRON]: "vmatron",
+				[Job.NURSERY]: "vnursery",
+				[Job.TEACHER]: "vteacher",
+				[Job.SCHOOL]: "vschool",
+				[Job.STEWARD]: "vsteward",
+				[Job.QUARTER]: "vquarter",
+				[Job.ATTENDANT]: "vattendant",
+				[Job.SPA]: "vspa"
+			};
+			return jobs[assignment];
+		}
 	}
 
 	/** @param {object} o */
diff --git a/src/gui/Encyclopedia/encyclopediaBeingInCharge.js b/src/gui/Encyclopedia/encyclopediaBeingInCharge.js
index a22593a9a1b6e145928a91cdcc08cf455727bfee..091847ce13606e00fc198badf89776dcebff09d1 100644
--- a/src/gui/Encyclopedia/encyclopediaBeingInCharge.js
+++ b/src/gui/Encyclopedia/encyclopediaBeingInCharge.js
@@ -121,19 +121,10 @@ App.Encyclopedia.addArticle("Rules Assistant", function() {
 	App.Events.addParagraph(f, r);
 
 	r = [];
-	r.push(App.Encyclopedia.topic("Rule activation"));
-	r.push("In order to apply a rule to slaves, the activation will need to be set. Choose an activation type");
-	r.push(App.UI.DOM.combineNodes("(",
-		App.UI.DOM.makeElement("span", App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("devotion", "From Rebellious to Devoted"), ","), ["devotion", "accept"])));
-	r.push(App.UI.DOM.makeElement("span", App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("trust", "Trust"), ","), ["trust", "careful"]));
-	r.push("sex drive,");
-	r.push(App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("health", "Health"), ","));
-	r.push(App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("weight", "Weight"), ","));
-	r.push(App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("muscles", "Musculature"), ","));
-	r.push("lactation, pregnancy, fetuses, implant size, or age) and then choose the level at which to apply. For example to apply a rule to obedient slaves, choose");
-	r.push(App.Encyclopedia.Dialog.linkDOM("devotion", "From Rebellious to Devoted", "devotion accept"));
-	r.push("for the activation and 4 or more for the lower limit by selecting <span class='encyclopedia interaction'>>=.</span>");
-	r.push(`You can also create custom conditions using any property of a slave, which you can find documented <a target='_blank' class='link-external' href='https://gitgud.io/pregmodfan/fc-pregmod/-/raw/pregmod-master/devNotes/legacy files/slave%20variables%20documentation.md'>here.</a>`);
+	r.push(App.Encyclopedia.topic("Rule activation conditions"));
+	r.push("To have control over which slaves the rule will apply to conditions can be created.");
+	r.push(App.Encyclopedia.Dialog.linkDOM("In-depth explanation", "RA Condition Editor"));
+	r.push("of the condition editor.");
 	App.Events.addParagraph(f, r);
 
 	r = [];
@@ -141,11 +132,6 @@ App.Encyclopedia.addArticle("Rules Assistant", function() {
 	r.push("Slaves can be selected for a rule by selecting slaves from the list so that a rule can apply only to them. Slaves can similarly be excluded from a rule.");
 	App.Events.addParagraph(f, r);
 
-	r = [];
-	r.push(App.Encyclopedia.topic("Applying a rule to specific assignments"));
-	r.push("You can apply a rule only to slaves on individual assignments by selecting them under <span class='encyclopedia interaction'>Apply to assignments.</span> For example a rule can give aphrodisiacs to slaves on whoring assignments. <span class='note'>This is mutually exclusive to automatically giving an assignment to slaves.</span>");
-	App.Events.addParagraph(f, r);
-
 	r = [];
 	r.push(App.Encyclopedia.topic("Automatically giving an assignment"));
 	r.push("A rule can be set to automatically set a slave to an assignment when activated. For example a");
@@ -153,11 +139,6 @@ App.Encyclopedia.addArticle("Rules Assistant", function() {
 	r.push("slave can be set to automatically be put on the whoring assignment. <span class='note'>This is mutually exclusive to applying a rule to assignments.</span>");
 	App.Events.addParagraph(f, r);
 
-	r = [];
-	r.push(App.Encyclopedia.topic("Applying a rule to facilities"));
-	r.push("You can apply a rule to slaves in any or all facilities as long as that facility has been constructed. The rule will only apply to slaves within the selected facilities. <span class='note'>This is mutually exclusive to automatically putting slaves into a facility.</span>");
-	App.Events.addParagraph(f, r);
-
 	r = [];
 	r.push(App.Encyclopedia.topic("Automatically assigning slaves to a facility"));
 	r.push("A rule can be set to automatically put a slave into a facility when activated. For example disobedient slaves can be set to automatically be confined in the arcade if it has been constructed. <span class='note'>This is mutually exclusive to applying a rule to facilities.</span>");
@@ -294,6 +275,7 @@ App.Encyclopedia.addCategory("beingInCharge", function() {
 	links.push(App.Encyclopedia.Dialog.linkDOM("Random Events", "Random Events"));
 	links.push(App.Encyclopedia.Dialog.linkDOM("Costs Summary", "Costs Summary"));
 	links.push(App.Encyclopedia.Dialog.linkDOM("Rules Assistant", "Rules Assistant"));
+	links.push(App.Encyclopedia.Dialog.linkDOM("RA Condition Editor", "RA Condition Editor"));
 	links.push(App.Encyclopedia.Dialog.linkDOM("The Corporation", "The Corporation"));
 	links.push(App.Encyclopedia.Dialog.linkDOM("Sexual Energy", "Sexual Energy"));
 	links.push(App.Encyclopedia.Dialog.linkDOM("PC Skills", "PC Skills"));
diff --git a/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js b/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js
new file mode 100644
index 0000000000000000000000000000000000000000..173ac2d0ad92829ad947d6eadb7d313e0236074a
--- /dev/null
+++ b/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js
@@ -0,0 +1,131 @@
+App.Encyclopedia.addArticle("RA Condition Editor", function() {
+	/**
+	 * @param {HTMLElement} container
+	 * @param {string} col1
+	 * @param {string} col2
+	 * @param {string} col3
+	 */
+	function tableHead(container, col1, col2, col3) {
+		App.UI.DOM.appendNewElement("div", container, col1, "head");
+		App.UI.DOM.appendNewElement("div", container, col2, "head");
+		App.UI.DOM.appendNewElement("div", container, col3, "head");
+	}
+
+	/**
+	 * @param {ReadonlyMap<string, Getter<*>>} getters
+	 */
+	function getterTable(getters) {
+		const container = document.createElement("p");
+		container.classList.add("rule-help-table");
+
+		tableHead(container, "Name", "Description", "Requirements");
+
+		for (const getter of getters.values()) {
+			if (getter.visible && !getter.visible()) {
+				continue;
+			}
+			App.UI.DOM.appendNewElement("div", container, getter.name);
+			App.UI.DOM.appendNewElement("div", container, getter.description);
+			const div = document.createElement("div");
+			if (getter.requirements) {
+				div.append(getter.requirements);
+			}
+			container.append(div);
+		}
+
+		return container;
+	}
+
+	/**
+	 * @param {HTMLElement} container
+	 * @param {string} name
+	 * @param {string} description
+	 * @param {string} dataTypes
+	 */
+	function transformerRow(container, name, description, dataTypes) {
+		App.UI.DOM.appendNewElement("div", container, name);
+		App.UI.DOM.appendNewElement("div", container, description);
+		App.UI.DOM.appendNewElement("div", container, dataTypes);
+	}
+
+	function transformerTable() {
+		const el = document.createElement("p");
+		el.classList.add("rule-help-table");
+
+		tableHead(el, "Name", "Description", "Data types");
+
+		transformerRow(el, "And", "True, if all input values are true.", "Boolean");
+		transformerRow(el, "Or", "True, if at least one input value is true.", "Boolean");
+		transformerRow(el, "Sum all", "Sums up all input values", "Number");
+		transformerRow(el, "Multiply all", "Multiplies all input values", "Number");
+		transformerRow(el, "Maximum", "Gives the largest input value", "Number");
+		transformerRow(el, "Minimum", "Gives the smallest input value", "Number");
+		transformerRow(el, "==, !=", "Compares the input values based on the comparator in " +
+			"the middle. Both sides need to have the same data type.", "Boolean, Number, String");
+		transformerRow(el, "<, >, <=, >=", "Compares the input values based on the comparator in " +
+			"the middle. Both sides need to have the same data type.", "Boolean, Number");
+		transformerRow(el, "-", "Subtracts the second value from the first value", "Number");
+		transformerRow(el, "/", "Divides the second value by the first value", "Number");
+		transformerRow(el, "Contains", "True, if the second value is somewhere in the first value",
+			"String");
+		transformerRow(el, "Not …", "Negates the input value.", "Boolean");
+		transformerRow(el, "If … Then … Else …",
+			"If the first value is true, returns the second value, otherwise the third value. The second " +
+			"and third input value have to be the same data type.", "Boolean / Any");
+
+		return el;
+	}
+
+	const acc = new SpacedTextAccumulator();
+
+	acc.push("Rule conditions consist of two types of elements, data getters and data transformers. Data getters can " +
+		"read out values from various places, which can then be used to base conditions on. Data transformers " +
+		"take one or more elements as input and transform the input values into a single new value. Together they " +
+		"can create complex conditions for activating rules.");
+	acc.toParagraph();
+
+	App.UI.DOM.appendNewElement("h2", acc.container(), "Finding errors");
+	acc.push("It is possible to create element trees which are invalid, for example because the input value of an " +
+		"element has the wrong data type. When this is the case there will be an error message above the rule " +
+		"editor and the broken elements are marked. As long as there are errors the rule cannot be saved and will " +
+		"revert to it's previous state when leaving the RA or editing another rule. In the error message the first " +
+		"error corresponds to the innermost broken element. When solving errors it is advised to work from inside to " +
+		"outside, as outer errors are often caused by errors further inside.");
+	acc.toParagraph();
+
+	App.UI.DOM.appendNewElement("h2", acc.container(), "Data transformers");
+	acc.push("Data transformers can handle 3 different data types: Boolean, Number and String. Not all transformers " +
+		"accept all data types. Number and Boolean can be used interchangeably, the conversion is as follows: " +
+		"Putting a Number into a boolean transformer will interpret 0 as false and all other values as true. " +
+		"Putting a Boolean into a number transformer will interpret false as 0 and true as 1.");
+	acc.toParagraph();
+	acc.container().append(transformerTable());
+
+	App.UI.DOM.appendNewElement("h2", acc.container(), "Data getters");
+
+	acc.push("There are a number of predefined getters which read values either from a slave or from the global " +
+		"state. They always have a predefined data type.");
+	acc.toParagraph();
+
+	let c = acc.container();
+	App.UI.DOM.appendNewElement("h3", c, "Boolean getters");
+	c.append(getterTable(App.RA.Activation.getterManager.booleanGetters));
+	App.UI.DOM.appendNewElement("h3", c, "Assignment getters");
+	acc.push("A special type of boolean getters checking if the slave has the given assignment");
+	acc.toParagraph();
+	c = acc.container();
+	c.append(getterTable(App.RA.Activation.getterManager.assignmentGetters));
+	App.UI.DOM.appendNewElement("h3", c, "Number getters");
+	c.append(getterTable(App.RA.Activation.getterManager.numberGetters));
+	App.UI.DOM.appendNewElement("h3", c, "String getters");
+	c.append(getterTable(App.RA.Activation.getterManager.stringGetters));
+	App.UI.DOM.appendNewElement("h3", c, "Custom getters");
+	acc.push("If greater freedom is required for the conditions needed, a custom data getter can be used.",
+		"It operates on a context object with the following properties: slave: The slave currently tested against.",
+		"It is required to explicitly set the return type. If the set type does not match the actual return type, " +
+		"the condition evaluation will fail! Documentation for slave attributes can be found " +
+		"<a target='_blank' class='link-external' href='https://gitgud.io/pregmodfan/fc-pregmod/-/raw/pregmod-master/devNotes/legacy files/slave%20variables%20documentation.md'>here.</a>");
+	acc.toParagraph();
+
+	return acc.container();
+}, "beingInCharge");
diff --git a/src/js/main.js b/src/js/main.js
index becda31cd2681bc718b1011526fed6693ea7b4b8..fb16243105f7d03fc3b0d29adcbd8af4fc76e5d9 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -184,7 +184,11 @@ App.MainView.full = function() {
 
 		penthouseCensus();
 		V.costs = Math.trunc(calculateCosts.predict());
-		V.currentRule = V.defaultRules[0];
+		if (V.defaultRules.length > 0) {
+			V.currentRule = V.defaultRules[0].ID;
+		} else {
+			V.currentRule = null;
+		}
 		SlaveSort.slaves(V.slaves);
 
 		App.UI.SlaveList.ScrollPosition.restore();
diff --git a/src/js/rulesAssistant.js b/src/js/rulesAssistant.js
index 455c1baf15b755021f050c8fdb15cb2b21acbe4d..3a3709ff5f93f2066a7df7f8a9341592641f28fb 100644
--- a/src/js/rulesAssistant.js
+++ b/src/js/rulesAssistant.js
@@ -108,7 +108,6 @@ globalThis.RAFacilityRemove = function(slave, rule) {
 globalThis.ruleAppliesP = function(rule, slave) {
 	let V = State.variables;
 	let cond = rule.condition;
-	let slaveAttribute = slave[cond.data.attribute];
 
 	// Check if slave should be excluded from having rule applied to again
 	if (cond.applyRuleOnce) {
@@ -124,51 +123,15 @@ globalThis.ruleAppliesP = function(rule, slave) {
 		}
 	}
 
-	// assignment / facility / special slaves / specific slaves check
-	if (cond.assignment.length > 0 && !cond.assignment.includes(slave.assignment)) {
-		return false;
-	} else if (cond.selectedSlaves.length > 0 && !cond.selectedSlaves.includes(slave.ID)) {
+	//  special slaves / specific slaves check
+	if (cond.selectedSlaves.length > 0 && !cond.selectedSlaves.includes(slave.ID)) {
 		return false;
 	} else if (cond.excludedSlaves.includes(slave.ID)) {
 		return false;
 	}
 
 	// attribute / function check
-	let flag = true;
-	switch (cond.function) {
-		case false: // never applies
-			flag = false;
-			break;
-		case "between": // between two values of a slave's attribute
-			if (slaveAttribute === undefined && cond.data.attribute.includes(".")) {
-				slaveAttribute = cond.data.attribute
-					.split(".")
-					.reduce(
-						(reduceSlave, attribute) =>
-							(reduceSlave && reduceSlave[attribute] !== undefined)
-								? reduceSlave[attribute]
-								: undefined,
-						slave
-					);
-			}
-			// check if slave value is between rule values, if bounds exist
-			flag = (cond.data.value[0] === null || slaveAttribute > cond.data.value[0]) &&
-				(cond.data.value[1] === null || slaveAttribute < cond.data.value[1]);
-			break;
-		case "belongs": // the attribute belongs in the list of values
-			flag = cond.data.value.includes(slave[cond.data.attribute]);
-			break;
-		case "custom": // user provided JS function
-			// TODO: This should use a cached Function instead of 'eval'ing
-			try {
-				flag = eval(cond.data)(slave);
-			} catch (e) {
-				// Put together a more useful message for the player. Does mean we are losing the stacktrace.
-				throw new Error(`Rule '${rule.name}' custom condition failed: '${e.message}'`);
-			}
-			break;
-	}
-	if (!flag) {
+	if (!App.RA.Activation.evaluate(slave, cond.activation)) {
 		return false;
 	}
 
@@ -176,7 +139,7 @@ globalThis.ruleAppliesP = function(rule, slave) {
 		V.rulesToApplyOnce[rule.ID].push(slave.ID);
 	}
 	// If rule always applies.
-	if (cond.applyRuleOnce && !V.rulesToApplyOnce[rule.ID].includes(slave.ID) && flag) {
+	if (cond.applyRuleOnce && !V.rulesToApplyOnce[rule.ID].includes(slave.ID)) {
 		V.rulesToApplyOnce[rule.ID].push(slave.ID);
 	}
 
@@ -207,9 +170,7 @@ App.RA.newRule = function() {
 	/** @returns {FC.RA.RuleConditions} */
 	function emptyConditions() {
 		return {
-			function: false,
-			data: {},
-			assignment: [],
+			activation: [true, 1, "and"],
 			selectedSlaves: [],
 			excludedSlaves: [],
 			applyRuleOnce: false,
@@ -489,9 +450,7 @@ App.RA.ruleDeepAssign = function deepAssign(target, source) {
 globalThis.initRules = function() {
 	const rule = emptyDefaultRule();
 	rule.name = "Obedient Slaves";
-	rule.condition.function = "between";
-	rule.condition.data.attribute = "devotion";
-	rule.condition.data.value = [20, null];
+	rule.condition.activation = ["vdevotion", 20, "gte", 1, "and"];
 
 	V.defaultRules = [rule];
 	V.rulesToApplyOnce = {};
diff --git a/src/js/rulesAssistantOptions.js b/src/js/rulesAssistantOptions.js
index be981014f9eecc1976b3d181b3fb01e5bc378309..c41aa7ff14e7f7d890e1d9730bf22cb3aa28b109 100644
--- a/src/js/rulesAssistantOptions.js
+++ b/src/js/rulesAssistantOptions.js
@@ -30,6 +30,10 @@ App.RA.options = (function() {
 			}
 		}
 		root = new Root(div);
+		V.passageSwitchHandler = () => {
+			saveSettings();
+			App.RA.Activation.Editor.reset();
+		};
 	}
 
 	function returnP(e) { return e.keyCode === 13; }
@@ -90,11 +94,20 @@ App.RA.options = (function() {
 
 	// reload the passage
 	function reload() {
+		saveSettings();
+		App.RA.Activation.Editor.reset();
 		const elem = root.element;
 		elem.innerHTML = "";
 		rulesAssistantOptions(elem);
 	}
 
+	/**
+	 * Save the settings for this rule.
+	 */
+	function saveSettings() {
+		App.RA.Activation.Editor.save(cond => current_rule.condition.activation = cond);
+	}
+
 	const parse = {
 		integer(string) {
 			let n = parseInt(string, 10);
@@ -1220,7 +1233,10 @@ App.RA.options = (function() {
 			super();
 			this.appendChild(new OptionsItem("New Rule", newRule));
 			this.appendChild(new OptionsItem("Remove Rule", removeRule));
-			this.appendChild(new OptionsItem("Apply rules", () => this.appendChild(new ApplicationLog())));
+			this.appendChild(new OptionsItem("Apply rules", () => {
+				saveSettings();
+				this.appendChild(new ApplicationLog());
+			}));
 			this.appendChild(new OptionsItem("Lower Priority", lowerPriority));
 			this.appendChild(new OptionsItem("Higher Priority", higherPriority));
 			this.appendChild(new OptionsItem("Rename", rename(this)));
@@ -1270,355 +1286,15 @@ App.RA.options = (function() {
 	class ConditionEditor extends Section {
 		constructor() {
 			super("Activation Condition");
-			this.appendChild(new ConditionFunction());
-			this.appendChild(new AssignmentInclusion());
-			this.appendChild(new FacilityHeadAssignmentInclusion());
+			this.appendChild(new ConditionBuilder());
 			this.appendChild(new SpecificInclusionExclusion());
 			this.appendChild(new ApplyRuleOnce());
 		}
 	}
 
-	class ConditionFunction extends Element {
-		constructor() {
-			super();
-			const items = [
-				["Never", false],
-				["Always", true],
-				["Custom", "custom"],
-				["Devotion", "devotion"],
-				["Trust", "trust"],
-				["Health", "health.condition"],
-				["Fatigue", "health.tired"],
-				["Sex", "genes"],
-				["Sex drive", "energy"],
-				["Height", "height"],
-				["Weight", "weight"],
-				["Age", "actualAge"],
-				["Body Age", "physicalAge"],
-				["Visible Age", "visualAge"],
-				["Muscles", "muscles"],
-				["Lactation", "lactation"],
-				["Pregnancy", "preg"],
-				["Pregnancy Multiples", "pregType"],
-				["Belly Implant", "bellyImplant"],
-				["Belly Size", "belly"],
-				["Education", "intelligenceImplant"],
-				["Intelligence", "intelligence"],
-				["Fetish", "fetish"],
-				["Accent", "accent"],
-				["Waist", "waist"],
-				["Amputation", "amp"],
-				["Carcinogen Buildup", "chem"],
-			];
-			this.fnlist = new List("Activation function", items, false);
-			this.fnlist.setValue(["between", "belongs"].includes(current_rule.condition.function) ? current_rule.condition.data.attribute : current_rule.condition.function);
-			this.fnlist.onchange = (value) => this.fnchanged(value);
-			this.appendChild(this.fnlist);
-			this.fneditor = null;
-
-			switch (current_rule.condition.function) {
-				case false:
-				case true:
-					break;
-				case "custom":
-					this.show_custom_editor(CustomEditor, current_rule.condition.data);
-					break;
-				case "between":
-					this.show_custom_editor(RangeEditor, current_rule.condition.function, current_rule.condition.data);
-					break;
-				case "belongs":
-					this.show_custom_editor(ItemEditor, current_rule.condition.function, current_rule.condition.data);
-					break;
-			}
-		}
-
-		betweenP(attribute) {
-			return [
-				"devotion",
-				"trust",
-				"health.condition",
-				"health.tired",
-				"energy",
-				"height",
-				"weight",
-				"actualAge",
-				"physicalAge",
-				"visualAge",
-				"muscles",
-				"lactation",
-				"preg",
-				"pregType",
-				"bellyImplant",
-				"belly",
-				"intelligenceImplant",
-				"intelligence",
-				"accent",
-				"waist",
-				"chem",
-			].includes(attribute);
-		}
-
-		belongsP(attribute) {
-			return [
-				"fetish",
-				"amp",
-				"genes",
-			].includes(attribute);
-		}
-
-		show_custom_editor(what, ...args) {
-			if (this.custom_editor !== null) { this.hide_custom_editor(); }
-			this.custom_editor = new what(...args);
-			this.appendChild(this.custom_editor);
-		}
-
-		hide_custom_editor() {
-			if (this.custom_editor) {
-				this.custom_editor.remove();
-				this.custom_editor = null;
-			}
-		}
-
+	class ConditionBuilder extends Element {
 		render() {
-			return document.createElement("div");
-		}
-
-		fnchanged(value) {
-			if (this.fneditor !== null) {
-				this.fneditor.element.remove();
-				this.fneditor = null;
-			}
-			if (value === true || value === false) {
-				current_rule.condition.function = value;
-				current_rule.condition.data = {};
-				this.hide_custom_editor();
-			} else if (value === "custom") {
-				current_rule.condition.function = "custom";
-				current_rule.condition.data = "";
-				this.show_custom_editor(CustomEditor, current_rule.condition.data);
-			} else if (this.betweenP(value)) {
-				current_rule.condition.function = "between";
-				current_rule.condition.data = {attribute: value, value: [null, null]};
-				this.show_custom_editor(RangeEditor, current_rule.condition.function, current_rule.condition.data);
-			} else if (this.belongsP(value)) {
-				current_rule.condition.function = "belongs";
-				current_rule.condition.data = {attribute: value, value: []};
-				this.show_custom_editor(ItemEditor, current_rule.condition.function, current_rule.condition.data);
-			}
-		}
-	}
-
-	class CustomEditor extends Element {
-		constructor(data) {
-			if (data.length === 0) { data = "(slave) => slave.slaveName === 'Fancy Name'"; }
-			super(data);
-		}
-
-		render(data) {
-			const elem = document.createElement("div");
-			const textarea = document.createElement("textarea");
-			textarea.innerHTML = data;
-			$(textarea).blur(() => {
-				current_rule.condition.data = textarea.value;
-				// TODO: this would be a good place to cache the Function object that will be used by RuleHasError and ruleAppliesP
-				reload();
-			});
-			elem.appendChild(textarea);
-
-			if (RuleHasError(current_rule)) {
-				const errorMessage = document.createElement("div");
-				$(errorMessage).addClass("yellow");
-				errorMessage.innerText = "WARNING: There are errors in this condition. Please ensure the syntax is correct and equality is either '==' or '===', not '='";
-				elem.appendChild(errorMessage);
-			}
-
-			const explanation = document.createElement("div");
-			explanation.innerHTML = `Insert <kbd>(slave) =></kbd> followed by a valid <a target='_blank' class='link-external' href='https://www.w3schools.com/js/js_comparisons.asp'>JavaScript comparison and/or logical operation</a>. For variable names to use see <a target='_blank' class='link-external' href='https://gitgud.io/pregmodfan/fc-pregmod/-/raw/pregmod-master/devNotes/legacy%20files/slave%20variables%20documentation.md'>this list</a>.`;
-			elem.appendChild(explanation);
-			return elem;
-		}
-	}
-
-
-	class RangeEditor extends Element {
-		render(fn, data) {
-			const elem = document.createElement("div");
-
-			const minlabel = document.createElement("label");
-			minlabel.innerHTML = "Lower bound: ";
-			elem.appendChild(minlabel);
-
-			const min = document.createElement("input");
-			min.setAttribute("type", "text");
-			min.value = `${data.value[0]}`;
-			min.onkeypress = e => { if (returnP(e)) { this.setmin(min.value); } };
-			min.onblur = e => this.setmin(min.value);
-			this.min = min;
-			elem.appendChild(min);
-
-			elem.appendChild(document.createElement("br"));
-
-			const maxLabel = document.createElement("label");
-			maxLabel.innerHTML = "Upper bound: ";
-			elem.appendChild(maxLabel);
-
-			const max = document.createElement("input");
-			max.setAttribute("type", "text");
-			max.value = `${data.value[1]}`;
-			max.onkeypress = e => { if (returnP(e)) { this.setmax(max.value); } };
-			max.onblur = e => this.setmax(max.value);
-			this.max = max;
-			elem.appendChild(max);
-
-			const infoBar = document.createElement("div");
-			infoBar.innerHTML = this.info(data.attribute);
-			elem.appendChild(infoBar);
-
-			return elem;
-		}
-
-		parse(value) {
-			value = value.trim();
-			if (value === "null") {
-				value = null;
-			} else {
-				value = parseInt(value);
-				if (isNaN(value)) { value = null; }
-			}
-			return value;
-		}
-
-		setmin(value) {
-			current_rule.condition.data.value[0] = this.parse(value);
-			this.min.value = `${current_rule.condition.data.value[0]}`;
-		}
-
-		setmax(value) {
-			current_rule.condition.data.value[1] = this.parse(value);
-			this.max.value = `${current_rule.condition.data.value[1]}`;
-		}
-
-		info(attribute) {
-			return ({
-				"devotion": "Very Hateful: (-∞, -95), Hateful: [-95, -50), Resistant: [-50, -20), Ambivalent: [-20, 20], Accepting: (20, 50], Devoted: (50, 95], Worshipful: (95, ∞)",
-				"trust": "Extremely terrified: (-∞, -95), Terrified: [-95, -50), Frightened: [-50, -20), Fearful: [-20, 20], Careful: (20, 50], Trusting: (50, 95], Total trust: (95, ∞)",
-				"health.condition": "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, ∞)",
-				"health.tired": "Energetic: (-∞, 0], Rested: (0, 30], Tired: (30, 60], Fatigued: (60, 90], Exhausted: (90, ∞)",
-				"energy": "Frigid: (-∞, 20], Poor: (20, 40], Average: (40, 60], Powerful: (60, 80], Sex Addict: (80, 100), Nympho: 100",
-				"weight": "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, ∞)",
-				"lactation": "None: 0, 1: Natural, 2: Lactation implant",
-				"preg": "Barren: -2, On contraceptives: -1, Not pregnant: 0, Pregnancy weeks: [1, ∞)",
-				"pregType": "Fetus count, known only after the 10th week of pregnancy",
-				"bellyImplant": "Volume in CCs. None: -1",
-				"belly": "Volume in CCs, any source",
-				"intelligenceImplant": "Education level. 0: uneducated, 15: educated, 30: advanced education, (0, 15): incomplete education.",
-				"intelligence": "From moronic to brilliant: [-100, 100]",
-				"accent": "No accent: 0, Nice accent: 1, Bad accent: 2, Can't speak language: 3 and above",
-				"waist": "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)",
-			}[attribute] || " ");
-		}
-	}
-
-	class ItemEditor extends Element {
-		render(fn, data) {
-			const elem = document.createElement("div");
-
-			const input = document.createElement("input");
-			input.setAttribute("type", "text");
-			input.value = JSON.stringify(data.value);
-			input.onkeypress = e => { if (returnP(e)) { this.setValue(input); } };
-			input.onblur = e => this.setValue(input);
-			this.input = input;
-			elem.appendChild(input);
-
-			const infoBar = document.createElement("div");
-			infoBar.innerHTML = this.info(data.attribute);
-			elem.appendChild(infoBar);
-
-			return elem;
-		}
-
-		info(attribute) {
-			return `Insert a valid JSON array. Known values: ${{
-				"fetish": "buttslut, cumslut, masochist, sadist, dom, submissive, boobs, pregnancy, none (AKA vanilla)",
-				"amp": "Amputated: 1, Not amputated: 0",
-				"genes": "XX, XY",
-			}[attribute]} Example: ["value"]`;
-		}
-
-		setValue(input) {
-			try {
-				const arr = JSON.parse(input.value);
-				current_rule.condition.data.value = arr;
-				input.value = JSON.stringify(arr);
-			} catch (e) {
-				alert(e);
-			}
-		}
-	}
-
-	class AssignmentInclusionBase extends ButtonList {
-		/**
-		 * @param {string} label
-		 * @param {FC.Data.JobDesc[]} [jobs]
-		 * @param {App.Entity.Facilities.SingleJobFacility[]} [facilities]
-		 */
-		constructor(label, jobs, facilities) {
-			super(label);
-			this._attributes = {};
-			if (jobs !== undefined) {
-				jobs.forEach(job => {
-					this._attributes[capFirstChar(job.position)] = job.assignment;
-				});
-			}
-			if (facilities !== undefined) {
-				facilities.forEach(f => {
-					if (f.established && f.desc.defaultJob != null) { /* eslint-disable-line eqeqeq */
-						const displayName = f.name === "the " + f.genericName ? f.genericName : f.name;
-						this._attributes[displayName] = f.desc.jobs[f.desc.defaultJob].assignment;
-					}
-				});
-			}
-			for (const i in this._attributes) {
-				this.appendChild(new ButtonItem(i, this.getAttribute(i), current_rule.condition.assignment.includes(this.getAttribute(i))));
-			}
-		}
-
-		onchange() {
-			const allValues = this.getAllValues();
-			current_rule.condition.assignment = this.getSelection().concat(current_rule.condition.assignment.filter(a => !allValues.includes(a)));
-		}
-
-		getAttribute(what) {
-			return this._attributes[what];
-		}
-	}
-
-
-	class AssignmentInclusion extends AssignmentInclusionBase {
-		constructor() {
-			let facilities = [];
-			for (const f of Object.values(App.Entity.facilities)) {
-				if (f === App.Entity.facilities.penthouse) {
-					continue;
-				}
-				if (f.established) {
-					facilities.push(f);
-				}
-			}
-			super("Apply to assignments and facilities", Object.values(App.Data.Facilities.penthouse.jobs), facilities);
-		}
-	}
-
-	class FacilityHeadAssignmentInclusion extends AssignmentInclusionBase {
-		constructor() {
-			const jobs = [];
-			for (const f of Object.values(App.Entity.facilities)) {
-				if (f.established && f.desc.manager !== null) {
-					jobs.push(f.desc.manager);
-				}
-			}
-			super("Apply to facility heads", jobs);
+			return App.RA.Activation.Editor.build(current_rule.condition.activation);
 		}
 	}
 
diff --git a/src/js/utilsDOM.js b/src/js/utilsDOM.js
index 62ee6645ceefe4d17a92651963fc164221a8317d..9f159b5c139e837e69edf0ee2930f0d4492a2564 100644
--- a/src/js/utilsDOM.js
+++ b/src/js/utilsDOM.js
@@ -276,6 +276,7 @@ App.UI.DOM.makeTextBox = function(defaultValue, onEnter, numberOnly = false) {
 		to 0 and trigger a change event we can't distinguish from setting the value to 0 explicitly.
 		The workaround is resetting the value to the last known valid value and not triggering onEnter.
 		*/
+		input.classList.add("number");
 		let oldValue = defaultValue;
 		updateValue = event => {
 			const newValue = Number(event.target.value);
diff --git a/src/zz1-last/init.js b/src/zz1-last/init.js
index e92d5bb79e823e29e756a2f17a6b5e8912fa4c70..05063454484bb3c67df767e69bd48478884facd9 100644
--- a/src/zz1-last/init.js
+++ b/src/zz1-last/init.js
@@ -1,5 +1,6 @@
 App.Art.cacheArtData();
 App.Corporate.Init();
+App.RA.Activation.populateGetters();
 
 // TODO: remove once setup object is no longer required.
 for (let key in App.Data.misc) {