Raw Source
Unknowngod / 5etoolsR20

// ==UserScript==
// @name         5etoolsR20
// @namespace    https://github.com/astranauta/
// @license      MIT
// @version      0.5.27
// @updateURL    https://github.com/TheGiddyLimit/5etoolsR20/raw/master/5etoolsR20.user.js
// @downloadURL  https://github.com/TheGiddyLimit/5etoolsR20/raw/master/5etoolsR20.user.js
// @description  Enhance your Roll20 experience
// @author       5egmegaanon/astranauta/MrLabRat/TheGiddyLimit/DBAWiseMan
// @match        https://app.roll20.net/editor/
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

/* eslint no-console: "off" */

var D20plus = function(version) {

	var monsterdataurl = "https://raw.githubusercontent.com/TheGiddyLimit/TheGiddyLimit.github.io/master/data/bestiary.json";
	var monsterdataurlTob = "https://raw.githubusercontent.com/TheGiddyLimit/TheGiddyLimit.github.io/master/data/bestiary-tob.json";
	var spelldataurl = "https://raw.githubusercontent.com/TheGiddyLimit/TheGiddyLimit.github.io/master/data/spells.json";
	var spellmetaurl = "https://raw.githubusercontent.com/TheGiddyLimit/TheGiddyLimit.github.io/master/data/spells-roll20.json";
	var itemdataurl = "https://raw.githubusercontent.com/TheGiddyLimit/TheGiddyLimit.github.io/master/data/items.json";

	var d20plus = {
		sheet: "ogl",
		version: version,
		timeout: 500,
		remaining: 0,
		scriptsLoaded: false,
		monsters: {},
		spells: {},
		items: {},
		initiative: {}
	};

	d20plus.scripts = [
		{name: "xml2json", url: "https://cdnjs.cloudflare.com/ajax/libs/x2js/1.2.0/xml2json.min.js"},
		{name: "listjs", url: "https://raw.githubusercontent.com/javve/list.js/v1.5.0/dist/list.min.js"},
		{name: "5etoolsutils", url: "https://raw.githubusercontent.com/TheGiddyLimit/TheGiddyLimit.github.io/master/js/utils.js"},
		{name: "5etoolsrender", url: "https://raw.githubusercontent.com/TheGiddyLimit/TheGiddyLimit.github.io/master/js/entryrender.js"}
	];

	// Inject external JS libraries
	d20plus.addScripts = function() {
		$.each(d20plus.scripts, function(i, v) {
			$.ajax({
				type: "GET",
				url: v.url,
				success: function(js) {
					try {
						window.eval(js);
						d20plus.log(`> JS [${v.name}] Loaded`);
					} catch (e) {
						d20plus.log(`> Error loading ${v.name}`);
					}
				}
			});
		});
	};

	// Window loaded
	window.onload = function() {
		window.unwatch("d20");
		var checkLoaded = setInterval(function() {
			if (!$("#loading-overlay").is(":visible")) {
				clearInterval(checkLoaded);
				d20plus.Init();
			}
		}, 1000);
	};

	// Page fully loaded and visible
	d20plus.Init = function() {
		d20plus.log("> Init (v" + d20plus.version + ")");
		d20plus.bindDropLocations();
		// Firebase will deny changes if we're not GM. Better to fail gracefully.
		if (window.is_gm) {
			d20plus.log("> Is GM");
		} else {
			d20plus.log("> Not GM. Exiting.");
			return;
		}
		d20plus.log("> Add JS");
		d20plus.addScripts();
		d20plus.log("> Add CSS");
		_.each(d20plus.cssRules, function(r) {d20plus.addCSS(window.document.styleSheets[window.document.styleSheets.length - 1], r.s, r.r);});
		d20plus.log("> Add HTML");
		d20plus.addHTML();
		d20plus.setSheet();
		d20plus.log("> Bind Graphics");
		d20.Campaign.pages.each(d20plus.bindGraphics);
		d20.Campaign.activePage().collection.on("add", d20plus.bindGraphics);
	};

	// Bind Graphics Add on page
	d20plus.bindGraphics = function(page) {
		try {
			if (page.get("archived") == false) {
				page.thegraphics.on("add", function(e) {
					var character = e.character;
					if (character) {
						var npc = character.attribs.find(function(a) {return a.get("name").toLowerCase() == "npc";});
						var isNPC = npc ? parseInt(npc.get("current")) : 0;
						if (isNPC) {
							var hpf = character.attribs.find(function(a) {return a.get("name").toLowerCase() == "npc_hpformula";});
							if (hpf) {
								var hpformula = hpf.get("current");
								if (hpformula) {
									d20plus.randomRoll(hpformula, function(result) {
										e.attributes.bar3_value = result.total;
										e.attributes.bar3_max = result.total;
										d20plus.log("> Rolled HP for [" + character.get("name") + "]");
									}, function(error) {
										d20plus.log("> Error Rolling HP Dice");
										console.log(error);
									});
								}
							}
						}
					}
				});
			}
		} catch (e) {
			console.log("D20Plus bindGraphics Exception", e);
			console.log("PAGE", page);
		}
	};

	// Create new Journal commands
	d20plus.addJournalCommands = function() {
		var first = $("#journalitemmenu ul li").first();
		first.after("<li data-action-type=\"cloneitem\">Duplicate</li>");
		first.after("<li style=\"height: 10px;\">&nbsp;</li>");
		$("#journalitemmenu ul").on(window.mousedowntype, "li[data-action-type=cloneitem]", function() {
			var id = $currentItemTarget.attr("data-itemid");
			var character = d20.Campaign.characters.get(id);
			var handout = d20.Campaign.handouts.get(id);
			d20plus.log("> Duplicating..");
			if (character) {
				character.editview.render();
				character.editview.$el.find("button.duplicate").trigger("click");
			}
			if (handout) {
				handout.view.render();
				var json = handout.toJSON();
				delete json.id;
				json.name = "Copy of " + json.name;
				handout.collection.create(json, {
					success: function(h) {
						handout._getLatestBlob("gmnotes", function(gmnotes) {h.updateBlobs({gmnotes: gmnotes});});
						handout._getLatestBlob("notes", function(notes) {h.updateBlobs({notes: notes});});
					}
				});
			}
		});
	};

	// Determine difficulty of current encounter (iniativewindow)
	d20plus.getDifficulty = function() {
		var difficulty = "Unknown";
		var partyXPThreshold = [0, 0, 0, 0];
		var players = [];
		var npcs = [];
		try {
			$.each(d20.Campaign.initiativewindow.cleanList(), function(i, v) {
				var page = d20.Campaign.pages.get(v._pageid);
				if (page) {
					var token = page.thegraphics.get(v.id);
					if (token) {
						var char = token.character;
						if (char) {
							var npc = char.attribs.find(function(a) {return a.get("name").toLowerCase() === "npc";});
							if (npc && npc.get("current") === "1") {
								npcs.push(char);
							} else {
								var level = char.attribs.find(function(a) {return a.get("name").toLowerCase() === "level";});
								// Can't determine difficulty without level
								if (!level || partyXPThreshold === null) {
									partyXPThreshold = null;
									return;
								}
								// Total party threshold
								for (i = 0; i < partyXPThreshold.length; i++) partyXPThreshold[i] += Parser.levelToXpThreshold(level.get("current"))[i];
								players.push(players.length + 1);
							}
						}
					}
				}
			});
			if (!players.length) return difficulty;
			// If a player doesn't have level set, fail out.
			if (partyXPThreshold !== null) {
				var len = npcs.length;
				var multiplier = 0;
				var adjustedxp = 0;
				var xp = 0;
				var index = 0;
				// Adjust for number of monsters
				if (len < 2) index = 0;
				else
				if (len < 3) index = 1;
				else
				if (len < 7) index = 2;
				else
				if (len < 11) index = 3;
				else
				if (len < 15) index = 4;
				else
					index = 5;
				// Adjust for smaller parties
				if (players.length < 3) index++;
				// Set multiplier
				multiplier = d20plus.multipliers[index];
				// Total monster xp
				$.each(npcs, function(i, v) {
					var cr = v.attribs.find(function(a) {return a.get("name").toLowerCase() === "npc_challenge";});
					if (cr) xp += parseInt(Parser.crToXp(cr.get("current")));
				});
				// Encounter's adjusted xp
				adjustedxp = xp * multiplier;
				console.log("Party XP Threshold", partyXPThreshold);
				console.log("Adjusted XP", adjustedxp);
				// Determine difficulty
				if (adjustedxp < partyXPThreshold[0]) difficulty = "Trivial";
				else
				if (adjustedxp < partyXPThreshold[1]) difficulty = "Easy";
				else
				if (adjustedxp < partyXPThreshold[2]) difficulty = "Medium";
				else
				if (adjustedxp < partyXPThreshold[3]) difficulty = "Hard";
				else
					difficulty = "Deadly";
			}
		} catch (e) {
			console.log("D20Plus getDifficulty Exception", e);
		}
		return difficulty;
	};

	// Determine if folder contains monster by that name
	d20plus.objectExists = function(folderObj, folderId, name) {
		const container = folderObj.find(function(a) {return a.id === folderId;});
		let result = false;
		$.each(container.i, function(i, v) {
			var char = d20.Campaign.characters.get(v);
			var handout = d20.Campaign.handouts.get(v);
			if (char && char.get("name") === name) result = true;
			if (handout && handout.get("name") === name) result = true;
		});
		return result;
	};

	// Find and delete object in folder of given name
	d20plus.deleteObject = function(folderObj, folderId, name) {
		const container = folderObj.find(function(a) {return a.id === folderId;});
		let result = false;
		$.each(container.i, function(i, v) {
			var char = d20.Campaign.characters.get(v);
			var handout = d20.Campaign.handouts.get(v);
			if (char && char.get("name") === name) {
				char.destroy();
				result = true;
			}
			if (handout && handout.get("name") === name) {
				handout.destroy();
				result = true;
			}
		});
		return result;
	};

	// Inject HTML
	d20plus.addHTML = function() {
		$("#mysettings > .content").children("hr").first().before(d20plus.settingsHtml);
		$("#mysettings > .content #button-monsters-select").change(function() { $("#import-monster-url").val(this.value); });
		$("#mysettings > .content a#button-monsters-load").on(window.mousedowntype, d20plus.monsters.button);
		$("#mysettings > .content a#button-spells-load").on(window.mousedowntype, d20plus.spells.button);
		$("#mysettings > .content a#import-items-load").on(window.mousedowntype, d20plus.items.button);
		$("#mysettings > .content a#bind-drop-locations").on(window.mousedowntype, d20plus.bindDropLocations);
		$("#initiativewindow .characterlist").before(d20plus.initiativeHeaders);
		d20plus.getInitTemplate();
		d20.Campaign.initiativewindow.rebuildInitiativeList();
		d20plus.hpAllowEdit();
		d20.Campaign.initiativewindow.model.on("change:turnorder", function() {d20plus.updateDifficulty();});
		d20plus.updateDifficulty();
		d20plus.addJournalCommands();
		const altBindButton = $(`<button id="bind-drop-locations-alt" class="btn bind-drop-locations" href="#" title="Bind drop locations and handouts" style="margin-right: 0.5em;">Bind</button>`);
		altBindButton.on("click", function(){d20plus.bindDropLocations();});
		$("#journal > .content:eq(1) > button.btn.superadd").after(altBindButton);
		$("#journal > .content:eq(1) btn#bind-drop-locations").on(window.mousedowntype, d20plus.bindDropLocations);
		$("body").append(d20plus.importDialogHtml);
		$("body").append(d20plus.importListHTML);
		$("#d20plus-import").dialog({
			autoOpen: false,
			resizable: false
		});
		$("#d20plus-importlist").dialog({
			autoOpen: false,
			resizable: true
		});
		/* Removed until I can figure out a way to show the new version without the certificate error
		$("body").append(d20plus.dmscreenHtml);
		var $dmsDialog = $("#dmscreen-dialog");
		$dmsDialog.dialog({
			title: "DM Screen",
			width: 700,
			height: 515,
			autoOpen: false
		});
		$("#floatingtoolbar > ul").append(d20plus.dmscreenButton);
		$("#dmscreen-button").on(window.mousedowntype, function(){$dmsDialog.dialog($dmsDialog.dialog("isOpen") ? "close" : "open");});*/
	};

	d20plus.updateDifficulty = function() {
		var $span = $("div#initiativewindow").parent().find(".ui-dialog-buttonpane > span.difficulty");
		var $btnpane = $("div#initiativewindow").parent().find(".ui-dialog-buttonpane");
		if (!$span.length) {
			$btnpane.prepend(d20plus.difficultyHtml);
			$span = $("div#initiativewindow").parent().find(".ui-dialog-buttonpane > span.difficulty");
		}
		$span.text("Difficulty: " + d20plus.getDifficulty());
	};

	// bind drop locations on sheet to accept custom handouts
	d20plus.bindDropLocations = function() {
		// Bind Spells and Items, add compendium-item to each of them
		var journalFolder = d20.Campaign.get("journalfolder");
		if (journalFolder === "") {
			d20.journal.addFolderToFolderStructure("Spells");
			d20.journal.addFolderToFolderStructure("Items");
			d20.journal.refreshJournalList();
			journalFolder = d20.Campaign.get("journalfolder");
		}
		var journalFolderObj = JSON.parse(journalFolder);
		var handouts = journalFolderObj.find(function(a) {return a.n && (a.n === "Spells" || a.n === "Items");});
		$("#journalfolderroot > ol.dd-list > li.dd-folder > div.dd-content:contains('Spells')").parent().find("ol li[data-itemid]").addClass("compendium-item").addClass("ui-draggable");
		$("#journalfolderroot > ol.dd-list > li.dd-folder > div.dd-content:contains('Items')").parent().find("ol li[data-itemid]").addClass("compendium-item").addClass("ui-draggable");
		d20.Campaign.characters.models.each(function(v, i) {
			v.view.rebindCompendiumDropTargets = function() {
				// ready character sheet for draggable
				$(".sheet-compendium-drop-target").each(function() {
					$(this).droppable({
						hoverClass: "dropping",
						tolerance: "pointer",
						activeClass: "active-drop-target",
						accept: ".compendium-item",
						drop: function(t, i) {
							var characterid = $(".characterdialog").has(t.target).attr("data-characterid");
							var character = d20.Campaign.characters.get(characterid).view;
							var inputData;
							if ($(i.helper[0]).hasClass("handout")) {
								console.log("Handout item dropped onto target!");
								t.originalEvent.dropHandled = !0;
								var id = $(i.helper[0]).attr("data-itemid");
								var handout = d20.Campaign.handouts.get(id);
								console.log(character);
								var data = "";
								handout._getLatestBlob("gmnotes", function(gmnotes) {
									data = gmnotes;
									handout.updateBlobs({gmnotes: gmnotes});
									data = JSON.parse(data);
									inputData = data.data;
									inputData.Name = data.name;
									inputData.Content = data.content;
									const r = $(t.target);
									r.find("*[accept]").each(function() {
										const $this = $(this);
										const acceptTag = $this.attr("accept");
										if (inputData[acceptTag] !== undefined) {
											if ("input" === this.tagName.toLowerCase()) {
												if ("checkbox" === $this.attr("type")) {
													if (inputData[acceptTag]) {
														$this.attr("checked", "checked");
													} else {
														$this.removeAttr("checked");
													}
												} else if ("radio" === $this.attr("type")) {
													if (inputData[acceptTag]) {
														$this.attr("checked", "checked");
													} else {
														$this.removeAttr("checked");
													}
												} else {
													$this.val(inputData[acceptTag]);
												}
											} else if ("select" === this.tagName.toLowerCase()) {
												$this.find("option").each(function () {
													const $this = $(this);
													if ($this.attr("value") === inputData[acceptTag] || $this.text() === inputData[acceptTag]) $this.attr("selected", "selected");
												});
											} else {
												$this.val(inputData[acceptTag]);
											}
											// persist the value
											character.saveSheetValues(this);
										}
									});
								});
							} else {
								console.log("Compendium item dropped onto target!");
								t.originalEvent.dropHandled = !0;
								inputData = $(i.helper[0]).attr("data-pagename");
								console.log("https://app.roll20.net/compendium/" + COMPENDIUM_BOOK_NAME + "/" + inputData + ".json?plaintext=true");
								$.get("https://app.roll20.net/compendium/" + COMPENDIUM_BOOK_NAME + "/" + inputData + ".json?plaintext=true", function(i) {
									var n = i.data;
									n.Name = i.name;
									n.Content = i.content;
									var r = $(t.target);
									r.find("*[accept]").each(function() {
										var t = $(this);
										var i = t.attr("accept");
										n[i] && ("input" === t[0].tagName.toLowerCase() && "checkbox" === t.attr("type") ? t.attr("value") === n[i] ? t.attr("checked", "checked") : t.removeAttr("checked") : "input" === t[0].tagName.toLowerCase() && "radio" === t.attr("type") ? t.attr("value") === n[i] ? t.attr("checked", "checked") : t.removeAttr("checked") : "select" === t[0].tagName.toLowerCase() ? t.find("option").each(function() {
											var e = $(this);
											(e.attr("value") === n[i] || e.text() === n[i]) && e.attr("selected", "selected");
										}) : $(this).val(n[i]), character.saveSheetValues(this));
									});
								});
							}
						}
					});
				});
			};
		});
	};

	d20plus.handleAjaxError = function(jqXHR, exception) {
		var msg = "";
		if (jqXHR.status === 0) {
			msg = "Could not connect.\n Check Network";
		} else if (jqXHR.status === 404) {
			msg = "Page not found [404]";
		} else if (jqXHR.status === 500) {
			msg = "Internal Server Error [500]";
		} else if (exception === 'parsererror') {
			msg = "Data parse failed";
		} else if (exception === 'timeout') {
			msg = "Timeout";
		} else if (exception === 'abort') {
			msg = "Request aborted";
		} else {
			msg = "Uncaught Error.\n" + jqXHR.responseText;
		}
		d20plus.log("> ERROR: " + msg);
	};

	// Import Monsters button was clicked
	d20plus.monsters.button = function() {
		var url = $("#import-monster-url").val();
		if (url !== null) d20plus.monsters.load(url);
	};

	// Fetch monster data from XML url and import it
	d20plus.monsters.load = function(url) {
		$("a.ui-tabs-anchor[href='#journal']").trigger("click");
		var x2js = new X2JS();
		var datatype = $("#import-datatype").val();
		if (datatype === "json") datatype = "text";
		$.ajax({
			type: "GET",
			url: url,
			dataType: datatype,
			success: function(data) {
				try {
					d20plus.log("Importing Data (" + $("#import-datatype").val().toUpperCase() + ")");
					monsterdata = (datatype === "XML") ? x2js.xml2json(data) : JSON.parse(data.replace(/^var .* \= /g, ""));
					var length = monsterdata.monster.length;
					monsterdata.monster.sort(function(a,b) {
						if (a.name < b.name) return -1;
						if (a.name > b.name) return 1;
						return 0;
					});
					// building list for checkboxes
					$("#import-list .list").html("");
					$.each(monsterdata.monster, function(i, v) {
						try {
							$("#import-list .list").append(`<label><input type="checkbox" data-listid="${i}"> <span class="name">${v.name}</span></label>`);
						} catch (e) {
							console.log("Error building list!", e);
							d20plus.addImportError(v.name);
						}
					});
					var options = {valueNames: [ 'name' ]};
					var importList = new List ("import-list", options);
					$("#import-options label").hide();
					$("#import-overwrite").parent().show();
					$("#delete-existing").parent().show();
					$("#organize-by-source").parent().show();
					$("#d20plus-importlist").dialog("open");
					$("#d20plus-importlist input#importlist-selectall").unbind("click");
					$("#d20plus-importlist input#importlist-selectall").bind("click", function() {$("#import-list .list input").prop("checked", $(this).prop("checked"));});
					$("#d20plus-importlist button").unbind("click");
					$("#d20plus-importlist button#importstart").bind("click", function() {
						$("#d20plus-importlist").dialog("close");
						var overwrite = $("#import-overwrite").prop("checked");
						var deleteExisting = $("#delete-existing").prop("checked");
						$("#import-list .list input").each(function() {
							if (!$(this).prop("checked")) return;
							var monsternum = parseInt($(this).data("listid"));
							var curmonster = monsterdata.monster[monsternum];
							try {
								console.log(`> ${(monsternum + 1)}/${length} Attempting to import monster [${curmonster.name}]`);
								d20plus.monsters.import(curmonster, overwrite, deleteExisting);
							} catch (e) {
								console.log("Error Importing!", e);
								d20plus.addImportError(curmonster.name);
							}
						});
					});
				} catch (e) {
					console.log("> Exception ", e);
				}
			},
			error: function(jqXHR, exception) {d20plus.handleAjaxError(jqXHR, exception);}
		});
		d20plus.timeout = 500;
	};

	// Create monster character from js data object
	d20plus.monsters.import = function(data, overwrite, deleteExisting) {
		var typeArr = Parser.monTypeToFullObj(data.type).asText.split(",");
		var fname = $("#organize-by-source").prop("checked") ? Parser.sourceJsonToFull(data.source) : typeArr[0].toLowerCase().replace(/\((any race)\)/g, "").capFirstLetter();
		var findex = 1;
		var folder;
		d20.journal.refreshJournalList();
		var journalFolder = d20.Campaign.get("journalfolder");
		if (journalFolder === "") {
			d20.journal.addFolderToFolderStructure("Characters");
			d20.journal.refreshJournalList();
			journalFolder = d20.Campaign.get("journalfolder");
		}
		var journalFolderObj = JSON.parse(journalFolder);
		var monsters = journalFolderObj.find(function(a) {return a.n && a.n == "Monsters";});
		if (!monsters) d20.journal.addFolderToFolderStructure("Monsters");
		d20.journal.refreshJournalList();
		journalFolder = d20.Campaign.get("journalfolder");
		journalFolderObj = JSON.parse(journalFolder);
		monsters = journalFolderObj.find(function(a) {return a.n && a.n == "Monsters";});
		var name = data.name || "(Unknown Name)";
		// check for duplicates
		var dupe = false;
		$.each(monsters.i, function(i, v) {
			if (d20plus.objectExists(monsters.i, v.id, name)) dupe = true;
			if (overwrite || deleteExisting) d20plus.deleteObject(monsters.i, v.id, name);
		});
		if (deleteExisting || (dupe && !overwrite)) return;
		var timeout = 0;
		d20plus.remaining++;
		if (d20plus.timeout == 500) {
			$("#d20plus-import").dialog("open");
			$("#import-remaining").text(d20plus.remaining);
		}
		timeout = d20plus.timeout;
		d20plus.timeout += 2500;
		setTimeout(function() {
			d20plus.log("Running import of [" + name + "]");
			$("#import-remaining").text(d20plus.remaining);
			$("#import-name").text(name);
			d20.journal.refreshJournalList();
			journalFolder = d20.Campaign.get("journalfolder");
			journalFolderObj = JSON.parse(journalFolder);
			monsters = journalFolderObj.find(function(a) {return a.n && a.n == "Monsters";});
			// make source folder
			for (i = -1; i < monsters.i.length; i++) {
				var theFolderName = (findex == 1) ? fname : fname + " " + findex;
				folder = monsters.i.find(function(f) {return f.n == theFolderName;});
				if (folder) {
					if (folder.i.length >= 90) {
						findex++;
					} else {
						break;
					}
				} else {
					d20.journal.addFolderToFolderStructure(theFolderName, monsters.id);
					d20.journal.refreshJournalList();
					journalFolder = d20.Campaign.get("journalfolder");
					journalFolderObj = JSON.parse(journalFolder);
					monsters = journalFolderObj.find(function(a) {return a.n && a.n == "Monsters";});
					folder = monsters.i.find(function(f) {return f.n == theFolderName;});
					break;
				}
			}
			if (!folder) {
				console.log("> Failed to find or create source folder!");
				return;
			}
			d20.Campaign.characters.create({name: name}, {
				success: function(character) {
					function getSetAvatarImage(avatar) {
						character.attributes.avatar = avatar;
						var tokensize = 1;
						if (character.size === "L") tokensize = 2;
						if (character.size === "H") tokensize = 3;
						if (character.size === "G") tokensize = 4;
						var lightradius = 5;
						if(character.senses && character.senses.toLowerCase().match(/(darkvision|blindsight|tremorsense|truesight)/)) lightradius = Math.max.apply(Math, character.senses.match(/\d+/g));
						var lightmin = 0;
						if(character.senses && character.senses.toLowerCase().match(/(blindsight|tremorsense|truesight)/)) lightmin = lightradius;
						var defaulttoken = {
							represents: character.id,
							name: character.name,
							showname: 1,
							imgsrc: avatar,
							width: 70 * tokensize,
							height: 70 * tokensize,
							bar2_value: data.ac.match(/^\d+/),		
							bar3_value: character.hp,		
							bar3_max: character.hp,
							light_hassight: true,
							light_radius: lightradius,
							light_dimradius: lightmin
						};
						character.updateBlobs({ avatar: avatar, defaulttoken: JSON.stringify(defaulttoken) });
						character.save({defaulttoken: (new Date).getTime()});
					}
					/* OGL Sheet */
					try {
						const type = Parser.monTypeToFullObj(data.type).asText;
						const source = data.source;
						const avatar = "https://TheGiddyLimit.github.io/img/" + source + "/" + name + ".png";
						character.size = data.size;
						character.name = name;
						character.senses = data.senses;
						character.hp = data.hp.match(/^\d+/);
						$.ajax({
							url: avatar,
							type: 'HEAD',
							error: function() {getSetAvatarImage("https://TheGiddyLimit.github.io/img/blank.png");},
							success: function() {getSetAvatarImage(avatar);}
						});
						var ac = data.ac.match(/^\d+/);
						var actype = /\(([^)]+)\)/.exec(data.ac);
						var hp = data.hp.match(/^\d+/);
						var hpformula = /\(([^)]+)\)/.exec(data.hp);
						var passive = data.passive != null ? data.passive : "";
						var passiveStr = passive !== "" ? "passive Perception " + passive : "";
						var senses = data.senses || "";
						var sensesStr = senses !== "" ? senses + ", " + passiveStr : passiveStr;
						var size = d20plus.getSizeString(data.size || "");
						var alignment = data.alignment || "(Unknown Alignment)";
						var cr = data.cr != null ? data.cr : "";
						var xp = Parser.crToXp(cr);
						character.attribs.create({name: "npc", current: 1});
						character.attribs.create({name: "npc_toggle", current: 1});
						character.attribs.create({name: "npc_options-flag", current: 0});
						character.attribs.create({name: "wtype", current: "@{whispertoggle}"});
						character.attribs.create({name: "rtype", current: "@{advantagetoggle}"});
						character.attribs.create({name: "advantagetoggle", current: "{{query=1}} {{advantage=1}} {{r2=[[@{d20}"});
						character.attribs.create({name: "whispertoggle", current: "/w gm "});
						character.attribs.create({name: "dtype", current: "full"});
						character.attribs.create({name: "npc_name", current: name});
						character.attribs.create({name: "npc_size", current: size});
						character.attribs.create({name: "type", current: type});
						character.attribs.create({name: "npc_type", current: size + " " + type + ", " + alignment});
						character.attribs.create({name: "npc_alignment", current: alignment});
						character.attribs.create({name: "npc_ac", current: ac != null ? ac[0] : ""});
						character.attribs.create({name: "npc_actype", current: actype != null ? actype[1] || "" : ""});
						character.attribs.create({name: "npc_hpbase", current: hp != null ? hp[0] : ""});
						character.attribs.create({name: "npc_hpformula", current: hpformula != null ? hpformula[1] || "" : ""});
						data.npc_speed = data.speed;
						if (d20plus.sheet === "shaped") {
							data.npc_speed = data.npc_speed.toLowerCase();
							var match = data.npc_speed.match(/^\s*(\d+)\s?(ft\.?|m\.?)/);
							if (match && match[1]) {
								data.speed = match[1] + ' ' + match[2];
								character.attribs.create({name: "speed", current: match[1] + ' ' + match[2]});
							}
							data.npc_speed = data.speed;
							var regex = /(burrow|climb|fly|swim)\s+(\d+)\s?(ft\.?|m\.?)/g;
							var speeds = void 0;
							while ((speeds = regex.exec(data.npc_speed)) !== null) character.attribs.create({name: "speed_"+speeds[1], current: speeds[2] + ' ' + speeds[3]});
							if (data.npc_speed && data.npc_speed.includes('hover')) character.attribs.create({name: "speed_fly_hover", current: 1});
							data.npc_speed = '';
						}
						character.attribs.create({name: "npc_speed", current: data.speed != null ? data.speed : ""});
						character.attribs.create({name: "strength", current: data.str});
						character.attribs.create({name: "strength_base", current: data.str});
						character.attribs.create({name: "dexterity", current: data.dex});
						character.attribs.create({name: "dexterity_base", current: data.dex});
						character.attribs.create({name: "constitution", current: data.con});
						character.attribs.create({name: "constitution_base", current: data.con});
						character.attribs.create({name: "intelligence", current: data.int});
						character.attribs.create({name: "intelligence_base", current: data.int});
						character.attribs.create({name: "wisdom", current: data.wis});
						character.attribs.create({name: "wisdom_base", current: data.wis});
						character.attribs.create({name: "charisma", current: data.cha});
						character.attribs.create({name: "charisma_base", current: data.cha});
						character.attribs.create({name: "passive", current: passive});
						character.attribs.create({name: "npc_languages", current: data.languages != null ? data.languages : ""});
						character.attribs.create({name: "npc_challenge", current: cr});
						character.attribs.create({name: "npc_xp", current: xp});
						character.attribs.create({name: "npc_vulnerabilities", current: data.vulnerable != null ? data.vulnerable : ""});
						character.attribs.create({name: "damage_vulnerabilities", current: data.vulnerable != null ? data.vulnerable : ""});
						character.attribs.create({name: "npc_resistances", current: data.resist != null ? data.resist : ""});
						character.attribs.create({name: "damage_resistances", current: data.resist != null ? data.resist : ""});
						character.attribs.create({name: "npc_immunities", current: data.immune != null ? data.immune : ""});
						character.attribs.create({name: "damage_immunities", current: data.immune != null ? data.immune : ""});
						character.attribs.create({name: "npc_condition_immunities", current: data.conditionImmune != null ? data.conditionImmune : ""});
						character.attribs.create({name: "damage_condition_immunities", current: data.conditionImmune != null ? data.conditionImmune : ""});
						character.attribs.create({name: "npc_senses", current: sensesStr});
						if (data.save != null && data.save.length > 0) {
							var savingthrows;
							if (data.save instanceof Array) {
								savingthrows = data.save;
							} else {
								savingthrows = data.save.split(", ");
							}
							character.attribs.create({name: "npc_saving_flag", current: 1});
							$.each(savingthrows, function(i, v) {
								var save = v.split(" ");
								character.attribs.create({name: "npc_" + save[0].toLowerCase() + "_save_base", current: parseInt(save[1])});
								character.attribs.create({name: save[0].toLowerCase() + "_saving_throw_proficient", current: parseInt(save[1])});
							});
						}
						if (data.skill != null) {
							const skills = data.skill;
                            const skillsString = Object.keys(skills).map(function(k){return k.uppercaseFirst() + ' ' + skills[k]}).join(', ');
							character.attribs.create({name: "npc_skills_flag", current: 1});
							character.attribs.create({name: "npc_skills", current: skillsString});
							var newRowId = d20plus.generateRowId();
							character.attribs.create({name: "repeating_npctrait_" + newRowId + "_name", current: "NPC Skills"});
							character.attribs.create({name: "repeating_npctrait_" + newRowId + "_desc", current: skillsString});
							$.each(skills, function(k, v) {
								character.attribs.create({name: "npc_" + $.trim(k).toLowerCase().replace(/ /g,"_") + "_base", current: parseInt($.trim(v)) || 0});
							});
						}
						if (data.trait != null) {
							if (!(data.trait instanceof Array)) {
								var tmp1 = data.trait;
								data.trait = [];
								data.trait.push(tmp1);
							}
							$.each(data.trait, function(i, v) {
								var newRowId = d20plus.generateRowId();
								var text = "";
								character.attribs.create({name: "repeating_npctrait_" + newRowId + "_name", current: v.name});
								if (v.text instanceof Array) {
									$.each(v.text, function(z, x) {
										if (!x) return;
										text += (z > 0 ? "\r\n" : "") + x;
									});
								} else {
									text = v.text;
								}
								character.attribs.create({name: "repeating_npctrait_" + newRowId + "_desc", current: text});
							});
						}
						if (data.action != null) {
							if (!(data.action instanceof Array)) {
								var tmp2 = data.action;
								data.action = [];
								data.action.push(tmp2);
							}
							var npc_exception_actions = ["Web (Recharge 5-6)"];
							$.each(data.action, function(i, v) {
								var newRowId = d20plus.generateRowId();
								var text = "";
								if (v.text instanceof Array) {
									$.each(v.text, function(z, x) {
										if (!x) return;
										text += (z > 0 ? "\r\n" : "") + x;
									});
								} else {
									text = v.text;
								}
								var actiontext = "";
								if (v.text instanceof Array) {
									actiontext = v.text[0];
								} else {
									actiontext = v.text;
								}
								var action_desc = actiontext; // required for later reduction of information dump.
								var rollbase = "@{wtype}&{template:npcaction} @{attack_display_flag} @{damage_flag} {{name=@{npc_name}}} {{rname=@{name}}} {{r1=[[1d20+(@{attack_tohit}+0)]]}} @{rtype}+(@{attack_tohit}+0)]]}} {{dmg1=[[@{attack_damage}+0]]}} {{dmg1type=@{attack_damagetype}}} {{dmg2=[[@{attack_damage2}+0]]}} {{dmg2type=@{attack_damagetype2}}} {{crit1=[[@{attack_crit}+0]]}} {{crit2=[[@{attack_crit2}+0]]}} {{description=@{description}}} @{charname_output}";
								// attack parsing
								if (actiontext.indexOf(" Attack:") > -1) {
									var name = v.name;
									var attacktype = "";
									var attacktype2 = "";
									if (actiontext.indexOf(" Weapon Attack:") > -1) {
										attacktype = actiontext.split(" Weapon Attack:")[0];
										attacktype2 = " Weapon Attack:";
									} else if (actiontext.indexOf(" Spell Attack:") > -1) {
										attacktype = actiontext.split(" Spell Attack:")[0];
										attacktype2 = " Spell Attack:";
									}
									var attackrange = "";
									var rangetype = "";
									if (attacktype.indexOf("Melee") > -1) {
										attackrange = (actiontext.match(/reach (.*?),/) || ["", ""])[1]
										rangetype = "Reach";
									} else {
										attackrange = (actiontext.match(/range (.*?),/) || ["", ""])[1];
										rangetype = "Range";
									}
									var tohit = (actiontext.match(/\+(.*) to hit/) || ["", ""])[1];
									var damage = "";
									var damagetype = "";
									var damage2 = "";
									var damagetype2 = "";
									var onhit = "";
									damageregex = /\d+ \((\d+d\d+\s?(?:\+|\-)?\s?\d*)\) (\S+ )?damage/g;
									damagesearches = damageregex.exec(actiontext);
									if (damagesearches) {
										onhit = damagesearches[0];
										damage = damagesearches[1];
										damagetype = (damagesearches[2] != null) ? damagesearches[2].trim() : "";
										damagesearches = damageregex.exec(actiontext);
										if (damagesearches) {
											onhit += " plus " + damagesearches[0];
											damage2 = damagesearches[1];
											damagetype2 = (damagesearches[2] != null) ? damagesearches[2].trim() : "";
										}
									}
									onhit = onhit.trim();
									var attacktarget = (actiontext.match(/\.,(?!.*\.,)(.*)\. Hit:/) || ["", ""])[1];
									// Cut the information dump in the description
									var atk_desc_simple_regex = /Hit: \d+ \((\d+d\d+\s?(?:\+|\-)?\s?\d*)\) (\S+ )?damage\.(.*)/g;
									var atk_desc_complex_regex = /(Hit:.*)/g;
									// is it a simple attack (just 1 damage type)?
									var match_simple_atk = atk_desc_simple_regex.exec(actiontext);
									if (match_simple_atk != null) {
										//if yes, then only display special effects, if any
										action_desc = match_simple_atk[3].trim();
									} else {
										//if not, simply cut everything before "Hit:" so there are no details lost.
										var match_compl_atk = atk_desc_complex_regex.exec(actiontext);
										if (match_compl_atk != null) action_desc = match_compl_atk[1].trim();
									}
									var tohitrange = "+" + tohit + ", " + rangetype + " " + attackrange + ", " + attacktarget + ".";
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name", current: name});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_flag", current: "on"});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_npc_options-flag", current: 0});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_display_flag", current: "{{attack=1}}"});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_options", current: "{{attack=1}}"});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_tohit", current: tohit});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_damage", current: damage});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_damagetype", current: damagetype});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_damage2", current: damage2});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_damagetype2", current: damagetype2});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name_display", current: name});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_rollbase", current: rollbase});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_type", current: attacktype});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_type_display", current: attacktype + attacktype2});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_tohitrange", current: tohitrange});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_range", current: attackrange});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_target", current: attacktarget});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_damage_flag", current: "{{damage=1}} {{dmg1flag=1}} {{dmg2flag=1}}"});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_attack_onhit", current: onhit});
								} else {
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name", current: v.name});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_npc_options-flag", current: 0});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_rollbase", current: rollbase});
									character.attribs.create({name: "repeating_npcaction_" + newRowId + "_name_display", current: v.name});
								}
								var descriptionFlag = Math.max(Math.ceil(text.length / 57), 1);
								character.attribs.create({
									name: "repeating_npcaction_" + newRowId + "_description",
									current: action_desc
								});
								character.attribs.create({
									name: "repeating_npcaction_" + newRowId + "_description_flag",
									current: descriptionFlag
								});
							});
						}
						if (data.reaction != null) {
							if (!(data.reaction instanceof Array)) {
								var tmp3 = data.reaction;
								data.reaction = [];
								data.reaction.push(tmp3);
							}
							character.attribs.create({name: "reaction_flag", current: 1});
							character.attribs.create({name: "npcreactionsflag", current: 1});
							$.each(data.reaction, function(i, v) {
								var newRowId = d20plus.generateRowId();
								var text = "";
								character.attribs.create({name: "repeating_npcreaction_" + newRowId + "_name", current: v.name});
								if (v.text instanceof Array) {
									$.each(v.text, function(z, x) {
										if (!x) return;
										text += (z > 0 ? "\r\n" : "") + x;
									});
								} else {
									text = v.text;
								}
								character.attribs.create({name: "repeating_npcreaction_" + newRowId + "_desc", current: text});
								character.attribs.create({name: "repeating_npcreaction_" + newRowId + "_description", current: text});
							});
						}
						if (data.legendary != null) {
							if (!(data.legendary instanceof Array)) {
								var tmp4 = data.legendary;
								data.legendary = [];
								data.legendary.push(tmp4);
							}
							character.attribs.create({name: "legendary_flag", current: "1"});
							let legendaryActions = data.legendaryActions || 3;
							character.attribs.create({name: "npc_legendary_actions", current: legendaryActions.toString()});
							$.each(data.legendary, function(i, v) {
								var newRowId = d20plus.generateRowId();
								var actiontext = "";
								var text = "";
								var rollbase = "@{wtype}&{template:npcaction} @{attack_display_flag} @{damage_flag} {{name=@{npc_name}}} {{rname=@{name}}} {{r1=[[1d20+(@{attack_tohit}+0)]]}} @{rtype}+(@{attack_tohit}+0)]]}} {{dmg1=[[@{attack_damage}+0]]}} {{dmg1type=@{attack_damagetype}}} {{dmg2=[[@{attack_damage2}+0]]}} {{dmg2type=@{attack_damagetype2}}} {{crit1=[[@{attack_crit}+0]]}} {{crit2=[[@{attack_crit2}+0]]}} {{description=@{description}}} @{charname_output}";
								if (v.attack != null) {
									if (!(v.attack instanceof Array)) {
										var tmp = v.attack;
										v.attack = [];
										v.attack.push(tmp);
									}
									$.each(v.attack, function(z, x) {
										if (!x) return;
										var attack = x.split("|");
										var name = "";
										if (v.attack.length > 1)
											name = (attack[0] == v.name) ? v.name : v.name + " - " + attack[0] + "";
										else
											name = v.name;
										var onhit = "";
										var damagetype = "";
										if (attack.length == 2) {
											damage = "" + attack[1];
											tohit = "";
										} else {
											damage = "" + attack[2];
											tohit = attack[1] || 0;
										}
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_name", current: name});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_attack_flag", current: "on"});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_npc_options-flag", current: 0});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_attack_display_flag", current: "{{attack=1}}"});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_attack_options", current: "{{attack=1}}"});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_attack_tohit", current: tohit});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_attack_damage", current: damage});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_name_display", current: name});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_rollbase", current: rollbase});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_attack_type", current: ""});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_attack_tohitrange", current: ""});
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_damage_flag", current: "{{damage=1}} {{dmg1flag=1}} {{dmg2flag=1}}"});
										if (damage !== "") {
											damage1 = damage.replace(/\s/g, "").split(/d|(?=\+|\-)/g);
											if (damage1[1])
												damage1[1] = damage1[1].replace(/[^0-9-+]/g, "");
											damage2 = isNaN(eval(damage1[1])) === false ? eval(damage1[1]) : 0;
											if (damage1.length < 2) {
												onhit = onhit + damage1[0] + " (" + damage + ")" + damagetype + " damage";
											} else if (damage1.length < 3) {
												onhit = onhit + Math.floor(damage1[0] * ((damage2 / 2) + 0.5)) + " (" + damage + ")" + damagetype + " damage";
											} else {
												onhit = onhit + (Math.floor(damage1[0] * ((damage2 / 2) + 0.5)) + parseInt(damage1[2], 10)) + " (" + damage + ")" + damagetype + " damage";
											}
										}
										character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_attack_onhit", current: onhit});
									});
								} else {
									character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_name", current: v.name});
									character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_npc_options-flag", current: 0});
									character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_rollbase", current: rollbase});
									character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_name_display", current: v.name});
								}
								if (v.text instanceof Array) {
									$.each(v.text, function(z, x) {
										if (!x) return;
										text += (z > 0 ? "\r\n" : "") + x;
									});
								} else {
									text = v.text;
								}
								var descriptionFlag = Math.max(Math.ceil(text.length / 57), 1);
								character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_description", current: text});
								character.attribs.create({name: "repeating_npcaction-l_" + newRowId + "_description_flag", current: descriptionFlag});
							});
						}
						character.view._updateSheetValues();
						var dirty = [];
						$.each(d20.journal.customSheets.attrDeps, function(i, v) {dirty.push(i);});
						d20.journal.notifyWorkersOfAttrChanges(character.view.model.id, dirty, true);
					} catch (e) {
						d20plus.log("> Error loading [" + name + "]");
						d20plus.addImportError(name);
						console.log(data);
						console.log(e);
					}
					/* end OGL Sheet */
					//character.updateBlobs({gmnotes: gmnotes});
					d20.journal.addItemToFolderStructure(character.id, folder.id);
				}
			});
			d20plus.remaining--;
			if (d20plus.remaining == 0) {
				setTimeout(function() {
					$("#import-name").text("DONE!");
					$("#import-remaining").text("0");
				}, 1000);
			}
		}, timeout);
	};

	// Import dialog showing names of monsters failed to import
	d20plus.addImportError = function(name) {
		var $span = $("#import-errors");
		if ($span.text() == "0") {
			$span.text(name);
		} else {
			$span.text($span.text() + ", " + name);
		}
	};

	// Get NPC size from chr
	d20plus.getSizeString = function(chr) {
		const result = Parser.sizeAbvToFull(chr);
		return result ? result : "(Unknown Size)";
	};

	// Create ID for repeating row
	d20plus.generateRowId = function() {return window.generateUUID().replace(/_/g, "Z");};

	// Create editable HP variable and autocalculate + or -
	d20plus.hpAllowEdit = function() {
		$("#initiativewindow").on(window.mousedowntype, ".hp.editable", function() {
			if ($(this).find("input").length > 0) return void $(this).find("input").focus();
			var val = $.trim($(this).text());
			$(this).html("<input type='text' value='" + val + "'/>");
			$(this).find("input").focus();
		});
		$("#initiativewindow").on("keydown", ".hp.editable", function(event) {
			if (event.which == 13) {
				var total = 0,
					el, token, id, char, hp,
					val = $.trim($(this).find("input").val()),
					matches = val.match(/[+\-]*(\.\d+|\d+(\.\d+)?)/g) || [];
				while (matches.length) {
					total += parseFloat(matches.shift());
				}
				el = $(this).parents("li.token");
				id = el.data("tokenid");
				token = d20.Campaign.pages.get(d20.Campaign.activePage()).thegraphics.get(id);
				char = token.character;
				npc = char.attribs.find(function(a) {return a.get("name").toLowerCase() === "npc";});
				if (npc && npc.get("current") == "1") {
					token.attributes.bar3_value = total;
				} else {
					hp = char.attribs.find(function(a) {return a.get("name").toLowerCase() === "hp";});
					if (hp) {
						hp.syncedSave({current: total});
					} else {
						char.attribs.create({name: "hp", current: total});
					}
				}
				d20.Campaign.initiativewindow.rebuildInitiativeList();
			}
		});
	};

	// Cross-browser add CSS rule
	d20plus.addCSS = function(sheet, selector, rules) {
		index = sheet.cssRules.length;
		if ("insertRule" in sheet) {
			sheet.insertRule(selector + "{" + rules + "}", index);
		} else if ("addRule" in sheet) {
			sheet.addRule(selector, rules, index);
		}
	};

	// Send string to chat using current char id
	d20plus.chatSend = function(str) {d20.textchat.doChatInput(str);};

	// Get character by name
	d20plus.charByName = function(name) {
		var char = null;
		d20.Campaign.characters.each(function(c) {if (c.get("name") == name) char = c;});
		return char;
	};

	// Prettier log
	d20plus.log = function(arg) {console.log("%cD20Plus", "color: #3076b9; font-size: large", arg);};

	// Return random result from rolling dice
	d20plus.randomRoll = function(roll, success, error) {d20.textchat.diceengine.process(roll, success, error);};

	// Return random integer between [0,int)
	d20plus.randomInt = function(int) {return d20.textchat.diceengine.random(int);};

	// Change character sheet formulas
	d20plus.setSheet = function() {
		d20plus.sheet = "ogl";
		if (d20.journal.customSheets.layouthtml.indexOf("shaped_d20") > 0) d20plus.sheet = "shaped";
		if (d20.journal.customSheets.layouthtml.indexOf("DnD5e_Character_Sheet") > 0) d20plus.sheet = "community";
		d20plus.log("> Switched Character Sheet Template to " + d20plus.sheet);
	};

	// Return Initiative Tracker template with formulas
	d20plus.getInitTemplate = function() {
		var cachedFunction = d20.Campaign.initiativewindow.rebuildInitiativeList;
		d20.Campaign.initiativewindow.rebuildInitiativeList = function() {
			var html = d20plus.initiativeTemplate;
			_.each(d20plus.formulas[d20plus.sheet], function(v, i) {html = html.replace("||" + i + "||", v);});
			$("#tmpl_initiativecharacter").replaceWith(html);
			var results = cachedFunction.apply(this, []);
			setTimeout(function() {
				$(".initmacrobutton").unbind("click");
				$(".initmacrobutton").bind("click", function() {
					console.log("Macro button clicked");
					tokenid = $(this).parent().parent().data("tokenid");
					var token, char;
					var page = d20.Campaign.activePage();
					if (page) token = page.thegraphics.get(tokenid);
					if (token) char = token.character;
					if (char) {
						char.view.showDialog();
						// d20.textchat.doChatInput(`%{` + char.id + `|` + d20plus.formulas[d20plus.sheet]["macro"] + `}`)
					}
				});
			}, 100);
			return results;
		};
	};

	// Import Spells button was clicked
	d20plus.spells.button = function() {
		var url = $("#import-spell-url").val();
		if (url !== null) d20plus.spells.load(url);
	};

	// Fetch spell data from file
	d20plus.spells.load = function(url) {
		$("a.ui-tabs-anchor[href='#journal']").trigger("click");
		var x2js = new X2JS();
		var datatype = $("#import-datatype").val();
		if (datatype === "json") datatype = "text";

		// if we're importing from 5etools, fetch spell metadata and merge it in
		if (url === spelldataurl) {
			$.ajax({
				type: "GET",
				url: spellmetaurl,
				dataType: datatype,
				success: chainLoad,
				error: function(jqXHR, exception) {d20plus.handleAjaxError(jqXHR, exception);}
			});

			function chainLoad(metadata) {
				const parsedMeta = JSON.parse(metadata);

				$.ajax({
					type: "GET",
					url: url,
					dataType: datatype,
					success: function(data) {handleSuccess(data, parsedMeta)},
					error: function(jqXHR, exception) {d20plus.handleAjaxError(jqXHR, exception);}
				});
			}
		}  else {
			$.ajax({
				type: "GET",
				url: url,
				dataType: datatype,
				success: handleSuccess,
				error: function(jqXHR, exception) {d20plus.handleAjaxError(jqXHR, exception);}
			});
		}

		d20plus.timeout = 500;

		function handleSuccess(data, meta) {
			try {
				d20plus.log("Importing Data (" + $("#import-datatype").val().toUpperCase() + ")");
				spelldata = (datatype === "XML") ? x2js.xml2json(data) : JSON.parse(data.replace(/^var .* \= /g, ""));
				var length = spelldata.spell.length;
				spelldata.spell.sort(function(a,b) {
					if (a.name < b.name) return -1;
					if (a.name > b.name) return 1;
					return 0;
				});

				if (meta) {
					for (let i = 0; i < spelldata.spell.length; ++i) {
						const curSpell = spelldata.spell[i];
						for (let j = 0; j < meta.spell.length; ++j) {
							const curMeta = meta.spell[j];
							if (curSpell.name === curMeta.name && curSpell.source === curMeta.source) {
								curSpell.roll20 = curMeta.data;
								break;
							}
						}
					}
				}

				// building list for checkboxes
				$("#import-list .list").html("");
				$.each(spelldata.spell, function(i, v) {
					try {
						$("#import-list .list").append(`<label><input type="checkbox" data-listid="${i}"> <span class="name">${v.name}</span> <span class="source">- ${v.source}</span></label>`);
					} catch (e) {
						console.log("Error building list!", e);
						d20plus.addImportError(v.name);
					}
				});
				var options = {valueNames: [ 'name' ]};
				var importList = new List ("import-list", options);
				$("#import-options label").hide();
				$("#import-overwrite").parent().show();
				$("#delete-existing").parent().show();
				$("#organize-by-source").parent().show();
				$("#import-showplayers").parent().show();
				$("#d20plus-importlist").dialog("open");
				$("#d20plus-importlist input#importlist-selectall").unbind("click");
				$("#d20plus-importlist input#importlist-selectall").bind("click", function() {
					$("#import-list .list input").prop("checked", $(this).prop("checked"));
				});
				$("#d20plus-importlist button").unbind("click");
				$("#d20plus-importlist button#importstart").bind("click", function() {
					$("#d20plus-importlist").dialog("close");
					var overwrite = $("#import-overwrite").prop("checked");
					var deleteExisting = $("#delete-existing").prop("checked");
					$("#import-list .list input").each(function() {
						if (!$(this).prop("checked")) return;
						var spellnum = parseInt($(this).data("listid"));
						var curspell = spelldata.spell[spellnum];
						try {
							console.log("> " + (spellnum + 1) + "/" + length + " Attempting to import spell [" + curspell.name + "]");
							d20plus.spells.import(curspell, overwrite, deleteExisting);
						} catch (e) {
							console.log("Error Importing!", e);
							d20plus.addImportError(curspell.name);
						}
					});
				});
			} catch (e) {
				console.log("> Exception ", e);
			}
		}
	};

	// Import individual spells
	d20plus.spells.import = function(data, overwrite, deleteExisting) {
		var level = Parser.spLevelToFull(data.level);
		if (level !== "cantrip") level += " level";
		var fname = $("#organize-by-source").prop("checked") ? Parser.sourceJsonToFull(data.source) : level.trim().capFirstLetter();
		var findex = 1;
		var folder;
		d20.journal.refreshJournalList();
		var journalFolder = d20.Campaign.get("journalfolder");
		if (journalFolder === "") {
			d20.journal.addFolderToFolderStructure("Characters");
			d20.journal.refreshJournalList();
			journalFolder = d20.Campaign.get("journalfolder");
		}
		var journalFolderObj = JSON.parse(journalFolder);
		var spells = journalFolderObj.find(function(a) {return a.n && a.n === "Spells";});
		if (!spells) d20.journal.addFolderToFolderStructure("Spells");
		d20.journal.refreshJournalList();
		journalFolder = d20.Campaign.get("journalfolder");
		journalFolderObj = JSON.parse(journalFolder);
		spells = journalFolderObj.find(function(a) {return a.n && a.n === "Spells";});
		var datas = data.source ? data.source : "PHB";
		var name = data.name + " " + datas || "(Unknown Name)";
		// check for duplicates
		var dupe = false;
		$.each(spells.i, function(i, v) {
			if (d20plus.objectExists(spells.i, v.id, name)) dupe = true;
			if (overwrite || deleteExisting) d20plus.deleteObject(spells.i, v.id, name);
		});
		if (deleteExisting || (dupe && !overwrite)) return;
		d20plus.remaining++;
		if (d20plus.timeout === 500) {
			$("#d20plus-import").dialog("open");
			$("#import-remaining").text("d20plus.remaining");
		}
		timeout = d20plus.timeout;
		d20plus.timeout += 2500;
		setTimeout(function() {
			d20plus.log("Running import of [" + name + "]");
			$("#import-remaining").text(d20plus.remaining);
			$("#import-name").text(name);
			d20.journal.refreshJournalList();
			journalFolder = d20.Campaign.get("journalfolder");
			journalFolderObj = JSON.parse(journalFolder);
			spells = journalFolderObj.find(function(a) {return a.n && a.n === "Spells";});
			// make source folder
			for (i = -1; i < spells.i.length; i++) {
				var theFolderName = (findex === 1) ? fname : fname + " " + findex;
				folder = spells.i.find(function(f) {return f.n === theFolderName;});
				if (folder) {
					if (folder.i.length >= 90) {
						findex++;
					} else {
						break;
					}
				} else {
					d20.journal.addFolderToFolderStructure(theFolderName, spells.id);
					d20.journal.refreshJournalList();
					journalFolder = d20.Campaign.get("journalfolder");
					journalFolderObj = JSON.parse(journalFolder);
					spells = journalFolderObj.find(function(a) {return a.n && a.n === "Spells";});
					folder = spells.i.find(function(f) {return f.n === theFolderName;});
					break;
				}
			}
			if (!folder) {
				console.log("> Failed to find or create source folder!");
				return;
			}
			// build spell handout
			d20.Campaign.handouts.create({
				name: name
			}, {
				success: function(handout) {
					if (!data.school) data.school = "A";
					if (!data.range) data.range = "Self";
					if (!data.duration) data.duration = "Instantaneous";
					if (!data.components) data.components = "";
					if (!data.time) data.components = "1 action";

					const r20Data = {};
					if (data.roll20) Object.assign(r20Data, data.roll20);
					Object.assign(
						r20Data,
						{
							"Level": String(data.level),
							"Range": Parser.spRangeToFull(data.range),
							"School": Parser.spSchoolAbvToFull(data.school),
							"Source": "5etoolsR20",
							"Classes": Parser.spClassesToFull(data.classes),
							"Category": "Spells",
							"Duration": Parser.spDurationToFull(data.duration),
							"Material": "",
							"Components": parseComponents(data.components),
							"Casting Time": Parser.spTimeListToFull(data.time)
						}
					);

					var r20json = {
						name: data.name,
						content: "",
						htmlcontent: "",
						data: r20Data
					};
					if (data.components.m && data.components.m.length) r20json.data["Material"] = data.components.m;
					if (data.meta) {
						if (data.meta.ritual) r20json.data["Ritual"] = "Yes";
					}
					if (data.duration.filter(d => d.concentration).length > 0) {
						r20json.data["Concentration"] = "Yes";
					}
					var notecontents = "";
					var gmnotes = "";
					notecontents += `<p><h3>${data.name}</h3>
<em>${Parser.spLevelSchoolMetaToFull(data.level, data.school, data.meta)}</em></p><p>
<strong>Casting Time:</strong> ${Parser.spTimeListToFull(data.time)}<br>
<strong>Range:</strong> ${Parser.spRangeToFull(data.range)}<br>
<strong>Components:</strong> ${Parser.spComponentsToFull(data.components)}<br>
<strong>Duration:</strong> ${Parser.spDurationToFull(data.duration)}<br>
</p>`;
					const renderer = new EntryRenderer();
					const renderStack = [];
					const entryList = {type: "entries", entries: data.entries};
					renderer.setBaseUrl("https://TheGiddyLimit.github.io/");
					renderer.recursiveEntryRender(entryList, renderStack, 1);
					r20json.content = renderStack.join(" ");
					notecontents += renderStack.join("");
					if (data.entriesHigherLevel) {
						const hLevelRenderStack = [];
						const higherLevelsEntryList = {type: "entries", entries: data.entriesHigherLevel};
						renderer.recursiveEntryRender(higherLevelsEntryList, hLevelRenderStack, 2);
						r20json.content += "\n\nAt Higher Levels: " + hLevelRenderStack.join(" ").replace("At Higher Levels.", "");
						notecontents += hLevelRenderStack.join("");
					}
					notecontents += `<p><strong>Classes:</strong> ${Parser.spClassesToFull(data.classes)}</p>`;
					gmnotes = JSON.stringify(r20json);
					console.log(notecontents);
					handout.updateBlobs({notes: notecontents, gmnotes: gmnotes});
					var injournals = ($("#import-showplayers").prop("checked")) ? ["all"].join(",") : "";
					handout.save({notes: (new Date).getTime(), inplayerjournals: injournals});
					d20.journal.addItemToFolderStructure(handout.id, folder.id);
				}
			});
			d20plus.remaining--;
			if (d20plus.remaining === 0) {
				setTimeout(function() {
					$("#import-name").text("DONE!");
					$("#import-remaining").text("0");
				}, 1000);
			}
			d20plus.log(`Finished import of [${name}]`);
		}, timeout);
	};

	// parse spell components
	function parseComponents(components) {
		const out = [];
		if (components.v) out.push("V");
		if (components.s) out.push("S");
		if (components.m) out.push("M");
		return out.join(" ");
	}

	// Import Items button was clicked
	d20plus.items.button = function() {
		var url = $("#import-items-url").val();
		if (url !== null) d20plus.items.load(url);
	};

	// Fetch items data from file
	d20plus.items.load = function(url) {
		$("a.ui-tabs-anchor[href='#journal']").trigger("click");
		var x2js = new X2JS();
		var datatype = $("#import-datatype").val();
		if (datatype === "json") datatype = "text";
		$.ajax({
			type: "GET",
			url: url,
			dataType: datatype,
			success: function(data) {
				try {
					d20plus.log("Importing Data (" + $("#import-datatype").val().toUpperCase() + ")");
					itemdata = (datatype === "XML") ? x2js.xml2json(data) : JSON.parse(data.replace(/^var .* \= /g, ""));
					var length = itemdata.item.length;
					itemdata.item.sort(function(a,b) {
						if (a.name < b.name) return -1;
						if (a.name > b.name) return 1;
						return 0;
					});
					// building list for checkboxes
					$("#import-list .list").html("");
					$.each(itemdata.item, function(i, v) {
						try {
							$("#import-list .list").append(`<label><input type="checkbox" data-listid="${i}"> <span class="name">${v.name}</span></label>`);
						} catch (e) {
							console.log("Error building list!", e);
							d20plus.addImportError(v.name);
						}
					});
					var options = {
						valueNames: [ 'name' ]
					};
					var importList = new List ("import-list", options);
					$("#import-options label").hide();
					$("#import-overwrite").parent().show();
					$("#delete-existing").parent().show();
					$("#organize-by-source").parent().show();
					$("#import-showplayers").parent().show();
					$("#d20plus-importlist").dialog("open");
					$("#d20plus-importlist input#importlist-selectall").unbind("click");
					$("#d20plus-importlist input#importlist-selectall").bind("click", function() {$("#import-list .list input").prop("checked", $(this).prop("checked"));});
					$("#d20plus-importlist button").unbind("click");
					$("#d20plus-importlist button#importstart").bind("click", function() {
						$("#d20plus-importlist").dialog("close");
						var overwrite = $("#import-overwrite").prop("checked");
						var deleteExisting = $("#delete-existing").prop("checked");
						$("#import-list .list input").each(function() {
							if (!$(this).prop("checked")) return;
							var itemnum = parseInt($(this).data("listid"));
							var curitem = itemdata.item[itemnum];
							try {
								console.log("> " + (itemnum + 1) + "/" + length + " Attempting to import item [" + curitem.name + "]");
								d20plus.items.import(curitem, overwrite, deleteExisting);
							} catch (e) {
								console.log("Error Importing!", e);
								d20plus.addImportError(curitem.name);
							}
						});
					});
				} catch (e) {
					console.log("> Exception ", e);
				}
			},
			error: function(jqXHR, exception) {d20plus.handleAjaxError(jqXHR, exception);}
		});
		d20plus.timeout = 500;
	};

	// Import individual items
	d20plus.items.import = function(data, overwrite, deleteExisting) {
		var fname = $("#organize-by-source").prop("checked") ? Parser.sourceJsonToFull(data.source) : (d20plus.items.parseType(data.type ? data.type.split(",")[0] : (data.wondrous ? "Wondrous Item" : data.technology)));
		var findex = 1;
		var folder;
		d20.journal.refreshJournalList();
		var journalFolder = d20.Campaign.get("journalfolder");
		if (journalFolder === "") {
			d20.journal.addFolderToFolderStructure("Characters");
			d20.journal.refreshJournalList();
			journalFolder = d20.Campaign.get("journalfolder");
		}
		var journalFolderObj = JSON.parse(journalFolder);
		var items = journalFolderObj.find(function(a) {return a.n && a.n === "Items";});
		if (!items) d20.journal.addFolderToFolderStructure("Items");
		d20.journal.refreshJournalList();
		journalFolder = d20.Campaign.get("journalfolder");
		journalFolderObj = JSON.parse(journalFolder);
		items = journalFolderObj.find(function(a) {return a.n && a.n === "Items";});
		var name = data.name || "(Unknown Name)";
		// check for duplicates
		var dupe = false;
		$.each(items.i, function(i, v) {
			if (d20plus.objectExists(items.i, v.id, name)) dupe = true;
			if (overwrite || deleteExisting) d20plus.deleteObject(items.i, v.id, name);
		});
		if (deleteExisting || (dupe && !overwrite)) return;
		d20plus.remaining++;
		if (d20plus.timeout === 500) {
			$("#d20plus-import").dialog("open");
			$("#import-remaining").text("d20plus.remaining");
		}
		timeout = d20plus.timeout;
		d20plus.timeout += 2500;
		setTimeout(function() {
			d20plus.log("Running import of [" + name + "]");
			$("#import-remaining").text(d20plus.remaining);
			$("#import-name").text(name);
			d20.journal.refreshJournalList();
			journalFolder = d20.Campaign.get("journalfolder");
			journalFolderObj = JSON.parse(journalFolder);
			items = journalFolderObj.find(function(a) {return a.n && a.n === "Items";});
			// make source folder
			for (i = -1; i < items.i.length; i++) {
				var theFolderName = (findex === 1) ? fname : fname + " " + findex;
				folder = items.i.find(function(f) {return f.n === theFolderName;});
				if (folder) {
					if (folder.i.length >= 90) {
						findex++;
					} else {
						break;
					}
				} else {
					d20.journal.addFolderToFolderStructure(theFolderName, items.id);
					d20.journal.refreshJournalList();
					journalFolder = d20.Campaign.get("journalfolder");
					journalFolderObj = JSON.parse(journalFolder);
					items = journalFolderObj.find(function(a) {return a.n && a.n === "Items";});
					folder = items.i.find(function(f) {return f.n === theFolderName;});
					break;
				}
			}
			if (!folder) {
				console.log("> Failed to find or create source folder!");
				return;
			}
			// build item handout
			d20.Campaign.handouts.create({
				name: name
			}, {
				success: function(handout) {
					var notecontents = "";
					const typeArray = [];
					if (data.wondrous) typeArray.push("Wondrous Item");
					if (data.technology) typeArray.push(data.technology);
					if (data.age) typeArray.push(data.age);
					if (data.weaponCategory) typeArray.push(data.weaponCategory+" Weapon");
					var type = data.type;
					if (data.type) typeArray.push(d20plus.items.parseType(data.type));
					var typestring = typeArray.join(", ");
					var damage = "";
					if (data.dmg1 && data.dmgType) damage = data.dmg1 + " " + Parser.dmgTypeToFull(data.dmgType);
					var armorclass = "";
					if (type === "S") armorclass = "+" + data.ac;
					if (type === "LA") armorclass = data.ac + " + Dex";
					if (type === "MA") armorclass = data.ac + " + Dex (max 2)";
					if (type === "HA") armorclass = data.ac;
					var properties = "";
					if (data.property) {
						var propertieslist = data.property.split(",");
						for (var i = 0; i < propertieslist.length; i++) {
							var a = d20plus.items.parseProperty(propertieslist[i]);
							var b = propertieslist[i];
							if (b === "V") a = a + " (" + data.dmg2 + ")";
							if (b === "T" || b === "A") a = a + " (" + data.range + "ft.)";
							if (b === "RLD") a = a + " (" + data.reload + " shots)";
							if (i > 0) a = ", " + a;
							properties += a;
						}
					}
					var reqAttune = data.reqAttune;
					var attunementstring = "";
					if (reqAttune) {
						if (reqAttune === "YES") {
							attunementstring = " (Requires Attunement)"
						} else if (reqAttune === "OPTIONAL") {
							attunementstring = " (Attunement Optional)"
						} else {
							reqAttune = " (Requires Attunement "+reqAttune+")";
						}
					}
					notecontents += `<p><h3>${data.name}</h3></p><em>${typestring}`;
					if (data.tier) notecontents += ", " + data.tier;
					var rarity = data.rarity;
					var ismagicitem = (rarity !== "None" && rarity !== "Unknown");
					if (ismagicitem) notecontents += ", " + rarity;
					if (attunementstring) notecontents += attunementstring;
					notecontents += `</em>`;
					if (damage) notecontents += `<p><strong>Damage: </strong>${damage}</p>`;
					if (properties) notecontents += `<p><strong>Properties: </strong>${properties}</p>`;
					if (armorclass) notecontents += `<p><strong>Armor Class: </strong>${armorclass}</p>`;
					if (data.weight) notecontents += `<p><strong>Weight: </strong>${data.weight} lbs.</p>`;
					var itemtext = data.entries ? data.entries : "";
					const renderer = new EntryRenderer();
					const renderStack = [];
					const entryList = {type: "entries", entries: data.entries};
					renderer.setBaseUrl("https://TheGiddyLimit.github.io/");
					renderer.recursiveEntryRender(entryList, renderStack, 1);
					var textstring = renderStack.join("");
					if (textstring) {
						notecontents += `<hr>`;
						notecontents += textstring;
					}
					console.log(notecontents);
					handout.updateBlobs({notes: notecontents});
					var injournals = ($("#import-showplayers").prop("checked")) ? ["all"].join(",") : "";
					handout.save({
						notes: (new Date).getTime(),
						inplayerjournals: injournals
					});
					d20.journal.addItemToFolderStructure(handout.id, folder.id);
				}
			});
			d20plus.remaining--;
			if (d20plus.remaining === 0) {
				setTimeout(function() {
					$("#import-name").text("DONE!");
					$("#import-remaining").text("0");
				}, 1000);
			}
		}, timeout);
	};

	d20plus.items.parseType = function(type) {
		const result = Parser.itemTypeToAbv(type);
		return result ? result : "n/a";
	};

	d20plus.items.parseDamageType = function(damagetype) {
		const result = Parser.dmgTypeToFull(damagetype);
		return result ? result : false;
	};

	d20plus.items.parseProperty = function(property) {
		if (property === "A") return "ammunition";
		if (property === "AF") return "ammunition";
		if (property === "BF") return "burst fire";
		if (property === "F") return "finesse";
		if (property === "H") return "heavy";
		if (property === "L") return "light";
		if (property === "LD") return "loading";
		if (property === "R") return "reach";
		if (property === "RLD") return "reload";
		if (property === "S") return "special";
		if (property === "T") return "thrown";
		if (property === "2H") return "two-handed";
		if (property === "V") return "versatile";
		return "n/a";
	}

	String.prototype.capFirstLetter = function() {
		return this.replace(/\w\S*/g, function(w) {return w.charAt(0).toUpperCase() + w.substr(1).toLowerCase();});
	};

	d20plus.dmscreenButton = `<li id="dmscreen-button" tip="DM Screen">
	<span class="pictos">N</span>
</li>`;

	// This is an older version of the repo. The newer version has a security error when loaded over SSL :(
	d20plus.dmscreenHtml = `<div id="dmscreen-dialog">
	<iframe src="//ftwinston.github.io/5edmscreen/mobile"></iframe>
</div>`;

	d20plus.difficultyHtml = `<span class="difficulty" style="position: absolute"></span>`;

	d20plus.multipliers = [1, 1.5, 2, 2.5, 3, 4, 5];

	d20plus.formulas = {
		"ogl": {
			"CR": "@{npc_challenge}",
			"AC": "@{ac}",
			"NPCAC": "@{npc_ac}",
			"HP": "@{hp}",
			"PP": "@{passive_wisdom}",
			"macro": ""
		},
		"community": {
			"CR": "@{npc_challenge}",
			"AC": "@{AC}",
			"NPCAC": "@{AC}",
			"HP": "@{HP}",
			"PP": "10 + @{perception}",
			"macro": ""
		},
		"shaped": {
			"CR": "@{challenge}",
			"AC": "@{AC}",
			"NPCAC": "@{AC}",
			"HP": "@{HP}",
			"PP": "@{repeating_skill_$11_passive}",
			"macro": "shaped_statblock"
		}
	};

	d20plus.importListHTML = `<div id="d20plus-importlist" title="Import...">
	<p><input type="checkbox" title="Select all" id="importlist-selectall"></p>
	<p>
	<span id="import-list"><input class="search" autocomplete="off" placeholder="Search list..."><br><span class="list" style="max-height: 600px; overflow-y: scroll; display: block; margin-top: 1em;"></span></span>
	</p>
	<p id="import-options">
	<label><input type="checkbox" title="Import by source" id="organize-by-source"> Import by source instead of type?</label>
	<label><input type="checkbox" title="Make items visible to all players" id="import-showplayers" checked> Make handouts visible to all players?</label>
	<label><input type="checkbox" title="Overwrite existing" id="import-overwrite"> Overwrite existing entries?</label>
	<label><input type="checkbox" title="Delete existing" id="delete-existing"> ONLY delete selected entries?</label>
	</p>
	<button type="button" id="importstart" alt="Load" title="Load Monsters" class="btn" role="button" aria-disabled="false">
	<span>Load</span>
	</button>
</div>`

	d20plus.importDialogHtml = `<div id="d20plus-import" title="Importing...">
	<p>
	<h3 id="import-name"></h3>
	</p>
	<span id="import-remaining"></span> remaining
	<p></p>
	Errors: <span id="import-errors">0</span>
</div>`;

	d20plus.refreshButtonHtml = `<button type="button" alt="Refresh" title="Refresh" class="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only pictos bigbuttonwithicons" role="button" aria-disabled="false">
	<span class="ui-button-text" style="">1</span>
</button>`;

	d20plus.settingsHtml = `<hr>
<h3>5etoolsR20 v${d20plus.version}</h3>
<label>Data Type:</label>
<select id="import-datatype" value="json">
	<option value="json">JSON</option>
	<option value="xml">XML</option>
</select>
<h4>Monster Importing</h4>
<p>
<label for="import-monster-url">Monster Data URL:</label>
<select id="button-monsters-select">
	<option value="${monsterdataurl}">Default</option>
	<option value="${monsterdataurlTob}">Tome of Beasts</option>
	<option value="">Custom</option>
</select>
<input type="text" id="import-monster-url" value="${monsterdataurl}">
<a class="btn" href="#" id="button-monsters-load">Import Monsters</a>
</p>
<h4>Item Importing</h4>
<p>
<label for="import-items-url">Item Data URL:</label>
<input type="text" id="import-items-url" value="${itemdataurl}">
<a class="btn" href="#" id="import-items-load">Import Items</a>
</p>
<h4>Spell Importing</h4>
<p>
<label for="import-spell-url">Spell Data URL:</label>
<input type="text" id="import-spell-url" value="${spelldataurl}">
<a class="btn" href="#" id="button-spells-load">Import Spells</a>
</p>
<a class="btn bind-drop-locations" href="#" id="bind-drop-locations">Prepare Drag-and-Drop Spells/Items</a>`;

	d20plus.cssRules = [
		{
			s: "#initiativewindow ul li span.initiative,#initiativewindow ul li span.ac,#initiativewindow ul li span.hp,#initiativewindow ul li span.pp,#initiativewindow ul li span.cr,#initiativewindow ul li span.initmacro",
			r: "font-size: 25px;font-weight: bold;text-align: right;float: right;padding: 2px 5px;width: 10%;min-height: 20px;"
		},
		{
			s: "#initiativewindow ul li span.editable input",
			r: "width: 100%; box-sizing: border-box;height: 100%;"
		},
		{
			s: "#initiativewindow div.header",
			r: "height: 30px;"
		},
		{
			s: "#initiativewindow div.header span",
			r: "cursor: default;font-size: 15px;font-weight: bold;text-align: right;float: right;width: 10%;min-height: 20px;padding: 5px;"
		},
		{
			s: ".ui-dialog-buttonpane span.difficulty",
			r: "display: inline-block;padding: 5px 4px 6px;margin: .5em .4em .5em 0;font-size: 18px;"
		},
		{
			s: ".ui-dialog-buttonpane.buttonpane-absolute-position",
			r: "position: absolute;bottom: 0;box-sizing: border-box;width: 100%;"
		},
		{
			s: ".ui-dialog.dialog-collapsed .ui-dialog-buttonpane",
			r: "position: initial;"
		},
		{
			s: "#dmscreen-dialog iframe",
			r: "width: 100%;height: 100%;position: absolute;top: 0;left: 0;border: 0;"
		},
		{
			s: ".token .cr,.header .cr",
			r: "display: none!important;"
		},
		{
			s: "li.handout.compendium-item .namecontainer",
			r: "box-shadow: inset 0px 0px 25px 2px rgb(195, 239, 184);"
		},
		{
			s: ".bind-drop-locations:active",
			r: "box-shadow: inset 0px 0px 25px 2px rgb(195, 239, 184);"
		}
	];

	d20plus.initiativeHeaders = `<div class="header">
	<span class="ui-button-text initmacro">Sheet</span>
	<span class="initiative" alt="Initiative" title="Initiative">Init</span>
  <span class="pp" alt="Passive Perception" title="Passive Perception">PP</span>
  <span class="ac" alt="AC" title="AC">AC</span>
  <span class="cr" alt="CR" title="CR">CR</span>
  <span class="hp" alt="HP" title="HP">HP</span>
</div>`;

	d20plus.initiativeTemplate = `<script id="tmpl_initiativecharacter" type="text/html">
	<![CDATA[
		<li class='token <$ if (this.layer === "gmlayer") { $>gmlayer<$ } $>' data-tokenid='<$!this.id$>' data-currentindex='<$!this.idx$>'>
			<span alt='Sheet Macro' title='Sheet Macro' class='initmacro'>
				<button type='button' class='initmacrobutton ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only pictos' role='button' aria-disabled='false'>
				<span class='ui-button-text'>N</span>
				</button>
			</span>
			<span alt='Initiative' title='Initiative' class='initiative <$ if (this.iseditable) { $>editable<$ } $>'>
				<$!this.pr$>
			</span>
			<$ var token = d20.Campaign.pages.get(d20.Campaign.activePage()).thegraphics.get(this.id); $>
			<$ var char = (token) ? token.character : null; $>
			<$ if (char) { $>
				<$ var npc = char.attribs.find(function(a){return a.get("name").toLowerCase() == "npc" }); $>
				<$ var passive = char.autoCalcFormula('@{passive}') || char.autoCalcFormula('||PP||'); $>
				<span class='pp' alt='Passive Perception' title='Passive Perception'><$!passive$></span>
				<span class='ac' alt='AC' title='AC'>
					<$ if(npc && npc.get("current") == "1") { $>
						<$!char.autoCalcFormula('||NPCAC||')$>
					<$ } else { $>
						<$!char.autoCalcFormula('||AC||')$>
					<$ } $>
				</span>
				<span class='cr' alt='CR' title='CR'>
					<$ if(npc && npc.get("current") == "1") { $>
						<$!char.attribs.find(function(e) { return e.get("name").toLowerCase() === "npc_challenge" }).get("current")$>
					<$ } $>
				</span>
				<span class='hp editable' alt='HP' title='HP'>
					<$ if(npc && npc.get("current") == "1") { $>
						<$!token.attributes.bar3_value$>
					<$ } else { $>
						<$!char.autoCalcFormula('||HP||')$>
					<$ } $>
				</span>
			<$ } $>
			<$ if (this.avatar) { $><img src='<$!this.avatar$>' /><$ } $>
			<span class='name'><$!this.name$></span>
				<div class='clear' style='height: 0px;'></div>
				<div class='controls'>
			<span class='pictos remove'>#</span>
			</div>
		</li>
	]]>
</script>`;

	/* object.watch polyfill by Eli Grey, http://eligrey.com */
	if (!Object.prototype.watch) {
		Object.defineProperty(Object.prototype, "watch", {
			enumerable: false,
			configurable: true,
			writable: false,
			value: function(prop, handler) {
				var
					oldval = this[prop],
					newval = oldval,
					getter = function() {
						return newval;
					},
					setter = function(val) {
						oldval = newval;
						return (newval = handler.call(this, prop, oldval, val));
					};
				if (delete this[prop]) {
					Object.defineProperty(this, prop, {
						get: getter,
						set: setter,
						enumerable: true,
						configurable: true
					});
				}
			}
		});
	}
	if (!Object.prototype.unwatch) {
		Object.defineProperty(Object.prototype, "unwatch", {
			enumerable: false,
			configurable: true,
			writable: false,
			value: function(prop) {
				var val = this[prop];
				delete this[prop];
				this[prop] = val;
			}
		});
	}
	/* end object.watch polyfill */

	window.d20ext = {};
	window.watch("d20ext", function(id, oldValue, newValue) {
		d20plus.log("> Set Development");
		newValue.environment = "development";
		return newValue;
	});
	window.d20 = {};
	window.watch("d20", function(id, oldValue, newValue) {
		d20plus.log("> Obtained d20 variable");
		window.unwatch("d20ext");
		window.d20ext.environment = "production";
		newValue.environment = "production";
		return newValue;
	});
	d20plus.log("> Injected");
};

// Inject
if (window.top === window.self) unsafeWindow.eval("(" + D20plus.toString() + ")('" + GM_info.script.version + "')");