diff --git a/css/general/layout.css b/css/general/layout.css index bb4e6b735f624bbd8da265a606f032626f19f57e..877cf5d1015c1a6a5190e8868800b94b8ac93e54 100644 --- a/css/general/layout.css +++ b/css/general/layout.css @@ -127,3 +127,8 @@ div.cheat-menu { position: absolute; right: 50px; } + +input[type="text"].number { + min-width: 6em; + max-width: 6em; +} diff --git a/css/rulesAssistant/conditionEditor.css b/css/rulesAssistant/conditionEditor.css new file mode 100644 index 0000000000000000000000000000000000000000..3ab3b7cfc3b11ec8b829111b326d7dfa61541346 --- /dev/null +++ b/css/rulesAssistant/conditionEditor.css @@ -0,0 +1,116 @@ +@media only screen and (min-width: 1000px) { + .rule-builder { + display: grid; + grid-template-columns: 60% 40%; + grid-column-gap: 1em; + } +} + +.rule-builder button { + color: var(--link-color); + background-color: var(--button-color); + border: solid 2px var(--button-border-color); + border-radius: 4px; + margin-left: 4px; +} + +.rule-builder button:hover { + background-color: var(--button-hover-color); + color: var(--link-hover-color); +} + +.rule-part-browser { +} + +.rule-part { + display: inline flow-root; + border-radius: 8px; + border: 2px solid #333333; + background-color: #1a1a1a; + padding: 4px; + margin: 4px; +} + +.validation-error::before { + font-family: "tme-fa-icons"; + content: "\e80d"; + color: red; + margin-right: 4px; +} + +.rule-draggable { + cursor: grab; +} + +.rule-drag-element { + display: inline flow-root; + background-image: repeating-linear-gradient(0, #1a1a1a, transparent 0.2em, transparent 0.2em, #1a1a1a 0.4em), + repeating-linear-gradient(90deg, #1a1a1a, #777 0.2em, #777 0.2em, #1a1a1a 0.4em); + width: 1.6em; + height: 2em; + vertical-align: middle; +} + +.rule-drop-location { + border: #999 dashed 1px; + border-radius: 4px; + display: inline flow-root; + background-color: #555; + width: 5em; + height: 1.5em; + vertical-align: middle; + margin: 0 4px +} + + +.rule-builder input[type="text"] { + margin-left: 0.2em; +} + +.rule-right-margin { + margin-right: 0.2em; +} + +.rule-left-margin { + margin-left: 0.2em; +} + +.rule-trash { + border: #F55 1px; + border-radius: 4px; + background-color: palevioletred; + width: 12em; + height: 2em; + text-align: center; + margin: 4px; + color: black; +} + +.rule-trash::before { + font-family: "tme-fa-icons"; + content: "\e828"; +} + +/* Encyclopedia help entry */ + +.rule-help-table { + display: grid; + grid-template-columns: max-content auto auto; +} + +.rule-help-table .head { + font-weight: bold; + position: sticky; + top: -1em; + background-color: #111; +} + +.rule-help-table > div { + padding: 0 0.5em +} + +.rule-help-table > :nth-child(6n+4), +.rule-help-table > :nth-child(6n+5), +.rule-help-table > :nth-child(6n+6) { + background-color: #222; +} diff --git a/devTools/types/FC/RA.d.ts b/devTools/types/FC/RA.d.ts index a771bac04b26912b7ce2018e7672581060b64bab..d9c75fff29ffea78e0b574b757ee501e71f33834 100644 --- a/devTools/types/FC/RA.d.ts +++ b/devTools/types/FC/RA.d.ts @@ -9,9 +9,7 @@ declare namespace FC { type ExpressiveNumericTarget = GenericNumericTarget<number | string>; interface RuleConditions { - function: boolean | string; - data: any; - assignment: Assignment[]; + activation: PostFixRule; selectedSlaves: number[]; excludedSlaves: number[]; applyRuleOnce: boolean; @@ -175,7 +173,7 @@ declare namespace FC { scarDesign: string; hornColor: string; labelTagsClear: boolean; - choosesOwnClothes: 0|1; + choosesOwnClothes: 0 | 1; pronoun: number; } @@ -185,5 +183,7 @@ declare namespace FC { condition: RuleConditions; set: RuleSetters; } + + type PostFixRule = Array<string | number | boolean> } } diff --git a/js/002-config/fc-js-init.js b/js/002-config/fc-js-init.js index 19f9622b9982a3b0ca38ffcad1a9503e0b88e761..07aae1b15b699bd263856df1ab215e35658b660a 100644 --- a/js/002-config/fc-js-init.js +++ b/js/002-config/fc-js-init.js @@ -68,6 +68,7 @@ App.Medicine.Surgery = {}; App.Medicine.Surgery.Procedures = {}; App.Medicine.Surgery.Reactions = {}; App.RA = {}; +App.RA.Activation = {}; App.Ratings = {}; App.SF = {}; App.SecExp = {}; diff --git a/js/rulesAssistant/01-stack.js b/js/rulesAssistant/01-stack.js new file mode 100644 index 0000000000000000000000000000000000000000..b0924afc86c6eac6a45b5686edfea7041d96ad86 --- /dev/null +++ b/js/rulesAssistant/01-stack.js @@ -0,0 +1,56 @@ +App.RA.Activation.Stack = class Stack { + constructor() { + /** + * @private + * @type {number[]} + */ + this._numbers = []; + /** + * @private + * @type {string[]} + */ + this._strings = []; + } + + /** + * @param {boolean} v + */ + pushBoolean(v) { + this._numbers.push(v ? 1 : 0); + } + + /** + * @returns {FC.Bool} + */ + popAsBoolean() { + return this._numbers.pop() ? 1 : 0; + } + + /** + * @param {number} v + */ + pushNumber(v) { + this._numbers.push(v); + } + + /** + * @returns {number} + */ + popNumber() { + return this._numbers.pop(); + } + + /** + * @param {string} v + */ + pushString(v) { + this._strings.push(v); + } + + /** + * @returns {string} + */ + popString() { + return this._strings.pop(); + } +}; diff --git a/js/rulesAssistant/conditionEditor.js b/js/rulesAssistant/conditionEditor.js new file mode 100644 index 0000000000000000000000000000000000000000..580105e2bd20c1e4fe761662c93f3f543b2c28e8 --- /dev/null +++ b/js/rulesAssistant/conditionEditor.js @@ -0,0 +1,1336 @@ +App.RA.Activation.Editor = (function() { + /** + * @type {HTMLDivElement} + */ + let editorNode = null; + /** + * @type {Map<string, RulePart>} + */ + let rulePartMap = null; + /** + * @type {RuleGroup} + */ + let currentRule = null; + + /** + * @param {FC.RA.PostFixRule} rule + * @returns {HTMLDivElement} + */ + function editor(rule) { + rulePartMap = new Map(); + currentRule = deserializeRule(rule); + editorNode = document.createElement("div"); + editorNode.append(buildEditor()); + return editorNode; + } + + function refreshEditor() { + if (editorNode !== null) { + $(editorNode).empty().append(buildEditor()); + } + } + + /** + * Save the rule, if it is valid. + * + * @param {(rule:FC.RA.PostFixRule)=>void} callback + */ + function saveEditor(callback) { + const error = currentRule.validate([]) === "error"; + if (!error) { + callback(serializeRule(currentRule)); + } + } + + function resetEditor() { + rulePartMap = null; + currentRule = null; + editorNode = null; + } + + /** + * @returns {HTMLElement} + */ + function buildEditor() { + const outerDiv = document.createElement("div"); + outerDiv.classList.add("rule-builder"); + + const ruleDiv = document.createElement("div"); + const errors = []; + if (currentRule.validate(errors) === "error") { + ruleDiv.append("Condition has errors:"); + for (const error of errors) { + ruleDiv.append(" ", error); + } + ruleDiv.append(" Changes have NOT been saved!"); + } else { + ruleDiv.append("Condition saved."); + } + ruleDiv.append(" ", App.Encyclopedia.Dialog.linkDOM("Help", "RA Condition Editor")); + ruleDiv.append(currentRule.render()); + outerDiv.append(ruleDiv); + + outerDiv.append(buildPartBrowser()); + + return outerDiv; + } + + /** + * @returns {HTMLDivElement} + */ + function buildPartBrowser() { + const container = document.createElement("div"); + App.UI.DOM.appendNewElement("h3", container, "Part Browser"); + const div = document.createElement("div"); + div.classList.add("rule-part-browser"); + div.append(new RulePartProvider(() => new RuleGroup("and")).render()); + div.append(new RulePartProvider(() => new RuleGroup("add")).render()); + div.append(new RulePartProvider(() => new RuleNegate()).render()); + div.append(new RulePartProvider(() => new RulePair("eq")).render()); + div.append(new RulePartProvider(() => new RuleTernary()).render()); + div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.booleanDefault)).render()); + div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.numberDefault)).render()); + div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.stringDefault)).render()); + div.append(new RulePartProvider(() => new RuleMapCheck(App.RA.Activation.getterManager.assignmentDefault)).render()); + div.append(new RulePartProvider(() => new RuleConstant(0)).render()); + div.append(new RulePartProvider(() => new RuleBooleanConstant(true)).render()); + div.append(new RulePartProvider(() => new RuleConstant("string")).render()); + div.append(new RulePartProvider(() => new RuleCustomCheck("bcontext => false")).render()); + div.append(new RulePartTrash().render()); + container.append(div); + return container; + } + + /** + * @abstract + */ + class RulePart { + constructor() { + this.id = generateNewID(); + rulePartMap.set(this.id, this); + + /** + * @type {?RuleContainer} + */ + this.parent = null; + + /** + * @type {HTMLElement} + * @protected + */ + this._dragElement = null; + + /** + * @type {boolean} + * @private + */ + this._showValidationError = true; + } + + /** + * @abstract + * @returns {HTMLElement} + */ + render() { + throw new Error("Method 'render()' must be implemented."); + } + + /** + * Validate the rule, gives the expected return type or "error". + * @abstract + * @param {Array<string>} errorList + * @returns {"number"|"string"|"error"} + */ + validate(errorList) { + throw new Error("Method 'validate()' must be implemented."); + } + + /** + * Makes the element not draggable + */ + disableDragging() { + if (this._dragElement != null) { + this._dragElement.draggable = false; + } + if (this.parent != null) { + this.parent.disableDragging(); + } + } + + /** + * Makes the element draggable again. + */ + enableDragging() { + if (this.parent != null) { + if (this._dragElement != null) { + this._dragElement.draggable = true; + } + this.parent.enableDragging(); + } + } + + /** + * @protected + * @param {HTMLElement} element + */ + _markValidationError(element) { + if (this._showValidationError && this.validate([]) === "error") { + element.classList.add("validation-error"); + } + } + + /** + * @param {boolean} value + */ + set showValidationError(value) { + this._showValidationError = value; + } + } + + /** + * @abstract + */ + class RuleContainer extends RulePart { + /** + * @abstract + * @param {RulePart} rulePart + */ + removeChild(rulePart) { + throw new Error("Method 'removeChild()' must be implemented."); + } + + /** + * @abstract + * @param {RuleContainer} maybeChild + * @returns {boolean} + */ + isParent(maybeChild) { + throw new Error("Method 'isParent()' must be implemented."); + } + + /** + * @param {HTMLElement} element + * @param {(child:RulePart)=>void} setChild + * @protected + */ + _allowDroppingOn(element, setChild) { + element.ondragover = ev => { + if (canDrop(ev, this)) { + // show that it can be dropped + ev.preventDefault(); + } + // stop containers further out from capturing the event. + ev.stopPropagation(); + }; + element.ondrop = ev => { + ev.preventDefault(); + // stop groups further out from capturing the event. + ev.stopPropagation(); + const rulePart = rulePartMap.get(ev.dataTransfer.getData("text/plain")); + setChild(rulePart); + refreshEditor(); + }; + } + + /** + * @param {HTMLElement} container + * @param {?RulePart} child + * @param {(child:RulePart)=>void} setChild + * @protected + */ + _conditionalDropLocation(container, child, setChild) { + if (child == null) { + const span = document.createElement("span"); + span.classList.add("rule-drop-location"); + this._allowDroppingOn(span, setChild); + container.append(span); + } else { + container.append(child.render()); + } + } + } + + class RulePartProvider extends RuleContainer { + /** + * @param {()=>RulePart} partFactory + */ + constructor(partFactory) { + super(); + this._partFactory = partFactory; + } + + render() { + const part = this._partFactory(); + part.parent = this; + part.showValidationError = false; + const element = part.render(); + part.showValidationError = true; + return element; + } + + /** + * @returns {"error"} + */ + validate(errorList) { + return "error"; + } + + removeChild(rulePart) { + } + + isParent(maybeChild) { + return false; + } + } + + class RulePartTrash extends RuleContainer { + render() { + const div = document.createElement("div"); + div.classList.add("rule-trash"); + div.ondragover = ev => { + // show that it can be dropped + ev.preventDefault(); + // stop groups further out from capturing the event. + ev.stopPropagation(); + }; + div.ondrop = ev => { + ev.preventDefault(); + // stop groups further out from capturing the event. + ev.stopPropagation(); + const rulePartID = ev.dataTransfer.getData("text/plain"); + const rulePart = rulePartMap.get(rulePartID); + rulePart.parent.removeChild(rulePart); + refreshEditor(); + }; + return div; + } + + /** + * @returns {"error"} + */ + validate(errorList) { + return "error"; + } + + removeChild(rulePart) { + } + + isParent(maybeChild) { + return false; + } + } + + /** + * @typedef {"and"|"or"|"add"|"mul"|"max"|"min"} RuleGroupAggregators + * @type {Map<RuleGroupAggregators, string>} + */ + const ruleGroupAggregators = new Map([ + ["and", "And"], + ["or", "Or"], + ["add", "Sum all"], + ["mul", "Multiply all"], + ["max", "Maximum"], + ["min", "Minimum"], + ]); + + class RuleGroup extends RuleContainer { + /** + * @param {RuleGroupAggregators} mode + */ + constructor(mode) { + super(); + this.mode = mode; + /** + * @type {RulePart[]} + * @private + */ + this._children = []; + } + + render() { + const div = document.createElement("div"); + div.classList.add("rule-part"); + div.append(ruleGroupAggregators.get(this.mode)); + const button = App.UI.DOM.appendNewElement("button", div, "<->"); + button.onclick = () => { + if (this.mode === "and") { + this.mode = "or"; + } else if (this.mode === "or") { + this.mode = "and"; + } else if (this.mode === "add") { + this.mode = "mul"; + } else if (this.mode === "mul") { + this.mode = "max"; + } else if (this.mode === "max") { + this.mode = "min"; + } else { + this.mode = "add"; + } + refreshEditor(); + }; + for (const rulePart of this._children) { + div.append(rulePart.render()); + } + let span = document.createElement("span"); + span.classList.add("rule-drop-location"); + div.append(span); + // interactions + this._allowDroppingOn(div, (child => this.addChild(child))); + if (this.parent !== null) { + // if null, it's the outermost and that can't be draggable + makeDraggable(div, this); + this._dragElement = div; + } + this._markValidationError(div); + return div; + } + + validate(errorList) { + if (this._children.length === 0) { + errorList.push("Condition group needs at least 1 condition."); + return "error"; + } + const expectedType = "number"; + for (const rulePart of this.children) { + if (rulePart.validate(errorList) !== expectedType) { + errorList.push("Condition group only accepts boolean and number conditions."); + return "error"; + } + } + return expectedType; + } + + /** + * @param {RulePart} rulePart + */ + addChild(rulePart) { + if (rulePart.parent != null) { + rulePart.parent.removeChild(rulePart); + } + this._children.push(rulePart); + rulePart.parent = this; + } + + /** + * @override + * @param {RulePart} rulePart + */ + removeChild(rulePart) { + this._children.delete(rulePart); + rulePart.parent = null; + } + + /** + * @override + * @param {RuleContainer} maybeChild + */ + isParent(maybeChild) { + for (const child of this._children) { + if (isSameOrParent(child, maybeChild)) { + return true; + } + } + return false; + } + + /** + * @returns {ReadonlyArray<RulePart>} + */ + get children() { + return this._children; + } + } + + class RuleNegate extends RuleContainer { + constructor() { + super(); + /** + * @type {?RulePart} + * @private + */ + this._child = null; + } + + render() { + const div = document.createElement("div"); + div.classList.add("rule-part"); + div.append("Not"); + + if (this.child == null) { + let span = document.createElement("span"); + span.classList.add("rule-drop-location"); + div.append(span); + this._allowDroppingOn(div, child => this.child = child); + } else { + div.append(this.child.render()); + div.ondragover = ev => { + // stop groups further out from capturing the event. + ev.stopPropagation(); + }; + } + + makeDraggable(div, this); + this._dragElement = div; + this._markValidationError(div); + + return div; + } + + validate(errorList) { + if (this._child == null) { + errorList.push("Negation needs a condition to negate."); + return "error"; + } + if (this._child.validate(errorList) === "number") { + return "number"; + } else { + errorList.push("Negation accepts only boolean and number conditions."); + return "error"; + } + } + + /** + * @param {RulePart} rulePart + */ + set child(rulePart) { + if (rulePart.parent != null) { + rulePart.parent.removeChild(rulePart); + } + this._child = rulePart; + rulePart.parent = this; + } + + /** + * @returns {RulePart} + */ + get child() { + return this._child; + } + + /** + * @override + * @param {RulePart} rulePart + */ + removeChild(rulePart) { + this._child = null; + rulePart.parent = null; + } + + /** + * @override + * @param {RuleContainer} maybeChild + */ + isParent(maybeChild) { + if (this._child == null) { + return false; + } + return (isSameOrParent(this._child, maybeChild)); + } + } + + /** + * @typedef {"sub" | "div" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte"| "substr"} RulePairComparators + * @type {Map<RulePairComparators, string>} + */ + const rulePairComparators = new Map([ + ["eq", "=="], + ["neq", "!="], + ["lt", "<"], + ["gt", ">"], + ["lte", "<="], + ["gte", ">="], + ["sub", "-"], + ["div", "/"], + ["substr", "Contains"], + ]); + + class RulePair extends RuleContainer { + /** + * @param {RulePairComparators} mode + */ + constructor(mode) { + super(); + this.mode = mode; + /** + * @type {RulePart} + */ + this._child1 = null; + /** + * @type {RulePart} + */ + this._child2 = null; + } + + render() { + const div = document.createElement("div"); + div.classList.add("rule-part"); + // drag element + makeNotDraggable(div); + div.append(createDragElement(this)); + // element 1 + this._conditionalDropLocation(div, this._child1, child => this.child1 = child); + + // operator + let matchFound = false; + const select = document.createElement("select"); + for (const [key, name] of rulePairComparators) { + const el = document.createElement("option"); + el.value = key; + el.textContent = name; + if (this.mode === key) { + el.selected = true; + matchFound = true; + } + select.append(el); + } + if (!matchFound) { + select.selectedIndex = -1; + } + select.onchange = () => { + /** @type {HTMLSelectElement} */ + // @ts-ignore + const option = select.children.item(select.selectedIndex); + // @ts-ignore + this.mode = option.value; + refreshEditor(); + }; + div.append(select); + + // element 2 + this._conditionalDropLocation(div, this._child2, child => this.child2 = child); + + this._markValidationError(div); + return div; + } + + validate(errorList) { + if (this._child1 == null || this._child2 == null) { + errorList.push("Comparator conditions need a condition on both sides."); + return "error"; + } + if (this.mode === "eq" || this.mode === "neq") { + const expectedType = this._child1.validate(errorList); + if (expectedType === this._child2.validate(errorList)) { + return "number"; + } else { + errorList.push("Both sides need to return the same type."); + return "error"; + } + } else if (this.mode === "substr") { + if (this._child1.validate(errorList) === "string" && this._child2.validate(errorList) === "string") { + return "number"; + } else { + errorList.push("Both sides need to return string."); + return "error"; + } + } else { + if (this._child1.validate(errorList) === "number" && this._child2.validate(errorList) === "number") { + return "number"; + } else { + errorList.push("Both sides need to return number."); + return "error"; + } + } + } + + /** + * @param {RulePart} child + */ + set child1(child) { + if (child.parent != null) { + child.parent.removeChild(child); + } + child.parent = this; + this._child1 = child; + } + + /** + * @returns {RulePart} + */ + get child1() { + return this._child1; + } + + /** + * @param {RulePart} child + */ + set child2(child) { + if (child.parent != null) { + child.parent.removeChild(child); + } + child.parent = this; + this._child2 = child; + } + + /** + * @returns {RulePart} + */ + get child2() { + return this._child2; + } + + /** + * @override + * @param {RulePart} rulePart + */ + removeChild(rulePart) { + if (this._child1 === rulePart) { + this._child1 = null; + } else if (this._child2 === rulePart) { + this._child2 = null; + } + rulePart.parent = null; + } + + /** + * @override + * @param {RuleContainer} maybeChild + */ + isParent(maybeChild) { + return isSameOrParent(this._child1, maybeChild) || isSameOrParent(this._child2, maybeChild); + } + } + + class RuleTernary extends RuleContainer { + constructor() { + super(); + /** + * @type {RulePart} + */ + this._condition = null; + /** + * @type {RulePart} + */ + this._ifTrue = null; + /** + * @type {RulePart} + */ + this._ifFalse = null; + } + + render() { + const div = document.createElement("div"); + div.classList.add("rule-part"); + // drag element + makeDraggable(div, this); + div.append(createDragElement(this)); + + // condition + App.UI.DOM.appendNewElement("span", div, "If", ["rule-left-margin"]); + this._conditionalDropLocation(div, this.condition, child => this.condition = child); + // ifTrue + div.append("Then"); + this._conditionalDropLocation(div, this.ifTrue, child => this.ifTrue = child); + // ifFalse + div.append("Else"); + this._conditionalDropLocation(div, this.ifFalse, child => this.ifFalse = child); + + this._markValidationError(div); + return div; + } + + validate(errorList) { + if (this._condition === null) { + errorList.push("Ternaries need a condition."); + return "error"; + } + if (this._condition.validate(errorList) !== "number") { + errorList.push("Ternaries conditions accepts only booleans or numbers."); + return "error"; + } + if (this._ifTrue == null || this._ifFalse == null) { + errorList.push("Ternaries need values on both sides."); + return "error"; + } + const expectedType = this._ifTrue.validate(errorList); + if (expectedType === this._ifFalse.validate(errorList)) { + return "number"; + } else { + errorList.push("Both sides need to return the same type."); + return "error"; + } + } + + /** + * @param {RulePart} child + */ + set condition(child) { + if (child.parent != null) { + child.parent.removeChild(child); + } + child.parent = this; + this._condition = child; + } + + /** + * @returns {RulePart} + */ + get condition() { + return this._condition; + } + + /** + * @param {RulePart} child + */ + set ifTrue(child) { + if (child.parent != null) { + child.parent.removeChild(child); + } + child.parent = this; + this._ifTrue = child; + } + + /** + * @returns {RulePart} + */ + get ifTrue() { + return this._ifTrue; + } + + /** + * @param {RulePart} child + */ + set ifFalse(child) { + if (child.parent != null) { + child.parent.removeChild(child); + } + child.parent = this; + this._ifFalse = child; + } + + /** + * @returns {RulePart} + */ + get ifFalse() { + return this._ifFalse; + } + + /** + * @override + * @param {RulePart} rulePart + */ + removeChild(rulePart) { + if (this._condition === rulePart) { + this._condition = null; + } else if (this._ifTrue === rulePart) { + this._ifTrue = null; + } else if (this._ifFalse === rulePart) { + this._ifFalse = null; + } + rulePart.parent = null; + } + + /** + * @override + * @param {RuleContainer} maybeChild + */ + isParent(maybeChild) { + return isSameOrParent(this._condition, maybeChild) || + isSameOrParent(this._ifTrue, maybeChild) || + isSameOrParent(this._ifFalse, maybeChild); + } + } + + class RuleBooleanConstant extends RulePart { + /** + * @param {boolean} mode + */ + constructor(mode) { + super(); + this.mode = mode; + } + + render() { + const b = App.UI.DOM.makeElement("button", this.mode ? "Always" : "Never", ["rule-part"]); + b.onclick = () => { + this.mode = !this.mode; + refreshEditor(); + }; + makeDraggable(b, this); + return b; + } + + /** + * @returns {"number"} + */ + validate() { + return "number"; + } + } + + class RuleConstant extends RulePart { + /** + * @param {number|string} value + */ + constructor(value) { + super(); + this.value = value; + this._stringMode = typeof value === "string"; + } + + render() { + const div = App.UI.DOM.makeElement("div", createDragElement(this), ["rule-part"]); + const button = document.createElement("button"); + button.append(this._stringMode ? "String" : "Number"); + button.onclick = () => { + this._stringMode = !this._stringMode; + if (this._stringMode) { + this.value = String(this.value); + } else { + this.value = Number(this.value); + if (Number.isNaN(this.value)) { + this.value = 0; + } + } + refreshEditor(); + }; + div.append(button); + div.append(makeTextBoxDragSafe(App.UI.DOM.makeTextBox(this.value, (v) => { + this.value = v; + refreshEditor(); + }, !this._stringMode), this)); + return div; + } + + /** + * @returns {"number"|"string"} + */ + validate() { + return this._stringMode ? "string" : "number"; + } + } + + class RuleMapCheck extends RulePart { + /** + * @param {string} key + */ + constructor(key) { + super(); + /** + * @type {"boolean"|"assignment"|"number"|"string"} + */ + this.mode = App.RA.Activation.getterManager.isBoolean(key) ? "boolean" + : App.RA.Activation.getterManager.isAssignment(key) ? "assignment" + : App.RA.Activation.getterManager.isNumber(key) ? "number" + : "string"; + this.key = key; + } + + render() { + // make container + const span = document.createElement("span"); + span.classList.add("rule-part"); + makeDraggable(span, this); + + // fill container + // name + App.UI.DOM.appendNewElement("span", span, this.mode === "assignment" ? "Assignment" : "Slave", ["rule-right-margin"]); + + // values + let matchFound = false; + let select = document.createElement("select"); + for (const [key, value] of this._getterMap) { + if (value.visible && !value.visible()) { + continue; + } + let el = document.createElement("option"); + el.value = key; + el.textContent = value.name; + if (value.enabled) { + el.disabled = !value.enabled(); + } + if (this.key === key) { + el.selected = true; + matchFound = true; + } + select.append(el); + } + if (!matchFound) { + select.selectedIndex = -1; + } + select.onchange = () => { + /** @type {HTMLSelectElement} */ + // @ts-ignore + const option = select.children.item(select.selectedIndex); + this.key = option.value; + refreshEditor(); + }; + span.append(select); + return span; + } + + validate() { + if (this.mode === "boolean" || this.mode === "assignment") { + return "number"; + } + return this.mode; + } + + /** + * @returns {ReadonlyMap<string, Getter<*>>} + * @private + */ + get _getterMap() { + return this.mode === "boolean" ? App.RA.Activation.getterManager.booleanGetters + : this.mode === "assignment" ? App.RA.Activation.getterManager.assignmentGetters + : this.mode === "number" ? App.RA.Activation.getterManager.numberGetters + : App.RA.Activation.getterManager.stringGetters; + } + } + + class RuleCustomCheck extends RulePart { + /** + * @param {string} check + */ + constructor(check) { + super(); + this.check = check; + /** + * @type {"boolean"|"number"|"string"} + * @private + */ + this._expectedType = check.first() === "b" ? "boolean" + : check.first() === "n" ? "number" + : "string"; + } + + render() { + const div = App.UI.DOM.makeElement("div", createDragElement(this), ["rule-part"]); + const button = document.createElement("button"); + button.append(capFirstChar(this._expectedType)); + button.onclick = () => { + if (this._expectedType === "boolean") { + this._expectedType = "number"; + } else if (this._expectedType === "number") { + this._expectedType = "string"; + } else { + this._expectedType = "boolean"; + } + this.check = this._expectedType.first() + this.check.slice(1); + refreshEditor(); + }; + div.append(button); + div.append(makeTextBoxDragSafe(App.UI.DOM.makeTextBox(this.check.slice(1), (v) => { + this.check = this._expectedType.first() + v; + refreshEditor(); + }), this)); + this._markValidationError(div); + return div; + } + + validate(errorList) { + try { + runWithReadonlyProxy(() => this._validateRule(this.check.slice(1))); + } catch (e) { + errorList.push(e.message + "."); + return "error"; + } + return this._expectedType === "boolean" ? "number" : this._expectedType; + } + + _validateRule(check) { + const context = new App.RA.Activation.Context(GenerateNewSlave()); + eval(check)(context); + return true; + } + } + + /** + * @param {HTMLInputElement} textBox + * @param {RulePart} rulePart + * @returns {HTMLSpanElement} + */ + function makeTextBoxDragSafe(textBox, rulePart) { + textBox.onfocus = () => rulePart.disableDragging(); + textBox.onmouseover = () => rulePart.disableDragging(); + textBox.onblur = () => rulePart.enableDragging(); + textBox.onmouseout = () => rulePart.enableDragging(); + return textBox; + } + + /** + * @param {RulePart} rulePart + */ + function createDragElement(rulePart) { + const element = document.createElement("div"); + element.classList.add("rule-drag-element"); + makeDraggable(element, rulePart); + return element; + } + + /** + * @param {HTMLElement} node + * @param {RulePart} rulePart + */ + function makeDraggable(node, rulePart) { + node.draggable = true; + node.classList.add("rule-draggable"); + node.ondragstart = ev => { + ev.stopPropagation(); + ev.dataTransfer.setData("text/plain", rulePart.id); + }; + } + + /** + * @param {HTMLElement} node + */ + function makeNotDraggable(node) { + node.ondragstart = ev => { + ev.stopPropagation(); + }; + } + + /** + * @param {DragEvent} event + * @param {RuleContainer} targetPart + * @returns {boolean} + * @private + */ + function canDrop(event, targetPart) { + const movedPartID = event.dataTransfer.getData("text/plain"); + const movedPart = rulePartMap.get(movedPartID); + // if it can't have children, any place is valid + if (!(movedPart instanceof RuleContainer)) { + return true; + } + // don't allow dragging onto itself + if (movedPart === targetPart) { + return false; + } + // don't allow dragging onto children + return !movedPart.isParent(targetPart); + } + + /** + * @param {RulePart} parent + * @param {RuleContainer} maybeChild + * @returns {boolean} + */ + function isSameOrParent(parent, maybeChild) { + if (parent === maybeChild) { + return true; + } + return (parent instanceof RuleContainer) && parent.isParent(maybeChild); + } + + /** + * @param {FC.RA.PostFixRule} rule + * @returns {RuleGroup} + */ + function deserializeRule(rule) { + let stack = new RuleFactoryStack(); + + /** + * @param {"and"|"or"|"add"|"mul"|"max"|"min"} mode + */ + function makeGroup(mode) { + const length = stack.popNumber(); + const group = new RuleGroup(mode); + const children = []; + for (let i = 0; i < length; i++) { + children.unshift(stack.popRulePart()); + } + for (const child of children) { + group.addChild(child); + } + stack.pushRulePart(group); + } + + /** + * @param {"sub" | "div" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "substr"} mode + */ + function makePair(mode) { + const pair = new RulePair(mode); + pair.child2 = stack.popRulePart(); + pair.child1 = stack.popRulePart(); + stack.pushRulePart(pair); + } + + function makeTernary() { + const ternary = new RuleTernary(); + ternary.ifFalse = stack.popRulePart(); + ternary.ifTrue = stack.popRulePart(); + ternary.condition = stack.popRulePart(); + stack.pushRulePart(ternary); + } + + /** + * @type {Map<string, function(): void>} + */ + const operators = new Map([ + // and, or, +, * can take arbitrarily many arguments, so the first one describes the argument count + ["and", () => makeGroup("and")], + ["or", () => makeGroup("or")], + ["add", () => makeGroup("add")], + ["mul", () => makeGroup("mul")], + ["max", () => makeGroup("max")], + ["min", () => makeGroup("min")], + ["sub", () => makePair("sub")], + ["div", () => makePair("div")], + ["eqstr", () => makePair("eq")], + ["neqstr", () => makePair("neq")], + ["eqnum", () => makePair("eq")], + ["neqnum", () => makePair("neq")], + ["gt", () => makePair("gt")], + ["gte", () => makePair("gte")], + ["lt", () => makePair("lt")], + ["lte", () => makePair("lte")], + ["substr", () => makePair("substr")], + ["not", () => { + const negate = new RuleNegate(); + negate.child = stack.popRulePart(); + stack.pushRulePart(negate); + }], + ["ternarystr", makeTernary], + ["ternarynum", makeTernary], + ]); + + for (let i = 0; i < rule.length; i++) { + const rulePart = rule[i]; + if (typeof rulePart === "string") { + const operation = operators.get(rulePart); + if (operation !== undefined) { + operation(); + } else if (App.RA.Activation.getterManager.has(rulePart)) { + stack.pushRulePart(new RuleMapCheck(rulePart)); + } else if (rulePart.startsWith("?")) { + stack.pushRulePart(new RuleCustomCheck(rulePart.slice(1))); + } else { + stack.pushRulePart(new RuleConstant(rulePart.slice(1))); + } + } else if (typeof rulePart === "number") { + // check if this is a length counter + const next = rule[i + 1]; + if (["and", "or", "add", "mul"].includes(next)) { + stack.pushNumber(rulePart); + } else { + stack.pushRulePart(new RuleConstant(rulePart)); + } + } else { + stack.pushRulePart(new RuleBooleanConstant(rulePart)); + } + } + + // @ts-ignore + return stack.popRulePart(); + } + + /** + * Expects a valid RulePart structure + * + * @param {RuleGroup} rulePart + * @returns {FC.RA.PostFixRule} + */ + function serializeRule(rulePart) { + /** + * @type {FC.RA.PostFixRule} + */ + const rule = []; + _serializeRulePart(rulePart); + return rule; + + /** + * @param {RulePart} rulePart + */ + function _serializeRulePart(rulePart) { + if (rulePart instanceof RuleGroup) { + for (const ruleChild of rulePart.children) { + _serializeRulePart(ruleChild); + } + rule.push(rulePart.children.length, rulePart.mode); + } else if (rulePart instanceof RulePair) { + _serializeRulePart(rulePart.child1); + _serializeRulePart(rulePart.child2); + let mode = rulePart.mode; + if (rulePart.mode === "eq" || rulePart.mode === "neq") { + if (rulePart.child1.validate([]) === "string") { + mode += "str"; + } else { + mode += "num"; + } + } + rule.push(mode); + } else if (rulePart instanceof RuleBooleanConstant) { + rule.push(rulePart.mode); + } else if (rulePart instanceof RuleMapCheck) { + rule.push(rulePart.key); + } else if (rulePart instanceof RuleConstant) { + rule.push(typeof rulePart.value === "string" ? "!" + rulePart.value : rulePart.value); + } else if (rulePart instanceof RuleNegate) { + _serializeRulePart(rulePart.child); + rule.push("not"); + } else if (rulePart instanceof RuleTernary) { + _serializeRulePart(rulePart.condition); + _serializeRulePart(rulePart.ifTrue); + _serializeRulePart(rulePart.ifFalse); + let mode = "ternary"; + if (rulePart.ifTrue.validate([]) === "string") { + mode += "str"; + } else { + mode += "num"; + } + rule.push(mode); + } else if (rulePart instanceof RuleCustomCheck) { + rule.push("?" + rulePart.check); + } + } + } + + class RuleFactoryStack extends App.RA.Activation.Stack { + constructor() { + super(); + /** + * @private + * @type {RulePart[]} + */ + this._ruleParts = []; + } + + /** + * @param {RulePart} v + */ + pushRulePart(v) { + this._ruleParts.push(v); + } + + /** + * @returns {RulePart} + */ + popRulePart() { + return this._ruleParts.pop(); + } + } + + /** + * @param {FC.RA.PostFixRule} rule + */ + function validate(rule) { + let mapExists = rulePartMap !== null; + if (!mapExists) { + rulePartMap = new Map(); + } + const rulePart = deserializeRule(rule); + if (!(rulePart instanceof RuleGroup)) { + console.log("validation error", rule, rulePart, "Outermost is not RuleGroup!"); + } + if (rulePart.mode !== "and" && rulePart.mode !== "or") { + console.log("validation error", rule, rulePart, "Outermost has to be \"and\" or \"or\" mode RuleGroup!"); + } + const errors = []; + const result = rulePart.validate(errors); + if (result === "error") { + console.log("validation error", rule, rulePart, errors); + } + if (!mapExists) { + rulePartMap = null; + } + return result !== "error"; + } + + return { + build: editor, + save: saveEditor, + reset: resetEditor, + validateRule: validate, + }; +})(); diff --git a/js/rulesAssistant/conditionEvaluation.js b/js/rulesAssistant/conditionEvaluation.js new file mode 100644 index 0000000000000000000000000000000000000000..938f17432209674528971a86f70e66dcb6874489 --- /dev/null +++ b/js/rulesAssistant/conditionEvaluation.js @@ -0,0 +1,752 @@ +/* eslint-disable sonarjs/no-identical-expressions */ + +App.RA.Activation.Context = class { + /** + * @param {App.Entity.SlaveState} slave + */ + constructor(slave) { + this._slave = slave; + } + + get slave() { + return this._slave; + } +}; + +/** + * @template {boolean|number|string} T + * @typedef {object} Getter + * @property {string} name + * @property {string} description Should include possible values if applicable. + * @property {string} [requirements] Plaintext description of requirements to use this getter. + * @property {()=>boolean} [enabled] Whether the getter can be used. + * @property {()=>boolean} [visible] Whether the getter should be shown. Mainly intended for disabled mods. + * @property {(s: App.RA.Activation.Context) =>T} val + */ + +App.RA.Activation.getterManager = (function() { + class GetterManager { + constructor() { + /** + * @private + * @type {Map<string, Getter<boolean>>} + */ + this._booleanGetters = new Map(); + /** + * @private + * @type {Map<string, Getter<boolean>>} + */ + this._assignmentGetters = new Map(); + /** + * @private + * @type {Map<string, Getter<number>>} + */ + this._numberGetters = new Map(); + /** + * @private + * @type {Map<string, Getter<string>>} + */ + this._stringGetters = new Map(); + } + + /** + * @param {string} key + * @private + */ + _validateKey(key) { + if (!/[a-zA-Z]/.test(key.first())) { + throw new Error(`Invalid Key: ${key}; The first character of a getter key has to be alphabetic`); + } + } + + /** + * @param {string} key + * @param {Getter<boolean>} getter + */ + addBoolean(key, getter) { + this._validateKey(key); + this._booleanGetters.set(key, getter); + } + + /** + * @param {string} key + * @param {Getter<boolean>} getter + */ + addAssignment(key, getter) { + this._validateKey(key); + this._assignmentGetters.set(key, getter); + } + + /** + * @param {string} key + * @param {Getter<number>} getter + */ + addNumber(key, getter) { + this._validateKey(key); + this._numberGetters.set(key, getter); + } + + /** + * @param {string} key + * @param {Getter<string>} getter + */ + addString(key, getter) { + this._validateKey(key); + this._stringGetters.set(key, getter); + } + + /** + * @param {string} key + * @returns {boolean} + */ + has(key) { + return this._booleanGetters.has(key) || this._assignmentGetters.has(key) || + this._numberGetters.has(key) || this._stringGetters.has(key); + } + + /** + * @param {string} key + * @returns {boolean} + */ + isBoolean(key) { + return this._booleanGetters.has(key); + } + + /** + * @param {string} key + * @returns {boolean} + */ + isAssignment(key) { + return this._assignmentGetters.has(key); + } + + /** + * @param {string} key + * @returns {boolean} + */ + isNumber(key) { + return this._numberGetters.has(key); + } + + /** + * @param {string} key + * @returns {boolean} + */ + isString(key) { + return this._stringGetters.has(key); + } + + /** + * @returns {ReadonlyMap<string, Getter<boolean>>} + */ + get booleanGetters() { + return this._booleanGetters; + } + + /** + * @returns {ReadonlyMap<string, Getter<boolean>>} + */ + get assignmentGetters() { + return this._assignmentGetters; + } + + /** + * @returns {ReadonlyMap<string, Getter<number>>} + */ + get numberGetters() { + return this._numberGetters; + } + + /** + * @returns {ReadonlyMap<string, Getter<string>>} + */ + get stringGetters() { + return this._stringGetters; + } + + /** + * @returns {string} + */ + get booleanDefault() { + return this._booleanGetters.keys().next().value; + } + + /** + * @returns {string} + */ + get assignmentDefault() { + return this._assignmentGetters.keys().next().value; + } + + /** + * @returns {string} + */ + get numberDefault() { + return this._numberGetters.keys().next().value; + } + + /** + * @returns {string} + */ + get stringDefault() { + return this._stringGetters.keys().next().value; + } + + /** + * @param {App.RA.Activation.Stack} stack + * @param {App.RA.Activation.Context} context + * @param {string} key + * @returns {boolean} True, if a getter exists for the given key + */ + read(stack, context, key) { + let getterB = this._booleanGetters.get(key); + if (getterB !== undefined) { + stack.pushBoolean(getterB.val(context)); + return true; + } + getterB = this._assignmentGetters.get(key); + if (getterB !== undefined) { + stack.pushBoolean(getterB.val(context)); + return true; + } + const getterN = this._numberGetters.get(key); + if (getterN !== undefined) { + stack.pushNumber(getterN.val(context)); + return true; + } + const getterS = this._stringGetters.get(key); + if (getterS !== undefined) { + stack.pushString(getterS.val(context)); + return true; + } + return false; + } + } + + return new GetterManager(); +})(); + +App.RA.Activation.populateGetters = function() { + const gm = App.RA.Activation.getterManager; + // Note: The first value of each type being added is taken as the default. + + // Booleans + gm.addBoolean("isfertile", { + name: "Is Fertile?", description: "Whether or not the slave is fertile.", + val: c => isFertile(c.slave) + }); + gm.addBoolean("isamputee", { + name: "Is Amputee?", description: "Whether or not the slave has no limbs.", + val: c => isAmputee(c.slave) + }); + gm.addBoolean("ispregnant", { + name: "Is Pregnant?", description: "Whether or not the slave is pregnant.", + val: c => c.slave.preg > 0 + }); + gm.addBoolean("isslim", { + name: "Is Slim?", description: "If the slave is considered slim or not by arcology standards.", + val: c => isSlim(c.slave) + }); + gm.addBoolean("isstacked", { + name: "Is Stacked?", description: "If the slave is considered stacked (big T&A) or not.", + val: c => isStacked(c.slave) + }); + gm.addBoolean("ismodded", { + name: "Is Modded?", description: "If the slave is considered heavily modded or not.", + val: c => SlaveStatsChecker.isModded(c.slave) + }); + gm.addBoolean("isunmodded", { + name: "Is Unmodded?", description: "If the slave is (relatively) unmodded.", + val: c => SlaveStatsChecker.isModded(c.slave) + }); + + // Assignments + // Penthouse Assignments + gm.addAssignment("rest", { + name: "Resting", description: "Resting in the penthouse.", + val: c => c.slave.assignment === Job.REST + }); + gm.addAssignment("fucktoy", { + name: "Fucktoy", description: "Pleasing the master.", + val: c => c.slave.assignment === Job.FUCKTOY + }); + gm.addAssignment("classes", { + name: "Taking classes", description: "Taking classes to better serve.", + val: c => c.slave.assignment === Job.CLASSES + }); + gm.addAssignment("house", { + name: "Cleaning", description: "Cleaning the penthouse.", + val: c => c.slave.assignment === Job.HOUSE + }); + gm.addAssignment("whore", { + name: "Whoring", description: "Whoring themself out.", + val: c => c.slave.assignment === Job.WHORE + }); + gm.addAssignment("public", { + name: "Serving public", description: "Serving the public.", + val: c => c.slave.assignment === Job.PUBLIC + }); + gm.addAssignment("subordinate", { + name: "Subordinate", description: "Subordinate to other slaves.", + val: c => c.slave.assignment === Job.SUBORDINATE + }); + gm.addAssignment("milked", { + name: "Milked", description: "Getting milked.", + val: c => c.slave.assignment === Job.MILKED + }); + gm.addAssignment("gloryhole", { + name: "Glory hole", description: "Working as a glory hole.", + val: c => c.slave.assignment === Job.GLORYHOLE + }); + gm.addAssignment("confinement", { + name: "Confined", description: "Confined at the penthouse.", + val: c => c.slave.assignment === Job.CONFINEMENT + }); + gm.addAssignment("choice", { + name: "Choose own", description: "Allowed to choose their own job.", + val: c => c.slave.assignment === Job.CHOICE + }); + // Leadership Assignments + gm.addAssignment("bodyguard", { + name: "Bodyguard", description: "Serving as Bodyguard.", + requirements: "Armory is built.", enabled: ()=>App.Entity.facilities.armory.established, + val: c => c.slave.assignment === Job.BODYGUARD + }); + gm.addAssignment("headgirl", { + name: "Head Girl", description: "Serving as Head Girl", + val: c => c.slave.assignment === Job.HEADGIRL + }); + gm.addAssignment("recruiter", { + name: "Recruiter", description: "Recruiting new slaves.", + val: c => c.slave.assignment === Job.RECRUITER + }); + gm.addAssignment("agent", { + name: "Agent", description: "Serving as an Agent in another arcology.", + val: c => c.slave.assignment === Job.AGENT + }); + gm.addAssignment("agentpartner", { + name: "Agent partner", description: "Serving an agent living in another arcology.", + val: c => c.slave.assignment === Job.AGENTPARTNER + }); + // Facility Assignments + gm.addAssignment("arcade", { + name: "Confined in arcade", description: "Confined in the arcade.", + requirements: "Arcade is built.", enabled: ()=>App.Entity.facilities.arcade.established, + val: c => c.slave.assignment === Job.ARCADE + }); + gm.addAssignment("madam", { + name: "Madam", description: "Serving as Madam.", + requirements: "Brothel is built.", enabled: ()=>App.Entity.facilities.brothel.established, + val: c => c.slave.assignment === Job.MADAM + }); + gm.addAssignment("brothel", { + name: "Brothel whoring?", description: "Working in the brothel.", + requirements: "Brothel is built.", enabled: ()=>App.Entity.facilities.brothel.established, + val: c => c.slave.assignment === Job.BROTHEL + }); + gm.addAssignment("warden", { + name: "Wardeness", description: "Serving as Wardeness.", + requirements: "Cellblock is built.", enabled: ()=>App.Entity.facilities.cellblock.established, + val: c => c.slave.assignment === Job.WARDEN + }); + gm.addAssignment("cellblock", { + name: "Confined in cellblock?", description: "Confined in the cellblock.", + requirements: "Cellblock is built.", enabled: ()=>App.Entity.facilities.cellblock.established, + val: c => c.slave.assignment === Job.CELLBLOCK + }); + gm.addAssignment("dj", { + name: "DJ", description: "Serving as DJ.", + requirements: "Club is built.", enabled: ()=>App.Entity.facilities.club.established, + val: c => c.slave.assignment === Job.DJ + }); + gm.addAssignment("club", { + name: "Serving club", description: "Serving in the club.", + requirements: "Club is built.", enabled: ()=>App.Entity.facilities.club.established, + val: c => c.slave.assignment === Job.CLUB + }); + gm.addAssignment("nurse", { + name: "Nurse", description: "Serving as Nurse.", + requirements: "Clinic is built.", enabled: ()=>App.Entity.facilities.clinic.established, + val: c => c.slave.assignment === Job.NURSE + }); + gm.addAssignment("clinic", { + name: "Getting treatment", description: "Getting treatment in the clinic.", + requirements: "Clinic is built.", enabled: ()=>App.Entity.facilities.clinic.established, + val: c => c.slave.assignment === Job.CLINIC + }); + gm.addAssignment("milkmaid", { + name: "Milkmaid", description: "Serving as Milkmaid", + requirements: "Dairy is built.", enabled: ()=>App.Entity.facilities.dairy.established, + val: c => c.slave.assignment === Job.MILKMAID + }); + gm.addAssignment("dairy", { + name: "Work dairy", description: "Working in the dairy", + requirements: "Dairy is built.", enabled: ()=>App.Entity.facilities.dairy.established, + val: c => c.slave.assignment === Job.DAIRY + }); + gm.addAssignment("farmer", { + name: "Farmer", description: "Serving as Farmer", + requirements: "Farmyard is built.", enabled: ()=>App.Entity.facilities.farmyard.established, + val: c => c.slave.assignment === Job.FARMER + }); + gm.addAssignment("farmyard", { + name: "Farmhand", description: "Working as a farmhand.", + requirements: "Farmyard is built.", enabled: ()=>App.Entity.facilities.farmyard.established, + val: c => c.slave.assignment === Job.FARMYARD + }); + gm.addAssignment("headgirlsuite", { + name: "Head Girl Servant", description: "Living with the Head Girl.", + requirements: "Head Girl Suite is built.", enabled: ()=>App.Entity.facilities.headGirlSuite.established, + val: c => c.slave.assignment === Job.HEADGIRLSUITE + }); + gm.addAssignment("concubine", { + name: "Concubine", description: "Serving as Concubine.", + requirements: "Master suite is built.", enabled: ()=>App.Entity.facilities.masterSuite.established, + val: c => c.slave.assignment === Job.CONCUBINE + }); + gm.addAssignment("mastersuite", { + name: "Master suite servant", description: "Serving in the master suite.", + requirements: "Master suite is built.", enabled: ()=>App.Entity.facilities.masterSuite.established, + val: c => c.slave.assignment === Job.MASTERSUITE + }); + gm.addAssignment("matron", { + name: "Matron", description: "Serving as Matron.", + requirements: "Nursery is built.", enabled: ()=>App.Entity.facilities.nursery.established, + visible: () => V.experimental.nursery > 0, + val: c => c.slave.assignment === Job.MATRON + }); + gm.addAssignment("nursery", { + name: "Nanny", description: "Working as a nanny.", + requirements: "Nursery is built.", enabled: ()=>App.Entity.facilities.nursery.established, + visible: () => V.experimental.nursery > 0, + val: c => c.slave.assignment === Job.NURSERY + }); + gm.addAssignment("teacher", { + name: "Schoolteacher", description: "Serving as Schoolteacher.", + requirements: "Schoolroom is built.", enabled: ()=>App.Entity.facilities.schoolroom.established, + val: c => c.slave.assignment === Job.TEACHER + }); + gm.addAssignment("school", { + name: "Learning", description: "Learning in the schoolroom.", + requirements: "Schoolroom is built.", enabled: ()=>App.Entity.facilities.schoolroom.established, + val: c => c.slave.assignment === Job.SCHOOL + }); + gm.addAssignment("steward", { + name: "Stewardess", description: "Serving as Stewardess.", + requirements: "Servants Quarters are built.", + enabled: ()=>App.Entity.facilities.servantsQuarters.established, + val: c => c.slave.assignment === Job.STEWARD + }); + gm.addAssignment("quarter", { + name: "Servant", description: "Working as a servant in the Servants Quarters.", + requirements: "Servants Quarters are built.", + enabled: ()=>App.Entity.facilities.servantsQuarters.established, + val: c => c.slave.assignment === Job.QUARTER + }); + gm.addAssignment("attendant", { + name: "Attendant", description: "Serving as Attendant.", + requirements: "Spa is built.", enabled: ()=>App.Entity.facilities.spa.established, + val: c => c.slave.assignment === Job.ATTENDANT + }); + gm.addAssignment("spa", { + name: "Spa resting", description: "Resting in the spa.", + requirements: "Spa is built.", enabled: () => App.Entity.facilities.spa.established, + val: c => c.slave.assignment === Job.SPA + }); + + // Numbers + gm.addNumber("devotion", { + name: "Devotion", + description: "Very Hateful: (-∞, -95), Hateful: [-95, -50), Resistant: [-50, -20), Ambivalent: [-20, 20], " + + "Accepting: (20, 50], Devoted: (50, 95], Worshipful: (95, ∞)", + val: c => c.slave.devotion + }); + gm.addNumber("trust", { + name: "Trust", + description: "Extremely terrified: (-∞, -95), Terrified: [-95, -50), Frightened: [-50, -20), " + + "Fearful: [-20, 20], Careful: (20, 50], Trusting: (50, 95], Total trust: (95, ∞)", + val: c => c.slave.trust + }); + gm.addNumber("health", { + name: "Health", + description: "Death: (-∞, -100), Near Death: [-100, -90), Extremely Unhealthy: [-90, -50), " + + "Unhealthy: [-50, -20), Healthy: [-20, 20], Very Healthy: (20, 50], Extremely Healthy: (50, 90], " + + "Unnaturally Healthy: (90, ∞)", + val: c => c.slave.health.condition + }); + gm.addNumber("fatigue", { + name: "Fatigue", + description: "Energetic: (-∞, 0], Rested: (0, 30], Tired: (30, 60], Fatigued: (60, 90], Exhausted: (90, ∞)", + val: c => c.slave.health.tired + }); + gm.addNumber("illness", { + name: "Illness", + description: "0: Not ill, 1: A little under the weather, 2: Slightly ill, can be treated at the clinic, " + + "3: Ill, can be treated at the clinic, 4: Very ill, can be treated at the clinic, " + + "5: Terribly ill, can be treated at the clinic", + val: c => c.slave.health.illness + }); + gm.addNumber("energy", { + name: "Sex drive", + description: "Frigid: (-∞, 20], Poor: (20, 40], Average: (40, 60], Powerful: (60, 80], " + + "Sex Addict: (80, 100), Nympho: 100", + val: c => c.slave.energy + }); + gm.addNumber("weight", { + name: "Weight", + description: "Emaciated: (-∞, -95), Skinny: [-95, -30), Thin: [-30, -10), Average: [-10, 10], " + + "Plush: (10, 30], Overweight: (30, 95], Fat: (95, 130], Obese: (130, 160], Super Obese: (160, 190], " + + "Dangerously Obese: (190, ∞)", + val: c => c.slave.weight + }); + gm.addNumber("height", {name: "Height", description: "Slave height in cm.", val: c => c.slave.height}); + gm.addNumber("age", {name: "Age", description: "Real slave age", val: c => c.slave.actualAge}); + gm.addNumber("physicalAge", { + name: "Body Age", description: "Age of the slave's body.", + val: c => c.slave.physicalAge + }); + gm.addNumber("visualAge", { + name: "Visible Age", description: "How old the slave looks.", + val: c => c.slave.visualAge + }); + gm.addNumber("muscles", { + name: "Muscles", + description: "Frail: (-∞, -96), Very weak: [-96, -31], Weak: [-31, -6), Soft: [-6, 5), Toned: [5, 30), " + + "Fit: [30, 50), Muscular: [50, 95), Hugely muscular: [95, ∞)", + val: c => c.slave.muscles + }); + gm.addNumber("lactation", { + name: "Lactation", description: "0: None, 1: Natural, 2: Lactation implant", + val: c => c.slave.lactation + }); + gm.addNumber("pregType", { + name: "Pregnancy Multiples", description: "Fetus count, known only after the 10th week of pregnancy", + val: c => c.slave.pregType + }); + gm.addNumber("bellyImplant", { + name: "Belly Implant", description: "Volume in CCs. None: -1", + val: c => c.slave.bellyImplant + }); + gm.addNumber("belly", { + name: "Belly Size", description: "Volume in CCs, any source", + val: c => c.slave.belly + }); + gm.addNumber("intelligenceImplant", { + name: "Education", + description: "Education level. 0: uneducated, 15: educated, 30: advanced education, " + + "(0, 15): incomplete education.", + val: c => c.slave.intelligenceImplant + }); + gm.addNumber("intelligence", { + name: "Intelligence", description: "From moronic to brilliant: [-100, 100]", + val: c => c.slave.intelligence + }); + gm.addNumber("accent", { + name: "Accent", description: "No accent: 0, Nice accent: 1, Bad accent: 2, Can't speak language: 3 and above", + val: c => c.slave.accent + }); + gm.addNumber("waist", { + name: "Waist", + description: "Masculine waist: (95, ∞), Ugly waist: (40, 95], Unattractive waist: (10, 40], " + + "Average waist: [-10, 10], Feminine waist: [-40, -10), Wasp waist: [-95, -40), Absurdly narrow: (-∞, -95)", + val: c => c.slave.waist + }); + gm.addNumber("chem", { + name: "Carcinogen Buildup", + description: "Side effects from drug use. If greater than 10 will have negative consequences.", + val: c => c.slave.chem + }); + gm.addNumber("boobs", { + name: "Breasts", + description: "0-299: Flat, 300-399: A-cup, 400-499: B-cup, 500-649: C-cup, 650-799: D-cup, 00-999: DD-cup, " + + "1000-1199: F-cup, 1200-1399: G-cup, 1400-1599: H-cup, 1600-1799: I-cup, 1800-2049: J-cup, " + + "2050-2299: K-cup, 2300-2599: L-cup, 2600-2899: M-cup, 2900-3249: N-cup, 3250-3599: O-cup, " + + "3600-3949: P-cup, 3950-4299: Q-cup, 4300-4699: R-cup, 4700-5099: S-cup, 5100-10499: Massive", + val: c => c.slave.boobs + }); + gm.addNumber("dick", { + name: "Dick", + description: "None: 0, Tiny: 1, Little: 2, Normal: 3, Big: 4, Huge: 5, Gigantic: 6, Massive: 7, Titanic: 8, " + + "Monstrous: 9, Inhuman: 10, Hypertrophied: 11+", + val: c => c.slave.dick + }); + gm.addNumber("balls", { + name: "Balls", + description: "None: 0, Vestigial: 1, Small: 2, Average: 3, Large: 4, Massive: 5, Huge: 6, Gigantic: 7, " + + "Enormous: 8, Monstrous: 9, Hypertrophied: 11+", + val: c => c.slave.balls + }); + gm.addNumber("lips", { + name: "Lips", + description: "Thin: (-∞, 10), Normal: [10, 20), Pretty: [20, 40), Plush: [40, 70), Huge (lisping): [70, 95), " + + "Facepussy (mute): [95, ∞)", + val: c => c.slave.lips + }); + gm.addNumber("butt", { + name: "Butt", + description: "Flat: 0, Less flat: 1, Small: 2, Big: 3, Large: 4, Huge: 5, Enormous: 6, Gigantic: 7, " + + "Ridiculous: 8, Immense: 9-10, Inhuman: 11-20", + val: c => c.slave.butt + }); + gm.addNumber("hips", { + name: "Hips", + description: "Very narrow: -2, Narrow: -1, Normal: 0, Wide hips: 1, Very wide hips: 2, Inhumanly wide hips: 3", + val: c => c.slave.hips + }); + + // Strings + gm.addString("label", {name: "Label", description: "Assigned Label", val: c => c.slave.custom.label}); + gm.addString("genes", { + name: "Sex", description: "Genetic sex: Male: XX, Female: XY", + val: c => c.slave.genes + }); + gm.addString("fetish", { + name: "Fetish", + description: "One of 'buttslut', 'cumslut', 'masochist', 'sadist', 'dom', 'submissive', 'boobs', " + + "'pregnancy', 'none' (AKA vanilla)", + val: c => c.slave.fetish + }); +}; + +/** + * @param {App.Entity.SlaveState} slave + * @param {FC.RA.PostFixRule} rule + * @returns {boolean} If the rule should be applied to the given slave + */ +App.RA.Activation.evaluate = function(slave, rule) { + const gm = App.RA.Activation.getterManager; + const context = new App.RA.Activation.Context(slave); + const stack = new App.RA.Activation.Stack(); + + /** + * @type {Map<string, function(): void>} + */ + const operators = new Map([ + // and, or, +, * can take arbitrarily many arguments, so the first one describes the argument count + ["and", () => { + const length = stack.popNumber(); + let value = 1; + for (let i = 0; i < length; i++) { + value &= stack.popAsBoolean(); + } + stack.pushNumber(value); + }], + ["or", () => { + const length = stack.popNumber(); + let value = 1; + for (let i = 0; i < length; i++) { + value |= stack.popAsBoolean(); + } + stack.pushNumber(value); + }], + ["add", () => { + const length = stack.popNumber(); + let value = 0; + for (let i = 0; i < length; i++) { + value += stack.popNumber(); + } + stack.pushNumber(value); + }], + ["mul", () => { + const length = stack.popNumber(); + let value = 1; + for (let i = 0; i < length; i++) { + value *= stack.popNumber(); + } + stack.pushNumber(value); + }], + ["max", () => { + const length = stack.popNumber(); + let value = stack.popNumber(); + for (let i = 1; i < length; i++) { + value = Math.max(value, stack.popNumber()); + } + stack.pushNumber(value); + }], + ["min", () => { + const length = stack.popNumber(); + let value = stack.popNumber(); + for (let i = 1; i < length; i++) { + value = Math.min(value, stack.popNumber()); + } + stack.pushNumber(value); + }], + ["sub", () => { + const subtract = stack.popNumber(); + stack.pushNumber(stack.popNumber() - subtract); + }], + ["div", () => { + const divisor = stack.popNumber(); + stack.pushNumber(stack.popNumber() / divisor); + }], + ["eqstr", () => stack.pushBoolean(stack.popString() === stack.popString())], + ["neqstr", () => stack.pushBoolean(stack.popString() !== stack.popString())], + ["eqnum", () => stack.pushBoolean(stack.popNumber() === stack.popNumber())], + ["neqnum", () => stack.pushBoolean(stack.popNumber() === stack.popNumber())], + ["gt", () => stack.pushBoolean(stack.popNumber() < stack.popNumber())], + ["gte", () => stack.pushBoolean(stack.popNumber() <= stack.popNumber())], + ["lt", () => stack.pushBoolean(stack.popNumber() > stack.popNumber())], + ["lte", () => stack.pushBoolean(stack.popNumber() >= stack.popNumber())], + ["substr", () => { + const value = stack.popString(); + stack.pushBoolean(stack.popString().includes(value)); + }], + ["not", () => stack.pushBoolean(stack.popNumber() === 0)], + ["ternarystr", () => { + const ifFalse = stack.popString(); + const ifTrue = stack.popString(); + stack.pushString(stack.popNumber() ? ifTrue : ifFalse); + }], + ["ternarynum", () => { + const ifFalse = stack.popNumber(); + const ifTrue = stack.popNumber(); + stack.pushNumber(stack.popNumber() ? ifTrue : ifFalse); + }], + ]); + + /** + * Custom getters start with "?" and then "b", "n" or "s" depending on return type + * @param {string} rulePart + */ + function evalCustom(rulePart) { + const expectedType = rulePart.charAt(1) === "b" ? "boolean" + : rulePart.charAt(1) === "n" ? "number" + : "string"; + try { + // TODO: This should use a cached Function instead of 'eval'ing + const value = eval(rulePart.slice(2))(context); + if (expectedType === "boolean") { + stack.pushNumber(value ? 1 : 0); + } else if (expectedType === "number") { + stack.pushNumber(value); + } else { + stack.pushString(value); + } + } catch (e) { + throw new Error(`Custom condition '${rulePart.slice(2)}' failed: '${e.message}'`); + } + } + + for (const rulePart of rule) { + if (typeof rulePart === "string") { + const operation = operators.get(rulePart); + if (operation !== undefined) { + operation(); + } else { + const result = gm.read(stack, context, rulePart); + if (!result) { + if (rulePart.startsWith("?")) { + evalCustom(rulePart); + } else { + stack.pushString(rulePart.slice(1)); + } + } + } + } else if (typeof rulePart === "number") { + stack.pushNumber(rulePart); + } else { + stack.pushBoolean(rulePart); + } + } + return !!stack.popNumber(); +}; diff --git a/src/002-config/fc-version.js b/src/002-config/fc-version.js index b4cf149a1bf198d68d6b525754d638288db47950..8081e88f01bad0ca123c8dc1334131837a361f40 100644 --- a/src/002-config/fc-version.js +++ b/src/002-config/fc-version.js @@ -2,5 +2,5 @@ App.Version = { base: "0.10.7.1", // The vanilla version the mod is based off of, this should never be changed. pmod: "4.0.0-alpha.12", commitHash: null, - release: 1157, // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js. + release: 1158, // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js. }; diff --git a/src/data/backwardsCompatibility/datatypeCleanup.js b/src/data/backwardsCompatibility/datatypeCleanup.js index 0afc936fb02554674492c2c51968b5b924dfa709..b6d9489e6973cd518550456ab26c3aba242d0d59 100644 --- a/src/data/backwardsCompatibility/datatypeCleanup.js +++ b/src/data/backwardsCompatibility/datatypeCleanup.js @@ -2142,6 +2142,168 @@ App.Entity.Utils.RARuleDatatypeCleanup = function() { } delete cond.specialSlaves; } + + if (cond.function !== undefined) { + try { + if (typeof cond.function === "boolean") { + cond.activation = [cond.function, 1, "and"]; + } else if (cond.function === "custom") { + cond.activation = ["?bc=>" + cond.data + "(c.slave)", 1, "and"]; + } else if (cond.function === "belongs") { + switch (cond.data.attribute) { + case "amp": + cond.activation = cond.data.value[0] === 1 + ? ["visamputee", 1, "and"] + : ["visamputee", "not", 1, "and"]; + break; + case "genes": + cond.activation = ["vgenes", cond.data.value[0], "eqstr", 1, "and"]; + break; + case "fetish": + cond.activation = []; + // eslint-disable-next-line no-case-declarations + let count = 0; + for (const fetish of cond.data.value) { + count++; + cond.activation.push("vfetish", "!" + fetish, "eqstr"); + } + cond.activation.push(count, "or", 1, "and"); + } + } else if (cond.function === "between") { + let count = convertBetween(); + if (count === 0) { + count++; + cond.activation = [false]; + console.log("no match", JSON.parse(JSON.stringify(cond))); + } + cond.activation.push(count, "and"); + } + + if (!App.RA.Activation.Editor.validateRule(cond.activation)) { + cond.activation = [false, 1, "and"]; + } + } catch (e) { + console.log("condition broke", e.message, JSON.parse(JSON.stringify(cond))); + cond.activation = [false, 1, "and"]; + } finally { + delete cond.function; + delete cond.data; + } + + // assignments + try { + if (cond.assignment.length > 0) { + const rule = []; + for (const assignment of cond.assignment) { + rule.push(assigmentToKey(assignment)); + } + rule.push(rule.length, "or"); + + if (!App.RA.Activation.Editor.validateRule(rule)) { + rule = [false, 1, "or"]; + } + + cond.activation.pop(); + const length = cond.activation.pop(); + cond.activation.push(...rule); + cond.activation.push(length + 1, "and"); + if (!App.RA.Activation.Editor.validateRule(cond.activation)) { + cond.activation = [false, 1, "and"]; + } + } + } catch (e) { + console.log("assignments broke", e.message, JSON.parse(JSON.stringify(cond))); + } finally { + delete cond.assignment; + } + } + + function convertBetween() { + //switch (cond.data.attribute) { + const values = { + "devotion": "vdevotion", + "trust": "vtrust", + "health.condition": "vhealth", + "health.tired": "vfatigue", + "energy": "venergy", + "height": "vheight", + "weight": "vweight", + "actualAge": "vage", + "physicalAge": "vphysicalAge", + "visualAge": "vvisualAge", + "muscles": "vmuscles", + "pregType": "vpregType", + "bellyImplant": "vbellyImplant", + "belly": "vbelly", + "intelligenceImplant": "vintelligenceImplant", + "intelligence": "vintelligence", + "accent": "vaccent", + "waist": "vwaist", + "chem": "vchem", + "lactation": "vlactation", + }; + if (values.hasOwnProperty(cond.data.attribute)) { + return addBetween(values[cond.data.attribute]); + } + return 0; + } + + function addBetween(key) { + let count = 0; + if (cond.data.value[0] !== null) { + cond.activation.push(key, cond.data.value[0], "gte"); + count++; + } + if (cond.data.value[1] !== null) { + cond.activation.push(key, cond.data.value[1], "lte"); + count++; + } + return count; + } + + function assigmentToKey(assignment) { + const jobs = { + [Job.REST]: "vrest", + [Job.CHOICE]: "vchoice", + [Job.FUCKTOY]: "vfucktoy", + [Job.CLASSES]: "vclasses", + [Job.HOUSE]: "vhouse", + [Job.WHORE]: "vwhore", + [Job.PUBLIC]: "vpublic", + [Job.SUBORDINATE]: "vsubordinate", + [Job.MILKED]: "vmilked", + [Job.GLORYHOLE]: "vgloryhole", + [Job.CONFINEMENT]: "vconfinement", + [Job.BODYGUARD]: "vbodyguard", + [Job.RECRUITER]: "vrecruiter", + [Job.HEADGIRL]: "vheadgirl", + [Job.ARCADE]: "varcade", + [Job.MADAM]: "vmadam", + [Job.BROTHEL]: "vbrothel", + [Job.WARDEN]: "vwarden", + [Job.CELLBLOCK]: "vcellblock", + [Job.DJ]: "vdj", + [Job.CLUB]: "vclub", + [Job.NURSE]: "vnurse", + [Job.CLINIC]: "vclinic", + [Job.MILKMAID]: "vmilkmaid", + [Job.DAIRY]: "vdairy", + [Job.FARMER]: "vfarmer", + [Job.FARMYARD]: "vfarmyard", + [Job.HEADGIRLSUITE]: "vheadgirlsuite", + [Job.CONCUBINE]: "vconcubine", + [Job.MASTERSUITE]: "vmastersuite", + [Job.MATRON]: "vmatron", + [Job.NURSERY]: "vnursery", + [Job.TEACHER]: "vteacher", + [Job.SCHOOL]: "vschool", + [Job.STEWARD]: "vsteward", + [Job.QUARTER]: "vquarter", + [Job.ATTENDANT]: "vattendant", + [Job.SPA]: "vspa" + }; + return jobs[assignment]; + } } /** @param {object} o */ diff --git a/src/gui/Encyclopedia/encyclopediaBeingInCharge.js b/src/gui/Encyclopedia/encyclopediaBeingInCharge.js index a22593a9a1b6e145928a91cdcc08cf455727bfee..091847ce13606e00fc198badf89776dcebff09d1 100644 --- a/src/gui/Encyclopedia/encyclopediaBeingInCharge.js +++ b/src/gui/Encyclopedia/encyclopediaBeingInCharge.js @@ -121,19 +121,10 @@ App.Encyclopedia.addArticle("Rules Assistant", function() { App.Events.addParagraph(f, r); r = []; - r.push(App.Encyclopedia.topic("Rule activation")); - r.push("In order to apply a rule to slaves, the activation will need to be set. Choose an activation type"); - r.push(App.UI.DOM.combineNodes("(", - App.UI.DOM.makeElement("span", App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("devotion", "From Rebellious to Devoted"), ","), ["devotion", "accept"]))); - r.push(App.UI.DOM.makeElement("span", App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("trust", "Trust"), ","), ["trust", "careful"])); - r.push("sex drive,"); - r.push(App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("health", "Health"), ",")); - r.push(App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("weight", "Weight"), ",")); - r.push(App.UI.DOM.combineNodes(App.Encyclopedia.Dialog.linkDOM("muscles", "Musculature"), ",")); - r.push("lactation, pregnancy, fetuses, implant size, or age) and then choose the level at which to apply. For example to apply a rule to obedient slaves, choose"); - r.push(App.Encyclopedia.Dialog.linkDOM("devotion", "From Rebellious to Devoted", "devotion accept")); - r.push("for the activation and 4 or more for the lower limit by selecting <span class='encyclopedia interaction'>>=.</span>"); - r.push(`You can also create custom conditions using any property of a slave, which you can find documented <a target='_blank' class='link-external' href='https://gitgud.io/pregmodfan/fc-pregmod/-/raw/pregmod-master/devNotes/legacy files/slave%20variables%20documentation.md'>here.</a>`); + r.push(App.Encyclopedia.topic("Rule activation conditions")); + r.push("To have control over which slaves the rule will apply to conditions can be created."); + r.push(App.Encyclopedia.Dialog.linkDOM("In-depth explanation", "RA Condition Editor")); + r.push("of the condition editor."); App.Events.addParagraph(f, r); r = []; @@ -141,11 +132,6 @@ App.Encyclopedia.addArticle("Rules Assistant", function() { r.push("Slaves can be selected for a rule by selecting slaves from the list so that a rule can apply only to them. Slaves can similarly be excluded from a rule."); App.Events.addParagraph(f, r); - r = []; - r.push(App.Encyclopedia.topic("Applying a rule to specific assignments")); - r.push("You can apply a rule only to slaves on individual assignments by selecting them under <span class='encyclopedia interaction'>Apply to assignments.</span> For example a rule can give aphrodisiacs to slaves on whoring assignments. <span class='note'>This is mutually exclusive to automatically giving an assignment to slaves.</span>"); - App.Events.addParagraph(f, r); - r = []; r.push(App.Encyclopedia.topic("Automatically giving an assignment")); r.push("A rule can be set to automatically set a slave to an assignment when activated. For example a"); @@ -153,11 +139,6 @@ App.Encyclopedia.addArticle("Rules Assistant", function() { r.push("slave can be set to automatically be put on the whoring assignment. <span class='note'>This is mutually exclusive to applying a rule to assignments.</span>"); App.Events.addParagraph(f, r); - r = []; - r.push(App.Encyclopedia.topic("Applying a rule to facilities")); - r.push("You can apply a rule to slaves in any or all facilities as long as that facility has been constructed. The rule will only apply to slaves within the selected facilities. <span class='note'>This is mutually exclusive to automatically putting slaves into a facility.</span>"); - App.Events.addParagraph(f, r); - r = []; r.push(App.Encyclopedia.topic("Automatically assigning slaves to a facility")); r.push("A rule can be set to automatically put a slave into a facility when activated. For example disobedient slaves can be set to automatically be confined in the arcade if it has been constructed. <span class='note'>This is mutually exclusive to applying a rule to facilities.</span>"); @@ -294,6 +275,7 @@ App.Encyclopedia.addCategory("beingInCharge", function() { links.push(App.Encyclopedia.Dialog.linkDOM("Random Events", "Random Events")); links.push(App.Encyclopedia.Dialog.linkDOM("Costs Summary", "Costs Summary")); links.push(App.Encyclopedia.Dialog.linkDOM("Rules Assistant", "Rules Assistant")); + links.push(App.Encyclopedia.Dialog.linkDOM("RA Condition Editor", "RA Condition Editor")); links.push(App.Encyclopedia.Dialog.linkDOM("The Corporation", "The Corporation")); links.push(App.Encyclopedia.Dialog.linkDOM("Sexual Energy", "Sexual Energy")); links.push(App.Encyclopedia.Dialog.linkDOM("PC Skills", "PC Skills")); diff --git a/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js b/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js new file mode 100644 index 0000000000000000000000000000000000000000..173ac2d0ad92829ad947d6eadb7d313e0236074a --- /dev/null +++ b/src/gui/Encyclopedia/encyclopediaRAActivationEditor.js @@ -0,0 +1,131 @@ +App.Encyclopedia.addArticle("RA Condition Editor", function() { + /** + * @param {HTMLElement} container + * @param {string} col1 + * @param {string} col2 + * @param {string} col3 + */ + function tableHead(container, col1, col2, col3) { + App.UI.DOM.appendNewElement("div", container, col1, "head"); + App.UI.DOM.appendNewElement("div", container, col2, "head"); + App.UI.DOM.appendNewElement("div", container, col3, "head"); + } + + /** + * @param {ReadonlyMap<string, Getter<*>>} getters + */ + function getterTable(getters) { + const container = document.createElement("p"); + container.classList.add("rule-help-table"); + + tableHead(container, "Name", "Description", "Requirements"); + + for (const getter of getters.values()) { + if (getter.visible && !getter.visible()) { + continue; + } + App.UI.DOM.appendNewElement("div", container, getter.name); + App.UI.DOM.appendNewElement("div", container, getter.description); + const div = document.createElement("div"); + if (getter.requirements) { + div.append(getter.requirements); + } + container.append(div); + } + + return container; + } + + /** + * @param {HTMLElement} container + * @param {string} name + * @param {string} description + * @param {string} dataTypes + */ + function transformerRow(container, name, description, dataTypes) { + App.UI.DOM.appendNewElement("div", container, name); + App.UI.DOM.appendNewElement("div", container, description); + App.UI.DOM.appendNewElement("div", container, dataTypes); + } + + function transformerTable() { + const el = document.createElement("p"); + el.classList.add("rule-help-table"); + + tableHead(el, "Name", "Description", "Data types"); + + transformerRow(el, "And", "True, if all input values are true.", "Boolean"); + transformerRow(el, "Or", "True, if at least one input value is true.", "Boolean"); + transformerRow(el, "Sum all", "Sums up all input values", "Number"); + transformerRow(el, "Multiply all", "Multiplies all input values", "Number"); + transformerRow(el, "Maximum", "Gives the largest input value", "Number"); + transformerRow(el, "Minimum", "Gives the smallest input value", "Number"); + transformerRow(el, "==, !=", "Compares the input values based on the comparator in " + + "the middle. Both sides need to have the same data type.", "Boolean, Number, String"); + transformerRow(el, "<, >, <=, >=", "Compares the input values based on the comparator in " + + "the middle. Both sides need to have the same data type.", "Boolean, Number"); + transformerRow(el, "-", "Subtracts the second value from the first value", "Number"); + transformerRow(el, "/", "Divides the second value by the first value", "Number"); + transformerRow(el, "Contains", "True, if the second value is somewhere in the first value", + "String"); + transformerRow(el, "Not …", "Negates the input value.", "Boolean"); + transformerRow(el, "If … Then … Else …", + "If the first value is true, returns the second value, otherwise the third value. The second " + + "and third input value have to be the same data type.", "Boolean / Any"); + + return el; + } + + const acc = new SpacedTextAccumulator(); + + acc.push("Rule conditions consist of two types of elements, data getters and data transformers. Data getters can " + + "read out values from various places, which can then be used to base conditions on. Data transformers " + + "take one or more elements as input and transform the input values into a single new value. Together they " + + "can create complex conditions for activating rules."); + acc.toParagraph(); + + App.UI.DOM.appendNewElement("h2", acc.container(), "Finding errors"); + acc.push("It is possible to create element trees which are invalid, for example because the input value of an " + + "element has the wrong data type. When this is the case there will be an error message above the rule " + + "editor and the broken elements are marked. As long as there are errors the rule cannot be saved and will " + + "revert to it's previous state when leaving the RA or editing another rule. In the error message the first " + + "error corresponds to the innermost broken element. When solving errors it is advised to work from inside to " + + "outside, as outer errors are often caused by errors further inside."); + acc.toParagraph(); + + App.UI.DOM.appendNewElement("h2", acc.container(), "Data transformers"); + acc.push("Data transformers can handle 3 different data types: Boolean, Number and String. Not all transformers " + + "accept all data types. Number and Boolean can be used interchangeably, the conversion is as follows: " + + "Putting a Number into a boolean transformer will interpret 0 as false and all other values as true. " + + "Putting a Boolean into a number transformer will interpret false as 0 and true as 1."); + acc.toParagraph(); + acc.container().append(transformerTable()); + + App.UI.DOM.appendNewElement("h2", acc.container(), "Data getters"); + + acc.push("There are a number of predefined getters which read values either from a slave or from the global " + + "state. They always have a predefined data type."); + acc.toParagraph(); + + let c = acc.container(); + App.UI.DOM.appendNewElement("h3", c, "Boolean getters"); + c.append(getterTable(App.RA.Activation.getterManager.booleanGetters)); + App.UI.DOM.appendNewElement("h3", c, "Assignment getters"); + acc.push("A special type of boolean getters checking if the slave has the given assignment"); + acc.toParagraph(); + c = acc.container(); + c.append(getterTable(App.RA.Activation.getterManager.assignmentGetters)); + App.UI.DOM.appendNewElement("h3", c, "Number getters"); + c.append(getterTable(App.RA.Activation.getterManager.numberGetters)); + App.UI.DOM.appendNewElement("h3", c, "String getters"); + c.append(getterTable(App.RA.Activation.getterManager.stringGetters)); + App.UI.DOM.appendNewElement("h3", c, "Custom getters"); + acc.push("If greater freedom is required for the conditions needed, a custom data getter can be used.", + "It operates on a context object with the following properties: slave: The slave currently tested against.", + "It is required to explicitly set the return type. If the set type does not match the actual return type, " + + "the condition evaluation will fail! Documentation for slave attributes can be found " + + "<a target='_blank' class='link-external' href='https://gitgud.io/pregmodfan/fc-pregmod/-/raw/pregmod-master/devNotes/legacy files/slave%20variables%20documentation.md'>here.</a>"); + acc.toParagraph(); + + return acc.container(); +}, "beingInCharge"); diff --git a/src/js/main.js b/src/js/main.js index becda31cd2681bc718b1011526fed6693ea7b4b8..fb16243105f7d03fc3b0d29adcbd8af4fc76e5d9 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -184,7 +184,11 @@ App.MainView.full = function() { penthouseCensus(); V.costs = Math.trunc(calculateCosts.predict()); - V.currentRule = V.defaultRules[0]; + if (V.defaultRules.length > 0) { + V.currentRule = V.defaultRules[0].ID; + } else { + V.currentRule = null; + } SlaveSort.slaves(V.slaves); App.UI.SlaveList.ScrollPosition.restore(); diff --git a/src/js/rulesAssistant.js b/src/js/rulesAssistant.js index 455c1baf15b755021f050c8fdb15cb2b21acbe4d..3a3709ff5f93f2066a7df7f8a9341592641f28fb 100644 --- a/src/js/rulesAssistant.js +++ b/src/js/rulesAssistant.js @@ -108,7 +108,6 @@ globalThis.RAFacilityRemove = function(slave, rule) { globalThis.ruleAppliesP = function(rule, slave) { let V = State.variables; let cond = rule.condition; - let slaveAttribute = slave[cond.data.attribute]; // Check if slave should be excluded from having rule applied to again if (cond.applyRuleOnce) { @@ -124,51 +123,15 @@ globalThis.ruleAppliesP = function(rule, slave) { } } - // assignment / facility / special slaves / specific slaves check - if (cond.assignment.length > 0 && !cond.assignment.includes(slave.assignment)) { - return false; - } else if (cond.selectedSlaves.length > 0 && !cond.selectedSlaves.includes(slave.ID)) { + // special slaves / specific slaves check + if (cond.selectedSlaves.length > 0 && !cond.selectedSlaves.includes(slave.ID)) { return false; } else if (cond.excludedSlaves.includes(slave.ID)) { return false; } // attribute / function check - let flag = true; - switch (cond.function) { - case false: // never applies - flag = false; - break; - case "between": // between two values of a slave's attribute - if (slaveAttribute === undefined && cond.data.attribute.includes(".")) { - slaveAttribute = cond.data.attribute - .split(".") - .reduce( - (reduceSlave, attribute) => - (reduceSlave && reduceSlave[attribute] !== undefined) - ? reduceSlave[attribute] - : undefined, - slave - ); - } - // check if slave value is between rule values, if bounds exist - flag = (cond.data.value[0] === null || slaveAttribute > cond.data.value[0]) && - (cond.data.value[1] === null || slaveAttribute < cond.data.value[1]); - break; - case "belongs": // the attribute belongs in the list of values - flag = cond.data.value.includes(slave[cond.data.attribute]); - break; - case "custom": // user provided JS function - // TODO: This should use a cached Function instead of 'eval'ing - try { - flag = eval(cond.data)(slave); - } catch (e) { - // Put together a more useful message for the player. Does mean we are losing the stacktrace. - throw new Error(`Rule '${rule.name}' custom condition failed: '${e.message}'`); - } - break; - } - if (!flag) { + if (!App.RA.Activation.evaluate(slave, cond.activation)) { return false; } @@ -176,7 +139,7 @@ globalThis.ruleAppliesP = function(rule, slave) { V.rulesToApplyOnce[rule.ID].push(slave.ID); } // If rule always applies. - if (cond.applyRuleOnce && !V.rulesToApplyOnce[rule.ID].includes(slave.ID) && flag) { + if (cond.applyRuleOnce && !V.rulesToApplyOnce[rule.ID].includes(slave.ID)) { V.rulesToApplyOnce[rule.ID].push(slave.ID); } @@ -207,9 +170,7 @@ App.RA.newRule = function() { /** @returns {FC.RA.RuleConditions} */ function emptyConditions() { return { - function: false, - data: {}, - assignment: [], + activation: [true, 1, "and"], selectedSlaves: [], excludedSlaves: [], applyRuleOnce: false, @@ -489,9 +450,7 @@ App.RA.ruleDeepAssign = function deepAssign(target, source) { globalThis.initRules = function() { const rule = emptyDefaultRule(); rule.name = "Obedient Slaves"; - rule.condition.function = "between"; - rule.condition.data.attribute = "devotion"; - rule.condition.data.value = [20, null]; + rule.condition.activation = ["vdevotion", 20, "gte", 1, "and"]; V.defaultRules = [rule]; V.rulesToApplyOnce = {}; diff --git a/src/js/rulesAssistantOptions.js b/src/js/rulesAssistantOptions.js index be981014f9eecc1976b3d181b3fb01e5bc378309..c41aa7ff14e7f7d890e1d9730bf22cb3aa28b109 100644 --- a/src/js/rulesAssistantOptions.js +++ b/src/js/rulesAssistantOptions.js @@ -30,6 +30,10 @@ App.RA.options = (function() { } } root = new Root(div); + V.passageSwitchHandler = () => { + saveSettings(); + App.RA.Activation.Editor.reset(); + }; } function returnP(e) { return e.keyCode === 13; } @@ -90,11 +94,20 @@ App.RA.options = (function() { // reload the passage function reload() { + saveSettings(); + App.RA.Activation.Editor.reset(); const elem = root.element; elem.innerHTML = ""; rulesAssistantOptions(elem); } + /** + * Save the settings for this rule. + */ + function saveSettings() { + App.RA.Activation.Editor.save(cond => current_rule.condition.activation = cond); + } + const parse = { integer(string) { let n = parseInt(string, 10); @@ -1220,7 +1233,10 @@ App.RA.options = (function() { super(); this.appendChild(new OptionsItem("New Rule", newRule)); this.appendChild(new OptionsItem("Remove Rule", removeRule)); - this.appendChild(new OptionsItem("Apply rules", () => this.appendChild(new ApplicationLog()))); + this.appendChild(new OptionsItem("Apply rules", () => { + saveSettings(); + this.appendChild(new ApplicationLog()); + })); this.appendChild(new OptionsItem("Lower Priority", lowerPriority)); this.appendChild(new OptionsItem("Higher Priority", higherPriority)); this.appendChild(new OptionsItem("Rename", rename(this))); @@ -1270,355 +1286,15 @@ App.RA.options = (function() { class ConditionEditor extends Section { constructor() { super("Activation Condition"); - this.appendChild(new ConditionFunction()); - this.appendChild(new AssignmentInclusion()); - this.appendChild(new FacilityHeadAssignmentInclusion()); + this.appendChild(new ConditionBuilder()); this.appendChild(new SpecificInclusionExclusion()); this.appendChild(new ApplyRuleOnce()); } } - class ConditionFunction extends Element { - constructor() { - super(); - const items = [ - ["Never", false], - ["Always", true], - ["Custom", "custom"], - ["Devotion", "devotion"], - ["Trust", "trust"], - ["Health", "health.condition"], - ["Fatigue", "health.tired"], - ["Sex", "genes"], - ["Sex drive", "energy"], - ["Height", "height"], - ["Weight", "weight"], - ["Age", "actualAge"], - ["Body Age", "physicalAge"], - ["Visible Age", "visualAge"], - ["Muscles", "muscles"], - ["Lactation", "lactation"], - ["Pregnancy", "preg"], - ["Pregnancy Multiples", "pregType"], - ["Belly Implant", "bellyImplant"], - ["Belly Size", "belly"], - ["Education", "intelligenceImplant"], - ["Intelligence", "intelligence"], - ["Fetish", "fetish"], - ["Accent", "accent"], - ["Waist", "waist"], - ["Amputation", "amp"], - ["Carcinogen Buildup", "chem"], - ]; - this.fnlist = new List("Activation function", items, false); - this.fnlist.setValue(["between", "belongs"].includes(current_rule.condition.function) ? current_rule.condition.data.attribute : current_rule.condition.function); - this.fnlist.onchange = (value) => this.fnchanged(value); - this.appendChild(this.fnlist); - this.fneditor = null; - - switch (current_rule.condition.function) { - case false: - case true: - break; - case "custom": - this.show_custom_editor(CustomEditor, current_rule.condition.data); - break; - case "between": - this.show_custom_editor(RangeEditor, current_rule.condition.function, current_rule.condition.data); - break; - case "belongs": - this.show_custom_editor(ItemEditor, current_rule.condition.function, current_rule.condition.data); - break; - } - } - - betweenP(attribute) { - return [ - "devotion", - "trust", - "health.condition", - "health.tired", - "energy", - "height", - "weight", - "actualAge", - "physicalAge", - "visualAge", - "muscles", - "lactation", - "preg", - "pregType", - "bellyImplant", - "belly", - "intelligenceImplant", - "intelligence", - "accent", - "waist", - "chem", - ].includes(attribute); - } - - belongsP(attribute) { - return [ - "fetish", - "amp", - "genes", - ].includes(attribute); - } - - show_custom_editor(what, ...args) { - if (this.custom_editor !== null) { this.hide_custom_editor(); } - this.custom_editor = new what(...args); - this.appendChild(this.custom_editor); - } - - hide_custom_editor() { - if (this.custom_editor) { - this.custom_editor.remove(); - this.custom_editor = null; - } - } - + class ConditionBuilder extends Element { render() { - return document.createElement("div"); - } - - fnchanged(value) { - if (this.fneditor !== null) { - this.fneditor.element.remove(); - this.fneditor = null; - } - if (value === true || value === false) { - current_rule.condition.function = value; - current_rule.condition.data = {}; - this.hide_custom_editor(); - } else if (value === "custom") { - current_rule.condition.function = "custom"; - current_rule.condition.data = ""; - this.show_custom_editor(CustomEditor, current_rule.condition.data); - } else if (this.betweenP(value)) { - current_rule.condition.function = "between"; - current_rule.condition.data = {attribute: value, value: [null, null]}; - this.show_custom_editor(RangeEditor, current_rule.condition.function, current_rule.condition.data); - } else if (this.belongsP(value)) { - current_rule.condition.function = "belongs"; - current_rule.condition.data = {attribute: value, value: []}; - this.show_custom_editor(ItemEditor, current_rule.condition.function, current_rule.condition.data); - } - } - } - - class CustomEditor extends Element { - constructor(data) { - if (data.length === 0) { data = "(slave) => slave.slaveName === 'Fancy Name'"; } - super(data); - } - - render(data) { - const elem = document.createElement("div"); - const textarea = document.createElement("textarea"); - textarea.innerHTML = data; - $(textarea).blur(() => { - current_rule.condition.data = textarea.value; - // TODO: this would be a good place to cache the Function object that will be used by RuleHasError and ruleAppliesP - reload(); - }); - elem.appendChild(textarea); - - if (RuleHasError(current_rule)) { - const errorMessage = document.createElement("div"); - $(errorMessage).addClass("yellow"); - errorMessage.innerText = "WARNING: There are errors in this condition. Please ensure the syntax is correct and equality is either '==' or '===', not '='"; - elem.appendChild(errorMessage); - } - - const explanation = document.createElement("div"); - explanation.innerHTML = `Insert <kbd>(slave) =></kbd> followed by a valid <a target='_blank' class='link-external' href='https://www.w3schools.com/js/js_comparisons.asp'>JavaScript comparison and/or logical operation</a>. For variable names to use see <a target='_blank' class='link-external' href='https://gitgud.io/pregmodfan/fc-pregmod/-/raw/pregmod-master/devNotes/legacy%20files/slave%20variables%20documentation.md'>this list</a>.`; - elem.appendChild(explanation); - return elem; - } - } - - - class RangeEditor extends Element { - render(fn, data) { - const elem = document.createElement("div"); - - const minlabel = document.createElement("label"); - minlabel.innerHTML = "Lower bound: "; - elem.appendChild(minlabel); - - const min = document.createElement("input"); - min.setAttribute("type", "text"); - min.value = `${data.value[0]}`; - min.onkeypress = e => { if (returnP(e)) { this.setmin(min.value); } }; - min.onblur = e => this.setmin(min.value); - this.min = min; - elem.appendChild(min); - - elem.appendChild(document.createElement("br")); - - const maxLabel = document.createElement("label"); - maxLabel.innerHTML = "Upper bound: "; - elem.appendChild(maxLabel); - - const max = document.createElement("input"); - max.setAttribute("type", "text"); - max.value = `${data.value[1]}`; - max.onkeypress = e => { if (returnP(e)) { this.setmax(max.value); } }; - max.onblur = e => this.setmax(max.value); - this.max = max; - elem.appendChild(max); - - const infoBar = document.createElement("div"); - infoBar.innerHTML = this.info(data.attribute); - elem.appendChild(infoBar); - - return elem; - } - - parse(value) { - value = value.trim(); - if (value === "null") { - value = null; - } else { - value = parseInt(value); - if (isNaN(value)) { value = null; } - } - return value; - } - - setmin(value) { - current_rule.condition.data.value[0] = this.parse(value); - this.min.value = `${current_rule.condition.data.value[0]}`; - } - - setmax(value) { - current_rule.condition.data.value[1] = this.parse(value); - this.max.value = `${current_rule.condition.data.value[1]}`; - } - - info(attribute) { - return ({ - "devotion": "Very Hateful: (-∞, -95), Hateful: [-95, -50), Resistant: [-50, -20), Ambivalent: [-20, 20], Accepting: (20, 50], Devoted: (50, 95], Worshipful: (95, ∞)", - "trust": "Extremely terrified: (-∞, -95), Terrified: [-95, -50), Frightened: [-50, -20), Fearful: [-20, 20], Careful: (20, 50], Trusting: (50, 95], Total trust: (95, ∞)", - "health.condition": "Death: (-∞, -100), Near Death: [-100, -90), Extremely Unhealthy: [-90, -50), Unhealthy: [-50, -20), Healthy: [-20, 20], Very Healthy: (20, 50], Extremely Healthy: (50, 90], Unnaturally Healthy: (90, ∞)", - "health.tired": "Energetic: (-∞, 0], Rested: (0, 30], Tired: (30, 60], Fatigued: (60, 90], Exhausted: (90, ∞)", - "energy": "Frigid: (-∞, 20], Poor: (20, 40], Average: (40, 60], Powerful: (60, 80], Sex Addict: (80, 100), Nympho: 100", - "weight": "Emaciated: (-∞, -95), Skinny: [-95, -30), Thin: [-30, -10), Average: [-10, 10], Plush: (10, 30], Overweight: (30, 95], Fat: (95, 130], Obese: (130, 160], Super Obese: (160, 190], Dangerously Obese: (190, ∞)", - "lactation": "None: 0, 1: Natural, 2: Lactation implant", - "preg": "Barren: -2, On contraceptives: -1, Not pregnant: 0, Pregnancy weeks: [1, ∞)", - "pregType": "Fetus count, known only after the 10th week of pregnancy", - "bellyImplant": "Volume in CCs. None: -1", - "belly": "Volume in CCs, any source", - "intelligenceImplant": "Education level. 0: uneducated, 15: educated, 30: advanced education, (0, 15): incomplete education.", - "intelligence": "From moronic to brilliant: [-100, 100]", - "accent": "No accent: 0, Nice accent: 1, Bad accent: 2, Can't speak language: 3 and above", - "waist": "Masculine waist: (95, ∞), Ugly waist: (40, 95], Unattractive waist: (10, 40], Average waist: [-10, 10], Feminine waist: [-40, -10), Wasp waist: [-95, -40), Absurdly narrow: (-∞, -95)", - }[attribute] || " "); - } - } - - class ItemEditor extends Element { - render(fn, data) { - const elem = document.createElement("div"); - - const input = document.createElement("input"); - input.setAttribute("type", "text"); - input.value = JSON.stringify(data.value); - input.onkeypress = e => { if (returnP(e)) { this.setValue(input); } }; - input.onblur = e => this.setValue(input); - this.input = input; - elem.appendChild(input); - - const infoBar = document.createElement("div"); - infoBar.innerHTML = this.info(data.attribute); - elem.appendChild(infoBar); - - return elem; - } - - info(attribute) { - return `Insert a valid JSON array. Known values: ${{ - "fetish": "buttslut, cumslut, masochist, sadist, dom, submissive, boobs, pregnancy, none (AKA vanilla)", - "amp": "Amputated: 1, Not amputated: 0", - "genes": "XX, XY", - }[attribute]} Example: ["value"]`; - } - - setValue(input) { - try { - const arr = JSON.parse(input.value); - current_rule.condition.data.value = arr; - input.value = JSON.stringify(arr); - } catch (e) { - alert(e); - } - } - } - - class AssignmentInclusionBase extends ButtonList { - /** - * @param {string} label - * @param {FC.Data.JobDesc[]} [jobs] - * @param {App.Entity.Facilities.SingleJobFacility[]} [facilities] - */ - constructor(label, jobs, facilities) { - super(label); - this._attributes = {}; - if (jobs !== undefined) { - jobs.forEach(job => { - this._attributes[capFirstChar(job.position)] = job.assignment; - }); - } - if (facilities !== undefined) { - facilities.forEach(f => { - if (f.established && f.desc.defaultJob != null) { /* eslint-disable-line eqeqeq */ - const displayName = f.name === "the " + f.genericName ? f.genericName : f.name; - this._attributes[displayName] = f.desc.jobs[f.desc.defaultJob].assignment; - } - }); - } - for (const i in this._attributes) { - this.appendChild(new ButtonItem(i, this.getAttribute(i), current_rule.condition.assignment.includes(this.getAttribute(i)))); - } - } - - onchange() { - const allValues = this.getAllValues(); - current_rule.condition.assignment = this.getSelection().concat(current_rule.condition.assignment.filter(a => !allValues.includes(a))); - } - - getAttribute(what) { - return this._attributes[what]; - } - } - - - class AssignmentInclusion extends AssignmentInclusionBase { - constructor() { - let facilities = []; - for (const f of Object.values(App.Entity.facilities)) { - if (f === App.Entity.facilities.penthouse) { - continue; - } - if (f.established) { - facilities.push(f); - } - } - super("Apply to assignments and facilities", Object.values(App.Data.Facilities.penthouse.jobs), facilities); - } - } - - class FacilityHeadAssignmentInclusion extends AssignmentInclusionBase { - constructor() { - const jobs = []; - for (const f of Object.values(App.Entity.facilities)) { - if (f.established && f.desc.manager !== null) { - jobs.push(f.desc.manager); - } - } - super("Apply to facility heads", jobs); + return App.RA.Activation.Editor.build(current_rule.condition.activation); } } diff --git a/src/js/utilsDOM.js b/src/js/utilsDOM.js index 62ee6645ceefe4d17a92651963fc164221a8317d..9f159b5c139e837e69edf0ee2930f0d4492a2564 100644 --- a/src/js/utilsDOM.js +++ b/src/js/utilsDOM.js @@ -276,6 +276,7 @@ App.UI.DOM.makeTextBox = function(defaultValue, onEnter, numberOnly = false) { to 0 and trigger a change event we can't distinguish from setting the value to 0 explicitly. The workaround is resetting the value to the last known valid value and not triggering onEnter. */ + input.classList.add("number"); let oldValue = defaultValue; updateValue = event => { const newValue = Number(event.target.value); diff --git a/src/zz1-last/init.js b/src/zz1-last/init.js index e92d5bb79e823e29e756a2f17a6b5e8912fa4c70..05063454484bb3c67df767e69bd48478884facd9 100644 --- a/src/zz1-last/init.js +++ b/src/zz1-last/init.js @@ -1,5 +1,6 @@ App.Art.cacheArtData(); App.Corporate.Init(); +App.RA.Activation.populateGetters(); // TODO: remove once setup object is no longer required. for (let key in App.Data.misc) {