// ==UserScript==
// @name Hordes UI Mod
// @version 0.11
// @description Various UI mods for Hordes.io.
// @author Sakaiyo
// @match https://hordes.io/play
// @grant GM_addStyle
// @namespace https://gf.qytechs.cn/users/160017
// ==/UserScript==
/**
* TODO: Implement GM and lvlup and system chat tabs
* TODO: Implement saving of dragged UI location
* TODO: (Maybe) Implement saving of chat filters
* TODO: Implement chat tabs
* TODO: Implement inventory sorting
* TODO: (Maybe) Improved healer party frames
* TODO: Opacity scaler for map
* TODO: Can we speed up windows becoming draggable? Slight delay
*/
(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 CHAT_MAXIMIZED_SIZE = 692; // The width+height of the maximized chat, so we don't save map size when it's maximized
const STORAGE_STATE_KEY = 'hordesio-uimodsakaiyo-state';
const CHAT_LVLUP_CLASS = 'js-chat-lvlup';
const CHAT_GM_CLASS = 'js-chat-gm';
const CHAT_SYSTEM_CLASS = 'js-chat-system';
let state = {
breakingVersion: BREAKING_VERSION
};
// 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;
}
/* 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);
}
`);
// MAIN MODS BELOW
const setupDom = {
newChatFilters: () => {
const $channelselect = document.querySelector('.channelselect');
if (!document.querySelector(`.${CHAT_LVLUP_CLASS}`)) {
const $lvlup = createElement({
element: 'small',
class: `btn border black textgrey ${CHAT_LVLUP_CLASS}`,
content: 'lvlup'
});
$channelselect.appendChild($lvlup);
}
if (!document.querySelector(`.${CHAT_GM_CLASS}`)) {
const $gm = createElement({
element: 'small',
class: `btn border black textgrey ${CHAT_GM_CLASS}`,
content: 'GM'
});
$channelselect.appendChild($gm);
}
if (!document.querySelector(`.${CHAT_SYSTEM_CLASS}`)) {
const $gm = createElement({
element: 'small',
class: `btn border black textgrey ${CHAT_SYSTEM_CLASS}`,
content: 'system'
});
$channelselect.appendChild($gm);
}
},
};
const wireDom = {
newChatFilters: () => {
},
// Drag all windows by their header
draggableUIWindows: () => {
Array.from(document.querySelectorAll('.window:not(.js-can-move)')).forEach($window => {
dragElement($window, $window.querySelector('.titleframe'));
$window.classList.add('js-can-move');
});
},
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);
},
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 resizeObserverCanvas = new ResizeObserver(() => {
// 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`;
}
});
resizeObserverCanvas.observe($canvas);
},
};
// Add new DOM, load our stored state, wire it up, then continuously rerun specific methods whenever UI changes
function initialize() {
Object.keys(setupDom).forEach((domMethod) => setupDom[domMethod]());
load();
Object.keys(wireDom).forEach((domMethod) => wireDom[domMethod]());
// Continuously re-run specific wireDom methods that need to be executed on UI change
const rerunOnChange = () => {
// If new window appears, e.g. even if window is closed and reopened, we need to rewire it
wireDom.draggableUIWindows();
};
document.querySelector('.layout').addEventListener('click', rerunOnChange);
document.querySelector('.layout').addEventListener('keypress', rerunOnChange);
}
// Initialize mods once UI DOM has loaded
const pageObserver = new MutationObserver((_, observer) => {
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 = 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);
}
})();