Hordes UI Mod

Various UI mods for Hordes.io.

当前为 2019-12-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Hordes UI Mod
  3. // @version 0.131
  4. // @description Various UI mods for Hordes.io.
  5. // @author Sakaiyo
  6. // @match https://hordes.io/play
  7. // @grant GM_addStyle
  8. // @namespace https://greasyfork.org/users/160017
  9. // ==/UserScript==
  10. /**
  11. * TODO: Implement chat tabs
  12. * TODO: Implement inventory sorting
  13. * TODO: (Maybe) Improved healer party frames
  14. * TODO: Opacity scaler for map
  15. * TODO: Context menu when right clicking username in chat, party invite and whisper.
  16. * Check if it's ok to emulate keypresses before releasing. If not, then maybe copy text to clipboard.
  17. * TODO: FIX BUG: Add support for resizing map back to saved position after minimizing it, from maximized position
  18. * TODO (Maybe): Ability to make GM chat look like normal chat?
  19. */
  20. (function() {
  21. 'use strict';
  22.  
  23. // If this version is different from the user's stored state,
  24. // e.g. they have upgraded the version of this script and there are breaking changes,
  25. // then their stored state will be deleted.
  26. const BREAKING_VERSION = 1;
  27.  
  28. // The width+height of the maximized chat, so we don't save map size when it's maximized
  29. // TODO: FIX BUG: This is NOT everyones max size. INSTEAD OF USING A STATIC SIZE, we should detect large instant resizes
  30. // Should also do this to avoid saving when minimizing menu after maximizing
  31. const CHAT_MAXIMIZED_SIZE = 692;
  32.  
  33. const STORAGE_STATE_KEY = 'hordesio-uimodsakaiyo-state';
  34. const CHAT_LVLUP_CLASS = 'js-chat-lvlup';
  35. const CHAT_GM_CLASS = 'js-chat-gm';
  36.  
  37. let state = {
  38. breakingVersion: BREAKING_VERSION,
  39. chat: {
  40. lvlup: true,
  41. GM: true,
  42. },
  43. windowsPos: {},
  44. };
  45.  
  46. // UPDATING STYLES BELOW - Must be invoked in main function
  47. GM_addStyle(`
  48. /* Transparent chat bg color */
  49. .frame.svelte-1vrlsr3 {
  50. background: rgba(0,0,0,0.4);
  51. }
  52.  
  53. /* Transparent map */
  54. .svelte-hiyby7 {
  55. opacity: 0.7;
  56. }
  57.  
  58. /* Allows windows to be moved */
  59. .window {
  60. position: relative;
  61. }
  62.  
  63. /* Allows last clicked window to appear above all other windows */
  64. .js-is-top {
  65. z-index: 9998 !important;
  66. }
  67. .panel.context:not(.commandlist) {
  68. z-index: 9999 !important;
  69. }
  70.  
  71. /* Custom chat filter colors */
  72. .js-chat-lvlup {
  73. color: #EE960B;
  74. }
  75. .js-chat-inv {
  76. color: #a6dcd5;
  77. }
  78. .js-chat-gm {
  79. color: #a6dcd5;
  80. }
  81.  
  82. /* Class that hides chat lines*/
  83. .js-line-hidden {
  84. display: none;
  85. }
  86.  
  87. /* Enable chat & map resize */
  88. .js-chat-resize {
  89. resize: both;
  90. overflow: auto;
  91. }
  92. .js-map-resize:hover {
  93. resize: both;
  94. overflow: auto;
  95. direction: rtl;
  96. }
  97.  
  98. /* The browser resize icon */
  99. *::-webkit-resizer {
  100. background: linear-gradient(to right, rgba(51, 77, 80, 0), rgba(203, 202, 165, 0.5));
  101. border-radius: 8px;
  102. box-shadow: 0 1px 1px rgba(0,0,0,1);
  103. }
  104. *::-moz-resizer {
  105. background: linear-gradient(to right, rgba(51, 77, 80, 0), rgba(203, 202, 165, 0.5));
  106. border-radius: 8px;
  107. box-shadow: 0 1px 1px rgba(0,0,0,1);
  108. }
  109. `);
  110.  
  111.  
  112. const modHelpers = {
  113. // Filters all chat based on custom filters
  114. filterAllChat: () => {
  115. Object.keys(state.chat).forEach(channel => {
  116. Array.from(document.querySelectorAll(`.text${channel}.content`)).forEach($textItem => {
  117. const $line = $textItem.parentNode.parentNode;
  118. $line.classList.toggle('js-line-hidden', !state.chat[channel]);
  119. });
  120. });
  121. },
  122. };
  123.  
  124. // MAIN MODS BELOW
  125. const mods = [
  126. // Creates DOM elements for custom chat filters
  127. function newChatFilters() {
  128. const $channelselect = document.querySelector('.channelselect');
  129. if (!document.querySelector(`.${CHAT_LVLUP_CLASS}`)) {
  130. const $lvlup = createElement({
  131. element: 'small',
  132. class: `btn border black ${CHAT_LVLUP_CLASS} ${state.chat.lvlup ? '' : 'textgrey'}`,
  133. content: 'lvlup'
  134. });
  135. $channelselect.appendChild($lvlup);
  136. }
  137. if (!document.querySelector(`.${CHAT_GM_CLASS}`)) {
  138. const $gm = createElement({
  139. element: 'small',
  140. class: `btn border black ${CHAT_GM_CLASS} ${state.chat.GM ? '' : 'textgrey'}`,
  141. content: 'GM'
  142. });
  143. $channelselect.appendChild($gm);
  144. }
  145. },
  146.  
  147. // Wire up new chat buttons to toggle in state+ui
  148. function newChatFilterButtons() {
  149. const $chatLvlup = document.querySelector(`.${CHAT_LVLUP_CLASS}`);
  150. $chatLvlup.addEventListener('click', () => {
  151. state.chat.lvlup = !state.chat.lvlup;
  152. $chatLvlup.classList.toggle('textgrey', !state.chat.lvlup);
  153. modHelpers.filterAllChat();
  154. save({chat: state.chat});
  155. });
  156.  
  157. const $chatGM = document.querySelector(`.${CHAT_GM_CLASS}`);
  158. $chatGM.addEventListener('click', () => {
  159. state.chat.GM = !state.chat.GM;
  160. $chatGM.classList.toggle('textgrey', !state.chat.GM);
  161. modHelpers.filterAllChat();
  162. save({chat: state.chat});
  163. });
  164. },
  165.  
  166. // Filter out chat in UI based on chat buttons state
  167. function filterChatObserver() {
  168. const chatObserver = new MutationObserver(modHelpers.filterAllChat);
  169. chatObserver.observe(document.querySelector('#chat'), { attributes: true, childList: true });
  170. },
  171.  
  172. // Drag all windows by their header
  173. function draggableUIWindows() {
  174. Array.from(document.querySelectorAll('.window:not(.js-can-move)')).forEach($window => {
  175. $window.classList.add('js-can-move');
  176. dragElement($window, $window.querySelector('.titleframe'));
  177. });
  178. },
  179.  
  180. // Save dragged UI windows position to state
  181. function saveDraggedUIWindows() {
  182. Array.from(document.querySelectorAll('.window:not(.js-window-is-saving)')).forEach($window => {
  183. $window.classList.add('js-window-is-saving');
  184. const $draggableTarget = $window.querySelector('.titleframe');
  185. const windowName = $draggableTarget.querySelector('[name="title"]').textContent;
  186. $draggableTarget.addEventListener('mouseup', () => {
  187. state.windowsPos[windowName] = $window.getAttribute('style');
  188. save({windowsPos: state.windowsPos});
  189. });
  190. });
  191. },
  192.  
  193. // Loads draggable UI windows position from state
  194. function loadDraggedUIWindowsPositions() {
  195. Array.from(document.querySelectorAll('.window:not(.js-has-loaded-pos)')).forEach($window => {
  196. $window.classList.add('js-has-loaded-pos');
  197. const windowName = $window.querySelector('[name="title"]').textContent;
  198. const pos = state.windowsPos[windowName];
  199. if (pos) {
  200. $window.setAttribute('style', pos);
  201. }
  202. });
  203. },
  204.  
  205. // Makes chat resizable
  206. function resizableChat() {
  207. // Add the appropriate classes
  208. const $chatContainer = document.querySelector('#chat').parentNode;
  209. $chatContainer.classList.add('js-chat-resize');
  210.  
  211. // Load initial chat and map size
  212. if (state.chatWidth && state.chatHeight) {
  213. $chatContainer.style.width = state.chatWidth;
  214. $chatContainer.style.height = state.chatHeight;
  215. }
  216.  
  217. // Save chat size on resize
  218. const resizeObserverChat = new ResizeObserver(() => {
  219. const chatWidthStr = window.getComputedStyle($chatContainer, null).getPropertyValue('width');
  220. const chatHeightStr = window.getComputedStyle($chatContainer, null).getPropertyValue('height');
  221. save({
  222. chatWidth: chatWidthStr,
  223. chatHeight: chatHeightStr,
  224. });
  225. });
  226. resizeObserverChat.observe($chatContainer);
  227. },
  228.  
  229. // Makes map resizable
  230. function resizeableMap() {
  231. const $map = document.querySelector('.svelte-hiyby7');
  232. const $canvas = $map.querySelector('canvas');
  233. $map.classList.add('js-map-resize');
  234.  
  235. const onMapResize = () => {
  236. // Get real values of map height/width, excluding padding/margin/etc
  237. const mapWidthStr = window.getComputedStyle($map, null).getPropertyValue('width');
  238. const mapHeightStr = window.getComputedStyle($map, null).getPropertyValue('height');
  239. const mapWidth = Number(mapWidthStr.slice(0, -2));
  240. const mapHeight = Number(mapHeightStr.slice(0, -2));
  241.  
  242. // If height/width are 0 or unset, don't resize canvas
  243. if (!mapWidth || !mapHeight) {
  244. return;
  245. }
  246.  
  247. if ($canvas.width !== mapWidth) {
  248. $canvas.width = mapWidth;
  249. }
  250.  
  251. if ($canvas.height !== mapHeight) {
  252. $canvas.height = mapHeight;
  253. }
  254.  
  255. // Save map size on resize, unless map has been maximized by user
  256. if (mapWidth !== CHAT_MAXIMIZED_SIZE && mapHeight !== CHAT_MAXIMIZED_SIZE) {
  257. save({
  258. mapWidth: mapWidthStr,
  259. mapHeight: mapHeightStr,
  260. });
  261. }
  262. };
  263.  
  264. if (state.mapWidth && state.mapHeight) {
  265. $map.style.width = state.mapWidth;
  266. $map.style.height = state.mapHeight;
  267. onMapResize(); // Update canvas size on initial load of saved map size
  268. }
  269.  
  270. // On resize of map, resize canvas to match
  271. const resizeObserverMap = new ResizeObserver(onMapResize);
  272. resizeObserverMap.observe($map);
  273.  
  274. // We need to observe canvas resizes to tell when the user presses M to open the big map
  275. // At that point, we resize the map to match the canvas
  276. const triggerResize = () => {
  277. // Get real values of map height/width, excluding padding/margin/etc
  278. const mapWidthStr = window.getComputedStyle($map, null).getPropertyValue('width');
  279. const mapHeightStr = window.getComputedStyle($map, null).getPropertyValue('height');
  280. const mapWidth = Number(mapWidthStr.slice(0, -2));
  281. const mapHeight = Number(mapHeightStr.slice(0, -2));
  282.  
  283. // If height/width are 0 or unset, we don't care about resizing yet
  284. if (!mapWidth || !mapHeight) {
  285. return;
  286. }
  287.  
  288. if ($canvas.width !== mapWidth) {
  289. $map.style.width = `${$canvas.width}px`;
  290. }
  291.  
  292. if ($canvas.height !== mapHeight) {
  293. $map.style.height = `${$canvas.height}px`;
  294. }
  295. };
  296.  
  297. // We debounce the canvas resize, so it doesn't resize every single
  298. // pixel you move when resizing the DOM. If this were to happen,
  299. // resizing would constantly be interrupted. You'd have to resize a tiny bit,
  300. // lift left click, left click again to resize a tiny bit more, etc.
  301. // Resizing is smooth when we debounce this canvas.
  302. const debouncedTriggerResize = debounce(triggerResize, 200);
  303. const resizeObserverCanvas = new ResizeObserver(debouncedTriggerResize);
  304. resizeObserverCanvas.observe($canvas);
  305. },
  306.  
  307. // The last clicked UI window displays above all other UI windows
  308. // This is useful when, for example, your inventory is near the market window,
  309. // and you want the window and the tooltips to display above the market window.
  310. function selectedWindowIsTop() {
  311. Array.from(document.querySelectorAll('.window:not(.js-is-top-initd)')).forEach($window => {
  312. $window.classList.add('js-is-top-initd');
  313.  
  314. $window.addEventListener('mousedown', () => {
  315. // First, make the other is-top window not is-top
  316. const $otherWindowContainer = document.querySelector('.js-is-top');
  317. if ($otherWindowContainer) {
  318. $otherWindowContainer.classList.remove('js-is-top');
  319. }
  320.  
  321. // Then, make our window's container (the z-index container) is-top
  322. $window.parentNode.classList.add('js-is-top');
  323. });
  324. });
  325. },
  326. ];
  327.  
  328. // Add new DOM, load our stored state, wire it up, then continuously rerun specific methods whenever UI changes
  329. function initialize() {
  330. // If the Hordes.io tab isn't active for long enough, it reloads the entire page, clearing this mod
  331. // We check for that and reinitialize the mod if that happens
  332. const $layout = document.querySelector('.layout');
  333. if ($layout.classList.contains('uimod-initd')) {
  334. return;
  335. }
  336.  
  337. $layout.classList.add('uimod-initd')
  338. load();
  339. mods.forEach(mod => mod());
  340.  
  341. // Continuously re-run specific mods methods that need to be executed on UI change
  342. const rerunObserver = new MutationObserver(() => {
  343. // If new window appears, e.g. even if window is closed and reopened, we need to rewire it
  344. // Fun fact: Some windows always exist in the DOM, even when hidden, e.g. Inventory
  345. // But some windows only exist in the DOM when open, e.g. Interaction
  346. const modsToRerun = [
  347. 'saveDraggedUIWindows',
  348. 'draggableUIWindows',
  349. 'loadDraggedUIWindowsPositions',
  350. 'selectedWindowIsTop',
  351. ];
  352. modsToRerun.forEach(modName => {
  353. mods.find(mod => mod.name === modName)();
  354. });
  355. });
  356. rerunObserver.observe(document.querySelector('.layout > .container'), { attributes: false, childList: true, })
  357. }
  358.  
  359. // Initialize mods once UI DOM has loaded
  360. const pageObserver = new MutationObserver(() => {
  361. const isUiLoaded = !!document.querySelector('.layout');
  362. if (isUiLoaded) {
  363. initialize();
  364. }
  365. });
  366. pageObserver.observe(document.body, { attributes: true, childList: true })
  367.  
  368. // UTIL METHODS
  369. // Save to in-memory state and localStorage to retain on refresh
  370. function save(items) {
  371. state = {
  372. ...state,
  373. ...items,
  374. };
  375. localStorage.setItem(STORAGE_STATE_KEY, JSON.stringify(state));
  376. }
  377.  
  378. // Load localStorage state if it exists
  379. // NOTE: If user is trying to load unsupported version of stored state,
  380. // e.g. they just upgraded to breaking version, then we delete their stored state
  381. function load() {
  382. const storedStateJson = localStorage.getItem(STORAGE_STATE_KEY)
  383. if (storedStateJson) {
  384. const storedState = JSON.parse(storedStateJson);
  385. if (storedState.breakingVersion !== BREAKING_VERSION) {
  386. localStorage.setItem(STORAGE_STATE_KEY, JSON.stringify(state));
  387. return;
  388. }
  389. state = {
  390. ...state,
  391. ...storedState,
  392. };
  393. }
  394. }
  395.  
  396. // Nicer impl to create elements in one method call
  397. function createElement(args) {
  398. const $node = document.createElement(args.element);
  399. if (args.class) { $node.className = args.class; }
  400. if (args.content) { $node.innerHTML = args.content; }
  401. if (args.src) { $node.src = args.src; }
  402. return $node;
  403. }
  404.  
  405. // ...Can't remember why I added this.
  406. // TODO: Remove this if not using. Can access chat input with it
  407. function simulateEnterPress() {
  408. const kbEvent = new KeyboardEvent("keydown", {
  409. bubbles: true, cancelable: true, keyCode: 13
  410. });
  411. document.body.dispatchEvent(kbEvent);
  412. }
  413.  
  414. // Credit: https://stackoverflow.com/a/14234618 (Has been slightly modified)
  415. // $draggedElement is the item that will be dragged.
  416. // $dragTrigger is the element that must be held down to drag $draggedElement
  417. function dragElement($draggedElement, $dragTrigger) {
  418. let offset = [0,0];
  419. let isDown = false;
  420. $dragTrigger.addEventListener('mousedown', function(e) {
  421. isDown = true;
  422. offset = [
  423. $draggedElement.offsetLeft - e.clientX,
  424. $draggedElement.offsetTop - e.clientY
  425. ];
  426. }, true);
  427. document.addEventListener('mouseup', function() {
  428. isDown = false;
  429. }, true);
  430.  
  431. document.addEventListener('mousemove', function(e) {
  432. event.preventDefault();
  433. if (isDown) {
  434. $draggedElement.style.left = (e.clientX + offset[0]) + 'px';
  435. $draggedElement.style.top = (e.clientY + offset[1]) + 'px';
  436. }
  437. }, true);
  438. }
  439.  
  440. // Credit: David Walsh
  441. function debounce(func, wait, immediate) {
  442. var timeout;
  443. return function() {
  444. var context = this, args = arguments;
  445. var later = function() {
  446. timeout = null;
  447. if (!immediate) func.apply(context, args);
  448. };
  449. var callNow = immediate && !timeout;
  450. clearTimeout(timeout);
  451. timeout = setTimeout(later, wait);
  452. if (callNow) func.apply(context, args);
  453. };
  454. }
  455. })();