Skip to content
Snippets Groups Projects
rulesAssistantOptions.tw 15 KiB
Newer Older
  • Learn to ignore specific revisions
  • vas's avatar
    vas committed
    :: Rules Assistant Options [script]
    
    // rewrite of the rules assistant options page in javascript
    // uses an object-oriented widget pattern
    // wrapped in a closure so as not to polute the global namespace
    // 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
    
    
    vas's avatar
    vas committed
    window.rulesAssistantOptions = (function() {
    	"use strict"
    	
    	let V
    	let r = ""
    
    	function rulesAssistantOptions() {
    		V = State.variables
    		V.nextButton = "Back to Main"
    		V.nextLink = "Main"
    		V.returnTo = "Main"
    		V.showEncyclopedia = 1
    		V.encyclopedia = "Personal Assistant"
    		
    
    vas's avatar
    vas committed
    		let tmp = document.createElement("div")
    		tmp.classList.add("passage")
    
    vas's avatar
    vas committed
    		tmp.classList.add("rules-assistant-options")
    
    vas's avatar
    vas committed
    		document.getElementById("passages").appendChild(tmp)
    
    
    vas's avatar
    vas committed
    		const root = new Root(tmp)
    	}
    
    
    vas's avatar
    vas committed
    	function onreturn(e, cb) {
    		if (e.keyCode === 13) cb()
    	}
    
    	// create a new rule and reload
    
    vas's avatar
    vas committed
    	function newRule(root) {
    
    vas's avatar
    vas committed
    		const id = generateNewID()
    
    vas's avatar
    vas committed
    		v.defaultRules.push({
    			ID: id,
    			name: `Rule ${id}`,
    			condition: {
    				function: false,
    				data: {},
    				excludeSpecialSlaves: false,
    				assignment: [],
    				excludeAssignment: [],
    				selectedSlaves: [],
    				excludedSlaves: [],
    				facility: [],
    				excludeFacility: [],
    			},
    			set: {
    				releaseRules: "no default setting",
    				clitSetting: "no default setting",
    				clitSettingXY: "no default setting",
    				clitSettingXX: "no default setting",
    				clitSettingEnergy: "no default setting",
    				speechRules: "no default setting",
    				clothes: "no default setting",
    				collar: "no default setting",
    				shoes: "no default setting",
    				virginAccessory: "no default setting",
    				aVirginAccessory: "no default setting",
    				vaginalAccessory: "no default setting",
    				aVirginDickAccessory: "no default setting",
    				dickAccessory: "no default setting",
    				bellyAccessory: "no default setting",
    				aVirginButtplug: "no default setting",
    				buttplug: "no default setting",
    				eyeColor: "no default setting",
    				makeup: "no default setting",
    				nails: "no default setting",
    				hColor: "no default setting",
    				hLength: "no default setting",
    				hStyle: "no default setting",
    				pubicHColor: "no default setting",
    				pubicHStyle: "no default setting",
    				nipplesPiercing: "no default setting",
    				areolaePiercing: "no default setting",
    				clitPiercing: "no default setting",
    				vaginaLube: "no default setting",
    				vaginaPiercing: "no default setting",
    				dickPiercing: "no default setting",
    				anusPiercing: "no default setting",
    				lipsPiercing: "no default setting",
    				tonguePiercing: "no default setting",
    				earPiercing: "no default setting",
    				nosePiercing: "no default setting",
    				eyebrowPiercing: "no default setting",
    				navelPiercing: "no default setting",
    				corsetPiercing: "no default setting",
    				boobsTat: "no default setting",
    				buttTat: "no default setting",
    				vaginaTat: "no default setting",
    				dickTat: "no default setting",
    				lipsTat: "no default setting",
    				anusTat: "no default setting",
    				shouldersTat: "no default setting",
    				armsTat: "no default setting",
    				legsTat: "no default setting",
    				backTat: "no default setting",
    				stampTat: "no default setting",
    				curatives: "no default setting",
    				livingRules: "no default setting",
    				relationshipRules: "no default setting",
    				standardPunishment: "no default setting",
    				standardReward: "no default setting",
    				diet: "no default setting",
    				dietCum: "no default setting",
    				dietMilk: "no default setting",
    				muscles: "no default setting",
    				XY: "no default setting",
    				XX: "no default setting",
    				gelding: "no default setting",
    				preg: "no default setting",
    				growth_boobs: "no default setting",
    				growth_butt: "no default setting",
    				growth_lips: "no default setting",
    				growth_dick: "no default setting",
    				growth_balls: "no default setting",
    				aphrodisiacs: "no default setting",
    				autoSurgery: 0,
    				autoBrand: 0,
    				pornFameSpending: "no default setting",
    				dietGrowthSupport: 0,
    				eyewear: "no default setting",
    				setAssignment: "no default setting",
    				facilityRemove: false,
    				removalAssignment: "rest",
    				surgery_eyes: "no default setting",
    				surgery_lactation: "no default setting",
    				surgery_prostate: "no default setting",
    				surgery_cosmetic: "no default setting",
    				surgery_accent: "no default setting",
    				surgery_shoulders: "no default setting",
    				surgery_shouldersImplant: "no default setting",
    				surgery_boobs: "no default setting",
    				surgery_hips: "no default setting",
    				surgery_hipsImplant: "no default setting",
    				surgery_butt: "no default setting",
    				surgery_faceShape: "no default setting",
    				surgery_lips: "no default setting",
    				surgery_holes: "not default setting",
    				underArmHColor: "no default setting",
    				underArmHStyle: "no default setting",
    				drug: "no default setting",
    				eyes: "no default setting",
    				pregSpeed: "no default setting",
    				bellyImplantVol: -1,
    			}
    		})
    
    vas's avatar
    vas committed
    		V.currentRule = V.defaultRules[V.defaultRules.length-1]
    		reload(root)
    	}
    
    	function removeRule(root) {
    		const idx = V.defaultRules.findIndex(rule => rule.ID === V.currentRule.ID)
    		V.defaultRules.splice(idx, 1)
    		V.currentRule = idx < V.defaultRules.length ? idx : V.defaultRules.length - 1
    
    vas's avatar
    vas committed
    		reload(root)
    	}
    
    
    vas's avatar
    vas committed
    	function lowerPriority(root) {
    		if (V.defaultRules.length === 1) return // nothing to swap with
    		const idx = V.defaultRules.findIndex(rule => rule.ID === V.currentRule.ID)
    		if (idx === 0) return // no lower rule
    		arraySwap(V.defaultRules, idx, idx-1)
    		reload(root)
    	}
    
    	function higherPriority(root) {
    		if (V.defaultRules.length === 1) return // nothing to swap with
    		const idx = V.defaultRules.findIndex(rule => rule.ID === V.currentRule.ID)
    		if (idx === V.defaultRules.length - 1) return // no higher rule
    		arraySwap(V.defaultRules, idx, idx+1)
    		reload(root)
    	}
    
    	function changeName(name, root) {
    		if (name === V.currentRule.name) return
    		V.currentRule = name
    		reload(root)
    	}
    
    	// reload the passage
    
    vas's avatar
    vas committed
    	function reload(root) {
    		root.element.remove()
    		rulesAssistantOptions()
    
    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) {
    			this.parent = null
    			this.element = this.render(...args)
    			this.children = []
    		}
    
    		appendChild(child) {
    			child.parent = this
    			this.children.push(child)
    			this.element.appendChild(child.element)
    		}
    
    		// return the first argument to simplify creation of basic container items
    		render(...args) {
    			return args[0]
    		}
    	}
    
    	// 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
    	class List extends Element {
    		constructor(prefix, textinput=false) {
    			super(prefix, textarea)
    			this.selectedItem = null
    			this.value = this.element.querySelector(".rajs-value")
    		}
    		
    		render(prefix, textinput) {
    			const elem = document.createElement("div")
    
    vas's avatar
    vas committed
    			const label = document.createElement("span")
    			label.innerHTML = prefix
    
    vas's avatar
    vas committed
    			let value
    			if (textinput) {
    				value = document.createElement("input")
    				value.classList.add("rajs-value")
    				// call the variable binding when the input field is no longer being edited, and when the enter key is pressed
    				value.onfocusout = () => { this.propagateChange() }
    
    vas's avatar
    vas committed
    				value.onkeypress = (e) => { onreturn(e, () => { this.propagateChange() })}
    
    vas's avatar
    vas committed
    			} else {
    				value = document.createElement("strong")
    			}
    			value.setAttribute("type", "text")
    
    vas's avatar
    vas committed
    			elem.appendChild(label)
    
    vas's avatar
    vas committed
    			elem.appendChild(value)
    			elem.classList.add("rajs-list")
    			return elem
    		}
    		
    		selectItem(item) {
    			if (this.selectedItem) this.selectedItem.deselect()
    			this.selectedItem = item
    			setValue(item.setValue)
    			this.propagateChange()
    		}
    
    		setValue(what) {
    			if (this.value.tagName === "input")
    				this.value.value = what
    			else
    				this.value.innerHTML = what
    		}
    
    		getValue(what) {
    			return (this.value.tagName === "input" ? this.parse(this.value.value) : this.value.innerHTML)
    		}
    
    		// customisable input field parser / sanity checker
    		parse(what) { return what }
    
    		propagateChange() {
    			if (this.onchange instanceof Function)
    
    vas's avatar
    vas committed
    				this.onchange(this.getValue(), this.selectedItem.data)
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	const parse = {
    		integer(string) {
    			let n = parseInt(string, 10)
    			return n === NaN? 0: n
    		},
    		boobs(string) {
    			return Math.clamp(parse.integer(n), 0, 48000)
    		},
    		butt(string) {
    			return Math.clamp(parse.integer(n), 0, 10)
    		},
    		lips(string) {
    			return Math.clamp(parse.integer(n), 0, 100)
    		},
    		dick(string) {
    			return Math.clamp(parse.integer(n), 0, 10)
    		},
    		balls(string) {
    			return Math.clamp(parse.integer(n), 0, 10)
    		},
    	}
    
    
    vas's avatar
    vas committed
    	// a clickable item of a list
    	class ListItem extends Element {
    
    vas's avatar
    vas committed
    		constructor(displayvalue, setvalue, data) {
    
    vas's avatar
    vas committed
    			super(displayvalue)
    
    vas's avatar
    vas committed
    			this.setvalue = setvalue ? setvalue : displayvalue
    			this.data = this.setvalue ? this.setvalue : displayvalue
    
    vas's avatar
    vas committed
    			this.selected = false
    		}
    		
    		render(displayvalue) {
    			const elem = document.createElement("span")
    			elem.classList.add("rajs-listitem")
    			elem.innerHTML = displayvalue
    			elem.onclick = () => { return this.select() }
    			return elem
    		}
    
    		select() {
    			if (this.selected) return false
    			this.parent.selectItem(this)
    			this.elem.classList.add("selected")
    			this.selected = true
    
    vas's avatar
    vas committed
    			return true
    
    vas's avatar
    vas committed
    		}
    
    		deselect() {
    			this.elem.classList.remove("selected")
    			this.selected = false
    		}
    	}
    
    	// a way to organise lists with too many elements in subsections
    	// children are bound to the master list
    	class ListSubSection extends Element {
    		render(label) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("em")
    
    vas's avatar
    vas committed
    			elem.innerText = label + ":"
    			return elem
    		}
    		
    		appendChild(child) {
    			super.appendChild(child)
    			child.parent = this.parent
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// similar to list, but is just a collection of buttons
    
    vas's avatar
    vas committed
    	class Options extends Element {
    		constructor(elements=[]) {
    			elements.forEach(element => { this.appendChild(element) })
    		}
    
    		render() {
    			const elem = document.createElement("div")
    			elem.classList.add("rajs-list")
    			return elem
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// options equivalent of ListItem
    
    vas's avatar
    vas committed
    	class OptionsItem extends Element {
    		constructor(label, onclick) {
    			super(label)
    			this.onclick = onclick
    		}
    		render(label, onclick) {
    			const elem = document.createElement("span")
    			elem.classList.add("rajs-listitem")
    			elem.innerHTML = label
    			elem.onclick = () => { return this.onclick() }
    			return elem
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// rule import field
    
    vas's avatar
    vas committed
    	class NewRuleField extends Element {
    		constructor(root) {
    			super()
    			this.root = root
    		}
    
    		render() {
    			const container = document.createElement("div")
    			const textarea = document.createElmenet("textarea")
    			textarea.placeholder = "Paste your rule here"
    			container.appendChild(textarea)
    			this.textarea = textarea
    			const button = document.createElement("button")
    			button.name = "Load"
    			button.onclick = () => { this.loadNewRule() }
    			container.appendChild(button)
    			return container
    		}
    
    		loadNewRule() {
    			const text = this.textarea.value
    			try {
    				const rule = JSON.parse(text)
    
    vas's avatar
    vas committed
    				if (!rule.ID) rule.ID = generateNewID()
    
    vas's avatar
    vas committed
    				reload(this.root)
    			} catch (e) {
    				alert(e)
    			}
    		}
    	}
    
    
    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() {
    			super()
    
    vas's avatar
    vas committed
    			if(V.defaultRules.length === 0) {
    				const paragraph = document.createElement("p")
    				paragraph.innerHTML = "<strong>No rules</strong>"
    
    vas's avatar
    vas committed
    				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))
    		}
    
    		render(element) {
    			const greeting = document.createElement("p")
    			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>`
    			element.appendChild(greeting)
    			return element
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// optoins displayed when there are no rules
    
    vas's avatar
    vas committed
    	class NoRules extends Options {
    		constructor(root) {
    			super()
    			this.root = root
    			const newrule = new OptionsItem("Add a new rule", () => { newRule(this.root) })
    			this.appendChild(newrule)
    			const importrule = new OptionsItem("Import a rule", () => { this.root.appendChild(new NewRuleField()) })
    			this.appendChild(importrule)
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// buttons for selecting the current rule
    	class RuleSelector extends List {
    		constructor(root) {
    			super("Current rule:")
    			if (!V.currentRule) V.currentRule = V.defaultRules[0]
    			V.defaultRules.forEach(rule => {
    				const item = new ListItem(rule.name, rule.name, rule)
    				this.appendChild(item)
    				if (rule.ID === V.currentRule.ID) item.select()
    			})
    			this.onchange = function (rulename, rule) {
    				V.currentRule = rule
    				reload(root)
    			}
    		}
    	}
    
    	// buttons for doing transformations on rules
    	class RuleOptions extends Options {
    		constructor(root) {
    			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 Priotity", () => lowerPriority(root)))
    			this.appendChild(new OptionsItem("Higher Priority", () => higherPriority(root)))
    			this.appendChild(new OptionsItem("Rename", () => this.appendChild(new RenameField(root))))
    			this.appendChild(new OptionsItem("Export this rule", () => this.appendChild(new ExportField())))
    			this.appendChild(new OptionsItem("Export all rules", () => this.appendChild(new ExportField(true))))
    			this.appendChild(new OptionsItem("Import a rule", () => this.appendChild(new NewRuleField())))
    		}
    	}
    
    	class ApplicationLog extends Element {
    		render() {
    			const elem = document.createElement("div")
    			elem.innerHTML = DefaultRules()
    			return elem
    		}
    	}
    
    	class RenameField extends Element {
    		constructor(root) {
    			super()
    			this.element.onfocusout = () => changeName(this.element.value, root)
    			this.element.onkeypress = (e) => onreturn(e, () => changeName(this.element.value, root))
    		}
    
    		render() {
    			const elem = document.createElement("div")
    			elem.setAttribute("type", "text")
    			elem.setAttribute("value", V.currentRule.name)
    		}
    	}
    
    	// parent section for condition editing
    	class ConditionEditor extends Element {
    		constructor() {
    			super()
    			this.appendChild(new ConditionFunction())
    			this.appendChild(new AssignmentInclusion())
    			this.appendChild(new AssignmentExclusion())
    			this.appendChild(new FacilityInclusion())
    			this.appendChild(new SpecialExclusion())
    			this.appendChild(new SpecificInclusionExclusion())
    		}
    
    		render() {
    			const element = document.createElement("div")
    			return element
    		}
    	}
    
    	// parent section for effect editing
    	class EffectEditor extends Element {
    		constructor() {
    			// TODO
    		}
    
    		render() {
    			const element = document.createElement("div")
    			return element
    		}
    	}
    
    
    vas's avatar
    vas committed
    	return rulesAssistantOptions
    })()