Цей скрипт не слід встановлювати безпосередньо. Це - бібліотека для інших скриптів для включення в мета директиву // @require https://update.greasyfork.org/scripts/460027/1263033/%F0%9F%90%AD%EF%B8%8F%20MouseHunt%20Utils.js
// ==UserScript==
// @name 🐭️ MouseHunt Utils
// @author bradp
// @version 1.11.0
// @description MouseHunt Utils is a library of functions that can be used to make other MouseHunt userscripts easily.
// @license MIT
// @namespace bradp
// @match https://www.mousehuntgame.com/*
// @icon https://i.mouse.rip/mouse.png
// @grant none
// ==/UserScript==
/* eslint-disable no-unused-vars */
/**
* Add styles to the page.
*
* @author bradp
* @since 1.0.0
*
* @example <caption>Basic usage</caption>
* addStyles(`.my-class {
* color: red;
* }`);
*
* @example <caption>With an identifier</caption>
* addStyles(`.my-class {
* display: none;
* }`, 'my-identifier');
*
* @example <caption>With an identifier, but will only add the styles once</caption>
* addStyles(`.my-other-class {
* color: blue;
* }`, 'my-identifier', true);
*
* @param {string} styles The styles to add.
* @param {string} identifier The identifier to use for the style element.
* @param {boolean} once Only add the styles once for the identifier.
*
* @return {Element} The style element.
*/
const addStyles = (styles, identifier = 'mh-utils-custom-styles', once = false) => {
identifier = `mh-utils-${identifier}`;
// Check to see if the existing element exists.
const existingStyles = document.getElementById(identifier);
// If so, append our new styles to the existing element.
if (existingStyles) {
if (once) {
return existingStyles;
}
existingStyles.innerHTML += styles;
return existingStyles;
}
// Otherwise, create a new element and append it to the head.
const style = document.createElement('style');
style.id = identifier;
style.innerHTML = styles;
document.head.appendChild(style);
return style;
};
/**
* Do something when ajax requests are completed.
*
* @author bradp
* @since 1.0.0
*
* @example <caption>Basic usage</caption>
* onRequest((response) => {
* console.log(response);
* }, 'managers/ajax/turns/activeturn.php');
*
* @example <caption>Basic usage, but skip the success check</caption>
* onRequest((response) => {
* console.log(response);
* }, 'managers/ajax/turns/activeturn.php', true);
*
* @example <caption>Basic usage, running for all ajax requests</caption>
* onRequest((response) => {
* console.log(response);
* });
*
* @param {Function} callback The callback to call when an ajax request is completed.
* @param {string} url The url to match. If not provided, all ajax requests will be matched.
* @param {boolean} skipSuccess Skip the success check.
*/
const onRequest = (callback, url = null, skipSuccess = false) => {
const req = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function () {
this.addEventListener('load', function () {
if (this.responseText) {
let response = {};
try {
response = JSON.parse(this.responseText);
} catch (e) {
return;
}
if (response.success || skipSuccess) {
if (! url) {
callback(response);
return;
}
if (this.responseURL.indexOf(url) !== -1) {
callback(response);
}
}
}
});
req.apply(this, arguments);
};
};
const onAjaxRequest = onRequest;
/**
* Run the callbacks depending on visibility.
*
* @author bradp
* @since 1.0.0
*
* @ignore
*
* @param {Object} settings Settings object.
* @param {Node} parentNode The parent node.
* @param {Object} callbacks The callbacks to run.
*
* @return {Object} The settings.
*/
const runCallbacks = (settings, parentNode, callbacks) => {
// Loop through the keys on our settings object.
Object.keys(settings).forEach((key) => {
// If the parentNode that's passed in contains the selector for the key.
if (parentNode && parentNode.classList && parentNode.classList.contains(settings[key].selector)) {
// Set as visible.
settings[key].isVisible = true;
// If there is a show callback, run it.
if (callbacks[key] && callbacks[key].show) {
callbacks[key].show();
}
} else if (settings[key].isVisible) {
// Mark as not visible.
settings[key].isVisible = false;
// If there is a hide callback, run it.
if (callbacks[key] && callbacks[key].hide) {
callbacks[key].hide();
}
}
});
return settings;
};
/**
* Do something when the overlay is shown or hidden.
*
* @param {Object} callbacks
* @param {Function} callbacks.show The callback to call when the overlay is shown.
* @param {Function} callbacks.hide The callback to call when the overlay is hidden.
* @param {Function} callbacks.change The callback to call when the overlay is changed.
*/
const onOverlayChange = (callbacks) => {
// Track the different overlay states.
let overlayData = {
map: {
isVisible: false,
selector: 'treasureMapPopup'
},
item: {
isVisible: false,
selector: 'itemViewPopup'
},
mouse: {
isVisible: false,
selector: 'mouseViewPopup'
},
image: {
isVisible: false,
selector: 'largerImage'
},
convertible: {
isVisible: false,
selector: 'convertibleOpenViewPopup'
},
adventureBook: {
isVisible: false,
selector: 'adventureBookPopup'
},
marketplace: {
isVisible: false,
selector: 'marketplaceViewPopup'
},
gifts: {
isVisible: false,
selector: 'giftSelectorViewPopup'
},
support: {
isVisible: false,
selector: 'supportPageContactUsForm'
},
premiumShop: {
isVisible: false,
selector: 'MHCheckout'
}
};
// Observe the overlayPopup element for changes.
const observer = new MutationObserver(() => {
if (callbacks.change) {
callbacks.change();
}
// Grab the overlayPopup element and make sure it has classes on it.
const overlayType = document.getElementById('overlayPopup');
if (overlayType && overlayType.classList.length <= 0) {
return;
}
// Grab the overlayBg and check if it is visible or not.
const overlayBg = document.getElementById('overlayBg');
if (overlayBg && overlayBg.classList.length > 0) {
// If there's a show callback, run it.
if (callbacks.show) {
callbacks.show();
}
} else if (callbacks.hide) {
// If there's a hide callback, run it.
callbacks.hide();
}
// Run all the specific callbacks.
overlayData = runCallbacks(overlayData, overlayType, callbacks);
});
// Observe the overlayPopup element for changes.
const observeTarget = document.getElementById('overlayPopup');
if (observeTarget) {
observer.observe(observeTarget, {
attributes: true,
attributeFilter: ['class']
});
}
};
/**
* TODO: update this docblock.
*
* @param {*} callback
*/
const onOverlayClose = (callback) => {
eventRegistry.addEventListener('js_dialog_hide', callback);
};
/**
* TODO: update this docblock.
*/
const getDialogMapping = () => {
return {
treasureMapPopup: 'map',
itemViewPopup: 'item',
mouseViewPopup: 'mouse',
largerImage: 'image',
convertibleOpenViewPopup: 'convertible',
adventureBookPopup: 'adventureBook',
marketplaceViewPopup: 'marketplace',
giftSelectorViewPopup: 'gifts',
supportPageContactUsForm: 'support',
MHCheckout: 'premiumShop',
};
};
/**
* TODO: update this docblock.
*
* @param {*} callback
* @param {*} overlay
* @param {*} once
*/
const onDialogShow = (callback, overlay = null, once = false) => {
eventRegistry.addEventListener('js_dialog_show', () => {
if (! activejsDialog) {
return;
}
// Get all the tokens and check the content.
const tokens = activejsDialog.getAllTokens();
// Make sure we have the 'content' key.
// For item and mouse views, the entire event fires twice, once while loading and
// once when the content is loaded. We only want to run this once, so we check if
// the content is empty in a weird way.
if (
! tokens ||
! tokens['{*content*}'] ||
! tokens['{*content*}'].value ||
tokens['{*content*}'].value === '' ||
tokens['{*content*}'].value.indexOf('data-item-type=""') > -1 || // Item view.
tokens['{*content*}'].value.indexOf('data-mouse-id=""') > -1 // Mouse view.
) {
return;
}
// Grab the attributes of the dialog to determine the type.
const atts = activejsDialog.getAttributes();
const dialogType = atts.className
.replace('jsDialogFixed', '')
.replace('wide', '')
.replace('default', '')
.replaceAll(' ', ' ')
.replaceAll(' ', '.')
.trim();
// Make sure this only ran once within the last 100ms for the same overlay.
if (window.mhutils?.lastDialog?.overlay === dialogType && (Date.now() - window.mhutils.lastDialog.timestamp) < 250) {
return;
}
const lastDialog = {
overlay: dialogType,
timestamp: Date.now(),
};
window.mhutils = window.mhutils ? { ...window.mhutils, ...lastDialog } : lastDialog;
if (! overlay && 'function' === typeof callback) {
return callback();
}
const dialogMapping = getDialogMapping();
if ('function' === typeof callback && (overlay === dialogType || overlay === dialogMapping[dialogType])) {
return callback();
}
}, null, once);
};
/**
* TODO: update this docblock.
*
* @param {*} callback
* @param {*} overlay
* @param {*} once
*/
const onDialogHide = (callback, overlay = null, once = false) => {
eventRegistry.addEventListener('js_dialog_hide', () => {
const dialogType = window?.mhutils?.lastDialog?.overlay || null;
window.mhutils.lastDialog = {};
if (! overlay) {
return callback();
}
const dialogMapping = getDialogMapping();
if (overlay === dialogType || overlay === dialogMapping[dialogType]) {
return callback();
}
}, null, once);
};
/**
* Do something when the page or tab changes.
*
* @param {Object} callbacks
* @param {Function} callbacks.show The callback to call when the page is navigated to.
* @param {Function} callbacks.hide The callback to call when the page is navigated away from.
* @param {Function} callbacks.change The callback to call when the page is changed.
*/
const onPageChange = (callbacks) => {
// Track our page tab states.
let tabData = {
blueprint: { isVisible: null, selector: 'showBlueprint' },
tem: { isVisible: false, selector: 'showTrapEffectiveness' },
trap: { isVisible: false, selector: 'editTrap' },
camp: { isVisible: false, selector: 'PageCamp' },
travel: { isVisible: false, selector: 'PageTravel' },
inventory: { isVisible: false, selector: 'PageInventory' },
shop: { isVisible: false, selector: 'PageShops' },
mice: { isVisible: false, selector: 'PageAdversaries' },
friends: { isVisible: false, selector: 'PageFriends' },
sendSupplies: { isVisible: false, selector: 'PageSupplyTransfer' },
team: { isVisible: false, selector: 'PageTeam' },
tournament: { isVisible: false, selector: 'PageTournament' },
news: { isVisible: false, selector: 'PageNews' },
scoreboards: { isVisible: false, selector: 'PageScoreboards' },
discord: { isVisible: false, selector: 'PageJoinDiscord' },
preferences: { isVisible: false, selector: 'PagePreferences' },
profile: { isVisible: false, selector: 'HunterProfile' },
};
// Observe the mousehuntContainer element for changes.
const observer = new MutationObserver(() => {
// If there's a change callback, run it.
if (callbacks.change) {
callbacks.change();
}
// Grab the container element and make sure it has classes on it.
const mhContainer = document.getElementById('mousehuntContainer');
if (mhContainer && mhContainer.classList.length > 0) {
// Run the callbacks.
tabData = runCallbacks(tabData, mhContainer, callbacks);
}
});
// Observe the mousehuntContainer element for changes.
const observeTarget = document.getElementById('mousehuntContainer');
if (observeTarget) {
observer.observe(observeTarget, {
attributes: true,
attributeFilter: ['class']
});
}
};
/**
* Do something when the trap tab is changed.
*
* @param {Object} callbacks
*/
const onTrapChange = (callbacks) => {
// Track our trap states.
let trapData = {
bait: {
isVisible: false,
selector: 'bait'
},
base: {
isVisible: false,
selector: 'base'
},
weapon: {
isVisible: false,
selector: 'weapon'
},
charm: {
isVisible: false,
selector: 'trinket'
},
skin: {
isVisible: false,
selector: 'skin'
}
};
// Observe the trapTabContainer element for changes.
const observer = new MutationObserver(() => {
// Fire the change callback.
if (callbacks.change) {
callbacks.change();
}
// If we're not viewing a blueprint tab, bail.
const mhContainer = document.getElementById('mousehuntContainer');
if (mhContainer.classList.length <= 0 || ! mhContainer.classList.contains('showBlueprint')) {
return;
}
// If we don't have the container, bail.
const trapContainerParent = document.querySelector('.campPage-trap-blueprintContainer');
if (! trapContainerParent || ! trapContainerParent.children || ! trapContainerParent.children.length > 0) {
return;
}
// If we're not in the itembrowser, bail.
const trapContainer = trapContainerParent.children[0];
if (! trapContainer || trapContainer.classList.length <= 0 || ! trapContainer.classList.contains('campPage-trap-itemBrowser')) {
return;
}
// Run the callbacks.
trapData = runCallbacks(trapData, trapContainer, callbacks);
});
// Grab the campPage-trap-blueprintContainer element and make sure it has children on it.
const observeTargetParent = document.querySelector('.campPage-trap-blueprintContainer');
if (! observeTargetParent || ! observeTargetParent.children || ! observeTargetParent.children.length > 0) {
return;
}
// Observe the first child of the campPage-trap-blueprintContainer element for changes.
const observeTarget = observeTargetParent.children[0];
if (observeTarget) {
observer.observe(observeTarget, {
attributes: true,
attributeFilter: ['class']
});
}
};
/**
* Add something to the event registry.
*
* @param {string} event The event name.
* @param {Function} callback The callback to run when the event is fired.
* @param {boolean} remove Whether or not to remove the event listener after it's fired.
*/
const onEvent = (event, callback, remove = false) => {
eventRegistry.addEventListener(event, callback, null, remove);
};
/**
* Do something when the user travels to a location.
*
* @param {string} location The location traveled to.
* @param {Object} options The options
* @param {string} options.shouldAddReminder Whether or not to add a reminder.
* @param {string} options.title The title of the reminder.
* @param {string} options.text The text of the reminder.
* @param {string} options.button The button text of the reminder.
* @param {string} options.action The action to take when the button is clicked.
* @param {string} options.callback The callback to run when the user is at the location.
*/
const onTravel = (location, options) => {
eventRegistry.addEventListener('travel_complete', () => onTravelCallback(location, options));
};
/**
* Do something when the user travels to a location.
* This is a callback for the onTravel function.
*
* @param {string} location The location traveled to.
* @param {Object} options The options
* @param {string} options.shouldAddReminder Whether or not to add a reminder.
* @param {string} options.title The title of the reminder.
* @param {string} options.text The text of the reminder.
* @param {string} options.button The button text of the reminder.
* @param {string} options.action The action to take when the button is clicked.
* @param {string} options.callback The callback to run when the user is at the location.
*/
const onTravelCallback = (location, options) => {
if (location && location !== getCurrentLocation()) {
return;
}
if (options?.shouldAddReminder) {
showHornMessage({
title: options.title || '',
text: options.text || '',
button: options.button || 'Dismiss',
action: options.action || null,
});
}
if (options.callback) {
options.callback();
}
};
/**
* TODO: update this docblock.
*
* @param {string} targetPage The target page.
* @param {string} targetTab The target tab.
* @param {string} targetSubtab The target subtab.
* @param {string} forceCurrentPage The current page.
* @param {string} forceCurrentTab The current tab.
* @param {string} forceCurrentSubtab The current subtab.
*/
const matchesCurrentPage = (targetPage = null, targetTab = null, targetSubtab = null, forceCurrentPage = null, forceCurrentTab = null, forceCurrentSubtab = null) => {
if (! targetPage) {
return false;
}
// Only targetPage is being checked.
const currentPage = forceCurrentPage || getCurrentPage();
if (! targetTab) {
return currentPage === targetPage;
}
// Only targetTab is being checked.
const currentTab = forceCurrentTab || getCurrentTab();
if (! targetSubtab) {
return currentPage === targetPage && currentTab === targetTab;
}
// Only targetSubtab is being checked.
const currentSubtab = forceCurrentSubtab || getCurrentSubtab();
if (currentSubtab === currentTab) {
return currentPage === targetPage && currentTab === targetTab;
}
return currentPage === targetPage && currentTab === targetTab && currentSubtab === targetSubtab;
};
/*
onNavigation(() => console.log('mouse stats by location'),
{
page: 'adversaries',
tab: 'your_stats',
subtab: 'location'
}
);
onNavigation(() => console.log('friend request page'),
{
page:'friends',
tab: 'requests'
}
);
onNavigation(() => console.log('hunter profile, but not when refreshing the page'),
{
page: 'hunterprofile',
onLoad: true
}
);
*/
/**
* TODO: update this docblock
*
* @param {Function} callback The callback to run when the user navigates to the page.
* @param {Object} options The options
* @param {string} options.page The page to watch for.
* @param {string} options.tab The tab to watch for.
* @param {string} options.subtab The subtab to watch for.
* @param {boolean} options.onLoad Whether or not to run the callback on load.
*/
const onNavigation = (callback, options = {}) => {
const defaults = {
page: false,
tab: false,
subtab: false,
onLoad: true,
};
// merge the defaults with the options
const { page, tab, subtab, onLoad } = Object.assign(defaults, options);
// If we don't pass in a page, then we want to run the callback on every page.
let bypassMatch = false;
if (! page) {
bypassMatch = true;
}
// We do this once on load in case we are starting on the page we want to watch for.
if (onLoad) {
if (bypassMatch || matchesCurrentPage(page, tab, subtab)) {
callback();
}
}
eventRegistry.addEventListener('set_page', (e) => {
const tabs = e?.data?.tabs || {};
const currentTab = Object.keys(tabs).find((key) => tabs[key].is_active_tab);
const forceCurrentTab = currentTab?.type;
if (! subtab) {
if (matchesCurrentPage(page, tab, false, getCurrentPage(), forceCurrentTab)) {
callback();
}
return;
}
if (currentTab?.subtabs && currentTab?.subtabs.length > 0) {
const forceSubtab = currentTab.subtabs.find((searchTab) => searchTab.is_active_subtab).subtab_type;
if (matchesCurrentPage(page, tab, subtab, getCurrentPage(), forceCurrentTab, forceSubtab)) {
callback();
}
}
});
eventRegistry.addEventListener('set_tab', (e) => {
const forceCurrentTab = e.page_arguments.tab;
const forceCurrentSubtab = e.page_arguments.sub_tab;
if (matchesCurrentPage(page, tab, subtab, getCurrentPage(), forceCurrentTab, forceCurrentSubtab)) {
callback();
}
});
};
const onNavigate = onNavigation;
/**
* Get the current page slug.
*
* @return {string} The page slug.
*/
const getCurrentPage = () => {
return hg.utils.PageUtil.getCurrentPage().toLowerCase(); // eslint-disable-line no-undef
};
/**
* Get the current page tab, defaulting to the current page if no tab is found.
*
* @return {string} The page tab.
*/
const getCurrentTab = () => {
const tab = hg.utils.PageUtil.getCurrentPageTab().toLowerCase(); // eslint-disable-line no-undef
if (tab.length <= 0) {
return getCurrentPage();
}
return tab;
};
/**
* Get the current page sub tab, defaulting to the current tab if no sub tab is found.
*
* @return {string} The page tab.
*/
const getCurrentSubtab = () => {
const subtab = hg.utils.PageUtil.getCurrentPageSubTab();
if (! subtab || subtab.length <= 0) {
return getCurrentTab();
}
return subtab.toLowerCase();
};
// Backwards compatibility.
const getCurrentSubTab = getCurrentSubtab;
/**
* Check if the overlay is visible.
*
* @return {boolean} True if the overlay is visible, false otherwise.
*/
const isOverlayVisible = () => {
return activejsDialog && activejsDialog.isVisible();
};
/**
* Get the current overlay.
*
* @return {string} The current overlay.
*/
const getCurrentOverlay = () => {
const overlay = document.getElementById('overlayPopup');
if (overlay && overlay.classList.length <= 0) {
return null;
}
let overlayType = overlay.classList.value;
overlayType = overlayType.replace('jsDialogFixed', '');
overlayType = overlayType.replace('default', '');
overlayType = overlayType.replace('wide', '');
overlayType = overlayType.replace('ajax', '');
overlayType = overlayType.replace('overlay', '');
// Replace some overlay types with more readable names.
overlayType = overlayType.replace('treasureMapPopup', 'map');
overlayType = overlayType.replace('itemViewPopup', 'item');
overlayType = overlayType.replace('mouseViewPopup', 'mouse');
overlayType = overlayType.replace('largerImage', 'image');
overlayType = overlayType.replace('convertibleOpenViewPopup', 'convertible');
overlayType = overlayType.replace('adventureBookPopup', 'adventureBook');
overlayType = overlayType.replace('marketplaceViewPopup', 'marketplace');
overlayType = overlayType.replace('giftSelectorViewPopup', 'gifts');
overlayType = overlayType.replace('supportPageContactUsForm', 'support');
overlayType = overlayType.replace('MHCheckout', 'premiumShop');
return overlayType.trim();
};
/**
* Get the current location.
*
* @return {string} The current location.
*/
const getCurrentLocation = () => {
const location = user?.environment_type || '';
return location.toLowerCase();
};
/**
* Check if the user is logged in.
*
* @return {boolean} True if the user is logged in, false otherwise.
*/
const isLoggedIn = () => {
return user.length > 0 && 'login' !== getCurrentPage();
};
/**
* Get the saved settings.
*
* @param {string} key The key to get.
* @param {boolean} defaultValue The default value.
* @param {string} identifier The identifier for the settings.
*
* @return {Object} The saved settings.
*/
const getSetting = (key = null, defaultValue = null, identifier = 'mh-utils-settings') => {
// Grab the local storage data.
const settings = JSON.parse(localStorage.getItem(identifier)) || {};
// If we didn't get a key passed in, we want all the settings.
if (! key) {
return settings;
}
// If the setting doesn't exist, return the default value.
if (Object.prototype.hasOwnProperty.call(settings, key)) {
return settings[key];
}
return defaultValue;
};
/**
* Save a setting.
*
* @param {string} key The setting key.
* @param {boolean} value The setting value.
* @param {string} identifier The identifier for the settings.
*/
const saveSetting = (key, value, identifier = 'mh-utils-settings') => {
// Grab all the settings, set the new one, and save them.
const settings = getSetting(null, {}, identifier);
settings[key] = value;
localStorage.setItem(identifier, JSON.stringify(settings));
};
/**
* Save a setting and toggle the class in the settings UI.
*
* @ignore
*
* @param {Node} node The setting node to animate.
* @param {string} key The setting key.
* @param {boolean} value The setting value.
*/
const saveSettingAndToggleClass = (node, key, value, identifier = 'mh-utils-settings') => {
node.parentNode.parentNode.classList.add('busy');
// Toggle the state of the checkbox.
node.classList.toggle('active');
// Save the setting.
saveSetting(key, value, identifier);
// Add the completed class & remove it in a second.
node.parentNode.parentNode.classList.remove('busy');
node.parentNode.parentNode.classList.add('completed');
setTimeout(() => {
node.parentNode.parentNode.classList.remove('completed');
}, 1000);
addSettingRefreshReminder();
};
/**
* Make the settings tab.
*
* @param {string} identifier The identifier for the settings.
* @param {string} name The name of the settings tab.
*/
const addSettingsTab = (identifier = 'userscript-settings', name = 'Userscript Settings') => {
addSettingsTabOnce(identifier, name);
onPageChange({ preferences: { show: () => addSettingsTabOnce(identifier, name) } });
return identifier;
};
/**
* Make the settings tab once.
*
* @ignore
*
* @param {string} identifier The identifier for the settings.
* @param {string} name The name of the settings tab.
*/
const addSettingsTabOnce = (identifier = 'userscript-settings', name = 'Userscript Settings') => {
if ('preferences' !== getCurrentPage()) {
return;
}
const existingSettings = document.querySelector(`#${identifier}`);
if (existingSettings) {
return;
}
const tabsContainer = document.querySelector('.mousehuntHud-page-tabHeader-container');
if (! tabsContainer) {
return;
}
const tabsContentContainer = document.querySelector('.mousehuntHud-page-tabContentContainer');
if (! tabsContentContainer) {
return;
}
// make sure the identifier is unique and safe to use as a class.
identifier = identifier.replace(/[^a-z0-9-_]/gi, '');
const settingsTab = document.createElement('a');
settingsTab.id = identifier;
settingsTab.href = '#';
settingsTab.classList.add('mousehuntHud-page-tabHeader', identifier);
settingsTab.setAttribute('data-tab', identifier);
settingsTab.setAttribute('onclick', 'hg.utils.PageUtil.onclickPageTabHandler(this); return false;');
const settingsTabText = document.createElement('span');
settingsTabText.innerText = name;
settingsTab.appendChild(settingsTabText);
tabsContainer.appendChild(settingsTab);
const settingsTabContent = document.createElement('div');
settingsTabContent.classList.add('mousehuntHud-page-tabContent', 'game_settings', identifier);
settingsTabContent.setAttribute('data-tab', identifier);
tabsContentContainer.appendChild(settingsTabContent);
if (identifier === getCurrentTab()) {
const tab = document.getElementById(identifier);
if (tab) {
tab.click();
}
}
};
/**
* Add a setting to the preferences page, both on page load and when the page changes.
*
* @param {string} name The setting name.
* @param {string} key The setting key.
* @param {boolean} defaultValue The default value.
* @param {string} description The setting description.
* @param {Object} section The section settings.
* @param {string} tab The tab to add the settings to.
* @param {Object} settings The settings for the settings.
*/
const addSetting = (name, key, defaultValue = true, description = '', section = {}, tab = 'userscript-settings', settings = null) => {
onPageChange({ preferences: { show: () => addSettingOnce(name, key, defaultValue, description, section, tab, settings) } });
addSettingOnce(name, key, defaultValue, description, section, tab, settings);
addSettingRefreshReminder();
onPageChange({ preferences: { show: addSettingRefreshReminder } });
};
/**
* Add a setting to the preferences page.
*
* @ignore
*
* @param {string} name The setting name.
* @param {string} key The setting key.
* @param {boolean} defaultValue The default value.
* @param {string} description The setting description.
* @param {Object} section The section settings.
* @param {string} tab The tab to add the settings to.
* @param {Object} settingSettings The settings for the settings.
*/
const addSettingOnce = (name, key, defaultValue = true, description = '', section = {}, tab = 'userscript-settings', settingSettings = null) => {
// Make sure we have the container for our settings.
const container = document.querySelector(`.mousehuntHud-page-tabContent.${tab}`);
if (! container) {
return;
}
section = {
id: section.id || 'settings',
name: section.name || 'Userscript Settings',
description: section.description || '',
};
let tabId = 'mh-utils-settings';
if (tab !== 'userscript-settings') {
tabId = tab;
}
section.id = `${tabId}-${section.id.replace(/[^a-z0-9-_]/gi, '')}`;
// If we don't have our custom settings section, then create it.
let sectionExists = document.querySelector(`#${section.id}`);
if (! sectionExists) {
// Make the element, add the ID and class.
const title = document.createElement('div');
title.id = section.id;
title.classList.add('PagePreferences__title');
// Set the title of our section.
const titleText = document.createElement('h3');
titleText.classList.add('PagePreferences__titleText');
titleText.textContent = section.name;
// Append the title.
title.appendChild(titleText);
// Add a separator.
const seperator = document.createElement('div');
seperator.classList.add('PagePreferences__separator');
// Append the separator.
title.appendChild(seperator);
// Append it.
container.appendChild(title);
sectionExists = document.querySelector(`#${section.id}`);
if (section.description) {
const settingSubHeader = makeElement('h4', ['settings-subheader', 'mh-utils-settings-subheader'], section.description);
sectionExists.insertBefore(settingSubHeader, seperator);
addStyles(`.mh-utils-settings-subheader {
padding-top: 10px;
padding-bottom: 10px;
font-size: 10px;
color: #848484;
}`, 'mh-utils-settings-subheader', true);
}
}
// If we already have a setting visible for our key, bail.
const settingExists = document.getElementById(`${section.id}-${key}`);
if (settingExists) {
return;
}
// Create the markup for the setting row.
const settings = document.createElement('div');
settings.classList.add('PagePreferences__settingsList');
settings.id = `${section.id}-${key}`;
const settingRow = document.createElement('div');
settingRow.classList.add('PagePreferences__setting');
const settingRowLabel = document.createElement('div');
settingRowLabel.classList.add('PagePreferences__settingLabel');
const settingName = document.createElement('div');
settingName.classList.add('PagePreferences__settingName');
settingName.innerHTML = name;
const defaultSettingText = document.createElement('div');
defaultSettingText.classList.add('PagePreferences__settingDefault');
if (settingSettings && (settingSettings.type === 'select' || settingSettings.type === 'multi-select')) {
addStyles(`.PagePreferences .mousehuntHud-page-tabContent.game_settings.userscript-settings .settingRow .settingRow-action-inputContainer.select.busy:before,
.PagePreferences .mousehuntHud-page-tabContent.game_settings.userscript-settings .settingRow .settingRow-action-inputContainer.select.completed:before,
.PagePreferences .mousehuntHud-page-tabContent.game_settings.better-mh-settings .settingRow .settingRow-action-inputContainer.select.busy:before,
.PagePreferences .mousehuntHud-page-tabContent.game_settings.better-mh-settings .settingRow .settingRow-action-inputContainer.select.completed:before {
left: unset;
right: -25px;
top: 30px;
}
.PagePreferences .mousehuntHud-page-tabContent.game_settings .settingRow .name {
height: unset;
min-height: 20px;
}
.PagePreferences__settingAction.inputDropdownWrapper.busy:before,
.PagePreferences__settingAction.inputDropdownWrapper.completed:before {
left: unset;
right: -40px;
}
.inputBoxContainer.multiSelect {
max-width: 400px;
}`, 'mh-utils-settings-select', true);
defaultSettingText.textContent = defaultValue.map((item) => item.name).join(', ');
} else {
defaultSettingText.textContent = defaultValue ? 'Enabled' : 'Disabled';
}
defaultSettingText.textContent = `Default setting: ${defaultSettingText.textContent}`;
const settingDescription = makeElement('div', 'PagePreferences__settingDescription');
settingDescription.innerHTML = description;
settingRowLabel.appendChild(settingName);
settingRowLabel.appendChild(defaultSettingText);
settingRowLabel.appendChild(settingDescription);
const settingRowAction = makeElement('div', 'PagePreferences__settingAction');
const settingRowInput = makeElement('div', 'settingRow-action-inputContainer');
if (settingSettings && (settingSettings.type === 'select' || settingSettings.type === 'multi-select')) {
// Create the dropdown.
const settingRowInputDropdown = document.createElement('div');
settingRowInputDropdown.classList.add('inputBoxContainer');
if (settingSettings.type === 'multi-select') {
settingRowInputDropdown.classList.add('multiSelect');
settingRowInput.classList.add('multiSelect', 'select');
}
const amount = settingSettings.type === 'multi-select' ? settingSettings.number : 1;
const makeOption = (option, foundSelected, currentSetting, dValue, i) => {
const settingRowInputDropdownSelectOption = document.createElement('option');
settingRowInputDropdownSelectOption.value = option.value;
settingRowInputDropdownSelectOption.textContent = option.name;
if (currentSetting && currentSetting === option.value) {
settingRowInputDropdownSelectOption.selected = true;
foundSelected = true;
} else if (! foundSelected && dValue && dValue[i] && dValue[i].value === option.value) {
settingRowInputDropdownSelectOption.selected = true;
foundSelected = true;
}
return {
settingRowInputDropdownSelectOption,
foundSelected
};
};
// make a multi-select dropdown.
for (let i = 0; i < amount; i++) {
const settingRowInputDropdownSelect = document.createElement('select');
settingRowInputDropdownSelect.classList.add('inputBox');
if (settingSettings.type === 'multi-select') {
settingRowInputDropdownSelect.classList.add('multiSelect');
}
const currentSetting = getSetting(`${key}-${i}`, null, tab);
let foundSelected = false;
settingSettings.options.forEach((option) => {
// If the value is 'optgroup', then we want to make an optgroup and use the options inside of it.
if (option.value === 'group') {
const settingRowInputDropdownSelectOptgroup = document.createElement('optgroup');
settingRowInputDropdownSelectOptgroup.label = option.name;
option.options.forEach((optgroupOption) => {
const result = makeOption(optgroupOption, foundSelected, currentSetting, defaultValue, i);
foundSelected = result.foundSelected;
settingRowInputDropdownSelectOptgroup.appendChild(result.settingRowInputDropdownSelectOption);
});
settingRowInputDropdownSelect.appendChild(settingRowInputDropdownSelectOptgroup);
} else {
const result = makeOption(option, foundSelected, currentSetting, defaultValue, i);
foundSelected = result.foundSelected;
settingRowInputDropdownSelect.appendChild(result.settingRowInputDropdownSelectOption);
}
});
settingRowInputDropdown.appendChild(settingRowInputDropdownSelect);
// Event listener for when the setting is clicked.
settingRowInputDropdownSelect.onchange = (event) => {
const parent = settingRowInputDropdownSelect.parentNode.parentNode.parentNode;
parent.classList.add('inputDropdownWrapper');
parent.classList.add('busy');
// save the setting.
saveSetting(`${key}-${i}`, event.target.value, tab);
parent.classList.remove('busy');
parent.classList.add('completed');
setTimeout(() => {
parent.classList.remove('completed');
}, 1000);
};
settingRowInput.appendChild(settingRowInputDropdown);
settingRowAction.appendChild(settingRowInput);
}
} else if (settingSettings && settingSettings.type === 'input') {
addStyles(`.settingRow-action-inputContainer.inputText {
display: flex;
align-items: stretch;
gap: 5px;
}`, 'mh-utils-settings-input', true);
const settingRowInputText = makeElement('input', 'inputBox');
settingRowInputText.value = getSetting(key, defaultValue, tab);
const inputSaveButton = makeElement('button', ['mousehuntActionButton', 'tiny', 'inputSaveButton']);
makeElement('span', '', 'Save', inputSaveButton);
// Event listener for when the setting is clicked.
inputSaveButton.addEventListener('click', (event) => {
const parent = event.target.parentNode.parentNode;
parent.classList.add('inputDropdownWrapper');
parent.classList.add('busy');
// save the setting.
saveSetting(key, settingRowInputText.value, tab);
parent.classList.remove('busy');
parent.classList.add('completed');
setTimeout(() => {
parent.classList.remove('completed');
}, 1000);
});
settingRowInput.classList.add('inputText');
settingRowInput.appendChild(settingRowInputText);
settingRowInput.appendChild(inputSaveButton);
settingRowAction.appendChild(settingRowInput);
} else if (settingSettings && settingSettings.type === 'textarea') {
addStyles(`.settingRow-action-inputContainer.textarea {
display: flex;
align-items: flex-end;
gap: 5px;
}
.PagePreferences__setting.textarea {
display: grid;
grid-template-columns: 350px 1fr;
}
.textarea .inputBox {
width: 100%;
min-height: 45px;
}
.textarea .PagePreferences__settingAction {
margin-bottom: 0;
}`, 'mh-utils-settings-textarea', true);
settingRow.classList.add('textarea');
const settingRowInputText = makeElement('textarea', 'inputBox');
settingRowInputText.value = getSetting(key, defaultValue, tab);
const inputSaveButton = makeElement('button', ['mousehuntActionButton', 'tiny', 'inputSaveButton']);
makeElement('span', '', 'Save', inputSaveButton);
// Event listener for when the setting is clicked.
inputSaveButton.addEventListener('click', (event) => {
const parent = event.target.parentNode.parentNode;
parent.classList.add('inputDropdownWrapper');
parent.classList.add('busy');
// save the setting.
saveSetting(key, settingRowInputText.value, tab);
parent.classList.remove('busy');
parent.classList.add('completed');
setTimeout(() => {
parent.classList.remove('completed');
}, 1000);
});
settingRowInput.classList.add('textarea');
settingRowInput.appendChild(settingRowInputText);
settingRowInput.appendChild(inputSaveButton);
settingRowAction.appendChild(settingRowInput);
} else {
const settingRowInputCheckbox = document.createElement('div');
settingRowInputCheckbox.classList.add('mousehuntSettingSlider');
// Depending on the current state of the setting, add the active class.
const currentSetting = getSetting(key, null, tab);
let isActive = false;
if (currentSetting) {
settingRowInputCheckbox.classList.add('active');
isActive = true;
} else if (null === currentSetting && defaultValue) {
settingRowInputCheckbox.classList.add('active');
isActive = true;
}
// Event listener for when the setting is clicked.
settingRowInputCheckbox.onclick = (event) => {
saveSettingAndToggleClass(event.target, key, ! isActive, tab);
};
// Add the input to the settings row.
settingRowInput.appendChild(settingRowInputCheckbox);
settingRowAction.appendChild(settingRowInput);
}
// Add the label and action to the settings row.
settingRow.appendChild(settingRowLabel);
settingRow.appendChild(settingRowAction);
// Add the settings row to the settings container.
settings.appendChild(settingRow);
sectionExists.appendChild(settings);
};
/**
* Add a refresh reminder to the settings page.
*
* @ignore
*/
const addSettingRefreshReminder = () => {
const existing = document.querySelector('.mh-utils-settings-refresh-message');
if (existing) {
return;
}
addStyles(`.mh-utils-settings-refresh-message {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
padding: 1em;
font-size: 1.5em;
text-align: center;
background-color: #d6f2d6;
border-top: 1px solid #6cc36c;
opacity: 1;
transition: opacity 0.5s ease-in-out;
pointer-events: none;
}
.mh-utils-settings-refresh-message-hidden {
opacity: 0;
}`, 'mh-utils-settings-refresh-message', true);
const settingsToggles = document.querySelectorAll('.mousehuntSettingSlider');
if (! settingsToggles) {
return;
}
settingsToggles.forEach((toggle) => {
if (toggle.getAttribute('data-has-refresh-reminder')) {
return;
}
toggle.setAttribute('data-has-refresh-reminder', true);
toggle.addEventListener('click', () => {
const refreshMessage = document.querySelector('.mh-utils-settings-refresh-message');
if (refreshMessage) {
refreshMessage.classList.remove('mh-utils-settings-refresh-message-hidden');
}
setTimeout(() => {
if (refreshMessage) {
refreshMessage.classList.add('mh-utils-settings-refresh-message-hidden');
}
}, 5000);
});
});
const existingRefreshMessage = document.querySelector('.mh-utils-settings-refresh-message');
if (! existingRefreshMessage) {
const body = document.querySelector('body');
if (body) {
makeElement('div', ['mh-utils-settings-refresh-message', 'mh-utils-settings-refresh-message-hidden'], 'Refresh the page to apply your changes.', body);
}
}
};
/**
* POST a request to the server and return the response.
*
* @async
* @param {string} url The url to post to, not including the base url.
* @param {Object} formData The form data to post.
*
* @return {Promise} The response.
*/
const doRequest = async (url, formData = {}) => {
// If we don't have the needed params, bail.
if ('undefined' === typeof lastReadJournalEntryId || 'undefined' === typeof user) {
return;
}
// If our needed params are empty, bail.
if (! lastReadJournalEntryId || ! user || ! user.unique_hash) { // eslint-disable-line no-undef
return;
}
// Build the form for the request.
const form = new FormData();
form.append('sn', 'Hitgrab');
form.append('hg_is_ajax', 1);
form.append('last_read_journal_entry_id', lastReadJournalEntryId ? lastReadJournalEntryId : 0); // eslint-disable-line no-undef
form.append('uh', user.unique_hash ? user.unique_hash : ''); // eslint-disable-line no-undef
// Add in the passed in form data.
for (const key in formData) {
form.append(key, formData[key]);
}
// Convert the form to a URL encoded string for the body.
const requestBody = new URLSearchParams(form).toString();
// Send the request.
const response = await fetch(
callbackurl ? callbackurl + url : 'https://www.mousehuntgame.com/' + url, // eslint-disable-line no-undef
{
method: 'POST',
body: requestBody,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
// Wait for the response and return it.
const data = await response.json();
return data;
};
/**
* Check if the legacy HUD is enabled.
*
* @return {boolean} Whether the legacy HUD is enabled.
*/
const isLegacyHUD = () => {
return hg.utils.PageUtil.isLegacy();
};
/**
* Check if an item is in the inventory.
*
* @async
*
* @param {string} item The item to check for.
*
* @return {boolean} Whether the item is in the inventory.
*/
const userHasItem = async (item) => {
const hasItem = await getUserItems([item]);
return hasItem.length > 0;
};
/**
* Check if an item is in the inventory.
*
* @async
*
* @param {Array} items The item to check for.
*
* @return {Array} The item data.
*/
const getUserItems = async (items) => {
return new Promise((resolve) => {
hg.utils.UserInventory.getItems(items, (resp) => {
resolve(resp);
});
});
};
/**
* Get the user's setup details.
*
* @return {Object} The user's setup details.
*/
const getUserSetupDetails = () => {
const userObj = user; // eslint-disable-line no-undef
const setup = {
type: userObj.trap_power_type_name,
stats: {
power: userObj.trap_power,
powerBonus: userObj.trap_power_bonus,
luck: userObj.trap_luck,
attractionBonus: userObj.trap_attraction_bonus,
cheeseEfect: userObj.trap_cheese_effect,
},
bait: {
id: parseInt(userObj.bait_item_id),
name: userObj.bait_name,
quantity: parseInt(userObj.bait_quantity),
power: 0,
powerBonus: 0,
luck: 0,
attractionBonus: 0,
},
base: {
id: parseInt(userObj.base_item_id),
name: userObj.base_name,
power: 0,
powerBonus: 0,
luck: 0,
attractionBonus: 0,
},
charm: {
id: parseInt(userObj.trinket_item_id),
name: userObj.trinket_name,
quantity: parseInt(userObj.trinket_quantity),
power: 0,
powerBonus: 0,
luck: 0,
attractionBonus: 0,
},
weapon: {
id: parseInt(userObj.weapon_item_id),
name: userObj.weapon_name,
power: 0,
powerBonus: 0,
luck: 0,
attractionBonus: 0,
},
aura: {
lgs: {
active: false,
power: 0,
powerBonus: 0,
luck: 0,
},
lightning: {
active: false,
power: 0,
powerBonus: 0,
luck: 0,
},
chrome: {
active: false,
power: 0,
powerBonus: 0,
luck: 0,
},
slayer: {
active: false,
power: 0,
powerBonus: 0,
luck: 0,
},
festive: {
active: false,
power: 0,
powerBonus: 0,
luck: 0,
},
luckycodex: {
active: false,
power: 0,
powerBonus: 0,
luck: 0,
},
riftstalker: {
active: false,
power: 0,
powerBonus: 0,
luck: 0,
},
},
location: {
name: userObj.environment_name,
id: userObj.environment_id,
slug: userObj.environment_type,
},
};
if ('camp' !== getCurrentPage()) {
return setup;
}
const calculations = document.querySelectorAll('.campPage-trap-trapStat');
if (! calculations) {
return setup;
}
calculations.forEach((calculation) => {
if (calculation.classList.length <= 1) {
return;
}
const type = calculation.classList[1];
const math = calculation.querySelectorAll('.math .campPage-trap-trapStat-mathRow');
if (! math) {
return;
}
math.forEach((row) => {
if (row.classList.contains('label')) {
return;
}
let value = row.querySelector('.campPage-trap-trapStat-mathRow-value');
let name = row.querySelector('.campPage-trap-trapStat-mathRow-name');
if (! value || ! name || ! name.innerText) {
return;
}
name = name.innerText;
value = value.innerText || '0';
let tempType = type;
let isBonus = false;
if (value.includes('%')) {
tempType = type + 'Bonus';
isBonus = true;
}
// Because attraction_bonus is silly.
tempType = tempType.replace('_bonusBonus', 'Bonus');
value = value.replace('%', '');
value = value.replace(',', '');
value = parseInt(value * 100) / 100;
if (tempType === 'attractionBonus') {
value = value / 100;
}
// Check if the name matches either setup.weapon.name, setup.base.name, setup.charm.name, setup.bait.name and if so, update the setup object with the value
if (setup.weapon.name === name) {
setup.weapon[tempType] = value;
} else if (setup.base.name === name) {
setup.base[tempType] = value;
} else if (setup.charm.name === name) {
setup.charm[tempType] = value;
} else if (setup.bait.name === name) {
setup.bait[tempType] = value;
} else if ('Your trap has no cheese effect bonus.' === name) {
setup.cheeseEffect = 'No Effect';
} else {
let auraType = name.replace(' Aura', '');
if (! auraType) {
return;
}
auraType = auraType.toLowerCase();
auraType = auraType.replaceAll(' ', '_');
// remove any non alphanumeric characters
auraType = auraType.replace(/[^a-z0-9_]/gi, '');
auraType = auraType.replace('golden_luck_boost', 'lgs');
auraType = auraType.replace('2023_lucky_codex', 'luckycodex');
auraType = auraType.replace('_set_bonus_2_pieces', '');
auraType = auraType.replace('_set_bonus_3_pieces', '');
if (! setup.aura[auraType]) {
setup.aura[auraType] = {
active: true,
type: auraType,
power: 0,
powerBonus: 0,
luck: 0,
};
} else {
setup.aura[auraType].active = true;
setup.aura[auraType].type = auraType;
}
value = parseInt(value);
if (isBonus) {
value = value / 100;
}
setup.aura[auraType][tempType] = value;
}
});
});
return setup;
};
/**
* Add a submenu item to a menu.
*
* @param {Object} options The options for the submenu item.
* @param {string} options.menu The menu to add the submenu item to.
* @param {string} options.label The label for the submenu item.
* @param {string} options.icon The icon for the submenu item.
* @param {string} options.href The href for the submenu item.
* @param {string} options.class The class for the submenu item.
* @param {Function} options.callback The callback for the submenu item.
* @param {boolean} options.external Whether the submenu item is external or not.
*/
const addSubmenuItem = (options) => {
// Default to sensible values.
const settings = Object.assign({}, {
menu: 'kingdom',
label: '',
icon: 'https://www.mousehuntgame.com/images/ui/hud/menu/special.png',
href: '',
class: '',
callback: null,
external: false,
}, options);
// Grab the menu item we want to add the submenu to.
const menuTarget = document.querySelector(`.mousehuntHud-menu .${settings.menu}`);
if (! menuTarget) {
return;
}
// If the menu already has a submenu, just add the item to it.
if (! menuTarget.classList.contains('hasChildren')) {
menuTarget.classList.add('hasChildren');
}
let hasSubmenu = true;
let submenu = menuTarget.querySelector('ul');
if (! submenu) {
hasSubmenu = false;
submenu = document.createElement('ul');
}
// Create the item.
const item = document.createElement('li');
item.classList.add('custom-submenu-item');
const cleanLabel = settings.label.toLowerCase().replace(/[^a-z0-9]/g, '-');
const exists = document.querySelector(`#custom-submenu-item-${cleanLabel}`);
if (exists) {
return;
}
item.id = `custom-submenu-item-${cleanLabel}`;
if (settings.class) {
item.classList.add(settings.class);
}
// Create the link.
const link = document.createElement('a');
link.href = settings.href || '#';
if (settings.callback) {
link.addEventListener('click', (e) => {
e.preventDefault();
settings.callback();
});
}
// Create the icon.
const icon = document.createElement('div');
icon.classList.add('icon');
icon.style = `background-image: url(${settings.icon});`;
// Create the label.
const name = document.createElement('div');
name.classList.add('name');
name.innerText = settings.label;
// Add the icon and label to the link.
link.appendChild(icon);
link.appendChild(name);
// If it's an external link, also add the icon for it.
if (settings.external) {
const externalLinkIcon = document.createElement('div');
externalLinkIcon.classList.add('external_icon');
link.appendChild(externalLinkIcon);
// Set the target to _blank so it opens in a new tab.
link.target = '_blank';
link.rel = 'noopener noreferrer';
}
// Add the link to the item.
item.appendChild(link);
// Add the item to the submenu.
submenu.appendChild(item);
if (! hasSubmenu) {
menuTarget.appendChild(submenu);
}
};
/**
* Add the mouse.rip link to the kingdom menu.
*
* @ignore
*/
const addMouseripLink = () => {
addSubmenuItem({
menu: 'kingdom',
label: 'mouse.rip',
icon: 'https://www.mousehuntgame.com/images/ui/hud/menu/prize_shoppe.png',
href: 'https://mouse.rip',
external: true,
});
};
/**
* Add an item to the top 'Hunters Online' menu.
*
* @param {Object} options The options for the menu item.
* @param {string} options.label The label for the menu item.
* @param {string} options.href The href for the menu item.
* @param {string} options.class The class for the menu item.
* @param {Function} options.callback The callback for the menu item.
* @param {boolean} options.external Whether the link is external or not.
*/
const addItemToGameInfoBar = (options) => {
const settings = Object.assign({}, {
label: '',
href: '',
class: '',
callback: null,
external: false,
}, options);
const safeLabel = settings.label.replace(/[^a-z0-9]/gi, '_').toLowerCase();
const exists = document.querySelector(`#mh-custom-topmenu-${safeLabel}`);
if (exists) {
return;
}
addStyles(`.mousehuntHud-gameInfo .mousehuntHud-menu {
position: relative;
top: unset;
left: unset;
display: inline;
width: unset;
height: unset;
padding-top: unset;
padding-left: unset;
background: unset;
}
`, 'mh-custom-topmenu', true);
const menu = document.querySelector('.mousehuntHud-gameInfo');
if (! menu) {
return;
}
const item = document.createElement('a');
item.id = `mh-custom-topmenu-${safeLabel}`;
item.classList.add('mousehuntHud-gameInfo-item');
item.classList.add('mousehuntHud-custom-menu-item');
item.href = settings.href || '#';
const name = document.createElement('div');
name.classList.add('name');
if (settings.label) {
name.innerText = settings.label;
}
item.appendChild(name);
if (settings.class) {
item.classList.add(settings.class);
}
if (settings.href) {
item.href = settings.href;
}
if (settings.callback) {
item.addEventListener('click', settings.callback);
}
if (settings.external) {
const externalLinkIconWrapper = document.createElement('div');
externalLinkIconWrapper.classList.add('mousehuntHud-menu');
const externalLinkIcon = document.createElement('div');
externalLinkIcon.classList.add('external_icon');
externalLinkIconWrapper.appendChild(externalLinkIcon);
item.appendChild(externalLinkIconWrapper);
}
menu.insertBefore(item, menu.firstChild);
};
/**
* Build a popup.
*
* Templates:
* ajax: no close button in lower right, 'prefix' instead of title. 'suffix' for close button area.
* default: {*title*} {*content*}
* error: in red, with error icon{*title*} {*content*}
* largerImage: full width image {*title*} {*image*}
* largerImageWithClass: smaller than larger image, with caption {*title*} {*image*} {*imageCaption*} {*imageClass*} (goes on the img tag)
* loading: Just says loading
* multipleItems: {*title*} {*content*} {*items*}
* singleItemLeft: {*title*} {*content*} {*items*}
* singleItemRight: {*title*} {*content*} {*items*}
*
* @param {Object} options The popup options.
* @param {string} options.title The title of the popup.
* @param {string} options.content The content of the popup.
* @param {boolean} options.hasCloseButton Whether or not the popup has a close button.
* @param {string} options.template The template to use for the popup.
* @param {boolean} options.show Whether or not to show the popup.
* @param {string} options.className The class name to add to the popup.
*/
const createPopup = (options) => {
// If we don't have jsDialog, bail.
if ('undefined' === typeof jsDialog || ! jsDialog) { // eslint-disable-line no-undef
return;
}
// Default to sensible values.
const settings = Object.assign({}, {
title: '',
content: '',
hasCloseButton: true,
template: 'default',
show: true,
className: '',
}, options);
// Initiate the popup.
const popup = new jsDialog(); // eslint-disable-line no-undef
popup.setIsModal(! settings.hasCloseButton);
// Set the template & add in the content.
popup.setTemplate(settings.template);
popup.addToken('{*title*}', settings.title);
popup.addToken('{*content*}', settings.content);
popup.setAttributes({
className: settings.className,
});
// If we want to show the popup, show it.
if (settings.show) {
popup.show();
}
return popup;
};
/**
* Create a popup with an image.
*
* @param {Object} options Popup options.
* @param {string} options.title The title of the popup.
* @param {string} options.image The image to show in the popup.
* @param {boolean} options.show Whether or not to show the popup.
*/
const createImagePopup = (options) => {
// Default to sensible values.
const settings = Object.assign({}, {
title: '',
image: '',
show: true,
}, options);
// Create the popup.
const popup = createPopup({
title: settings.title,
template: 'largerImage',
show: false,
});
// Add the image to the popup.
popup.addToken('{*image*}', settings.image);
// If we want to show the popup, show it.
if (settings.show) {
popup.show();
}
return popup;
};
/**
* Show a map-popup.
*
* @param {Object} options The popup options.
* @param {string} options.title The title of the popup.
* @param {string} options.content The content of the popup.
* @param {string} options.closeClass The class to add to the close button.
* @param {string} options.closeText The text to add to the close button.
* @param {boolean} options.show Whether or not to show the popup.
*/
const createMapPopup = (options) => {
// Check to make sure we can call the hg views.
if (! (hg && hg.views && hg.views.TreasureMapDialogView)) { // eslint-disable-line no-undef
return;
}
// Default to sensible values.
const settings = Object.assign({}, {
title: '',
content: '',
closeClass: 'acknowledge',
closeText: 'ok',
show: true,
}, options);
// Initiate the popup.
const dialog = new hg.views.TreasureMapDialogView(); // eslint-disable-line no-undef
// Set all the content and options.
dialog.setTitle(options.title);
dialog.setContent(options.content);
dialog.setCssClass(options.closeClass);
dialog.setContinueAction(options.closeText);
// If we want to show & we can show, show it.
if (settings.show && hg.controllers && hg.controllers.TreasureMapDialogController) { // eslint-disable-line no-undef
hg.controllers.TreasureMapController.show(); // eslint-disable-line no-undef
hg.controllers.TreasureMapController.showDialog(dialog); // eslint-disable-line no-undef
}
return dialog;
};
/**
* Create a welcome popup.
*
* @param {Object} options The popup options.
* @param {string} options.id The ID of the popup.
* @param {string} options.title The title of the popup.
* @param {string} options.content The content of the popup.
* @param {Array} options.columns The columns of the popup.
* @param {string} options.columns.title The title of the column.
* @param {string} options.columns.content The content of the column.
*/
const createWelcomePopup = (options = {}) => {
if (! (options && options.id && options.title && options.content)) {
return;
}
if (! isLoggedIn()) {
return;
}
const hasSeenWelcome = getSetting('has-seen-welcome', false, options.id);
if (hasSeenWelcome) {
return;
}
addStyles(`#overlayPopup.mh-welcome .jsDialog.top,
#overlayPopup.mh-welcome .jsDialog.bottom,
#overlayPopup.mh-welcome .jsDialog.background {
padding: 0;
margin: 0;
background: none;
}
#overlayPopup.mh-welcome .jsDialogContainer .prefix,
#overlayPopup.mh-welcome .jsDialogContainer .content {
padding: 0;
}
#overlayPopup.mh-welcome #jsDialogClose,
#overlayPopup.mh-welcome .jsDialogContainer .suffix {
display: none;
}
#overlayPopup.mh-welcome .jsDialogContainer {
padding: 0 20px;
background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_border.png);
background-repeat: repeat-y;
background-size: 100%;
}
#overlayPopup.mh-welcome .jsDialogContainer::before {
position: absolute;
top: -80px;
right: 0;
left: 0;
height: 100px;
content: '';
background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_header.png);
background-repeat: no-repeat;
background-size: 100%;
}
#overlayPopup.mh-welcome .jsDialogContainer::after {
position: absolute;
top: 100%;
right: 0;
left: 0;
height: 126px;
content: '';
background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_footer.png);
background-repeat: no-repeat;
background-size: 100%;
}
.mh-welcome .mh-title {
position: relative;
top: -90px;
display: flex;
align-items: center;
justify-content: center;
width: 412px;
height: 90px;
margin: 20px auto 0;
font-family: Georgia, serif;
font-size: 26px;
font-weight: 700;
color: #7d3b0a;
text-align: center;
text-shadow: 1px 1px 1px #e9d5a2;
background: url(https://www.mousehuntgame.com/images/ui/larry_gifts/ribbon.png?asset_cache_version=2) no-repeat;
}
.mh-welcome .mh-inner-wrapper {
display: flex;
padding: 5px 10px 25px;
margin-top: -90px;
}
.mh-welcome .text {
margin-left: 30px;
line-height: 18px;
text-align: left;
}
.mh-welcome .text p {
font-size: 13px;
line-height: 19px;
}
.mh-welcome .mh-inner-title {
padding: 10px 0;
font-size: 1.5em;
font-weight: 700;
}
.mh-welcome .mh-button-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.mh-welcome .mh-button {
padding: 10px 50px;
font-size: 1.5em;
color: #000;
background: linear-gradient(to bottom, #fff600, #f4e830);
border: 1px solid #000;
border-radius: 5px;
box-shadow: 0 0 10px 1px #d6d13b inset;
}
.mh-welcome .mh-intro-text {
margin: 2em 1em;
font-size: 15px;
line-height: 25px;
}
.mh-welcome-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2em;
margin: 1em;
-ms-grid-columns: 1fr 2em 1fr;
}
.mh-welcome-column h2 {
margin-bottom: 1em;
font-size: 16px;
color: #7d3b0a;
border-bottom: 1px solid #cba36d;
}
.mh-welcome-column ul {
margin-left: 3em;
list-style: disc;
}
`, 'mh-welcome', true);
const markup = `<div class="mh-welcome">
<h1 class="mh-title">${options.title}</h1>
<div class="mh-inner-wrapper">
<div class="text">
<div class="mh-intro-text">
${options.content}
</div>
<div class="mh-welcome-columns">
${options.columns.map((column) => `<div class="mh-welcome-column">
<h2>${column.title}</h2>
${column.content}
</div>`).join('')}
</div>
</div>
</div>
<div class="mh-button-wrapper">
<a href="#" id="mh-welcome-${options.id}-continue" class="mh-button">Continue</a>
</div>
</div>`;
// Initiate the popup.
const welcomePopup = createPopup({
hasCloseButton: false,
template: 'ajax',
content: markup,
show: false,
});
// Set more of our tokens.
welcomePopup.addToken('{*prefix*}', '');
welcomePopup.addToken('{*suffix*}', '');
// Set the attribute and show the popup.
welcomePopup.setAttributes({ className: `mh-welcome mh-welcome-popup-${options.id}` });
// If we want to show the popup, show it.
welcomePopup.show();
// Add the event listener to the continue button.
const continueButton = document.getElementById(`mh-welcome-${options.id}-continue`);
continueButton.addEventListener('click', () => {
saveSetting('has-seen-welcome', true, options.id);
welcomePopup.hide();
});
};
/**
* Create a popup with the larry's office style.
*
* @param {string} content Content to display in the popup.
* @param {Array} classes Classes to add to the popup.
*/
const createLarryPopup = (content, classes = []) => {
const message = {
content: { body: content },
css_class: ['larryOffice', ...classes].join(' '),
show_overlay: true,
is_modal: true
};
hg.views.MessengerView.addMessage(message);
hg.views.MessengerView.go();
};
/**
* Add a popup similar to the larry's gift popup.
*
* createPaperPopup({
* title: 'Whoa! A popup!',
* content: {
* title: 'This is the title of the content',
* text: 'This is some text for the content Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quid ergo hoc loco intellegit honestum? Dicimus aliquem hilare vivere; Cui Tubuli nomen odio non est? Duo Reges: constructio interrete. Sed venio ad inconstantiae crimen, ne saepius dicas me aberrare; Aliena dixit in physicis nec ea ipsa, quae tibi probarentur;',
* image: 'https://api.mouse.rip/hunter/trap/8209591.png',
* },
* button: {
* text: 'A button',
* href: '#',
* },
* show: true,
* });
*
* @param {Object} options The popup options.
* @param {string} options.title The title of the popup.
* @param {Object} options.content The content of the popup.
* @param {string} options.content.title The title of the popup.
* @param {string} options.content.text The text of the popup.
* @param {string} options.content.image The image of the popup.
* @param {Array} options.button The button of the popup.
* @param {string} options.button.text The text of the button.
* @param {string} options.button.href The url of the button.
* @param {boolean} options.show Whether to show the popup or not.
*/
const createPaperPopup = (options) => {
// If we don't have jsDialog, bail.
if ('undefined' === typeof jsDialog || ! jsDialog) { // eslint-disable-line no-undef
return;
}
// Add the styles for our popup.
addStyles(`#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialog.top,
#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialog.bottom,
#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialog.background {
padding: 0;
margin: 0;
background: none;
}
#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer .prefix,
#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer .content {
padding: 0;
}
#overlayPopup.mh-paper-popup-dialog-wrapper #jsDialogClose,
#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer .suffix {
display: none;
}
#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer {
padding: 0 20px;
background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_border.png);
background-repeat: repeat-y;
background-size: 100%;
}
#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer::before {
position: absolute;
top: -80px;
right: 0;
left: 0;
height: 100px;
content: '';
background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_header.png);
background-repeat: no-repeat;
background-size: 100%;
}
#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer::after {
position: absolute;
top: 100%;
right: 0;
left: 0;
height: 126px;
content: '';
background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_footer.png);
background-repeat: no-repeat;
background-size: 100%;
}
.mh-paper-popup-dialog-wrapper .mh-title {
position: relative;
top: -40px;
display: flex;
align-items: center;
justify-content: center;
width: 412px;
height: 99px;
margin: 20px auto 0;
font-family: Georgia, serif;
font-size: 34px;
font-weight: 700;
color: #7d3b0a;
text-align: center;
text-shadow: 1px 1px 1px #e9d5a2;
background: url(https://www.mousehuntgame.com/images/ui/larry_gifts/ribbon.png?asset_cache_version=2) no-repeat;
}
.mh-paper-popup-dialog-wrapper .mh-inner-wrapper {
display: flex;
padding: 5px 10px 25px;
}
.mh-paper-popup-dialog-wrapper .mh-inner-image-wrapper {
position: relative;
padding: 10px;
margin: 0 auto 10px;
background: #f7e3af;
border-radius: 10px;
box-shadow: 0 3px 10px #bd7d3c;
}
.mh-paper-popup-dialog-wrapper .mh-inner-image {
width: 200px;
height: 200px;
background-color: #f5edd7;
border-radius: 5px;
box-shadow: 0 0 100px #6c340b inset;
}
.mh-paper-popup-dialog-wrapper .mh-inner-text {
margin-left: 30px;
line-height: 18px;
text-align: left;
}
.mh-paper-popup-dialog-wrapper .mh-inner-title {
padding: 10px 0;
font-size: 1.5em;
font-weight: 700;
}
.mh-paper-popup-dialog-wrapper .mh-button-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.mh-paper-popup-dialog-wrapper .mh-button {
padding: 10px 50px;
font-size: 1.5em;
color: #000;
background: linear-gradient(to bottom, #fff600, #f4e830);
border: 1px solid #000;
border-radius: 5px;
box-shadow: 0 0 10px 1px #d6d13b inset;
}
`);
// Default to sensible values.
const settings = Object.assign({}, {
title: '',
content: {
title: '',
text: '',
image: '',
},
button: {
text: '',
href: '',
},
show: true,
className: '',
}, options);
// Build the markup with our content.
const markup = `<div class="mh-paper-popup-wrapper">
<div class="mh-title">${settings.title}</div>
<div class="mh-inner-wrapper">
<div class="mh-inner-image-wrapper">
<img class="mh-inner-image" src="${settings.content.image}" />
</div>
<div class="mh-inner-text">
<div class="mh-inner-title">${settings.content.title}</div>
<p>${settings.content.text}</p>
</div>
</div>
<div class="mh-button-wrapper">
<a href="${settings.button.href}" class="mh-button">${settings.button.text}</a>
</div>
</div>`;
// Initiate the popup.
const popup = createPopup({
hasCloseButton: false,
template: 'ajax',
content: markup,
show: false,
});
// Set more of our tokens.
popup.addToken('{*prefix*}', '');
popup.addToken('{*suffix*}', '');
// Set the attribute and show the popup.
popup.setAttributes({ className: `mh-paper-popup-dialog-wrapper ${settings.className}` });
// If we want to show the popup, show it.
if (settings.show) {
popup.show();
}
return popup;
};
/**
* Show a message in the horn dialog.
*
* Type can be one of these: bait_empty unknown_error bait_disarmed recent_turn recent_linked_turn puzzle
*
* @param {Object} options Options for the message.
* @param {string} options.title Title of the message. Keep it under 50 characters.
* @param {string} options.text Text of the message. Keep it under 90 characters.
* @param {string} options.button Text of the button.
* @param {Function} options.action Callback for the button.
* @param {number} options.dismiss Time to dismiss the message.
* @param {string} options.type Type of the message.
* @param {string} options.classname Classname of the message.
*/
const showHornMessage = (options) => {
const huntersHornView = document.querySelector('.huntersHornView__messageContainer');
if (! huntersHornView) {
return;
}
const settings = {
title: options.title || 'Hunters Horn',
text: options.text || 'This is a message from the Hunters Horn',
button: options.button || 'OK',
action: options.action || (() => { }),
dismiss: options.dismiss || null,
type: options.type || 'recent_linked_turn',
classname: options.classname || '',
image: options.image || null,
imageLink: options.imageLink || null,
imageCallback: options.imageCallback || null,
};
// do the other effects
const backdrop = document.querySelector('.huntersHornView__backdrop');
if (backdrop) {
backdrop.classList.add('huntersHornView__backdrop--active');
}
const gameInfo = document.querySelector('.mousehuntHud-gameInfo');
if (gameInfo) {
gameInfo.classList.add('blur');
}
const messageWrapper = makeElement('div', ['huntersHornView__message huntersHornView__message--active', settings.classname]);
const message = makeElement('div', ['huntersHornMessageView', `huntersHornMessageView--${settings.type}`]);
makeElement('div', 'huntersHornMessageView__title', settings.title, message);
const content = makeElement('div', 'huntersHornMessageView__content');
if (settings.image) {
const imgWrapper = makeElement('div', 'huntersHornMessageView__friend');
const img = makeElement('a', 'huntersHornMessageView__friendProfilePic');
if (settings.imageLink) {
img.href = settings.imageLink;
} else if (settings.imageCallback) {
img.addEventListener('click', settings.imageCallback);
} else {
img.href = '#';
}
img.style.backgroundImage = `url(${settings.image})`;
imgWrapper.appendChild(img);
content.appendChild(imgWrapper);
}
makeElement('div', 'huntersHornMessageView__text', settings.text, content);
const buttonSpacer = makeElement('div', 'huntersHornMessageView__buttonSpacer');
const button = makeElement('button', 'huntersHornMessageView__action');
const buttonLabel = makeElement('div', 'huntersHornMessageView__actionLabel');
makeElement('span', 'huntersHornMessageView__actionText', settings.button, buttonLabel);
button.appendChild(buttonLabel);
button.addEventListener('click', () => {
if (settings.action) {
settings.action();
}
messageWrapper.innerHTML = '';
backdrop.classList.remove('huntersHornView__backdrop--active');
gameInfo.classList.remove('blur');
});
buttonSpacer.appendChild(button);
content.appendChild(buttonSpacer);
message.appendChild(content);
if (settings.dismiss) {
const countdown = makeElement('button', ['huntersHornMessageView__countdown']);
makeElement('div', 'huntersHornMessageView__countdownButtonImage', '', countdown);
const svgMarkup = `<svg class="huntersHornMessageView__countdownSVG">
<circle r="46%" cx="50%" cy="50%" class="huntersHornMessageView__countdownCircleTrack"></circle>
<circle r="46%" cx="50%" cy="50%" class="huntersHornMessageView__countdownCircle" style="animation-duration: ${settings.dismiss}ms;"></circle>
</svg>`;
countdown.innerHTML += svgMarkup;
message.appendChild(countdown);
}
messageWrapper.appendChild(message);
// remove any existing messages
const existingMessages = huntersHornView.querySelector('.huntersHornView__message');
if (existingMessages) {
existingMessages.remove();
}
huntersHornView.appendChild(messageWrapper);
if (settings.dismiss) {
setTimeout(() => {
const countdown = messageWrapper.querySelector('.huntersHornMessageView__countdown');
if (countdown) {
countdown.classList.add('huntersHornMessageView__countdown--complete');
}
messageWrapper.innerHTML = '';
backdrop.classList.remove('huntersHornView__backdrop--active');
gameInfo.classList.remove('blur');
}, settings.dismiss);
}
};
const toggleHornDom = (verb = 'remove') => {
const els = [
{
selector: '.huntersHornView__horn',
class: 'huntersHornView__horn--active',
},
{
selector: '.huntersHornView__backdrop',
class: 'huntersHornView__backdrop--active',
},
{
selector: '.huntersHornView__message',
class: 'huntersHornView__message--active',
},
{
selector: '.mousehuntHud-environmentName',
class: 'blur'
},
{
selector: '.mousehuntHud-gameInfo',
class: 'blur'
},
{
selector: '.huntersHornView__horn',
class: 'huntersHornView__horn--hide'
},
{
selector: '.huntersHornView__backdrop',
class: 'huntersHornView__backdrop--active'
},
{
selector: '.huntersHornView__message',
class: 'huntersHornView__message--active'
},
];
els.forEach((el) => {
const dom = document.querySelector(el.selector);
if (dom) {
dom.classList[verb](el.class);
}
}
);
return document.querySelector('.huntersHornView__message');
};
/**
* TODO: document this
*
* @param {*} message
*/
const showHuntersHornMessage = (message) => {
const defaultValues = {
callback: null,
countdown: null,
actionText: null,
};
message = Object.assign(defaultValues, message);
// if the callback was passed in, we need to wrap it in a function that will dismiss the message
if (message.callback) {
const originalCallback = message.callback;
message.callback = () => {
originalCallback();
dismissHuntersHornMessage();
};
} else {
message.callback = dismissHuntersHornMessage;
}
const messageDom = toggleHornDom('add');
const messageView = new hg.views.HuntersHornMessageView(message);
messageDom.innerHTML = '';
messageDom.appendChild(messageView.render()[0]);
};
/**
* TODO: document this
*/
const dismissHuntersHornMessage = () => {
toggleHornDom('remove');
};
/**
* Make an element draggable. Saves the position to local storage.
*
* @param {string} dragTarget The selector for the element that should be dragged.
* @param {string} dragHandle The selector for the element that should be used to drag the element.
* @param {number} defaultX The default X position.
* @param {number} defaultY The default Y position.
* @param {string} storageKey The key to use for local storage.
* @param {boolean} savePosition Whether or not to save the position to local storage.
*/
const makeElementDraggable = (dragTarget, dragHandle, defaultX = null, defaultY = null, storageKey = null, savePosition = true) => {
const modal = document.querySelector(dragTarget);
if (! modal) {
return;
}
const handle = document.querySelector(dragHandle);
if (! handle) {
return;
}
/**
* Make sure the coordinates are within the bounds of the window.
*
* @param {string} type The type of coordinate to check.
* @param {number} value The value of the coordinate.
*
* @return {number} The value of the coordinate, or the max/min value if it's out of bounds.
*/
const keepWithinLimits = (type, value) => {
if ('top' === type) {
return value < -20 ? -20 : value;
}
if (value < (handle.offsetWidth * -1) + 20) {
return (handle.offsetWidth * -1) + 20;
}
if (value > document.body.clientWidth - 20) {
return document.body.clientWidth - 20;
}
return value;
};
/**
* When the mouse is clicked, add the class and event listeners.
*
* @param {Object} e The event object.
*/
const onMouseDown = (e) => {
e.preventDefault();
setTimeout(() => {
// Get the current mouse position.
x1 = e.clientX;
y1 = e.clientY;
// Add the class to the element.
modal.classList.add('mh-is-dragging');
// Add the onDrag and finishDrag events.
document.onmousemove = onDrag;
document.onmouseup = finishDrag;
}, 50);
};
/**
* When the drag is finished, remove the dragging class and event listeners, and save the position.
*/
const finishDrag = () => {
document.onmouseup = null;
document.onmousemove = null;
// Remove the class from the element.
modal.classList.remove('mh-is-dragging');
if (storageKey) {
localStorage.setItem(storageKey, JSON.stringify({ x: modal.offsetLeft, y: modal.offsetTop }));
}
};
/**
* When the mouse is moved, update the element's position.
*
* @param {Object} e The event object.
*/
const onDrag = (e) => {
e.preventDefault();
// Calculate the new cursor position.
x2 = x1 - e.clientX;
y2 = y1 - e.clientY;
x1 = e.clientX;
y1 = e.clientY;
const newLeft = keepWithinLimits('left', modal.offsetLeft - x2);
const newTop = keepWithinLimits('top', modal.offsetTop - y2);
// Set the element's new position.
modal.style.left = `${newLeft}px`;
modal.style.top = `${newTop}px`;
};
// Set the default position.
let startX = defaultX || 0;
let startY = defaultY || 0;
// If the storageKey was passed in, get the position from local storage.
if (! storageKey) {
storageKey = `mh-draggable-${dragTarget}-${dragHandle}`;
}
if (savePosition) {
const storedPosition = localStorage.getItem(storageKey);
if (storedPosition) {
const position = JSON.parse(storedPosition);
// Make sure the position is within the bounds of the window.
startX = keepWithinLimits('left', position.x);
startY = keepWithinLimits('top', position.y);
}
}
// Set the element's position.
modal.style.left = `${startX}px`;
modal.style.top = `${startY}px`;
// Set up our variables to track the mouse position.
let x1 = 0,
y1 = 0,
x2 = 0,
y2 = 0;
// Add the event listener to the handle.
handle.onmousedown = onMouseDown;
};
const makeDraggableModal = (opts) => {
const {
id,
title,
content,
defaultX,
defaultY,
storageKey,
savePosition,
} = opts;
// set the defaults for the options
opts = Object.assign({
id: 'mh-utils-modal',
title: '',
content: '',
defaultX: null,
defaultY: null,
storageKey: 'mh-utils-modal',
savePosition: true,
}, opts);
// Remove the existing modal.
const existing = document.getElementById(`mh-utils-modal-${id}`);
if (existing) {
existing.remove();
}
// Create the modal.
const modalWrapper = makeElement('div', 'mh-utils-modal-wrapper');
modalWrapper.id = `mh-utils-modal-${id}`;
const modal = makeElement('div', 'mh-utils-modal');
const header = makeElement('div', 'mh-utils-modal-header');
makeElement('h1', 'mh-utils-modal-title', title, header);
// Create a close button icon.
const closeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
closeIcon.classList.add('mh-utils-modal-close');
closeIcon.setAttribute('viewBox', '0 0 24 24');
closeIcon.setAttribute('width', '18');
closeIcon.setAttribute('height', '18');
closeIcon.setAttribute('fill', 'none');
closeIcon.setAttribute('stroke', 'currentColor');
closeIcon.setAttribute('stroke-width', '1.5');
// Create the path.
const closePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
closePath.setAttribute('d', 'M18 6L6 18M6 6l12 12');
closeIcon.appendChild(closePath);
// Close the modal when the icon is clicked.
closeIcon.addEventListener('click', () => {
modalWrapper.remove();
});
// Append the button.
header.appendChild(closeIcon);
// Add the header to the modal.
modal.appendChild(header);
// Make the mouse stats table.
const mouseBody =
document.createElement('div');
mouseBody.classList.add('mh-utils-modal-body');
modal.appendChild(content);
// Add the modal to the wrapper.
modalWrapper.appendChild(modal);
// Add the wrapper to the body.
document.body.appendChild(modalWrapper);
// Make the modal draggable.
makeElementDraggable(
`mh-utils-modal-${id}`,
'mh-utils-modal',
'mh-utils-modal-header',
defaultX,
defaultY,
storageKey,
savePosition
);
};
/**
* Creates an element with the given tag, classname, text, and appends it to the given element.
*
* @param {string} tag The tag of the element to create.
* @param {string} classes The classes of the element to create.
* @param {string} text The text of the element to create.
* @param {HTMLElement} appendTo The element to append the created element to.
*
* @return {HTMLElement} The created element.
*/
const makeElement = (tag, classes = '', text = '', appendTo = null) => {
const element = document.createElement(tag);
// if classes is an array, join it with a space.
if (Array.isArray(classes)) {
classes = classes.join(' ');
}
element.className = classes;
element.innerHTML = text;
if (appendTo) {
appendTo.appendChild(element);
return appendTo;
}
return element;
};
/**
* Return an anchor element with the given text and href.
*
* @param {string} text Text to use for link.
* @param {string} href URL to link to.
* @param {boolean} tiny Use the tiny button style.
* @param {Array} extraClasses Extra classes to add to the link.
* @param {boolean} encodeAsSpace Encode spaces as %20 instead of _.
*
* @return {string} HTML for link.
*/
const makeButton = (text, href, tiny = true, extraClasses = [], encodeAsSpace = false) => {
href = href.replace(/\s/g, '_');
if (encodeAsSpace) {
href = href.replace(/_/g, '%20');
} else {
href = href.replace(/\s/g, '_');
}
href = href.replace(/\$/g, '_');
return `<a href="${href}" class="mousehuntActionButton ${tiny ? 'tiny' : ''} ${extraClasses.join(' ')}"><span>${text}</span></a>`;
};
/**
* Creates a popup with two choices.
*
* createChoicePopup({
* title: 'Choose your first trap',
* choices: [
* {
* id: 'treasurer_mouse',
* name: 'Treasurer',
* image: 'https://www.mousehuntgame.com/images/mice/medium/bb55034f6691eb5e3423927e507b5ec9.jpg?cv=2',
* meta: 'Mouse',
* text: 'This is a mouse',
* button: 'Select',
* callback: () => {
* console.log('treasurer selected');
* }
* },
* {
* id: 'high_roller_mouse',
* name: 'High Roller',
* image: 'https://www.mousehuntgame.com/images/mice/medium/3f71c32f9d8da2b2727fc8fd288f7974.jpg?cv=2',
* meta: 'Mouse',
* text: 'This is a mouse',
* button: 'Select',
* callback: () => {
* console.log('high roller selected');
* }
* },
* ],
* });
*
* @param {Object} options The options for the popup.
* @param {string} options.title The title of the popup.
* @param {Array} options.choices The choices for the popup.
* @param {string} options.choices[].id The ID of the choice.
* @param {string} options.choices[].name The name of the choice.
* @param {string} options.choices[].image The image of the choice.
* @param {string} options.choices[].meta The smaller text under the name.
* @param {string} options.choices[].text The description of the choice.
* @param {string} options.choices[].button The text of the button.
* @param {string} options.choices[].action The action to take when the button is clicked.
*/
const createChoicePopup = (options) => {
let choices = '';
const numChoices = options.choices.length;
let currentChoice = 0;
options.choices.forEach((choice) => {
choices += `<a href="#" id=${choice.id}" class="weaponContainer">
<div class="weapon">
<div class="trapImage" style="background-image: url(${choice.image});"></div>
<div class="trapDetails">
<div class="trapName">${choice.name}</div>
<div class="trapDamageType">${choice.meta}</div>
<div class="trapDescription">${choice.text}</div>
<div class="trapButton" id="${choice.id}-action">${choice.button || 'Select'}</div>
</div>
</div>
</a>`;
currentChoice++;
if (currentChoice < numChoices) {
choices += '<div class="spacer"></div>';
}
});
const content = `<div class="trapIntro">
<div id="OnboardArrow" class="larryCircle">
<div class="woodgrain">
<div class="whiteboard">${options.title}</div>
</div>
<div class="characterContainer">
<div class="character"></div>
</div>
</div>
</div>
<div>
${choices}
</div>`;
hg.views.MessengerView.addMessage({
content: { body: content },
css_class: 'chooseTrap',
show_overlay: true,
is_modal: true
});
hg.views.MessengerView.go();
options.choices.forEach((choice) => {
const target = document.querySelector(`#${choice.id}-action`);
if (target) {
target.addEventListener('click', () => {
hg.views.MessengerView.hide();
if (choice.action) {
choice.action();
}
});
}
});
};
/**
* Creates a favorite button that can toggle.
*
* @async
*
* @example <caption>Creating a favorite button</caption>
* createFavoriteButton({
* id: 'testing_favorite',
* target: infobar,
* size: 'small',
* defaultState: false,
* });
*
* @param {Object} options The options for the button.
* @param {string} options.selector The selector for the button.
* @param {string} options.size Whether or not to use the small version of the button.
* @param {string} options.active Whether or not the button should be active by default.
* @param {string} options.onChange The function to run when the button is toggled.
* @param {string} options.onActivate The function to run when the button is activated.
* @param {string} options.onDeactivate The function to run when the button is deactivated.
*/
const createFavoriteButton = async (options) => {
addStyles(`.custom-favorite-button {
top: 0;
right: 0;
display: inline-block;
width: 35px;
height: 35px;
vertical-align: middle;
background: url(https://www.mousehuntgame.com/images/ui/camp/trap/star_empty.png?asset_cache_version=2) 50% 50% no-repeat;
background-size: 90%;
border-radius: 50%;
}
.custom-favorite-button-small {
width: 20px;
height: 20px;
}
.custom-favorite-button:hover {
background-color: #fff;
outline: 2px solid #ccc;
background-image: url(https://www.mousehuntgame.com/images/ui/camp/trap/star_favorite.png?asset_cache_version=2);
}
.custom-favorite-button.active {
background-image: url(https://www.mousehuntgame.com/images/ui/camp/trap/star_favorite.png?asset_cache_version=2);
}
.custom-favorite-button.busy {
background-image: url(https://www.mousehuntgame.com/images/ui/loaders/small_spinner.gif?asset_cache_version=2);
}
`, 'custom-favorite-button', true);
const {
id = null,
target = null,
size = 'small',
state = false,
isSetting = true,
defaultState = false,
onChange = null,
onActivate = null,
onDeactivate = null,
} = options;
const star = document.createElement('a');
star.classList.add('custom-favorite-button');
if (size === 'small') {
star.classList.add('custom-favorite-button-small');
}
star.setAttribute('data-item-id', id);
star.setAttribute('href', '#');
star.style.display = 'inline-block';
let currentSetting = false;
if (isSetting) {
currentSetting = getSetting(id, defaultState);
} else {
currentSetting = state;
}
if (currentSetting) {
star.classList.add('active');
} else {
star.classList.add('inactive');
}
star.addEventListener('click', async (e) => {
star.classList.add('busy');
e.preventDefault();
e.stopPropagation();
const currentStar = e.target;
const currentState = ! currentStar.classList.contains('active');
if (onChange !== null) {
await onChange(currentState);
} else if (isSetting) {
saveSetting(id, currentState);
}
currentStar.classList.remove('inactive');
currentStar.classList.remove('active');
if (currentState) {
currentStar.classList.add('active');
if (onActivate !== null) {
await onActivate(currentState);
}
} else {
currentStar.classList.add('inactive');
if (onDeactivate !== null) {
await onDeactivate(currentState);
}
}
currentStar.classList.remove('busy');
});
if (target) {
target.appendChild(star);
}
return star;
};
/**
* Wait for a specified amount of time.
*
* @param {number} ms The number of milliseconds to wait.
*/
const wait = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
/**
* Log to the console.
*
* @param {string|Object} message The message to log.
* @param {Object} args The arguments to pass to the console.
*/
const clog = (message, ...args) => {
// If a string is passed in, log it in line with our prefix.
if ('string' === typeof message) {
console.log(`%c[MH Utils] %c${message}`, 'color: #ff0000; font-weight: bold;', 'color: #000000;'); // eslint-disable-line no-console
console.log(...args); // eslint-disable-line no-console
} else {
// Otherwise, log it separately.
console.log('%c[MH Utils]', 'color: #ff0000; font-weight: bold;'); // eslint-disable-line no-console
console.log(message); // eslint-disable-line no-console
}
};
/**
* Log to the console if debug mode is enabled.
*
* @param {string|Object} message The message to log.
* @param {Object} args The arguments to pass to the console.
*/
const debug = (message, ...args) => {
if (getSetting('debug-mode', false)) {
clog(message, ...args);
}
};
/**
* Add a setting to enable debug mode.
*/
const enableDebugMode = () => {
const debugSettings = {
debugModeEnabled: true,
debug: getSetting('debug-mode', false)
};
window.mhutils = window.mhutils ? { ...window.mhutils, ...debugSettings } : debugSettings;
addSetting('Debug Mode', 'debug-mode', false, 'Enable debug mode', {}, 'game_settings');
};
/**
* Helper to run a callback when loaded, on ajax request, on overlay close, and on travel.
*
* @param {Function} action The callback to run.
*/
const run = async (action) => {
action();
onAjaxRequest(action);
onOverlayClose(action);
onTravel(null, { callback: action });
};
/**
* Check if dark mode is enabled.
*
* @return {boolean} True if dark mode is enabled, false otherwise.
*/
const isDarkMode = () => {
return !! getComputedStyle(document.documentElement).getPropertyValue('--mhdm-white');
};
/**
* Adds classes to the body to enable styling based on the location or if dark mode is enabled.
*/
const addBodyClasses = () => {
const addLocationBodyClass = () => {
const addClass = () => {
const location = getCurrentLocation();
document.body.classList.add(`mh-location-${location}`);
};
addClass();
onTravel(null, { callback: addClass });
};
const addDarkModeBodyClass = () => {
if (isDarkMode()) {
document.body.classList.add('mh-dark-mode');
}
};
addLocationBodyClass();
addDarkModeBodyClass();
};
/**
* Wait for the app to initialize, then add classes to the body.
*/
setTimeout(() => {
addBodyClasses();
eventRegistry.addEventListener('app_init', addBodyClasses);
}, 250);