diff --git a/devTools/types/FC/RA.d.ts b/devTools/types/FC/RA.d.ts index d9c75fff29ffea78e0b574b757ee501e71f33834..4552816cf06dc92914abb6a88d289ca20914f201 100644 --- a/devTools/types/FC/RA.d.ts +++ b/devTools/types/FC/RA.d.ts @@ -8,8 +8,12 @@ declare namespace FC { type NumericTarget = GenericNumericTarget<number>; type ExpressiveNumericTarget = GenericNumericTarget<number | string>; - interface RuleConditions { + interface RuleConditionEditorArguments { activation: PostFixRule; + advancedMode: boolean; + } + + interface RuleConditions extends RuleConditionEditorArguments{ selectedSlaves: number[]; excludedSlaves: number[]; applyRuleOnce: boolean; diff --git a/js/rulesAssistant/conditionEditorSimple.js b/js/rulesAssistant/conditionEditorSimple.js new file mode 100644 index 0000000000000000000000000000000000000000..0dd064745d83a4d77e11f0c6ebbde7184a18acc2 --- /dev/null +++ b/js/rulesAssistant/conditionEditorSimple.js @@ -0,0 +1,335 @@ +/** + * All functions should only be called from z1-conditionEditorController.js + */ +App.RA.Activation.SimpleEditor = (function() { + /** + * @typedef {object} RuleState + * @property {"always"|"never"|"boolean"|"number"|"string"} activeRuleType + * @property {string} boolGetter + * @property {boolean} negateBool + * @property {string} stringGetter + * @property {string} stringComparator + * @property {string} stringValue + * @property {string} numberGetter + * @property {number} numberUpperValue + * @property {"lt"|"lte"|""} numberUpperComparator + * @property {number} numberLowerValue + * @property {"gt"|"gte"|""} numberLowerComparator + */ + + /** + * @type {HTMLDivElement} + */ + let editorNode = null; + /** + * @type {RuleState} + */ + let currentRule = null; + + /** + * @param {FC.RA.PostFixRule} rule + * @param {HTMLDivElement}parent + */ + function editor(rule, parent) { + 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; + } + return serializeRule(currentRule); + } + + function resetEditor() { + currentRule = null; + editorNode = null; + } + + /** + * @returns {HTMLElement} + */ + function buildEditor() { + const outerDiv = document.createElement("div"); + // selector + const selectorDiv = document.createElement("div"); + selectorDiv.classList.add("button-group"); + outerDiv.append(selectorDiv); + App.UI.DOM.appendNewElement("button", selectorDiv, "Always", currentRule.activeRuleType === "always" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "always"; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", selectorDiv, "Never", currentRule.activeRuleType === "never" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "never"; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", selectorDiv, "Boolean", currentRule.activeRuleType === "boolean" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "boolean"; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", selectorDiv, "Number", currentRule.activeRuleType === "number" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "number"; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", selectorDiv, "String", currentRule.activeRuleType === "string" ? ["selected", "disabled"] : []).onclick = () => { + currentRule.activeRuleType = "string"; + refreshEditor(); + }; + + // add bool + if (currentRule.activeRuleType === "boolean") { + const boolDiv = document.createElement("div"); + boolDiv.classList.add("button-group"); + outerDiv.append(boolDiv); + /** + * @type {selectOption[]} + */ + const options = []; + App.RA.Activation.getterManager.booleanGetters.forEach((getter, key) => { + if (!getter.visible || getter.visible()) { + options.push({ + key: key, name: getter.name, enabled: !getter.enabled || getter.enabled() + }); + } + }); + boolDiv.append(App.UI.DOM.makeSelect(options, currentRule.boolGetter, key => { + currentRule.boolGetter = key; + refreshEditor(); + })); + boolDiv.append(" should be "); + App.UI.DOM.appendNewElement("button", boolDiv, "True", currentRule.negateBool ? [] : ["selected", "disabled"]).onclick = () => { + currentRule.negateBool = false; + refreshEditor(); + }; + App.UI.DOM.appendNewElement("button", boolDiv, "False", currentRule.negateBool ? ["selected", "disabled"] : []).onclick = () => { + currentRule.negateBool = true; + refreshEditor(); + }; + } else if (currentRule.activeRuleType === "number") { + const numberDiv = document.createElement("div"); + outerDiv.append(numberDiv); + /** + * @type {selectOption[]} + */ + const options = []; + App.RA.Activation.getterManager.numberGetters.forEach((getter, key) => { + if (!getter.visible || getter.visible()) { + options.push({ + key: key, name: getter.name, enabled: !getter.enabled || getter.enabled() + }); + } + }); + numberDiv.append(App.UI.DOM.makeSelect(options, currentRule.numberGetter, key => { + currentRule.numberGetter = key; + refreshEditor(); + })); + numberDiv.append(" should be "); + numberDiv.append(App.UI.DOM.makeSelect( + [{key: "gt", name: "greater than"}, {key: "gte", name: "greater than or equal to"}, + {key: "", name: "ignored"}], + currentRule.numberLowerComparator, key => { + currentRule.numberLowerComparator = key; + refreshEditor(); + })); + if (currentRule.numberLowerComparator !== "") { + numberDiv.append(" ", App.UI.DOM.makeTextBox(currentRule.numberLowerValue, val => { + currentRule.numberLowerValue = val; + }, true)); + } + numberDiv.append(" and "); + numberDiv.append(App.UI.DOM.makeSelect( + [{key: "lt", name: "less than"}, {key: "lte", name: "less than or equal to"}, + {key: "", name: "ignored"}], + currentRule.numberUpperComparator, key => { + currentRule.numberUpperComparator = key; + refreshEditor(); + })); + if (currentRule.numberUpperComparator !== "") { + numberDiv.append(" ", App.UI.DOM.makeTextBox(currentRule.numberUpperValue, val => { + currentRule.numberUpperValue = val; + }, true)); + } + } else if (currentRule.activeRuleType === "string") { + const stringDiv = document.createElement("div"); + outerDiv.append(stringDiv); + /** + * @type {selectOption[]} + */ + const options = []; + App.RA.Activation.getterManager.stringGetters.forEach((getter, key) => { + if (!getter.visible || getter.visible()) { + options.push({ + key: key, name: getter.name, enabled: !getter.enabled || getter.enabled() + }); + } + }); + stringDiv.append(App.UI.DOM.makeSelect(options, currentRule.stringGetter, key => { + currentRule.stringGetter = key; + refreshEditor(); + })); + stringDiv.append(" should "); + stringDiv.append(App.UI.DOM.makeSelect( + [{key: "eqstr", name: "equal"}, {key: "substr", name: "contain"}, {key: "match", name: "match"}], + currentRule.stringComparator, key => { + currentRule.stringComparator = key; + refreshEditor(); + })); + stringDiv.append(" ", App.UI.DOM.makeTextBox(currentRule.stringValue, val => { + currentRule.stringValue = val; + })); + } + + return outerDiv; + } + + /** + * @param {FC.RA.PostFixRule} rule + * @returns {RuleState} + */ + function deserializeRule(rule) { + // About the TS errors in this function: we can assume a lot about the rule composition because we know it's in + // the simple format. The rule itself is still a normal FC.RA.PostFixRule which would allow a lot more. + // Therefore, TS is not happy even though we now everything's fine. + /** + * @type {RuleState} + */ + const ruleState = { + activeRuleType: "always", + boolGetter: "isfertile", + negateBool: false, + stringGetter: "label", + stringValue: "", + stringComparator: "eqstr", + numberGetter: "devotion", + numberUpperValue: 100, + numberUpperComparator: "", + numberLowerValue: -100, + numberLowerComparator: "" + }; + // we know there is only one rule. + let i = 0; + const rulePart = rule[i]; + if (rulePart === true) { + ruleState.activeRuleType = "always"; + } else if (rulePart === false) { + ruleState.activeRuleType = "never"; + } else if (App.RA.Activation.getterManager.isBoolean(rulePart)) { + ruleState.activeRuleType = "boolean"; + ruleState.boolGetter = rulePart; + if (rule[i + 1] === "not") { + ruleState.negateBool = true; + i++; + } + } else if (App.RA.Activation.getterManager.isNumber(rulePart)) { + ruleState.activeRuleType = "number"; + ruleState.numberGetter = rulePart; + // check if there is a lower rule: + if (rule[i + 2].startsWith("g")) { + ruleState.numberLowerValue = rule[i + 1]; + ruleState.numberLowerComparator = rule[i + 2]; + // check if there is also an upper value: + if (rule[i + 3] === ruleState.numberGetter) { + ruleState.numberUpperValue = rule[i + 4]; + ruleState.numberUpperComparator = rule[i + 5]; + i += 3; + } + } else { + ruleState.numberUpperValue = rule[i + 1]; + ruleState.numberUpperComparator = rule[i + 2]; + } + i += 2; + } else if (App.RA.Activation.getterManager.isString(rulePart)) { + ruleState.activeRuleType = "string"; + ruleState.stringGetter = rulePart; + ruleState.stringValue = rule[i + 1].slice(1); + ruleState.stringComparator = rule[i + 2]; + i += 2; + } else { + throw new Error("Rule is not in simple mode format!"); + } + i++; + + return ruleState; + } + + /** + * Expects a valid RulePart structure + * + * @param {RuleState} ruleState + * @returns {FC.RA.PostFixRule} + */ + function serializeRule(ruleState) { + /** + * @type {FC.RA.PostFixRule} + */ + const rule = []; + let counter = 0; + + switch (ruleState.activeRuleType) { + case "always": + rule.push(true); + counter++; + break; + case "never": + rule.push(false); + counter++; + break; + case "boolean": + rule.push(ruleState.boolGetter); + if (ruleState.negateBool) { + rule.push("not"); + } + counter++; + break; + case "number": + // eslint-disable-next-line no-case-declarations + let any = false; + if (ruleState.numberLowerComparator !== "") { + any = true; + rule.push(ruleState.numberGetter); + rule.push(ruleState.numberLowerValue); + rule.push(ruleState.numberLowerComparator); + counter++; + } + if (ruleState.numberUpperComparator !== "") { + any = true; + rule.push(ruleState.numberGetter); + rule.push(ruleState.numberUpperValue); + rule.push(ruleState.numberUpperComparator); + counter++; + } + if (!any) { + rule.push(true); + counter++; + } + break; + case "string": + rule.push(ruleState.stringGetter); + rule.push("!"+ruleState.stringValue); + rule.push(ruleState.stringComparator); + counter++; + break; + } + rule.push(counter, "and"); + return rule; + } + + return { + build: editor, + save: saveEditor, + reset: resetEditor, + }; +})(); diff --git a/js/rulesAssistant/conditionEditor.js b/js/rulesAssistant/conditionEditorTree.js similarity index 98% rename from js/rulesAssistant/conditionEditor.js rename to js/rulesAssistant/conditionEditorTree.js index e6f4ec6be0004e6857e6223bd5ec849d95752fb4..87b8831b4c84ac0ce7038e4de05b276dee6b1f02 100644 --- a/js/rulesAssistant/conditionEditor.js +++ b/js/rulesAssistant/conditionEditorTree.js @@ -1,4 +1,7 @@ -App.RA.Activation.Editor = (function() { +/** + * All functions should only be called from z1-conditionEditorController.js + */ +App.RA.Activation.TreeEditor = (function() { /** * @type {HTMLDivElement} */ @@ -14,14 +17,13 @@ App.RA.Activation.Editor = (function() { /** * @param {FC.RA.PostFixRule} rule - * @returns {HTMLDivElement} + * @param {HTMLDivElement}parent */ - function editor(rule) { + function editor(rule, parent) { rulePartMap = new Map(); currentRule = deserializeRule(rule); - editorNode = document.createElement("div"); + editorNode = parent; editorNode.append(buildEditor()); - return editorNode; } function refreshEditor() { @@ -31,18 +33,19 @@ App.RA.Activation.Editor = (function() { } /** - * Save the rule, if it is valid. + * If the rule is valid, returns the serialized rule, otherwise null. * - * @param {(rule:FC.RA.PostFixRule)=>void} callback + * @returns {FC.RA.PostFixRule} */ - function saveEditor(callback) { + function saveEditor() { if (currentRule == null) { - return; + return null; } const error = currentRule.validate([]) === "error"; if (!error) { - callback(serializeRule(currentRule)); + return serializeRule(currentRule); } + return null; } function resetEditor() { diff --git a/js/rulesAssistant/z1-conditionEditorController.js b/js/rulesAssistant/z1-conditionEditorController.js new file mode 100644 index 0000000000000000000000000000000000000000..a45aa13f4624221969686df21c0af27483cf14bb --- /dev/null +++ b/js/rulesAssistant/z1-conditionEditorController.js @@ -0,0 +1,98 @@ +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"); + if (advanced) { + outerNode.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( + "<p>Resetting will delete your current conditions. Do you want to continue?</p>", + 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 { + outerNode.append(App.UI.DOM.link("Switch to advanced mode", () => { + args.advancedMode = true; + $(outerNode).empty(); + fillOuterNode(args); + })); + App.RA.Activation.SimpleEditor.build(args.activation, editorNode); + } + 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, + }; +})(); diff --git a/src/data/backwardsCompatibility/datatypeCleanup.js b/src/data/backwardsCompatibility/datatypeCleanup.js index af14361979dfaa92ceca64a05095bf32b9010935..6c747999a1efdcf08bf950f35cb13c77f66a18a0 100644 --- a/src/data/backwardsCompatibility/datatypeCleanup.js +++ b/src/data/backwardsCompatibility/datatypeCleanup.js @@ -2305,6 +2305,10 @@ App.Entity.Utils.RARuleDatatypeCleanup = function() { }; return jobs[assignment]; } + + if (!cond.hasOwnProperty("advancedMode")) { + cond.advancedMode = true; + } } /** @param {object} o */ diff --git a/src/js/rulesAssistant.js b/src/js/rulesAssistant.js index 66be45512f37b12852935668bed9da7d7b17b256..c247619be5f65642ac3fad48be8fef87ac24ab7f 100644 --- a/src/js/rulesAssistant.js +++ b/src/js/rulesAssistant.js @@ -178,6 +178,7 @@ App.RA.newRule = function() { function emptyConditions() { return { activation: ["devotion", 20, "gt", 1, "and"], + advancedMode: false, selectedSlaves: [], excludedSlaves: [], applyRuleOnce: false, diff --git a/src/js/rulesAssistantOptions.js b/src/js/rulesAssistantOptions.js index d47e17530bd85da037165d1c16ba5bef4abd7997..ebc47642f3788a91202122c1cd5244b7f69d6fca 100644 --- a/src/js/rulesAssistantOptions.js +++ b/src/js/rulesAssistantOptions.js @@ -105,7 +105,7 @@ App.RA.options = (function() { * Save the settings for this rule. */ function saveSettings() { - App.RA.Activation.Editor.save(cond => current_rule.condition.activation = cond); + App.RA.Activation.Editor.save(current_rule.condition); } const parse = { @@ -1294,7 +1294,7 @@ App.RA.options = (function() { class ConditionBuilder extends Element { render() { - return App.RA.Activation.Editor.build(current_rule.condition.activation); + return App.RA.Activation.Editor.build(current_rule.condition); } }