Skip to content
Snippets Groups Projects
rulesAssistantOptions.js 103 KiB
Newer Older
  • Learn to ignore specific revisions
  • brickode's avatar
    brickode committed
    /* eslint-disable camelcase */
    
    brickode's avatar
    brickode committed
    /* eslint-disable no-unused-vars */
    
    vas's avatar
    vas committed
    // rewrite of the rules assistant options page in javascript
    // uses an object-oriented widget pattern
    
    klorpa's avatar
    klorpa committed
    // wrapped in a closure so as not to pollute the global namespace
    
    vas's avatar
    vas committed
    // the widgets are generic enough to be reusable; if similar user interfaces are ported to JS, we could move the classes to the global scope
    
    vas's avatar
    vas committed
    
    
    brickode's avatar
    brickode committed
    window.rulesAssistantOptions = (function() {
    
    vas's avatar
    vas committed
    	"use strict";
    
    vas's avatar
    vas committed
    	let V, current_rule;
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    	function rulesAssistantOptions(element) {
    
    vas's avatar
    vas committed
    		V = State.variables;
    		V.nextButton = "Back to Main";
    		V.nextLink = "Main";
    		V.returnTo = "Main";
    		V.showEncyclopedia = 1;
    		V.encyclopedia = "Personal Assistant";
    
    		if (V.currentRule !== null) {
    			const idx = V.defaultRules.findIndex(rule => rule.ID === V.currentRule);
    
    DCoded's avatar
    DCoded committed
    			if (idx === -1) {
    
    				current_rule = V.defaultRules[0];
    
    DCoded's avatar
    DCoded committed
    			} else {
    
    				current_rule = V.defaultRules[idx];
    
    DCoded's avatar
    DCoded committed
    			}
    
    brickode's avatar
    brickode committed
    		const root = new Root(element);
    
    vas's avatar
    vas committed
    	}
    
    
    	function returnP(e) { return e.keyCode === 13; }
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    	function newRule(root) {
    
    		const rule = emptyDefaultRule();
    		V.defaultRules.push(rule);
    
    vas's avatar
    vas committed
    		V.currentRule = rule.ID;
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    	function removeRule(root) {
    
    vas's avatar
    vas committed
    		const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
    
    vas's avatar
    vas committed
    		V.defaultRules.splice(idx, 1);
    
    		if (V.defaultRules.length > 0) {
    			const new_idx = idx < V.defaultRules.length ? idx : V.defaultRules.length - 1;
    			V.currentRule = V.defaultRules[new_idx].ID;
    
    DCoded's avatar
    DCoded committed
    		} else {
    			V.currentRule = null;
    		}
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    
    vas's avatar
    vas committed
    	function lowerPriority(root) {
    
    DCoded's avatar
    DCoded committed
    		if (V.defaultRules.length === 1) { return; } // nothing to swap with
    
    vas's avatar
    vas committed
    		const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
    
    DCoded's avatar
    DCoded committed
    		if (idx === 0) { return; } // no lower rule
    
    brickode's avatar
    brickode committed
    		arraySwap(V.defaultRules, idx, idx - 1);
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    	function higherPriority(root) {
    
    DCoded's avatar
    DCoded committed
    		if (V.defaultRules.length === 1) { return; } // nothing to swap with
    
    vas's avatar
    vas committed
    		const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
    
    DCoded's avatar
    DCoded committed
    		if (idx === V.defaultRules.length - 1) { return; } // no higher rule
    
    brickode's avatar
    brickode committed
    		arraySwap(V.defaultRules, idx, idx + 1);
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    	function changeName(name, root) {
    
    DCoded's avatar
    DCoded committed
    		if (name === current_rule.name) { return; }
    
    vas's avatar
    vas committed
    		current_rule.name = name;
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    	// reload the passage
    
    vas's avatar
    vas committed
    	function reload(root) {
    
    vas's avatar
    vas committed
    		const elem = root.element;
    
    vas's avatar
    vas committed
    		elem.innerHTML = "";
    
    vas's avatar
    vas committed
    		rulesAssistantOptions(elem);
    
    vas's avatar
    vas committed
    	}
    
    
    vas's avatar
    vas committed
    	const parse = {
    		integer(string) {
    			let n = parseInt(string, 10);
    
    brickode's avatar
    brickode committed
    			return isNaN(n) ? 0 : n;
    
    vas's avatar
    vas committed
    		},
    		boobs(string) {
    			return Math.clamp(parse.integer(string), 0, 48000);
    		},
    		butt(string) {
    
    x's avatar
    x committed
    			return Math.clamp(parse.integer(string), 0, 20);
    
    vas's avatar
    vas committed
    		},
    		lips(string) {
    			return Math.clamp(parse.integer(string), 0, 100);
    		},
    		dick(string) {
    
    x's avatar
    x committed
    			return Math.clamp(parse.integer(string), 0, 30);
    
    vas's avatar
    vas committed
    		},
    		balls(string) {
    
    x's avatar
    x committed
    			return Math.clamp(parse.integer(string), 0, 125);
    
    vas's avatar
    vas committed
    		},
    	};
    
    
    vas's avatar
    vas committed
    	// the Element class wraps around a DOM element and adds extra functionality
    	// this is safer than extending DOM objects directly
    	// it also turns DOM manipulation into an implementation detail
    	class Element {
    		constructor(...args) {
    
    vas's avatar
    vas committed
    			this.parent = null;
    			this.element = this.render(...args);
    			this.children = [];
    
    vas's avatar
    vas committed
    		}
    
    		appendChild(child) {
    
    vas's avatar
    vas committed
    			child.parent = this;
    			this.children.push(child);
    			this.element.appendChild(child.element);
    
    vas's avatar
    vas committed
    		}
    
    		// return the first argument to simplify creation of basic container items
    		render(...args) {
    
    vas's avatar
    vas committed
    			return args[0];
    
    vas's avatar
    vas committed
    		}
    
    
    		remove() {
    			const idx = this.parent.children.findIndex(child => child === this);
    			this.parent.children.slice(idx, 1);
    			this.element.remove();
    		}
    
    vas's avatar
    vas committed
    	}
    
    klorpa's avatar
    klorpa committed
    
    
    vas's avatar
    vas committed
    	class Section extends Element {
    
    brickode's avatar
    brickode committed
    		constructor(header, hidden = false) {
    
    vas's avatar
    vas committed
    			super(header);
    			this.hidey = this.element.querySelector("div");
    
    DCoded's avatar
    DCoded committed
    			if (hidden) { this.toggle_hidey(); }
    
    vas's avatar
    vas committed
    		}
    
    klorpa's avatar
    klorpa committed
    
    
    vas's avatar
    vas committed
    		render(header) {
    			const section = document.createElement("section");
    			section.classList.add("rajs-section");
    			const h1 = document.createElement("h1");
    
    brickode's avatar
    brickode committed
    			h1.onclick = () => { this.toggle_hidey(); };
    
    vas's avatar
    vas committed
    			h1.innerHTML = header;
    			const hidey = document.createElement("div");
    			section.appendChild(h1);
    			section.appendChild(hidey);
    			return section;
    		}
    
    		appendChild(child) {
    			child.parent = this;
    			this.children.push(child);
    			this.hidey.appendChild(child.element);
    		}
    
    		toggle_hidey() {
    
    brickode's avatar
    brickode committed
    			switch (this.hidey.style.display) {
    
    vas's avatar
    vas committed
    				case "none":
    					this.hidey.style.display = "initial";
    					break;
    				default:
    					this.hidey.style.display = "none";
    					break;
    			}
    		}
    	}
    
    vas's avatar
    vas committed
    
    	// list of clickable elements
    	// has a short explanation (the prefix) and a value display
    	// value display can optionally be an editable text input field
    	// it can be "bound" to a variable by setting its "onchange" method
    
    ezsh's avatar
    ezsh committed
    	class EditorWithShortcuts extends Element {
    
    brickode's avatar
    brickode committed
    		constructor(prefix, data = [], editor = false, ...args) {
    
    Skriv's avatar
    Skriv committed
    			super(`${prefix}: `, editor, ...args);
    
    vas's avatar
    vas committed
    			this.selectedItem = null;
    
    vas's avatar
    vas committed
    			data.forEach(item => this.appendChild(new ListItem(...item)));
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    
    		createEditor(...args) { return null; }
    
    ezsh's avatar
    ezsh committed
    
    		render(prefix, editor, ...args) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    			const label = document.createElement("span");
    			label.innerHTML = prefix;
    
    ezsh's avatar
    ezsh committed
    			this.value = editor ? this.createEditor(...args) : document.createElement("strong");
    
    vas's avatar
    vas committed
    			elem.appendChild(label);
    
    ezsh's avatar
    ezsh committed
    			elem.appendChild(this.value);
    
    vas's avatar
    vas committed
    			elem.classList.add("rajs-list");
    			return elem;
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    		inputEdited() {
    
    DCoded's avatar
    DCoded committed
    			if (this.selectedItem) { this.selectedItem.deselect(); }
    
    vas's avatar
    vas committed
    			this.propagateChange();
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    		selectItem(item) {
    
    DCoded's avatar
    DCoded committed
    			if (this.selectedItem) { this.selectedItem.deselect(); }
    
    vas's avatar
    vas committed
    			this.selectedItem = item;
    
    vas's avatar
    vas committed
    			this.setValue(item.data);
    
    vas's avatar
    vas committed
    			this.propagateChange();
    
    vas's avatar
    vas committed
    		}
    
    		setValue(what) {
    
    DCoded's avatar
    DCoded committed
    			if (this.value.tagName === "INPUT") {
    
    Skriv's avatar
    Skriv committed
    				this.value.value = `${what}`;
    
    DCoded's avatar
    DCoded committed
    			} else {
    
    Skriv's avatar
    Skriv committed
    				this.value.innerHTML = `${what}`;
    
    DCoded's avatar
    DCoded committed
    			}
    
    vas's avatar
    vas committed
    		}
    
    
    		getData(what) {
    
    DCoded's avatar
    DCoded committed
    			return (this.value.tagName === "INPUT" ? this.parse(this.value.value) : this.selectedItem.data);
    
    vas's avatar
    vas committed
    
    
    klorpa's avatar
    klorpa committed
    		// customizable input field parser / sanity checker
    
    		parse(what) { return what; }
    
    vas's avatar
    vas committed
    
    		propagateChange() {
    
    DCoded's avatar
    DCoded committed
    			if (this.onchange instanceof Function) {
    
    vas's avatar
    vas committed
    				this.onchange(this.getData());
    
    DCoded's avatar
    DCoded committed
    			}
    
    vas's avatar
    vas committed
    		}
    	}
    
    	// a clickable item of a list
    	class ListItem extends Element {
    
    vas's avatar
    vas committed
    		constructor(displayvalue, data) {
    
    vas's avatar
    vas committed
    			super(displayvalue);
    
    brickode's avatar
    brickode committed
    			this.data = data !== undefined ? data : displayvalue;
    
    vas's avatar
    vas committed
    			this.selected = false;
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    		render(displayvalue) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("span");
    			elem.classList.add("rajs-listitem");
    			elem.innerHTML = displayvalue;
    
    brickode's avatar
    brickode committed
    			elem.onclick = () => { return this.select(); };
    
    vas's avatar
    vas committed
    			return elem;
    
    vas's avatar
    vas committed
    		}
    
    		select() {
    
    DCoded's avatar
    DCoded committed
    			if (this.selected) { return false; }
    
    vas's avatar
    vas committed
    			this.parent.selectItem(this);
    
    vas's avatar
    vas committed
    			this.element.classList.add("selected");
    
    vas's avatar
    vas committed
    			this.selected = true;
    			return true;
    
    vas's avatar
    vas committed
    		}
    
    		deselect() {
    
    vas's avatar
    vas committed
    			this.element.classList.remove("selected");
    
    vas's avatar
    vas committed
    			this.selected = false;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    ezsh's avatar
    ezsh committed
    	class List extends EditorWithShortcuts {
    		constructor(prefix, data = [], textinput = false) {
    			super(prefix, data, textinput);
    		}
    
    		createEditor() {
    			let res = document.createElement("input");
    			res.setAttribute("type", "text");
    			res.classList.add("rajs-value"); //
    			// call the variable binding when the input field is no longer being edited, and when the enter key is pressed
    
    			res.onblur = () => {
    				this.inputEdited();
    			};
    			res.onkeypress = (e) => {
    
    DCoded's avatar
    DCoded committed
    				if (returnP(e)) { this.inputEdited(); }
    
    ezsh's avatar
    ezsh committed
    			return res;
    		}
    	}
    
    	class NumberRange extends EditorWithShortcuts {
    		constructor(prefix, data = [], min, max, spinbox = false) {
    
    			super(prefix, data, spinbox, min, max);
    
    ezsh's avatar
    ezsh committed
    			this.nullValue = data.length ? data[0][1] : null;
    		}
    
    		createEditor(min, max) {
    			let res = document.createElement("input");
    			res.setAttribute("type", "number");
    
    			res.setAttribute("min", min);
    			res.setAttribute("max", max);
    
    ezsh's avatar
    ezsh committed
    			res.classList.add("rajs-value"); //
    
    			res.onblur = () => {
    				this.inputEdited();
    			};
    			res.onkeypress = (e) => {
    
    DCoded's avatar
    DCoded committed
    				if (returnP(e)) { this.inputEdited(); }
    
    ezsh's avatar
    ezsh committed
    			return res;
    		}
    
    
    		parse(what) {
    			return what === "" ? this.nullValue : parseInt(what);
    		}
    
    klorpa's avatar
    klorpa committed
    	// a way to organize lists with too many elements in subsections
    
    vas's avatar
    vas committed
    	// children are bound to the master list
    	class ListSubSection extends Element {
    
    vas's avatar
    vas committed
    		constructor(parent, label, pairs) {
    
    vas's avatar
    vas committed
    			super(label);
    
    vas's avatar
    vas committed
    			this.parent = parent;
    
    vas's avatar
    vas committed
    			pairs.forEach(item => this.appendChild(new ListItem(...item)));
    
    vas's avatar
    vas committed
    		}
    
    klorpa's avatar
    klorpa committed
    
    
    vas's avatar
    vas committed
    		render(label) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    			const lelem = document.createElement("em");
    
    Skriv's avatar
    Skriv committed
    			lelem.innerText = `${label}: `;
    
    vas's avatar
    vas committed
    			elem.appendChild(lelem);
    
    vas's avatar
    vas committed
    			return elem;
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    		appendChild(child) {
    
    vas's avatar
    vas committed
    			super.appendChild(child);
    			child.parent = this.parent;
    			this.parent.children.push(child);
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// similar to list, but is just a collection of buttons
    
    vas's avatar
    vas committed
    	class Options extends Element {
    
    brickode's avatar
    brickode committed
    		constructor(elements = []) {
    
    vas's avatar
    vas committed
    			super();
    
    			elements.forEach(element => { this.appendChild(element); });
    
    vas's avatar
    vas committed
    		}
    
    		render() {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    			elem.classList.add("rajs-list");
    			return elem;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// options equivalent of ListItem
    
    vas's avatar
    vas committed
    	class OptionsItem extends Element {
    		constructor(label, onclick) {
    
    vas's avatar
    vas committed
    			super(label);
    			this.label = label;
    			this.onclick = onclick;
    
    vas's avatar
    vas committed
    		}
    
    brickode's avatar
    brickode committed
    		render(label, onclick) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("span");
    			elem.classList.add("rajs-listitem");
    			elem.innerHTML = label;
    
    brickode's avatar
    brickode committed
    			elem.onclick = () => { return this.onclick(this); };
    
    vas's avatar
    vas committed
    			return elem;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	class ButtonList extends Element {
    		render(label) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    			const labelel = document.createElement("span");
    
    vas's avatar
    vas committed
    			labelel.innerHTML = label += ": ";
    
    vas's avatar
    vas committed
    			elem.appendChild(labelel);
    			return elem;
    
    vas's avatar
    vas committed
    		}
    
    		getSelection() {
    			return (this.children
    				.filter(child => child.selected)
    				.map(child => child.setvalue)
    
    vas's avatar
    vas committed
    			);
    
    vas's avatar
    vas committed
    		}
    
    
    		onchange() { return; }
    
    vas's avatar
    vas committed
    	}
    
    	class ButtonItem extends Element {
    
    brickode's avatar
    brickode committed
    		constructor(label, setvalue, selected = false) {
    
    vas's avatar
    vas committed
    			super(label, selected);
    			this.selected = selected;
    			this.setvalue = setvalue ? setvalue : label;
    
    vas's avatar
    vas committed
    		}
    
    		render(label, selected) {
    
    vas's avatar
    vas committed
    			const container = document.createElement("div");
    			container.classList.add("rajs-listitem");
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			const labelel = document.createElement("span");
    			labelel.innerHTML = label;
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			const button = document.createElement("input");
    			button.setAttribute("type", "checkbox");
    			button.checked = selected;
    			button.onchange = () => this.onchange(button.checked);
    
    vas's avatar
    vas committed
    			labelel.onclick = () => button.click();
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			container.appendChild(labelel);
    			container.appendChild(button);
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			return container;
    
    vas's avatar
    vas committed
    		}
    
    		onchange(value) {
    
    vas's avatar
    vas committed
    			this.selected = value;
    
    vas's avatar
    vas committed
    			this.parent.onchange(this);
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// rule import field
    
    vas's avatar
    vas committed
    	class NewRuleField extends Element {
    		constructor(root) {
    
    vas's avatar
    vas committed
    			super();
    			this.root = root;
    
    vas's avatar
    vas committed
    		}
    
    		render() {
    
    vas's avatar
    vas committed
    			let element = document.getElementById("importfield");
    
    brickode's avatar
    brickode committed
    			if (element !== null) {
    				return element;
    			}
    
    vas's avatar
    vas committed
    			const container = document.createElement("div");
    
    vas's avatar
    vas committed
    			container.id = "importfield";
    
    vas's avatar
    vas committed
    			const textarea = document.createElement("textarea");
    
    vas's avatar
    vas committed
    			textarea.placeholder = "Paste your rule here";
    			container.appendChild(textarea);
    			this.textarea = textarea;
    			const button = document.createElement("button");
    			button.name = "Load";
    
    vas's avatar
    vas committed
    			button.innerHTML = "Load";
    
    brickode's avatar
    brickode committed
    			button.onclick = () => { this.loadNewRule(); };
    
    vas's avatar
    vas committed
    			container.appendChild(button);
    			return container;
    
    vas's avatar
    vas committed
    		}
    
    		loadNewRule() {
    
    vas's avatar
    vas committed
    			const text = this.textarea.value;
    
    vas's avatar
    vas committed
    			try {
    
    vas's avatar
    vas committed
    				const rule = JSON.parse(text);
    
    DCoded's avatar
    DCoded committed
    				if (rule instanceof Array) {
    
    vas's avatar
    vas committed
    					rule.forEach(r => V.defaultRules.push(r));
    
    DCoded's avatar
    DCoded committed
    				} else {
    
    vas's avatar
    vas committed
    					V.defaultRules.push(rule);
    
    DCoded's avatar
    DCoded committed
    				}
    
    vas's avatar
    vas committed
    				reload(this.root);
    
    vas's avatar
    vas committed
    			} catch (e) {
    
    Skriv's avatar
    Skriv committed
    				alert(`Couldn't import that rule:\n${e.message}`);
    
    vas's avatar
    vas committed
    			}
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// the base element, parent of all elements
    
    vas's avatar
    vas committed
    	class Root extends Element {
    
    vas's avatar
    vas committed
    		constructor(element) {
    			super(element);
    
    brickode's avatar
    brickode committed
    			if (V.defaultRules.length === 0) {
    
    vas's avatar
    vas committed
    				const paragraph = document.createElement("p");
    				paragraph.innerHTML = "<strong>No rules</strong>";
    				this.appendChild(new Element(paragraph));
    				this.appendChild(new NoRules(this));
    				return;
    
    vas's avatar
    vas committed
    			}
    
    vas's avatar
    vas committed
    			this.appendChild(new RuleSelector(this));
    			this.appendChild(new RuleOptions(this));
    
    vas's avatar
    vas committed
    			this.appendChild(new ConditionEditor(this));
    
    vas's avatar
    vas committed
    			this.appendChild(new EffectEditor(this));
    
    vas's avatar
    vas committed
    		}
    
    		render(element) {
    
    vas's avatar
    vas committed
    			const greeting = document.createElement("p");
    
    kopareigns's avatar
    kopareigns committed
    			greeting.innerHTML = `<em>${properTitle()}, I will review your slaves and make changes that will have a beneficial effect. Apologies, ${properTitle()}, but this function is... not fully complete. It may have some serious limitations. Please use the 'no default setting' option to identify areas I should not address.</em>`;
    
    vas's avatar
    vas committed
    			element.appendChild(greeting);
    			return element;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    klorpa's avatar
    klorpa committed
    	// options displayed when there are no rules
    
    vas's avatar
    vas committed
    	class NoRules extends Options {
    		constructor(root) {
    
    vas's avatar
    vas committed
    			super();
    			this.root = root;
    
    brickode's avatar
    brickode committed
    			const newrule = new OptionsItem("Add a new rule", () => { newRule(this.root); });
    
    vas's avatar
    vas committed
    			this.appendChild(newrule);
    
    brickode's avatar
    brickode committed
    			const importrule = new OptionsItem("Import a rule", () => { this.root.appendChild(new NewRuleField(this.root)); });
    
    vas's avatar
    vas committed
    			this.appendChild(importrule);
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// buttons for selecting the current rule
    	class RuleSelector extends List {
    		constructor(root) {
    
    vas's avatar
    vas committed
    			super("Current rule", V.defaultRules.map(i => [i.name, i]));
    
    vas's avatar
    vas committed
    			this.setValue(current_rule.name);
    
    Skriv's avatar
    Skriv committed
    			this.onchange = function(rule) {
    
    				V.currentRule = rule.ID;
    
    vas's avatar
    vas committed
    				reload(root);
    			};
    
    vas's avatar
    vas committed
    		}
    	}
    
    	// buttons for doing transformations on rules
    	class RuleOptions extends Options {
    		constructor(root) {
    
    vas's avatar
    vas committed
    			super();
    			this.appendChild(new OptionsItem("New Rule", () => newRule(root)));
    			this.appendChild(new OptionsItem("Remove Rule", () => removeRule(root)));
    			this.appendChild(new OptionsItem("Apply rules", () => this.appendChild(new ApplicationLog())));
    
    			this.appendChild(new OptionsItem("Lower Priority", () => lowerPriority(root)));
    
    vas's avatar
    vas committed
    			this.appendChild(new OptionsItem("Higher Priority", () => higherPriority(root)));
    			this.appendChild(new OptionsItem("Rename", () => this.appendChild(new RenameField(root))));
    
    vas's avatar
    vas committed
    			this.appendChild(new OptionsItem("Export this rule", () => this.appendChild(new ExportField(current_rule))));
    			this.appendChild(new OptionsItem("Export all rules", () => this.appendChild(new ExportField(...V.defaultRules))));
    
    vas's avatar
    vas committed
    			this.appendChild(new OptionsItem("Import rule(s)", () => this.appendChild(new NewRuleField(root))));
    
    vas's avatar
    vas committed
    		}
    	}
    
    	class ApplicationLog extends Element {
    		render() {
    
    vas's avatar
    vas committed
    			const elem = document.querySelector("#application-log") || document.createElement("div");
    			elem.id = "application-log";
    
    			clearSummaryCache();
    
    vas's avatar
    vas committed
    			elem.innerHTML = V.slaves.map(slave => DefaultRules(slave)).join("");
    
    vas's avatar
    vas committed
    			return elem;
    
    vas's avatar
    vas committed
    		}
    	}
    
    	class RenameField extends Element {
    		constructor(root) {
    
    vas's avatar
    vas committed
    			super();
    
    			this.element.onblur = () => changeName(this.element.value, root);
    
    DCoded's avatar
    DCoded committed
    			this.element.onkeypress = (e) => { if (returnP(e)) { changeName(this.element.value, root); } };
    
    vas's avatar
    vas committed
    		}
    
    		render() {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("input");
    
    vas's avatar
    vas committed
    			elem.setAttribute("type", "text");
    
    vas's avatar
    vas committed
    			elem.setAttribute("value", current_rule.name);
    			return elem;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	class ExportField extends Element {
    
    vas's avatar
    vas committed
    		render(...args) {
    			let element = document.getElementById("exportfield");
    			if (element === null) {
    
    vas's avatar
    vas committed
    				element = document.createElement("textarea");
    
    vas's avatar
    vas committed
    				element.id = "exportfield";
    			}
    
    vas's avatar
    vas committed
    			element.value = JSON.stringify(args, null, 2);
    
    vas's avatar
    vas committed
    			return element;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// parent section for condition editing
    
    vas's avatar
    vas committed
    	class ConditionEditor extends Section {
    
    vas's avatar
    vas committed
    		constructor() {
    
    vas's avatar
    vas committed
    			super("Activation Condition");
    
    vas's avatar
    vas committed
    			this.appendChild(new ConditionFunction());
    			this.appendChild(new AssignmentInclusion());
    
    			this.appendChild(new SpecialInclusion());
    
    vas's avatar
    vas committed
    			this.appendChild(new SpecificInclusionExclusion());
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	class ConditionFunction extends Element {
    		constructor() {
    
    vas's avatar
    vas committed
    			super();
    
    vas's avatar
    vas committed
    			const items = [
    				["Never", false],
    				["Always", true],
    				["Custom", "custom"],
    				["Devotion", "devotion"],
    				["Trust", "trust"],
    				["Health", "health"],
    
    Pregmodder's avatar
    Pregmodder committed
    				["Sex", "genes"],
    
    vas's avatar
    vas committed
    				["Sex drive", "energy"],
    
    Pregmodder's avatar
    Pregmodder committed
    				["Height", "height"],
    
    vas's avatar
    vas committed
    				["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"],
    
    vas's avatar
    vas committed
    				["Accent", "accent"],
    				["Waist", "waist"],
    
    vas's avatar
    vas committed
    				["Amputation", "amp"],
    
    vas's avatar
    vas committed
    			];
    			this.fnlist = new List("Activation function", items);
    
    vas's avatar
    vas committed
    			this.fnlist.setValue(current_rule.condition.function === "between" ? current_rule.condition.data.attribute : current_rule.condition.function);
    
    vas's avatar
    vas committed
    			this.fnlist.onchange = (value) => this.fnchanged(value);
    
    vas's avatar
    vas committed
    			this.appendChild(this.fnlist);
    
    vas's avatar
    vas committed
    			this.fneditor = null;
    
    vas's avatar
    vas committed
    
    
    brickode's avatar
    brickode committed
    			switch (current_rule.condition.function) {
    
    vas's avatar
    vas committed
    				case false:
    				case true:
    
    vas's avatar
    vas committed
    					break;
    
    vas's avatar
    vas committed
    				case "custom":
    
    					this.show_custom_editor(CustomEditor, current_rule.condition.data);
    
    vas's avatar
    vas committed
    					break;
    
    				case "between":
    
    					this.show_custom_editor(RangeEditor, current_rule.condition.function, current_rule.condition.data);
    
    vas's avatar
    vas committed
    					break;
    
    				case "belongs":
    					this.show_custom_editor(ItemEditor, current_rule.condition.function, current_rule.condition.data);
    					break;
    
    vas's avatar
    vas committed
    			}
    		}
    
    
    		betweenP(attribute) {
    			return [
    				"devotion",
    				"trust",
    				"health",
    				"energy",
    
    Pregmodder's avatar
    Pregmodder committed
    				"height",
    
    				"weight",
    				"actualAge",
    				"physicalAge",
    				"visualAge",
    				"muscles",
    				"lactation",
    				"preg",
    				"pregType",
    				"bellyImplant",
    				"belly",
    				"intelligenceImplant",
    				"intelligence",
    
    vas's avatar
    vas committed
    				"accent",
    				"waist",
    
    			].includes(attribute);
    		}
    
    		belongsP(attribute) {
    			return [
    				"fetish",
    
    vas's avatar
    vas committed
    				"amp",
    
    Pregmodder's avatar
    Pregmodder committed
    				"genes",
    
    			].includes(attribute);
    		}
    
    
    		show_custom_editor(what, ...args) {
    
    DCoded's avatar
    DCoded committed
    			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;
    			}
    		}
    
    
    vas's avatar
    vas committed
    		render() {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    			return elem;
    
    vas's avatar
    vas committed
    		}
    
    		fnchanged(value) {
    			if (this.fneditor !== null) {
    
    vas's avatar
    vas committed
    				this.fneditor.element.remove();
    				this.fneditor = null;
    
    vas's avatar
    vas committed
    			}
    
    			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";
    
    brickode's avatar
    brickode committed
    				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";
    
    brickode's avatar
    brickode committed
    				current_rule.condition.data = {attribute: value, value: []};
    
    				this.show_custom_editor(ItemEditor, current_rule.condition.function, current_rule.condition.data);
    
    vas's avatar
    vas committed
    			}
    		}
    	}
    
    	class CustomEditor extends Element {
    		constructor(data) {
    
    DCoded's avatar
    DCoded committed
    			if (data.length === 0) { data = "(slave) => slave.slaveName === 'Fancy Name'"; }
    
    vas's avatar
    vas committed
    			super(data);
    
    vas's avatar
    vas committed
    		}
    
    		render(data) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    			const textarea = document.createElement("textarea");
    			textarea.innerHTML = data;
    
    vas's avatar
    vas committed
    			textarea.onblur = () => current_rule.condition.data = textarea.value;
    
    vas's avatar
    vas committed
    			elem.appendChild(textarea);
    			const explanation = document.createElement("div");
    			explanation.innerHTML = "Insert a valid <a target='_blank' class='link-external' href='https://www.w3schools.com/js/js_comparisons.asp'>JavaScript comparison and/or logical operation</a>.";
    			elem.appendChild(explanation);
    
    vas's avatar
    vas committed
    			return elem;
    
    vas's avatar
    vas committed
    		}
    	}
    
    	class RangeEditor extends Element {
    
    vas's avatar
    vas committed
    		render(fn, data) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			const minlabel = document.createElement("label");
    			minlabel.innerHTML = "Lower bound: ";
    			elem.appendChild(minlabel);
    
    
    vas's avatar
    vas committed
    			const min = document.createElement("input");
    			min.setAttribute("type", "text");
    
    Skriv's avatar
    Skriv committed
    			min.value = `${data.value[0]}`;
    
    DCoded's avatar
    DCoded committed
    			min.onkeypress = e => { if (returnP(e)) { this.setmin(min.value); } };
    
    brickode's avatar
    brickode committed
    			min.onblur = e => this.setmin(min.value);
    
    			this.min = min;
    
    vas's avatar
    vas committed
    			elem.appendChild(min);
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			elem.appendChild(document.createElement("br"));
    
    			const maxlabel = document.createElement("label");
    			maxlabel.innerHTML = "Upper bound: ";
    			elem.appendChild(maxlabel);
    
    
    vas's avatar
    vas committed
    			const max = document.createElement("input");
    			max.setAttribute("type", "text");
    
    Skriv's avatar
    Skriv committed
    			max.value = `${data.value[1]}`;
    
    DCoded's avatar
    DCoded committed
    			max.onkeypress = e => { if (returnP(e)) { this.setmax(max.value); } };
    
    brickode's avatar
    brickode committed
    			max.onblur = e => this.setmax(max.value);
    
    			this.max = max;
    
    vas's avatar
    vas committed
    			elem.appendChild(max);
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			const infobar = document.createElement("div");
    			infobar.innerHTML = this.info(data.attribute);
    			elem.appendChild(infobar);
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			return elem;
    
    vas's avatar
    vas committed
    		}
    
    		parse(value) {
    
    			value = value.trim();
    
    DCoded's avatar
    DCoded committed
    			if (value === "null") {
    				value = null;
    			} else {
    
    vas's avatar
    vas committed
    				value = parseInt(value);
    
    DCoded's avatar
    DCoded committed
    				if (isNaN(value)) { value = null; }
    
    vas's avatar
    vas committed
    			}
    
    vas's avatar
    vas committed
    			return value;
    
    vas's avatar
    vas committed
    		}
    
    		setmin(value) {
    
    			current_rule.condition.data.value[0] = this.parse(value);
    
    Skriv's avatar
    Skriv committed
    			this.min.value = `${current_rule.condition.data.value[0]}`;
    
    vas's avatar
    vas committed
    		}
    
    		setmax(value) {
    
    			current_rule.condition.data.value[1] = this.parse(value);
    
    Skriv's avatar
    Skriv committed
    			this.max.value = `${current_rule.condition.data.value[1]}`;
    
    vas's avatar
    vas committed
    		}
    
    		info(attribute) {
    
    vas's avatar
    vas committed
    			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": "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, ∞)",
    				"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], Fat: (30, 95], Overweight: (95, ∞)",
    				"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",
    
    Pregmodder's avatar
    Pregmodder committed
    				"intelligenceImplant": "Education level. 0: uneducated, 15: educated, 30: advanced education, (0, 15): incomplete education.",
    				"intelligence": "From moronic to brilliant: [-100, 100]",
    
    vas's avatar
    vas committed
    				"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)",
    
    brickode's avatar
    brickode committed
    			} [attribute] || " ");
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    	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);
    
    DCoded's avatar
    DCoded committed
    			input.onkeypress = e => { if (returnP(e)) { this.setValue(input); } };
    
    brickode's avatar
    brickode committed
    			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) {
    
    Skriv's avatar
    Skriv committed
    			return `Insert a valid JSON array. Known values: ${{
    
    vas's avatar
    vas committed
    				"fetish": "buttslut, cumslut, masochist, sadist, dom, submissive, boobs, pregnancy, none (AKA vanilla)",
    
    vas's avatar
    vas committed
    				"amp": "Amputated: 1, Not amputated: 0",
    
    Pregmodder's avatar
    Pregmodder committed
    				"genes": "XX, XY",
    
    Skriv's avatar
    Skriv committed
    			}[attribute]}`;
    
    klorpa's avatar
    klorpa committed
    
    
    		setValue(input) {
    			try {
    				const arr = JSON.parse(input.value);
    				current_rule.condition.data.value = arr;
    				input.value = JSON.stringify(arr);
    			} catch (e) {
    				alert(e);
    			}
    		}
    	}
    
    
    
    vas's avatar
    vas committed
    	class AssignmentInclusion extends ButtonList {
    
    vas's avatar
    vas committed
    		constructor() {
    
    vas's avatar
    vas committed
    			super("Apply to assignments and facilities");
    
    klorpa's avatar
    klorpa committed
    			const items = ["Classes", "Confined", "Fucktoy", "Gloryhole", "House Servant", "Milked", "Public Servant", "Rest", "Subordinate Slave", "Whore"];
    
    DCoded's avatar
    DCoded committed
    			if (V.HGSuite > 0) {
    				items.push("Head Girl Suite");
    			}
    			if (V.brothel > 0) {
    				items.push("Brothel");
    			}
    			if (V.club > 0) {
    				items.push("Club");
    			}
    			if (V.arcade > 0) {
    				items.push("Arcade");
    			}
    			if (V.dairy > 0) {
    				items.push("Dairy");
    			}
    			if (V.servantsQuarters > 0) {
    				items.push("Servant Quarters");
    			}
    			if (V.masterSuite > 0) {
    				items.push("Master Suite");
    			}
    			if (V.schoolroom > 0) {
    				items.push("Schoolroom");
    			}
    			if (V.spa > 0) {
    				items.push("Spa");
    			}
    			if (V.nursery > 0) {
    				items.push("Nursery");
    			}
    			if (V.clinic > 0) {
    				items.push("Clinic");
    			}
    			if (V.cellblock > 0) {
    				items.push("Cellblock");
    			}
    
    vas's avatar
    vas committed
    			items.forEach(
    
    				i => this.appendChild(new ButtonItem(i, this.getAttribute(i), current_rule.condition.assignment.includes(this.getAttribute(i)))));
    
    vas's avatar
    vas committed
    		}
    
    		onchange() {
    
    vas's avatar
    vas committed
    			current_rule.condition.assignment = this.getSelection();
    
    vas's avatar
    vas committed
    		}
    
    		getAttribute(what) {
    			return {
    				"Rest": "rest",
    				"Fucktoy": "please you",
    				"Subordinate Slave": "be a subordinate slave",
    				"House Servant": "be a servant",
    				"Confined": "stay confined",
    				"Whore": "whore",
    				"Public Servant": "serve the public",
    				"Classes": "take classes",
    				"Milked": "get milked",
    				"Gloryhole": "work a glory hole",
    				"Head Girl Suite": "live with your Head Girl",
    				"Brothel": "work in the brothel",
    				"Club": "serve in the club",
    				"Arcade": "be confined in the arcade",
    				"Dairy": "work in the dairy",
    
    				"Farmyard": "work as a farmhand",
    
    vas's avatar
    vas committed
    				"Servant Quarters": "work as a servant",
    				"Master Suite": "serve in the master suite",
    				"Schoolroom": "learn in the schoolroom",
    				"Spa": "rest in the spa",
    
    brickode's avatar
    brickode committed
    				"Nursery": "work as a nanny",
    
    vas's avatar
    vas committed
    				"Clinic": "get treatment in the clinic",
    				"Cellblock": "be confined in the cellblock",
    
    brickode's avatar
    brickode committed
    			} [what];
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    	class SpecialInclusion extends List {
    
    vas's avatar
    vas committed
    		constructor() {
    
    vas's avatar
    vas committed
    			const items = [
    
    				["Include", -1],
    				["Exclude", 0],
    				["Only", 1]
    
    vas's avatar
    vas committed
    			];
    
    			super("Special slaves", items);
    			this.setValue(current_rule.condition.specialSlaves);
    			this.onchange = (value) => current_rule.condition.specialSlaves = value;
    
    vas's avatar
    vas committed
    		}
    	}
    
    	class SpecificInclusionExclusion extends Options {
    		constructor() {
    
    vas's avatar
    vas committed
    			super();
    
    vas's avatar
    vas committed
    			this.appendChild(new OptionsItem("Limit to specific slaves", () => this.show_slave_selection()));
    			this.appendChild(new OptionsItem("Exclude specific slaves", () => this.show_slave_exclusion()));
    			this.subwidget = null;
    		}
    
    		show_slave_selection() {
    
    DCoded's avatar
    DCoded committed
    			if (this.subwidget) { this.subwidget.remove(); }
    
    vas's avatar
    vas committed
    			this.subwidget = new SlaveSelection();
    			this.appendChild(this.subwidget);
    		}
    
    		show_slave_exclusion() {
    
    DCoded's avatar
    DCoded committed
    			if (this.subwidget) { this.subwidget.remove(); }
    
    vas's avatar
    vas committed
    			this.subwidget = new SlaveExclusion();
    			this.appendChild(this.subwidget);
    		}
    	}
    
    	class SlaveSelection extends ButtonList {
    		constructor() {
    			super("Include specific slaves");
    			V.slaves.forEach(slave => this.appendChild(new ButtonItem(
    				[slave.slaveName, slave.slaveSurname].join(" "),
    				slave.ID,
    				current_rule.condition.selectedSlaves.includes(slave.ID))));
    		}
    
    
    		onchange() {
    			current_rule.condition.selectedSlaves = this.getSelection();
    		}
    
    vas's avatar
    vas committed
    	}
    
    	class SlaveExclusion extends ButtonList {
    		constructor() {
    			super("Exclude specific slaves");
    			V.slaves.forEach(slave => this.appendChild(new ButtonItem(
    				[slave.slaveName, slave.slaveSurname].join(" "),
    				slave.ID,
    				current_rule.condition.excludedSlaves.includes(slave.ID))));
    		}
    
    
    		onchange() {
    			current_rule.condition.excludedSlaves = this.getSelection();
    		}
    
    vas's avatar
    vas committed
    	}
    
    
    vas's avatar
    vas committed
    	// parent section for effect editing
    	class EffectEditor extends Element {
    		constructor() {
    
    vas's avatar
    vas committed
    			super();
    			this.appendChild(new AppearanceSection());
    			this.appendChild(new CosmeticSection());
    			this.appendChild(new BodyModSection());
    			this.appendChild(new AutosurgerySection());
    			this.appendChild(new RegimenSection());
    			this.appendChild(new BehaviourSection());
    
    vas's avatar
    vas committed
    			this.appendChild(new OtherSection());
    
    vas's avatar
    vas committed
    		}
    
    		render() {
    
    vas's avatar
    vas committed
    			const element = document.createElement("div");
    			return element;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	class AppearanceSection extends Section {
    
    		constructor() {
    
    vas's avatar
    vas committed
    			super("Appearance Settings");
    
    vas's avatar
    vas committed
    			this.appendChild(new ClothesList());
    			this.appendChild(new CollarList());
    			this.appendChild(new ShoeList());
    			this.appendChild(new CorsetList());
    
    vas's avatar
    vas committed
    			this.appendChild(new LeggingsList());
    
    			this.appendChild(new VagChastityList());
    
    vas's avatar
    vas committed
    			this.appendChild(new VagAccVirginsList());
    			this.appendChild(new VagAccAVirginsList());
    			this.appendChild(new VagAccOtherList());
    
    DCoded's avatar
    DCoded committed
    			this.appendChild(new VaginalAttachmentsList());
    
    vas's avatar
    vas committed
    			if (V.seeDicks !== 0 || V.makeDicks !== 0) {
    
    				this.appendChild(new DickChastityList());
    
    vas's avatar
    vas committed
    				this.appendChild(new DickAccVirginsList());
    				this.appendChild(new DickAccOtherList());
    
    vas's avatar
    vas committed
    			}
    
    			this.appendChild(new AnalChastityList());