🐭️ MouseHunt Utils

MouseHunt Utils is a library of functions that can be used to make other MouseHunt userscripts easily.

当前为 2023-08-04 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/464008/1230586/%F0%9F%90%AD%EF%B8%8F%20MouseHunt%20Utils.js

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