🐭️ MouseHunt Utils

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

当前为 2023-10-07 提交的版本,查看 最新版本

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

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