您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Various UI mods for Hordes.io.
当前为
- // ==UserScript==
- // @name Hordes UI Mod
- // @version 0.131
- // @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: Opacity scaler for map
- * TODO: Context menu when right clicking username in chat, party invite and whisper.
- * Check if it's ok to emulate keypresses before releasing. If not, then maybe copy text to clipboard.
- * TODO: FIX BUG: Add support for resizing map back to saved position after minimizing it, from maximized position
- * TODO (Maybe): Ability to make GM chat look like normal chat?
- */
- (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;
- // 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_LVLUP_CLASS = 'js-chat-lvlup';
- const CHAT_GM_CLASS = 'js-chat-gm';
- let state = {
- breakingVersion: BREAKING_VERSION,
- chat: {
- lvlup: true,
- GM: true,
- },
- windowsPos: {},
- };
- // 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);
- }
- /* Transparent map */
- .svelte-hiyby7 {
- opacity: 0.7;
- }
- /* 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;
- }
- /* Custom chat filter colors */
- .js-chat-lvlup {
- color: #EE960B;
- }
- .js-chat-inv {
- color: #a6dcd5;
- }
- .js-chat-gm {
- color: #a6dcd5;
- }
- /* Class that hides chat lines*/
- .js-line-hidden {
- 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);
- }
- `);
- const modHelpers = {
- // Filters all chat based on custom filters
- filterAllChat: () => {
- 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]);
- });
- });
- },
- };
- // MAIN MODS BELOW
- const mods = [
- // Creates DOM elements for custom chat filters
- function newChatFilters() {
- const $channelselect = document.querySelector('.channelselect');
- if (!document.querySelector(`.${CHAT_LVLUP_CLASS}`)) {
- const $lvlup = createElement({
- element: 'small',
- class: `btn border black ${CHAT_LVLUP_CLASS} ${state.chat.lvlup ? '' : 'textgrey'}`,
- content: 'lvlup'
- });
- $channelselect.appendChild($lvlup);
- }
- if (!document.querySelector(`.${CHAT_GM_CLASS}`)) {
- const $gm = createElement({
- 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 $chatLvlup = document.querySelector(`.${CHAT_LVLUP_CLASS}`);
- $chatLvlup.addEventListener('click', () => {
- state.chat.lvlup = !state.chat.lvlup;
- $chatLvlup.classList.toggle('textgrey', !state.chat.lvlup);
- modHelpers.filterAllChat();
- save({chat: state.chat});
- });
- 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({chat: state.chat});
- });
- },
- // 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({windowsPos: state.windowsPos});
- });
- });
- },
- // 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');
- save({
- chatWidth: chatWidthStr,
- chatHeight: chatHeightStr,
- });
- });
- resizeObserverChat.observe($chatContainer);
- },
- // Makes map resizable
- function resizeableMap() {
- const $map = document.querySelector('.svelte-hiyby7');
- 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) {
- save({
- mapWidth: mapWidthStr,
- mapHeight: mapHeightStr,
- });
- }
- };
- 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, 200);
- const resizeObserverCanvas = new ResizeObserver(debouncedTriggerResize);
- resizeObserverCanvas.observe($canvas);
- },
- // 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');
- });
- });
- },
- ];
- // 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;
- }
- $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',
- ];
- modsToRerun.forEach(modName => {
- mods.find(mod => mod.name === modName)();
- });
- });
- rerunObserver.observe(document.querySelector('.layout > .container'), { attributes: false, childList: true, })
- }
- // 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) {
- state = {
- ...state,
- ...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 createElement(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;
- }
- // ...Can't remember why I added this.
- // TODO: Remove this if not using. Can access chat input with it
- function simulateEnterPress() {
- const kbEvent = new KeyboardEvent("keydown", {
- bubbles: true, cancelable: true, keyCode: 13
- });
- document.body.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);
- };
- }
- })();