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