From 16b7023bbe7812b8525cd51b4eccbc5b08cc42ed Mon Sep 17 00:00:00 2001
From: Arkerthan <arkerthan@mailbox.org>
Date: Mon, 17 Jan 2022 14:55:54 +0100
Subject: [PATCH] add numeric types

---
 css/rulesAssistant/activationConditions.css |   6 +
 devTools/types/FC/RA.d.ts                   |  17 +-
 src/js/rulesAssistantActivationCondition.js | 337 +++++++++++++++++---
 3 files changed, 315 insertions(+), 45 deletions(-)

diff --git a/css/rulesAssistant/activationConditions.css b/css/rulesAssistant/activationConditions.css
index 62dc010f9d2..0c7a4217e2f 100644
--- a/css/rulesAssistant/activationConditions.css
+++ b/css/rulesAssistant/activationConditions.css
@@ -8,3 +8,9 @@
     border: red 5px solid;
     cursor: grab;
 }
+.rule-drop-location {
+    display: inline-block;
+    background-color: orange;
+    min-width: 5em;
+    min-height: 1em;
+}
diff --git a/devTools/types/FC/RA.d.ts b/devTools/types/FC/RA.d.ts
index 1816bfdbefc..ed1a6de959e 100644
--- a/devTools/types/FC/RA.d.ts
+++ b/devTools/types/FC/RA.d.ts
@@ -187,18 +187,25 @@ declare namespace FC {
 			set: RuleSetters;
 		}
 
-		type RulePart = RuleGroup | RuleNegation | RuleCondition;
+		type RulePart = RuleGroup | RulePair | RuleNegation | RuleCondition;
 
 		interface RuleGroup {
-			type: "and" | "or"
+			type: "and" | "or" | "+" | "*"
 			parts: RulePart[]
 		}
 
+		interface RulePair {
+			type: "-" | "/" | "<" | "<=" | ">=" | ">" | "==" | "!="
+			part1: RulePart
+			part2: RulePart
+		}
+
 		interface RuleNegation {
 			type: "negate"
+			part: RulePart
 		}
 
-		type RuleCondition = RuleConstant | RuleCustom | RuleBool;
+		type RuleCondition = RuleConstant | RuleCustom | RuleMapCheck;
 
 		interface RuleConstant {
 			type: "constant"
@@ -210,8 +217,8 @@ declare namespace FC {
 			value: string
 		}
 
-		interface RuleBool {
-			type: "bool"
+		interface RuleMapCheck {
+			type: "bool" | "number"
 			key: string
 		}
 	}
diff --git a/src/js/rulesAssistantActivationCondition.js b/src/js/rulesAssistantActivationCondition.js
index 8fda389f00d..b8142613840 100644
--- a/src/js/rulesAssistantActivationCondition.js
+++ b/src/js/rulesAssistantActivationCondition.js
@@ -1,5 +1,13 @@
 App.RA.Activation = {};
 
+/**
+ * TODO add encyclopedia entry
+ *
+ * Numeric and Boolean checks can be used interchangeably.
+ * Putting a Numeric value into a boolean aggregator will interpret 0 as false and all other values as true.
+ * Putting a Boolean value into a numeric aggregator will interpret false as 0 and true as 1.
+ */
+
 App.RA.Activation = (function() {
 	/**
 	 * @type {HTMLDivElement}
@@ -22,6 +30,8 @@ App.RA.Activation = (function() {
 	function editor(rule) {
 		// TODO do this once on story ready
 		boolPopulate();
+		numericPopulate();
+
 		rulePartMap = new Map();
 		nextRuleId = 0;
 		currentRule = ruleFactory(testRule);
@@ -62,7 +72,11 @@ App.RA.Activation = (function() {
 				]
 			},
 			{type: "bool", key: "isfertile"},
-			{type: "constant", value: true}
+			{
+				type: ">",
+				part1: {type: "number", key: "devotion"},
+				part2: {type: "number", key: "trust"},
+			},
 		]
 	};
 
@@ -112,25 +126,127 @@ App.RA.Activation = (function() {
 
 		/**
 		 * Check if this rule applies to the given slave
+		 * 0    : no
+		 * other: yes
 		 *
 		 * @param {App.Entity.SlaveState} slave
-		 * @returns {boolean}
+		 * @returns {number}
 		 */
 		applies(slave) {
-			return false;
+			return 0;
 		}
 	}
 
 	class RuleContainer extends RulePart {
-		constructor() {
+		/**
+		 * @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 {RulePart} parent
+		 * @param {RuleContainer} maybeChild
+		 * @returns {boolean}
+		 * @protected
+		 */
+		_isSameOrParent(parent, maybeChild) {
+			if (parent === maybeChild) {
+				return true;
+			} else if ((parent instanceof RuleContainer) &&
+				parent.isParent(maybeChild)) {
+				return true;
+			}
+			return false;
+		}
+	}
+
+	class RuleGroup extends RuleContainer {
+		/**
+		 * @param {"and"|"or"|"+"|"*"} mode
+		 */
+		constructor(mode) {
 			super();
+			this.mode = mode;
 			/**
 			 * @type {RulePart[]}
-			 * @protected
+			 * @private
 			 */
 			this._children = [];
 		}
 
+		render() {
+			const div = document.createElement("div");
+			div.classList.add("rule-group");
+			div.append(this.mode);
+			for (const rulePart of this._children) {
+				div.append(rulePart.render());
+			}
+			div.ondragover = ev => {
+				if (canDrop(ev, this)) {
+					// 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);
+				this.addChild(rulePart);
+				refreshEditor();
+			};
+			if (this.parent !== null) {
+				// if null, it's the outermost and that can't be draggable
+				makeDraggable(div, this);
+			}
+			return div;
+		}
+
+		validate() {
+			if (this._children.length === 0) {
+				return false;
+			}
+			return this._children.reduce((c, rulePart) => c && rulePart.validate(), true);
+		}
+
+		applies(slave) {
+			let agr = this._aggregator();
+			return this._children.reduce((c, rulePart) => agr(c, rulePart.applies(slave)), 1);
+		}
+
+		/**
+		 * @returns {function(number, number):number}
+		 * @private
+		 */
+		_aggregator() {
+			switch (this.mode) {
+				case "and":
+					return (a, b) => (a !== 0 && b !== 0) ? 1 : 0;
+				case "or":
+					return (a, b) => (a !== 0 || b !== 0) ? 1 : 0;
+				case "+":
+					return (a, b) => a + b;
+				case "*":
+					return (a, b) => a * b;
+			}
+		}
+
+
 		/**
 		 * @param {RulePart} rulePart
 		 */
@@ -143,6 +259,7 @@ App.RA.Activation = (function() {
 		}
 
 		/**
+		 * @override
 		 * @param {RulePart} rulePart
 		 */
 		removeChild(rulePart) {
@@ -151,14 +268,12 @@ App.RA.Activation = (function() {
 		}
 
 		/**
+		 * @override
 		 * @param {RuleContainer} maybeChild
 		 */
 		isParent(maybeChild) {
 			for (const child of this._children) {
-				if (child === maybeChild) {
-					return true;
-				} else if ((child instanceof RuleContainer) &&
-					child.isParent(maybeChild)) {
+				if (this._isSameOrParent(child, maybeChild)) {
 					return true;
 				}
 			}
@@ -173,23 +288,37 @@ App.RA.Activation = (function() {
 		}
 	}
 
-	class RuleGroup extends RuleContainer {
+	class RulePair extends RuleContainer {
 		/**
-		 * @param {"and"|"or"} mode
+		 * @param {"-" | "/" | "<" | "<=" | ">=" | ">" | "==" | "!="} 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-group");
-			div.append(this.mode);
-			for (const rulePart of this._children) {
-				div.append(rulePart.render());
+			if (this.parent !== null) {
+				// if null, it's the outermost and that can't be draggable
+				makeDraggable(div, this);
 			}
-			div.ondragover = ev => {
+			// element 1
+			let span = document.createElement("span");
+			span.classList.add("rule-drop-location");
+			if (this._child1 != null) {
+				span.append(this._child1.render());
+			}
+			span.ondragover = ev => {
 				if (canDrop(ev, this)) {
 					// show that it can be dropped
 					ev.preventDefault();
@@ -197,38 +326,123 @@ App.RA.Activation = (function() {
 				// stop groups further out from capturing the event.
 				ev.stopPropagation();
 			};
-			div.ondrop = ev => {
+			span.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);
-				this.addChild(rulePart);
+				this.child1 = rulePartMap.get(rulePartID);
 				refreshEditor();
 			};
-			if (this.parent !== null) {
-				// if null, it's the outermost and that can't be draggable
-				makeDraggable(div, this);
+			div.append(span);
+			// operator
+			div.append(this.mode);
+			// element 2
+			span = document.createElement("span");
+			span.classList.add("rule-drop-location");
+			if (this._child2 != null) {
+				span.append(this._child2.render());
 			}
+			span.ondragover = ev => {
+				if (canDrop(ev, this)) {
+					// show that it can be dropped
+					ev.preventDefault();
+				}
+				// stop groups further out from capturing the event.
+				ev.stopPropagation();
+			};
+			span.ondrop = ev => {
+				ev.preventDefault();
+				// stop groups further out from capturing the event.
+				ev.stopPropagation();
+				const rulePartID = ev.dataTransfer.getData("text/plain");
+				this.child2 = rulePartMap.get(rulePartID);
+				refreshEditor();
+			};
+			div.append(span);
 			return div;
 		}
 
 		validate() {
-			if (this._children.length === 0) {
+			if (this._child1 == null || this._child2 == null) {
 				return false;
 			}
-			return this._children.reduce((c, rulePart) => c && rulePart.validate(), true);
+			return this._child1.validate() && this._child2.validate();
 		}
 
 		applies(slave) {
-			/**
-			 * @type {function(boolean, boolean):boolean}
-			 */
-			const cmp = this.mode === "and"
-				? (a, b) => a && b
-				: (a, b) => a || b;
+			return this._comparator(this._child1.applies(slave), this._child2.applies(slave));
+		}
 
-			return this._children.reduce((c, rulePart) => cmp(c, rulePart.applies(slave)), true);
+		/**
+		 * @param {number} a
+		 * @param {number} b
+		 * @returns {number}
+		 * @private
+		 */
+		_comparator(a, b) {
+			switch (this.mode) {
+				case "-":
+					return a - b;
+				case "/":
+					return a / b;
+				case "<":
+					return a < b ? 1 : 0;
+				case "<=":
+					return a <= b ? 1 : 0;
+				case ">":
+					return a > b ? 1 : 0;
+				case ">=":
+					return a >= b ? 1 : 0;
+				case "==":
+					return a === b ? 1 : 0;
+				case "!=":
+					return a !== b ? 1 : 0;
+			}
+		}
+
+		/**
+		 * @param {RulePart} child
+		 */
+		set child1(child) {
+			if (child.parent != null) {
+				child.parent.removeChild(child);
+			}
+			child.parent = this;
+			this._child1 = child;
+		}
+
+		/**
+		 * @param {RulePart} child
+		 */
+		set child2(child) {
+			if (child.parent != null) {
+				child.parent.removeChild(child);
+			}
+			child.parent = this;
+			this._child2 = child;
+		}
+
+		/**
+		 * @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 this._isSameOrParent(this._child1, maybeChild) ||
+				this._isSameOrParent(this._child2, maybeChild);
 		}
 	}
 
@@ -252,17 +466,18 @@ App.RA.Activation = (function() {
 		}
 
 		applies(slave) {
-			return this.mode;
+			return this.mode ? 1 : 0;
 		}
 	}
 
-
-	class RuleBoolCheck extends RulePart {
+	class RuleMapCheck extends RulePart {
 		/**
+		 * @param {"bool"|"number"} mode
 		 * @param {string} key
 		 */
-		constructor(key) {
+		constructor(mode, key) {
 			super();
+			this.mode = mode;
 			this.key = key;
 		}
 
@@ -276,7 +491,7 @@ App.RA.Activation = (function() {
 			let matchFound = false;
 			let select = document.createElement("select");
 
-			for (const [key, value] of boolCheckMap.entries()) {
+			for (const [key, value] of this._map.entries()) {
 				let el = document.createElement("option");
 				el.value = key;
 				el.textContent = value.name;
@@ -301,11 +516,23 @@ App.RA.Activation = (function() {
 		}
 
 		validate() {
-			return boolCheckMap.has(this.key);
+			return this._map.has(this.key);
 		}
 
 		applies(slave) {
-			return boolCheckMap.get(this.key).cond(slave);
+			if (this.mode === "bool") {
+				return boolCheckMap.get(this.key).cond(slave) ? 1 : 0;
+			} else {
+				return numericMap.get(this.key).val(slave);
+			}
+		}
+
+		/**
+		 * @returns {Map<string, {name: string}>}
+		 * @private
+		 */
+		get _map() {
+			return this.mode === "bool" ? boolCheckMap : numericMap;
 		}
 	}
 
@@ -355,6 +582,19 @@ App.RA.Activation = (function() {
 		boolCheckMap.set("isamputee", {name: "Is Amputee?", cond: isAmputee});
 	}
 
+	/**
+	 * TODO: encapsulate for outside access
+	 * TODO: add availability check
+	 *
+	 * @type {Map<string, {name: string, val: (function(FC.SlaveState): number)}>}
+	 */
+	const numericMap = new Map();
+
+	function numericPopulate() {
+		numericMap.set("devotion", {name: "Devotion", val: s => s.devotion});
+		numericMap.set("trust", {name: "Trust", val: s => s.trust});
+	}
+
 	/**
 	 * @param {FC.RA.RulePart} rulePart
 	 * @returns {RulePart}
@@ -364,15 +604,30 @@ App.RA.Activation = (function() {
 		switch (rulePart.type) {
 			case "and":
 			case "or":
+			case "+":
+			case "*":
 				rule = new RuleGroup(rulePart.type);
 				for (const ruleChild of rulePart.parts) {
 					rule.addChild(ruleFactory(ruleChild));
 				}
 				return rule;
+			case "-":
+			case "/":
+			case "<":
+			case "<=":
+			case ">":
+			case ">=":
+			case "!=":
+			case "==":
+				rule = new RulePair(rulePart.type);
+				rule.child1 = ruleFactory(rulePart.part1);
+				rule.child2 = ruleFactory(rulePart.part2);
+				return rule;
 			case "constant":
 				return new RuleConstant(rulePart.value);
 			case "bool":
-				return new RuleBoolCheck(rulePart.key);
+			case "number":
+				return new RuleMapCheck(rulePart.type, rulePart.key);
 		}
 	}
 
@@ -387,10 +642,12 @@ App.RA.Activation = (function() {
 				obj.parts.push(serializeRule(ruleChild));
 			}
 			return obj;
+		} else if (rulePart instanceof RulePair) {
+			return {type: rulePart.mode, part1: serializeRule(rulePart.child1), part2: serializeRule(rulePart.child2)};
 		} else if (rulePart instanceof RuleConstant) {
 			return {type: "constant", value: rulePart.mode};
-		} else if (rulePart instanceof RuleBoolCheck) {
-			return {type: "bool", key: rulePart.key};
+		} else if (rulePart instanceof RuleMapCheck) {
+			return {type: rulePart.mode, key: rulePart.key};
 		}
 	}
 
-- 
GitLab