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