Invite players and send SB+ directly from the map interface
当前为
// ==UserScript==
// @name MouseHunt - Mapping Helper
// @author Tran Situ (tsitu)
// @namespace https://greasyfork.org/en/users/232363-tsitu
// @version 1.0
// @description Invite players and send SB+ directly from the map interface
// @match http://www.mousehuntgame.com/*
// @match https://www.mousehuntgame.com/*
// ==/UserScript==
(function() {
// RH 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/relichunter.php"
) {
try {
const map = JSON.parse(this.responseText).treasure_map;
if (map) {
const obj = {};
const condensed = {};
const condensedGroups = [];
map.groups.forEach(el => {
// TODO: Compress goals array for individual mice/items in the future
const innerObj = {};
innerObj.name = el.name;
innerObj.profile_pic = el.profile_pic;
innerObj.snuid = el.snuid;
condensedGroups.push(innerObj);
});
condensed.groups = condensedGroups;
condensed.hunters = map.hunters;
condensed.invited_hunters = map.invited_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;
// console.log(obj);
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());
/**
* 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 refreshButton = document.createElement("button");
refreshButton.innerText = "Refresh";
refreshButton.className = "treasureMapPopup-action-button tsitu-mapping";
refreshButton.style.cursor = "pointer";
refreshButton.style.fontSize = "9px";
refreshButton.style.padding = "2px";
refreshButton.style.margin = "3px 5px 0px 0px";
refreshButton.style.textShadow = "none";
refreshButton.style.display = "inline-block";
refreshButton.addEventListener("click", function() {
const mapName = document.querySelector(
".treasureMapPopup-header-title.mapName"
).textContent;
user.quests.QuestRelicHunter.maps.forEach(el => {
if (el.name === mapName) {
// Reset hash to bust cache
el.hash = "";
}
});
// Close map dialog and re-open either with current map, default, or overview
const mapIdEl = document.querySelector("[data-map-id].active");
const mapId = mapIdEl ? mapIdEl.getAttribute("data-map-id") : -1;
document.getElementById("jsDialogClose").click();
mapId === -1
? hg.views.TreasureMapView.show()
: hg.views.TreasureMapView.show(mapId);
});
refreshSpan.appendChild(refreshButton);
document
.querySelector(
".treasureMapPopup-state.viewMap .treasureMapPopup-header-subtitle"
)
.insertAdjacentElement("afterend", refreshSpan);
// Utility handler that opens supply transfer page and 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;
}
// Corkboard image click handling
document.querySelectorAll("[data-message-id]").forEach(msg => {
const snuid = msg
.querySelector(".messageBoardView-message-name")
.href.split("snuid=")[1];
const img = msg.querySelector(".messageBoardView-message-image");
img.href = "#";
img.onclick = function() {
transferSB(snuid);
};
});
// Hunter container image click handling
document
.querySelectorAll(".treasureMapPopup-hunter:not(.empty)")
.forEach(el => {
const img = el.querySelector(".treasureMapPopup-hunter-image");
const snuid = el.getAttribute("data-snuid");
img.style.cursor = "pointer";
img.onclick = function() {
transferSB(snuid);
};
});
// Features that require cache checking
const cacheRaw = localStorage.getItem("tsitu-mapping-cache");
if (cacheRaw) {
const cache = JSON.parse(cacheRaw);
const mapName = document.querySelector(
".treasureMapPopup-header-title.mapName"
).textContent;
if (cache[mapName] !== undefined) {
// Must specify <a> because favorite button <div> also matches the selector
const mapIdEl = document.querySelector("a[data-map-id].active");
if (mapIdEl) {
// Abstract equality comparison because map ID can be number or string
const mapId = mapIdEl.getAttribute("data-map-id");
if (mapId == cache[mapName].map_id) {
// "Last refreshed" timestamp
const refreshSpan = document.querySelector(".tsitu-refresh-span");
if (refreshSpan && 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";
inputLabel.setAttribute("class", "tsitu-mapping");
const inputField = document.createElement("input");
inputField.setAttribute("type", "number");
inputField.setAttribute("class", "tsitu-mapping");
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 inviteButton = document.createElement("button");
inviteButton.innerText = "Invite";
inviteButton.setAttribute("class", "tsitu-mapping");
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);
// console.log(response);
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/relichunter.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);
}
});
}
}
}
});
// Invited hunters aka pending invites
const invitedArr = cache[mapName].invited_hunters;
const invitedSpan = document.createElement("span");
invitedSpan.className = "tsitu-mapping";
invitedSpan.style.marginLeft = "5px";
invitedSpan.innerText =
invitedArr.length > 0
? "Pending Invites:"
: "Pending Invites: None";
if (invitedArr.length > 0) {
let count = 1;
invitedArr.forEach(snuid => {
const link = document.createElement("a");
link.innerText = `[${count}]`;
link.href = `https://www.mousehuntgame.com/profile.php?snuid=${snuid}`;
link.target = "_blank";
invitedSpan.appendChild(document.createTextNode("\u00A0"));
invitedSpan.appendChild(link);
// Prevent text from running past width of dialog (fails when >999)
if (count < 100) {
if (count === 20) {
invitedSpan.appendChild(document.createElement("br"));
} else if ((count - 20) % 30 === 0) {
invitedSpan.appendChild(document.createElement("br"));
}
} else {
if (count === 108) {
invitedSpan.appendChild(document.createElement("br"));
} else if ((count - 108) % 24 === 0) {
invitedSpan.appendChild(document.createElement("br"));
}
}
count += 1;
});
// -- Text debugging --
// for (let i = 0; i < 1337; i++) {
// const link = document.createElement("a");
// link.innerText = `[${i + 1}]`;
// link.href = `#`;
// invitedSpan.appendChild(document.createTextNode("\u00A0"));
// invitedSpan.appendChild(link);
// // Prevent text from running past width of dialog (formatting breaks past 1k but meh)
// if (i + 1 < 100) {
// if (i + 1 === 20) {
// invitedSpan.appendChild(document.createElement("br"));
// } else if ((i + 1 - 20) % 30 === 0) {
// invitedSpan.appendChild(document.createElement("br"));
// }
// } else {
// if (i + 1 === 108) {
// invitedSpan.appendChild(document.createElement("br"));
// } else if ((i + 1 - 108) % 24 === 0) {
// invitedSpan.appendChild(document.createElement("br"));
// }
// }
// }
}
const span = document.createElement("span");
span.style.display = "inline-block";
span.style.marginBottom = "10px";
span.appendChild(inputLabel);
span.appendChild(inputField);
span.appendChild(inviteButton);
span.appendChild(invitedSpan);
document
.querySelector(".treasureMapPopup-hunterContainer")
.insertAdjacentElement("afterend", span);
}
}
}
// "x caught these mice" image click handling
const groups = cache[mapName].groups;
const format = {};
groups.forEach(el => {
if (el.profile_pic !== null) {
format[el.profile_pic] = [el.name, el.snuid];
}
});
document
.querySelectorAll(".treasureMapPopup-goals-group-header")
.forEach(group => {
const text = group.textContent.split(":(")[0] + ":";
if (text !== "Uncaught mice in other locations:") {
const img = group.querySelector(
".treasureMapPopup-goals-group-header-image"
);
if (img) {
const pic = img.style.backgroundImage
.split('url("')[1]
.split('")')[0];
if (format[pic] !== undefined) {
if (format[pic][0] === text) {
img.style.cursor = "pointer";
img.onclick = function() {
const snuid = format[pic][1];
transferSB(snuid);
};
}
}
}
}
});
}
}
}
// 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("[data-tab=map_mice]");
const groupLen = document.querySelectorAll(
".treasureMapPopup-goals-groups"
).length;
// Prevent conflict with 'Bulk Map Invites'
const inviteHeader = document.querySelector(
".treasureMapPopup-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
});
}
})();