Greasy Fork

Greasy Fork is available in English.

MouseHunt - Favorite Setups+

Unlimited custom favorite trap setups!

当前为 2023-04-01 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MouseHunt - Favorite Setups+
// @author       PersonalPalimpsest (asterios)
// @namespace    http://greasyfork.icu/en/users/900615-personalpalimpsest
// @version      2.3.3
// @description  Unlimited custom favorite trap setups!
// @grant        GM_addStyle
// @match        http://www.mousehuntgame.com/*
// @match        https://www.mousehuntgame.com/*
// ==/UserScript==
GM_addStyle ( `
#tsitu-fave-setups {
  background-color: #F5F5F5;
  position: fixed;
  z-index: 69;
  left: 5px;
  top: 5px;
  border: solid 3px #696969;
  padding: 10px;
  text-align: center;
  border-radius: 10px;
  max-height: 95vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  font-size: inherit;
  resize: both;
  min-width: min-content;
}

#tsitu-fave-setups button {
	  cursor: pointer;
}

#tsitu-fave-setups .title {
    font-weight: bold;
    font-size: 160%;
    text-decoration: underline;
}

#tsitu-fave-setups #close-button {
    float: right;
    font-size: 15px;
    color: rgba(690,0,0,0.69);
    padding: 1px 5px;
    margin: -11px;
    //background-color: rgba(420,0,0,0.420);
    //border-radius: 0px 5px;
    //border: none;
}

.btn-group {
  display: inline-flex;
  justify-content: center;
  padding: 8px 0px;
}
.btn-group .button {
  margin: -1px;
  flex-grow: 1;
  font-size: 100%;
}
#saveButton {
  font-weight: bold;
  font-size: 120%;
}

.favInput {
  //display: flex;
  //flex-grow: 1;
  width: calc(100% - 5px*2 - 2px);
  padding: 1px 5px;
  //justify-content: center;
  margin: -1px -1px;
  font-size: 110%;
}

#collapsible {
  display: none;
}

#dataListDiv {
  max-height: 0px;
  overflow-y: hidden;
  opacity: 1;
  transition: max-height 250ms ease-in-out, opacity 500ms;
}

#collapsible:checked + #dataListDiv {
  max-height: 100%;
  overflow-y: visible;
  opacity: 1;
}

#dataListTable {
 width: 100%;
}

.setupSelectorDiv {
  padding-bottom: 5px;
}

#scroller {
  /* fill parent */
  display: block;
  width: 100%;
  height: 100%;
  /* set to some transparent color */
  border-color: rgba(0, 0, 0, 0.0);
  transition: border-color 300ms ease-in-out;
  overflow-y: scroll;
  padding-right: 2px;
  margin-bottom: 5px;
}

#scroller:hover {
  /* the color we want the scrollbar on hover */
  border-color: rgba(0, 0, 0, 0.3);
}

#scroller::-webkit-scrollbar,
#scroller::-webkit-scrollbar-thumb,
#scroller::-webkit-scrollbar-corner {
  /* add border to act as background-color */
  border-right-style: outset;
  border-right-width: 3px;
  /* inherit border-color to inherit transitions */
  border-color: inherit;
}
#scroller::-webkit-scrollbar {
  width: 3px;
  height: 3px;
  border-color: rgba(0,0,0,0);
}
#scroller::-webkit-scrollbar-thumb {
  border-color: inherit;
  border-radius: 50px;
}

#scroller::-webkit-scrollbar-thumb:active {
  border-color: rgba(0, 0, 0, 0.5);
}

// #setupTableDiv {
//   overflow-y: scroll;
//   max-height: 100%;
// }
// #setupTableDiv::-webkit-scrollbar
// {
//   width: 5px;
//   background-color: #F5F5F5;
// }
// #setupTableDiv::-webkit-scrollbar-thumb
// {
//   background-color: #696969;
//   border-radius: 420px;
// }
#setupTbody tr:nth-child(odd){
  background-color: rgba(0, 0, 0, 0.25);
}
#setupTbody tr:nth-child(even){
  background-color: rgba(0, 0, 0, 0.069);
}
.tsitu-fave-setup-row {
    display: grid;
    align-items: stretch;
    grid-template-columns: 1fr auto 2em;
    grid-template-rows: 50% 50%;
    grid-template-areas:
        "a c d"
        "b c e";
    padding: 3px;
    border-radius: 3px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.tsitu-fave-setup-namespan {
    grid-area: a;
    font-size: inherit;
    text-align: left;
    text-overflow: ellipsis;
    padding-top: 3px;
    padding-left: 3px;
    margin-right: -1px;
}
.travelButton {
    grid-area: b;
    overflow: hidden;
    padding: 0px 2px;
    font-size: inherit;
    display: flex;
    align-items: center;
    justify-content: center;
    width: auto;
    text-overflow: ellipsis;
    min-width: max-content;
}
#imgSpan {
  grid-area: c;
  //align-items: center;
  max-height: 100%;
  max-width: max-content;
  min-width: max-content;
  padding: 0px 0px 0px 0px;
  margin-left: 3px;
  margin-right: -1px;
  //justify-items: center;
  overflow: hidden;
}
#imgSpan img {
  max-height: 100%;
  max-width: 100%;
  height: 3.2vh; /* change this to change the overall size which should scale with this */
//   width: auto;
//   object-fit: scale-down;
  margin-bottom: -3px;
}
#editButton {
  grid-area: d;
  text-align: center;
  font-size: inherit;
  padding: 0;
  margin-bottom: -1px;
}
#deleteButton {
  grid-area: e;
  text-align: center;
  font-size: inherit;
  padding: 0;
  margin-top: -1px;
}
#resizeButton {
    display: flex;
    flex-direction: right;
    float: right;
    font-size: 15px;
    color: rgba(690,0,0,0.69);
    padding: 1px 5px;
    margin: -11px;
    background-color: rgba(420,0,0,0.420);
    border-radius: 5px 0px;
    border: none;
}
`);
(function () {
	// Observe Camp page for mutations (to re-inject button)
	const observerTarget = document.querySelector(".mousehuntPage-content");
	if (observerTarget) {
		MutationObserver =
			window.MutationObserver ||
			window.WebKitMutationObserver ||
			window.MozMutationObserver;

		const observer = new MutationObserver(function () {
			const campExists = document.querySelector(
				".mousehuntPage-content[data-page = PageCamp]"
			);
			if (campExists) {
				// Disconnect and reconnect later to prevent infinite mutation loop
				observer.disconnect();

				// Re-render buttons (mainly for alternate TEM area placement)
				injectUI();

				observer.observe(observerTarget, {
					childList: true,
					subtree: true
				});
			}
		});

		observer.observe(observerTarget, {
			childList: true,
			subtree: true
		});
	}

	// Sorted from low to high (matches top HUD except weapon/base swapped for clarity)
	const displayOrder = {
		weapon: 1,
		base: 2,
		bait: 3,
		cheese: 3,
		trinket: 4,
		charm: 4,
		skin: 5,
		location: 6
	};

	// Pull and save location list
	const xhr = new XMLHttpRequest();
	xhr.open(
		"POST",
		`https://www.mousehuntgame.com/managers/ajax/pages/page.php?page_class=HunterProfile&page_arguments%5Btab%5D=mice&page_arguments%5Bsub_tab%5D=location&uh=${user.unique_hash}`
	);
	xhr.onload = function () {
		const response = JSON.parse(xhr.responseText);
		const locations =
			  response.page.tabs.mice.subtabs[1].mouse_list.categories;
		if (locations) {
			const masterObj = {};

			locations.forEach(el => {
				const obj = {};
				obj["type"] = el.type;
				masterObj[el.name] = obj;
			});

			localStorage.setItem(
				"ast-location-mapping",
				JSON.stringify(masterObj)
			);
		};
	};
	xhr.send();

	// Re-render on location change
	const travelObserver = XMLHttpRequest.prototype.open;
	XMLHttpRequest.prototype.open = function () {
		this.addEventListener('load', function () {
			if (this.responseURL == `https://www.mousehuntgame.com/managers/ajax/users/changeenvironment.php`) {
				console.log('Travel detected');
				toggleRender();
				toggleRender();
			}
		})
		travelObserver.apply(this, arguments);
	}

	const originalOpen = XMLHttpRequest.prototype.open;
	XMLHttpRequest.prototype.open = function () {
		this.addEventListener("load", function () {
			if (
				this.responseURL ===
				"https://www.mousehuntgame.com/managers/ajax/users/gettrapcomponents.php"
			) {
				let data;
				try {
					data = JSON.parse(this.responseText).components;
					if (data && data.length > 0) {
						const ownedItems = JSON.parse(
							localStorage.getItem("tsitu-owned-components")
						) || {
							bait: {},
							base: {},
							weapon: {},
							trinket: {},
							skin: {},
							location: {}
						};

						data.forEach(el => {
							let key = el.name;
							const arr = [el.item_id, el.thumbnail];
							if (el.classification === "skin") {
								arr.push(el.component_name);
							}

							if (el.classification === "weapon") {
								if (el.name.indexOf("Golem Guardian") >= 0) {
									// Golem Guardian edge case
									arr[0] = 2732;
									key = "Golem Guardian Trap";
								} else if (el.name.indexOf("Isle Idol") >= 0) {
									// Isle Idol edge case
									arr[0] = 1127;
									key = "Isle Idol Trap";
								}
							}

							ownedItems[el.classification][key] = arr;

							// switch statement for all 5 classifications
							// ^ custom array, last element = image_trap hash if available
							// ^ ideally thumbnail is also just the hash portion, img src can be trivially built dynamically
							// ^ i believe this is for synergy with equipment-preview, so it's not necessary for now
						});

						// Edge case cleanup
						Object.keys(ownedItems.weapon).forEach(el => {
							if (
								(el.indexOf("Golem Guardian") >= 0 &&
								 el !== "Golem Guardian Trap") ||
								(el.indexOf("Isle Idol") >= 0 && el !== "Isle Idol Trap")
							) {
								delete ownedItems.weapon[el];
							}
						});

						localStorage.setItem(
							"tsitu-owned-components",
							JSON.stringify(ownedItems)
						);
						localStorage.setItem("favorite-setup-timestamp", Date.now());
						// const existing = document.querySelector("#tsitu-fave-setups");
						// if (existing)
						toggleRender();
						toggleRender();
					} else {
						console.log(
							"Invalid components array data from gettrapcomponents.php"
						);
					}
				} catch (error) {
					console.log(
						"Failed to process server response for gettrapcomponents.php"
					);
					console.error(error.stack);
				}
			}
		});
		originalOpen.apply(this, arguments);
	};

	function toggleRender() {
		const existing = document.querySelector("#tsitu-fave-setups");
		if (existing) {
			// console.log(existing);
			localStorage.setItem('showSetups', "N");
			localStorage.setItem("favorite-setup-saved-height",existing.style.height);
			localStorage.setItem("favorite-setup-saved-width",existing.style.width);
			// console.log(existing.style.height);
			existing.remove();
		}
		else {
			localStorage.setItem('showSetups', "Y");
			const rawData = localStorage.getItem("tsitu-owned-components");
			var editSort = -1; // ast location mod. change to -2 if you want new setups to appear above location sorted setups until they are manually sorted.
			const locMap = JSON.parse(localStorage.getItem("ast-location-mapping")); // ast location mod
			// aliases for locations with multiple environment_names for the same environment_type
			locMap["Cursed City"] = {"type": "lost_city"};
			locMap["Sand Crypts"] = {"type": "sand_dunes"};
			locMap["Twisted Garden"] = {"type": "desert_oasis"};

			if (rawData) {
				const data = JSON.parse(rawData);
				data.location = locMap;
				const dataKeys = Object.keys(data).sort((a, b) => {
					return displayOrder[a] - displayOrder[b];
				});

				async function batchLoad(
				baitName,
				 baseName,
				 weaponName,
				 trinketName,
				 skinName
				) {
					if (weaponName.indexOf("Golem Guardian") >= 0) {
						weaponName = "Golem Guardian Trap";
					}
					if (weaponName.indexOf("Isle Idol") >= 0) {
						weaponName = "Isle Idol Trap";
					}

					// Diff current setup with proposed batch to minimize server load
					const diff = {};
					if (data.bait[baitName] && user.bait_name !== baitName) {
						diff.bait = data.bait[baitName][0];
					}
					if (data.base[baseName] && user.base_name !== baseName) {
						diff.base = data.base[baseName][0];
					}
					if (data.weapon[weaponName] && user.weapon_name !== weaponName) {
						diff.weapon = data.weapon[weaponName][0];
					}
					if (data.trinket[trinketName] && user.trinket_name !== trinketName) {
						diff.trinket = data.trinket[trinketName][0];
					}
					// if (
					//   data.skin[skinName] &&
					//   data.skin[skinName][2] === weaponName &&
					//   user.skin_item_id !== data.skin[skinName][0]
					//   // note: this will probably proc every single time... diff AFTER weapon swap?
					// ) {
					//   diff.skin = data.skin[skinName][0];
					// }

					if (baitName === "N/A") diff.bait = "disarm";
					if (trinketName === "N/A") diff.trinket = "disarm";
					// if (skinName === "N/A") diff.skin = "disarm";

					const diffKeys = Object.keys(diff).sort((a, b) => {
						return displayOrder[a] - displayOrder[b];
					});

					if (diffKeys.length === 0) {
						return; // Cancel if setup isn't changing
					} else if (diffKeys.length >= 2) {
						localStorage.setItem("tsitu-batch-loading", true); // Minimize Mapping Helper TEM requests by setting an in-progress bool
					}

					function sleep(ms) {
						return new Promise(resolve => setTimeout(resolve, ms));
					}

					let counter = 0;
					for (let classification of diffKeys) {
						/**
           * TODO: Investigate bug that de-skins a weapon if you've used mobile app FS to arm a skinless weapon setup
           * Attempted to emulate browser item selector click by calling `app.pages.CampPage.armItem(element)`
           * Passed in a "fake" element with data-item-id so that `tmpItem` is derived
           * Inside `armItem`: 'syncInventory' and/or 'loadItems' fills in 'trapItems' so that 'getItemById' works
           * Final TrapControl requests seem to be identical with script... so the stuff before might be relevant
           */
						counter += 1;
						if (counter === diffKeys.length) {
							localStorage.setItem("tsitu-batch-loading", false); // Reset bool in time for last request
						}

						const id = diff[classification];
						if (id === "disarm") {
							await hg.utils.TrapControl.disarmItem(classification).go();
						} else {
							await hg.utils.TrapControl.armItem(id, classification).go();
							// console.log(id, classification);
							// const testEl = document.createElement("a");
							// testEl.setAttribute("data-item-id", id);
							// console.log(testEl);
							// await app.pages.CampPage.armItem(testEl);
						}
						await sleep(420);
					}

					// Another reset just in case something goes wrong inside the for...of
					localStorage.setItem("tsitu-batch-loading", false);

					// Deprecated the old method because unable to prevent userinventory.php calls from syncArmedItems (caused by mobile/regular desync)
					// Witnessed up to an 18 request simul-slam (at least +1 increments starting from 3 / n-1 duplicates with 1 response's items[] different)
					// If switching back to a previous setup then things do seem to be cached
					// CBS may investigate at some point, but going to use the new method above for v1.0 and beyond
				}

				// Main popup styling
				const mainDiv = document.createElement("div");
				mainDiv.id = "tsitu-fave-setups";

				// Top div styling (close button, title, drag instructions)
				const topDiv = document.createElement("div");
				topDiv.id = "header"
				topDiv.title = "Drag header to reposition this popup";

				const titleSpan = document.createElement("span");
				titleSpan.className = "title";
				titleSpan.innerText = "Favorite Setups";

				const closeButton = document.createElement("button");
				closeButton.id = "close-button";
				closeButton.textContent = "×";
				closeButton.onclick = function () {
					document.body.removeChild(mainDiv);
					localStorage.setItem('showSetups', "N");
				};

				topDiv.appendChild(closeButton);
				topDiv.appendChild(titleSpan);

				// Build <datalist> dropdowns
				const dataListTable = document.createElement("table");
				dataListTable.id = "dataListTable";

				for (let rawCategory of dataKeys) {
					let category = rawCategory;
					if (category === "sort") continue;
					if (category === "skin") continue; // note: only show appropriate skins if implementing
					if (category === "bait") category = "cheese";
					if (category === "trinket") category = "charm";

					const dataList = document.createElement("datalist");
					dataList.id = `favorite-setup-datalist-${category}`;
					for (let item of Object.keys(data[rawCategory]).sort()) {
						const option = document.createElement("option");
						option.value = item;
						dataList.appendChild(option);
					}

					const dataListInput = document.createElement("input");
					dataListInput.id = `favorite-setup-input-${category}`;
					dataListInput.className = "favInput";
					dataListInput.setAttribute("placeholder", `Select ${category}: `);
					dataListInput.setAttribute(
						"list",
						`favorite-setup-datalist-${category}`
					);

					const inputCol = document.createElement("td");
					inputCol.className = "inputCol";
					inputCol.appendChild(dataList);
					inputCol.appendChild(dataListInput);

					const dataListRow = document.createElement("tr");
					dataListRow.className = "dataListRow";

					dataListRow.appendChild(inputCol);
					dataListTable.appendChild(dataListRow);
				}

				const nameInput = document.createElement("input");
				nameInput.type = "text";
				nameInput.id = "favorite-setup-name";
				nameInput.className = "favInput";
				nameInput.setAttribute("placeholder", "Setup name: ");
				nameInput.required = true;
				nameInput.minLength = 1;
				nameInput.maxLength = 30;
				nameInput.addEventListener("keyup", function(event) {
					// Number 13 is the "Enter" key on the keyboard
					if (event.keyCode === 13) {
						// Cancel the default action, if needed
						event.preventDefault();
						// Trigger the button element with a click
						document.getElementById("saveButton").click();
					}
				});

				const nameInputCol = document.createElement("td");
				nameInputCol.appendChild(nameInput);

				const nameRow = document.createElement("tr");
				nameRow.appendChild(nameInputCol);
				dataListTable.appendChild(nameRow);

				// Hidden checkbox to toggle dataListDiv visibility
				const collapsibleCheckbox = document.createElement("input");
				collapsibleCheckbox.id = "collapsible";
				collapsibleCheckbox.type = "checkbox";

				const dataListDiv = document.createElement("div");
				dataListDiv.id = "dataListDiv";
				dataListDiv.appendChild(dataListTable);

				// Import setup / Save setup / Reset input buttons
				const saveButton = document.createElement("button");
				saveButton.id = "saveButton";
				saveButton.className = "button";
				saveButton.textContent = "Save Setup";
				saveButton.onclick = function () {
					const bait = document.querySelector("#favorite-setup-input-cheese")
					.value;
					const base = document.querySelector("#favorite-setup-input-base").value;
					const weapon = document.querySelector("#favorite-setup-input-weapon")
					.value;
					const charm = document.querySelector("#favorite-setup-input-charm")
					.value;
					// const skin = document.querySelector("#favorite-setup-input-skin").value;
					const name = document.querySelector("#favorite-setup-name").value;
					const location = document.querySelector("#favorite-setup-input-location").value;

					if (name.length >= 1 && name.length <= 30) {
						const obj = {};
						obj[name] = {
							bait: "N/A",
							base: "N/A",
							weapon: "N/A",
							trinket: "N/A",
							skin: "N/A",
							location: "N/A"
						};

						if (data.bait[bait] !== undefined) obj[name].bait = bait;
						if (data.base[base] !== undefined) obj[name].base = base;
						if (data.weapon[weapon] !== undefined) obj[name].weapon = weapon;
						if (data.trinket[charm] !== undefined) obj[name].trinket = charm;
						// if (data.skin[skin] !== undefined) obj[name].skin = skin;
						if (data.location[location] !== undefined) obj[name].location = location;
						obj[name].sort = editSort;
						console.log("saved setup '"+name+"': "+JSON.stringify(obj[name]));

						const storedRaw = localStorage.getItem("favorite-setups-saved");
						if (storedRaw) {
							const storedData = JSON.parse(storedRaw);
							if (storedData[name] !== undefined) {
								if (confirm(`Do you want to overwrite saved setup '${name}'?`)) {
									obj[name].sort = storedData[name].sort;
								} else {
									return;
								}
							}
							storedData[name] = obj[name];
							localStorage.setItem(
								"favorite-setups-saved",
								JSON.stringify(storedData)
							);
						} else {
							localStorage.setItem("favorite-setups-saved", JSON.stringify(obj));
						}
						var saveScroll = document.getElementById("scroller").scrollTop; // ast location mod
						toggleRender();
						toggleRender();
						document.getElementById("scroller").scrollTop = saveScroll;
						console.log("scroll position before/after: "+saveScroll+" / "+document.getElementById("scroller").scrollTop);
					} else {
						alert(
							"Please enter a name for your setup that is between 1-30 characters"
						);
					}
				};

				const loadButton = document.createElement("button");
				loadButton.id = "loadButton";
				loadButton.className = "button";
				loadButton.textContent = "Import setup";
				loadButton.onclick = function () {
					document.querySelector("#collapsible").checked = true; // to toggle collapsible
					document.querySelector("#favorite-setup-input-cheese").value =
						user.bait_name || "";
					document.querySelector("#favorite-setup-input-base").value =
						user.base_name || "";
					document.querySelector("#favorite-setup-input-weapon").value =
						user.weapon_name || "";
					document.querySelector("#favorite-setup-input-charm").value =
						user.trinket_name || "";
					document.querySelector("#favorite-setup-input-location").value =
						user.environment_name || "";
					// if (user.skin_name) {
					//   document.querySelector("#favorite-setup-input-skin").value =
					//     user.skin_name; // not really a thing, gotta use a qS probably or parse from LS ID-name map
					// }
					document.getElementById("favorite-setup-name").focus();
					console.log("loaded items: ",user.bait_name, user.base_name, user.weapon_name, user.trinket_name, user.environment_name);
				};

				const resetButton = document.createElement("button");
				resetButton.className = "button";
				resetButton.textContent = "Reset inputs";
				resetButton.onclick = function () {
					document.querySelector("#collapsible").checked = false; // to toggle collapsible
					document.querySelector("#favorite-setup-input-cheese").value = "";
					document.querySelector("#favorite-setup-input-base").value = "";
					document.querySelector("#favorite-setup-input-weapon").value = "";
					document.querySelector("#favorite-setup-input-charm").value = "";
					// document.querySelector("#favorite-setup-input-skin").value = "";
					document.querySelector("#favorite-setup-name").value = "";
					document.querySelector("#favorite-setup-input-location").value = "";
				};

				const disarmButton = document.createElement("button");
				disarmButton.className = "button";
				disarmButton.textContent = "Disarm";
				disarmButton.onclick = function () {
					hg.utils.TrapControl.disarmBait().go();
				};

				const buttonSpan = document.createElement("span");
				buttonSpan.className = "btn-group";
				buttonSpan.appendChild(loadButton);
				buttonSpan.appendChild(saveButton);
				buttonSpan.appendChild(resetButton);
				buttonSpan.appendChild(disarmButton);

				// Sort existing saved setups
				const savedRaw = localStorage.getItem("favorite-setups-saved");
				const savedSetups = JSON.parse(savedRaw) || {};
				const savedSetupSortKeys = Object.keys(savedSetups).sort((a, b) => {
					return savedSetups[a].sort - savedSetups[b].sort;
				});

				// Create setup dropdown selector
				const setupSelector = document.createElement("datalist");
				setupSelector.id = "favorite-setup-selector";
				for (let item of savedSetupSortKeys) {
					const option = document.createElement("option");
					option.value = item;
					setupSelector.appendChild(option);
				}

				const setupSelectorInput = document.createElement("input");
				setupSelectorInput.id = "favorite-setup-selector-input";
				setupSelectorInput.className =  "favInput";
				setupSelectorInput.setAttribute("placeholder", "Jump to setup:");
				setupSelectorInput.setAttribute("list", "favorite-setup-selector");
				setupSelectorInput.oninput = function () {
					const name = setupSelectorInput.value;
					if (savedSetups[name] !== undefined) {
						// remove background color for all setups (other than the banding)
						const rows = document.querySelectorAll("tr.tsitu-fave-setup-row");
						rows.forEach(el => {
							el.style.backgroundColor = "";
						});

						/**
           * Return row element that matches dropdown setup name
           * @param {string} name Dropdown setup name
           * @return {HTMLElement|false} <tr> that should be highlighted and scrolled to
           */
						function findElement(name) {
							for (let el of rows) {
								const spans = el.querySelectorAll("span");
								if (spans.length === 1) {
									if (name === spans[0].textContent) {
										el.style.display = "grid";
										return el;
									}
								}
							}

							return false;
						}

						// Calculate index for nth-child
						const targetEl = findElement(name);
						let nthChildValue = 0;
						for (let i = 0; i < rows.length; i++) {
							const el = rows[i];
							if (el === targetEl) {
								nthChildValue = i + 1;
								break;
							}
						}

						// tr:nth-child value (min = 1)
						const scrollRow = document.querySelector(
							`tr.tsitu-fave-setup-row:nth-child(${nthChildValue})`
						);
						if (scrollRow) {
							scrollRow.style.backgroundColor = "#D6EBA1";
							scrollRow.scrollIntoView({
								behavior: "auto",
								block: "nearest",
								inline: "nearest"
							});
						}

						setupSelectorInput.value = "";
					}
				};

				const setupSelectorDiv = document.createElement("div");
				setupSelectorDiv.className = "setupSelectorDiv";
				setupSelectorDiv.appendChild(setupSelector);
				setupSelectorDiv.appendChild(setupSelectorInput);

				// TODO: Improve async logic, probably await completion of a component switch otherwise might overlap and/or silently fail
				// TODO: [med]  Import/export setup "profiles" (separate dropdown of profiles) (export specific profile obj to dropbox/pastebin?)
				// ^ Profile management could be an elegant bulk grouping solution if done properly
				// TODO: [med]  Mobile UX for drag & drop as well as scrollable div (jquery-ui-touch-punch did not work for simulating touch events)
				// TODO: [low]  Skin implementation/checks (in-progress, but either save for later or scrap entirely since use case is minimal)

				const scroller = document.createElement("div");
				scroller.id = "scroller";
				// Setup table styling
				const setupTable = document.createElement("table");
				const setupTbody = document.createElement("tbody");
				setupTbody.id = "setupTbody";

				const setupTableDiv = document.createElement("div");
				setupTableDiv.id = "setupTableDiv";

				// Sort setups from the current location to the top of the list
				function locSort (name) {
					//console.log("saved loc: "+savedSetups[name].location, "\n loc bool: "+!!savedSetups[name].location, "\n current loc: "+user.environment_name, "\n current loc bool: "+!!user.environment_name, "\n current loc is saved loc: "+(user.environment_name===savedSetups[name].location), "\n test: "+(!!savedSetups[name].location && user.environment_name == savedSetups[name].location));
					if (user.environment_name === savedSetups[name].location) {
						//console.log("saved loc: "+savedSetups[name].location, "\n loc bool: "+!!savedSetups[name].location, "\n current loc: "+user.environment_name, "\n current loc bool: "+!!user.environment_name, "\n current loc is saved loc: "+(user.environment_name===savedSetups[name].location), "\n test: "+(!!savedSetups[name].location && user.environment_name == savedSetups[name].location));
						savedSetups[name].sort -= 420;
						//console.log("location sorted setup: "+savedSetups[name])
					};
				};

				savedSetupSortKeys.forEach(name => {
					locSort(name);
				});

				const sortedSetupKeys = Object.keys(savedSetups).sort((a, b) => {
					return savedSetups[a].sort - savedSetups[b].sort;
				});

				// Generate and append each saved setup as a new <tr>
				sortedSetupKeys.forEach(name => {
					generateRow(name);
				});

				function generateRow(name) {
					const el = savedSetups[name];
					const elKeys = Object.keys(savedSetups[name]).sort((a, b) => {
						return displayOrder[a] - displayOrder[b];
					});

					const imgSpan = document.createElement("button");
					imgSpan.className = "button";
					imgSpan.id = "imgSpan";

					for (let type of elKeys) {
						if (type === "sort") continue;
						if (type === "skin") continue;
						if (type === "location") continue;

						const img = document.createElement("img");
						let item = el[type];
						if (data.weapon["Golem Guardian Trap"] !== undefined) {
							if (type === "weapon") {
								if (item.indexOf("Golem Guardian") >= 0) {
									item = "Golem Guardian Trap";
								} else if (item.indexOf("Isle Idol") >= 0) {
									item = "Isle Idol Trap";
								}
							}
						}
						img.title = item;
						if (item === "N/A") {
							if (type === "bait") img.title = "Disarm Bait";
							if (type === "trinket") img.title = "Disarm Charm";
							// if (type === "skin") img.title = "Disarm Skin";
						}
						img.onclick = function () {
							// Mobile tooltip behavior = LOW priority because long pressing works on FF
							// const appendTitle = img.querySelector(".append-title");
							// if (!appendTitle) {
							//   const appendSpan = document.createElement("span");
							//   appendSpan.className = "append-title";
							//   appendSpan.style.position = "absolute";
							//   appendSpan.style.padding = "4px";
							//   // appendSpan.textContent = item;
							//   appendSpan.textContent = img.title;
							//   img.append(appendSpan);
							// } else {
							//   appendTitle.remove();
							// }
						};
						img.src =
							"https://www.mousehuntgame.com/images/items/stats/ee8f12ab8e042415063ef4140cefab7b.gif?cv=243";
						if (data[type][item]) img.src = data[type][item][1];
						imgSpan.appendChild(img);
					};
					imgSpan.onclick = function () { //ast location mod
						batchLoad(el.bait, el.base, el.weapon, el.trinket, el.skin);
						console.log("armed '"+name+"': ", el.bait, el.base, el.weapon, el.trinket, el.skin, el.location);
					};

					const nameSpan = document.createElement("span");
					nameSpan.className = "tsitu-fave-setup-namespan";
					nameSpan.textContent = name;

					const editButton = document.createElement("button");
					editButton.id = "editButton";
					editButton.className = "button";
					editButton.textContent = "✏️";
					editButton.onclick = function () {
						document.querySelector("#collapsible").checked = true;
						document.querySelector("#favorite-setup-input-cheese").value =
							el.bait === "N/A" ? "" : el.bait;
						document.querySelector("#favorite-setup-input-base").value =
							el.base === "N/A" ? "" : el.base;
						document.querySelector("#favorite-setup-input-weapon").value =
							el.weapon === "N/A" ? "" : el.weapon;
						document.querySelector("#favorite-setup-input-charm").value =
							el.trinket === "N/A" ? "" : el.trinket;
						document.querySelector("#favorite-setup-input-location").value =
							el.location === "N/A" ? "" : el.location;
						// document.querySelector("#favorite-setup-input-skin").value =
						// el.skin === "N/A" ? "" : el.skin;
						document.querySelector("#favorite-setup-name").value = name || "";
						editSort = el.sort; // for sorting name-edited setups after the originating setup this button was clicked on
						console.log("editing setup: "+name+" from sort position "+editSort);
						document.getElementById("favorite-setup-name").focus(); // ast location mod
					};

					const deleteButton = document.createElement("button");
					deleteButton.id = "deleteButton";
					deleteButton.className = "button";
					deleteButton.textContent = "🗑️";
					deleteButton.onclick = function () {
						if (confirm(`Delete setup '${name}'?`)) {
							const storedRaw = localStorage.getItem("favorite-setups-saved");
							if (storedRaw) {
								const storedData = JSON.parse(storedRaw);
								if (storedData[name]) delete storedData[name];
								localStorage.setItem(
									"favorite-setups-saved",
									JSON.stringify(storedData)
								);
								// to delete from DOM without a re-render
								var i = this.parentNode.rowIndex;
								console.log("deleted '"+name+"' from rowIndex: "+i);
								document.getElementById("setupTbody").deleteRow(i);
							}
						}
					};

					const travelButton = document.createElement("button"); //ast location mod
					travelButton.className = "travelButton";
					travelButton.title = "Left click to travel, Right click to update setup location to current location"
					if (el.location) {
						travelButton.textContent = el.location;
					} else {
						travelButton.textContent = 'N/A'
					};
					travelButton.onclick = function () {
						app.pages.TravelPage.travel (locMap[el.location].type);
					};
					//refresh setup with location to ease migration of old setups
					travelButton.oncontextmenu = function() {
						const bait = el.bait;
						const base = el.base;
						const weapon = el.weapon;
						const charm = el.trinket;
						const location = user.environment_name // ast location mod

						if (name.length >= 1 && name.length <= 30) {
							const obj = {};
							obj[name] = {
								bait: "N/A",
								base: "N/A",
								weapon: "N/A",
								trinket: "N/A",
								skin: "N/A"
								,location: "N/A" // ast location mod
							};

							if (data.bait[bait] !== undefined) obj[name].bait = bait;
							if (data.base[base] !== undefined) obj[name].base = base;
							if (data.weapon[weapon] !== undefined) obj[name].weapon = weapon;
							if (data.trinket[charm] !== undefined) obj[name].trinket = charm;
							// if (data.skin[skin] !== undefined) obj[name].skin = skin;
							obj[name].location = location; // ast location mod
							obj[name].sort = editSort; // ast location mod
							console.log("saved setup '"+name+"': "+JSON.stringify(obj[name])); // ast location mod

							const storedRaw = localStorage.getItem("favorite-setups-saved");
							if (storedRaw) {
								const storedData = JSON.parse(storedRaw);
								if (storedData[name] !== undefined) {
									if (confirm(`Do you want to overwrite saved setup '${name}'?`)) {
										obj[name].sort = storedData[name].sort;
									} else {
										return;
									}
								}
								storedData[name] = obj[name];
								localStorage.setItem(
									"favorite-setups-saved",
									JSON.stringify(storedData)
								);
							} else {
								localStorage.setItem("favorite-setups-saved", JSON.stringify(obj));
							}
							var saveScroll = document.getElementById("scroller").scrollTop; // ast location mod
							toggleRender();
							toggleRender();
							document.getElementById("scroller").scrollTop = saveScroll;
						} else {
							alert(
								"Please enter a name for your setup that is between 1-20 characters"
							);
						};
					};

					const setupRow = document.createElement("tr");
					setupRow.className = "tsitu-fave-setup-row";
					setupRow.appendChild(nameSpan);
					setupRow.appendChild(travelButton);
					setupRow.appendChild(imgSpan);
					setupRow.appendChild(editButton);
					setupRow.appendChild(deleteButton);
					setupTbody.appendChild(setupRow);

					// if (user.environment_name !== el.location) {
					// const allLocations = document.querySelectorAll('tr.tsitu-fave-setup-row .travelButton');
					// let locSetupCount = 0;
					// // for (const loc of allLocations) {
					// // if (loc.textContent === el.location) {
					// // locSetupCount++;
					// // console.log(locSetupCount)
					// // }
					// // console.log(locSetupCount)
					// // }
					// console.log(el.location);
					// console.log(allLocations);
					// if (locSetupCount > 1) {
					// setupRow.style.display = "none";
					// }
					// }
				}

				// Toggle sort lock/unlock
				const toggleSort = document.createElement("button"); // ast location mod
				toggleSort.id = "toggleSort";
				toggleSort.innerText = "Click to unlock drag and drop sort";// "Reset Sort Order";
				toggleSort.onclick = function () {
					var disabled = $(setupTbody).sortable("option", "disabled");
					if (disabled) {
						$(setupTbody).sortable("enable");
						toggleSort.innerText = "Drag to sort";
						GM_addStyle( //disable setup name selection when dragging
							" .tsitu-fave-setup-namespan {                    grid-area: a;                    font-size: inherit;                    text-align: left;                    text-overflow: ellipsis;                    user-select: none;}"
						);
					} else {
						$(setupTbody).sortable("disable");
						toggleSort.innerText = "Click to unlock sort";
						GM_addStyle(
							" .tsitu-fave-setup-namespan {                    grid-area: a;                    font-size: inherit;                    text-align: left;                    text-overflow: ellipsis;                    user-select: text;}"
						);
					}
				};

				// Make the table drag & drop-able via jQuery sortable()
				GM_addStyle(
					".ui-state-highlight-tsitu { height: 68px; background-color: #FAFFAF; }"
				);
				$(setupTbody).sortable({
					placeholder: "ui-state-highlight-tsitu",
					scroll: true,
					scrollSensitivity: 80,
					scrollSpeed: 20,
					cursor: "move",
					disabled: true,
					update: function() {
						const storedRaw = localStorage.getItem("favorite-setups-saved");
						if (storedRaw) {
							const storedData = JSON.parse(storedRaw);
							const nameSpans = document.querySelectorAll(
								".tsitu-fave-setup-namespan"
							);
							if (nameSpans.length === Object.keys(storedData).length) {
								for (let i = 0; i < nameSpans.length; i++) {
									const name = nameSpans[i].textContent;
									if (storedData[name] !== undefined) {
										storedData[name].sort = i;
									}
								}
								localStorage.setItem(
									"favorite-setups-saved",
									JSON.stringify(storedData)
								);
							}
						}
					}
				});
				setupTable.appendChild(setupTbody);
				setupTableDiv.appendChild(setupTable);

				// Append everything to main popup UI
				mainDiv.appendChild(topDiv);
				mainDiv.appendChild(buttonSpan);
				mainDiv.appendChild(collapsibleCheckbox);
				mainDiv.appendChild(dataListDiv);
				mainDiv.appendChild(setupSelectorDiv);
				scroller.appendChild(setupTableDiv);
				mainDiv.appendChild(scroller);
				mainDiv.appendChild(toggleSort);
				document.body.appendChild(mainDiv);

				const resizeObserver = new ResizeObserver(entries => {
					for (let entry of entries) {
						// console.log(`${entry.target.style.height} h + ${entry.target.style.width} w`);
						localStorage.setItem("favorite-setup-saved-height",entry.target.style.height);
						localStorage.setItem("favorite-setup-saved-width",entry.target.style.width);
					}
				});

				resizeObserver.observe(mainDiv);

				mainDiv.style.height = localStorage.getItem("favorite-setup-saved-height");
				mainDiv.style.width = localStorage.getItem("favorite-setup-saved-width");

				dragElement(mainDiv, topDiv);

				// Reposition popup based on previous dragged location
				const posTop = localStorage.getItem("favorite-setup-pos-top");
				const posLeft = localStorage.getItem("favorite-setup-pos-left");
				if (posTop && posLeft) {
					const intTop = parseInt(posTop);
					if (intTop > 0 && intTop < window.innerHeight - 150) {
						mainDiv.style.top = posTop;
					}
					const intLeft = parseInt(posLeft);
					if (intLeft > 0 && intLeft < window.innerWidth - 150) {
						mainDiv.style.left = posLeft;
					}
				}
			} else {
				alert(
					"No owned item data available. Please refresh, click any of the 5 setup-changing boxes, and try again"
				);
			}
			hideExtraLocationRows ()
		}
	}
	function hideExtraLocationRows () {
		const savedSetupRows = document.querySelectorAll('tr.tsitu-fave-setup-row');

		for (const setup of savedSetupRows) {

			const setupLoc = setup.querySelector('.travelButton').textContent;
			let setupLocCount = 0;

			for (const setup of savedSetupRows) {
				if (setupLoc === setup.querySelector('.travelButton').textContent) {
					setupLocCount++;
					// hide extra rows of non-current locations that already have a visible setup
					if (user.environment_name !== setupLoc && setupLocCount > 1) {
						setup.style.display = "none";
					}
				}
			}
			// console.log(`Total # of setups for ${setupLoc}: ${setupLocCount}`);
		}
	}

	// Inject initial button/link into UI
	function injectUI() {
		document.querySelectorAll("#fave-setup-button").forEach(el => el.remove());

		const lsPlacement = localStorage.getItem("favorite-setup-placement");
		if (lsPlacement === "tem") {
			const target = document.querySelector(
				".campPage-trap-armedItemContainer"
			);
			if (target) {
				const div = document.createElement("div");
				div.id = "fave-setup-button";
				const button = document.createElement("button");
				button.innerText = "Favorite Setups";
				button.addEventListener("click", function () {
					toggleRender()
				});
				button.addEventListener("contextmenu", function () {
					if (confirm("Toggle 'Favorite Setups' placement?")) {
						localStorage.setItem("favorite-setup-placement", "top");
						injectUI();
					} else {
						localStorage.setItem("favorite-setup-placement", "tem");
					}
				});
				div.appendChild(document.createElement("br"));
				div.appendChild(button);
				target.appendChild(div);
			}
		} else {
			const target = document.querySelector(".mousehuntHud-gameInfo");
			if (target) {
				const link = document.createElement("a");
				link.id = "fave-setup-button";
				link.innerText = "[Favorite Setups]";
				link.addEventListener("click", function () {
					toggleRender();
					return false; // Prevent default link clicked behavior
				});
				link.addEventListener("contextmenu", function () {
					if (confirm("Toggle '[Favorite Setups]' placement?")) {
						localStorage.setItem("favorite-setup-placement", "tem");
						injectUI();
					} else {
						localStorage.setItem("favorite-setup-placement", "top");
					}
				});
				target.prepend(link);
			}
		}
	}
	// retain previous open/close behaviour
	var openedSettings = localStorage.getItem('showSetups');
	if(openedSettings == "Y") {
		toggleRender();
	}
	injectUI();

	/**
   * Element dragging functionality
   * @param {HTMLElement} el Element that actually moves
   * @param {HTMLElement} target Element to drag in order to move 'el'
   */
	function dragElement(el, target) {
		var pos1 = 0,
			pos2 = 0,
			pos3 = 0,
			pos4 = 0;

		if (document.getElementById(target.id + "header")) {
			document.getElementById(target.id + "header").onmousedown = dragMouseDown;
		} else {
			target.onmousedown = dragMouseDown;
		}

		function dragMouseDown(e) {
			e = e || window.event;
			pos3 = e.clientX;
			pos4 = e.clientY;
			document.onmouseup = closeDragElement;
			document.onmousemove = elementDrag;
		}

		function elementDrag(e) {
			e = e || window.event;
			pos1 = pos3 - e.clientX;
			pos2 = pos4 - e.clientY;
			pos3 = e.clientX;
			pos4 = e.clientY;
			el.style.top = el.offsetTop - pos2 + "px";
			el.style.left = el.offsetLeft - pos1 + "px";
		}

		function closeDragElement() {
			document.onmouseup = null;
			document.onmousemove = null;
			localStorage.setItem("favorite-setup-pos-top", el.style.top);
			localStorage.setItem("favorite-setup-pos-left", el.style.left);
		}
	}
})();