MouseHunt Utils is a library of functions that can be used to make other MouseHunt userscripts easily.
สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @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: 'Pag###pplyTransfer' }, 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 forc###btab = currentTab.subtabs.find((searchTab) => searchTab.is_active_subtab).subtab_type; if (matchesCurrentPage(page, tab, subtab, getCurrentPage(), forceCurrentTab, forc###btab)) { 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.getCurrentPag###bTab(); 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 r###lt = makeOption(optgroupOption, foundSelected, currentSetting, defaultValue, i); foundSelected = r###lt.foundSelected; settingRowInputDropdownSelectOptgroup.appendChild(r###lt.settingRowInputDropdownSelectOption); }); settingRowInputDropdownSelect.appendChild(settingRowInputDropdownSelectOptgroup); } else { const r###lt = makeOption(option, foundSelected, currentSetting, defaultValue, i); foundSelected = r###lt.foundSelected; settingRowInputDropdownSelect.appendChild(r###lt.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);