Hordes UI Mod

Various UI mods for Hordes.io.

À partir de 2019-12-22. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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://greatest.deepsurf.us/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);
	}
})();