🐭️ MouseHunt Utils Beta

Testing version

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/464008/1261903/%F0%9F%90%AD%EF%B8%8F%20MouseHunt%20Utils%20Beta.js

  1. // ==UserScript==
  2. // @name 🐭️ MouseHunt Utils
  3. // @author bradp
  4. // @version 1.10.5
  5. // @description MouseHunt Utils is a library of functions that can be used to make other MouseHunt userscripts easily.
  6. // @license MIT
  7. // @namespace bradp
  8. // @match https://www.mousehuntgame.com/*
  9. // @icon https://i.mouse.rip/mouse.png
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. /* eslint-disable no-unused-vars */
  14.  
  15. /**
  16. * Add styles to the page.
  17. *
  18. * @author bradp
  19. * @since 1.0.0
  20. *
  21. * @example <caption>Basic usage</caption>
  22. * addStyles(`.my-class {
  23. * color: red;
  24. * }`);
  25. *
  26. * @example <caption>With an identifier</caption>
  27. * addStyles(`.my-class {
  28. * display: none;
  29. * }`, 'my-identifier');
  30. *
  31. * @example <caption>With an identifier, but will only add the styles once</caption>
  32. * addStyles(`.my-other-class {
  33. * color: blue;
  34. * }`, 'my-identifier', true);
  35. *
  36. * @param {string} styles The styles to add.
  37. * @param {string} identifier The identifier to use for the style element.
  38. * @param {boolean} once Only add the styles once for the identifier.
  39. *
  40. * @return {Element} The style element.
  41. */
  42. const addStyles = (styles, identifier = 'mh-utils-custom-styles', once = false) => {
  43. identifier = `mh-utils-${identifier}`;
  44.  
  45. // Check to see if the existing element exists.
  46. const existingStyles = document.getElementById(identifier);
  47.  
  48. // If so, append our new styles to the existing element.
  49. if (existingStyles) {
  50. if (once) {
  51. return existingStyles;
  52. }
  53.  
  54. existingStyles.innerHTML += styles;
  55. return existingStyles;
  56. }
  57.  
  58. // Otherwise, create a new element and append it to the head.
  59. const style = document.createElement('style');
  60. style.id = identifier;
  61. style.innerHTML = styles;
  62. document.head.appendChild(style);
  63.  
  64. return style;
  65. };
  66.  
  67. /**
  68. * Do something when ajax requests are completed.
  69. *
  70. * @author bradp
  71. * @since 1.0.0
  72. *
  73. * @example <caption>Basic usage</caption>
  74. * onRequest((response) => {
  75. * console.log(response);
  76. * }, 'managers/ajax/turns/activeturn.php');
  77. *
  78. * @example <caption>Basic usage, but skip the success check</caption>
  79. * onRequest((response) => {
  80. * console.log(response);
  81. * }, 'managers/ajax/turns/activeturn.php', true);
  82. *
  83. * @example <caption>Basic usage, running for all ajax requests</caption>
  84. * onRequest((response) => {
  85. * console.log(response);
  86. * });
  87. *
  88. * @param {Function} callback The callback to call when an ajax request is completed.
  89. * @param {string} url The url to match. If not provided, all ajax requests will be matched.
  90. * @param {boolean} skipSuccess Skip the success check.
  91. */
  92. const onRequest = (callback, url = null, skipSuccess = false) => {
  93. const req = XMLHttpRequest.prototype.open;
  94. XMLHttpRequest.prototype.open = function () {
  95. this.addEventListener('load', function () {
  96. if (this.responseText) {
  97. let response = {};
  98. try {
  99. response = JSON.parse(this.responseText);
  100. } catch (e) {
  101. return;
  102. }
  103.  
  104. if (response.success || skipSuccess) {
  105. if (! url) {
  106. callback(response);
  107. return;
  108. }
  109.  
  110. if (this.responseURL.indexOf(url) !== -1) {
  111. callback(response);
  112. }
  113. }
  114. }
  115. });
  116. req.apply(this, arguments);
  117. };
  118. };
  119.  
  120. const onAjaxRequest = onRequest;
  121.  
  122. /**
  123. * Run the callbacks depending on visibility.
  124. *
  125. * @author bradp
  126. * @since 1.0.0
  127. *
  128. * @ignore
  129. *
  130. * @param {Object} settings Settings object.
  131. * @param {Node} parentNode The parent node.
  132. * @param {Object} callbacks The callbacks to run.
  133. *
  134. * @return {Object} The settings.
  135. */
  136. const runCallbacks = (settings, parentNode, callbacks) => {
  137. // Loop through the keys on our settings object.
  138. Object.keys(settings).forEach((key) => {
  139. // If the parentNode that's passed in contains the selector for the key.
  140. if (parentNode && parentNode.classList && parentNode.classList.contains(settings[ key ].selector)) {
  141. // Set as visible.
  142. settings[ key ].isVisible = true;
  143.  
  144. // If there is a show callback, run it.
  145. if (callbacks[ key ] && callbacks[ key ].show) {
  146. callbacks[ key ].show();
  147. }
  148. } else if (settings[ key ].isVisible) {
  149. // Mark as not visible.
  150. settings[ key ].isVisible = false;
  151.  
  152. // If there is a hide callback, run it.
  153. if (callbacks[ key ] && callbacks[ key ].hide) {
  154. callbacks[ key ].hide();
  155. }
  156. }
  157. });
  158.  
  159. return settings;
  160. };
  161.  
  162. /**
  163. * Do something when the overlay is shown or hidden.
  164. *
  165. * @param {Object} callbacks
  166. * @param {Function} callbacks.show The callback to call when the overlay is shown.
  167. * @param {Function} callbacks.hide The callback to call when the overlay is hidden.
  168. * @param {Function} callbacks.change The callback to call when the overlay is changed.
  169. */
  170. const onOverlayChange = (callbacks) => {
  171. // Track the different overlay states.
  172. let overlayData = {
  173. map: {
  174. isVisible: false,
  175. selector: 'treasureMapPopup'
  176. },
  177. item: {
  178. isVisible: false,
  179. selector: 'itemViewPopup'
  180. },
  181. mouse: {
  182. isVisible: false,
  183. selector: 'mouseViewPopup'
  184. },
  185. image: {
  186. isVisible: false,
  187. selector: 'largerImage'
  188. },
  189. convertible: {
  190. isVisible: false,
  191. selector: 'convertibleOpenViewPopup'
  192. },
  193. adventureBook: {
  194. isVisible: false,
  195. selector: 'adventureBookPopup'
  196. },
  197. marketplace: {
  198. isVisible: false,
  199. selector: 'marketplaceViewPopup'
  200. },
  201. gifts: {
  202. isVisible: false,
  203. selector: 'giftSelectorViewPopup'
  204. },
  205. support: {
  206. isVisible: false,
  207. selector: 'supportPageContactUsForm'
  208. },
  209. premiumShop: {
  210. isVisible: false,
  211. selector: 'MHCheckout'
  212. }
  213. };
  214.  
  215. // Observe the overlayPopup element for changes.
  216. const observer = new MutationObserver(() => {
  217. if (callbacks.change) {
  218. callbacks.change();
  219. }
  220.  
  221. // Grab the overlayPopup element and make sure it has classes on it.
  222. const overlayType = document.getElementById('overlayPopup');
  223. if (overlayType && overlayType.classList.length <= 0) {
  224. return;
  225. }
  226.  
  227. // Grab the overlayBg and check if it is visible or not.
  228. const overlayBg = document.getElementById('overlayBg');
  229. if (overlayBg && overlayBg.classList.length > 0) {
  230. // If there's a show callback, run it.
  231. if (callbacks.show) {
  232. callbacks.show();
  233. }
  234. } else if (callbacks.hide) {
  235. // If there's a hide callback, run it.
  236. callbacks.hide();
  237. }
  238.  
  239. // Run all the specific callbacks.
  240. overlayData = runCallbacks(overlayData, overlayType, callbacks);
  241. });
  242.  
  243. // Observe the overlayPopup element for changes.
  244. const observeTarget = document.getElementById('overlayPopup');
  245. if (observeTarget) {
  246. observer.observe(observeTarget, {
  247. attributes: true,
  248. attributeFilter: ['class']
  249. });
  250. }
  251. };
  252.  
  253. /**
  254. * TODO: update this docblock.
  255. *
  256. * @param {*} callback
  257. */
  258. const onOverlayClose = (callback) => {
  259. eventRegistry.addEventListener('js_dialog_hide', callback);
  260. };
  261.  
  262. /**
  263. * TODO: update this docblock.
  264. */
  265. const getDialogMapping = () => {
  266. return {
  267. treasureMapPopup: 'map',
  268. itemViewPopup: 'item',
  269. mouseViewPopup: 'mouse',
  270. largerImage: 'image',
  271. convertibleOpenViewPopup: 'convertible',
  272. adventureBookPopup: 'adventureBook',
  273. marketplaceViewPopup: 'marketplace',
  274. giftSelectorViewPopup: 'gifts',
  275. supportPageContactUsForm: 'support',
  276. MHCheckout: 'premiumShop',
  277. };
  278. };
  279.  
  280. /**
  281. * TODO: update this docblock.
  282. *
  283. * @param {*} callback
  284. * @param {*} overlay
  285. * @param {*} once
  286. */
  287. const onDialogShow = (callback, overlay = null, once = false) => {
  288. eventRegistry.addEventListener('js_dialog_show', () => {
  289. if (! activejsDialog) {
  290. return;
  291. }
  292.  
  293. // Get all the tokens and check the content.
  294. const tokens = activejsDialog.getAllTokens();
  295.  
  296. // Make sure we have the 'content' key.
  297. // For item and mouse views, the entire event fires twice, once while loading and
  298. // once when the content is loaded. We only want to run this once, so we check if
  299. // the content is empty in a weird way.
  300. if (
  301. ! tokens ||
  302. ! tokens[ '{*content*}' ] ||
  303. ! tokens[ '{*content*}' ].value ||
  304. tokens[ '{*content*}' ].value === '' ||
  305. tokens[ '{*content*}' ].value.indexOf('data-item-type=""') > -1 || // Item view.
  306. tokens[ '{*content*}' ].value.indexOf('data-mouse-id=""') > -1 // Mouse view.
  307. ) {
  308. return;
  309. }
  310.  
  311. // Grab the attributes of the dialog to determine the type.
  312. const atts = activejsDialog.getAttributes();
  313. const dialogType = atts.className
  314. .replace('jsDialogFixed', '')
  315. .replace('wide', '')
  316. .replace('default', '')
  317. .replaceAll(' ', ' ')
  318. .replaceAll(' ', '.')
  319. .trim();
  320.  
  321. // Make sure this only ran once within the last 100ms for the same overlay.
  322. if (window.mhutils?.lastDialog?.overlay === dialogType && (Date.now() - window.mhutils.lastDialog.timestamp) < 250) {
  323. return;
  324. }
  325.  
  326. const lastDialog = {
  327. overlay: dialogType,
  328. timestamp: Date.now(),
  329. };
  330.  
  331. window.mhutils = window.mhutils ? { ...window.mhutils, ...lastDialog } : lastDialog;
  332.  
  333. if (! overlay && 'function' === typeof callback) {
  334. return callback();
  335. }
  336.  
  337. const dialogMapping = getDialogMapping();
  338.  
  339. if ('function' === typeof callback && (overlay === dialogType || overlay === dialogMapping[ dialogType ])) {
  340. return callback();
  341. }
  342. }, null, once);
  343. };
  344.  
  345. /**
  346. * TODO: update this docblock.
  347. *
  348. * @param {*} callback
  349. * @param {*} overlay
  350. * @param {*} once
  351. */
  352. const onDialogHide = (callback, overlay = null, once = false) => {
  353. eventRegistry.addEventListener('js_dialog_hide', () => {
  354. const dialogType = window?.mhutils?.lastDialog?.overlay || null;
  355. window.mhutils.lastDialog = {};
  356.  
  357. if (! overlay) {
  358. return callback();
  359. }
  360.  
  361. const dialogMapping = getDialogMapping();
  362. if (overlay === dialogType || overlay === dialogMapping[ dialogType ]) {
  363. return callback();
  364. }
  365. }, null, once);
  366. };
  367.  
  368. /**
  369. * Do something when the page or tab changes.
  370. *
  371. * @param {Object} callbacks
  372. * @param {Function} callbacks.show The callback to call when the page is navigated to.
  373. * @param {Function} callbacks.hide The callback to call when the page is navigated away from.
  374. * @param {Function} callbacks.change The callback to call when the page is changed.
  375. */
  376. const onPageChange = (callbacks) => {
  377. // Track our page tab states.
  378. let tabData = {
  379. blueprint: { isVisible: null, selector: 'showBlueprint' },
  380. tem: { isVisible: false, selector: 'showTrapEffectiveness' },
  381. trap: { isVisible: false, selector: 'editTrap' },
  382. camp: { isVisible: false, selector: 'PageCamp' },
  383. travel: { isVisible: false, selector: 'PageTravel' },
  384. inventory: { isVisible: false, selector: 'PageInventory' },
  385. shop: { isVisible: false, selector: 'PageShops' },
  386. mice: { isVisible: false, selector: 'PageAdversaries' },
  387. friends: { isVisible: false, selector: 'PageFriends' },
  388. sendSupplies: { isVisible: false, selector: 'PageSupplyTransfer' },
  389. team: { isVisible: false, selector: 'PageTeam' },
  390. tournament: { isVisible: false, selector: 'PageTournament' },
  391. news: { isVisible: false, selector: 'PageNews' },
  392. scoreboards: { isVisible: false, selector: 'PageScoreboards' },
  393. discord: { isVisible: false, selector: 'PageJoinDiscord' },
  394. preferences: { isVisible: false, selector: 'PagePreferences' },
  395. profile: { isVisible: false, selector: 'HunterProfile' },
  396. };
  397.  
  398. // Observe the mousehuntContainer element for changes.
  399. const observer = new MutationObserver(() => {
  400. // If there's a change callback, run it.
  401. if (callbacks.change) {
  402. callbacks.change();
  403. }
  404.  
  405. // Grab the container element and make sure it has classes on it.
  406. const mhContainer = document.getElementById('mousehuntContainer');
  407. if (mhContainer && mhContainer.classList.length > 0) {
  408. // Run the callbacks.
  409. tabData = runCallbacks(tabData, mhContainer, callbacks);
  410. }
  411. });
  412.  
  413. // Observe the mousehuntContainer element for changes.
  414. const observeTarget = document.getElementById('mousehuntContainer');
  415. if (observeTarget) {
  416. observer.observe(observeTarget, {
  417. attributes: true,
  418. attributeFilter: ['class']
  419. });
  420. }
  421. };
  422.  
  423. /**
  424. * Do something when the trap tab is changed.
  425. *
  426. * @param {Object} callbacks
  427. */
  428. const onTrapChange = (callbacks) => {
  429. // Track our trap states.
  430. let trapData = {
  431. bait: {
  432. isVisible: false,
  433. selector: 'bait'
  434. },
  435. base: {
  436. isVisible: false,
  437. selector: 'base'
  438. },
  439. weapon: {
  440. isVisible: false,
  441. selector: 'weapon'
  442. },
  443. charm: {
  444. isVisible: false,
  445. selector: 'trinket'
  446. },
  447. skin: {
  448. isVisible: false,
  449. selector: 'skin'
  450. }
  451. };
  452.  
  453. // Observe the trapTabContainer element for changes.
  454. const observer = new MutationObserver(() => {
  455. // Fire the change callback.
  456. if (callbacks.change) {
  457. callbacks.change();
  458. }
  459.  
  460. // If we're not viewing a blueprint tab, bail.
  461. const mhContainer = document.getElementById('mousehuntContainer');
  462. if (mhContainer.classList.length <= 0 || ! mhContainer.classList.contains('showBlueprint')) {
  463. return;
  464. }
  465.  
  466. // If we don't have the container, bail.
  467. const trapContainerParent = document.querySelector('.campPage-trap-blueprintContainer');
  468. if (! trapContainerParent || ! trapContainerParent.children || ! trapContainerParent.children.length > 0) {
  469. return;
  470. }
  471.  
  472. // If we're not in the itembrowser, bail.
  473. const trapContainer = trapContainerParent.children[ 0 ];
  474. if (! trapContainer || trapContainer.classList.length <= 0 || ! trapContainer.classList.contains('campPage-trap-itemBrowser')) {
  475. return;
  476. }
  477.  
  478. // Run the callbacks.
  479. trapData = runCallbacks(trapData, trapContainer, callbacks);
  480. });
  481.  
  482. // Grab the campPage-trap-blueprintContainer element and make sure it has children on it.
  483. const observeTargetParent = document.querySelector('.campPage-trap-blueprintContainer');
  484. if (! observeTargetParent || ! observeTargetParent.children || ! observeTargetParent.children.length > 0) {
  485. return;
  486. }
  487.  
  488. // Observe the first child of the campPage-trap-blueprintContainer element for changes.
  489. const observeTarget = observeTargetParent.children[ 0 ];
  490. if (observeTarget) {
  491. observer.observe(observeTarget, {
  492. attributes: true,
  493. attributeFilter: ['class']
  494. });
  495. }
  496. };
  497.  
  498. /**
  499. * Add something to the event registry.
  500. *
  501. * @param {string} event The event name.
  502. * @param {Function} callback The callback to run when the event is fired.
  503. * @param {boolean} remove Whether or not to remove the event listener after it's fired.
  504. */
  505. const onEvent = (event, callback, remove = false) => {
  506. eventRegistry.addEventListener(event, callback, null, remove);
  507. };
  508.  
  509. /**
  510. * Do something when the user travels to a location.
  511. *
  512. * @param {string} location The location traveled to.
  513. * @param {Object} options The options
  514. * @param {string} options.shouldAddReminder Whether or not to add a reminder.
  515. * @param {string} options.title The title of the reminder.
  516. * @param {string} options.text The text of the reminder.
  517. * @param {string} options.button The button text of the reminder.
  518. * @param {string} options.action The action to take when the button is clicked.
  519. * @param {string} options.callback The callback to run when the user is at the location.
  520. */
  521. const onTravel = (location, options) => {
  522. eventRegistry.addEventListener('travel_complete', () => onTravelCallback(location, options));
  523. };
  524.  
  525. /**
  526. * Do something when the user travels to a location.
  527. * This is a callback for the onTravel function.
  528. *
  529. * @param {string} location The location traveled to.
  530. * @param {Object} options The options
  531. * @param {string} options.shouldAddReminder Whether or not to add a reminder.
  532. * @param {string} options.title The title of the reminder.
  533. * @param {string} options.text The text of the reminder.
  534. * @param {string} options.button The button text of the reminder.
  535. * @param {string} options.action The action to take when the button is clicked.
  536. * @param {string} options.callback The callback to run when the user is at the location.
  537. *
  538. */
  539. const onTravelCallback = (location, options) => {
  540. if (location && location !== getCurrentLocation()) {
  541. return;
  542. }
  543.  
  544. if (options?.shouldAddReminder) {
  545. showHornMessage({
  546. title: options.title || '',
  547. text: options.text || '',
  548. button: options.button || 'Dismiss',
  549. action: options.action || null,
  550. });
  551. }
  552.  
  553. if (options.callback) {
  554. options.callback();
  555. }
  556. };
  557.  
  558. /**
  559. * TODO: update this docblock.
  560. *
  561. * @param {string} targetPage The target page.
  562. * @param {string} targetTab The target tab.
  563. * @param {string} targetSubtab The target subtab.
  564. * @param {string} forceCurrentPage The current page.
  565. * @param {string} forceCurrentTab The current tab.
  566. * @param {string} forceCurrentSubtab The current subtab.
  567. */
  568. const matchesCurrentPage = (targetPage = null, targetTab = null, targetSubtab = null, forceCurrentPage = null, forceCurrentTab = null, forceCurrentSubtab = null) => {
  569. if (! targetPage) {
  570. return false;
  571. }
  572.  
  573. // Only targetPage is being checked.
  574. const currentPage = forceCurrentPage || getCurrentPage();
  575. if (! targetTab) {
  576. return currentPage === targetPage;
  577. }
  578.  
  579. // Only targetTab is being checked.
  580. const currentTab = forceCurrentTab || getCurrentTab();
  581. if (! targetSubtab) {
  582. return currentPage === targetPage && currentTab === targetTab;
  583. }
  584.  
  585. // Only targetSubtab is being checked.
  586. const currentSubtab = forceCurrentSubtab || getCurrentSubtab();
  587. if (currentSubtab === currentTab) {
  588. return currentPage === targetPage && currentTab === targetTab;
  589. }
  590.  
  591. return currentPage === targetPage && currentTab === targetTab && currentSubtab === targetSubtab;
  592. };
  593.  
  594. /*
  595. onNavigation(() => console.log('mouse stats by location'),
  596. {
  597. page: 'adversaries',
  598. tab: 'your_stats',
  599. subtab: 'location'
  600. }
  601. );
  602.  
  603. onNavigation(() => console.log('friend request page'),
  604. {
  605. page:'friends',
  606. tab: 'requests'
  607. }
  608. );
  609.  
  610. onNavigation(() => console.log('hunter profile, but not when refreshing the page'),
  611. {
  612. page: 'hunterprofile',
  613. onLoad: true
  614. }
  615. );
  616. */
  617.  
  618. /**
  619. * TODO: update this docblock
  620. *
  621. * @param {Function} callback The callback to run when the user navigates to the page.
  622. * @param {Object} options The options
  623. * @param {string} options.page The page to watch for.
  624. * @param {string} options.tab The tab to watch for.
  625. * @param {string} options.subtab The subtab to watch for.
  626. * @param {boolean} options.onLoad Whether or not to run the callback on load.
  627. */
  628. const onNavigation = (callback, options = {}) => {
  629. const defaults = {
  630. page: false,
  631. tab: false,
  632. subtab: false,
  633. onLoad: false,
  634. };
  635.  
  636. // merge the defaults with the options
  637. const { page, tab, subtab, onLoad } = Object.assign(defaults, options);
  638.  
  639. // If we don't pass in a page, then we want to run the callback on every page.
  640. let bypassMatch = false;
  641. if (! page) {
  642. bypassMatch = true;
  643. }
  644.  
  645. // We do this once on load in case we are starting on the page we want to watch for.
  646. if (onLoad) {
  647. if (bypassMatch || matchesCurrentPage(page, tab, subtab)) {
  648. callback();
  649. }
  650. }
  651.  
  652. eventRegistry.addEventListener('set_page', (e) => {
  653. const tabs = e?.data?.tabs || {};
  654.  
  655. const currentTab = Object.keys(tabs).find((key) => tabs[ key ].is_active_tab);
  656. const forceCurrentTab = currentTab?.type;
  657.  
  658. if (! subtab) {
  659. if (matchesCurrentPage(page, tab, false, getCurrentPage(), forceCurrentTab)) {
  660. callback();
  661. }
  662.  
  663. return;
  664. }
  665.  
  666. if (currentTab?.subtabs && currentTab?.subtabs.length > 0) {
  667. const forceSubtab = currentTab.subtabs.find((searchTab) => searchTab.is_active_subtab).subtab_type;
  668.  
  669. if (matchesCurrentPage(page, tab, subtab, getCurrentPage(), forceCurrentTab, forceSubtab)) {
  670. callback();
  671. }
  672. }
  673. });
  674.  
  675. eventRegistry.addEventListener('set_tab', (e) => {
  676. const forceCurrentTab = e.page_arguments.tab;
  677. const forceCurrentSubtab = e.page_arguments.sub_tab;
  678.  
  679. if (matchesCurrentPage(page, tab, subtab, getCurrentPage(), forceCurrentTab, forceCurrentSubtab)) {
  680. callback();
  681. }
  682. });
  683. };
  684.  
  685. const onNavigate = onNavigation;
  686.  
  687. /**
  688. * Get the current page slug.
  689. *
  690. * @return {string} The page slug.
  691. */
  692. const getCurrentPage = () => {
  693. return hg.utils.PageUtil.getCurrentPage().toLowerCase(); // eslint-disable-line no-undef
  694. };
  695.  
  696. /**
  697. * Get the current page tab, defaulting to the current page if no tab is found.
  698. *
  699. * @return {string} The page tab.
  700. */
  701. const getCurrentTab = () => {
  702. const tab = hg.utils.PageUtil.getCurrentPageTab().toLowerCase(); // eslint-disable-line no-undef
  703. if (tab.length <= 0) {
  704. return getCurrentPage();
  705. }
  706.  
  707. return tab;
  708. };
  709.  
  710. /**
  711. * Get the current page sub tab, defaulting to the current tab if no sub tab is found.
  712. *
  713. * @return {string} The page tab.
  714. */
  715. const getCurrentSubtab = () => {
  716. const subtab = hg.utils.PageUtil.getCurrentPageSubTab();
  717. if (! subtab || subtab.length <= 0) {
  718. return getCurrentTab();
  719. }
  720.  
  721. return subtab.toLowerCase();
  722. };
  723.  
  724. // Backwards compatibility.
  725. const getCurrentSubTab = getCurrentSubtab;
  726.  
  727. /**
  728. * Check if the overlay is visible.
  729. *
  730. * @return {boolean} True if the overlay is visible, false otherwise.
  731. */
  732. const isOverlayVisible = () => {
  733. return activejsDialog && activejsDialog.isVisible();
  734. };
  735.  
  736. /**
  737. * Get the current overlay.
  738. *
  739. * @return {string} The current overlay.
  740. */
  741. const getCurrentOverlay = () => {
  742. const overlay = document.getElementById('overlayPopup');
  743. if (overlay && overlay.classList.length <= 0) {
  744. return null;
  745. }
  746.  
  747. let overlayType = overlay.classList.value;
  748. overlayType = overlayType.replace('jsDialogFixed', '');
  749. overlayType = overlayType.replace('default', '');
  750. overlayType = overlayType.replace('wide', '');
  751. overlayType = overlayType.replace('ajax', '');
  752. overlayType = overlayType.replace('overlay', '');
  753.  
  754. // Replace some overlay types with more readable names.
  755. overlayType = overlayType.replace('treasureMapPopup', 'map');
  756. overlayType = overlayType.replace('itemViewPopup', 'item');
  757. overlayType = overlayType.replace('mouseViewPopup', 'mouse');
  758. overlayType = overlayType.replace('largerImage', 'image');
  759. overlayType = overlayType.replace('convertibleOpenViewPopup', 'convertible');
  760. overlayType = overlayType.replace('adventureBookPopup', 'adventureBook');
  761. overlayType = overlayType.replace('marketplaceViewPopup', 'marketplace');
  762. overlayType = overlayType.replace('giftSelectorViewPopup', 'gifts');
  763. overlayType = overlayType.replace('supportPageContactUsForm', 'support');
  764. overlayType = overlayType.replace('MHCheckout', 'premiumShop');
  765.  
  766. return overlayType.trim();
  767. };
  768.  
  769. /**
  770. * Get the current location.
  771. *
  772. * @return {string} The current location.
  773. */
  774. const getCurrentLocation = () => {
  775. const location = user?.environment_type || '';
  776. return location.toLowerCase();
  777. };
  778.  
  779. /**
  780. * Check if the user is logged in.
  781. *
  782. * @return {boolean} True if the user is logged in, false otherwise.
  783. */
  784. const isLoggedIn = () => {
  785. return user.length > 0 && 'login' !== getCurrentPage();
  786. };
  787.  
  788. /**
  789. * Get the saved settings.
  790. *
  791. * @param {string} key The key to get.
  792. * @param {boolean} defaultValue The default value.
  793. * @param {string} identifier The identifier for the settings.
  794. *
  795. * @return {Object} The saved settings.
  796. */
  797. const getSetting = (key = null, defaultValue = null, identifier = 'mh-utils-settings') => {
  798. // Grab the local storage data.
  799. const settings = JSON.parse(localStorage.getItem(identifier)) || {};
  800.  
  801. // If we didn't get a key passed in, we want all the settings.
  802. if (! key) {
  803. return settings;
  804. }
  805.  
  806. // If the setting doesn't exist, return the default value.
  807. if (Object.prototype.hasOwnProperty.call(settings, key)) {
  808. return settings[ key ];
  809. }
  810.  
  811. return defaultValue;
  812. };
  813.  
  814. /**
  815. * Save a setting.
  816. *
  817. * @param {string} key The setting key.
  818. * @param {boolean} value The setting value.
  819. * @param {string} identifier The identifier for the settings.
  820. */
  821. const saveSetting = (key, value, identifier = 'mh-utils-settings') => {
  822. // Grab all the settings, set the new one, and save them.
  823. const settings = getSetting(null, {}, identifier);
  824. settings[ key ] = value;
  825.  
  826. localStorage.setItem(identifier, JSON.stringify(settings));
  827. };
  828.  
  829. /**
  830. * Save a setting and toggle the class in the settings UI.
  831. *
  832. * @ignore
  833. *
  834. * @param {Node} node The setting node to animate.
  835. * @param {string} key The setting key.
  836. * @param {boolean} value The setting value.
  837. */
  838. const saveSettingAndToggleClass = (node, key, value, identifier = 'mh-utils-settings') => {
  839. node.parentNode.parentNode.classList.add('busy');
  840.  
  841. // Toggle the state of the checkbox.
  842. node.classList.toggle('active');
  843.  
  844. // Save the setting.
  845. saveSetting(key, value, identifier);
  846.  
  847. // Add the completed class & remove it in a second.
  848. node.parentNode.parentNode.classList.remove('busy');
  849. node.parentNode.parentNode.classList.add('completed');
  850. setTimeout(() => {
  851. node.parentNode.parentNode.classList.remove('completed');
  852. }, 1000);
  853.  
  854. addSettingRefreshReminder();
  855. };
  856.  
  857. /**
  858. * Make the settings tab.
  859. *
  860. * @param {string} identifier The identifier for the settings.
  861. * @param {string} name The name of the settings tab.
  862. */
  863. const addSettingsTab = (identifier = 'userscript-settings', name = 'Userscript Settings') => {
  864. addSettingsTabOnce(identifier, name);
  865. onPageChange({ preferences: { show: () => addSettingsTabOnce(identifier, name) } });
  866.  
  867. return identifier;
  868. };
  869.  
  870. /**
  871. * Make the settings tab once.
  872. *
  873. * @ignore
  874. *
  875. * @param {string} identifier The identifier for the settings.
  876. * @param {string} name The name of the settings tab.
  877. */
  878. const addSettingsTabOnce = (identifier = 'userscript-settings', name = 'Userscript Settings') => {
  879. if ('preferences' !== getCurrentPage()) {
  880. return;
  881. }
  882.  
  883. const existingSettings = document.querySelector(`#${identifier}`);
  884. if (existingSettings) {
  885. return;
  886. }
  887.  
  888. const tabsContainer = document.querySelector('.mousehuntHud-page-tabHeader-container');
  889. if (! tabsContainer) {
  890. return;
  891. }
  892.  
  893. const tabsContentContainer = document.querySelector('.mousehuntHud-page-tabContentContainer');
  894. if (! tabsContentContainer) {
  895. return;
  896. }
  897.  
  898. // make sure the identifier is unique and safe to use as a class.
  899. identifier = identifier.replace(/[^a-z0-9-_]/gi, '');
  900.  
  901. const settingsTab = document.createElement('a');
  902. settingsTab.id = identifier;
  903. settingsTab.href = '#';
  904. settingsTab.classList.add('mousehuntHud-page-tabHeader', identifier);
  905. settingsTab.setAttribute('data-tab', identifier);
  906. settingsTab.setAttribute('onclick', 'hg.utils.PageUtil.onclickPageTabHandler(this); return false;');
  907.  
  908. const settingsTabText = document.createElement('span');
  909. settingsTabText.innerText = name;
  910.  
  911. settingsTab.appendChild(settingsTabText);
  912. tabsContainer.appendChild(settingsTab);
  913.  
  914. const settingsTabContent = document.createElement('div');
  915. settingsTabContent.classList.add('mousehuntHud-page-tabContent', 'game_settings', identifier);
  916. settingsTabContent.setAttribute('data-tab', identifier);
  917.  
  918. tabsContentContainer.appendChild(settingsTabContent);
  919.  
  920. if (identifier === getCurrentTab()) {
  921. const tab = document.getElementById(identifier);
  922. if (tab) {
  923. tab.click();
  924. }
  925. }
  926. };
  927.  
  928. /**
  929. * Add a setting to the preferences page, both on page load and when the page changes.
  930. *
  931. * @param {string} name The setting name.
  932. * @param {string} key The setting key.
  933. * @param {boolean} defaultValue The default value.
  934. * @param {string} description The setting description.
  935. * @param {Object} section The section settings.
  936. * @param {string} tab The tab to add the settings to.
  937. * @param {Object} settings The settings for the settings.
  938. */
  939. const addSetting = (name, key, defaultValue = true, description = '', section = {}, tab = 'userscript-settings', settings = null) => {
  940. onPageChange({ preferences: { show: () => addSettingOnce(name, key, defaultValue, description, section, tab, settings) } });
  941. addSettingOnce(name, key, defaultValue, description, section, tab, settings);
  942.  
  943. addSettingRefreshReminder();
  944. onPageChange({ preferences: { show: addSettingRefreshReminder } });
  945. };
  946.  
  947. /**
  948. * Add a setting to the preferences page.
  949. *
  950. * @ignore
  951. *
  952. * @param {string} name The setting name.
  953. * @param {string} key The setting key.
  954. * @param {boolean} defaultValue The default value.
  955. * @param {string} description The setting description.
  956. * @param {Object} section The section settings.
  957. * @param {string} tab The tab to add the settings to.
  958. * @param {Object} settingSettings The settings for the settings.
  959. */
  960. const addSettingOnce = (name, key, defaultValue = true, description = '', section = {}, tab = 'userscript-settings', settingSettings = null) => {
  961. // Make sure we have the container for our settings.
  962. const container = document.querySelector(`.mousehuntHud-page-tabContent.${tab}`);
  963. if (! container) {
  964. return;
  965. }
  966.  
  967. section = {
  968. id: section.id || 'settings',
  969. name: section.name || 'Userscript Settings',
  970. description: section.description || '',
  971. };
  972.  
  973. let tabId = 'mh-utils-settings';
  974. if (tab !== 'userscript-settings') {
  975. tabId = tab;
  976. }
  977.  
  978. section.id = `${tabId}-${section.id.replace(/[^a-z0-9-_]/gi, '')}`;
  979.  
  980. // If we don't have our custom settings section, then create it.
  981. let sectionExists = document.querySelector(`#${section.id}`);
  982. if (! sectionExists) {
  983. // Make the element, add the ID and class.
  984. const title = document.createElement('div');
  985. title.id = section.id;
  986. title.classList.add('PagePreferences__title');
  987.  
  988. // Set the title of our section.
  989. const titleText = document.createElement('h3');
  990. titleText.classList.add('PagePreferences__titleText');
  991. titleText.textContent = section.name;
  992.  
  993. // Append the title.
  994. title.appendChild(titleText);
  995.  
  996. // Add a separator.
  997. const seperator = document.createElement('div');
  998. seperator.classList.add('PagePreferences__separator');
  999.  
  1000. // Append the separator.
  1001. title.appendChild(seperator);
  1002.  
  1003. // Append it.
  1004. container.appendChild(title);
  1005.  
  1006. sectionExists = document.querySelector(`#${section.id}`);
  1007.  
  1008. if (section.description) {
  1009. const settingSubHeader = makeElement('h4', ['settings-subheader', 'mh-utils-settings-subheader'], section.description);
  1010. sectionExists.insertBefore(settingSubHeader, seperator);
  1011.  
  1012. addStyles(`.mh-utils-settings-subheader {
  1013. padding-top: 10px;
  1014. padding-bottom: 10px;
  1015. font-size: 10px;
  1016. color: #848484;
  1017. }`, 'mh-utils-settings-subheader', true);
  1018. }
  1019. }
  1020.  
  1021. // If we already have a setting visible for our key, bail.
  1022. const settingExists = document.getElementById(`${section.id}-${key}`);
  1023. if (settingExists) {
  1024. return;
  1025. }
  1026.  
  1027. // Create the markup for the setting row.
  1028. const settings = document.createElement('div');
  1029. settings.classList.add('PagePreferences__settingsList');
  1030. settings.id = `${section.id}-${key}`;
  1031.  
  1032. const settingRow = document.createElement('div');
  1033. settingRow.classList.add('PagePreferences__setting');
  1034.  
  1035. const settingRowLabel = document.createElement('div');
  1036. settingRowLabel.classList.add('PagePreferences__settingLabel');
  1037.  
  1038. const settingName = document.createElement('div');
  1039. settingName.classList.add('PagePreferences__settingName');
  1040. settingName.innerHTML = name;
  1041.  
  1042. const defaultSettingText = document.createElement('div');
  1043. defaultSettingText.classList.add('PagePreferences__settingDefault');
  1044.  
  1045. if (settingSettings && (settingSettings.type === 'select' || settingSettings.type === 'multi-select')) {
  1046. addStyles(`.PagePreferences .mousehuntHud-page-tabContent.game_settings.userscript-settings .settingRow .settingRow-action-inputContainer.select.busy:before,
  1047. .PagePreferences .mousehuntHud-page-tabContent.game_settings.userscript-settings .settingRow .settingRow-action-inputContainer.select.completed:before,
  1048. .PagePreferences .mousehuntHud-page-tabContent.game_settings.better-mh-settings .settingRow .settingRow-action-inputContainer.select.busy:before,
  1049. .PagePreferences .mousehuntHud-page-tabContent.game_settings.better-mh-settings .settingRow .settingRow-action-inputContainer.select.completed:before {
  1050. left: unset;
  1051. right: -25px;
  1052. top: 30px;
  1053. }
  1054.  
  1055. .PagePreferences .mousehuntHud-page-tabContent.game_settings .settingRow .name {
  1056. height: unset;
  1057. min-height: 20px;
  1058. }
  1059.  
  1060. .PagePreferences__settingAction.inputDropdownWrapper.busy:before,
  1061. .PagePreferences__settingAction.inputDropdownWrapper.completed:before {
  1062. left: unset;
  1063. right: -40px;
  1064. }
  1065.  
  1066. .inputBoxContainer.multiSelect {
  1067. max-width: 400px;
  1068. }`, 'mh-utils-settings-select', true);
  1069.  
  1070. defaultSettingText.textContent = defaultValue.map((item) => item.name).join(', ');
  1071. } else {
  1072. defaultSettingText.textContent = defaultValue ? 'Enabled' : 'Disabled';
  1073. }
  1074.  
  1075. defaultSettingText.textContent = `Default setting: ${defaultSettingText.textContent}`;
  1076.  
  1077. const settingDescription = document.createElement('div');
  1078. settingDescription.classList.add('PagePreferences__settingDescription');
  1079. settingDescription.innerHTML = description;
  1080.  
  1081. settingRowLabel.appendChild(settingName);
  1082. settingRowLabel.appendChild(defaultSettingText);
  1083. settingRowLabel.appendChild(settingDescription);
  1084.  
  1085. const settingRowAction = document.createElement('div');
  1086. settingRowAction.classList.add('PagePreferences__settingAction');
  1087.  
  1088. const settingRowInput = document.createElement('div');
  1089. settingRowInput.classList.add('settingRow-action-inputContainer');
  1090.  
  1091. if (settingSettings && (settingSettings.type === 'select' || settingSettings.type === 'multi-select')) {
  1092. // Create the dropdown.
  1093. const settingRowInputDropdown = document.createElement('div');
  1094. settingRowInputDropdown.classList.add('inputBoxContainer');
  1095.  
  1096. if (settingSettings.type === 'multi-select') {
  1097. settingRowInputDropdown.classList.add('multiSelect');
  1098. settingRowInput.classList.add('multiSelect', 'select');
  1099. }
  1100.  
  1101. const amount = settingSettings.type === 'multi-select' ? settingSettings.number : 1;
  1102.  
  1103. // make a multi-select dropdown.
  1104. for (let i = 0; i < amount; i++) {
  1105. const settingRowInputDropdownSelect = document.createElement('select');
  1106. settingRowInputDropdownSelect.classList.add('inputBox');
  1107.  
  1108. if (settingSettings.type === 'multi-select') {
  1109. settingRowInputDropdownSelect.classList.add('multiSelect');
  1110. }
  1111.  
  1112. const currentSetting = getSetting(`${key}-${i}`, null, tab);
  1113. let foundSelected = false;
  1114.  
  1115. settingSettings.options.forEach((option) => {
  1116. const settingRowInputDropdownSelectOption = document.createElement('option');
  1117. settingRowInputDropdownSelectOption.value = option.value;
  1118. settingRowInputDropdownSelectOption.textContent = option.name;
  1119.  
  1120. if (currentSetting && currentSetting === option.value) {
  1121. settingRowInputDropdownSelectOption.selected = true;
  1122. foundSelected = true;
  1123. } else if (! foundSelected && defaultValue && defaultValue[ i ] && defaultValue[ i ].value === option.value) {
  1124. settingRowInputDropdownSelectOption.selected = true;
  1125. foundSelected = true;
  1126. }
  1127.  
  1128. settingRowInputDropdownSelect.appendChild(settingRowInputDropdownSelectOption);
  1129. });
  1130.  
  1131. settingRowInputDropdown.appendChild(settingRowInputDropdownSelect);
  1132.  
  1133. // Event listener for when the setting is clicked.
  1134. settingRowInputDropdownSelect.onchange = (event) => {
  1135. const parent = settingRowInputDropdownSelect.parentNode.parentNode.parentNode;
  1136. parent.classList.add('inputDropdownWrapper');
  1137. parent.classList.add('busy');
  1138.  
  1139. // save the setting.
  1140. saveSetting(`${key}-${i}`, event.target.value, tab);
  1141.  
  1142. parent.classList.remove('busy');
  1143. parent.classList.add('completed');
  1144. setTimeout(() => {
  1145. parent.classList.remove('completed');
  1146. }, 1000);
  1147. };
  1148.  
  1149. settingRowInput.appendChild(settingRowInputDropdown);
  1150. settingRowAction.appendChild(settingRowInput);
  1151. }
  1152. } else {
  1153. const settingRowInputCheckbox = document.createElement('div');
  1154. settingRowInputCheckbox.classList.add('mousehuntSettingSlider');
  1155.  
  1156. // Depending on the current state of the setting, add the active class.
  1157. const currentSetting = getSetting(key, null, tab);
  1158. let isActive = false;
  1159. if (currentSetting) {
  1160. settingRowInputCheckbox.classList.add('active');
  1161. isActive = true;
  1162. } else if (null === currentSetting && defaultValue) {
  1163. settingRowInputCheckbox.classList.add('active');
  1164. isActive = true;
  1165. }
  1166.  
  1167. // Event listener for when the setting is clicked.
  1168. settingRowInputCheckbox.onclick = (event) => {
  1169. saveSettingAndToggleClass(event.target, key, ! isActive, tab);
  1170. };
  1171.  
  1172. // Add the input to the settings row.
  1173. settingRowInput.appendChild(settingRowInputCheckbox);
  1174. settingRowAction.appendChild(settingRowInput);
  1175. }
  1176.  
  1177. // Add the label and action to the settings row.
  1178. settingRow.appendChild(settingRowLabel);
  1179. settingRow.appendChild(settingRowAction);
  1180.  
  1181. // Add the settings row to the settings container.
  1182. settings.appendChild(settingRow);
  1183. sectionExists.appendChild(settings);
  1184. };
  1185.  
  1186. /**
  1187. * Add a refresh reminder to the settings page.
  1188. *
  1189. * @ignore
  1190. */
  1191. const addSettingRefreshReminder = () => {
  1192. const existing = document.querySelector('.mh-utils-settings-refresh-message');
  1193. if (existing) {
  1194. return;
  1195. }
  1196.  
  1197. addStyles(`.mh-utils-settings-refresh-message {
  1198. position: fixed;
  1199. right: 0;
  1200. bottom: 0;
  1201. left: 0;
  1202. z-index: 5;
  1203. padding: 1em;
  1204. font-size: 1.5em;
  1205. text-align: center;
  1206. background-color: #d6f2d6;
  1207. border-top: 1px solid #6cc36c;
  1208. opacity: 1;
  1209. transition: opacity 0.5s ease-in-out;
  1210. pointer-events: none;
  1211. }
  1212.  
  1213. .mh-utils-settings-refresh-message-hidden {
  1214. opacity: 0;
  1215. }`, 'mh-utils-settings-refresh-message', true);
  1216.  
  1217. const settingsToggles = document.querySelectorAll('.mousehuntSettingSlider');
  1218. if (! settingsToggles) {
  1219. return;
  1220. }
  1221.  
  1222. settingsToggles.forEach((toggle) => {
  1223. if (toggle.getAttribute('data-has-refresh-reminder')) {
  1224. return;
  1225. }
  1226.  
  1227. toggle.setAttribute('data-has-refresh-reminder', true);
  1228.  
  1229. toggle.addEventListener('click', () => {
  1230. const refreshMessage = document.querySelector('.mh-utils-settings-refresh-message');
  1231. if (refreshMessage) {
  1232. refreshMessage.classList.remove('mh-utils-settings-refresh-message-hidden');
  1233. }
  1234.  
  1235. setTimeout(() => {
  1236. if (refreshMessage) {
  1237. refreshMessage.classList.add('mh-utils-settings-refresh-message-hidden');
  1238. }
  1239. }, 5000);
  1240. });
  1241. });
  1242.  
  1243. const existingRefreshMessage = document.querySelector('.mh-utils-settings-refresh-message');
  1244. if (! existingRefreshMessage) {
  1245. const body = document.querySelector('body');
  1246. if (body) {
  1247. makeElement('div', ['mh-utils-settings-refresh-message', 'mh-utils-settings-refresh-message-hidden'], 'Refresh the page to apply your changes.', body);
  1248. }
  1249. }
  1250. };
  1251.  
  1252. /**
  1253. * POST a request to the server and return the response.
  1254. *
  1255. * @async
  1256. * @param {string} url The url to post to, not including the base url.
  1257. * @param {Object} formData The form data to post.
  1258. *
  1259. * @return {Promise} The response.
  1260. */
  1261. const doRequest = async (url, formData = {}) => {
  1262. // If we don't have the needed params, bail.
  1263. if ('undefined' === typeof lastReadJournalEntryId || 'undefined' === typeof user) {
  1264. return;
  1265. }
  1266.  
  1267. // If our needed params are empty, bail.
  1268. if (! lastReadJournalEntryId || ! user || ! user.unique_hash) { // eslint-disable-line no-undef
  1269. return;
  1270. }
  1271.  
  1272. // Build the form for the request.
  1273. const form = new FormData();
  1274. form.append('sn', 'Hitgrab');
  1275. form.append('hg_is_ajax', 1);
  1276. form.append('last_read_journal_entry_id', lastReadJournalEntryId ? lastReadJournalEntryId : 0); // eslint-disable-line no-undef
  1277. form.append('uh', user.unique_hash ? user.unique_hash : ''); // eslint-disable-line no-undef
  1278.  
  1279. // Add in the passed in form data.
  1280. for (const key in formData) {
  1281. form.append(key, formData[ key ]);
  1282. }
  1283.  
  1284. // Convert the form to a URL encoded string for the body.
  1285. const requestBody = new URLSearchParams(form).toString();
  1286.  
  1287. // Send the request.
  1288. const response = await fetch(
  1289. callbackurl ? callbackurl + url : 'https://www.mousehuntgame.com/' + url, // eslint-disable-line no-undef
  1290. {
  1291. method: 'POST',
  1292. body: requestBody,
  1293. headers: {
  1294. 'Content-Type': 'application/x-www-form-urlencoded',
  1295. },
  1296. }
  1297. );
  1298.  
  1299. // Wait for the response and return it.
  1300. const data = await response.json();
  1301. return data;
  1302. };
  1303.  
  1304. /**
  1305. * Check if the legacy HUD is enabled.
  1306. *
  1307. * @return {boolean} Whether the legacy HUD is enabled.
  1308. */
  1309. const isLegacyHUD = () => {
  1310. return hg.utils.PageUtil.isLegacy();
  1311. };
  1312.  
  1313. /**
  1314. * Check if an item is in the inventory.
  1315. *
  1316. * @async
  1317. *
  1318. * @param {string} item The item to check for.
  1319. *
  1320. * @return {boolean} Whether the item is in the inventory.
  1321. */
  1322. const userHasItem = async (item) => {
  1323. const hasItem = await getUserItems([item]);
  1324. return hasItem.length > 0;
  1325. };
  1326.  
  1327. /**
  1328. * Check if an item is in the inventory.
  1329. *
  1330. * @async
  1331. *
  1332. * @param {Array} items The item to check for.
  1333. *
  1334. * @return {Array} The item data.
  1335. */
  1336. const getUserItems = async (items) => {
  1337. return new Promise((resolve) => {
  1338. hg.utils.UserInventory.getItems(items, (resp) => {
  1339. resolve(resp);
  1340. });
  1341. });
  1342. };
  1343.  
  1344. /**
  1345. * Get the user's setup details.
  1346. *
  1347. * @return {Object} The user's setup details.
  1348. */
  1349. const getUserSetupDetails = () => {
  1350. const userObj = user; // eslint-disable-line no-undef
  1351. const setup = {
  1352. type: userObj.trap_power_type_name,
  1353. stats: {
  1354. power: userObj.trap_power,
  1355. powerBonus: userObj.trap_power_bonus,
  1356. luck: userObj.trap_luck,
  1357. attractionBonus: userObj.trap_attraction_bonus,
  1358. cheeseEfect: userObj.trap_cheese_effect,
  1359. },
  1360. bait: {
  1361. id: parseInt(userObj.bait_item_id),
  1362. name: userObj.bait_name,
  1363. quantity: parseInt(userObj.bait_quantity),
  1364. power: 0,
  1365. powerBonus: 0,
  1366. luck: 0,
  1367. attractionBonus: 0,
  1368. },
  1369. base: {
  1370. id: parseInt(userObj.base_item_id),
  1371. name: userObj.base_name,
  1372. power: 0,
  1373. powerBonus: 0,
  1374. luck: 0,
  1375. attractionBonus: 0,
  1376. },
  1377. charm: {
  1378. id: parseInt(userObj.trinket_item_id),
  1379. name: userObj.trinket_name,
  1380. quantity: parseInt(userObj.trinket_quantity),
  1381. power: 0,
  1382. powerBonus: 0,
  1383. luck: 0,
  1384. attractionBonus: 0,
  1385. },
  1386. weapon: {
  1387. id: parseInt(userObj.weapon_item_id),
  1388. name: userObj.weapon_name,
  1389. power: 0,
  1390. powerBonus: 0,
  1391. luck: 0,
  1392. attractionBonus: 0,
  1393. },
  1394. aura: {
  1395. lgs: {
  1396. active: false,
  1397. power: 0,
  1398. powerBonus: 0,
  1399. luck: 0,
  1400. },
  1401. lightning: {
  1402. active: false,
  1403. power: 0,
  1404. powerBonus: 0,
  1405. luck: 0,
  1406. },
  1407. chrome: {
  1408. active: false,
  1409. power: 0,
  1410. powerBonus: 0,
  1411. luck: 0,
  1412. },
  1413. slayer: {
  1414. active: false,
  1415. power: 0,
  1416. powerBonus: 0,
  1417. luck: 0,
  1418. },
  1419. festive: {
  1420. active: false,
  1421. power: 0,
  1422. powerBonus: 0,
  1423. luck: 0,
  1424. },
  1425. luckycodex: {
  1426. active: false,
  1427. power: 0,
  1428. powerBonus: 0,
  1429. luck: 0,
  1430. },
  1431. riftstalker: {
  1432. active: false,
  1433. power: 0,
  1434. powerBonus: 0,
  1435. luck: 0,
  1436. },
  1437. },
  1438. location: {
  1439. name: userObj.environment_name,
  1440. id: userObj.environment_id,
  1441. slug: userObj.environment_type,
  1442. },
  1443. };
  1444.  
  1445. if ('camp' !== getCurrentPage()) {
  1446. return setup;
  1447. }
  1448.  
  1449. const calculations = document.querySelectorAll('.campPage-trap-trapStat');
  1450. if (! calculations) {
  1451. return setup;
  1452. }
  1453.  
  1454. calculations.forEach((calculation) => {
  1455. if (calculation.classList.length <= 1) {
  1456. return;
  1457. }
  1458.  
  1459. const type = calculation.classList[ 1 ];
  1460. const math = calculation.querySelectorAll('.math .campPage-trap-trapStat-mathRow');
  1461. if (! math) {
  1462. return;
  1463. }
  1464.  
  1465. math.forEach((row) => {
  1466. if (row.classList.contains('label')) {
  1467. return;
  1468. }
  1469.  
  1470. let value = row.querySelector('.campPage-trap-trapStat-mathRow-value');
  1471. let name = row.querySelector('.campPage-trap-trapStat-mathRow-name');
  1472.  
  1473. if (! value || ! name || ! name.innerText) {
  1474. return;
  1475. }
  1476.  
  1477. name = name.innerText;
  1478. value = value.innerText || '0';
  1479.  
  1480. let tempType = type;
  1481. let isBonus = false;
  1482. if (value.includes('%')) {
  1483. tempType = type + 'Bonus';
  1484. isBonus = true;
  1485. }
  1486.  
  1487. // Because attraction_bonus is silly.
  1488. tempType = tempType.replace('_bonusBonus', 'Bonus');
  1489.  
  1490. value = value.replace('%', '');
  1491. value = value.replace(',', '');
  1492. value = parseInt(value * 100) / 100;
  1493.  
  1494. if (tempType === 'attractionBonus') {
  1495. value = value / 100;
  1496. }
  1497.  
  1498. // 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
  1499. if (setup.weapon.name === name) {
  1500. setup.weapon[ tempType ] = value;
  1501. } else if (setup.base.name === name) {
  1502. setup.base[ tempType ] = value;
  1503. } else if (setup.charm.name === name) {
  1504. setup.charm[ tempType ] = value;
  1505. } else if (setup.bait.name === name) {
  1506. setup.bait[ tempType ] = value;
  1507. } else if ('Your trap has no cheese effect bonus.' === name) {
  1508. setup.cheeseEffect = 'No Effect';
  1509. } else {
  1510. let auraType = name.replace(' Aura', '');
  1511. if (! auraType) {
  1512. return;
  1513. }
  1514.  
  1515. auraType = auraType.toLowerCase();
  1516. auraType = auraType.replaceAll(' ', '_');
  1517. // remove any non alphanumeric characters
  1518. auraType = auraType.replace(/[^a-z0-9_]/gi, '');
  1519. auraType = auraType.replace('golden_luck_boost', 'lgs');
  1520. auraType = auraType.replace('2023_lucky_codex', 'luckycodex');
  1521. auraType = auraType.replace('_set_bonus_2_pieces', '');
  1522. auraType = auraType.replace('_set_bonus_3_pieces', '');
  1523.  
  1524. if (! setup.aura[ auraType ]) {
  1525. setup.aura[ auraType ] = {
  1526. active: true,
  1527. type: auraType,
  1528. power: 0,
  1529. powerBonus: 0,
  1530. luck: 0,
  1531. };
  1532. } else {
  1533. setup.aura[ auraType ].active = true;
  1534. setup.aura[ auraType ].type = auraType;
  1535. }
  1536.  
  1537. value = parseInt(value);
  1538.  
  1539. if (isBonus) {
  1540. value = value / 100;
  1541. }
  1542.  
  1543. setup.aura[ auraType ][ tempType ] = value;
  1544. }
  1545. });
  1546. });
  1547.  
  1548. return setup;
  1549. };
  1550.  
  1551. /**
  1552. * Add a submenu item to a menu.
  1553. *
  1554. * @param {Object} options The options for the submenu item.
  1555. * @param {string} options.menu The menu to add the submenu item to.
  1556. * @param {string} options.label The label for the submenu item.
  1557. * @param {string} options.icon The icon for the submenu item.
  1558. * @param {string} options.href The href for the submenu item.
  1559. * @param {string} options.class The class for the submenu item.
  1560. * @param {Function} options.callback The callback for the submenu item.
  1561. * @param {boolean} options.external Whether the submenu item is external or not.
  1562. */
  1563. const addSubmenuItem = (options) => {
  1564. // Default to sensible values.
  1565. const settings = Object.assign({}, {
  1566. menu: 'kingdom',
  1567. label: '',
  1568. icon: 'https://www.mousehuntgame.com/images/ui/hud/menu/special.png',
  1569. href: '',
  1570. class: '',
  1571. callback: null,
  1572. external: false,
  1573. }, options);
  1574.  
  1575. // Grab the menu item we want to add the submenu to.
  1576. const menuTarget = document.querySelector(`.mousehuntHud-menu .${settings.menu}`);
  1577. if (! menuTarget) {
  1578. return;
  1579. }
  1580.  
  1581. // If the menu already has a submenu, just add the item to it.
  1582. if (! menuTarget.classList.contains('hasChildren')) {
  1583. menuTarget.classList.add('hasChildren');
  1584. }
  1585.  
  1586. let hasSubmenu = true;
  1587. let submenu = menuTarget.querySelector('ul');
  1588. if (! submenu) {
  1589. hasSubmenu = false;
  1590. submenu = document.createElement('ul');
  1591. }
  1592.  
  1593. // Create the item.
  1594. const item = document.createElement('li');
  1595. item.classList.add('custom-submenu-item');
  1596. const cleanLabel = settings.label.toLowerCase().replace(/[^a-z0-9]/g, '-');
  1597.  
  1598. const exists = document.querySelector(`#custom-submenu-item-${cleanLabel}`);
  1599. if (exists) {
  1600. return;
  1601. }
  1602.  
  1603. item.id = `custom-submenu-item-${cleanLabel}`;
  1604. if (settings.class) {
  1605. item.classList.add(settings.class);
  1606. }
  1607.  
  1608. // Create the link.
  1609. const link = document.createElement('a');
  1610. link.href = settings.href || '#';
  1611.  
  1612. if (settings.callback) {
  1613. link.addEventListener('click', (e) => {
  1614. e.preventDefault();
  1615. settings.callback();
  1616. });
  1617. }
  1618.  
  1619. // Create the icon.
  1620. const icon = document.createElement('div');
  1621. icon.classList.add('icon');
  1622. icon.style = `background-image: url(${settings.icon});`;
  1623.  
  1624. // Create the label.
  1625. const name = document.createElement('div');
  1626. name.classList.add('name');
  1627. name.innerText = settings.label;
  1628.  
  1629. // Add the icon and label to the link.
  1630. link.appendChild(icon);
  1631. link.appendChild(name);
  1632.  
  1633. // If it's an external link, also add the icon for it.
  1634. if (settings.external) {
  1635. const externalLinkIcon = document.createElement('div');
  1636. externalLinkIcon.classList.add('external_icon');
  1637. link.appendChild(externalLinkIcon);
  1638.  
  1639. // Set the target to _blank so it opens in a new tab.
  1640. link.target = '_blank';
  1641. link.rel = 'noopener noreferrer';
  1642. }
  1643.  
  1644. // Add the link to the item.
  1645. item.appendChild(link);
  1646.  
  1647. // Add the item to the submenu.
  1648. submenu.appendChild(item);
  1649.  
  1650. if (! hasSubmenu) {
  1651. menuTarget.appendChild(submenu);
  1652. }
  1653. };
  1654.  
  1655. /**
  1656. * Add the mouse.rip link to the kingdom menu.
  1657. *
  1658. * @ignore
  1659. */
  1660. const addMouseripLink = () => {
  1661. addSubmenuItem({
  1662. menu: 'kingdom',
  1663. label: 'mouse.rip',
  1664. icon: 'https://www.mousehuntgame.com/images/ui/hud/menu/prize_shoppe.png',
  1665. href: 'https://mouse.rip',
  1666. external: true,
  1667. });
  1668. };
  1669.  
  1670. /**
  1671. * Add an item to the top 'Hunters Online' menu.
  1672. *
  1673. * @param {Object} options The options for the menu item.
  1674. * @param {string} options.label The label for the menu item.
  1675. * @param {string} options.href The href for the menu item.
  1676. * @param {string} options.class The class for the menu item.
  1677. * @param {Function} options.callback The callback for the menu item.
  1678. * @param {boolean} options.external Whether the link is external or not.
  1679. */
  1680. const addItemToGameInfoBar = (options) => {
  1681. const settings = Object.assign({}, {
  1682. label: '',
  1683. href: '',
  1684. class: '',
  1685. callback: null,
  1686. external: false,
  1687. }, options);
  1688.  
  1689. const safeLabel = settings.label.replace(/[^a-z0-9]/gi, '_').toLowerCase();
  1690. const exists = document.querySelector(`#mh-custom-topmenu-${safeLabel}`);
  1691. if (exists) {
  1692. return;
  1693. }
  1694.  
  1695. addStyles(`.mousehuntHud-gameInfo .mousehuntHud-menu {
  1696. position: relative;
  1697. top: unset;
  1698. left: unset;
  1699. display: inline;
  1700. width: unset;
  1701. height: unset;
  1702. padding-top: unset;
  1703. padding-left: unset;
  1704. background: unset;
  1705. }
  1706. `, 'mh-custom-topmenu', true);
  1707.  
  1708. const menu = document.querySelector('.mousehuntHud-gameInfo');
  1709. if (! menu) {
  1710. return;
  1711. }
  1712.  
  1713. const item = document.createElement('a');
  1714. item.id = `mh-custom-topmenu-${safeLabel}`;
  1715. item.classList.add('mousehuntHud-gameInfo-item');
  1716. item.classList.add('mousehuntHud-custom-menu-item');
  1717.  
  1718. item.href = settings.href || '#';
  1719.  
  1720. const name = document.createElement('div');
  1721. name.classList.add('name');
  1722.  
  1723. if (settings.label) {
  1724. name.innerText = settings.label;
  1725. }
  1726.  
  1727. item.appendChild(name);
  1728.  
  1729. if (settings.class) {
  1730. item.classList.add(settings.class);
  1731. }
  1732.  
  1733. if (settings.href) {
  1734. item.href = settings.href;
  1735. }
  1736.  
  1737. if (settings.callback) {
  1738. item.addEventListener('click', settings.callback);
  1739. }
  1740.  
  1741. if (settings.external) {
  1742. const externalLinkIconWrapper = document.createElement('div');
  1743. externalLinkIconWrapper.classList.add('mousehuntHud-menu');
  1744.  
  1745. const externalLinkIcon = document.createElement('div');
  1746. externalLinkIcon.classList.add('external_icon');
  1747.  
  1748. externalLinkIconWrapper.appendChild(externalLinkIcon);
  1749. item.appendChild(externalLinkIconWrapper);
  1750. }
  1751.  
  1752. menu.insertBefore(item, menu.firstChild);
  1753. };
  1754.  
  1755. /**
  1756. * Build a popup.
  1757. *
  1758. * Templates:
  1759. * ajax: no close button in lower right, 'prefix' instead of title. 'suffix' for close button area.
  1760. * default: {*title*} {*content*}
  1761. * error: in red, with error icon{*title*} {*content*}
  1762. * largerImage: full width image {*title*} {*image*}
  1763. * largerImageWithClass: smaller than larger image, with caption {*title*} {*image*} {*imageCaption*} {*imageClass*} (goes on the img tag)
  1764. * loading: Just says loading
  1765. * multipleItems: {*title*} {*content*} {*items*}
  1766. * singleItemLeft: {*title*} {*content*} {*items*}
  1767. * singleItemRight: {*title*} {*content*} {*items*}
  1768. *
  1769. * @param {Object} options The popup options.
  1770. * @param {string} options.title The title of the popup.
  1771. * @param {string} options.content The content of the popup.
  1772. * @param {boolean} options.hasCloseButton Whether or not the popup has a close button.
  1773. * @param {string} options.template The template to use for the popup.
  1774. * @param {boolean} options.show Whether or not to show the popup.
  1775. * @param {string} options.className The class name to add to the popup.
  1776. */
  1777. const createPopup = (options) => {
  1778. // If we don't have jsDialog, bail.
  1779. if ('undefined' === typeof jsDialog || ! jsDialog) { // eslint-disable-line no-undef
  1780. return;
  1781. }
  1782.  
  1783. // Default to sensible values.
  1784. const settings = Object.assign({}, {
  1785. title: '',
  1786. content: '',
  1787. hasCloseButton: true,
  1788. template: 'default',
  1789. show: true,
  1790. className: '',
  1791. }, options);
  1792.  
  1793. // Initiate the popup.
  1794. const popup = new jsDialog(); // eslint-disable-line no-undef
  1795. popup.setIsModal(! settings.hasCloseButton);
  1796.  
  1797. // Set the template & add in the content.
  1798. popup.setTemplate(settings.template);
  1799. popup.addToken('{*title*}', settings.title);
  1800. popup.addToken('{*content*}', settings.content);
  1801.  
  1802. popup.setAttributes({
  1803. className: settings.className,
  1804. });
  1805.  
  1806. // If we want to show the popup, show it.
  1807. if (settings.show) {
  1808. popup.show();
  1809. }
  1810.  
  1811. return popup;
  1812. };
  1813.  
  1814. /**
  1815. * Create a popup with an image.
  1816. *
  1817. * @param {Object} options Popup options.
  1818. * @param {string} options.title The title of the popup.
  1819. * @param {string} options.image The image to show in the popup.
  1820. * @param {boolean} options.show Whether or not to show the popup.
  1821. */
  1822. const createImagePopup = (options) => {
  1823. // Default to sensible values.
  1824. const settings = Object.assign({}, {
  1825. title: '',
  1826. image: '',
  1827. show: true,
  1828. }, options);
  1829.  
  1830. // Create the popup.
  1831. const popup = createPopup({
  1832. title: settings.title,
  1833. template: 'largerImage',
  1834. show: false,
  1835. });
  1836.  
  1837. // Add the image to the popup.
  1838. popup.addToken('{*image*}', settings.image);
  1839.  
  1840. // If we want to show the popup, show it.
  1841. if (settings.show) {
  1842. popup.show();
  1843. }
  1844.  
  1845. return popup;
  1846. };
  1847.  
  1848. /**
  1849. * Show a map-popup.
  1850. *
  1851. * @param {Object} options The popup options.
  1852. * @param {string} options.title The title of the popup.
  1853. * @param {string} options.content The content of the popup.
  1854. * @param {string} options.closeClass The class to add to the close button.
  1855. * @param {string} options.closeText The text to add to the close button.
  1856. * @param {boolean} options.show Whether or not to show the popup.
  1857. */
  1858. const createMapPopup = (options) => {
  1859. // Check to make sure we can call the hg views.
  1860. if (! (hg && hg.views && hg.views.TreasureMapDialogView)) { // eslint-disable-line no-undef
  1861. return;
  1862. }
  1863.  
  1864. // Default to sensible values.
  1865. const settings = Object.assign({}, {
  1866. title: '',
  1867. content: '',
  1868. closeClass: 'acknowledge',
  1869. closeText: 'ok',
  1870. show: true,
  1871. }, options);
  1872.  
  1873. // Initiate the popup.
  1874. const dialog = new hg.views.TreasureMapDialogView(); // eslint-disable-line no-undef
  1875.  
  1876. // Set all the content and options.
  1877. dialog.setTitle(options.title);
  1878. dialog.setContent(options.content);
  1879. dialog.setCssClass(options.closeClass);
  1880. dialog.setContinueAction(options.closeText);
  1881.  
  1882. // If we want to show & we can show, show it.
  1883. if (settings.show && hg.controllers && hg.controllers.TreasureMapDialogController) { // eslint-disable-line no-undef
  1884. hg.controllers.TreasureMapController.show(); // eslint-disable-line no-undef
  1885. hg.controllers.TreasureMapController.showDialog(dialog); // eslint-disable-line no-undef
  1886. }
  1887.  
  1888. return dialog;
  1889. };
  1890.  
  1891. /**
  1892. * Create a welcome popup.
  1893. *
  1894. * @param {Object} options The popup options.
  1895. * @param {string} options.id The ID of the popup.
  1896. * @param {string} options.title The title of the popup.
  1897. * @param {string} options.content The content of the popup.
  1898. * @param {Array} options.columns The columns of the popup.
  1899. * @param {string} options.columns.title The title of the column.
  1900. * @param {string} options.columns.content The content of the column.
  1901. */
  1902. const createWelcomePopup = (options = {}) => {
  1903. if (! (options && options.id && options.title && options.content)) {
  1904. return;
  1905. }
  1906.  
  1907. if (! isLoggedIn()) {
  1908. return;
  1909. }
  1910.  
  1911. const hasSeenWelcome = getSetting('has-seen-welcome', false, options.id);
  1912. if (hasSeenWelcome) {
  1913. return;
  1914. }
  1915.  
  1916. addStyles(`#overlayPopup.mh-welcome .jsDialog.top,
  1917. #overlayPopup.mh-welcome .jsDialog.bottom,
  1918. #overlayPopup.mh-welcome .jsDialog.background {
  1919. padding: 0;
  1920. margin: 0;
  1921. background: none;
  1922. }
  1923.  
  1924. #overlayPopup.mh-welcome .jsDialogContainer .prefix,
  1925. #overlayPopup.mh-welcome .jsDialogContainer .content {
  1926. padding: 0;
  1927. }
  1928.  
  1929. #overlayPopup.mh-welcome #jsDialogClose,
  1930. #overlayPopup.mh-welcome .jsDialogContainer .suffix {
  1931. display: none;
  1932. }
  1933.  
  1934. #overlayPopup.mh-welcome .jsDialogContainer {
  1935. padding: 0 20px;
  1936. background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_border.png);
  1937. background-repeat: repeat-y;
  1938. background-size: 100%;
  1939. }
  1940.  
  1941. #overlayPopup.mh-welcome .jsDialogContainer::before {
  1942. position: absolute;
  1943. top: -80px;
  1944. right: 0;
  1945. left: 0;
  1946. height: 100px;
  1947. content: '';
  1948. background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_header.png);
  1949. background-repeat: no-repeat;
  1950. background-size: 100%;
  1951. }
  1952.  
  1953. #overlayPopup.mh-welcome .jsDialogContainer::after {
  1954. position: absolute;
  1955. top: 100%;
  1956. right: 0;
  1957. left: 0;
  1958. height: 126px;
  1959. content: '';
  1960. background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_footer.png);
  1961. background-repeat: no-repeat;
  1962. background-size: 100%;
  1963. }
  1964.  
  1965. .mh-welcome .mh-title {
  1966. position: relative;
  1967. top: -90px;
  1968. display: flex;
  1969. align-items: center;
  1970. justify-content: center;
  1971. width: 412px;
  1972. height: 90px;
  1973. margin: 20px auto 0;
  1974. font-family: Georgia, serif;
  1975. font-size: 26px;
  1976. font-weight: 700;
  1977. color: #7d3b0a;
  1978. text-align: center;
  1979. text-shadow: 1px 1px 1px #e9d5a2;
  1980. background: url(https://www.mousehuntgame.com/images/ui/larry_gifts/ribbon.png?asset_cache_version=2) no-repeat;
  1981. }
  1982.  
  1983. .mh-welcome .mh-inner-wrapper {
  1984. display: flex;
  1985. padding: 5px 10px 25px;
  1986. margin-top: -90px;
  1987. }
  1988.  
  1989. .mh-welcome .text {
  1990. margin-left: 30px;
  1991. line-height: 18px;
  1992. text-align: left;
  1993. }
  1994.  
  1995. .mh-welcome .text p {
  1996. font-size: 13px;
  1997. line-height: 19px;
  1998. }
  1999.  
  2000. .mh-welcome .mh-inner-title {
  2001. padding: 10px 0;
  2002. font-size: 1.5em;
  2003. font-weight: 700;
  2004. }
  2005.  
  2006. .mh-welcome .mh-button-wrapper {
  2007. display: flex;
  2008. align-items: center;
  2009. justify-content: center;
  2010. }
  2011.  
  2012. .mh-welcome .mh-button {
  2013. padding: 10px 50px;
  2014. font-size: 1.5em;
  2015. color: #000;
  2016. background: linear-gradient(to bottom, #fff600, #f4e830);
  2017. border: 1px solid #000;
  2018. border-radius: 5px;
  2019. box-shadow: 0 0 10px 1px #d6d13b inset;
  2020. }
  2021.  
  2022. .mh-welcome .mh-intro-text {
  2023. margin: 2em 1em;
  2024. font-size: 15px;
  2025. line-height: 25px;
  2026. }
  2027.  
  2028. .mh-welcome-columns {
  2029. display: grid;
  2030. grid-template-columns: 1fr 1fr;
  2031. gap: 2em;
  2032. margin: 1em;
  2033. -ms-grid-columns: 1fr 2em 1fr;
  2034. }
  2035.  
  2036. .mh-welcome-column h2 {
  2037. margin-bottom: 1em;
  2038. font-size: 16px;
  2039. color: #7d3b0a;
  2040. border-bottom: 1px solid #cba36d;
  2041. }
  2042.  
  2043. .mh-welcome-column ul {
  2044. margin-left: 3em;
  2045. list-style: disc;
  2046. }
  2047. `, 'mh-welcome', true);
  2048.  
  2049. const markup = `<div class="mh-welcome">
  2050. <h1 class="mh-title">${options.title}</h1>
  2051. <div class="mh-inner-wrapper">
  2052. <div class="text">
  2053. <div class="mh-intro-text">
  2054. ${options.content}
  2055. </div>
  2056. <div class="mh-welcome-columns">
  2057. ${options.columns.map((column) => `<div class="mh-welcome-column">
  2058. <h2>${column.title}</h2>
  2059. ${column.content}
  2060. </div>`).join('')}
  2061. </div>
  2062. </div>
  2063. </div>
  2064. <div class="mh-button-wrapper">
  2065. <a href="#" id="mh-welcome-${options.id}-continue" class="mh-button">Continue</a>
  2066. </div>
  2067. </div>`;
  2068.  
  2069. // Initiate the popup.
  2070. const welcomePopup = createPopup({
  2071. hasCloseButton: false,
  2072. template: 'ajax',
  2073. content: markup,
  2074. show: false,
  2075. });
  2076.  
  2077. // Set more of our tokens.
  2078. welcomePopup.addToken('{*prefix*}', '');
  2079. welcomePopup.addToken('{*suffix*}', '');
  2080.  
  2081. // Set the attribute and show the popup.
  2082. welcomePopup.setAttributes({ className: `mh-welcome mh-welcome-popup-${options.id}` });
  2083.  
  2084. // If we want to show the popup, show it.
  2085. welcomePopup.show();
  2086.  
  2087. // Add the event listener to the continue button.
  2088. const continueButton = document.getElementById(`mh-welcome-${options.id}-continue`);
  2089. continueButton.addEventListener('click', () => {
  2090. saveSetting('has-seen-welcome', true, options.id);
  2091. welcomePopup.hide();
  2092. });
  2093. };
  2094.  
  2095. /**
  2096. * Create a popup with the larry's office style.
  2097. *
  2098. * @param {string} content Content to display in the popup.
  2099. * @param {Array} classes Classes to add to the popup.
  2100. */
  2101. const createLarryPopup = (content, classes = []) => {
  2102. const message = {
  2103. content: { body: content },
  2104. css_class: ['larryOffice', ...classes].join(' '),
  2105. show_overlay: true,
  2106. is_modal: true
  2107. };
  2108.  
  2109. hg.views.MessengerView.addMessage(message);
  2110. hg.views.MessengerView.go();
  2111. };
  2112.  
  2113. /**
  2114. * Add a popup similar to the larry's gift popup.
  2115. *
  2116. * createPaperPopup({
  2117. * title: 'Whoa! A popup!',
  2118. * content: {
  2119. * title: 'This is the title of the content',
  2120. * 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;',
  2121. * image: 'https://api.mouse.rip/hunter/trap/8209591.png',
  2122. * },
  2123. * button: {
  2124. * text: 'A button',
  2125. * href: '#',
  2126. * },
  2127. * show: true,
  2128. * });
  2129. *
  2130. * @param {Object} options The popup options.
  2131. * @param {string} options.title The title of the popup.
  2132. * @param {Object} options.content The content of the popup.
  2133. * @param {string} options.content.title The title of the popup.
  2134. * @param {string} options.content.text The text of the popup.
  2135. * @param {string} options.content.image The image of the popup.
  2136. * @param {Array} options.button The button of the popup.
  2137. * @param {string} options.button.text The text of the button.
  2138. * @param {string} options.button.href The url of the button.
  2139. * @param {boolean} options.show Whether to show the popup or not.
  2140. */
  2141. const createPaperPopup = (options) => {
  2142. // If we don't have jsDialog, bail.
  2143. if ('undefined' === typeof jsDialog || ! jsDialog) { // eslint-disable-line no-undef
  2144. return;
  2145. }
  2146.  
  2147. // Add the styles for our popup.
  2148. addStyles(`#overlayPopup.mh-paper-popup-dialog-wrapper .jsDialog.top,
  2149. #overlayPopup.mh-paper-popup-dialog-wrapper .jsDialog.bottom,
  2150. #overlayPopup.mh-paper-popup-dialog-wrapper .jsDialog.background {
  2151. padding: 0;
  2152. margin: 0;
  2153. background: none;
  2154. }
  2155.  
  2156. #overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer .prefix,
  2157. #overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer .content {
  2158. padding: 0;
  2159. }
  2160.  
  2161. #overlayPopup.mh-paper-popup-dialog-wrapper #jsDialogClose,
  2162. #overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer .suffix {
  2163. display: none;
  2164. }
  2165.  
  2166. #overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer {
  2167. padding: 0 20px;
  2168. background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_border.png);
  2169. background-repeat: repeat-y;
  2170. background-size: 100%;
  2171. }
  2172.  
  2173. #overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer::before {
  2174. position: absolute;
  2175. top: -80px;
  2176. right: 0;
  2177. left: 0;
  2178. height: 100px;
  2179. content: '';
  2180. background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_header.png);
  2181. background-repeat: no-repeat;
  2182. background-size: 100%;
  2183. }
  2184.  
  2185. #overlayPopup.mh-paper-popup-dialog-wrapper .jsDialogContainer::after {
  2186. position: absolute;
  2187. top: 100%;
  2188. right: 0;
  2189. left: 0;
  2190. height: 126px;
  2191. content: '';
  2192. background-image: url(https://www.mousehuntgame.com/images/ui/newsposts/np_footer.png);
  2193. background-repeat: no-repeat;
  2194. background-size: 100%;
  2195. }
  2196.  
  2197. .mh-paper-popup-dialog-wrapper .mh-title {
  2198. position: relative;
  2199. top: -40px;
  2200. display: flex;
  2201. align-items: center;
  2202. justify-content: center;
  2203. width: 412px;
  2204. height: 99px;
  2205. margin: 20px auto 0;
  2206. font-family: Georgia, serif;
  2207. font-size: 34px;
  2208. font-weight: 700;
  2209. color: #7d3b0a;
  2210. text-align: center;
  2211. text-shadow: 1px 1px 1px #e9d5a2;
  2212. background: url(https://www.mousehuntgame.com/images/ui/larry_gifts/ribbon.png?asset_cache_version=2) no-repeat;
  2213. }
  2214.  
  2215. .mh-paper-popup-dialog-wrapper .mh-inner-wrapper {
  2216. display: flex;
  2217. padding: 5px 10px 25px;
  2218. }
  2219.  
  2220. .mh-paper-popup-dialog-wrapper .mh-inner-image-wrapper {
  2221. position: relative;
  2222. padding: 10px;
  2223. margin: 0 auto 10px;
  2224. background: #f7e3af;
  2225. border-radius: 10px;
  2226. box-shadow: 0 3px 10px #bd7d3c;
  2227. }
  2228.  
  2229. .mh-paper-popup-dialog-wrapper .mh-inner-image {
  2230. width: 200px;
  2231. height: 200px;
  2232. background-color: #f5edd7;
  2233. border-radius: 5px;
  2234. box-shadow: 0 0 100px #6c340b inset;
  2235. }
  2236.  
  2237. .mh-paper-popup-dialog-wrapper .mh-inner-text {
  2238. margin-left: 30px;
  2239. line-height: 18px;
  2240. text-align: left;
  2241. }
  2242.  
  2243. .mh-paper-popup-dialog-wrapper .mh-inner-title {
  2244. padding: 10px 0;
  2245. font-size: 1.5em;
  2246. font-weight: 700;
  2247. }
  2248.  
  2249. .mh-paper-popup-dialog-wrapper .mh-button-wrapper {
  2250. display: flex;
  2251. align-items: center;
  2252. justify-content: center;
  2253. }
  2254.  
  2255. .mh-paper-popup-dialog-wrapper .mh-button {
  2256. padding: 10px 50px;
  2257. font-size: 1.5em;
  2258. color: #000;
  2259. background: linear-gradient(to bottom, #fff600, #f4e830);
  2260. border: 1px solid #000;
  2261. border-radius: 5px;
  2262. box-shadow: 0 0 10px 1px #d6d13b inset;
  2263. }
  2264. `);
  2265.  
  2266. // Default to sensible values.
  2267. const settings = Object.assign({}, {
  2268. title: '',
  2269. content: {
  2270. title: '',
  2271. text: '',
  2272. image: '',
  2273. },
  2274. button: {
  2275. text: '',
  2276. href: '',
  2277. },
  2278. show: true,
  2279. className: '',
  2280. }, options);
  2281.  
  2282. // Build the markup with our content.
  2283. const markup = `<div class="mh-paper-popup-wrapper">
  2284. <div class="mh-title">${settings.title}</div>
  2285. <div class="mh-inner-wrapper">
  2286. <div class="mh-inner-image-wrapper">
  2287. <img class="mh-inner-image" src="${settings.content.image}" />
  2288. </div>
  2289. <div class="mh-inner-text">
  2290. <div class="mh-inner-title">${settings.content.title}</div>
  2291. <p>${settings.content.text}</p>
  2292. </div>
  2293. </div>
  2294. <div class="mh-button-wrapper">
  2295. <a href="${settings.button.href}" class="mh-button">${settings.button.text}</a>
  2296. </div>
  2297. </div>`;
  2298.  
  2299. // Initiate the popup.
  2300. const popup = createPopup({
  2301. hasCloseButton: false,
  2302. template: 'ajax',
  2303. content: markup,
  2304. show: false,
  2305. });
  2306.  
  2307. // Set more of our tokens.
  2308. popup.addToken('{*prefix*}', '');
  2309. popup.addToken('{*suffix*}', '');
  2310.  
  2311. // Set the attribute and show the popup.
  2312. popup.setAttributes({ className: `mh-paper-popup-dialog-wrapper ${settings.className}` });
  2313.  
  2314. // If we want to show the popup, show it.
  2315. if (settings.show) {
  2316. popup.show();
  2317. }
  2318.  
  2319. return popup;
  2320. };
  2321.  
  2322. /**
  2323. * Show a message in the horn dialog.
  2324. *
  2325. * Type can be one of these: bait_empty unknown_error bait_disarmed recent_turn recent_linked_turn puzzle
  2326. *
  2327. * @param {Object} options Options for the message.
  2328. * @param {string} options.title Title of the message. Keep it under 50 characters.
  2329. * @param {string} options.text Text of the message. Keep it under 90 characters.
  2330. * @param {string} options.button Text of the button.
  2331. * @param {Function} options.action Callback for the button.
  2332. * @param {number} options.dismiss Time to dismiss the message.
  2333. * @param {string} options.type Type of the message.
  2334. * @param {string} options.classname Classname of the message.
  2335. */
  2336. const showHornMessage = (options) => {
  2337. const huntersHornView = document.querySelector('.huntersHornView__messageContainer');
  2338. if (! huntersHornView) {
  2339. return;
  2340. }
  2341.  
  2342. const settings = {
  2343. title: options.title || 'Hunters Horn',
  2344. text: options.text || 'This is a message from the Hunters Horn',
  2345. button: options.button || 'OK',
  2346. action: options.action || (() => { }),
  2347. dismiss: options.dismiss || null,
  2348. type: options.type || 'recent_linked_turn',
  2349. classname: options.classname || '',
  2350. image: options.image || null,
  2351. imageLink: options.imageLink || null,
  2352. imageCallback: options.imageCallback || null,
  2353. };
  2354.  
  2355. // do the other effects
  2356. const backdrop = document.querySelector('.huntersHornView__backdrop');
  2357. if (backdrop) {
  2358. backdrop.classList.add('huntersHornView__backdrop--active');
  2359. }
  2360.  
  2361. const gameInfo = document.querySelector('.mousehuntHud-gameInfo');
  2362. if (gameInfo) {
  2363. gameInfo.classList.add('blur');
  2364. }
  2365.  
  2366. const messageWrapper = makeElement('div', ['huntersHornView__message huntersHornView__message--active', settings.classname]);
  2367. const message = makeElement('div', ['huntersHornMessageView', `huntersHornMessageView--${settings.type}`]);
  2368. makeElement('div', 'huntersHornMessageView__title', settings.title, message);
  2369. const content = makeElement('div', 'huntersHornMessageView__content');
  2370. if (settings.image) {
  2371. const imgWrapper = makeElement('div', 'huntersHornMessageView__friend');
  2372. const img = makeElement('a', 'huntersHornMessageView__friendProfilePic');
  2373. if (settings.imageLink) {
  2374. img.href = settings.imageLink;
  2375. } else if (settings.imageCallback) {
  2376. img.addEventListener('click', settings.imageCallback);
  2377. } else {
  2378. img.href = '#';
  2379. }
  2380.  
  2381. img.style.backgroundImage = `url(${settings.image})`;
  2382.  
  2383. imgWrapper.appendChild(img);
  2384. content.appendChild(imgWrapper);
  2385. }
  2386. makeElement('div', 'huntersHornMessageView__text', settings.text, content);
  2387. const buttonSpacer = makeElement('div', 'huntersHornMessageView__buttonSpacer');
  2388. const button = makeElement('button', 'huntersHornMessageView__action');
  2389. const buttonLabel = makeElement('div', 'huntersHornMessageView__actionLabel');
  2390. makeElement('span', 'huntersHornMessageView__actionText', settings.button, buttonLabel);
  2391.  
  2392. button.appendChild(buttonLabel);
  2393.  
  2394. button.addEventListener('click', () => {
  2395. if (settings.action) {
  2396. settings.action();
  2397. }
  2398.  
  2399. messageWrapper.innerHTML = '';
  2400. backdrop.classList.remove('huntersHornView__backdrop--active');
  2401. gameInfo.classList.remove('blur');
  2402. });
  2403.  
  2404. buttonSpacer.appendChild(button);
  2405. content.appendChild(buttonSpacer);
  2406.  
  2407. message.appendChild(content);
  2408.  
  2409. if (settings.dismiss) {
  2410. const countdown = makeElement('button', ['huntersHornMessageView__countdown']);
  2411. makeElement('div', 'huntersHornMessageView__countdownButtonImage', '', countdown);
  2412.  
  2413. const svgMarkup = `<svg class="huntersHornMessageView__countdownSVG">
  2414. <circle r="46%" cx="50%" cy="50%" class="huntersHornMessageView__countdownCircleTrack"></circle>
  2415. <circle r="46%" cx="50%" cy="50%" class="huntersHornMessageView__countdownCircle" style="animation-duration: ${settings.dismiss}ms;"></circle>
  2416. </svg>`;
  2417. countdown.innerHTML += svgMarkup;
  2418. message.appendChild(countdown);
  2419. }
  2420.  
  2421. messageWrapper.appendChild(message);
  2422.  
  2423. // remove any existing messages
  2424. const existingMessages = huntersHornView.querySelector('.huntersHornView__message');
  2425. if (existingMessages) {
  2426. existingMessages.remove();
  2427. }
  2428.  
  2429. huntersHornView.appendChild(messageWrapper);
  2430.  
  2431. if (settings.dismiss) {
  2432. setTimeout(() => {
  2433. const countdown = messageWrapper.querySelector('.huntersHornMessageView__countdown');
  2434. if (countdown) {
  2435. countdown.classList.add('huntersHornMessageView__countdown--complete');
  2436. }
  2437. messageWrapper.innerHTML = '';
  2438. backdrop.classList.remove('huntersHornView__backdrop--active');
  2439. gameInfo.classList.remove('blur');
  2440. }, settings.dismiss);
  2441. }
  2442. };
  2443.  
  2444. const toggleHornDom = (verb = 'remove') => {
  2445. const els = [
  2446. {
  2447. selector: '.huntersHornView__horn',
  2448. class: 'huntersHornView__horn--active',
  2449. },
  2450. {
  2451. selector: '.huntersHornView__backdrop',
  2452. class: 'huntersHornView__backdrop--active',
  2453. },
  2454. {
  2455. selector: '.huntersHornView__message',
  2456. class: 'huntersHornView__message--active',
  2457. },
  2458. {
  2459. selector: '.mousehuntHud-environmentName',
  2460. class: 'blur'
  2461. },
  2462. {
  2463. selector: '.mousehuntHud-gameInfo',
  2464. class: 'blur'
  2465. },
  2466. {
  2467. selector: '.huntersHornView__horn',
  2468. class: 'huntersHornView__horn--hide'
  2469. },
  2470. {
  2471. selector: '.huntersHornView__backdrop',
  2472. class: 'huntersHornView__backdrop--active'
  2473. },
  2474. {
  2475. selector: '.huntersHornView__message',
  2476. class: 'huntersHornView__message--active'
  2477. },
  2478. ];
  2479.  
  2480. els.forEach((el) => {
  2481. const dom = document.querySelector(el.selector);
  2482. if (dom) {
  2483. dom.classList[ verb ](el.class);
  2484. }
  2485. }
  2486. );
  2487.  
  2488. return document.querySelector('.huntersHornView__message');
  2489. };
  2490.  
  2491. /**
  2492. * TODO: document this
  2493. *
  2494. * @param {*} message
  2495. */
  2496. const showHuntersHornMessage = (message) => {
  2497. const defaultValues = {
  2498. callback: null,
  2499. countdown: null,
  2500. actionText: null,
  2501. };
  2502.  
  2503. message = Object.assign(defaultValues, message);
  2504.  
  2505. // if the callback was passed in, we need to wrap it in a function that will dismiss the message
  2506. if (message.callback) {
  2507. const originalCallback = message.callback;
  2508. message.callback = () => {
  2509. originalCallback();
  2510. dismissHuntersHornMessage();
  2511. };
  2512. } else {
  2513. message.callback = dismissHuntersHornMessage;
  2514. }
  2515.  
  2516. const messageDom = toggleHornDom('add');
  2517. const messageView = new hg.views.HuntersHornMessageView(message);
  2518. messageDom.innerHTML = '';
  2519. messageDom.appendChild(messageView.render()[ 0 ]);
  2520. };
  2521.  
  2522. /**
  2523. * TODO: document this
  2524. */
  2525. const dismissHuntersHornMessage = () => {
  2526. toggleHornDom('remove');
  2527. };
  2528.  
  2529. /**
  2530. * Make an element draggable. Saves the position to local storage.
  2531. *
  2532. * @param {string} dragTarget The selector for the element that should be dragged.
  2533. * @param {string} dragHandle The selector for the element that should be used to drag the element.
  2534. * @param {number} defaultX The default X position.
  2535. * @param {number} defaultY The default Y position.
  2536. * @param {string} storageKey The key to use for local storage.
  2537. * @param {boolean} savePosition Whether or not to save the position to local storage.
  2538. */
  2539. const makeElementDraggable = (dragTarget, dragHandle, defaultX = null, defaultY = null, storageKey = null, savePosition = true) => {
  2540. const modal = document.querySelector(dragTarget);
  2541. if (! modal) {
  2542. return;
  2543. }
  2544.  
  2545. const handle = document.querySelector(dragHandle);
  2546. if (! handle) {
  2547. return;
  2548. }
  2549.  
  2550. /**
  2551. * Make sure the coordinates are within the bounds of the window.
  2552. *
  2553. * @param {string} type The type of coordinate to check.
  2554. * @param {number} value The value of the coordinate.
  2555. *
  2556. * @return {number} The value of the coordinate, or the max/min value if it's out of bounds.
  2557. */
  2558. const keepWithinLimits = (type, value) => {
  2559. if ('top' === type) {
  2560. return value < -20 ? -20 : value;
  2561. }
  2562.  
  2563. if (value < (handle.offsetWidth * -1) + 20) {
  2564. return (handle.offsetWidth * -1) + 20;
  2565. }
  2566.  
  2567. if (value > document.body.clientWidth - 20) {
  2568. return document.body.clientWidth - 20;
  2569. }
  2570.  
  2571. return value;
  2572. };
  2573.  
  2574. /**
  2575. * When the mouse is clicked, add the class and event listeners.
  2576. *
  2577. * @param {Object} e The event object.
  2578. */
  2579. const onMouseDown = (e) => {
  2580. e.preventDefault();
  2581. setTimeout(() => {
  2582. // Get the current mouse position.
  2583. x1 = e.clientX;
  2584. y1 = e.clientY;
  2585.  
  2586. // Add the class to the element.
  2587. modal.classList.add('mh-is-dragging');
  2588.  
  2589. // Add the onDrag and finishDrag events.
  2590. document.onmousemove = onDrag;
  2591. document.onmouseup = finishDrag;
  2592. }, 50);
  2593. };
  2594.  
  2595. /**
  2596. * When the drag is finished, remove the dragging class and event listeners, and save the position.
  2597. */
  2598. const finishDrag = () => {
  2599. document.onmouseup = null;
  2600. document.onmousemove = null;
  2601.  
  2602. // Remove the class from the element.
  2603. modal.classList.remove('mh-is-dragging');
  2604.  
  2605. if (storageKey) {
  2606. localStorage.setItem(storageKey, JSON.stringify({ x: modal.offsetLeft, y: modal.offsetTop }));
  2607. }
  2608. };
  2609.  
  2610. /**
  2611. * When the mouse is moved, update the element's position.
  2612. *
  2613. * @param {Object} e The event object.
  2614. */
  2615. const onDrag = (e) => {
  2616. e.preventDefault();
  2617.  
  2618. // Calculate the new cursor position.
  2619. x2 = x1 - e.clientX;
  2620. y2 = y1 - e.clientY;
  2621.  
  2622. x1 = e.clientX;
  2623. y1 = e.clientY;
  2624.  
  2625. const newLeft = keepWithinLimits('left', modal.offsetLeft - x2);
  2626. const newTop = keepWithinLimits('top', modal.offsetTop - y2);
  2627.  
  2628. // Set the element's new position.
  2629. modal.style.left = `${newLeft}px`;
  2630. modal.style.top = `${newTop}px`;
  2631. };
  2632.  
  2633. // Set the default position.
  2634. let startX = defaultX || 0;
  2635. let startY = defaultY || 0;
  2636.  
  2637. // If the storageKey was passed in, get the position from local storage.
  2638. if (! storageKey) {
  2639. storageKey = `mh-draggable-${dragTarget}-${dragHandle}`;
  2640. }
  2641.  
  2642. if (savePosition) {
  2643. const storedPosition = localStorage.getItem(storageKey);
  2644. if (storedPosition) {
  2645. const position = JSON.parse(storedPosition);
  2646.  
  2647. // Make sure the position is within the bounds of the window.
  2648. startX = keepWithinLimits('left', position.x);
  2649. startY = keepWithinLimits('top', position.y);
  2650. }
  2651. }
  2652.  
  2653. // Set the element's position.
  2654. modal.style.left = `${startX}px`;
  2655. modal.style.top = `${startY}px`;
  2656.  
  2657. // Set up our variables to track the mouse position.
  2658. let x1 = 0,
  2659. y1 = 0,
  2660. x2 = 0,
  2661. y2 = 0;
  2662.  
  2663. // Add the event listener to the handle.
  2664. handle.onmousedown = onMouseDown;
  2665. };
  2666.  
  2667. const makeDraggableModal = (opts) => {
  2668. const {
  2669. id,
  2670. title,
  2671. content,
  2672. defaultX,
  2673. defaultY,
  2674. storageKey,
  2675. savePosition,
  2676. } = opts;
  2677.  
  2678. // set the defaults for the options
  2679. opts = Object.assign({
  2680. id: 'mh-utils-modal',
  2681. title: '',
  2682. content: '',
  2683. defaultX: null,
  2684. defaultY: null,
  2685. storageKey: 'mh-utils-modal',
  2686. savePosition: true,
  2687. }, opts);
  2688.  
  2689. // Remove the existing modal.
  2690. const existing = document.getElementById(`mh-utils-modal-${id}`);
  2691. if (existing) {
  2692. existing.remove();
  2693. }
  2694.  
  2695. // Create the modal.
  2696. const modalWrapper = makeElement('div', 'mh-utils-modal-wrapper');
  2697. modalWrapper.id = `mh-utils-modal-${id}`;
  2698.  
  2699. const modal = makeElement('div', 'mh-utils-modal');
  2700. const header = makeElement('div', 'mh-utils-modal-header');
  2701. makeElement('h1', 'mh-utils-modal-title', title, header);
  2702.  
  2703. // Create a close button icon.
  2704. const closeIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  2705. closeIcon.classList.add('mh-utils-modal-close');
  2706. closeIcon.setAttribute('viewBox', '0 0 24 24');
  2707. closeIcon.setAttribute('width', '18');
  2708. closeIcon.setAttribute('height', '18');
  2709. closeIcon.setAttribute('fill', 'none');
  2710. closeIcon.setAttribute('stroke', 'currentColor');
  2711. closeIcon.setAttribute('stroke-width', '1.5');
  2712.  
  2713. // Create the path.
  2714. const closePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  2715. closePath.setAttribute('d', 'M18 6L6 18M6 6l12 12');
  2716. closeIcon.appendChild(closePath);
  2717.  
  2718. // Close the modal when the icon is clicked.
  2719. closeIcon.addEventListener('click', () => {
  2720. modalWrapper.remove();
  2721. });
  2722.  
  2723. // Append the button.
  2724. header.appendChild(closeIcon);
  2725.  
  2726. // Add the header to the modal.
  2727. modal.appendChild(header);
  2728.  
  2729. // Make the mouse stats table.
  2730. const mouseBody =
  2731. document.createElement('div');
  2732. mouseBody.classList.add('mh-utils-modal-body');
  2733.  
  2734. modal.appendChild(content);
  2735.  
  2736. // Add the modal to the wrapper.
  2737. modalWrapper.appendChild(modal);
  2738.  
  2739. // Add the wrapper to the body.
  2740. document.body.appendChild(modalWrapper);
  2741.  
  2742. // Make the modal draggable.
  2743. makeElementDraggable(
  2744. `mh-utils-modal-${id}`,
  2745. 'mh-utils-modal',
  2746. 'mh-utils-modal-header',
  2747. defaultX,
  2748. defaultY,
  2749. storageKey,
  2750. savePosition
  2751. );
  2752. };
  2753.  
  2754. /**
  2755. * Creates an element with the given tag, classname, text, and appends it to the given element.
  2756. *
  2757. * @param {string} tag The tag of the element to create.
  2758. * @param {string} classes The classes of the element to create.
  2759. * @param {string} text The text of the element to create.
  2760. * @param {HTMLElement} appendTo The element to append the created element to.
  2761. *
  2762. * @return {HTMLElement} The created element.
  2763. */
  2764. const makeElement = (tag, classes = '', text = '', appendTo = null) => {
  2765. const element = document.createElement(tag);
  2766.  
  2767. // if classes is an array, join it with a space.
  2768. if (Array.isArray(classes)) {
  2769. classes = classes.join(' ');
  2770. }
  2771.  
  2772. element.className = classes;
  2773. element.innerHTML = text;
  2774.  
  2775. if (appendTo) {
  2776. appendTo.appendChild(element);
  2777. return appendTo;
  2778. }
  2779.  
  2780. return element;
  2781. };
  2782.  
  2783. /**
  2784. * Return an anchor element with the given text and href.
  2785. *
  2786. * @param {string} text Text to use for link.
  2787. * @param {string} href URL to link to.
  2788. * @param {boolean} tiny Use the tiny button style.
  2789. * @param {Array} extraClasses Extra classes to add to the link.
  2790. * @param {boolean} encodeAsSpace Encode spaces as %20 instead of _.
  2791. *
  2792. * @return {string} HTML for link.
  2793. */
  2794. const makeButton = (text, href, tiny = true, extraClasses = [], encodeAsSpace = false) => {
  2795. href = href.replace(/\s/g, '_');
  2796.  
  2797. if (encodeAsSpace) {
  2798. href = href.replace(/_/g, '%20');
  2799. } else {
  2800. href = href.replace(/\s/g, '_');
  2801. }
  2802.  
  2803. href = href.replace(/\$/g, '_');
  2804.  
  2805. return `<a href="${href}" class="mousehuntActionButton ${tiny ? 'tiny' : ''} ${extraClasses.join(' ')}"><span>${text}</span></a>`;
  2806. };
  2807.  
  2808. /**
  2809. * Creates a popup with two choices.
  2810. *
  2811. * createChoicePopup({
  2812. * title: 'Choose your first trap',
  2813. * choices: [
  2814. * {
  2815. * id: 'treasurer_mouse',
  2816. * name: 'Treasurer',
  2817. * image: 'https://www.mousehuntgame.com/images/mice/medium/bb55034f6691eb5e3423927e507b5ec9.jpg?cv=2',
  2818. * meta: 'Mouse',
  2819. * text: 'This is a mouse',
  2820. * button: 'Select',
  2821. * callback: () => {
  2822. * console.log('treasurer selected');
  2823. * }
  2824. * },
  2825. * {
  2826. * id: 'high_roller_mouse',
  2827. * name: 'High Roller',
  2828. * image: 'https://www.mousehuntgame.com/images/mice/medium/3f71c32f9d8da2b2727fc8fd288f7974.jpg?cv=2',
  2829. * meta: 'Mouse',
  2830. * text: 'This is a mouse',
  2831. * button: 'Select',
  2832. * callback: () => {
  2833. * console.log('high roller selected');
  2834. * }
  2835. * },
  2836. * ],
  2837. * });
  2838. *
  2839. * @param {Object} options The options for the popup.
  2840. * @param {string} options.title The title of the popup.
  2841. * @param {Array} options.choices The choices for the popup.
  2842. * @param {string} options.choices[].id The ID of the choice.
  2843. * @param {string} options.choices[].name The name of the choice.
  2844. * @param {string} options.choices[].image The image of the choice.
  2845. * @param {string} options.choices[].meta The smaller text under the name.
  2846. * @param {string} options.choices[].text The description of the choice.
  2847. * @param {string} options.choices[].button The text of the button.
  2848. * @param {string} options.choices[].action The action to take when the button is clicked.
  2849. */
  2850. const createChoicePopup = (options) => {
  2851. let choices = '';
  2852. const numChoices = options.choices.length;
  2853. let currentChoice = 0;
  2854.  
  2855. options.choices.forEach((choice) => {
  2856. choices += `<a href="#" id=${choice.id}" class="weaponContainer">
  2857. <div class="weapon">
  2858. <div class="trapImage" style="background-image: url(${choice.image});"></div>
  2859. <div class="trapDetails">
  2860. <div class="trapName">${choice.name}</div>
  2861. <div class="trapDamageType">${choice.meta}</div>
  2862. <div class="trapDescription">${choice.text}</div>
  2863. <div class="trapButton" id="${choice.id}-action">${choice.button || 'Select'}</div>
  2864. </div>
  2865. </div>
  2866. </a>`;
  2867.  
  2868. currentChoice++;
  2869. if (currentChoice < numChoices) {
  2870. choices += '<div class="spacer"></div>';
  2871. }
  2872. });
  2873.  
  2874. const content = `<div class="trapIntro">
  2875. <div id="OnboardArrow" class="larryCircle">
  2876. <div class="woodgrain">
  2877. <div class="whiteboard">${options.title}</div>
  2878. </div>
  2879. <div class="characterContainer">
  2880. <div class="character"></div>
  2881. </div>
  2882. </div>
  2883. </div>
  2884. <div>
  2885. ${choices}
  2886. </div>`;
  2887.  
  2888. hg.views.MessengerView.addMessage({
  2889. content: { body: content },
  2890. css_class: 'chooseTrap',
  2891. show_overlay: true,
  2892. is_modal: true
  2893. });
  2894. hg.views.MessengerView.go();
  2895.  
  2896. options.choices.forEach((choice) => {
  2897. const target = document.querySelector(`#${choice.id}-action`);
  2898. if (target) {
  2899. target.addEventListener('click', () => {
  2900. hg.views.MessengerView.hide();
  2901. if (choice.action) {
  2902. choice.action();
  2903. }
  2904. });
  2905. }
  2906. });
  2907. };
  2908.  
  2909. /**
  2910. * Creates a favorite button that can toggle.
  2911. *
  2912. * @async
  2913. *
  2914. * @example <caption>Creating a favorite button</caption>
  2915. * createFavoriteButton({
  2916. * id: 'testing_favorite',
  2917. * target: infobar,
  2918. * size: 'small',
  2919. * defaultState: false,
  2920. * });
  2921. *
  2922. * @param {Object} options The options for the button.
  2923. * @param {string} options.selector The selector for the button.
  2924. * @param {string} options.size Whether or not to use the small version of the button.
  2925. * @param {string} options.active Whether or not the button should be active by default.
  2926. * @param {string} options.onChange The function to run when the button is toggled.
  2927. * @param {string} options.onActivate The function to run when the button is activated.
  2928. * @param {string} options.onDeactivate The function to run when the button is deactivated.
  2929. */
  2930. const createFavoriteButton = async (options) => {
  2931. addStyles(`.custom-favorite-button {
  2932. top: 0;
  2933. right: 0;
  2934. display: inline-block;
  2935. width: 35px;
  2936. height: 35px;
  2937. vertical-align: middle;
  2938. background: url(https://www.mousehuntgame.com/images/ui/camp/trap/star_empty.png?asset_cache_version=2) 50% 50% no-repeat;
  2939. background-size: 90%;
  2940. border-radius: 50%;
  2941. }
  2942.  
  2943. .custom-favorite-button-small {
  2944. width: 20px;
  2945. height: 20px;
  2946. }
  2947.  
  2948. .custom-favorite-button:hover {
  2949. background-color: #fff;
  2950. outline: 2px solid #ccc;
  2951. background-image: url(https://www.mousehuntgame.com/images/ui/camp/trap/star_favorite.png?asset_cache_version=2);
  2952. }
  2953.  
  2954. .custom-favorite-button.active {
  2955. background-image: url(https://www.mousehuntgame.com/images/ui/camp/trap/star_favorite.png?asset_cache_version=2);
  2956. }
  2957.  
  2958. .custom-favorite-button.busy {
  2959. background-image: url(https://www.mousehuntgame.com/images/ui/loaders/small_spinner.gif?asset_cache_version=2);
  2960. }
  2961. `, 'custom-favorite-button', true);
  2962.  
  2963. const {
  2964. id = null,
  2965. target = null,
  2966. size = 'small',
  2967. state = false,
  2968. isSetting = true,
  2969. defaultState = false,
  2970. onChange = null,
  2971. onActivate = null,
  2972. onDeactivate = null,
  2973. } = options;
  2974.  
  2975. const star = document.createElement('a');
  2976.  
  2977. star.classList.add('custom-favorite-button');
  2978. if (size === 'small') {
  2979. star.classList.add('custom-favorite-button-small');
  2980. }
  2981.  
  2982. star.setAttribute('data-item-id', id);
  2983. star.setAttribute('href', '#');
  2984.  
  2985. star.style.display = 'inline-block';
  2986.  
  2987. let currentSetting = false;
  2988. if (isSetting) {
  2989. currentSetting = getSetting(id, defaultState);
  2990. } else {
  2991. currentSetting = state;
  2992. }
  2993.  
  2994. if (currentSetting) {
  2995. star.classList.add('active');
  2996. } else {
  2997. star.classList.add('inactive');
  2998. }
  2999.  
  3000. star.addEventListener('click', async (e) => {
  3001. star.classList.add('busy');
  3002. e.preventDefault();
  3003. e.stopPropagation();
  3004. const currentStar = e.target;
  3005. const currentState = ! currentStar.classList.contains('active');
  3006.  
  3007. if (onChange !== null) {
  3008. await onChange(currentState);
  3009. } else if (isSetting) {
  3010. saveSetting(id, currentState);
  3011. }
  3012.  
  3013. currentStar.classList.remove('inactive');
  3014. currentStar.classList.remove('active');
  3015.  
  3016. if (currentState) {
  3017. currentStar.classList.add('active');
  3018. if (onActivate !== null) {
  3019. await onActivate(currentState);
  3020. }
  3021. } else {
  3022. currentStar.classList.add('inactive');
  3023. if (onDeactivate !== null) {
  3024. await onDeactivate(currentState);
  3025. }
  3026. }
  3027.  
  3028. currentStar.classList.remove('busy');
  3029. });
  3030.  
  3031. if (target) {
  3032. target.appendChild(star);
  3033. }
  3034.  
  3035. return star;
  3036. };
  3037.  
  3038. /**
  3039. * Wait for a specified amount of time.
  3040. *
  3041. * @param {number} ms The number of milliseconds to wait.
  3042. */
  3043. const wait = (ms) => {
  3044. return new Promise((resolve) => setTimeout(resolve, ms));
  3045. };
  3046.  
  3047. /**
  3048. * Log to the console.
  3049. *
  3050. * @param {string|Object} message The message to log.
  3051. * @param {Object} args The arguments to pass to the console.
  3052. */
  3053. const clog = (message, ...args) => {
  3054. // If a string is passed in, log it in line with our prefix.
  3055. if ('string' === typeof message) {
  3056. console.log(`%c[MH Utils] %c${message}`, 'color: #ff0000; font-weight: bold;', 'color: #000000;'); // eslint-disable-line no-console
  3057. console.log(...args); // eslint-disable-line no-console
  3058. } else {
  3059. // Otherwise, log it separately.
  3060. console.log('%c[MH Utils]', 'color: #ff0000; font-weight: bold;'); // eslint-disable-line no-console
  3061. console.log(message); // eslint-disable-line no-console
  3062. }
  3063. };
  3064.  
  3065. /**
  3066. * Log to the console if debug mode is enabled.
  3067. *
  3068. * @param {string|Object} message The message to log.
  3069. * @param {Object} args The arguments to pass to the console.
  3070. */
  3071. const debug = (message, ...args) => {
  3072. if (getSetting('debug-mode', false)) {
  3073. clog(message, ...args);
  3074. }
  3075. };
  3076.  
  3077. /**
  3078. * Add a setting to enable debug mode.
  3079. */
  3080. const enableDebugMode = () => {
  3081. const debugSettings = {
  3082. debugModeEnabled: true,
  3083. debug: getSetting('debug-mode', false)
  3084. };
  3085.  
  3086. window.mhutils = window.mhutils ? { ...window.mhutils, ...debugSettings } : debugSettings;
  3087.  
  3088. addSetting('Debug Mode', 'debug-mode', false, 'Enable debug mode', {}, 'game_settings');
  3089. };
  3090.  
  3091. /**
  3092. * Helper to run a callback when loaded, on ajax request, on overlay close, and on travel.
  3093. *
  3094. * @param {Function} action The callback to run.
  3095. */
  3096. const run = async (action) => {
  3097. action();
  3098. onAjaxRequest(action);
  3099. onOverlayClose(action);
  3100. onTravel(null, { callback: action });
  3101. };
  3102.  
  3103. /**
  3104. * Check if dark mode is enabled.
  3105. *
  3106. * @return {boolean} True if dark mode is enabled, false otherwise.
  3107. */
  3108. const isDarkMode = () => {
  3109. return !! getComputedStyle(document.documentElement).getPropertyValue('--mhdm-white');
  3110. };
  3111.  
  3112. /**
  3113. * Adds classes to the body to enable styling based on the location or if dark mode is enabled.
  3114. */
  3115. const addBodyClasses = () => {
  3116. const addLocationBodyClass = () => {
  3117. const addClass = () => {
  3118. const location = getCurrentLocation();
  3119. document.body.classList.add(`mh-location-${location}`);
  3120. };
  3121.  
  3122. addClass();
  3123. onTravel(null, { callback: addClass });
  3124. };
  3125.  
  3126. const addDarkModeBodyClass = () => {
  3127. if (isDarkMode()) {
  3128. document.body.classList.add('mh-dark-mode');
  3129. }
  3130. };
  3131.  
  3132. addLocationBodyClass();
  3133. addDarkModeBodyClass();
  3134. };
  3135.  
  3136. /**
  3137. * Wait for the app to initialize, then add classes to the body.
  3138. */
  3139. setTimeout(() => {
  3140. addBodyClasses();
  3141. eventRegistry.addEventListener('app_init', addBodyClasses);
  3142. }, 250);