// ==UserScript==
// @name MouseHunt - Mapping Helper
// @author Tran Situ (tsitu)
// @namespace https://gf.qytechs.cn/en/users/232363-tsitu
// @version 2.0
// @description Invite players and send SB+ directly from the map interface
// @match http://www.mousehuntgame.com/*
// @match https://www.mousehuntgame.com/*
// ==/UserScript==
(function() {
// Endpoint listener - caches maps (which come in one at a time)
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
this.addEventListener("load", function() {
if (
this.responseURL ===
"https://www.mousehuntgame.com/managers/ajax/users/treasuremap.php"
) {
try {
const map = JSON.parse(this.responseText).treasure_map;
if (map) {
const obj = {};
const condensed = {};
condensed.hunters = map.hunters;
condensed.is_complete = map.is_complete;
condensed.is_owner = map.is_owner;
condensed.is_scavenger_hunt = map.is_scavenger_hunt;
condensed.is_wanted_poster = map.is_wanted_poster;
condensed.map_class = map.map_class;
condensed.map_id = map.map_id;
condensed.timestamp = Date.now();
obj[map.name] = condensed;
const mapCacheRaw = localStorage.getItem("tsitu-mapping-cache");
if (mapCacheRaw) {
const mapCache = JSON.parse(mapCacheRaw);
mapCache[map.name] = condensed;
localStorage.setItem(
"tsitu-mapping-cache",
JSON.stringify(mapCache)
);
} else {
localStorage.setItem("tsitu-mapping-cache", JSON.stringify(obj));
}
render();
}
} catch (error) {
console.log("Server response doesn't contain a valid treasure map");
console.error(error.stack);
}
}
});
originalOpen.apply(this, arguments);
};
// Renders custom UI elements onto the DOM
function render() {
// Clear out existing custom elements
// Uses static collection instead of live one from getElementsByClassName
document.querySelectorAll(".tsitu-mapping").forEach(el => el.remove());
// document.querySelectorAll(".tsitu-queso-mapper").forEach(el => el.remove());
// Parent element that gets inserted at the end
const masterEl = document.createElement("fieldset");
masterEl.className = "tsitu-mapping";
masterEl.style.width = "50%";
masterEl.style.marginLeft = "15px";
masterEl.style.padding = "5px";
masterEl.style.border = "1px";
masterEl.style.borderStyle = "dotted";
const masterElLegend = document.createElement("legend");
masterElLegend.innerText = "Mapping Helper v2.0 by tsitu";
masterEl.appendChild(masterElLegend);
/**
* Refresh button
* Iterate thru QRH.maps array for element matching current map and set its hash to empty string
* This forces a hard refresh via hasCachedMap, which is called in show/showMap
*/
const refreshSpan = document.createElement("span");
refreshSpan.className = "tsitu-mapping tsitu-refresh-span";
const refreshTextSpan = document.createElement("span");
refreshTextSpan.innerText = "Refresh";
const refreshButton = document.createElement("button");
refreshButton.className = "mousehuntActionButton tiny tsitu-mapping";
refreshButton.style.cursor = "pointer";
refreshButton.style.fontSize = "9px";
refreshButton.style.padding = "2px";
refreshButton.style.margin = "0px 5px 5px 10px";
refreshButton.style.textShadow = "none";
refreshButton.style.display = "inline-block";
refreshButton.appendChild(refreshTextSpan);
refreshButton.addEventListener("click", function() {
// Clear cache (is it possible to only do so for just a single map?)
hg.views.TreasureMapManagerView.clearMapCache();
// Parse map ID from 'Preview' button (should be robust enough)
let mapId = -1;
const previewButton = document.querySelector(
".treasureMapView-mapMenu-group-actions .mousehuntActionButton.tiny.lightBlue"
);
if (previewButton) {
mapId = previewButton.onclick
.toString()
.split("RewardsDialog(")[1]
.split(");")[0];
}
// Close dialog and re-open with either current map or overview
document.getElementById("jsDialogClose").click();
mapId === -1
? hg.views.TreasureMapManagerView.show()
: hg.views.TreasureMapManagerView.show(mapId);
});
refreshSpan.appendChild(refreshButton);
masterEl.appendChild(refreshSpan);
// Utility function that opens supply transfer page and auto-selects SB+
function transferSB(snuid) {
const newWindow = window.open(
`https://www.mousehuntgame.com/supplytransfer.php?fid=${snuid}`
);
newWindow.addEventListener("load", function() {
if (newWindow.supplyTransfer1) {
newWindow.supplyTransfer1.setSelectedItemType("super_brie_cheese");
newWindow.supplyTransfer1.renderTabMenu();
newWindow.supplyTransfer1.render();
}
});
return false;
}
// Features that require cache checking
const cacheRaw = localStorage.getItem("tsitu-mapping-cache");
if (cacheRaw) {
const cache = JSON.parse(cacheRaw);
const checkMap = document.querySelector(
".treasureMapManagerView-task.active"
);
const checkPreview = document.querySelector(
".treasureMapView-previewBar-content"
);
let mapName;
if (checkMap) {
mapName = checkMap.querySelector(".treasureMapManagerView-task-name")
.textContent;
}
if (checkPreview) {
mapName = checkPreview.textContent
.split("'s ")[1]
.split(".Back to Invites")[0];
}
if (cache[mapName] !== undefined) {
// Must specify <a> because favorite button <div> also matches the selector
const mapIdEl = document.querySelector("a[data-task-id].active");
if (mapIdEl) {
// Abstract equality comparison because map ID can be number or string
const mapId = mapIdEl.getAttribute("data-task-id");
if (mapId == cache[mapName].map_id) {
// "Last refreshed" timestamp
if (cache[mapName].timestamp) {
const timeSpan = document.createElement("span");
timeSpan.innerText = `(This map was last refreshed on: ${new Date(
parseInt(cache[mapName].timestamp)
).toLocaleString()})`;
refreshSpan.appendChild(timeSpan);
}
// Invite via Hunter ID (only for map captains)
if (cache[mapName].is_owner) {
const inputLabel = document.createElement("label");
inputLabel.innerText = "Hunter ID: ";
inputLabel.htmlFor = "tsitu-mapping-id-input";
const inputField = document.createElement("input");
inputField.setAttribute("type", "number");
inputField.setAttribute("name", "tsitu-mapping-id-input");
inputField.setAttribute("data-lpignore", "true"); // Get rid of LastPass Autofill
inputField.setAttribute("min", 1);
inputField.setAttribute("max", 9999999);
inputField.setAttribute("placeholder", "e.g. 1234567");
inputField.setAttribute("required", true);
inputField.addEventListener("keyup", function(e) {
if (e.keyCode === 13) {
inviteButton.click(); // 'Enter' pressed
}
});
const overrideStyle =
"input[name='tsitu-mapping-id-input'] { -webkit-appearance:textfield; -moz-appearance:textfield; appearance:textfield; } input[name='tsitu-mapping-id-input']::-webkit-outer-spin-button, input[name='tsitu-mapping-id-input']::-webkit-inner-spin-button { display:none; -webkit-appearance:none; margin:0; }";
let stylePresent = false;
document.querySelectorAll("style").forEach(style => {
if (style.textContent === overrideStyle) {
stylePresent = true;
}
});
if (!stylePresent) {
const spinOverride = document.createElement("style");
spinOverride.innerHTML = overrideStyle;
document.body.appendChild(spinOverride);
}
const inviteButton = document.createElement("button");
inviteButton.style.marginLeft = "5px";
inviteButton.innerText = "Invite";
inviteButton.addEventListener("click", function() {
const rawText = inputField.value;
if (rawText.length > 0) {
const hunterId = parseInt(rawText);
if (typeof hunterId === "number" && !isNaN(hunterId)) {
if (hunterId > 0 && hunterId < 9999999) {
postReq(
"https://www.mousehuntgame.com/managers/ajax/pages/friends.php",
`sn=Hitgrab&hg_is_ajax=1&action=community_search_by_id&user_id=${hunterId}&uh=${user.unique_hash}`
).then(res => {
let response = null;
try {
if (res) {
response = JSON.parse(res.responseText);
const data = response.friend;
if (data.has_invitable_map) {
if (
confirm(
`Are you sure you'd like to invite this hunter?\n\nName: ${data.name}\nTitle: ${data.title_name} (${data.title_percent}%)\nLocation: ${data.environment_name}\nLast Active: ${data.last_active_formatted} ago`
)
) {
postReq(
"https://www.mousehuntgame.com/managers/ajax/users/treasuremap.php",
`sn=Hitgrab&hg_is_ajax=1&action=send_invites&map_id=${mapId}&snuids%5B%5D=${data.snuid}&uh=${user.unique_hash}`
).then(res2 => {
let inviteRes = null;
try {
if (res2) {
inviteRes = JSON.parse(res2.responseText);
if (inviteRes.success === 1) {
refreshButton.click();
} else {
alert(
"Map invite unsuccessful - may be because map is full"
);
}
}
} catch (error2) {
alert("Error while inviting hunter to map");
console.error(error2.stack);
}
});
}
} else {
if (data.name) {
alert(
`${data.name} cannot to be invited to a map at this time`
);
} else {
alert("Invalid hunter information");
}
}
}
} catch (error) {
alert("Error while requesting hunter information");
console.error(error.stack);
}
});
}
}
}
});
const span = document.createElement("span");
span.className = "tsitu-mapping";
span.style.display = "inline-block";
span.style.marginBottom = "10px";
span.style.marginLeft = "10px";
span.appendChild(inputLabel);
span.appendChild(inputField);
span.appendChild(inviteButton);
masterEl.insertAdjacentElement(
"afterbegin",
document.createElement("br")
);
masterEl.insertAdjacentElement("afterbegin", span);
}
}
}
const imgIDMap = {};
const idNameMap = {};
cache[mapName].hunters.forEach(el => {
if (el.profile_pic) imgIDMap[el.profile_pic] = el.sn_user_id;
idNameMap[el.sn_user_id] = el.name;
});
// Utility function for image hover behavior
function imgHover(img) {
let imgURL;
if (img.src) {
imgURL = img.src;
} else if (img.style.backgroundImage) {
imgURL = img.style.backgroundImage.split('url("')[1].split('")')[0];
}
const snuid = imgIDMap[imgURL];
if (snuid) {
const name = idNameMap[snuid];
if (name) {
img.title = `Send SB+ to ${name}`;
}
img.href = "#";
img.style.cursor = "pointer";
img.onclick = function() {
transferSB(snuid);
};
img.onmouseenter = function() {
img.style.border = "dashed 1px green";
};
img.onmouseleave = function() {
img.style.border = "";
};
}
}
// Hunter container images
document
.querySelectorAll(
".treasureMapView-hunter:not(.empty) .treasureMapView-hunter-image"
)
.forEach(img => {
imgHover(img);
});
// Corkboard message images
document
.querySelectorAll("[data-message-id] .messageBoardView-message-image")
.forEach(img => {
imgHover(img);
});
// "x found these mice" images
document
.querySelectorAll(".treasureMapView-block-content-heading-image")
.forEach(img => {
imgHover(img);
});
}
}
// Final render
document
.querySelector(".treasureMapView-mapMenu")
.insertAdjacentElement("afterend", masterEl);
}
// POST to specified endpoint URL with desired form data
function postReq(url, form) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
resolve(this);
}
};
xhr.onerror = function() {
reject(this);
};
xhr.send(form);
});
}
// MutationObserver logic for map UI
// Observers are attached to a *specific* element (will DC if removed from DOM)
const observerTarget = document.getElementById("overlayPopup");
if (observerTarget) {
MutationObserver =
window.MutationObserver ||
window.WebKitMutationObserver ||
window.MozMutationObserver;
const observer = new MutationObserver(function() {
// Callback
// Render if treasure map popup is available
const mapTab = observerTarget.querySelector(
".treasureMapManagerView-header-navigation-item.tasks.active"
);
const groupLen = document.querySelectorAll(
".treasureMapView-goals-groups"
).length;
// Prevent conflict with 'Bulk Map Invites'
const inviteHeader = document.querySelector(
".treasureMapManagerDialogView-inviteFriend-header"
);
if (
mapTab &&
mapTab.className.indexOf("active") >= 0 &&
groupLen > 0 &&
!inviteHeader
) {
// Disconnect and reconnect later to prevent infinite mutation loop
observer.disconnect();
render();
observer.observe(observerTarget, {
childList: true,
subtree: true
});
}
});
observer.observe(observerTarget, {
childList: true,
subtree: true
});
}
// Queso Mapper functionality (deprecated as of v2.0 - may reinstate if in demand)
function quesoMapperFuncDepr() {
// ***
// Check for valid Queso Canyon map name
const mapNameSelector = document.querySelector(
".treasureMapPopup-header-title.mapName"
);
if (mapNameSelector) {
const split = mapNameSelector.textContent.split("Rare ");
const mapName = split.length === 2 ? split[1] : split[0];
if (quesoMaps.indexOf(mapName) >= 0) {
// Queso Mapper toggling
const quesoToggle = document.createElement("input");
quesoToggle.type = "checkbox";
quesoToggle.className = "tsitu-mapping";
quesoToggle.name = "tsitu-queso-toggle";
quesoToggle.addEventListener("click", function() {
localStorage.setItem("tsitu-queso-toggle", quesoToggle.checked);
render();
});
const quesoToggleLabel = document.createElement("label");
quesoToggleLabel.className = "tsitu-mapping";
quesoToggleLabel.htmlFor = "tsitu-queso-toggle";
quesoToggleLabel.innerText = "Toggle Queso Mapper functionality";
const qtChecked = localStorage.getItem("tsitu-queso-toggle") || "false";
quesoToggle.checked = qtChecked === "true";
if (quesoToggle.checked) {
quesoRender();
}
const quesoToggleDiv = document.createElement("div");
quesoToggleDiv.className = "tsitu-queso-mapper";
if (!quesoToggle.checked) quesoToggleDiv.style.marginBottom = "10px";
quesoToggleDiv.appendChild(quesoToggle);
quesoToggleDiv.appendChild(quesoToggleLabel);
document
.querySelector(".treasureMapPopup-hunterContainer")
.insertAdjacentElement("afterend", quesoToggleDiv);
}
}
// ***
function quesoRender() {
const mapMice = document.querySelectorAll(
"div.treasureMapPopup-goals-group-goal.treasureMapPopup-searchIndex.mouse"
);
if (mapMice.length > 0) {
// Generate DOM elements
const displayDiv = document.createElement("div");
displayDiv.className = "tsitu-queso-mapper";
displayDiv.style.fontSize = "14px";
displayDiv.style.marginBottom = "10px";
displayDiv.innerText = "Preferred Location & Cheese -> ";
const cacheSel = localStorage.getItem("tsitu-queso-mapper-sel");
if (cacheSel) {
const cache = JSON.parse(cacheSel);
for (let location in quesoData) {
const locSel = `${classBuilder(location, "loc")}`;
if (cache[locSel] !== undefined) {
const locationSpan = document.createElement("span");
locationSpan.innerText = `${location}: `;
let cheeseCount = 0;
for (let cheese in quesoData[location]) {
const cheeseSel = `${classBuilder(location, cheese)}`;
if (cache[locSel].indexOf(cheeseSel) >= 0) {
const cheeseSpan = document.createElement("span");
let prependStr = "";
if (cheeseCount > 0) prependStr = ", ";
const imgSpan = document.createElement("span");
imgSpan.setAttribute(
"style",
`background-image:url('${quesoImg[cheese]}');width:20px;height:20px;display:inline-block;background-size:contain;background-repeat:no-repeat;position:relative;top:4px;`
);
let appendStr = "";
if (cheese !== "Standard" && cheese !== "SB+") {
appendStr += " Queso";
}
cheeseSpan.innerText = `${prependStr + cheese + appendStr}`;
locationSpan.append(cheeseSpan);
locationSpan.append(document.createTextNode("\u00A0"));
locationSpan.append(imgSpan);
cheeseCount += 1;
}
}
displayDiv.appendChild(locationSpan);
}
}
} else {
displayDiv.style.marginTop = "5px";
displayDiv.innerText = "Preferred Location & Cheese -> N/A";
}
const target = document.querySelector(
".treasureMapPopup-map-stateContainer.viewGoals"
);
if (target) target.insertAdjacentElement("beforebegin", displayDiv);
mapMice.forEach(el => {
if (el.className.indexOf("tsitu-queso-mapper-mouse") < 0) {
function listener() {
const span = el.querySelector("span");
if (span) {
const mouse = span.textContent;
const mouseData = quesoMice[mouse];
if (mouseData) {
const toCache = {};
for (let arr of mouseData) {
const locSel = classBuilder(arr[0], "loc");
const cheeseSel = classBuilder(arr[0], arr[1]);
if (toCache[locSel] === undefined) {
toCache[locSel] = [cheeseSel];
} else {
toCache[locSel].push(cheeseSel);
}
localStorage.setItem(
"tsitu-queso-mapper-sel",
JSON.stringify(toCache)
);
render();
}
}
}
}
el.addEventListener("mouseover", function() {
listener();
});
el.addEventListener("click", function() {
listener();
});
}
el.classList.add("tsitu-queso-mapper-mouse");
});
}
function classBuilder(location, cheese) {
let retVal = "";
switch (location) {
case "Queso River":
retVal += "river-";
break;
case "Prickly Plains":
retVal += "plains-";
break;
case "Cantera Quarry":
retVal += "quarry-";
break;
case "Cork Collecting":
retVal += "cork-";
break;
case "Pressure Building":
retVal += "pressure-";
break;
case "Small Eruption":
retVal += "small-";
break;
case "Medium Eruption":
retVal += "medium-";
break;
case "Large Eruption":
retVal += "large-";
break;
case "Epic Eruption":
retVal += "epic-";
break;
default:
retVal += location;
}
switch (cheese) {
case "Standard":
retVal += "standard";
break;
case "SB+":
retVal += "super";
break;
case "Bland":
retVal += "bland";
break;
case "Mild":
retVal += "mild";
break;
case "Medium":
retVal += "medium";
break;
case "Hot":
retVal += "hot";
break;
case "Flamin'":
retVal += "flamin";
break;
case "Wildfire":
retVal += "wildfire";
break;
default:
retVal += cheese;
}
return retVal;
}
}
// Valid Queso map variants
const quesoMaps = [
"Queso Canyoneer Treasure Map",
"Queso Geyser Treasure Map",
"Queso Canyon Grand Tour Treasure Map"
];
// Queso cheese image icons
const quesoImg = {
"Standard":
"https://www.mousehuntgame.com/images/items/bait/7e0daa548364166c46c0804e6cb122c6.gif?cv=243",
"SB+":
"https://www.mousehuntgame.com/images/items/bait/d3bb758c09c44c926736bbdaf22ee219.gif?cv=243",
"Bland":
"https://www.mousehuntgame.com/images/items/bait/4752dbfdce202c0d7ad60ce0bacbebae.gif?cv=243",
"Mild":
"https://www.mousehuntgame.com/images/items/bait/7193159aa90c85ba67cbe02d209e565f.gif?cv=243",
"Medium":
"https://www.mousehuntgame.com/images/items/bait/be747798c5e6a7747ba117e9c32a8a1f.gif?cv=243",
"Hot":
"https://www.mousehuntgame.com/images/items/bait/11d1170bc85f37d67e26b0a05902bc3f.gif?cv=243",
"Flamin'":
"https://www.mousehuntgame.com/images/items/bait/5a69c1ea617ba622bd1dd227afb69a68.gif?cv=243",
"Wildfire":
"https://www.mousehuntgame.com/images/items/bait/73891a065f1548e474177165734ce78d.gif?cv=243"
};
// Location -> Cheese -> Mouse
const quesoData = {
"Queso River": {
"Standard": [
"Tiny Saboteur",
"Pump Raider",
"Croquet Crusher",
"Queso Extractor"
],
"SB+": ["Sleepy Merchant"],
"Wildfire": ["Queen Quesada"]
},
"Prickly Plains": {
"Bland": ["Spice Seer", "Old Spice Collector"],
"Mild": ["Spice Farmer", "Granny Spice"],
"Medium": ["Spice Sovereign", "Spice Finder"],
"Hot": ["Spice Raider", "Spice Reaper"],
"Flamin'": ["Inferna, The Engulfed"]
},
"Cantera Quarry": {
"Bland": ["Chip Chiseler", "Tiny Toppler"],
"Mild": ["Ore Chipper", "Rubble Rummager"],
"Medium": ["Nachore Golem", "Rubble Rouser"],
"Hot": ["Grampa Golem", "Fiery Crusher"],
"Flamin'": ["Nachous, The Molten"]
},
"Cork Collecting": {
"Bland": ["Fuzzy Drake"],
"Mild": ["Cork Defender"],
"Medium": ["Burly Bruiser"],
"Hot": ["Horned Cork Hoarder"],
"Flamin'": ["Rambunctious Rain Rumbler", "Corky, the Collector"],
"Wildfire": ["Corkataur"]
},
"Pressure Building": {
"Mild": ["Steam Sailor"],
"Medium": ["Warming Wyvern"],
"Hot": ["Vaporior"],
"Flamin'": ["Pyrehyde"],
"Wildfire": ["Emberstone Scaled"]
},
"Small Eruption": {
Mild: ["Sizzle Pup"],
Medium: ["Sizzle Pup"],
Hot: ["Sizzle Pup"]
// Mild: ["Mild Spicekin", "Sizzle Pup"],
// Medium: ["Sizzle Pup", "Smoldersnap", "Mild Spicekin"],
// Hot: ["Sizzle Pup", "Ignatia"]
},
"Medium Eruption": {
"Medium": ["Bearded Elder"],
"Hot": ["Bearded Elder"],
"Flamin'": ["Bearded Elder"]
// Mild: ["Mild Spicekin"],
// Medium: ["Bearded Elder", "Smoldersnap"],
// Hot: ["Bearded Elder", "Ignatia"],
// "Flamin'": ["Bearded Elder", "Bruticus, the Blazing"]
},
"Large Eruption": {
"Hot": ["Cinderstorm"],
"Flamin'": ["Cinderstorm"]
// Medium: ["Smoldersnap"],
// Hot: ["Cinderstorm", "Ignatia"],
// "Flamin'": ["Cinderstorm", "Bruticus, the Blazing"]
},
"Epic Eruption": {
"Flamin'": ["Stormsurge, the Vile Tempest"],
"Wildfire": ["Kalor'ignis of the Geyser"]
// Hot: ["Ignatia", "Stormsurge, the Vile Tempest"],
// "Flamin'": ["Stormsurge, the Vile Tempest", "Bruticus, the Blazing"],
},
"Any Eruption": {
"Mild": ["Mild Spicekin"],
"Medium": ["Smoldersnap"],
"Hot": ["Ignatia"],
"Flamin'": ["Bruticus, the Blazing"]
}
};
// Alternate representation: Mouse -> Location -> Cheese
const quesoMice = {};
for (let location in quesoData) {
for (let cheese in quesoData[location]) {
const arr = quesoData[location][cheese];
for (let mouse of arr) {
if (quesoMice[mouse] === undefined) {
quesoMice[mouse] = [[location, cheese]];
} else {
quesoMice[mouse].push([location, cheese]);
}
}
}
}
}
})();