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

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

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

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

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

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

		const ruleDiv = document.createElement("div");
		ruleDiv.classList.add("condition-viewer");
		const errors = [];
		if (currentRule.validate(errors) === "error") {
			ruleDiv.append("Condition has errors:");
			for (const error of errors) {
				ruleDiv.append(" ", error);
			}
			ruleDiv.append(" Changes have NOT been saved!");
		} else {
			ruleDiv.append("Condition saved.");
		}
		ruleDiv.append(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 RulePartTrash().render());
		div.append(new RulePartProvider(() => new RuleGroup("and")).render());
		div.append(new RulePartProvider(() => new RuleGroup("add")).render());
		div.append(new RulePartProvider(() => new RuleNegate()).render());
		div.append(new RulePartProvider(() => new RulePair("eq")).render());
		div.append(new RulePartProvider(() => new RuleTernary()).render());
		div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.booleanDefault)).render());
		div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.numberDefault)).render());
		div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.stringDefault)).render());
		div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.assignmentDefault)).render());
		div.append(new RulePartProvider(() => new RuleConstant(0)).render());
		div.append(new RulePartProvider(() => new RuleBooleanConstant(true)).render());
		div.append(new RulePartProvider(() => new RuleConstant("some text")).render());
		div.append(new RulePartProvider(() => new RuleCustomCheck("bcontext => false")).render());
		div.append(new RulePartTrash().render());
		container.append(div);
		return container;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		removeChild(rulePart) {
		}

		isParent(maybeChild) {
			return false;
		}
	}

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

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

		removeChild(rulePart) {
		}

		isParent(maybeChild) {
			return false;
		}
	}

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

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

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

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

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

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

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

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

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

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

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

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

			return div;
		}

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

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

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

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

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

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

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

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

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

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

			this._markValidationError(div);
			return div;
		}

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

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

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

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

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

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

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

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

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

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

			this._markValidationError(div);
			return div;
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

			outerDiv.append(leftDiv);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	return new GetterManager();
})();

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

	// Booleans
	gm.addBoolean("isfertile", {
		name: "Is Fertile?", description: "Whether or not the slave is fertile.",
		val: c => isFertile(c.slave)
	});
	gm.addBoolean("isvirile", {
		name: "Is Virile?", description: "Whether or not the slave is virile, has fertile sperm.",
		val: c => isVirile(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.isUnmodded(c.slave)
	});
	gm.addBoolean("canmove", {
		name: "Can Move?", description: "Can the slave move at all?",
		val: c => canMove(c.slave)
	});
	gm.addBoolean("canstand", {
		name: "Can Stand?", description: "Can the slave stand independently?",
		val: c => canStand(c.slave)
	});
	gm.addBoolean("canwalk", {
		name: "Can Walk?", description: "Can the slave walk independently; if they can, they can move and stand too.",
		val: c => canWalk(c.slave)
	});
	gm.addBoolean("hasinternalballs", {
		name: "Has Internal Balls?", description: "If the slaves has internal balls. False, if the slave has no balls",
		val: c => c.slave.balls > 0 && c.slave.scrotum === 0
	});

	// Assignments
	// Groups
	gm.addAssignment("any_penthouse", {
		name: "Penthouse", description: "Serving in your penthouse.",
		val: c =>
			[
				Job.REST, Job.FUCKTOY, Job.HOUSE, Job.WHORE, Job.PUBLIC, Job.SUBORDINATE, Job.MILKED, Job.GLORYHOLE,
				Job.CONFINEMENT, Job.CHOICE
			].includes(c.slave.assignment)
	});
	gm.addAssignment("any_leader", {
		name: "Leader", description: "Serving in any leadership position.",
		val: c =>
			[
				Job.BODYGUARD, Job.HEADGIRL, Job.RECRUITER, Job.AGENT, Job.AGENTPARTNER, Job.MADAM, Job.WARDEN, Job.DJ,
				Job.NURSE, Job.MILKMAID, Job.FARMER, Job.CONCUBINE, Job.MATRON, Job.TEACHER, Job.STEWARD, Job.ATTENDANT
			].includes(c.slave.assignment)
	});
	gm.addAssignment("any_facility", {
		name: "Facility", description: "Serving in any facility.",
		val: c =>
			[
				Job.ARCADE, Job.BROTHEL, Job.CELLBLOCK, Job.CLUB, Job.CLINIC, Job.DAIRY, Job.FARMYARD,
				Job.HEADGIRLSUITE, Job.MASTERSUITE, Job.NURSERY, Job.SCHOOL, Job.QUARTER, Job.SPA
			].includes(c.slave.assignment)
	});
	// 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("pregWeeks", {
		name: "Pregnancy Progress", description: "Fetal development status, in weeks equivalent to unaccelerated pregnancy. Negative is postpartum.",
		val: c => c.slave.pregWeek
	});
	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("face", {
		name: "Face", description: "Very Ugly: (-∞, -96), Ugly: [-96, -40), Unattractive: [-40, -10), " +
			"Average: [-10, 10), Attractive: [10, 40), Beautiful: [40, 95), Very beautiful: [95, ∞)",
		val: c => c.slave.face
	});
	gm.addNumber("beautyscore", {
		name: "Beauty Score", description: "As seen in the slave description",
		val: c => Beauty(c.slave)
	});
	gm.addNumber("sexscore", {
		name: "Sexual Score", description: "As seen in the slave description",
		val: c => FResult(c.slave)
	});
	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("addict", {
		name: "Aphrodisiac Addiction",
		description: "No addiction: 0, Minor addiction: 1-2, Serious addiction: 3 and above",
		val: c => c.slave.addict
	});
	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
	});
	gm.addNumber("shoulders", {
		name: "Shoulders",
		description: "Very narrow: -2, Narrow: -1, Feminine: 0, Broad: 1, Very broad: 2",
		val: c => c.slave.shoulders
	});
	gm.addNumber("vagina", {
		name: "Vagina Size",
		description: "No Vagina: -1, Virgin: 0, Tight: 1, higher numbers are looser.",
		val: c => c.slave.vagina
	});
	gm.addNumber("anus", {
		name: "Anus Size",
		description: "Virgin: 0, Tight: 1, higher numbers are looser.",
		val: c => c.slave.anus
	});
	gm.addNumber("wetness", {
		name: "Vaginal Wetness",
		description: "How easily the slave gets wet: dry: 0, wet: 1, soaking wet: 2",
		val: c => c.slave.vaginaLube
	});
	gm.addNumber("oralskill", {
		name: "Oral Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.oral
	});
	gm.addNumber("analskill", {
		name: "Anal Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.anal
	});
	gm.addNumber("vaginalskill", {
		name: "Vaginal Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.vaginal
	});
	gm.addNumber("penetrativeskill", {
		name: "Penetrative Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.penetrative
	});
	gm.addNumber("whoringskill", {
		name: "Whoring Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.whoring
	});
	gm.addNumber("entertainmentskill", {
		name: "Entertainment Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.entertainment
	});
	gm.addNumber("combatskill", {
		name: "Combat Skill",
		description: "Unskilled: (-∞, 10), Basic: [10, 30), Skilled: [30, 60), Expert: [60, 99), Master: [99, ∞)",
		val: c => c.slave.skill.combat
	});

	// Strings
	gm.addString("label", {name: "Label", description: "Assigned Label", val: c => c.slave.custom.label});
	gm.addString("genes", {
		name: "Genetic sex", description: "Genetic sex: Female: XX, Male: XY",
		val: c => c.slave.genes
	});
	gm.addString("phenotypesex", {
		name: "Phenotype sex",
		description: "A three-character string that encodes state of sexual organs" +
			" as female('X'), male('Y'), both ('H'), or absent ('-'): vagina/dick, ovaries/balls, tits",
		val: c => phenotypeSex(c.slave)
	});
	gm.addString("fetish", {
		name: "Fetish",
		description: "One of 'buttslut', 'cumslut', 'masochist', 'sadist', 'dom', 'submissive', 'boobs', " +
			"'pregnancy', 'none' (AKA vanilla)",
		val: c => c.slave.fetish
	});
	gm.addString("teeth", {
		name: "Teeth",
		description: "One of 'normal', 'crooked', 'gapped', 'straightening braces', 'cosmetic braces', 'removable', 'pointy', 'fangs', 'fang', 'baby', 'mixed'",
		val: c => c.slave.teeth
	});
	gm.addString("faceshape", {
		name: "Face Shape",
		description: "One of 'normal', 'masculine', 'androgynous', 'cute', 'sensual', 'exotic', 'feline' (catmod only)",
		val: c => c.slave.faceShape
	});
	gm.addString("title", {
		name: "Title",
		description: "Slave title (class) without adjectives (slavegirl, MILF, bimbo, shemale, herm, etc.). ",
		requirements: "The alternate titles game option is enabled",
		enabled: () => V.newDescriptions === 1,
		val: c => SlaveTitle(c.slave, false, false)
	});
};

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

	/**
	 * @type {Map<string, function(): void>}
	 */
	const operators = new Map([
		// and, or, +, * can take arbitrarily many arguments, so the first one describes the argument count
		["and", () => {
			const length = stack.popNumber();
			let value = 1;
			for (let i = 0; i < length; i++) {
				value &= stack.popAsBoolean();
			}
			stack.pushNumber(value);
		}],
		["or", () => {
			const length = stack.popNumber();
			let value = 0;
			for (let i = 0; i < length; i++) {
				value |= stack.popAsBoolean();
			}
			stack.pushNumber(value);
		}],
		["add", () => {
			const length = stack.popNumber();
			let value = 0;
			for (let i = 0; i < length; i++) {
				value += stack.popNumber();
			}
			stack.pushNumber(value);
		}],
		["mul", () => {
			const length = stack.popNumber();
			let value = 1;
			for (let i = 0; i < length; i++) {
				value *= stack.popNumber();
			}
			stack.pushNumber(value);
		}],
		["max", () => {
			const length = stack.popNumber();
			let value = stack.popNumber();
			for (let i = 1; i < length; i++) {
				value = Math.max(value, stack.popNumber());
			}
			stack.pushNumber(value);
		}],
		["min", () => {
			const length = stack.popNumber();
			let value = stack.popNumber();
			for (let i = 1; i < length; i++) {
				value = Math.min(value, stack.popNumber());
			}
			stack.pushNumber(value);
		}],
		["sub", () => {
			const subtract = stack.popNumber();
			stack.pushNumber(stack.popNumber() - subtract);
		}],
		["div", () => {
			const divisor = stack.popNumber();
			stack.pushNumber(stack.popNumber() / divisor);
		}],
		["eqstr", () => stack.pushBoolean(stack.popString() === stack.popString())],
		["neqstr", () => stack.pushBoolean(stack.popString() !== stack.popString())],
		["eqnum", () => stack.pushBoolean(stack.popNumber() === stack.popNumber())],
		["neqnum", () => stack.pushBoolean(stack.popNumber() === stack.popNumber())],
		["gt", () => stack.pushBoolean(stack.popNumber() < stack.popNumber())],
		["gte", () => stack.pushBoolean(stack.popNumber() <= stack.popNumber())],
		["lt", () => stack.pushBoolean(stack.popNumber() > stack.popNumber())],
		["lte", () => stack.pushBoolean(stack.popNumber() >= stack.popNumber())],
		["substr", () => {
			const value = stack.popString();
			stack.pushBoolean(stack.popString().includes(value));
		}],
		["match", () => {
			const value = stack.popString();
			stack.pushBoolean(stack.popString().match(value) !== null);
		}],
		["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: Can we cache the function (and is that useful)?
			const value = (new Function("c", `return (${rulePart.slice(2)})(c)`))(context);
			if (expectedType === "boolean") {
				stack.pushNumber(value ? 1 : 0);
			} else if (expectedType === "number") {
				stack.pushNumber(value);
			} else {
				// no guarantee we are getting a string, and it's easy to ensure it is one.
				stack.pushString(value.toString());
			}
		} 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();
};

repo_name: Arkerthan/fc

license: mit


code:
App.RA.Activation.Editor = (function() {
	/**
	 * Should the advanced mode (tree editor) be used?
	 * @type {boolean}
	 */
	let advanced = false;
	/**
	 * Keep a reference to the outermost node, so we can refresh it when needed.
	 * @type {HTMLDivElement}
	 */
	let outerNode = null;

	/**
	 * @param {FC.RA.RuleConditionEditorArguments} args
	 * @returns {HTMLDivElement}
	 */
	function editor(args) {
		outerNode = document.createElement("div");
		fillOuterNode(args);
		return outerNode;
	}

	/**
	 * @param {FC.RA.RuleConditionEditorArguments} args
	 */
	function fillOuterNode(args) {
		advanced = args.advancedMode;
		let editorNode = document.createElement("div");
		let switchNode = document.createElement("div");
		outerNode.append(switchNode);
		if (advanced) {
			switchNode.append(App.UI.DOM.link("Reset to simple mode", () => {
				if (SugarCube.Dialog.isOpen()) {
					SugarCube.Dialog.close();
				}
				SugarCube.Dialog.setup("Reset RA to simple mode");
				$(SugarCube.Dialog.body()).empty().append(
					"
Resetting will delete your current conditions. Do you want to continue?
",
					App.UI.DOM.link("Yes, delete conditions.", () => {
						args.advancedMode = false;
						args.activation = App.RA.newRule.conditions().activation;
						SugarCube.Dialog.close();
						$(outerNode).empty();
						fillOuterNode(args);
					}), " ",
					App.UI.DOM.makeElement("p", App.UI.DOM.link("Abort.", () => {
						SugarCube.Dialog.close();
					})));
				SugarCube.Dialog.open();
			}));
			App.RA.Activation.TreeEditor.build(args.activation, editorNode);
		} else {
			switchNode.append(App.UI.DOM.link("Switch to advanced mode", () => {
				args.advancedMode = true;
				$(outerNode).empty();
				fillOuterNode(args);
			}));
			App.RA.Activation.SimpleEditor.build(args.activation, editorNode);
		}
		switchNode.append(" / ", App.Encyclopedia.link("Help", "RA Condition Editor"));
		outerNode.append(editorNode);
	}

	/**
	 * Save the rule, if it is valid.
	 *
	 * @param {FC.RA.RuleConditionEditorArguments} args
	 */
	function saveEditor(args) {
		if (advanced) {
			let rule = App.RA.Activation.TreeEditor.save();
			if (rule == null) {
				return;
			}
			args.advancedMode = advanced;
			args.activation = rule;
		} else {
			let rule = App.RA.Activation.SimpleEditor.save();
			if (rule == null) {
				return;
			}
			args.advancedMode = advanced;
			args.activation = rule;
		}
	}

	function resetEditor() {
		App.RA.Activation.TreeEditor.reset();
		App.RA.Activation.SimpleEditor.reset();
		outerNode = null;
		advanced = false;
	}

	return {
		build: editor,
		save: saveEditor,
		reset: resetEditor,
		// Because of this reference we need to load after conditionEditorTree.js
		validateRule: App.RA.Activation.TreeEditor.validateRule,
	};
})();


code:
/**
 * @typedef {object} selectOption
 * @property {string} key
 * @property {string} name
 * @property {boolean} [enabled]
 */

/**
 * Creates a new dropdown with available and unavailable options.
 * @param {Array<selectOption>} options A list of options to display
 * @param {string} selected
 * @param {function(string):void} onchange
 *
 * @returns {HTMLSelectElement}
 */
App.UI.DOM.makeSelect = function(options, selected, onchange) {
	const select = document.createElement("select");
	let matchFound = false;

	for (const choice of options) {
		const option = document.createElement("option");

		option.text = choice.name;
		option.value = choice.key;

		if (selected === choice.key) {
			option.selected = true;
			matchFound = true;
		}

		if ("enabled" in choice && !choice.enabled) {
			option.disabled = true;
		}

		select.append(option);
	}

	if (!matchFound) {
		select.selectedIndex = -1;
	}

	select.onchange = () => onchange(select.value);

	return select;
};


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/**
 * replaces special HTML characters with their '&xxx' forms
 * @param {string} text
 * @returns {string}
 */
App.Utils.escapeHtml = function(text) {
	const map = {
		'&': '&',
		'<': '<',
		'>': '>',
		'"': '"',
		"'": '''
	};
	return text.replace(/[&<>"']/g, m => map[m]);
};

App.Utils.expandHTML = (function() {
	const element = document.createElement("div");

	/**
	 * @param {string} s
	 * @returns {string}
	 */
	function expand(s) {
		element.innerHTML = s;
		return element.textContent;
	}

	return expand;
})();

/**
 * Creates an object where the items are accessible via their ids.
 *
 * @param {Iterable} list
 * @returns {{}}
 */
function mapIdList(list) {
	let mappedList = {};
	for (const item of list) {
		mappedList[item.id] = item;
	}
	return mappedList;
}

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

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

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

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

	/** 2. topological sort with visitor
	 * @param {string} idstr
	 * @param {number[]} [ancestors]
	 */
	function visit(idstr, ancestors = []) {
		let node = nodes[idstr];
		let id = node.id;

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

		ancestors.push(id);

		visited[idstr] = true;

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

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

		sorted.unshift(id);
	}
	Object.keys(nodes).forEach(node => visit(node));

	return sorted;
};

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

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

/**
 * @param {object} target
 * @param {object} source
 */
function deepAssign(target, source) {
	_.merge(target, _.cloneDeep(source));
}

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

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

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

/**
 * Convert a Blob to a base64 string
 * @param {Blob} blob
 */
async function blobToBase64(blob) {
	return new Promise((resolve, reject) => {
		const reader = new FileReader();
		reader.onerror = (e) => reject(reader.error);
		reader.onloadend = (e) => {
			const dataUrl = /** @type {string} */ (reader.result);
			// remove mime type prefix from data url
			const base64 = dataUrl.substring(dataUrl.indexOf(',') + 1);
			resolve(base64);
		};
		reader.readAsDataURL(blob);
	});
}


code:
# Custom Drugs

To add custom drugs through modding, pass object(s) with the following properties to the `App.Mods.Drug.add` function. More than one drug can be added at a time by using multiple arguments.

## Custom Drug Object Properties

- `name` (String): The name of the drug, should be unique(however a PC and a slave drug under the same name will be fine) and not conflict with existing drugs. This is displayed in menus.
- `text` (String): The text used in the link to select the drug. Typically the first letter is in upper case.
- `type` (String): The category of the drug, used in the slave interaction menu. Can be an existing vanilla category(`Lips`, `Breasts`, `Nipples`, `Butt`, `Dick`/`Clit`(Will change automatically depending on slave genital type), `Fertility`, `Hormones`, `Psych`, `Misc`), or entirely new. Not required for PC drugs.
- `isPCDrug` (Boolean, optional): If the drug is for PC instead of slaves, default to `false`.
- `isConsumerGrade` (Boolean, optional): Lists the drug as consumer grade in manage personal affairs if true, other wise listed as slave grade, default to `false`. Only used is the drug is for PC.
- `available` (Function → *boolean*): If the drug should be displayed at all. The slave object will be passed as the first parameter.
- `enable` (Function → true or *string*): If the drug link should be enabled. The slave object will be passed as the first parameter. If the drug is allowed, return `true`. If not, return a string, which will be displayed to the player as a tool tip.
- `effect` (Function → *string* or *object*): The effect of the drug. The slave object will be passed as the first parameter. The returned string will be displayed in end week report.

## Example:
```js
App.Mods.Drugs.add(
	{
		name: "hip enhancer",
		text: "Hip enhancer",
		type: "Structural",
		available: function (slave) {
			return slave.indentureRestrictions < 2;
		},
		enable: function (slave) {
			//Disable if the slave's hip is already very wide or wider.
			if (slave.hips < 2) return true;
			else return "Hips are already too wide";
		},
		effect: function (slave) {
			//Get the pronouns of the slaves to be used in end week report.
			const { he, him, his, himself, He, His } = getPronouns(slave);
			if (slave.hips >= 2) {
				//If the slave's hips is already very wide, stop the drug regimen, notify in end week report.
				slave.drugs = "no drugs";
				return ` ${His} hips is already as wide is possible, <span class="yellow">${his} drug regimen has been ended.</span>`;
			} else {
				//If not, widen hips and notify in end week report.
				slave.hips++;
				return ` The hip enhancer <span class="lime">widens ${his} hip</span>.`;
			}
		}
	},
	/* More than one drug can be added at a time */
	{
		//Drug name should be unique, but two drug with identical name is okay if one is for PC and one is for slaves.
		name: "hip enhancer",
		text: "Hip enhancer",
		//PC Drugs do not require type
		isPCDrug: true,
		isConsumerGrade: true,
		available: () => true,
		enable: function (slave) {
			if (slave.hips < 2) return true;
			else return "Your hips are already too wide";
		},
		effect: function (slave) {
			if (slave.hips >= 2) {
				slave.drugs = "no drugs";
				return "Your hips is already as wide is possible, <span class=\"yellow\">your drug regimen has ended.</span>";
			} else {
				slave.hips++;
				return "The hip enhancer <span class=\"lime\">widens your hip</span>.";
			}
		}
	}
);
```

path: mods/custom drugs.md


code:
# Custom Events

To add a custom event(s) through modding, add `new App.Events.EVENT_NAME()` (following the file structure laid out within `devNotes\jsEventCreationGuide.md` is strongly recommended) to the applicable array.

## Example

`EVENT_NAME` is bigTitsPlease.
To add this as a new `random` `individual` event place `App.Mods.events.random.individual.push(new App.Events.bigTitsPlease())` within the `index.js` outlined as apart of `mods\mods.md`.

## Array list and notes

- `App.Mods.events.nonRandom` - hooks into the end of `App.Events.getNonrandomEvents` (`src\events\nonRandomEvent.js`) which has the following warning `// ORDER MATTERS - if multiple events from this list trigger in a single week, they are executed in this order`.

- `App.Mods.events.random.individual` - hooks into the end of `App.Events.getIndividualEvents` (`src\events\randomEvent.js`), `// instantiate all possible random individual events here`.

- `App.Mods.events.random.nonIndividual` - hooks into the end of `App.Events.getNonindividualEvents` (`src\events\randomEvent.js`), `// instantiate all possible random nonindividual events here` and `Note: recruitment events should NOT be added to this list; they go in getNonindividualRecruitmentEvents instead.`.

- `App.Mods.events.random.recruitment` - hooks into the end of `App.Events.getNonindividualRecruitmentEvents` (`src\events\randomEvent.js`).

- `App.Mods.events.recruit` - hooks into the end of `this.eventList` (`App.Events.RERecruit` - `src\events\reRecruit.js`).


code:
// Always scope your code to not pollute the global namespace.
{
	// Get the mod so we can modify it:
	const mod = App.Modding.currentMod;

	// Set the name and version:
	mod.name = "Example Mod";
	mod.version = "1.0";
	mod.description = "This is an example mod showing off the modding API and gives examples for moddable systems.";

	// For bigger mods it can be useful to split the mod in multiple parts.
	// These parts can then be loaded as subscripts.
	// Subscripts will be loaded in series and only once this script is done.
	mod.addSubscript("subscript");

	// Usage examples for moddable systems
	mod.addSubscript("rulesAssistantGetters");

	// Write to console, so we can easily see that the mod has loaded:
	console.log("Index Loading: Success!");

	// Tell the modding system this file is done. Do this at the end of EVERY script file.
	App.Modding.scriptDone();
}


code:
// Add a new boolean getter for use in the RA condition editor.
// The first argument is a unique name for the getter. To ensure uniqueness with other mods it is recommended to
// prefix the name with the mod name.
// The second argument is an object describing the getter.
// 'name' is the string shown in the RA condition editor when selecting.
// 'description' should be a short description of the value the getter returns, with possible values if applicable.
//      It will be displayed in the encyclopedia.
// 'val' is the actual getter reading out the value and returning it. It operates on a 'context' object with the current
//      slave as an attribute.
// It is important that the getter always returns the data type that it is being added as, in this case 'string'.
// 'addNumber()' and 'addBoolean()' also exist. The usage is the same.
App.RA.Activation.getterManager.addString("example_slavename",
	{name: "Slave Name", description: "The slave's name.", val: context => context.slave.slaveName}
);

console.log("Rule Assistant Getters loaded.");

App.Modding.scriptDone();


code:
// Show the subscript loaded successfully:
console.log("Subscript loaded!");

// Tell the modding system this file is done. Do this at the end of EVERY script file.
App.Modding.scriptDone();


code:
# Player Mods

Player mods are scripts that are loaded after loading the game itself.

There is currently very limited support for these mods with few systems exposing an API for modding.

To use the mods in this directory copy the `mods` directory directly next to the game HTML file like this:

* `FC_pregmod.html`
* `mods/`
    * `example/`
        * `index.js`
        * ...
    * ...

The example mod explains the modding API itself and has examples for all moddable systems.


free-cities

description: The Free Cities game

type: module

type: git

compile: sh compile.sh

build: npx gulp all

build:debug: npx gulp all --debug

build:release: npx gulp all --minify --release

serve: npx http-server --port 6969 -c-1

extra:all: node devTools/scripts/extraChecks.js

spell:all: node devTools/scripts/spellingChecks.js

lint:all: node devTools/scripts/eslintChecks.js


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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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


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


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


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


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


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


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


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


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


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

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

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


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


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


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


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


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

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

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


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


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


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


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


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


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


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


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


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


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


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


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

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="hair fore neat darkbrown.svg"
   inkscape:version="0.92.1 r15371"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1166"
	 inkscape:window-height="991"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="0.236"
	 inkscape:cx="-16.949153"
	 inkscape:cy="500"
	 inkscape:window-x="-8"
	 inkscape:window-y="0"
	 inkscape:window-maximized="0"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#3F4040;}
</style><path
	 id="Hair_Black"
	 class="st0"
	 d="M484.6,101.6c5.1,7.6,15,14,52.5,7.3c18.7,5.2,2.6,37.9,1.1,61c-1.3,19.3-7.8,36.4-18.9,51.5  c17.1-12.2,36.2-10.4,50.8-41.2c41.1-23.4-1.2-125.9-49.9-115.4C487.2,71.9,487.1,84.2,484.6,101.6z"
	 style="fill:#463325;fill-opacity:1" /></svg>

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


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


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


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


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


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


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


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


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


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

<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   id="Layer_1"
   x="0px"
   y="0px"
   viewBox="0 0 1000 1000"
   style="enable-background:new 0 0 1000 1000;"
   xml:space="preserve"
   sodipodi:docname="hair fore neat platinum.svg"
   inkscape:version="0.92.1 r15371"><metadata
	 id="metadata10"><rdf:RDF><cc:Work
		 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
		   rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
	 id="defs8" /><sodipodi:namedview
	 pagecolor="#ffffff"
	 bordercolor="#666666"
	 borderopacity="1"
	 objecttolerance="10"
	 gridtolerance="10"
	 guidetolerance="10"
	 inkscape:pageopacity="0"
	 inkscape:pageshadow="2"
	 inkscape:window-width="1166"
	 inkscape:window-height="991"
	 id="namedview6"
	 showgrid="false"
	 inkscape:zoom="0.236"
	 inkscape:cx="-777.54237"
	 inkscape:cy="500"
	 inkscape:window-x="-8"
	 inkscape:window-y="0"
	 inkscape:window-maximized="0"
	 inkscape:current-layer="Layer_1" /><style
	 type="text/css"
	 id="style2">
	.st0{fill:#3F4040;}
</style><path
	 id="Hair_Black"
	 class="st0"
	 d="M484.6,101.6c5.1,7.6,15,14,52.5,7.3c18.7,5.2,2.6,37.9,1.1,61c-1.3,19.3-7.8,36.4-18.9,51.5  c17.1-12.2,36.2-10.4,50.8-41.2c41.1-23.4-1.2-125.9-49.9-115.4C487.2,71.9,487.1,84.2,484.6,101.6z"
	 style="fill:#fcf3c1;fill-opacity:1" /></svg>

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


path: resources/vector/body/addon/hair fore neat purple.svg


path: resources/vector/body/addon/hair fore neat red.svg


path: resources/vector/body/addon/hair fore neat silver.svg


path: resources/vector/body/addon/hair fore neat strawberry-blonde.svg