您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Various UI mods for Hordes.io.
当前为
- // ==UserScript==
- // @name Hordes UI Mod
- // @version 0.170
- // @description Various UI mods for Hordes.io.
- // @author Sakaiyo
- // @match https://hordes.io/play
- // @grant GM_addStyle
- // @namespace https://greasyfork.org/users/160017
- // ==/UserScript==
- /**
- * TODO: Implement chat tabs
- * TODO: Implement inventory sorting
- * TODO: (Maybe) Improved healer party frames
- * TODO: FIX BUG: Add support for resizing map back to saved position after minimizing it, from maximized position
- * TODO: (Maybe): Add toggleable option to include chat messages to right of party frame
- * TODO: Remove all reliance on svelte- classes, likely breaks with updates
- * TODO: Add cooldown on skills (leverage skill icon URL, have a map for each skill icon mapping to its respective cooldown)
- */
- (function() {
- 'use strict';
- // If this version is different from the user's stored state,
- // e.g. they have upgraded the version of this script and there are breaking changes,
- // then their stored state will be deleted.
- const BREAKING_VERSION = 1;
- const VERSION = '0.170'; // Should match version in UserScript description
- // The width+height of the maximized chat, so we don't save map size when it's maximized
- // TODO: FIX BUG: This is NOT everyones max size. INSTEAD OF USING A STATIC SIZE, we should detect large instant resizes
- // Should also do this to avoid saving when minimizing menu after maximizing
- const CHAT_MAXIMIZED_SIZE = 692;
- const STORAGE_STATE_KEY = 'hordesio-uimodsakaiyo-state';
- const CHAT_GM_CLASS = 'js-chat-gm';
- let state = {
- breakingVersion: BREAKING_VERSION,
- chat: {
- GM: true,
- },
- windowsPos: {},
- blockList: {},
- friendsList: {},
- mapOpacity: 70, // e.g. 70 = opacity: 0.7
- };
- // tempState is saved only between page refreshes.
- const tempState = {
- // The last name clicked in chat
- chatName: null
- };
- // UPDATING STYLES BELOW - Must be invoked in main function
- GM_addStyle(`
- /* Transparent chat bg color */
- .frame.svelte-1vrlsr3 {
- background: rgba(0,0,0,0.4);
- }
- /* Our mod's chat message color */
- .textuimod {
- color: #00dd33;
- }
- /* Allows windows to be moved */
- .window {
- position: relative;
- }
- /* Allows last clicked window to appear above all other windows */
- .js-is-top {
- z-index: 9998 !important;
- }
- .panel.context:not(.commandlist) {
- z-index: 9999 !important;
- }
- /* The item icon being dragged in the inventory */
- .container.svelte-120o2pb {
- z-index: 9999 !important;
- }
- /* All purpose hidden class */
- .js-hidden {
- display: none;
- }
- /* Custom chat context menu, invisible by default */
- .js-chat-context-menu {
- display: none;
- }
- .js-chat-context-menu .name {
- color: white;
- padding: 2px 4px;
- }
- /* Allow names in chat to be clicked */
- #chat .name {
- pointer-events: all !important;
- }
- /* Custom chat filter colors */
- .js-chat-gm {
- color: #a6dcd5;
- }
- /* Class that hides chat lines */
- .js-line-hidden,
- .js-line-blocked {
- display: none;
- }
- /* Enable chat & map resize */
- .js-chat-resize {
- resize: both;
- overflow: auto;
- }
- .js-map-resize:hover {
- resize: both;
- overflow: auto;
- direction: rtl;
- }
- /* The browser resize icon */
- *::-webkit-resizer {
- background: linear-gradient(to right, rgba(51, 77, 80, 0), rgba(203, 202, 165, 0.5));
- border-radius: 8px;
- box-shadow: 0 1px 1px rgba(0,0,0,1);
- }
- *::-moz-resizer {
- background: linear-gradient(to right, rgba(51, 77, 80, 0), rgba(203, 202, 165, 0.5));
- border-radius: 8px;
- box-shadow: 0 1px 1px rgba(0,0,0,1);
- }
- .js-map-opacity {
- position: absolute;
- top: 46px;
- right: 12px;
- z-index: 999;
- width: 100px;
- height: 100px;
- text-align: right;
- display: none;
- pointer-events: all;
- }
- .js-map-opacity:hover {
- display: block;
- }
- .js-map-opacity button {
- border-radius: 10px;
- font-size: 18px;
- padding: 0 5px;
- background: rgba(0,0,0,0.4);
- border: 0;
- color: white;
- font-weight: bold;
- pointer: cursor;
- }
- /* On hover of map, show opacity controls */
- .js-map:hover .js-map-opacity {
- display: block;
- }
- /* Custom css for settings page, duplicates preexisting settings pane grid */
- .uimod-settings {
- display: grid;
- grid-template-columns: 2fr 3fr;
- grid-gap: 8px;
- align-items: center;
- max-height: 390px;
- margin: 0 20px;
- overflow-y: auto;
- }
- /* Friends list CSS, similar to settings but supports 4 columns */
- .uimod-friends {
- display: grid;
- grid-template-columns: 2fr 2fr 2fr 1fr;
- grid-gap: 8px;
- align-items: center;
- max-height: 390px;
- margin: 0 20px;
- overflow-y: auto;
- }
- /* Our custom window, closely mirrors main settings window */
- .uimod-custom-window {
- position: absolute;
- top: 100px;
- left: 50%;
- transform: translate(-50%, 0);
- min-width: 350px;
- max-width: 600px;
- width: 90%;
- height: 80%;
- min-height: 350px;
- max-height: 500px;
- z-index: 9;
- padding: 0px 10px 5px;
- }
- `);
- const modHelpers = {
- // Automated chat command helpers
- // (We've been OK'd to do these by the dev - all automation like this should receive approval from the dev)
- whisperPlayer: playerName => {
- enterTextIntoChat(`/whisper ${tempState.chatName} `);
- },
- partyPlayer: playerName => {
- enterTextIntoChat(`/partyinvite ${tempState.chatName}`);
- submitChat();
- },
- // Filters all chat based on custom filters
- filterAllChat: () => {
- // Blocked user filter
- Object.keys(state.blockList).forEach(blockedName => {
- // Get the `.name` elements from the blocked user, if we haven't already hidden their messages
- const $blockedChatNames = Array.from(document.querySelectorAll(`[data-chat-name="${blockedName}"]:not(.js-line-blocked)`));
- // Hide each of their messages
- $blockedChatNames.forEach($name => {
- // Add the class name to $name so we can track whether it's been hidden in our CSS selector $blockedChatNames
- $name.classList.add('js-line-blocked');
- const $line = $name.parentNode.parentNode.parentNode;
- // Add the class name to $line so we can visibly hide the entire chat line
- $line.classList.add('js-line-blocked');
- });
- })
- // Custom channel filter
- Object.keys(state.chat).forEach(channel => {
- Array.from(document.querySelectorAll(`.text${channel}.content`)).forEach($textItem => {
- const $line = $textItem.parentNode.parentNode;
- $line.classList.toggle('js-line-hidden', !state.chat[channel]);
- });
- });
- },
- // Makes chat context menu visible and appear under the mouse
- showChatContextMenu: (name, mousePos) => {
- // Right before we show the context menu, we want to handle showing/hiding Friend/Unfriend
- const $contextMenu = document.querySelector('.js-chat-context-menu');
- $contextMenu.querySelector('[name="friend"]').classList.toggle('js-hidden', !!state.friendsList[name]);
- $contextMenu.querySelector('[name="unfriend"]').classList.toggle('js-hidden', !state.friendsList[name]);
- $contextMenu.querySelector('.js-name').textContent = name;
- $contextMenu.setAttribute('style', `display: block; left: ${mousePos.x}px; top: ${mousePos.y}px;`);
- },
- // Close chat context menu if clicking outside of it
- closeChatContextMenu: clickEvent => {
- const $target = clickEvent.target;
- // If clicking on name or directly on context menu, don't close it
- // Still closes if clicking on context menu item
- if ($target.classList.contains('js-is-context-menu-initd')
- || $target.classList.contains('js-chat-context-menu')) {
- return;
- }
- const $contextMenu = document.querySelector('.js-chat-context-menu');
- $contextMenu.setAttribute('style', 'display: none');
- },
- friendPlayer: playerName => {
- if (state.friendsList[playerName]) {
- return;
- }
- state.friendsList[playerName] = true;
- modHelpers.addChatMessage(`${playerName} has been added to your friends list.`);
- save();
- },
- unfriendPlayer: playerName => {
- if (!state.friendsList[playerName]) {
- return;
- }
- delete state.friendsList[playerName];
- modHelpers.addChatMessage(`${playerName} is no longer on your friends list.`);
- save();
- },
- // Adds player to block list, to be filtered out of chat
- blockPlayer: playerName => {
- if (state.blockList[playerName]) {
- return;
- }
- state.blockList[playerName] = true;
- modHelpers.filterAllChat();
- modHelpers.addChatMessage(`${playerName} has been blocked.`)
- save();
- },
- // Removes player from block list and makes their messages visible again
- unblockPlayer: playerName => {
- delete state.blockList[playerName];
- modHelpers.addChatMessage(`${playerName} has been unblocked.`);
- save();
- // Make messages visible again
- const $chatNames = Array.from(document.querySelectorAll(`.js-line-blocked[data-chat-name="${playerName}"]`));
- $chatNames.forEach($name => {
- $name.classList.remove('js-line-blocked');
- const $line = $name.parentNode.parentNode.parentNode;
- $line.classList.remove('js-line-blocked');
- });
- },
- // Pushes message to chat
- // TODO: The margins for the message are off slightly compared to other messages - why?
- addChatMessage: text => {
- const newMessageHTML = `
- <div class="linewrap svelte-1vrlsr3">
- <span class="time svelte-1vrlsr3">00.00</span>
- <span class="textuimod content svelte-1vrlsr3">
- <span class="capitalize channel svelte-1vrlsr3">UIMod</span>
- </span>
- <span class="svelte-1vrlsr3">${text}</span>
- </div>
- `;
- const element = makeElement({
- element: 'article',
- class: 'line svelte-1vrlsr3',
- content: newMessageHTML});
- document.querySelector('#chat').appendChild(element);
- },
- };
- // MAIN MODS BELOW
- const mods = [
- // Creates DOM elements for custom chat filters
- function newChatFilters() {
- const $channelselect = document.querySelector('.channelselect');
- if (!document.querySelector(`.${CHAT_GM_CLASS}`)) {
- const $gm = makeElement({
- element: 'small',
- class: `btn border black ${CHAT_GM_CLASS} ${state.chat.GM ? '' : 'textgrey'}`,
- content: 'GM'
- });
- $channelselect.appendChild($gm);
- }
- },
- // Wire up new chat buttons to toggle in state+ui
- function newChatFilterButtons() {
- const $chatGM = document.querySelector(`.${CHAT_GM_CLASS}`);
- $chatGM.addEventListener('click', () => {
- state.chat.GM = !state.chat.GM;
- $chatGM.classList.toggle('textgrey', !state.chat.GM);
- modHelpers.filterAllChat();
- save();
- });
- },
- // Filter out chat in UI based on chat buttons state
- function filterChatObserver() {
- const chatObserver = new MutationObserver(modHelpers.filterAllChat);
- chatObserver.observe(document.querySelector('#chat'), { attributes: true, childList: true });
- },
- // Drag all windows by their header
- function draggableUIWindows() {
- Array.from(document.querySelectorAll('.window:not(.js-can-move)')).forEach($window => {
- $window.classList.add('js-can-move');
- dragElement($window, $window.querySelector('.titleframe'));
- });
- },
- // Save dragged UI windows position to state
- function saveDraggedUIWindows() {
- Array.from(document.querySelectorAll('.window:not(.js-window-is-saving)')).forEach($window => {
- $window.classList.add('js-window-is-saving');
- const $draggableTarget = $window.querySelector('.titleframe');
- const windowName = $draggableTarget.querySelector('[name="title"]').textContent;
- $draggableTarget.addEventListener('mouseup', () => {
- state.windowsPos[windowName] = $window.getAttribute('style');
- save();
- });
- });
- },
- // Loads draggable UI windows position from state
- function loadDraggedUIWindowsPositions() {
- Array.from(document.querySelectorAll('.window:not(.js-has-loaded-pos)')).forEach($window => {
- $window.classList.add('js-has-loaded-pos');
- const windowName = $window.querySelector('[name="title"]').textContent;
- const pos = state.windowsPos[windowName];
- if (pos) {
- $window.setAttribute('style', pos);
- }
- });
- },
- // Makes chat resizable
- function resizableChat() {
- // Add the appropriate classes
- const $chatContainer = document.querySelector('#chat').parentNode;
- $chatContainer.classList.add('js-chat-resize');
- // Load initial chat and map size
- if (state.chatWidth && state.chatHeight) {
- $chatContainer.style.width = state.chatWidth;
- $chatContainer.style.height = state.chatHeight;
- }
- // Save chat size on resize
- const resizeObserverChat = new ResizeObserver(() => {
- const chatWidthStr = window.getComputedStyle($chatContainer, null).getPropertyValue('width');
- const chatHeightStr = window.getComputedStyle($chatContainer, null).getPropertyValue('height');
- state.chatWidth = chatWidthStr;
- state.chatHeight = chatHeightStr;
- save();
- });
- resizeObserverChat.observe($chatContainer);
- },
- // Makes map resizable
- function resizeableMap() {
- const $map = document.querySelector('.container canvas').parentNode;
- const $canvas = $map.querySelector('canvas');
- $map.classList.add('js-map-resize');
- const onMapResize = () => {
- // Get real values of map height/width, excluding padding/margin/etc
- const mapWidthStr = window.getComputedStyle($map, null).getPropertyValue('width');
- const mapHeightStr = window.getComputedStyle($map, null).getPropertyValue('height');
- const mapWidth = Number(mapWidthStr.slice(0, -2));
- const mapHeight = Number(mapHeightStr.slice(0, -2));
- // If height/width are 0 or unset, don't resize canvas
- if (!mapWidth || !mapHeight) {
- return;
- }
- if ($canvas.width !== mapWidth) {
- $canvas.width = mapWidth;
- }
- if ($canvas.height !== mapHeight) {
- $canvas.height = mapHeight;
- }
- // Save map size on resize, unless map has been maximized by user
- if (mapWidth !== CHAT_MAXIMIZED_SIZE && mapHeight !== CHAT_MAXIMIZED_SIZE) {
- state.mapWidth = mapWidthStr;
- state.mapHeight = mapHeightStr;
- save();
- }
- };
- if (state.mapWidth && state.mapHeight) {
- $map.style.width = state.mapWidth;
- $map.style.height = state.mapHeight;
- onMapResize(); // Update canvas size on initial load of saved map size
- }
- // On resize of map, resize canvas to match
- const resizeObserverMap = new ResizeObserver(onMapResize);
- resizeObserverMap.observe($map);
- // We need to observe canvas resizes to tell when the user presses M to open the big map
- // At that point, we resize the map to match the canvas
- const triggerResize = () => {
- // Get real values of map height/width, excluding padding/margin/etc
- const mapWidthStr = window.getComputedStyle($map, null).getPropertyValue('width');
- const mapHeightStr = window.getComputedStyle($map, null).getPropertyValue('height');
- const mapWidth = Number(mapWidthStr.slice(0, -2));
- const mapHeight = Number(mapHeightStr.slice(0, -2));
- // If height/width are 0 or unset, we don't care about resizing yet
- if (!mapWidth || !mapHeight) {
- return;
- }
- if ($canvas.width !== mapWidth) {
- $map.style.width = `${$canvas.width}px`;
- }
- if ($canvas.height !== mapHeight) {
- $map.style.height = `${$canvas.height}px`;
- }
- };
- // We debounce the canvas resize, so it doesn't resize every single
- // pixel you move when resizing the DOM. If this were to happen,
- // resizing would constantly be interrupted. You'd have to resize a tiny bit,
- // lift left click, left click again to resize a tiny bit more, etc.
- // Resizing is smooth when we debounce this canvas.
- const debouncedTriggerResize = debounce(triggerResize, 50);
- const resizeObserverCanvas = new ResizeObserver(debouncedTriggerResize);
- resizeObserverCanvas.observe($canvas);
- },
- function mapOpacityControls() {
- const $map = document.querySelector('.container canvas');
- if (!$map.parentNode.classList.contains('js-map')) {
- $map.parentNode.classList.add('js-map');
- }
- const $mapContainer = document.querySelector('.js-map');
- // On load, update map opacity to match state
- // We modify the opacity of the canvas and the background color alpha of the parent container
- // We do this to allow our opacity buttons to be visible on hover with 100% opacity
- // (A surprisingly difficult enough task to require this implementation)
- const updateMapOpacity = () => {
- $map.setAttribute('style', `opacity: ${String(state.mapOpacity / 100)}`);
- const mapContainerBgColor = window.getComputedStyle($mapContainer, null).getPropertyValue('background-color');
- // Credit for this regexp + This opacity+rgba dual implementation: https://stackoverflow.com/questions/16065998/replacing-changing-alpha-in-rgba-javascript
- const newBgColor = mapContainerBgColor.replace(/[\d\.]+\)$/g, `${state.mapOpacity / 100})`);
- $mapContainer.setAttribute('style', `background-color: ${newBgColor}`);
- };
- updateMapOpacity();
- const $opacityButtons = makeElement({
- element: 'div',
- class: 'js-map-opacity',
- content: `<button class="js-map-opacity-add">+</button><button class="js-map-opacity-minus">-</button>`,
- });
- // Add it right before the map container div
- $map.parentNode.insertBefore($opacityButtons, $map);
- const $addBtn = document.querySelector('.js-map-opacity-add');
- const $minusBtn = document.querySelector('.js-map-opacity-minus');
- // Hide the buttons if map opacity is maxed/minimum
- if (state.mapOpacity === 100) {
- $addBtn.setAttribute('style', 'visibility: hidden');
- }
- if (state.mapOpacity === 0) {
- $minusBtn.setAttribute('style', 'visibility: hidden');
- }
- // Wire it up
- $addBtn.addEventListener('click', clickEvent => {
- // Update opacity
- state.mapOpacity += 10;
- save();
- updateMapOpacity();
- // Hide this button if the opacity is max
- if (state.mapOpacity === 100) {
- const $btn = clickEvent.target;
- // We use visibility to hide the button but keep its position in the UI
- $btn.setAttribute('style', 'visibility: hidden');
- }
- // If map opacity is not the lowest, then make minus button visible
- if (state.mapOpacity !== 0) {
- $minusBtn.setAttribute('style', 'visibility: visible');
- }
- save();
- });
- $minusBtn.addEventListener('click', clickEvent => {
- // Update opacity
- state.mapOpacity -= 10;
- save();
- updateMapOpacity();
- // Hide this button if the opacity is lowest
- if (state.mapOpacity === 0) {
- const $btn = clickEvent.target;
- $btn.setAttribute('style', 'visibility: hidden');
- }
- // If map opacity is not the max, then make add button visible
- if (state.mapOpacity !== 100) {
- $addBtn.setAttribute('style', 'visibility: visible');
- }
- });
- },
- // The last clicked UI window displays above all other UI windows
- // This is useful when, for example, your inventory is near the market window,
- // and you want the window and the tooltips to display above the market window.
- function selectedWindowIsTop() {
- Array.from(document.querySelectorAll('.window:not(.js-is-top-initd)')).forEach($window => {
- $window.classList.add('js-is-top-initd');
- $window.addEventListener('mousedown', () => {
- // First, make the other is-top window not is-top
- const $otherWindowContainer = document.querySelector('.js-is-top');
- if ($otherWindowContainer) {
- $otherWindowContainer.classList.remove('js-is-top');
- }
- // Then, make our window's container (the z-index container) is-top
- $window.parentNode.classList.add('js-is-top');
- });
- });
- },
- // The F icon and the UI that appears when you click it
- function customFriendsList() {
- var friendsIconElement = makeElement({
- element: 'div',
- class: 'btn border black js-friends-list-icon',
- content: 'F',
- });
- // Add the icon to the right of Elixir icon
- const $elixirIcon = document.querySelector('#sysgem');
- $elixirIcon.parentNode.insertBefore(friendsIconElement, $elixirIcon.nextSibling);
- // Create the friends list UI
- document.querySelector('.js-friends-list-icon').addEventListener('click', () => {
- if (document.querySelector('.js-friends-list')) {
- // Don't open the friends list twice.
- return;
- }
- let friendsListHTML = '';
- Object.keys(state.friendsList).sort().forEach(friendName => {
- friendsListHTML += `
- <div data-player-name="${friendName}">${friendName}</div>
- <div class="btn blue js-whisper-player" data-player-name="${friendName}">Whisper</div>
- <div class="btn blue js-party-player" data-player-name="${friendName}">Party invite</div>
- <div class="btn orange js-unfriend-player" data-player-name="${friendName}">Remove</div>
- `;
- });
- const customFriendsWindowHTML = `
- <h3 class="textprimary">Friends list</h3>
- <div class="uimod-friends">${friendsListHTML}</div>
- <p></p>
- <div class="btn purp js-close-custom-friends-list">Close</div>
- `;
- const $customFriendsList = makeElement({
- element: 'div',
- class: 'menu panel-black js-friends-list uimod-custom-window',
- content: customFriendsWindowHTML,
- });
- document.body.appendChild($customFriendsList);
- // Wire up the buttons
- Array.from(document.querySelectorAll('.js-whisper-player')).forEach($button => {
- $button.addEventListener('click', clickEvent => {
- const name = clickEvent.target.getAttribute('data-player-name');
- modHelpers.whisperPlayer(name);
- });
- });
- Array.from(document.querySelectorAll('.js-party-player')).forEach($button => {
- $button.addEventListener('click', clickEvent => {
- const name = clickEvent.target.getAttribute('data-player-name');
- modHelpers.partyPlayer(name);
- });
- });
- Array.from(document.querySelectorAll('.js-unfriend-player')).forEach($button => {
- $button.addEventListener('click', clickEvent => {
- const name = clickEvent.target.getAttribute('data-player-name');
- modHelpers.unfriendPlayer(name);
- // Remove the blocked player from the list
- Array.from(document.querySelectorAll(`.js-friends-list [data-player-name="${name}"]`)).forEach($element => {
- $element.parentNode.removeChild($element);
- });
- });
- });
- // The close button for our custom UI
- document.querySelector('.js-close-custom-friends-list').addEventListener('click', () => {
- const $friendsListWindow = document.querySelector('.js-friends-list');
- $friendsListWindow.parentNode.removeChild($friendsListWindow);
- });
- });
- },
- // Custom settings UI, currently just Blocked players
- function customSettings() {
- const $settings = document.querySelector('.divide:not(.js-settings-initd)');
- if (!$settings) {
- return;
- }
- $settings.classList.add('js-settings-initd');
- const $settingsChoiceList = $settings.querySelector('.choice').parentNode;
- $settingsChoiceList.appendChild(makeElement({
- element: 'div',
- class: 'choice js-blocked-players',
- content: 'Blocked players',
- }));
- // Upon click, we display our custom settings window UI
- document.querySelector('.js-blocked-players').addEventListener('click', () => {
- let blockedPlayersHTML = '';
- Object.keys(state.blockList).sort().forEach(blockedName => {
- blockedPlayersHTML += `
- <div data-player-name="${blockedName}">${blockedName}</div>
- <div class="btn orange js-unblock-player" data-player-name="${blockedName}">Unblock player</div>
- `;
- });
- const customSettingsHTML = `
- <h3 class="textprimary">Blocked players</h3>
- <div class="settings uimod-settings">${blockedPlayersHTML}</div>
- <p></p>
- <div class="btn purp js-close-custom-settings">Close</div>
- `;
- const $customSettings = makeElement({
- element: 'div',
- class: 'menu panel-black js-custom-settings uimod-custom-window',
- content: customSettingsHTML,
- });
- document.body.appendChild($customSettings);
- // Wire up all the unblock buttons
- Array.from(document.querySelectorAll('.js-unblock-player')).forEach($button => {
- $button.addEventListener('click', clickEvent => {
- const name = clickEvent.target.getAttribute('data-player-name');
- modHelpers.unblockPlayer(name);
- // Remove the blocked player from the list
- Array.from(document.querySelectorAll(`.js-custom-settings [data-player-name="${name}"]`)).forEach($element => {
- $element.parentNode.removeChild($element);
- });
- });
- });
- // And the close button for our custom UI
- document.querySelector('.js-close-custom-settings').addEventListener('click', () => {
- const $customSettingsWindow = document.querySelector('.js-custom-settings');
- $customSettingsWindow.parentNode.removeChild($customSettingsWindow);
- });
- });
- },
- // This creates the initial chat context menu (which starts as hidden)
- function createChatContextMenu() {
- if (document.querySelector('.js-chat-context-menu')) {
- return;
- }
- let contextMenuHTML = `
- <div class="js-name">...</div>
- <div class="choice" name="party">Party invite</div>
- <div class="choice" name="whisper">Whisper</div>
- <div class="choice" name="friend">Friend</div>
- <div class="choice" name="unfriend">Unfriend</div>
- <div class="choice" name="block">Block</div>
- `
- document.body.appendChild(makeElement({
- element: 'div',
- class: 'panel context border grey js-chat-context-menu',
- content: contextMenuHTML,
- }));
- const $chatContextMenu = document.querySelector('.js-chat-context-menu');
- $chatContextMenu.querySelector('[name="party"]').addEventListener('click', () => {
- modHelpers.partyPlayer(tempState.chatName);
- });
- $chatContextMenu.querySelector('[name="whisper"]').addEventListener('click', () => {
- modHelpers.whisperPlayer(tempState.chatName);
- });
- $chatContextMenu.querySelector('[name="friend"]').addEventListener('click', () => {
- modHelpers.friendPlayer(tempState.chatName);
- });
- $chatContextMenu.querySelector('[name="unfriend"]').addEventListener('click', () => {
- modHelpers.unfriendPlayer(tempState.chatName);
- });
- $chatContextMenu.querySelector('[name="block"]').addEventListener('click', () => {
- modHelpers.blockPlayer(tempState.chatName);
- });
- },
- // This opens a context menu when you click a user's name in chat
- function chatContextMenu() {
- Array.from(document.querySelectorAll('.name:not(.js-is-context-menu-initd)')).forEach($name => {
- $name.classList.add('js-is-context-menu-initd');
- // Add name to element so we can target it in CSS when filtering chat for block list
- $name.setAttribute('data-chat-name', $name.textContent);
- const showContextMenu = clickEvent => {
- // TODO: Is there a way to pass the name to showChatContextMenumethod, instead of storing in tempState?
- tempState.chatName = $name.textContent;
- modHelpers.showChatContextMenu($name.textContent, {x: clickEvent.pageX, y: clickEvent.pageY});
- };
- $name.addEventListener('click', showContextMenu);
- });
- },
- ];
- // Add new DOM, load our stored state, wire it up, then continuously rerun specific methods whenever UI changes
- function initialize() {
- // If the Hordes.io tab isn't active for long enough, it reloads the entire page, clearing this mod
- // We check for that and reinitialize the mod if that happens
- const $layout = document.querySelector('.layout');
- if ($layout.classList.contains('uimod-initd')) {
- return;
- }
- modHelpers.addChatMessage(`Hordes UI Mod v${VERSION} by Sakaiyo has been initialized.`);
- $layout.classList.add('uimod-initd')
- load();
- mods.forEach(mod => mod());
- // Continuously re-run specific mods methods that need to be executed on UI change
- const rerunObserver = new MutationObserver(() => {
- // If new window appears, e.g. even if window is closed and reopened, we need to rewire it
- // Fun fact: Some windows always exist in the DOM, even when hidden, e.g. Inventory
- // But some windows only exist in the DOM when open, e.g. Interaction
- const modsToRerun = [
- 'saveDraggedUIWindows',
- 'draggableUIWindows',
- 'loadDraggedUIWindowsPositions',
- 'selectedWindowIsTop',
- 'customSettings',
- ];
- modsToRerun.forEach(modName => {
- mods.find(mod => mod.name === modName)();
- });
- });
- rerunObserver.observe(document.querySelector('.layout > .container'), { attributes: false, childList: true, });
- // Rerun only on chat
- const chatRerunObserver = new MutationObserver(() => {
- mods.find(mod => mod.name === 'chatContextMenu')();
- modHelpers.filterAllChat();
- });
- chatRerunObserver.observe(document.querySelector('#chat'), { attributes: false, childList: true, });
- // Event listeners for document.body might be kept when the game reloads, so don't reinitialize them
- if (!document.body.classList.contains('js-uimod-initd')) {
- document.body.classList.add('js-uimod-initd');
- // Close chat context menu when clicking outside of it
- document.body.addEventListener('click', modHelpers.closeChatContextMenu);
- }
- }
- // Initialize mods once UI DOM has loaded
- const pageObserver = new MutationObserver(() => {
- const isUiLoaded = !!document.querySelector('.layout');
- if (isUiLoaded) {
- initialize();
- }
- });
- pageObserver.observe(document.body, { attributes: true, childList: true })
- // UTIL METHODS
- // Save to in-memory state and localStorage to retain on refresh
- function save(items) {
- localStorage.setItem(STORAGE_STATE_KEY, JSON.stringify(state));
- }
- // Load localStorage state if it exists
- // NOTE: If user is trying to load unsupported version of stored state,
- // e.g. they just upgraded to breaking version, then we delete their stored state
- function load() {
- const storedStateJson = localStorage.getItem(STORAGE_STATE_KEY)
- if (storedStateJson) {
- const storedState = JSON.parse(storedStateJson);
- if (storedState.breakingVersion !== BREAKING_VERSION) {
- localStorage.setItem(STORAGE_STATE_KEY, JSON.stringify(state));
- return;
- }
- state = {
- ...state,
- ...storedState,
- };
- }
- }
- // Nicer impl to create elements in one method call
- function makeElement(args) {
- const $node = document.createElement(args.element);
- if (args.class) { $node.className = args.class; }
- if (args.content) { $node.innerHTML = args.content; }
- if (args.src) { $node.src = args.src; }
- return $node;
- }
- function enterTextIntoChat(text) {
- // Open chat input
- const enterEvent = new KeyboardEvent('keydown', {
- bubbles: true, cancelable: true, keyCode: 13
- });
- document.body.dispatchEvent(enterEvent);
- // Place text into chat
- const $input = document.querySelector('#chatinput input');
- $input.value = text;
- // Get chat input to recognize slash commands and change the channel
- // by triggering the `input` event.
- // (Did some debugging to figure out the channel only changes when the
- // svelte `input` event listener exists.)
- const inputEvent = new KeyboardEvent('input', {
- bubbles: true, cancelable: true
- })
- $input.dispatchEvent(inputEvent);
- }
- function submitChat() {
- const $input = document.querySelector('#chatinput input');
- const kbEvent = new KeyboardEvent('keydown', {
- bubbles: true, cancelable: true, keyCode: 13
- });
- $input.dispatchEvent(kbEvent);
- }
- // Credit: https://stackoverflow.com/a/14234618 (Has been slightly modified)
- // $draggedElement is the item that will be dragged.
- // $dragTrigger is the element that must be held down to drag $draggedElement
- function dragElement($draggedElement, $dragTrigger) {
- let offset = [0,0];
- let isDown = false;
- $dragTrigger.addEventListener('mousedown', function(e) {
- isDown = true;
- offset = [
- $draggedElement.offsetLeft - e.clientX,
- $draggedElement.offsetTop - e.clientY
- ];
- }, true);
- document.addEventListener('mouseup', function() {
- isDown = false;
- }, true);
- document.addEventListener('mousemove', function(e) {
- event.preventDefault();
- if (isDown) {
- $draggedElement.style.left = (e.clientX + offset[0]) + 'px';
- $draggedElement.style.top = (e.clientY + offset[1]) + 'px';
- }
- }, true);
- }
- // Credit: David Walsh
- function debounce(func, wait, immediate) {
- var timeout;
- return function() {
- var context = this, args = arguments;
- var later = function() {
- timeout = null;
- if (!immediate) func.apply(context, args);
- };
- var callNow = immediate && !timeout;
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- if (callNow) func.apply(context, args);
- };
- }
- })();