libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//URL

	urlParams = new URLSearchParams(window.location.search);

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//Objects

	//getKeyByValue code by SO UncleLaz: https://stackoverflow.com/questions/9907419/how-to-get-a-key-in-a-javascript-object-by-its-value
	//CC-BY-SA-4.0
	function getKeyByValue(object, value) {
	  return Object.keys(object).find(key => object[key] === value);
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//RNG

	//Random integer from 0 to n
	function randomIntegerFromZeroToValue(value) {
		var absoluteValuePlusOne = Math.abs(value) + 1;
		if(value >= 0) { //Positive case
			return Math.floor(Math.random() * absoluteValuePlusOne)
		} else { //Negative case: flip sign
			return 0 - Math.floor(Math.random() * absoluteValuePlusOne)
		};
	};

	//Random thing from array
	function randomChoice(array) {
		if(array.length === 0) { throw new Error(`The array ${array} is empty`) };
		var length = array.length;
		var randomIndex = randomIntegerFromZeroToValue(length - 1);
		return array[randomIndex];
	};

	//Random integer from m to n
	function randomIntegerBetweenTwoValues(min,max) {
		if(min > max) {
			var temp = max; //the need of a temporary space has always annoyed me
			max = min;
			min = temp;
		};
		return Math.floor(Math.random() * (max - min + 1)) + min
	};

	//cyrb128 idk where this comes from but it was in the same thread
	function cyrb128(str) {
		let h1 = 1779033703, h2 = 3144134277,
			h3 = 1013904242, h4 = 2773480762;
		for (let i = 0, k; i < str.length; i++) {
			k = str.charCodeAt(i);
			h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
			h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
			h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
			h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
		}
		h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
		h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
		h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
		h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
		return [(h1^h2^h3^h4)>>>0, (h2^h1)>>>0, (h3^h1)>>>0, (h4^h1)>>>0];
	}

	function mulberry32(a) { //Mulberry32 implemented in JS from StackOverflow, https://gist.github.com/tommyettinger/46a874533244883189143505d203312c
		return function() {
		  var t = a += 0x6D2B79F5;
		  t = Math.imul(t ^ t >>> 15, t | 1);
		  t ^= t + Math.imul(t ^ t >>> 7, t | 61);
		  return ((t ^ t >>> 14) >>> 0) / 4294967296;
		}
	} //returns random function seeded with a

	//Seeded randbetween
	function seededRandBetween(min,max,randomFunction) {
		if(min > max) {
			var temp = max;
			max = min;
			min = temp;
		};
		return Math.floor(randomFunction() * (max - min + 1)) + min
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//Arrays

	//Shallow array comparer by SO Tim Down: https://stackoverflow.com/a/10260204
	//CC-BY-SA-3.0
	function arraysIdentical(arr1, arr2) {
		var i = arr1.length;
		if (i !== arr2.length) {
			return false;
		};
		while (i--) {
			if (arr1[i] !== arr2[i]) {
				return false;
			};
		};
		return true;
	};

	function indexOf(arr, val, comparer) {
		for (var i = 0, len = arr.length; i < len; ++i) {
			if ( i in arr && comparer(arr[i], val) ) {
				return i;
			};
		};
		return -1;
	};

	function averageNumericArray(array) {
		var total = array.reduce(addTwoNumbers,0)
		return total / array.length
	};

	function sumNumericArray(array) { //Sum of array numbers
		return array.reduce((partialSum, a) => partialSum + a, 0);
	};

	function pad_array(arr,len,fill) { //https://stackoverflow.com/a/38851957
		//console.log("Padding array");
		return arr.concat(Array(len).fill(fill)).slice(0,len);
	}
	
	//Function to check if an array includes a given array by SO Johnny Tisdale: https://stackoverflow.com/a/60922255
	//CC-BY-SA-4.0
	function includesArray(parentArray, testArray) {
		for (let i = 0; i < parentArray.length; i++) {
			if (parentArray[i].every(function(value, index) { return value === testArray[index]})) {
				return true;
			};
		};
		return false;
	};

	function addArraysInPairs(array1,array2,fill=0) { //e.g. [1,2,3] + [10,0,-1] = [11,2,2]
		//console.log("Adding in pairs: " + array1 + " and " + array2 + ".");
		if(array1.length > array2.length) { //zero-padding
			array2 = pad_array(array2,array1.length,fill); //if a1 is longer, pad a2 to a1's length
		} else if(array2.length > array1.length) {
			array1 = pad_array(array1,array2.length,fill); //if a2 is longer, pad a1 to a2's length
		};
		var tempArray = [];
		for(z = 0; z < array1.length; z++) {
			//console.log("Forming output values (" + array1[z] + " + " + array2[z] + ")");
			tempArray[z] = array1[z] + array2[z];
			//console.log("Sum" + tempArray[z]);
		};
		//console.log("Added into " + tempArray + ".");
		return tempArray;
	};

	function tryJoin(stringOrArray,joiner) {
		//console.log(`tryJoin: ${stringOrArray}`);
		if(typeof(stringOrArray) === "string") {
			//console.log("tryJoin: String");
			return stringOrArray;
		} else if(Array.isArray(stringOrArray)) {
			//console.log("tryJoin: Array");
			return stringOrArray.join(joiner);
		} else {
			throw new TypeError(`Unexpected type: ${typeof(stringOrArray)}`);
		};
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//Checks

	//Element exists in the elements object
	function elementExists(elementName) {
		return typeof(elements[elementName]) === "object";
	};

	//Has a given state
	function isState(elementName,inputState) {
		if(!elementExists(elementName)) {
			throw new Error(`Element ${elementName} doesn't exist`);
		};

		var infoState = elements[elementName].state;

		if(infoState == undefined) { infoState = "undefined" };

		if(inputState == undefined) { inputState = "undefined" };
		if(inputState instanceof Array) {
			var limit = 0;
			while(inputState.includes(undefined) && limit < 3) {
				inputState[inputState.indexOf(undefined)] = "undefined"
				limit++;
			};
		};

		if(inputState instanceof Array) {
			return inputState.includes(infoState);
		};

		return infoState == inputState;
	};

	//Check if pixel of given element exists at given location
	function hasPixel(x,y,elementInput) {
		if(isEmpty(x,y,true)) { //if empty, it can't have a pixel
			return false;
		} else {
			if(elementInput.includes(",")) { //CSTA
				elementInput = elementInput.split(",");
			};
			if(Array.isArray(elementInput)) { //if element list
				for(i = 0; i < elementInput.length; i++) { if(!elementExists(elementInput[i])) { console.log(`hasPixel: Element "${elementInput[i]}" doesn't exist`) } };
				return elementInput.includes(pixelMap[x][y].element);
			} else { //if single element
				if(!elementExists(elementInput)) { console.log(`hasPixel: Element "${elementInput}" doesn't exist`) };
				return pixelMap[x][y].element === elementInput;
			};
		};		
	};

	//Is movable
	var backupCategoryWhitelist = ["land","powders","weapons","food","life","corruption","states","fey","Fantastic Creatures","dyes","energy liquids","random liquids","random gases","random rocks"];
	var backupElementWhitelist = ["mercury", "chalcopyrite_ore", "chalcopyrite_dust", "copper_concentrate", "fluxed_copper_concentrate", "unignited_pyrestone", "ignited_pyrestone", "everfire_dust", "extinguished_everfire_dust", "mistake", "polusium_oxide", "vaporized_polusium_oxide", "glowstone_dust", "redstone_dust", "soul_mud", "wet_soul_sand", "nitrogen_snow", "fusion_catalyst", "coal", "coal_coke", "blast_furnace_fuel", "molten_mythril"];

	function commonMovableCriteria(name,shallowBlacklist=null) {
		if(typeof(elements[name]) !== "object") {
			throw new Error(`Nonexistent element ${name}`);
		};
		var info = elements[name];
		//console.log(`${name} (${JSON.stringify(elements[name])})`);
		if(typeof(info.state) === "undefined") {
			var state = null;
		} else {
			var state = info.state;
		};
		if(typeof(info.category) === "undefined") {
			var category = "other";
		} else {
			var category = info.category;
		};
		if(shallowBlacklist !== null && shallowBlacklist.includes(name)) {
			return false;
		};
		if(elements[name].tool) {
			return false;
		};
		if(elements[name].behavior && elements[name].behavior.toString() == elements.wall.behavior.toString() && !elements[name].tick) {
			return false;
		};
		if(["liquid","gas"].includes(state)) {
			return true;
		};
		if(info.movable) {
			return true;
		};
		if(elements[name].behavior instanceof Array) {
			var behaviorString = elements[name].behavior.toString();
			return behaviorString.includes("M1") || behaviorString.includes("M2");
		};
		if(backupCategoryWhitelist.includes(category)) {
			return true;
		};
		if(backupElementWhitelist.includes(name)) {
			return true;
		};
		if(category.includes("mudstone")) {
			return true;
		};
		return false;
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//Math(s)

	//Base n logarithm from https://stackoverflow.com/a/3019290
	function logN(number,base) { //Vulnerable to float issues
		return Math.log(number) / Math.log(base);
	};

	//Distance between points
	function pyth(xA,yA,xB,yB) {
		var a = Math.abs(xB - xA);
		var b = Math.abs(yB - yA);
		var c = Math.sqrt(a**2 + b**2);
		return c;
	};

	//Limit number to [min, max]
	function bound(number,lowerBound,upperBound) {
		return Math.min(upperBound,Math.max(lowerBound,number));
	};
	
	//Emergency color wrapper
	rgbColorBound = function(color) {
		return bound(color,0,255);
	};

	function addTwoNumbers(number1,number2) { //reducer
		return number1 + number2
	}

	//Logistic curve
		//x = real number
		//L = maximum value
		//x_0 = "the x value of the sigmoid midpoint" i.e. the x center of the bendy part
		//k = steepness
	function logisticCurve(x,L,k,x0) {
		return L/(   1 + (  Math.E ** ( -k * (x - x0) )  )   );
	};

	// https://stackoverflow.com/questions/10756313/javascript-jquery-map-a-range-of-numbers-to-another-range-of-numbers
	// Function from August Miller
	//Map a range of numbers to another range of numbers
	function scale (number, inMin, inMax, outMin, outMax) {
		return (number - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
	}

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//Color

	function rgbStringToUnvalidatedObject(string) { //turns rgb() to {r,g,b} with no bounds checking
		//console.log("Splitting string into object");
		string = string.split(",");
		var red = parseFloat(string[0].substring(4));
		var green = parseFloat(string[1]);
		var blue = parseFloat(string[2].slice(0,-1));
		//console.log("String split: outputs " + red + ", " + green + ", " + blue + ".");
		return {r: red, g: green, b: blue};
	};

	function rgbStringToObject(string,doRounding=true,doBounding=true) { //turns rgb() to {r,g,b}
		//console.log(`rgbStringToObject: ${string}`);
			//console.log("Splitting string into object");
		string = string.split(",");
		if( (!string[0].startsWith("rgb(")) || (!string[2].endsWith(")")) ) {
			throw new Error("Color must start with \"rgb(\" and end with \")\"");
		};
		var red = parseFloat(string[0].substring(4));
		var green = parseFloat(string[1]);
		var blue = parseFloat(string[2].slice(0,-1));
			//console.log(`Colors loaded (${red}, ${green}, ${blue})`);
		//NaN checking
			var redNaN = isNaN(red);
			var greenNaN = isNaN(green);
			var blueNaN = isNaN(blue);
			var NanErrorString = "One or more colors are NaN:"
			if(redNaN) { NanErrorString += " red" };
			if(greenNaN) { NanErrorString += " green" };
			if(blueNaN) { NanErrorString += " blue" };
			if(redNaN || greenNaN || blueNaN) { throw new Error(NanErrorString) };
		if(doRounding) {
			red = Math.round(red);
			green = Math.round(green);
			blue = Math.round(blue);
				//console.log(`Colors rounded to (${red}, ${green}, ${blue})`);
		};
		if(doBounding) {
			red = bound(red,0,255)
			green = bound(green,0,255)
			blue = bound(blue,0,255)
				//console.log(`Colors bounded to (${red}, ${green}, ${blue})`);
		};
			//console.log("String split: outputs " + red + ", " + green + ", " + blue + ".");
		return {r: red, g: green, b: blue};
	};

	function hslColorStringToObject(color) {
		if(!color.startsWith("hsl(") || !color.endsWith(")")) {
			throw new Error(`The color ${color} is not a valid hsl() color`)
		};
		var colorTempArray = color.split(",")
		if(colorTempArray.length !== 3) {
			throw new Error(`The color ${color} is not a valid hsl() color`)
		};
		if(!colorTempArray[1].endsWith("%")) { console.log(`hslColorStringToObject: Saturation in color ${color} was missing a %`); colorTempArray[1] += "%"; }
		if(!colorTempArray[2].endsWith("%)")) { console.log(`hslColorStringToObject: Lightness in color ${color} was missing a %`); colorTempArray[2] = [colorTempArray[2].slice(0, colorTempArray[2].length - 1), "%", colorTempArray[2].slice(colorTempArray[2].length - 1)].join(''); }
		var hue = parseFloat(colorTempArray[0].substring(4));
		var saturation = parseFloat(colorTempArray[1].slice(0,-1))
		var lightness = parseFloat(colorTempArray[2].slice(0,-2));
		//NaN checking
            var hueNaN,saturationNaN,lightnessNaN;
			isNaN(hue) ? hueNaN = true : hueNaN = false;
			isNaN(saturation) ? saturationNaN = true : saturationNaN = false;
			isNaN(lightness) ? lightnessNaN = true : lightnessNaN = false;
			var NanErrorString = "One or more colors are NaN:"
			if(hueNaN) { NanErrorString += " hue" };
			if(saturationNaN) { NanErrorString += " saturation" };
			if(lightnessNaN) { NanErrorString += " lightness" };
			if(hueNaN || saturationNaN || lightnessNaN) { throw new Error(NanErrorString) };
		return {h: hue, s: saturation, l: lightness};
	};

	function rgbToHex(color) {
		//console.log(`rgbToHex called on ${typeof(color) === "object" ? JSON.stringify(color) : color}`);
		if(typeof(color) == "object") { //Expects object like "{r: 172, g: 11, b: 34}"
			var red = color.r;
			var green = color.g;
			var blue = color.b;
				//console.log(`Colors loaded (${red}, ${green}, ${blue})`);
			red = Math.round(red);
			green = Math.round(green);
			blue = Math.round(blue);
				//console.log(`Colors rounded to (${red}, ${green}, ${blue})`);
			red = bound(red,0,255)
			green = bound(green,0,255)
			blue = bound(blue,0,255)
				//console.log(`Colors bounded to (${red}, ${green}, ${blue})`);
			red = red.toString(16);
			green = green.toString(16);
			blue = blue.toString(16);
				//console.log(`Colors converted to (0x${red}, 0x${green}, 0x${blue})`);
			//console.log("Padding R");
			while(red.length < 2) {
				red = "0" + red;
			};
			//console.log("Padding G");
			while(green.length < 2) {
				green = "0" + green;
			};
			//console.log("Padding B");
			while(blue.length < 2) {
				blue = "0" + blue;
			};
				//console.log(`Colors padded to (0x${red}, 0x${green}, 0x${blue}), concatenating...`);
			return "#" + red + green + blue;
		} else if(typeof(color) == "string") { //Expects string like "rgb(20,137,4)". Also doesn't round properly for some reason...
				//console.log("Splitting string")
			color = rgbStringToUnvalidatedObject(color);
			red = color.r;
			green = color.g;
			blue = color.b;
				//console.log(`Colors loaded (${red}, ${green}, ${blue})`);
			red = Math.round(red);
			green = Math.round(green);
			blue = Math.round(blue);
				//console.log(`Colors rounded to (${red}, ${green}, ${blue})`);
			red = bound(red,0,255)
			green = bound(green,0,255)
			blue = bound(blue,0,255)
				//console.log(`Colors bounded to (${red}, ${green}, ${blue})`);
			red = red.toString(16);
			green = green.toString(16);
			blue = blue.toString(16);
				//console.log(`Colors converted to (0x${red}, 0x${green}, 0x${blue})`);
			//console.log("Padding R");
			while(red.length < 2) {
				red = "0" + red;
			};
			//console.log("Padding G");
			while(green.length < 2) {
				green = "0" + green;
			};
			//console.log("Padding B");
			while(blue.length < 2) {
				blue = "0" + blue;
			};
				//console.log(`Colors padded to (0x${red}, 0x${green}, 0x${blue}), concatenating...`);
			return "#" + red + green + blue;
			} else {
			throw new Error(`Received invalid color: ${color}`);
		};
	};

	function linearBlendTwoColorObjects(color1,color2,weight1=0.5) { /*third argument is for color1 and expects a float from 0
																  to 1, where 0 means "all color2" and 1 means "all color1"*/
		var w1 = Math.min(Math.max(weight1,0),1);
		var red1 = color1.r;
		var green1 = color1.g;
		var blue1 = color1.b;
		var red2 = color2.r;
		var green2 = color2.g;
		var blue2 = color2.b;
		var red3 = (red1 * w1) + (red2 * (1 - w1));
		var green3 = (green1 * w1) + (green2 * (1 - w1));
		var blue3 = (blue1 * w1) + (blue2 * (1 - w1));
		return {r: red3, g: green3, b: blue3};
	};

	function lightenColor(color,offset,outputType="rgb") {
		if(typeof(color) === "string") {
			if(color.length < 10) {
			//console.log(`detected as hex: ${color}`);
				//catch missing octothorpes
				if(!color.startsWith("#")) {
					color = "#" + color;
				};
			//console.log(`octothorpe checked: ${color}`);

				offset = parseFloat(offset);
				if(isNaN(offset)) {
					throw new Error("Offset is NaN");
				};
				
				color = hexToRGB(color);
				if(color === null) {
					throw new Error("hexToRGB(color) was null (maybe it's an invalid hex triplet?)");
				};
				
			//console.log("converted color: " + JSON.stringify(color));
				var red = color.r + offset;
				var green = color.g + offset;
				var blue = color.b + offset;
			//console.log(`altered color: rgb(${red},${green},${blue})`);
				
				//rounding and bounding
				red = Math.round(red);
				green = Math.round(green);
				blue = Math.round(blue);
			//console.log(`rounded color: rgb(${red},${green},${blue})`);
				red = bound(red,0,255)
				green = bound(green,0,255)
				blue = bound(blue,0,255)
			//console.log(`bounded color: rgb(${red},${green},${blue})`);

				color = {r: red, g: green, b: blue};

				switch(outputType.toLowerCase()) {
					case "rgb":
						return `rgb(${red},${green},${blue})`;
						break;
					case "hex":
						return rgbToHex(color);
						break;
					case "json":
						return color;
						break;
					default:
						throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
				};
			} else {
				if(color.startsWith("rgb(")) {
					color = convertColorFormats(color,"json"); //object conversion
				//console.log(`color converted to object: ${JSON.stringify(color)}`);

					offset = parseFloat(offset);
					if(isNaN(offset)) {
						throw new Error("Offset is NaN");
					};
					
					var red = color.r + offset;
					var green = color.g + offset;
					var blue = color.b + offset;
				//console.log(`altered color: rgb(${red},${green},${blue})`);
					
					//rounding and bounding
					red = Math.round(red);
					green = Math.round(green);
					blue = Math.round(blue);
				//console.log(`rounded color: rgb(${red},${green},${blue})`);
					red = bound(red,0,255)
					green = bound(green,0,255)
					blue = bound(blue,0,255)
				//console.log(`bounded color: rgb(${red},${green},${blue})`);

					color = {r: red, g: green, b: blue};

					switch(outputType.toLowerCase()) {
						case "rgb":
							return `rgb(${red},${green},${blue})`;
							break;
						case "hex":
							return rgbToHex(color);
							break;
						case "json":
							return color;
							break;
						default:
							throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
					};
				} /*else if(color.startsWith("hsl")) {
					throw new Error("HSL is not implemented yet");
				}*/ else {
					throw new Error('Color must be of the type "rgb(red,green,blue)"'/* or "hsl(hue,saturation%,luminance%)"*/);
				};
			};
		} else if(typeof(color) === "object") {
			if(typeof(color.r) === "undefined" || typeof(color.g) === "undefined" || typeof(color.b) === "undefined") {
				throw new Error("Color must be of the form {r: red, g: green, b: blue}");
			};
			
		//console.log("received color: " + JSON.stringify(color));
			var red = color.r + offset;
			var green = color.g + offset;
			var blue = color.b + offset;
		//console.log(`altered color: rgb(${red},${green},${blue})`);
			
			//rounding and bounding
			red = Math.round(red);
			green = Math.round(green);
			blue = Math.round(blue);
		//console.log(`rounded color: rgb(${red},${green},${blue})`);
			red = bound(red,0,255)
			green = bound(green,0,255)
			blue = bound(blue,0,255)
		//console.log(`bounded color: rgb(${red},${green},${blue})`);

			color = {r: red, g: green, b: blue};

			switch(outputType.toLowerCase()) {
				case "rgb":
					return `rgb(${red},${green},${blue})`;
					break;
				case "hex":
					return rgbToHex(color);
					break;
				case "json":
					return color;
					break;
				default:
					throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
			};
		};
	};

	function rgbObjectToString(color) {
		if(typeof(color) !== "object") {
			throw new Error("Input color is not an object");
		};
		var red = color.r;
		var green = color.g;
		var blue = color.b;
			//console.log(`Colors loaded (${red}, ${green}, ${blue})`);
		red = Math.round(red);
		green = Math.round(green);
		blue = Math.round(blue);
			//console.log(`Colors rounded to (${red}, ${green}, ${blue})`);
		red = bound(red,0,255)
		green = bound(green,0,255)
		blue = bound(blue,0,255)
			//console.log(`Colors bounded to (${red}, ${green}, ${blue})`);
		return `rgb(${red},${green},${blue})`
	};
	
	function convertColorFormats(color,outputType="rgb") { //Hex triplet and object to rgb(), while rgb() is untouched
		if(typeof(color) === "undefined") {
			//console.log("Warning: An element has an undefined color. Unfortunately, due to how the code is structured, I can't say which one.");
			//color = "#FF00FF";
			throw new Error("Color is undefined!");
		};
		//console.log("Logged color for convertColorFormats: " + color);
		if(typeof(color) === "string") {
			if(typeof(color) === "string" && color.length < 10) {
				//console.log(`detected as hex: ${color}`);
					//catch missing octothorpes
					if(!color.startsWith("#")) {
						color = "#" + color;
					};
				//console.log(`octothorpe checked: ${color}`);

				color = hexToRGB(color);
				if(color === null) {
					throw new Error("hexToRGB(color) was null (maybe it's an invalid hex triplet?)");
				};
				
				switch(outputType.toLowerCase()) {
					case "rgb":
						return `rgb(${color.r},${color.g},${color.b})`;
						break;
					case "hex":
						return rgbToHex(color);
						break;
					case "json":
						return color;
						break;
					case "array":
						return [color.r, color.g, color.b];
						break;
					default:
						throw new Error("outputType must be \"rgb\", \"hex\", \"json\", or \"array\"");
				};
			} else {
				if(typeof(color) === "string" && color.startsWith("rgb(")) {
					//console.log(`convertColorFormats: calling rgbStringToObject on color ${color}`);
					color = rgbStringToObject(color,true,false);
					switch(outputType.toLowerCase()) {
						case "rgb":
							if(typeof(color) === "string") { color = rgbStringToObject(color) };
							return `rgb(${color.r},${color.g},${color.b})`;
							break;
						case "hex":
							return rgbToHex(color);
							break;
						case "json":
							return color;
							break;
						case "array":
							return [color.r, color.g, color.b];
							break;
						default:
							throw new Error("outputType must be \"rgb\", \"hex\", \"json\", or \"array\"");
					};
				} else {
					throw new Error('Color must be of the type "rgb(red,green,blue)"');
				};
			};
		} else if(typeof(color) === "object") {
			switch(outputType.toLowerCase()) {
				case "rgb":
					return `rgb(${color.r},${color.g},${color.b})`;
					break;
				case "hex":
					return rgbToHex(color);
					break;
				case "json":
					return color;
					break;
				case "array":
					return [color.r, color.g, color.b];
					break;
				default:
					throw new Error("outputType must be \"rgb\", \"hex\", \"json\", or \"array\"");
			};
		};
	};
	
	function rgbHexCatcher(color) {
		return convertColorFormats(color,"rgb");
	};

	
	function averageColorObjects(color1,color2,weight1=0.5) { //misnomer, actually a linear interpolation but it's too late to rename that
		//third argument is for color1 and expects a float from 0 to 1, where 0 means "all color2" and 1 means "all color1"
		//(backwards from how it should work)
		var w1 = Math.min(Math.max(weight1,0),1)
		var red1 = color1.r
		var green1 = color1.g
		var blue1 = color1.b
		var red2 = color2.r
		var green2 = color2.g
		var blue2 = color2.b
		var red3 = (red1 * w1) + (red2 * (1 - w1))
		var green3 = (green1 * w1) + (green2 * (1 - w1))
		var blue3 = (blue1 * w1) + (blue2 * (1 - w1))
		return {r: red3, g: green3, b: blue3}
	};

	function lerpColors(color1,color2,outputType="rgb",weight1=0.5) {
		color1 = convertColorFormats(color1,"json");
		color2 = convertColorFormats(color2,"json");
		theColor = averageColorObjects(color1,color2,weight1);
		return convertColorFormats(theColor,outputType);
	};

	function multiplyColors(color1,color2,outputType="rgb") {
		//normalize rgb()/hex by turning any hex into rgb() and then rgb()s to {r,g,b}
		if(typeof(color1) !== "object") {
			color1 = convertColorFormats(color1,"json");
		};
		if(typeof(color2) !== "object") {
			color2 = convertColorFormats(color2,"json");
		};
		var finalR = Math.round(color1.r * (color2.r/255));
		var finalG = Math.round(color1.g * (color2.g/255));
		var finalB = Math.round(color1.b * (color2.b/255));
		var finalColor = {r: finalR, g: finalG, b: finalB};
		switch(outputType.toLowerCase()) {
			case "rgb":
				return `rgb(${finalColor.r},${finalColor.g},${finalColor.b})`;
				break;
			case "hex":
				return rgbToHex(finalColor);
				break;
			case "json":
				return finalColor;
				break;
			default:
				throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
		};
	};

	function divideColors(color1,color2,outputType="rgb") { //color2 is the divisor and color1 the dividend (base/original color)
		//normalize rgb()/hex by turning any hex into rgb() and then rgb()s to {r,g,b}
		if(typeof(color1) !== "object") {
			color1 = convertColorFormats(color1,"json");
		};
		if(typeof(color2) !== "object") {
			color2 = convertColorFormats(color2,"json");
		};
		var finalR = bound(Math.round(255 / (color2.r / color1.r)),0,255);
		var finalG = bound(Math.round(255 / (color2.g / color1.g)),0,255);
		var finalB = bound(Math.round(255 / (color2.b / color1.b)),0,255);
		if(isNaN(finalR)) { finalR = 255 };
		if(isNaN(finalG)) { finalG = 255 };
		if(isNaN(finalB)) { finalB = 255 };
		var finalColor = {r: finalR, g: finalG, b: finalB};
		switch(outputType.toLowerCase()) {
			case "rgb":
				return `rgb(${finalColor.r},${finalColor.g},${finalColor.b})`;
				break;
			case "hex":
				return rgbToHex(finalColor);
				break;
			case "json":
				return finalColor;
				break;
			default:
				throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
		};
	};

	function addColors(color1,color2,outputType="rgb") {
		//normalize rgb()/hex by turning any hex into rgb() and then rgb()s to {r,g,b}
		if(typeof(color1) !== "object") {
			color1 = convertColorFormats(color1,"json");
		};
		if(typeof(color2) !== "object") {
			color2 = convertColorFormats(color2,"json");
		};
		var finalR = bound(Math.round(color1.r + color2.r),0,255)
		var finalG = bound(Math.round(color1.g + color2.g),0,255)
		var finalB = bound(Math.round(color1.b + color2.b),0,255)
		var finalColor = {r: finalR, g: finalG, b: finalB};
		switch(outputType.toLowerCase()) {
			case "rgb":
				return `rgb(${finalColor.r},${finalColor.g},${finalColor.b})`;
				break;
			case "hex":
				return rgbToHex(finalColor);
				break;
			case "json":
				return finalColor;
				break;
			default:
				throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
		};
	};

	function subtractColors(color1,color2,outputType="rgb") {
		//normalize rgb()/hex by turning any hex into rgb() and then rgb()s to {r,g,b}
		if(typeof(color1) !== "object") {
			color1 = convertColorFormats(color1,"json");
		};
		if(typeof(color2) !== "object") {
			color2 = convertColorFormats(color2,"json");
		};
		var finalR = bound(Math.round(color1.r - color2.r),0,255)
		var finalG = bound(Math.round(color1.g - color2.g),0,255)
		var finalB = bound(Math.round(color1.b - color2.b),0,255)
		var finalColor = {r: finalR, g: finalG, b: finalB};
		switch(outputType.toLowerCase()) {
			case "rgb":
				return `rgb(${finalColor.r},${finalColor.g},${finalColor.b})`;
				break;
			case "hex":
				return rgbToHex(finalColor);
				break;
			case "json":
				return finalColor;
				break;
			default:
				throw new Error("outputType must be \"rgb\", \"hex\", \"json\"");
		};
	};

	function averageRgbPrefixedColorArray(colorArray,returnObject=false) { //array of rgb()s to single rgb() of average color
		//averageRgbPrefixedColorArray(["rgb(255,0,0)", "rgb(0,0,0)", "rgb(0,0,255)"]);
		//console.log("Averaging started");
		var reds = [];
		var greens = [];
		var blues = [];
		for(k = 0; k < colorArray.length; k++) {
			//console.log("Average function: Executing catcher on " + colorArray);
			var color = convertColorFormats(colorArray[k]);
			//console.log("Logged color for aRPCA: " + color);
			color = color.split(","); 
			var red = parseFloat(color[0].substring(4));
			reds.push(red)
			var green = parseFloat(color[1]);
			greens.push(green)
			var blue = parseFloat(color[2].slice(0,-1));
			blues.push(blue)
		};
		redAverage = Math.round(averageNumericArray(reds));
		greenAverage = Math.round(averageNumericArray(greens));
		blueAverage = Math.round(averageNumericArray(blues));
		var output; 
		returnObject ? output = {r: redAverage, g: greenAverage, b: blueAverage} : output = `rgb(${redAverage},${greenAverage},${blueAverage})`;
		//console.log("Averaging finished, product: " + output);
		return output;
	};

	//https://stackoverflow.com/questions/46432335/hex-to-hsl-convert-javascript
	function rgbStringToHSL(rgb,outputType="array") { //Originally a hex-to-HSL function, edited to take RGB and spit out an array
		//console.log("HSLing some RGBs");
		var result = rgbStringToUnvalidatedObject(rgb);

		var r = result.r;
		var g = result.g;
		var b = result.b;

		r /= 255, g /= 255, b /= 255;
		var max = Math.max(r, g, b), min = Math.min(r, g, b);
		var h, s, l = (max + min) / 2;

		if(max == min){
			h = s = 0; // achromatic
		} else {
			var d = max - min;
			s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
			switch(max) {
				case r: h = (g - b) / d + (g < b ? 6 : 0); break;
				case g: h = (b - r) / d + 2; break;
				case b: h = (r - g) / d + 4; break;
			}
			h /= 6;
		};

		s = s*100;
		s = Math.round(s);
		l = l*100;
		l = Math.round(l);
		h = Math.round(360*h);

		//var colorInHSL = 'hsl(' + h + ', ' + s + '%, ' + l + '%)';
		//Edit to return an array
		switch(outputType.toLowerCase()) {
			case "array":
				return [h,s,l];
				break;
			case "hsl":
				return `hsl(${h},${s}%,${l}%)`;
				break;
			case "json":
				return {h: h, s: s, l: l};
			default:
				throw new Error("outputType must be \"array\", \"hsl\", or \"json\"");
				break;				
		};
		//console.log("HSL output "+ colorInHSL + ".");
		
	};

	function normalizeColorToHslObject(color,arrayType=null) {
		var ambiguousArrayError = "changeSaturation can't tell if the array input is supposed to be RGB or HSL. Please use an \"arrayType\" argument of \"rgb\" or \"hsl\".";
		var isHsl = false;
		if(Array.isArray(color)) {
			if(arrayType === null) {
				throw new Error(ambiguousArrayError);
			} else if(arrayType === "rgb") {
				color = `rgb(${color[0]},${color[1]},${color[2]})`;
				color = rgbStringToHSL(color,"json"); //rgb arr to hsl obj
			} else if(arrayType === "hsl") {
				color = {h: color[0], s: color[1], l: color[2]}; //hsl arr to hsl obj
			} else {
				throw new Error(ambiguousArrayError);
			};
		} else {
			//by this point, any array cases would have been handled, leaving just hex (rgb), json rgb, json hsl, string rgb, and string hsl 
			if(typeof(color) === "string") {
				if(color.length < 10) { //detect hex: assume hex triplet if too short to be a well-formed rgb()
					if(!color.startsWith("#")) {
						color = "#" + color; //catch missing #
					};
					isHsl = false;
				};
				if(color.startsWith("rgb(")) { //detect rgb(): self-explanatory
					isHsl = false;
				};
				if(color.startsWith("hsl(")) { //detect hsl(): self-explanatory
					isHsl = true;
				};
			} else if(typeof(color) === "object") {
				if(typeof(color.r) !== "undefined") { //detect {r,g,b}: check for r key
					isHsl = false;
				};
				if(typeof(color.h) !== "undefined") { //detect {h,s,l}: check for h key
					isHsl = true;
				};
			};
			if(!isHsl) {
				color = convertColorFormats(color,"rgb"); //make any RGBs rgb()
				color = rgbStringToHSL(color,"json"); //make that rgb() an {h,s,l}
			} else { //by this point, it would have to either be a string or an object
				if(typeof(color) === "string") { //if it's a string
					color = hslColorStringToObject(color) //now it's an object
				};
			};
		};
		return color;		
	};

	function convertHslObjects(color,outputType="rgb") {
		switch(outputType.toLowerCase()) {
			//RGB cases
			case "rgb":
				color = hexToRGB(hslToHex(...Object.values(color))); //hsl to hex, hex to rgb_json, and rgb_json to rgb()
				return `rgb(${color.r},${color.g},${color.b})`;
				break;
			case "hex":
				color = hslToHex(...Object.values(color)); //hsl to hex
				return color;
				break;
			case "rgbjson":
			case "rgb-json":
			case "rgb_json":
				color = hexToRGB(hslToHex(...Object.values(color))); //hsl to hex and hex to rgb_json
				return color; 
				break;
			case "rgbarray":
			case "rgb-array":
			case "rgb_array":
				color = hexToRGB(hslToHex(...Object.values(color))); //hsl to hex, hex to rgb_json, and rgb_json to rgb_array
				return [color.r, color.g, color.b]; 
				break;
			//HSL cases
			case "hsl":
				//note: color was previously converted to {h, s, l}
				return `hsl(${color.h},${color.s}%,${color.l}%)`;
				break;
			case "hsljson":
			case "hsl-json":
			case "hsl_json":
				return color; 
				break;
			case "hslarray":
			case "hsl-array":
			case "hsl_array":
				return [color.h, color.s, color.l]; 
				break;
			default:
				throw new Error("outputType must be \"rgb\", \"hex\", \"rgb_json\", \"rgb_array\", \"hsl\", \"hsl_json\", or \"hsl_array\"");
		};
	}

	function changeSaturation(color,saturationChange,operationType="add",outputType="rgb",arrayType=null) {
		color = normalizeColorToHslObject(color,arrayType);
		//only {h,s,l} should exist now
		
		//Math
		switch(operationType.toLowerCase()) {
			case "+":
			case "add":
				color.s += saturationChange;
				break;
			case "-":
			case "subtract":
				color.s -= saturationChange;
				break;
			case "*":
			case "x":
			case "×":
			case "multiply":
				color.s *= saturationChange;
				break;
			case "/":
			case "÷":
			case "divide":
				color.s /= saturationChange;
				break;
			case "=":
			case "set":
				color.s = saturationChange;
				break;
			case ">":
			case ">=":
			case "min": //lower-bounds the color
				color.s = Math.max(color.s,saturationChange); //math.max to bound it to the higher of the input number or the existing color
				break;
			case "<":
			case "<=":
			case "max": //upper-bounds the color
				color.s = Math.min(color.s,saturationChange); //math.min to bound it to the lower of the input number or the existing color
				break;
			default:
				throw new Error("Operation must be \"add\", \"subtract\", \"multiply\", \"divide\", \"set\", \"min\", or \"max\"");
		};
		
		color.h = Math.round(color.h % 360);
		color.s = Math.round(bound(color.s,0,100));
		color.l = Math.round(bound(color.l,0,100));
		
		return convertHslObjects(color,outputType)
	};

	function changeLuminance(color,luminanceChange,operationType="add",outputType="rgb",arrayType=null) {
		color = normalizeColorToHslObject(color,arrayType);
		//only {h,s,l} should exist now
		
		//Math
		switch(operationType.toLowerCase()) {
			case "+":
			case "add":
				color.l += luminanceChange;
				break;
			case "-":
			case "subtract":
				color.l -= luminanceChange;
				break;
			case "*":
			case "x":
			case "×":
			case "multiply":
				color.l *= luminanceChange;
				break;
			case "/":
			case "÷":
			case "divide":
				color.l /= luminanceChange;
				break;
			case "=":
			case "set":
				color.l = luminanceChange;
				break;
			case ">":
			case ">=":
			case "min":
				color.l = Math.max(color.l,luminanceChange);
				break;
			case "<":
			case "<=":
			case "max":
				color.l = Math.min(color.l,luminanceChange);
				break;
			default:
				throw new Error("Operation must be \"add\", \"subtract\", \"multiply\", \"divide\", \"set\", \"min\", or \"max\"");
		};
		
		color.h = Math.round(color.h % 360);
		color.s = Math.round(bound(color.s,0,100));
		color.l = Math.round(bound(color.l,0,100));
		
		return convertHslObjects(color,outputType);
	};

	function changeHue(color,hueChange,operationType="add",outputType="rgb",arrayType=null) {
		color = normalizeColorToHslObject(color,arrayType);
		//only {h,s,l} should exist now
		
		//Math
		switch(operationType.toLowerCase()) {
			case "+":
			case "add":
				color.h += hueChange;
				break;
			case "-":
			case "subtract":
				color.h -= hueChange;
				break;
			case "*":
			case "x":
			case "×":
			case "multiply":
				color.h *= hueChange;
				break;
			case "/":
			case "÷":
			case "divide":
				color.h /= hueChange;
				break;
			case "=":
			case "set":
				color.h = hueChange;
				break;
			case ">":
			case ">=":
			case "min":
				color.h = Math.max(color.h,hueChange);
				break;
			case "<":
			case "<=":
			case "max":
				color.h = Math.min(color.h,hueChange);
				break;
			default:
				throw new Error("Operation must be \"add\", \"subtract\", \"multiply\", \"divide\", \"set\", \"min\", or \"max\"");
		};
		
		color.h = Math.round(color.h % 360);
		color.s = Math.round(bound(color.s,0,100));
		color.l = Math.round(bound(color.l,0,100));
		
		return convertHslObjects(color,outputType);
	};

	function colorToHsl(color,outputType="rgb") {
		color = convertColorFormats(color,"rgb");
		color = rgbStringToHSL(color,outputType);
		return color;
	};

	//https://stackoverflow.com/questions/36721830/convert-hsl-to-rgb-and-hex
	function hslToHex(h, s, l) { //h, s, l params to hex triplet
	  //console.log(`Hexing some HSLs (the HSLs are ${h},${s},${l})`)
	  s = bound(s,0,100); //limit to 0-100
	  l = bound(l,0,100);
	  l /= 100;
	  var a = s * Math.min(l, 1 - l) / 100;
	  var f = n => {
		var k = (n + h / 30) % 12;
		var color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
		return Math.round(255 * color).toString(16).padStart(2, '0');   // convert to Hex and prefix "0" if needed
	  };
	  //console.log(`Hexed to #${f(0)}${f(8)}${f(4)}`)
	  return `#${f(0)}${f(8)}${f(4)}`;
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//Pixels

	function exposedToAir(pixel) {	
		return (isEmpty(pixel.x+1,pixel.y) || isEmpty(pixel.x-1,pixel.y) || isEmpty(pixel.x,pixel.y+1) || isEmpty(pixel.x,pixel.y-1));
	};

	function tryTarnish(pixel,element,chance) {
		if(exposedToAir(pixel)) {
			if(Array.isArray(element)) {
				if(Math.random() < chance) {
					changePixel(pixel,randomChoice(element));
				};
			} else {
				if(Math.random() < chance) {
					changePixel(pixel,element);
				};
			};
		};
	};

	//Try to create a pixel, return true if it could be created and false if it couldn't
	function tryCreatePixel(elementInput,x,y) {
		//array handling
		if(elementInput.includes(",")) { //CSTA
			elementInput = elementInput.split(",");
		};
		if(Array.isArray(elementInput)) { //if element list
			elementInput = elementInput.filter(function(e) {
				return elementExists(e);
			});
			if(elementInput.length === 0) { throw new Error("elementInput has no existing elements") };
			elementInput = randomChoice(elementInput);
		};

		//existence check
		if(!elementExists(elementInput)) {
			throw new Error("Element " + elementInput + " doesn't exist!");
		};

		//actual creation check
		if(isEmpty(x,y)) {
			createPixel(elementInput,x,y);
			return true;
		} else {
			return false;
		};
	};

	function tryCreatePixelReturn(elementInput,x,y) {
		//array handling
		if(elementInput.includes(",")) { //CSTA
			elementInput = elementInput.split(",");
		};
		if(Array.isArray(elementInput)) { //if element list
			elementInput = elementInput.filter(function(e) {
				return elementExists(e);
			});
			if(elementInput.length === 0) { throw new Error("elementInput has no existing elements") };
			elementInput = randomChoice(elementInput);
		};

		//existence check
		if(!elementExists(elementInput)) {
			throw new Error("Element " + elementInput + " doesn't exist!");
		};

		//actual creation check
		if(isEmpty(x,y)) {
			return createPixelReturn(elementInput,x,y);
		} else {
			return false;
		};
	};

	function createPixelReturn(element,x,y) { //sugar
		var newPixel = new Pixel(x, y, element);
		currentPixels.push(newPixel);
		checkUnlock(element);
		return newPixel;
	};

	function changePixelReturn(pixel,element,changetemp=true) {
		pixel.element = element;
		pixel.color = pixelColorPick(pixel);
		pixel.start = pixelTicks;
		var elementInfo = elements[element];
		if (elementInfo.burning == true) {
			pixel.burning = true;
			pixel.burnStart = pixelTicks;
		}
		else if (pixel.burning && !elementInfo.burn) {
			delete pixel.burning;
			delete pixel.burnStart;
		}
		delete pixel.origColor; // remove stain
		if (pixel.r && !elementInfo.rotatable) {
			delete pixel.r;
		}
		if (pixel.flipX && !elementInfo.flippableX) {
			delete pixel.flipX;
		}
		if (pixel.flipY && !elementInfo.flippableY) {
			delete pixel.flipY;
		}
		// If elementInfo.flippableX, set it to true or false randomly
		if (elementInfo.flipX !== undefined) { pixel.flipX = elementInfo.flipX }
		else if (elementInfo.flippableX) {
			pixel.flipX = Math.random() >= 0.5;
		}
		// If elementInfo.flippableY, set it to true or false randomly
		if (elementInfo.flipY !== undefined) { pixel.flipY = elementInfo.flipY }
		else if (elementInfo.flippableY) {
			pixel.flipY = Math.random() >= 0.5;
		}
		if (elementInfo.temp != undefined && changetemp) {
			pixel.temp = elementInfo.temp;
			pixelTempCheck(pixel)
		}
		// If elementInfo.properties, set each key to its value
		if (elementInfo.properties !== undefined) {
			for (var key in elementInfo.properties) {
				// If it is an array or object, make a copy of it
				if (typeof elementInfo.properties[key] == "object") {
					pixel[key] = JSON.parse(JSON.stringify(elementInfo.properties[key]));
				}
				else {
					pixel[key] = elementInfo.properties[key];
				}
			}
		}
		checkUnlock(element);
		return pixel;
	};

	function storeFirstTouchingElement(pixel,propertyName,copyTemp=true,spread=true) {
		var info = elements[pixel.element];
		if(pixel[propertyName]) {
			return false;
		};
		for(i = 0; i < adjacentCoords.length; i++) {
			var newCoords = {x: pixel.x+adjacentCoords[i][0], y: pixel.y+adjacentCoords[i][1]};

			if (!isEmpty(newCoords.x,newCoords.y,true)) {
				newPixel = pixelMap[newCoords.x][newCoords.y];
				if (info.ignore && info.ignore.indexOf(newPixel.element) !== -1) {
					continue;
				};
				if (newPixel.element != pixel.element && newPixel.element != "wire") {
					pixel[propertyName] = newPixel.element;
					if(copyTemp) { pixel.temp = newPixel.temp };
					return newPixel.element;
				}
				else if (newPixel[propertyName] && spread) {
					pixel[propertyName] = newPixel[propertyName];
					pixel.temp = newPixel.temp;
					return newPixel[propertyName];
				}
			}
		};
	};

	function breakPixel(pixel,changetemp=false) {
		var info = elements[pixel.element];
		if(typeof(info.breakInto) === "undefined") {
			return false;
		};
		var breakIntoElement = info.breakInto;
		if(Array.isArray(breakIntoElement)) {
			breakIntoElement = breakIntoElement[Math.floor(Math.random() * breakIntoElement.length)]
		};
		changePixel(pixel,breakIntoElement,changetemp)
	};

	function tryBreak(pixel,changetemp=false,defaultBreakIntoDust=false) {
		var info = elements[pixel.element];
		var hardness = defaultHardness;
		if(typeof(info.hardness) === "number") {
			hardness = info.hardness;
		};
		hardness = 1 - hardness; //invert hardness, so a hardness of 0 becomes a 100% chance and a hardness of 1 becomes a 0% chance
		if(Math.random() < hardness) {
			return breakPixel(pixel,changetemp=false,defaultBreakIntoDust=false);
		} else {
			return false;
		};
	};

	function reactionStealer(pixel,newPixel,reactionTarget) {
		if(!elements[reactionTarget]) {
			throw new Error(`No such element ${reactionTarget}!`);
		};
		if(typeof(newPixel) === "undefined") { //timing issue?
			return false;
		};
		var newElement = newPixel.element;
		var newInfo = elements[newElement];
		if(typeof(newInfo.reactions) === "undefined") {
			return false;
		};
		if(typeof(newInfo.reactions[reactionTarget]) === "undefined") {
			return false;
		};
		var pixel2 = pixel;
		var pixel1 = newPixel;
		var r = newInfo.reactions[reactionTarget];
		
		if (r.setting && settings[r.setting]===0) {
			return false;
		}
		// r has the attribute "y" which is a range between two y values
		// r.y example: [10,30]
		// return false if y is defined and pixel1's y is not in the range
		if (r.tempMin !== undefined && pixel1.temp < r.tempMin) {
			return false;
		}
		if (r.tempMax !== undefined && pixel1.temp > r.tempMax) {
			return false;
		}
		if (r.charged && !pixel.charge) {
			return false;
		}
		if (r.chance !== undefined && Math.random() > r.chance) {
			return false;
		}
		if (r.y !== undefined && (pixel1.y < r.y[0] || pixel1.y > r.y[1])) {
			return false;
		}
		if (r.elem1 !== undefined) {
			// if r.elem1 is an array, set elem1 to a random element from the array, otherwise set it to r.elem1
			if (Array.isArray(r.elem1)) {
				var elem1 = r.elem1[Math.floor(Math.random() * r.elem1.length)];
			} else { var elem1 = r.elem1; }
			
			if (elem1 == null) {
				deletePixel(pixel1.x,pixel1.y);
			}
			else {
				changePixel(pixel1,elem1);
			}
		}
		if (r.charge1) { pixel1.charge = r.charge1; }
		if (r.temp1) { pixel1.temp += r.temp1; pixelTempCheck(pixel1); }
		if (r.color1) { // if it's a list, use a random color from the list, else use the color1 attribute
			pixel1.color = pixelColorPick(pixel1, Array.isArray(r.color1) ? r.color1[Math.floor(Math.random() * r.color1.length)] : r.color1);
		}
		if (r.attr1) { // add each attribute to pixel1
			for (var key in r.attr1) {
				pixel1[key] = r.attr1[key];
			}
		}
		if (r.elem2 !== undefined) {
			// if r.elem2 is an array, set elem2 to a random element from the array, otherwise set it to r.elem2
			if (Array.isArray(r.elem2)) {
				var elem2 = r.elem2[Math.floor(Math.random() * r.elem2.length)];
			} else { var elem2 = r.elem2; }

			if (elem2 == null) {
				deletePixel(pixel2.x,pixel2.y);
			}
			else {
				changePixel(pixel2,elem2);
			}
		}
		if (r.charge2) { pixel2.charge = r.charge2; }
		if (r.temp2) { pixel2.temp += r.temp2; pixelTempCheck(pixel2); }
		if (r.color2) { // if it's a list, use a random color from the list, else use the color2 attribute
			pixel2.color = pixelColorPick(pixel2, Array.isArray(r.color2) ? r.color2[Math.floor(Math.random() * r.color2.length)] : r.color2);
		}
		if (r.attr2) { // add each attribute to pixel2
			for (var key in r.attr2) {
				pixel2[key] = r.attr2[key];
			}
		}
		if (r.func) { r.func(pixel1,pixel2); }
		return r.elem1!==undefined || r.elem2!==undefined;
	};

	function spreadingProperty(pixel,propertyName,whitelist=null) {
		if(isNaN(pixel[propertyName])) {
			pixel[propertyName] = 0;
		};
		var randomNeighborOffset = adjacentCoords[Math.floor(Math.random() * adjacentCoords.length)];
		var rX = randomNeighborOffset[0];
		var rY = randomNeighborOffset[1];
		var rfX = pixel.x+rX;
		var rfY = pixel.y+rY;
		if(!isEmpty(rfX,rfY,true)) {
			if(!pixelMap[rfX]) {
				return false;
			};
			var rOtherPixel = pixelMap[rfX][rfY];
			var rOtherElement = rOtherPixel.element;
			if(whitelist === null || (whitelist !== null && whitelist.includes(rOtherElement))) {
				if(typeof(rOtherPixel) === "undefined" || isEmpty(rfX,rfY,true)) {
					return false;
				};
				if(isNaN(pixel[propertyName])) { //should include undefined
					pixel[propertyName] = 0;
				};
				var averageValue = (pixel[propertyName] + rOtherPixel[propertyName]) / 2;
				pixel[propertyName] = averageValue;
				rOtherPixel[propertyName] = averageValue;
			};
		};
		return true;
	};

	function spreadingPropertyReturn(pixel,propertyName,whitelist=null) {
		if(isNaN(pixel[propertyName])) {
			pixel[propertyName] = 0;
		};
		var recipients = []; //will never be more than one but done with [] for forEach
		var randomNeighborOffset = adjacentCoords[Math.floor(Math.random() * adjacentCoords.length)];
		var rX = randomNeighborOffset[0];
		var rY = randomNeighborOffset[1];
		var rfX = pixel.x+rX;
		var rfY = pixel.y+rY;
		if(!isEmpty(rfX,rfY,true)) {
			if(!pixelMap[rfX]) {
				return [];
			};
			var rOtherPixel = pixelMap[rfX][rfY];
			var rOtherElement = rOtherPixel.element;
			if(whitelist === null || (whitelist !== null && whitelist.includes(rOtherElement))) {
				if(typeof(rOtherPixel) === "undefined" || isEmpty(rfX,rfY,true)) {
					return [];
				};
				if(isNaN(pixel[propertyName])) { //should include undefined
					pixel[propertyName] = 0;
				};
				var averageValue = (pixel[propertyName] + rOtherPixel[propertyName]) / 2;
				pixel[propertyName] = averageValue;
				rOtherPixel[propertyName] = averageValue;
				recipients.push(rOtherPixel);
			};
		};
		return recipients;
	};

	function swapNumericPropertyValues(pixel1,pixel2,propertyName,whitelist=null) {
		if(!pixel1 || !pixel2) {
			return false;
		};
		if(isNaN(pixel1[propertyName])) {
			pixel1[propertyName] = 0;
		};
		if(isNaN(pixel2[propertyName])) {
			pixel2[propertyName] = 0;
		};
		if(whitelist === null || (whitelist !== null && whitelist.includes(pixel1.element) && whitelist.includes(pixel2.element))) {
			var temp1 = pixel1[propertyName];
			pixel1[propertyName] = pixel2[propertyName];
			pixel2[propertyName] = temp1;
		};
		return true;
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//World

	function breakCircle(x,y,radius,respectHardness=false,changeTemp=false,defaultBreakIntoDust=false) {
		var coords = circleCoords(x,y,radius);
		for(i = 0; i < coords.length; i++) {
			coordX = coords[i].x;
			coordY = coords[i].y;
			if(!isEmpty(coordX,coordY,true)) {
				var pixel = pixelMap[coordX][coordY];
				respectHardness ? tryBreak(pixel,changeTemp,defaultBreakIntoDust) : breakPixel(pixel,changeTemp,defaultBreakIntoDust);
			};
		};
	};
	
	function fillCircle(element,x,y,radius,overwrite=false) {
		var coords = circleCoords(x,y,radius);
		var newElement = element;
		if(Array.isArray(newElement)) {
			newElement = newElement[Math.floor(Math.random() * newElement.length)];
		};
		for(i = 0; i < coords.length; i++) {
			coordX = Math.round(coords[i].x);
			coordY = Math.round(coords[i].y);
			if(overwrite && !isEmpty(coordX,coordY,true)) {
				changePixel(pixelMap[coordX][coordY],element);
			};
			if(isEmpty(coordX,coordY,false)) {
				createPixel(element,coordX,coordY);
			};
		};
	};

	function fillCircleReturn(element,x,y,radius,overwrite=false) {
		//console.log("fcr");
		var pixels = [];
		//console.log("pixels initted");
		var coords = circleCoords(x,y,radius);
		//console.log("coords gotten");
		var newElement = element;
		//console.log("element processing");
		if(Array.isArray(newElement)) {
			newElement = newElement[Math.floor(Math.random() * newElement.length)];
		};
		//console.log("element processed");
		for(i = 0; i < coords.length; i++) {
			//console.log("iterator through spots: " + i);
			coordX = Math.round(coords[i].x);
			coordY = Math.round(coords[i].y);
			//console.log(`coord: (${coords[i].x},${coords[i].y})`);
			if(overwrite && !isEmpty(coordX,coordY,true)) {
				//console.log("replaced pixel");
				pixels.push(changePixelReturn(pixelMap[coordX][coordY],newElement));
			};
			if(isEmpty(coordX,coordY,false)) {
				//console.log("created pixel");
				pixels.push(createPixelReturn(newElement,coordX,coordY));
			};
		};
		//console.log("fcr finished");
		//console.log(pixels.map(x => x.element));
		return pixels;
	};

	function isOpenAndOnSurface(x,y,includeBottomBound=true) {
		if(!isEmpty(x,y,false)) {
			return false;
		};
		if(y + 1 == height) {
			return includeBottomBound;
		};
		return !isEmpty(x,y+1,true);
	};

	//Freeze pixel
	function freezePixel(pixel,changetemp=true) {
		var info = elements[pixel.element];
		var result = info.stateLow;
		if (!result) {
			return false
		};
		if(result instanceof Array) {
			result = result.filter(elementExists);
			if(result.length == 0) {
				return false;
			};
		} else {
			if(!(elementExists(result))) {
				return false;
			};
		};
		
		while(result instanceof Array) {
			result = randomChoice(result);
		};
		
		changePixel(pixel,result,changetemp);
		return true;
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//Logic

	function xor(c1,c2) {
		if(!!c1 && !c2) {
			return true;
		} else if(!c1 && !!c2) {
			return true;
		} else {
			return false;
		};
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//currentPixels operations

	function findInCurrentPixels(x,y) {
		var pixel = currentPixels.filter(function(pixelObject) {
			return pixelObject.x == x && pixelObject.y == y;
		});
		if(pixel.length <= 0) {
			return undefined;
		};
		if(pixel.length > 1) {
			pixel.length = 1;
		};
		pixel = pixel[0];
		return pixel;
	};

	function filterCurrentPixels(filterFunction) {
		return currentPixels.filter(filterFunction);
	};
	
	//Filter test functions

	function _filterTest_xIsTwenty(pixel) {
		return pixel.x == 20;
	};

	function _filterTest_tempIsOdd(pixel) {
		return Math.trunc(pixel.temp) % 2 == 1;
	};

	function _filterTest_redRock(pixel) {
		if(typeof(convertColorFormats) === "undefined") {
			throw new Error("code_library.js is required!");
		};
		var color = rgbStringToHSL(convertColorFormats(pixel.color,"rgb"),"json");
		var isRed = ((color.h % 360) >= 350) || ((color.h % 360) <= 10);
		var isVivid = (color.s > 30);
		var isBright = (color.l > 20);
		return isRed && isVivid && isBright;
	};

	//Ghost pixel repair function
	function rebuildCurrentPixels() {
		var currPix = []; //rebuild currentPixels from pixelMap to try to fix bug
		for(pmi = 0; pmi < pixelMap.length; pmi++) {
			var pixelMapPart = pixelMap[pmi];
			for(pmj = 0; pmj < pixelMapPart.length; pmj++) {
				var pixelMapUnit = pixelMapPart[pmj];
				if(typeof(pixelMapUnit) === "object") {
					if(pixelMapUnit !== null) {
						currPix.push(pixelMapUnit);
					};
				};
			};
		};
		currentPixels = currPix;
	};


libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//Sugar functions

	function newElement(name="element_name",color="#FF00FF",otherProps={}) {
		elements[name] = {
			color: color,
		};
		for(property in otherProps) {
			elements[name][property] = otherProps[property];
		};
		return elements[name];
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

//Fixes

	//fix -1-caused ghost pixels
	function deletePixel(x,y) {
		// remove pixelMap[x][y] from currentPixels
		var pixelIndex = currentPixels.indexOf(pixelMap[x][y]);
		if(pixelIndex !== -1) {
			currentPixels.splice(pixelIndex,1)
		};
		if (pixelMap[x][y]) {pixelMap[x][y].del = true}
		delete pixelMap[x][y];
		/*for (var i = 0; i < currentPixels.length; i++) {
			if (currentPixels[i].x == x && currentPixels[i].y == y) {
				currentPixels.splice(i, 1);
				break;
			}
		}*/
		/*if (id != null) {
			for (var i = 0; i < currentPixels.length; i++) {
				if (currentPixels[i].id == id) {
					currentPixels.splice(i, 1);
					return;
				}
			}
		}*/
	};

libraryLoaded = true;
this.libraryLoaded = true;
window.libraryLoaded = true;

runAfterLoad(function() {
	if(!libraryLoaded) {
		libraryLoaded = true;
		this.libraryLoaded = true;
		window.libraryLoaded = true
	}
})
