// ==UserScript==
// @name MouseHunt Enhanced Search (Beta)
// @description Improve the search logic of search bars in the game
// @author LethalVision
// @version 0.6.0
// @match https://www.mousehuntgame.com/*
// @match https://apps.facebook.com/mousehunt/*
// @icon https://www.google.com/s2/favicons?domain=mousehuntgame.com
// @grant none
// @license MIT
// @namespace https://gf.qytechs.cn/en/users/683695-lethalvision
// ==/UserScript==
// reference dictionaries in the format of <item name>:<search tags> - item names *must* be exact
// search tags are case insensitive, set tags to empty to indicate that the item should be skipped
// only exceptions/special cases need to be defined here, the script automatically generates the acronyms otherwise
const CHEESE = {
"Bonefort Cheese":"BF ","Checkmate Cheese":"CMC","Cloud Cheesecake":"CCC","Dragonvine Cheese":"DVC","Empowered SUPER|brie+":"ESB","Galleon Gouda":"GGC",
"Limelight Cheese":"LLC","Nian Gao'da Cheese":"NGD","Polter-Geitost":"PG ","SUPER|brie+":"SB ","Wildfire Queso":"WF "
}
const CHARMS = {
"Baitkeep Charm":"BKC","Festive Anchor Charm":"FAC_EAC","Timesplit Charm":"TSC","Dragonbane Charm":"DBC","Super Dragonbane Charm":"SDBC","Extreme Dragonbane Charm":"EDBC",
"Ultimate Dragonbane Charm":"UDBC","Party Charm":"PaC","Super Party Charm":"SPaC","Extreme Party Charm":"EPaC","Ultimate Party Charm":"UPaC",
"Rift Vacuum Charm":"RVC_Calcified Rift Mist_CRM","Rift Super Vacuum Charm":"RSVC_Calcified Rift Mist_CRM"
}
const CRAFTING = {
"Chrome Celestial Dissonance Upgrade Kit":"CCDT","Chrome MonstroBot Upgrade Kit":"CMBT","Chrome Oasis Upgrade Kit":"COWNT_CPOT","Chrome School of Sharks Upgrade Kit":"CSOS",
"Chrome Sphynx Wrath Upgrade Kit":"CSW","Chrome Storm Wrought Ballista Upgrade Kit":"CSWBT","Chrome Temporal Turbine Upgrade Kit":"CTT",
"Chrome Thought Obliterator Upgrade Kit":"CTOT_CTHOT_CF2","Ful'Mina's Tooth":"Fulmina","Sandblasted Metal":"SBM","Stale SUPER brie+":"stale SB"
}
const SPECIAL = {
"Ful'Mina's Gift":"fulmina","Ful'mina's Charged Toothlet":"fulmina","SUPER|brie+ Supply Pack":"SB ","Timesplit Rune":"TSR","Sky Sprocket":"HAL_high altitude loot",
"Skysoft Silk":"HAL_high altitude loot","Enchanted Wing":"HAL_high altitude loot","Cloudstone Bangle":"HAL_high altitude loot","Sky Glass":"glore","Sky Ore":"glore"
}
const WEAPON = {
"Biomolecular Re-atomizer Trap":"BRAT_BRT","Birthday Party Piñata Bonanza":"BPPB_Pinata","Blackstone Pass Trap":"BPT_BSP","Brain Extractor":"BE _Brain Bits",
"Christmas Crystalabra Trap":"CCT_Calcified Rift Mist_CRM","Chrome MonstroBot":"CMB","Chrome RhinoBot":"CRB","Chrome Thought Obliterator Trap":"CTOT_CTHOT_CF2",
"Circlet of Pursuing Trap":"A2","Circlet of Seeking Trap":"A1","Enraged RhinoBot":"ERB","Glacier Gatler":"GG_Charm","Ice Blaster":"IB_Charm","Icy RhinoBot":"IRB",
"Interdimensional Crossbow":"IDCT_ICT","Maniacal Brain Extractor":"MBE_Brain Bits","Multi-Crystal Laser":"MCL",
"Mysteriously unYielding Null-Onyx Rampart of Cascading Amperes":"MYNORCA","RhinoBot":"RB ","Rift Glacier Gatler":"RGG_Charm","S.A.M. F.E.D. DN-5":"SAMFED DN5_SAM FED DN5",
"S.L.A.C.":"SLAC","S.L.A.C. II":"SLAC2_SLAC 2","S.S. Scoundrel Sleigher Trap":"SSSST_S4T","S.T.I.N.G. Trap":"STING_L1","S.T.I.N.G.E.R. Trap":"STINGER_L2",
"Sandstorm MonstroBot":"SMB","Sleeping Stone Trap":"SST_T1","Slumbering Boulder Trap":"SBT_T2","Snow Barrage":"SNOB","Steam Laser Mk. I":"SLM1_SLMK1_SLMK 1_MARK 1",
"Steam Laser Mk. II":"SLM2_SLMK2_SLMK 2_MARK 2","Steam Laser Mk. II (Broken!)":"SLM2_SLMK2_SLMK 2_MARK 2","Steam Laser Mk. III":"SLM3_SLMK3_SLMK 3_MARK 3",
"Surprise Party Trap":"SPT_Party Charm","Tarannosaurus Rex Trap":"TREX","The Forgotten Art of Dance":"TFAOD_FART_FAD","The Haunted Manor Trap":"THMT_Charm",
"The Holiday Express Trap":"THET_Charm","Thought Manipulator Trap":"TMT_F1","Thought Obliterator Trap":"TOT_THOT_F2","Timesplit Dissonance Trap":"TDT_TDW_TSD_TSW"
}
const BASE = {
"10 Layer Birthday Cake Base":"Ten Layer_Charm","Clockwork Base":"CWB_CB ","Condemned Base":"CB _Charm","Cupcake Birthday Base":"CBB_Charm",
"Extra Sweet Cupcake Birthday Base":"ESCBB_Charm","Rift Mist Diffuser Base":"RMDB_Charm","Skello-ton Base":"SB _Brain Bits",
"Sprinkly Sweet Cupcake Birthday Base":"SSCBB_Charm","All Season Express Track Base":"ASETB_Charm"
}
// trap selector ignore lists
const ignoreList = ['no_tag_selected','recommended','default','search_match'];
var tagList;
// piggyback on Mousehunt's jQuery
// this is a smart thing to do that will never backfire on me
var $ = $ || window.$;
// one-time init functions go here
function init() {
initMp();
initTrap();
initInv();
}
// == Marketplace Search ==
function initMp() {
// extend templateutil.render to inject desired search terms
const _parentRender = hg.utils.TemplateUtil.render;
hg.utils.TemplateUtil.render = function(templateType, templateData) {
if (templateType == 'ViewMarketplace_search') {
// only edit marketplace search
templateData.search_terms.forEach(category => updateMpSearch(category));
}
return _parentRender(templateType, templateData);
}
}
function updateMpSearch(category) {
var refDict = getDict(category.name);
if (!refDict) { // invalid name
return;
}
category.terms.forEach(function(listing) {
var tag = getInitials(listing.value, refDict);
if (tag && listing.value.indexOf('hidden') == -1) {
// skip if the listing already has "hidden" tag added
listing.value += `<span class="hidden">${tag}</span>`;
}
});
}
// == Trap Setup Search ==
function initTrap() {
// hook into ajax calls to inject search tags into trap data
var callback = function(options, originalOptions, jqXHR) {
const componentUrl = 'managers/ajax/users/gettrapcomponents.php';
if (options.url.indexOf(componentUrl) != -1) {
var _parentSuccess = originalOptions.success || options.success;
var _extendedSuccess = function (data) {
if (data.components && data.components.length > 0) {
//console.log(data.components);
data.components.forEach(component => updateTrapSearch(component));
}
_parentSuccess(data);
};
originalOptions.success = options.success = _extendedSuccess;
}
}
$.ajaxPrefilter(callback);
// hook into renderFromFile to hide the injected search tags
const _parentRenderFile = hg.utils.TemplateUtil.renderFromFile;
hg.utils.TemplateUtil.renderFromFile = function(TemplateGroupSource,templateType,templateData) {
if (TemplateGroupSource == 'CampPage' && templateType == 'tag_groups') {
modifyTemplateData(templateData);
// ugly timeout delay here because cleanup should only run after renderFromFile returns
setTimeout(cleanTrapSelector, 5);
}
return _parentRenderFile(TemplateGroupSource,templateType,templateData);
}
}
// inject search tags into trap data
function updateTrapSearch(component) {
var refDict = getDict(component.classification);
if (!refDict) { // invalid name
return;
}
var tag = getInitials(component.name, refDict);
if (!tag) {
return; // item not in refDict and getInitials fails to generate an acronym for it
}
// Tag the trap component with its acronym
if (!component.tag_types) {
component.tag_types = [];
}
// count number of non-hidden tags
var count = component.tag_types.length;
const hiddenTagList = ['draconic', 'tactical', 'shadow', 'physical', 'rift', 'hydro', 'arcane', 'forgotten', 'law',
'charm_weak', 'charm_strong', 'charm_epic', 'charm_rare', 'trap_parts', 'bait_standard', 'event'];
for (var i=0; i<hiddenTagList.length; i++) {
if (component.tag_types.indexOf(hiddenTagList[i]) != -1) {
count--;
}
if (count == 0) break;
}
if (count == 0) {
// add default tag if all tags are hidden
component.tag_types.push('default');
}
component.tag_types.push(tag);
}
// remove the search tags before they are rendered
function modifyTemplateData(templateData) {
if (!templateData.tag_groups || templateData.tag_groups.length == 0) {
return; // skip if it's empty
}
var tagList = app.pages.CampPage.getTags();
templateData.tag_groups = templateData.tag_groups.filter(function(tagGroup) {
var tagName = tagGroup.type;
// if tagName is invalid, reject this tag group
// else if tagName is in ignoreList or tagList, render this tag group
return (tagName && (ignoreList.includes(tagName) || tagList[tagName]));
});
}
// unfortunately the tag selector filter doesn't use renderFromFile so we have to
// remove the search tags from the HTML directly
function cleanTrapSelector() {
var tagList = app.pages.CampPage.getTags();
var count = 0;
// clean up the tag dropdown
var selectQuery = document.querySelectorAll('select[data-filter=tag]');
if (selectQuery.length > 0) {
var selectElem = selectQuery[0];
// remove the custom tags we added for the enhanced search
for (var i=0; i<selectElem.length; i++) {
var optValue = selectElem.options[i].value;
if (!ignoreList.includes(optValue) && !tagList[optValue]) {
selectElem.remove(i);
i--; // options now has one less element
count++;
}
}
//console.log(`Removed ${count} select options`);
}
}
// == Inventory Search ==
// TODO: break this up for the different tabs? (cheese, crafting, special, etc.)
function initInv() {
const inventoryUrl = 'managers/ajax/pages/page.php';
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
this.addEventListener("load", function() {
if ( this.responseURL.indexOf(inventoryUrl) != -1) {
updateInventory();
}
});
originalOpen.apply(this, arguments);
};
// in case the window is refreshed in inventory.php
if (window.location.href.includes("inventory.php")) {
updateInventory();
}
}
// inject search tags into the HTML element
function updateInventory() {
var query = document.querySelectorAll('.inventoryPage-item:not(.tagged)');
if (query.length > 0) {
query.forEach(function(item){
item.classList.add("tagged");
var refDict = getDict(item.getAttribute('data-item-classification'))
if (!refDict) { // invalid classification
return;
}
var dataName = item.getAttribute('data-name');
var tag = getInitials(dataName, refDict);
if (!tag) {
return;
} else {
item.setAttribute('data-name', `${dataName}_${tag}`);
}
});
}
}
// == helpers ==
// returns the appropriate reference dict given a classification string
// returns undefined if there is no match
function getDict(classification) {
if (typeof classification === 'string') {
switch(classification.toLowerCase()) {
case 'cheese':
case 'bait':
return CHEESE;
case 'charms':
case 'trinket':
return CHARMS;
case 'crafting':
return CRAFTING;
case 'special':
case 'stat':
return SPECIAL;
case 'weapon':
return WEAPON;
case 'base':
return BASE;
}
}
// no match
return undefined;
}
// get acronym from a given item name and refDict
// this generates an acronym if item name is valid but not in the refDict, but returns undefined otherwise
function getInitials(itemName, refDict) {
// check refDict first
if (refDict[itemName] != undefined) {
return refDict[itemName];
}
var wordList = itemName.split(' ');
if (wordList.length < 2) { // 0-1 letter acronyms are useless
return undefined;
} else if (refDict == CHARMS && itemName.indexOf('Rift 20') != -1) {
return 'R'+ wordList[1]; // special provision for rift 20xx charms
} else if (refDict == CHARMS && itemName.indexOf('Warpath') != -1) {
return undefined; // there's like 10+ warpath charms to exclude
} else if (refDict == SPECIAL && itemName.indexOf('Airship') != -1) {
return undefined; // exclude all airship parts
}
var acronym = '';
for (var i=0; i < wordList.length; i++) {
if (wordList[i].length == 0) {
// empty word, skip
continue;
}
var letter = wordList[i].charAt(0); // first letter of word
if (isNaN(letter)) { // not a number
acronym += letter;
} else { // is a number
// return nothing, this skips weird acronyms like 20xx charms and KG cheese
return undefined;
}
}
if (acronym.length == 2) {
// pad out length 2 tags so that they're searchable via "XX "
acronym += ' ';
}
return acronym.toUpperCase();
}
// This just checks if there are filters active in the trap selector
// ported wholesale from source code because "hasFilter" is public but this isn't?? Hitgrab why
function hasActiveFilters() {
var campPage = app.pages.CampPage;
return campPage.hasFilter('componentName') || campPage.hasFilter('componentPowerType') || campPage.hasFilter('componentTagType');
}
// start script
init();