/* eslint-disable */ // @ts-nocheck if (new URLSearchParams(window.location.search).get('mobile') === 'true') { // Removed brainless unsafeWindow console.log('Eagler Pocket Mobile v1.40'); // TODO: remove the mobile check is implement the dynamic enabling and disabling of individual features function isMobile() { try { document.createEvent('TouchEvent'); return true; } catch (e) { return false; } } if (!isMobile()) { alert('WARNING: This script was created for mobile, and may break functionality in non-mobile browsers!'); } // TODO: consolidate all of these into a single object? window.crouchLock = false; // Used for crouch mobile control window.sprintLock = false; // Used for sprint mobile control window.keyboardFix = false; // keyboardFix ? "Standard Keyboard" : "Compatibility Mode" window.inputFix = false; // If true, Duplicate Mode window.blockNextInput = false; // Used for Duplicate Mode window.hiddenInputFocused = false; // Used for keyboard display on mobile window.canvasTouchMode = 0; // Used for canvas touch handling /* 0 Idle 1 Touch initiated 2 Primary touch 3 Secondary touch 4 Scroll 5 Finished */ window.canvasTouchStartX = null; window.canvasTouchStartY = null; window.canvasTouchPreviousX = null; window.canvasTouchPreviousY = null; window.canvasPrimaryID = null; window.buttonTouchStartX = null; // charCodeAt is designed for unicode characters, and doesn't match the behavior of the keyCodes used by KeyboardEvents, thus necessitating this function String.prototype.toKeyCode = function () { const keyCodeList = { '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57, backspace: 8, tab: 9, enter: 13, shift: 16, ctrl: 17, alt: 18, pause_break: 19, caps_lock: 20, escape: 27, ' ': 32, page_up: 33, page_down: 34, end: 35, home: 36, left_arrow: 37, up_arrow: 38, right_arrow: 39, down_arrow: 40, insert: 45, delete: 46, a: 65, b: 66, c: 67, d: 68, e: 69, f: 70, g: 71, h: 72, i: 73, j: 74, k: 75, l: 76, m: 77, n: 78, o: 79, p: 80, q: 81, r: 82, s: 83, t: 84, u: 85, v: 86, w: 87, x: 88, y: 89, z: 90, left_window_key: 91, right_window_key: 92, select_key: 93, numpad_0: 96, numpad_1: 97, numpad_2: 98, numpad_3: 99, numpad_4: 100, numpad_5: 101, numpad_6: 102, numpad_7: 103, numpad_8: 104, numpad_9: 105, '*': 106, '+': 107, '-': 109, '.': 110, '/': 111, f1: 112, f2: 113, f3: 114, f4: 115, f5: 116, f6: 117, f7: 118, f8: 119, f9: 120, f10: 121, f11: 122, f12: 123, num_lock: 144, scroll_lock: 145, ';': 186, '=': 187, ',': 188, '-': 189, '.': 190, '/': 191, '\u0060': 192, '[': 219, '\u005C': 220, ']': 221, '\u0022': 222, }; return keyCodeList[this]; }; // Overrides the addEventListener behavior to all code injection on keydown event listeners. This function has thrown TypeErrors on some Android devices because fn is not recognized as a function // This is used by Compatibility Mode to block invalid keyEvents const _addEventListener = EventTarget.prototype.addEventListener; Object.defineProperty(EventTarget.prototype, 'addEventListener', { value: function (type, fn, ...rest) { if (type == 'keydown') { // Check if a keydown event is being added _addEventListener.call( this, type, function (...args) { if (args[0].isTrusted && window.keyboardFix) { // When we are in compatibility mode, we ignore all trusted keyboard events return; } return fn.apply(this, args); // Appends the rest of the function specified by addEventListener }, ...rest, ); } else { // If it's not a keydown event, behave like normal (hopefully) _addEventListener.call(this, type, fn, ...rest); } }, }); // Overrides preventDefault, because on some (Android) devices you couldn't type into hiddenInput const _preventDefault = Event.prototype.preventDefault; Event.prototype.preventDefault = function (shouldBypass) { if (document.activeElement.id != 'hiddenInput' || shouldBypass) { // activeElement is what element is currently focused this._preventDefault = _preventDefault; this._preventDefault(); } }; // Key and mouse events // Note: the client must have the key, keyCode, and which parameters defined or it will crash // Note: for text inputs, the client only reads from the "key" paramater // * an exception to this appears to be the shift and backspace key // Note: for inGame inputs, the client only reads from the "keyCode character" function keyEvent(name, state) { const charCode = name.toKeyCode(); const evt = new KeyboardEvent(state, { key: name, keyCode: charCode, which: charCode, }); window.dispatchEvent(evt); } function mouseEvent(number, state, element, event = { clientX: 0, clientY: 0, screenX: 0, screenY: 0 }) { element.dispatchEvent( new PointerEvent(state, { button: number, buttons: number, clientX: event.clientX, clientY: event.clientY, screenX: event.screenX, screenY: event.screenY, }), ); } function wheelEvent(element, delta) { element.dispatchEvent( new WheelEvent('wheel', { wheelDeltaY: delta, }), ); } function setButtonVisibility(pointerLocked) { const inGameStyle = document.getElementById('inGameStyle'); const inMenuStyle = document.getElementById('inMenuStyle'); inGameStyle.disabled = pointerLocked; inMenuStyle.disabled = !pointerLocked; } // POINTERLOCK // When requestpointerlock is called, this dispatches an event, saves the requested element to window.fakelock, and unhides the touch controls window.fakelock = null; Object.defineProperty(Element.prototype, 'requestPointerLock', { value: function () { window.fakelock = this; document.dispatchEvent(new Event('pointerlockchange')); setButtonVisibility(true); return true; }, }); // Makes pointerLockElement return window.fakelock Object.defineProperty(Document.prototype, 'pointerLockElement', { get: function () { return window.fakelock; }, }); // When exitPointerLock is called, this dispatches an event, clears the Object.defineProperty(Document.prototype, 'exitPointerLock', { value: function () { window.fakelock = null; document.dispatchEvent(new Event('pointerlockchange')); setButtonVisibility(false); return true; }, }); // FULLSCREEN window.fakefull = null; // Stops the client from crashing when fullscreen is requested Object.defineProperty(Element.prototype, 'requestFullscreen', { value: function () { window.fakefull = this; document.dispatchEvent(new Event('fullscreenchange')); return true; }, }); Object.defineProperty(document, 'fullscreenElement', { get: function () { return window.fakefull; }, }); Object.defineProperty(Document.prototype, 'exitFullscreen', { value: function () { window.fakefull = null; document.dispatchEvent(new Event('fullscreenchange')); return true; }, }); // FILE UPLOADING // Safari doesn't recognize the element.click() used to display the file uploader as an action performed by the user, so it ignores it. // This hijacks the element.createElement() function to add the file upload to the DOM, so the user can manually press the button again. const _createElement = document.createElement; document.createElement = function (type, ignore) { this._createElement = _createElement; const element = this._createElement(type); if (type == 'input' && !ignore) { // We set the ingore flag to true when we create the hiddenInput document.querySelectorAll('#fileUpload').forEach((e) => e.parentNode.removeChild(e)); // Get rid of any left over fileUpload inputs element.id = 'fileUpload'; element.addEventListener( 'change', function (e) { element.hidden = true; element.style.display = 'none'; }, { passive: false, once: true }, ); window.addEventListener( 'focus', function (e) { setTimeout(() => { element.hidden = true; element.style.display = 'none'; }, 300); }, { once: true }, ); document.body.appendChild(element); } return element; }; // Lazy way to hide touch controls through CSS. const inGameStyle = document.createElement('style'); inGameStyle.id = 'inGameStyle'; inGameStyle.textContent = ` .inGame { display: none; }`; document.documentElement.appendChild(inGameStyle); const inMenuStyle = document.createElement('style'); inMenuStyle.id = 'inMenuStyle'; inMenuStyle.textContent = ` .inMenu { display: none; }`; document.documentElement.appendChild(inMenuStyle); // The canvas is created by the client after it finishes unzipping and loading. When the canvas is created, this applies any necessary event listeners and creates buttons function waitForElm(selector) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver((mutations) => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); observer.observe(document.documentElement, { childList: true, subtree: true, }); }); } function createTouchButton(buttonClass, buttonDisplay, elementName) { const touchButton = document.createElement(elementName ?? 'button', true); touchButton.classList.add(buttonClass); touchButton.classList.add(buttonDisplay); touchButton.classList.add('mobileControl'); touchButton.addEventListener( 'touchmove', function (e) { e.preventDefault(); }, false, ); touchButton.addEventListener('contextmenu', function (e) { e.preventDefault(); }); return touchButton; } waitForElm('canvas').then(() => { insertCanvasElements(); }); function insertCanvasElements() { // Translates touchmove events to mousemove events when inGame, and touchmove events to wheele events when inMenu const canvas = document.querySelector('canvas'); canvas.addEventListener( 'touchstart', function (e) { if (window.canvasTouchMode < 2) { // If a touch is initiated but not assigned if (window.canvasPrimaryID == null) { window.canvasTouchMode = 1; const primaryTouch = e.changedTouches[0]; window.canvasPrimaryID = primaryTouch.identifier; canvasTouchStartX = primaryTouch.clientX; canvasTouchStartY = primaryTouch.clientY; canvasTouchPreviousX = canvasTouchStartX; canvasTouchPreviousY = canvasTouchStartY; window.touchTimer = setTimeout(function (e) { // If our touch is still set to initiaited, set it to secondary touch if (window.canvasTouchMode == 1) { window.canvasTouchMode = 3; mouseEvent(2, 'mousedown', canvas, primaryTouch); if (window.fakelock) { // We only dispatch mouseup inGame because we want to be able to click + drag items in GUI's mouseEvent(2, 'mouseup', canvas, primaryTouch); } } }, 300); } else if (window.canvasTouchMode == 1 && !window.fakelock) { // If we already have a primary touch, it means we're using two fingers window.canvasTouchMode = 4; clearTimeout(window.crouchTimer); // TODO: Find out why this isn't redudnant } } }, false, ); canvas.addEventListener( 'touchmove', function (e) { e.preventDefault(); // Prevents window zoom when using two fingers let primaryTouch; for (let touchIndex = 0; touchIndex < e.targetTouches.length; touchIndex++) { // Iterate through our touches to find a touch event matching the primary touch ID if (e.targetTouches[touchIndex].identifier == window.canvasPrimaryID) { primaryTouch = e.targetTouches[touchIndex]; break; } } if (primaryTouch) { primaryTouch.distanceX = primaryTouch.clientX - canvasTouchStartX; primaryTouch.distanceY = primaryTouch.clientY - canvasTouchStartY; primaryTouch.squaredNorm = primaryTouch.distanceX * primaryTouch.distanceX + primaryTouch.distanceY * primaryTouch.distanceY; primaryTouch.movementX = primaryTouch.clientX - canvasTouchPreviousX; primaryTouch.movementY = primaryTouch.clientY - canvasTouchPreviousY; if (window.canvasTouchMode == 1) { // If the primary touch is still only initiated if (primaryTouch.squaredNorm > 25) { // If our touch becomes a touch + drag clearTimeout(window.crouchTimer); window.canvasTouchMode = 2; if (!window.fakelock) { // When we're inGame, we don't want to be placing blocks when we are moving the camera around mouseEvent(1, 'mousedown', canvas, primaryTouch); } } } else { // If our touch is primary, secondary, scroll or finished if (window.canvasTouchMode == 4) { // If our touch is scrolling wheelEvent(canvas, primaryTouch.movementY); } else { canvas.dispatchEvent( new MouseEvent('mousemove', { clientX: primaryTouch.clientX, clientY: primaryTouch.clientY, screenX: primaryTouch.screenX, screenY: primaryTouch.screenY, // The top four are used for item position when in GUI's, the bottom two are for moving the camera inGame movementX: primaryTouch.movementX, movementY: primaryTouch.movementY, }), ); } } canvasTouchPreviousX = primaryTouch.clientX; canvasTouchPreviousY = primaryTouch.clientY; } }, false, ); function canvasTouchEnd(e) { for (let touchIndex = 0; touchIndex < e.changedTouches.length; touchIndex++) { // Iterate through changed touches to find primary touch if (e.changedTouches[touchIndex].identifier == window.canvasPrimaryID) { const primaryTouch = e.changedTouches[touchIndex]; // When any of the controlling fingers go away, we want to wait until we aren't receiving any other touch events if (window.canvasTouchMode == 2) { mouseEvent(1, 'mouseup', canvas, primaryTouch); } else if (window.canvasTouchMode == 3) { e.preventDefault(); // This prevents some mobile devices from dispatching a mousedown + mouseup event after a touch is ended mouseEvent(2, 'mouseup', canvas, primaryTouch); } window.canvasTouchMode = 5; } } if (e.targetTouches.length == 0) { // We want to wait until all fingers are off the canvas before we reset for the next cycle window.canvasTouchMode = 0; window.canvasPrimaryID = null; } } canvas.addEventListener('touchend', canvasTouchEnd, false); canvas.addEventListener('touchcancel', canvasTouchEnd, false); // TODO: Find out why this is different than touchend setButtonVisibility(window.fakelock != null); //Updates our mobile controls when the canvas finally loads // All of the touch buttons const strafeRightButton = createTouchButton('strafeRightButton', 'inGame', 'div'); strafeRightButton.classList.add('strafeSize'); strafeRightButton.style.cssText = 'left:24vh;bottom:22vh;'; document.body.appendChild(strafeRightButton); const strafeLeftButton = createTouchButton('strafeLeftButton', 'inGame', 'div'); strafeLeftButton.classList.add('strafeSize'); strafeLeftButton.style.cssText = 'left:5.5vh;bottom:22vh;'; document.body.appendChild(strafeLeftButton); const forwardButton = createTouchButton('forwardButton', 'inGame', 'div'); // We use a div here so can use the targetTouches property of touchmove events. If we didn't it would require me to make an actual touch handler and I don't want to forwardButton.style.cssText = 'left:14vh;bottom:22vh;'; forwardButton.addEventListener( 'touchstart', function (e) { keyEvent('w', 'keydown'); strafeRightButton.classList.remove('hide'); strafeLeftButton.classList.remove('hide'); forwardButton.classList.add('active'); }, false, ); forwardButton.addEventListener( 'touchmove', function (e) { e.preventDefault(); const touch = e.targetTouches[0]; // We are just hoping that the user will only ever use one finger on the forward button if (!buttonTouchStartX) { // TODO: move this to a touchstart event handler buttonTouchStartX = touch.pageX; } const movementX = touch.pageX - buttonTouchStartX; if (movementX * 10 > window.innerHeight) { strafeRightButton.classList.add('active'); strafeLeftButton.classList.remove('active'); keyEvent('d', 'keydown'); keyEvent('a', 'keyup'); } else if (movementX * 10 < 0 - window.innerHeight) { strafeLeftButton.classList.add('active'); strafeRightButton.classList.remove('active'); keyEvent('a', 'keydown'); keyEvent('d', 'keyup'); } else { strafeRightButton.classList.remove('active'); strafeLeftButton.classList.remove('active'); } }, false, ); forwardButton.addEventListener( 'touchend', function (e) { keyEvent('w', 'keyup'); // Luckily, it doesn't seem like eagler cares if we dispatch extra keyup events, so we can get away with just dispatching all of them here keyEvent('d', 'keyup'); keyEvent('a', 'keyup'); strafeRightButton.classList.remove('active'); strafeLeftButton.classList.remove('active'); strafeRightButton.classList.add('hide'); strafeLeftButton.classList.add('hide'); forwardButton.classList.remove('active'); buttonTouchStartX = null; }, false, ); strafeRightButton.classList.add('hide'); strafeLeftButton.classList.add('hide'); document.body.appendChild(forwardButton); const rightButton = createTouchButton('rightButton', 'inGame'); rightButton.style.cssText = 'left:24vh;bottom:12vh;'; rightButton.addEventListener( 'touchstart', function (e) { keyEvent('d', 'keydown'); }, false, ); rightButton.addEventListener( 'touchend', function (e) { keyEvent('d', 'keyup'); }, false, ); document.body.appendChild(rightButton); const leftButton = createTouchButton('leftButton', 'inGame'); leftButton.style.cssText = 'left: 4vh; bottom:12vh;'; leftButton.addEventListener( 'touchstart', function (e) { keyEvent('a', 'keydown'); }, false, ); leftButton.addEventListener( 'touchend', function (e) { keyEvent('a', 'keyup'); }, false, ); document.body.appendChild(leftButton); const backButton = createTouchButton('backButton', 'inGame'); backButton.style.cssText = 'left:14vh;bottom:2vh;'; backButton.addEventListener( 'touchstart', function (e) { keyEvent('s', 'keydown'); }, false, ); backButton.addEventListener( 'touchend', function (e) { keyEvent('s', 'keyup'); }, false, ); document.body.appendChild(backButton); const jumpButton = createTouchButton('jumpButton', 'inGame'); jumpButton.style.cssText = 'right:20vh;bottom:20vh;'; jumpButton.addEventListener( 'touchstart', function (e) { keyEvent(' ', 'keydown'); }, false, ); jumpButton.addEventListener( 'touchend', function (e) { keyEvent(' ', 'keyup'); }, false, ); document.body.appendChild(jumpButton); const crouchButton = createTouchButton('crouchButton', 'inGame'); crouchButton.style.cssText = 'left:14vh;bottom:12vh;'; crouchButton.addEventListener( 'touchstart', function (e) { keyEvent('shift', 'keydown'); window.crouchLock = window.crouchLock ? null : false; window.crouchTimer = setTimeout(function (e) { // Allows us to lock the button after a long press window.crouchLock = window.crouchLock != null; crouchButton.classList.toggle('active'); }, 1000); }, false, ); crouchButton.addEventListener( 'touchend', function (e) { if (!window.crouchLock) { keyEvent('shift', 'keyup'); crouchButton.classList.remove('active'); window.crouchLock = false; } clearTimeout(window.crouchTimer); }, false, ); document.body.appendChild(crouchButton); const inventoryButton = createTouchButton('inventoryButton', 'inGame'); inventoryButton.classList.add('smallMobileControl'); inventoryButton.style.cssText = 'right:19.5vh;bottom:0vh;'; inventoryButton.addEventListener( 'touchstart', function (e) { keyEvent('e', 'keydown'); }, false, ); inventoryButton.addEventListener( 'touchend', function (e) { keyEvent('shift', 'keydown'); // Sometimes shift gets stuck on, which interferes with item manipulation in GUI's keyEvent('shift', 'keyup'); // Sometimes shift gets stuck on, which interferes with item manipulation in GUI's keyEvent('e', 'keyup'); }, false, ); document.body.appendChild(inventoryButton); const exitButton = createTouchButton('exitButton', 'inMenu'); exitButton.classList.add('smallMobileControl'); exitButton.style.cssText = 'top: 0.5vh; margin: auto; left: 1vh; right:8vh;'; exitButton.addEventListener( 'touchstart', function (e) { keyEvent('`', 'keydown'); }, false, ); exitButton.addEventListener( 'touchend', function (e) { keyEvent('`', 'keyup'); }, false, ); document.body.appendChild(exitButton); // ---Input Handling--- // This code is a mess, specifically because Android is so so SO inconsistent with how it handles the keyboard // Some keyboards dispatch key events, some directly append text, and none of them meet the most basic standards supported by most other devices // This mess is my attempt at dealing with that, and it will most likely only ever be triggered by Android // // It has three main modes. // 1) Standard keyboard mode: // This mode keeps the hiddenInput empty, saves the last key press, and on every keypress checks if it the keys are being pressed incorrectly. // If there is a problem, it switches to compatibility mode, using beforeinput and input events instead of keydown and keyup // 2) Compatibility mode: // This most is most likely going to be used by Android, because Android only every dispatches keyCode 229 for any keypress // When we enter this mode, we grab the last known key press and redispatch it, and programatically dispatch key events by reading e.inputType and e.data from beforeinput // Unfortunately, Android is weird with this as well. Sometimes it only dispatches insertCompositionText events, and sometimes it gives the correct inputTypes as well // Additionally, programmatically setting the input's text contents (BECAUSE ANDROID IGNORES PREVENTDEFAULT AGHHHHH) dispatches a repeat of the previous event // Luckily, we can check if this happens when we first create the input, which necessitates the third mode: // 3) Duplicate mode: // If we are getting duplicate inputs, this mode ignores every other input if it matches the state saved in window.previousKey // If users make it to this mode and still are having issues, it may be best just to remove support for their device // ---Input Handling--- const hiddenInput = document.createElement('input', true); hiddenInput.id = 'hiddenInput'; hiddenInput.classList.add('inMenu'); hiddenInput.style.cssText = 'position:absolute;top: 0vh; margin: auto; left: 8vh; right:0vh; width: 8vh; height: 8vh;font-size:20px;z-index: -10;color: transparent;text-shadow: 0 0 0 black;'; // We hide the input behind a key because display: none and opacity:0 causes issues hiddenInput.addEventListener( 'beforeinput', function (e) { // For some reason beforeinput doesn't have the same deletion problems that input has on Android e.stopImmediatePropagation(); // Android ignores this and the prevent default, so this will probably be removed in the future e.preventDefault(true); // We pass a value because we've hijacked the prevent default function to have a "should bypass" boolean value const inputData = e.inputType == 'insertLineBreak' ? 'return' : e.data == null ? 'delete' : e.data.slice(-1); // Saves the last key press. if (!window.lastKey) { // When we first set hiddenInput's text contents to " " we can use this to check if Duplicate Mode is needed window.console.warn('Enabling blocking duplicate key events. Some functionality may be lost.'); window.inputFix = true; } if (window.keyboardFix) { if (e.inputType == 'insertLineBreak') { // Detects return key press keyEvent('enter', 'keydown'); keyEvent('enter', 'keyup'); } else { const sliceInputType = e.inputType.slice(0, 1); // Android doesn't constiently dispatch the correct inputType, but most of them either start with i for insert or d for delete, so this dumb solution should be good enough. if (sliceInputType == 'i' && e.data) { // Android sometimes always dispatches insertCompositionText inputTypes, so checking that e.data isn't null is necessary const isDuplicate = window.lastKey == inputData && window.blockNextInput && window.inputFix; if (isDuplicate) { // If our key press matches the last unblocked key press and we are in duplicaye mode, ignore the input window.blockNextInput = false; } else { const isShift = inputData.toLowerCase() != inputData; if (isShift) { // The Eaglerclient only uses e.key, e.keyCode and e.which, so we have to dispatch the shift key event separately keyEvent('shift', 'keydown'); keyEvent(inputData, 'keydown'); keyEvent(inputData, 'keyup'); keyEvent('shift', 'keyup'); } else { keyEvent(inputData, 'keydown'); keyEvent(inputData, 'keyup'); } window.blockNextInput = true; } } else if (sliceInputType == 'd' || !e.data) { keyEvent('backspace', 'keydown'); keyEvent('backspace', 'keyup'); window.blockNextInput = false; // If we delete a character, there couldn't be a duplicate of the previous key press } } } window.lastKey = inputData; // Saves the last key pressed hiddenInput.value = ' '; //This previously allowed us to have a character to delete, but beforeinput doesn't require this. This does allow us to check wether Duplicate Mode is necessary though }, false, ); hiddenInput.addEventListener( 'input', function (e) { // Since we are using beforeInput for input detection, setting the text contents of hiddenInput causes weird behavior, so we use input instead if (hiddenInput.value != ' ') { // Avoid updating it if not needed so Duplicate Mode doesn't throw a fit hiddenInput.value = ' '; } }, false, ); hiddenInput.addEventListener( 'keydown', function (e) { // Enables Compatibility Mode if we receive an invalid key press event if ((e.keyCode == 229 || e.which == 229) && !window.keyboardFix) { window.console.warn( 'Switching from keydown to input events due to invalid KeyboardEvent. Some functionality will be lost.', ); window.keyboardFix = true; if (window.lastKey) { // Resend the last saved key press (which is being tracked by the beforeinput event listener) so the transition to Compatibility Mode isn't noticeable keyEvent(window.lastKey, 'keydown'); keyEvent(window.lastKey, 'keyup'); } } }, false, ); hiddenInput.addEventListener('blur', function (e) { // Updates window.hiddenInputFocused to reflect the actual state of the focus window.hiddenInputFocused = false; }); document.body.appendChild(hiddenInput); const keyboardButton = createTouchButton('keyboardButton', 'inMenu'); keyboardButton.classList.add('smallMobileControl'); keyboardButton.style.cssText = 'top: 0.5vh; margin: auto; left: 6vh; right:0vh;'; keyboardButton.addEventListener( 'touchstart', function (e) { e.preventDefault(); }, false, ); keyboardButton.addEventListener( 'touchend', function (e) { e.preventDefault(); if (window.hiddenInputFocused) { hiddenInput.blur(); } else { hiddenInput.select(); window.hiddenInputFocused = true; } }, false, ); document.body.appendChild(keyboardButton); const placeButton = createTouchButton('placeButton', 'inGame'); placeButton.style.cssText = 'right:6vh;bottom:37vh;'; placeButton.addEventListener( 'touchstart', function (e) { mouseEvent(2, 'mousedown', canvas); }, false, ); placeButton.addEventListener( 'touchend', function (e) { mouseEvent(2, 'mouseup', canvas); }, false, ); document.body.appendChild(placeButton); const breakButton = createTouchButton('breakButton', 'inGame'); breakButton.style.cssText = 'right:19vh;bottom:41vh;'; breakButton.addEventListener( 'touchstart', function (e) { mouseEvent(0, 'mousedown', canvas); }, false, ); breakButton.addEventListener( 'touchend', function (e) { mouseEvent(0, 'mouseup', canvas); }, false, ); document.body.appendChild(breakButton); const selectButton = createTouchButton('selectButton', 'inGame'); selectButton.style.cssText = 'right:6vh;bottom:49vh;'; selectButton.addEventListener( 'touchstart', function (e) { mouseEvent(1, 'mousedown', canvas); }, false, ); selectButton.addEventListener( 'touchend', function (e) { mouseEvent(1, 'mouseup', canvas); }, false, ); document.body.appendChild(selectButton); const scrollUpButton = createTouchButton('scrollUpButton', 'inGame'); scrollUpButton.classList.add('smallMobileControl'); scrollUpButton.style.cssText = 'right:6.6vh;bottom:0vh;'; scrollUpButton.addEventListener( 'touchstart', function (e) { wheelEvent(canvas, -10); }, false, ); document.body.appendChild(scrollUpButton); const scrollDownButton = createTouchButton('scrollDownButton', 'inGame'); scrollDownButton.classList.add('smallMobileControl'); scrollDownButton.style.cssText = 'right:25.8vh;bottom:0vh;'; scrollDownButton.addEventListener( 'touchstart', function (e) { wheelEvent(canvas, 10); }, false, ); document.body.appendChild(scrollDownButton); const throwButton = createTouchButton('throwButton', 'inGame'); throwButton.classList.add('smallMobileControl'); throwButton.style.cssText = 'right:13vh;bottom:0vh;'; throwButton.addEventListener( 'touchstart', function (e) { keyEvent('q', 'keydown'); }, false, ); throwButton.addEventListener( 'touchend', function (e) { keyEvent('q', 'keyup'); }, false, ); document.body.appendChild(throwButton); const sprintButton = createTouchButton('sprintButton', 'inGame'); sprintButton.style.cssText = 'right:19vh;bottom:53vh;'; sprintButton.addEventListener( 'touchstart', function (e) { keyEvent('r', 'keydown'); window.sprintLock = window.sprintLock ? null : false; window.sprintTimer = setTimeout(function (e) { window.sprintLock = window.sprintLock != null; sprintButton.classList.toggle('active'); }, 1000); }, false, ); sprintButton.addEventListener( 'touchend', function (e) { if (!window.sprintLock) { keyEvent('r', 'keyup'); sprintButton.classList.remove('active'); window.sprintLock = false; } clearTimeout(window.sprintTimer); }, false, ); document.body.appendChild(sprintButton); const pauseButton = createTouchButton('pauseButton', 'inGame'); pauseButton.classList.add('smallMobileControl'); pauseButton.style.cssText = 'top: 0.5vh; margin: auto; left: 0vh; right: 0vh;'; pauseButton.addEventListener( 'touchstart', function (e) { keyEvent('`', 'keydown'); }, false, ); pauseButton.addEventListener( 'touchend', function (e) { keyEvent('`', 'keyup'); }, false, ); document.body.appendChild(pauseButton); const chatButton = createTouchButton('chatButton', 'inGame'); chatButton.classList.add('smallMobileControl'); chatButton.style.cssText = 'top: 0.5vh; margin: auto; left: 0vh; right: 14vh;'; chatButton.addEventListener( 'touchstart', function (e) { keyEvent('t', 'keydown'); }, false, ); // For some reason dispatching a keyup event for this closes the chat, which is really weird document.body.appendChild(chatButton); const perspectiveButton = createTouchButton('perspectiveButton', 'inGame'); perspectiveButton.classList.add('smallMobileControl'); perspectiveButton.style.cssText = 'top: 0.5vh; margin: auto; left: 0vh; right: 28vh;'; perspectiveButton.addEventListener( 'touchstart', function (e) { keyEvent('f', 'keydown'); keyEvent('5', 'keydown'); }, false, ); perspectiveButton.addEventListener( 'touchend', function (e) { keyEvent('f', 'keyup'); keyEvent('5', 'keyup'); }, false, ); document.body.appendChild(perspectiveButton); const screenshotButton = createTouchButton('screenshotButton', 'inGame'); screenshotButton.classList.add('smallMobileControl'); screenshotButton.style.cssText = 'top: 0.5vh; margin: auto; left: 28vh; right: 0vh;'; screenshotButton.addEventListener( 'touchstart', function (e) { keyEvent('f', 'keydown'); keyEvent('2', 'keydown'); }, false, ); screenshotButton.addEventListener( 'touchend', function (e) { keyEvent('f', 'keyup'); keyEvent('2', 'keyup'); }, false, ); document.body.appendChild(screenshotButton); const coordinatesButton = createTouchButton('coordinatesButton', 'inGame'); coordinatesButton.classList.add('smallMobileControl'); coordinatesButton.style.cssText = 'top: 0.5vh; margin: auto; left: 14vh; right: 0vh;'; coordinatesButton.addEventListener( 'touchstart', function (e) { keyEvent('f', 'keydown'); keyEvent('3', 'keydown'); }, false, ); coordinatesButton.addEventListener( 'touchend', function (e) { keyEvent('f', 'keyup'); keyEvent('3', 'keyup'); }, false, ); document.body.appendChild(coordinatesButton); } // CSS for touch screen buttons, along with fixing iOS's issues with 100vh ignoring the naviagtion bar, and actually disabling zoom because safari ignores user-scalable=no :( const customStyle = document.createElement('style'); customStyle.textContent = ` html, body, canvas { height: 100svh !important; height: -webkit-fill-available !important; touch-action: pan-x pan-y; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; outline: none; -webkit-tap-highlight-color: rgba(255, 255, 255, 0); } .mobileControl { position: absolute; width: 9vh; height: 9vh; -webkit-user-select: none; -ms-user-select: none; user-select: none; padding:0px; background-color: transparent; box-sizing: content-box; image-rendering: pixelated; background-size: cover; outline:none; box-shadow: none; border: none; margin: 1vh; opacity: 0.5; } .mobileControl:active { opacity: 0.75; } .strafeSize { width: 7.5vh; height: 7.5vh; } .smallMobileControl { width: 6vh; height: 6vh; margin: 1vh 0vh; } .hide { display: none; } #fileUpload { position: absolute; left: 0; right: 100vw; top: 0; bottom: 100vh; width: 100vw; height: 100vh; background-color:rgba(255,255,255,0.5); } .strafeRightButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAARRJREFUOE+dk0EORTAQhkci2HAEF3ESJ7Rgyw2cwAEkVhI7QhDJe/n7Mk3R0rxu0M7/dWb841RV9SHDmufZdHTbdwCCoO97a5EaOAwDtW1LEtQ0zQnkui4dx/EKBwTLyfNclAZQFEU0juOjGDHLsshLANr3/QdCWUjRdqnZWoO6rqO6rk93pGkqvx9BV3EQBFK4ritZgbIsEyJVrKajA03TdO8RQCYIgAziPqE0LQh/rSxLLQyQJEkojmN6BeFWE+xaFmKNGXE/dDATSBqSfXQ1mwpTy4KQzXtyNoN0Y8EwiNXfzplbG1Ln9r+c/TY2p2ZjYLnWN6HuXPqoKAryPE/EYDMMQzHRWNu2Sa3v++KdYzkezy+eN1EbyruiHgAAAABJRU5ErkJgggAA"); } .strafeLeftButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAUFJREFUOE+dlDGqhDAQhrMgKhYWHsCLeARP4Lk8hIW9N7CxtbQQrAQLQVFUhPf485gwyWbZfZvGJM58zvz546Oqqh/BxrZtfPnx/EEgEzCO40eQaZpE3/dCggBp21ZLdBxH3Pf9BOP7mHddJ2NegoiA4CAIxL7vVjCqkaCyLKVGqOhVFbYeKRag67p0ECUURaHlJkki4jjW9sIwFMuySH0UCMJCNA7yfV+tj+NQcxP6LxAvhaBZlsntr0FIBoyD1nX904haIwGHYRB1XQveHlUFSJqmAhpRRU8g3gIEN0EEiaJIWQGtaaB5njWfmCCzEvqo5iO0RiBbezYI95E0ZJ7n0pD8+E0/cU2443GF1KlxEJns3W3lcVbQO4DtvRIbFdGv4BsQcuSpAdQ0jWLg3tA4z1NOPc+TT9d11TskY59ifgE9QD9CDm6XPgAAAABJRU5ErkJgggAA"); } .forwardButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAAXNSR0IArs4c6QAAAcZJREFUSEulVEuqwkAQ7OAnoujalRfxCJ4gl/ESHsKFa72BJ3AhIiJk60IwKH6ZR43U0JlMiOH1xsx0T1ldU9PRarUy4sX1evW3aq+jxWJher2ePXg4HGoD+AdOp5PsdjuxwEhuNpsCaLPZlPf7bfcHg0Euf7lcROeZPB6P9tMBb7dbB8IiHEQQvKod1O/3+y/wbDYzw+HQMQaz2+1mwTQjMAwxRz1zBM6y7AuMA9CG7bKQDLFeLpd2OZlMXJ0vRRD4fD67ln0J5vO5ZU9ZkiRxqmjwghRkrLWlrmmaynq9ljiOJYoiud/vMh6PZTQaBS8bGj+fz7wUqPTbA9tGoyGtVssCvV4v+Xw+ollrQqXAmgbZdjodt22MkcfjUcoaditcni+Fz5ZO8VnrLp2PtSu0FJotWEJfBL4RYE2H+MA5xijUNtNsAQZd6YwyrStdEdKWbMkca+0QPqzcy/PtRrZgSZuFnrPW+mfgqrmg87CeBg76uA5gqBauKABzVmAIMbrdbm7IcB/W0wOIjvrpgdQdmwAPMtZ28tsEc90JH4vugMDOxxiZfDH/1RjnLfB0OjUhUCT7/X7wf5CDDRHtdtv+4sK4Rv4PfpOwZeVjdiYAAAAASUVORK5CYIIA"); } .rightButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAAXNSR0IArs4c6QAAAVhJREFUSEudlT2qhTAQhSOICKK1lRtxCS7PRVjYuwaxtbKyFewURUS4j5PHyNy8/OibKpiZb04mmdFrmuYjDLZtm2nL+d1TwSpsmiYJSdNU0NpGnedZjOMovLquP1EUibZtnSqeOAAKk2As+r53xvm+L67r0volSSKWZZFqX4MRoMIJSN+d4KqqZOY8z0WWZV8qbcoBXtf1txS4FBSdBwAchqE4jsOYQFcTgM/z/AZzRwLTN1sCLug1+EkC+AzDIIIg+KuYLkNVrB6bTlAUhUAMmbbGtlJgD08Nx+aGBBz+L7Dusozgsixlg+BV6BTrVMLPWQoX2FRb/r5R433fZanuy3sKtj03Dr47zwV2NYjahY/Appa2Tau7QbhiOhIFmibZazACqEF0k4yAfKKpIrSKbUqe7t0NglLQ7+RpsMtPjk2Au66Tow7DgxscyOI4lkv4wbgv9yP/HwHyemKMQPbVAAAAAElFTkSuQmCC"); } .leftButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAAXNSR0IArs4c6QAAAUdJREFUSEutlTuqhUAMQEcQEUFrt+a6XIa17sDG1sJSsBLsFEVFuJcEMoxhPm8uL5WOycl3YtA0zUcYZN930yfneaCCVdA8z05jncKyLGIcRxFUVYUR933/E4gbAfS+bzs4DEPxPM/LVncGClmWiXVdMVorGAAgHOxKC8AgP5VimibRti0CiqJ4+ZLgsiw/eZ6LYRic0anAOI7FeZ5a8LZtIgAwuIRumoQDSc8ExlLYwCYgB1Pj4PxVCh4xdLeua7SHlE3iFTFBCQhTQRPCHXjVmINt4+UFBpCrFJSFN5ii/Gvz1KyszePp/8u4qaPjckClAJvjOPCCeUVsc6C70riEdBcExitJEmyiLQPdxMjtxsGUFhnptptpddLNM0bsWo3qd+7kFTH9TnyANl253bquk3qQRhRF+A7PXK7rkkdpmqIO6QOQ5AuINXqeGbRlFgAAAABJRU5ErkJgggAA"); } .backButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAAXNSR0IArs4c6QAAAXtJREFUSEuVlT3KhEAMhiMsKgt6BC/iETyB59pDWFjrDWxsLSwFK8FCUFZ2RdiPCBky+ca/wBbjxGfeeROzVp7nPxDxfr/lo9trC8F7oK7rbgP7voemacBK03RTXFXVbYjpBYRiKHBd17Cuq8r1fR/meYbn8wnjOGqMx+OhcjGP7yN4WRazYp6MEAx+KK45nK81MHqJ3pyFhJ1aIcFJkpydoe3HcazWmscmsOu6l+CfzweM4NfrtXUFt6JtWyiKAo7g6Dn+wjCEIAiU56h4miawCDwMg1YgtIPACKAi8mtItbiniscV8+KcqUYoqeWHGcHSVK5a7km1JEoVz+QxQfZUm9ReBlOiSbXJWw7Wise7gn95UvWet3TLf11x9OVx1Sa1XMguGK8kBw8OmSzLNlFRFAGCKAh66vHecKEJxqGHs+KoK64MHQm/1G7yJf71yTHKi7cNelRMfyeXps7FpA1clqVKx+nP4/v9bkvP87Tn2KsUjuOAbdtqjXt/DvSA6WCSwboAAAAASUVORK5CYIIA"); } .jumpButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAARlJREFUOE+dlD2KhVAMhSMIWrkx1+CqXISFtS7C1sLSVrAQFEVFmOFcyH25eXNVJpWS+OXkz6Cu6x8Stq6rfH39HEjQX5BhGG5h4zhS3/cUlGVpFLVt+yp7GIZ0XZeNBQTmgJIkoW3bjEMGywwcw36AzvN0QTrbPM9UVZXhpGlKgGgDaFmWD6jrOkcFQ+I4Nt/u++7AOOkXSPZIQ1AGPtQwJHBAmAy6D9MQXYqGOT2SoKIoiMvxjRGwLMtMKywoz3MzflakQVyWhGqQGb8G/ac0BzRNk52aD3bbbK2IS3gaP8fZqWmQXMq7hfzaI5+ip8PDliPRK0USBgVs+mjNiUAR/wqeVPj8FtQ0jY3BJcOO4/ByoyiyPo77BUACR9z5EdScAAAAAElFTkSuQmCC"); } .crouchButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAASVJREFUOE+dlDsKhEAMhkcQtPJiVh7AU3kCKwsLKz2ErYWlrWAhKIqKsEsCmc08fLB/pSR++ZPM6FRV9RFMy7Lw19fPDgfZIH3f38KGYRBd1wknz3N01DTNq+qu64rzPGUuQEAKKAgCsa4rBngyr0A5FAfQcRwqSK82TZMoyxI5YRgKgOgC0DzPP1DbtooLgvi+j99u26bAqKgB4jPikCiK0EmapgYMCigg2AxMH6RDeCtFURgwZUYclGWZgHbAiU0Ei+MYRyFBSZLg+smRDoK2wCXJBsL166B/WlNA4zjKrdlgtvkow9YdUQtP66c8uTUdxA/l3YE0ztGVo6eLR0t45YjDwAFJv7R4RcAR/QqeXFzFJaiua5kDNxm07/sl1/M8GaO8L0fuVtxHFTIkAAAAAElFTkSuQmCC"); } .crouchButton:active { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAStJREFUOE+dlLGKhEAMhiOICqKdYGthaetL2GhlNQ9nZWFh5YtZiIKwRwLJjXHc3bu/WZnJfPknyazXtu0LACAMQ/whHcch3/qD44IgkK26rsFDUJIkjwc/bRRFAVmW/YJw4Rv5vg/neUooQlDiCEFpmsK2bbRhB9sJOIb3nSCdbV1XWJaFOE3TUCKtG6gsy4sLhkRRRGf3fb/AOKnTEWezIV3XkZNhGG4wjBeQMYbaz8XWEPsq8zzfYI+gcRwBr4NOXGKYMYZKIaC+7184XOxIg/Ba6JLlAuV5Dp4G/edqBOLJtrvmgrnqgy6rqroPpF2TT+3n2EeQPZTvBpLjBKRr9M17wxhugoD0HOlnwmBcZ9nv8ALiv4J3brAzWnEcy5I3TRNN9l9lQ/DsD7Mvyc0ay6bsAAAAAElFTkSuQmCC"); } .inventoryButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAedJREFUOE+tlM1OwkAQx6elTYiCkSMnvKAX38KLr+DRm69hfAIP3jwbE24ceA4+TDBpCMWEFGgMBCgBUlrzXzLLLsVYo3NpOjv72/987Bq1Wi2mf7AgCATFYOB0OiXTNCV6s9mQYRiUyWSkbzKZHDx6uVyS7/vU7Xa3wPl8Ts1mkxgSx7EAAQhfFEUaHH7ECEWGIb6u6+4UAliv16VCAMIwJMuyxAb8Q72qdl8q1MmUkW6j0dCAgADAZYAiVoON+OeD4O/1erpCABEAs237YK14nRcBRUlg/X6f1uv1toYodrvdFikiVQ7CBiiEArVhh05DyhLY6XRoNBpJIMAMTTtRCeBwOCQ057h8KRiB80a5XC61DzXUFAIYF8/o/u6W3t0Penh8ovJJNuErZU2yS+eJOMdxdk1Bygy8ub4iz/+k55dXCfzJd3F6JIZaU+h5Hi0WC/IiS5xUNEMqFAo0Ho81Xz6fp9lspvlQGplypVKJB4MBAci3I20j1DipUAWqQ4yZ+24ecfD+FCSAqKFq2MSDrN5bvi081BgxWAKIOVSvFwYcm9W7zI+BGsfXL1FDBvImfgiQGgNYKWfCzxti8NqgWQbXEO9ZGuMUkcF+UySw1WqJGvzFMIOr1WqrsFqtaiws/sYAYvsC6d7MZCUlFo0AAAAASUVORK5CYIIA"); } .chatButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAh5JREFUOE99lN8vA0EQx2fjkYY3HvB/8V4kEg8SBJUGVVJU4rfeVRO01bpIT8VviYjEq2tF678g5ZGVmbm9Hy02l9zd7MxnZ747u+Lhsig7OrtBDQkS8BFCkAn/BQiQUrILmvHTfqOfeXEDAkE4397Z5TqQn/BBmMtwtQACTy5vQU8kQTxcFSmJSqncsKAT4AGohBQwrqcBpHQzUiDlUB/gpFL3QSAsX5WGIBUcHJn4K47sWmyONERN4toBfftA6ISQz9q7q4ONRM2UiM2BVtAW50izFT3N2t9fmKTRa7lMhr7REIF2tExDVv3BXtqAlkAbJGIRml9JZmhjxN1ZgXatalk0MTAehs/am73DvHOubrxrmNH2/DSDUjkW+7ZokGfFYo0GQ9PwUXuDhJb9Vaf+YA80B9pgPRIm/9W9QwZdF3LYflCxShQ4FI4QSPWRl6ZsWNrqTIgkW9vPc2nnRtbOiEHDs/P/gNBDUGnxqXHqz80Dg97iNLcvsWtfnrghv7+/YGxhmWA4cPX6sTQ5xtpJgK3DY8B4YWZSEvsANcJRD4mODjeAmiiSu2EnbzKokN6l81gpPVPARCzuBP4GoeKwGe1DnDwy+UwqULWMIHe7fSeepXFuBVUWmvWjAouNIDS8WJyRs6K6PjzXinOVeIr1ZJSSj6UqGHmDU8SC7R70/3NrqnnVqM51oy9FZXRDd7Oxa+C7y9/ZXKGnfI9WP7ZEKinpenUjAAAAAElFTkSuQmCC"); } .pauseButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAjJJREFUOE9lVO1qE1EQnXkK+6P6FP7oI/gMraIoFIMr1WL9aNEfRWNtS1Li1ti6aYr5sEkXyRrRRC2IFNqCgptIE5/myszcO7tbLwuZPTv3zJmTOxcP+10zMXkB3DJggB5EZIjeERCMMZJCMIX2l/Ki3jdAIqLv5ybPJwmchxkS4RVyV4AIP/QPINiuAB5+6bKI0WD4X0HdkCJwghxhIWgAGJMockQu4ewGlXImYCJq37VGROn2RU3WlqQvwx7S98KbOscZIpc4ffOW7mm+3uRYMHFZMImLQUOiH72IPfo7HKqCmdxtNbRZ9jmeyXm8kZIdRnix0uQ/Br9/6nAH4zhWsy97c7YeQt3f4PiKd0dV1vwNjYvVlph90A2ZaBSLR7Suzt21McJuqUB5cE0xgN1SkTHKL73dE6KvnRZDo3igVa7P39NztFNYZ/zG/IK2y5j9Z17W2tLa5/CdVWSJEGB24YESba++YILZ+4TRQthaW9HD+6oeMil+bNUMndrT3/ZAIkLu4aJWLz/Pc5x7tKhE5ZU8m05Pee890H6MmlVD54A8cstbeqxm+8+WGfaWnujI+E+XtbWtdiREncYOz+No8EeEI0Lv+KcoMgYuTV3kuH/ySwsR5oa4sh9JAUc0HhKRnBNxIjXxAuit4NoiONjviNlERMBpLIqcKr0+UteKXiWaCZBSVDVHgzGE7VAkUsN2yLLvotV9zyinfcF63uQ3g0SN7UEmKWlV1abbp8G1F94/2dEyKXciad8AAAAASUVORK5CYIIA"); } .exitButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAiBJREFUOE99U11rE1EQnfsvFKwg/QkK+iaIio+KJNAnbUQoRaGgf0NRTFPU3TTJBjfdTZpN/aJtUHwU8anJJlH/y8o5c+92k1bnJXvn48yZMxNzmETZ2aXzQjMikunn6YYgkuZtb/+LGAAZY+TMuaX/IfwzBpBWMxAzHMQZaEyPxkw+rWeRpCPteFWDWIdRRiITC+Qm1KgRybJ5cOtzFKtBxO5mOIiyLBOZjca5RqXVCvPiRl01QYKIlCsVFu1sw6/p1VbEhgSCa3I0IoHSfQVxFtsigBfJRNt1BXKjHfQ77DcbpQyUKw9yEBSiCSkXDP6O7zN/sx3raJ97IcWejVOOEO1/lX4cHoMVFuDg7pRWpHzrGnO2gq6O+SFqM06NrPWG3wi2eFZ43y6tyN3rV/PcN+GuAu2FDTL/lU7owI6MGLm3vk6NcWMa0O01a5vHQxoRrzPQmt22bxkpEOpWHz2eE9aBoSNg69VXtomIFw1Uo27zbc4IHw83NsjCCax6z7/R2Xv5gmD13nslsONvcSe/0ykDa0+f6N3YYggLS7qhOye+Xz97zt9G/yNZmtCrZSjC+mEH33/mWwPIzSuXyO5wwX/j8kWOHySflFHo16jRdKQawVAEQ7JOpqOd9Iu0yMiIeefV+F+bjSe5gAv3d0L4on5kZOxoP9I/kvSTvDM6uCPiai0j+ItvsnU+PJYvLOteYfZ89WqKiZrg/LkONucvWvgqB/7CAfgAAAAASUVORK5CYIIA"); } .keyboardButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAfdJREFUOE+VVN1KG0EU/ubait7ZC9un6IXP4BvU9qJQJFhsEUGpPkAsWJL+xEjMxhRjEhMXyRqx2lYoRfCmF81uyca3UGKvp5xzZpJsFI3Lwjkz88035+ebUafHdf1w/DHsp6FBv1KKp2isoKC1FghNk2ss4byjH1BEROtj44+6AMapCInwCrk9gAj3j0/gbOSgTr/VOYjQ9yPAzo47nIRTBLTuRhT6waB7IzgmovRtakTEaRO7orR4cKefyG4zJkLUPcpWU6rRrc11P+kUBfHryOMatYIAz2Ze96VnOmXaZTtoi11aTyGZK3Fj1M/DGqPPGw08n53D0PAI/rUv2V61L/BgePTamPCEKaQ+IJmvSAlO6i4TtfwAL97MRyLiUI0UIm03c18+JvBpa0eIvtcqJD+EDR8v5xeQyZYQm5661RIpYXKJVXwuVCW1r26ZjyWi2OJbo2UR5E1RWDkSIrP6Dultl3uhDioFTapt/gnwamkZmWwZsemnbG/67BrZ9Eoc6zt7oP3KK+U16SBsBDg8+30vUU5OPEGm6glRrbjJ9zH0/0phSYzmgg7i53Y9uZP9RJ3bLe0SPd7iO7s1KTYREa5JEfXqb8AkeyLK6zO/BbfqSoiUsCGMjkVVdr1XY4xz3sd1fM3pnC9tN5vMm9QbnH2n+uv5HyQVKCmWNUnHAAAAAElFTkSuQmCC"); } .placeButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAd5JREFUOE+tlMlqQkEQRfs5gAv10xwQEf1Fd4J+h8PChYi6EKeNOCvqC6fltv2ISQxJgQjd1afq1vCCVqsVmn+w/X5vKYGAm83GxGIxh77dbiYIAhOPx93Zer1+Gfp0OpnVamVGo9EDuNvtTLfbNYKEYWhBADm73+8ROOf42IyCwP6Px+NnhgDb7bbLEMD1ejWJRMI+kJxsNvtlccjOSUZup9OJAIGS5eFwMM1m0zpXKhUHJEN8KBNBJ5NJNEOAOGDJZNI9JBjAVCpl8vl8JEOglASbTqfmcrk8akix+/2+lYhUOfHgeDxaIHflcvlbyQ44HA7Ncrl0QB4L6mdYLBbfBy4WC+tMg3xLp9MOTq2oqW+ZTMZ2nBpGMgQITA3QI78Rr+5rtZoNOBgMnk1Bsg+kATK/EQLqnnqXSiXbacYmkuFsNrOpMyr86Di1ZAtoDIZ8bQ9Zabi5c5Lr9Xo4n88NQG2HXyM1hbNqtfpzU3ygdllDyzwK6I8NgTUFiuAkC6guy0Gyt9utG+xCoeB2V0NNIOwTkDnESctOwTUm2pRcLmfPfD+t36caCkg0/7Ol4vtfGKnwG8TXBjWBJPM9e8ckEQW+IdkBe72ercFfjBk8n8+PDBuNRoTF5W8MkOwDqQTfZInxo/8AAAAASUVORK5CYIIA"); } .breakButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAeRJREFUOE+tlMmKAjEURVMO4EL9MZcKKn6iO0G/w2HhQkRdiNNCcVbUak7kplNlNU1DP6iFMXXq3vvyEnQ6ndD8Q51OJ0sJBNzv9yaVSjn08/k0QRCYdDrt1na7XeKnr9er2Ww2ZjKZvIHH49H0+30jSBiGFgSQtdfrFYGzfjgcHLxQKJjpdPqtEGC323UKATweD5PJZCyI36iXWty0220HrNfrVp2zzIZerxcBAgGgGFANnI8Dy+VyFoDdarVqZrNZVCFAIFQ2m03MSsoEwwVPuVw28/nc3O/3d4aEPRwOrUU2kBuFKhQmKZM67FJYdsDxeGzW67UFbrdbu6FYLDqVcWVxWCJwtVq5fNhQKpVMPp//yEw2a7Wa+yBOyDCi0AeSEWEDpQEo55GySqXiOk7jiGg0Gn03BcsAdb7URaBqgN9R5eufhI8MF4uFbcLlcokcC/nS8dD0oIr9Kme52WyGy+XSANR0xJsATN1MPE9+l33g+Xy2+5Ms+1Dl5sOdZQHJUeMUbwovak1KsYttNetHoF6kk+TlqwbMmGkM+a05/8jQV8jGRqNhHSl8ZpjibKr8BnHbcAMFssx9RjNU/qT4WckiBzyeoQMOBgN3/fzUxd/WmZLb7fZW2Gq1Ivv58y8FSPUFhmoAc4SumvkAAAAASUVORK5CYIIA"); } .selectButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAcRJREFUOE+tlMuuAUEQhqsZCWJ4KAsLL0pG4pKQWHgHzMJK2LlFIm5BMCd/SbXumRGccyoRme7O13/VX9Wq0+kE9A9xOByYogS43W4pkUho9O12I6UUJZNJvbbZbGKvPp1OtFqtaDKZPID7/Z6GwyEJJAgCBgGItfv9bsGxjjOsSCn+n06nT4UA9vt98jxPKyiXy9Rutz/6xtn1ev0EIt3BYEC1Wo3S6TRvFItF6vV6dL1eyXEc/Y09rJVKJd5HuhEgFAIohf3Uo263y3ABXi6XRw1R7NFoRJVKhdXg9mw2y3WCUaiTaZhciMwkkLIGjsdjWi6XDETKSDefz78VWq/XrZQt4GKxoGq1qoG5XM5qIXFUlEI1ah6bMhQCiFpKZDIZqwexjvYBAC2FX7PZ1Apns9nTZQG+UhiXO+prmhipIW5AH0oNXddlg9AWpikyPWj4RqMRraHnecF8PicBisu/NsUEHo9Hq4apVCrWadQPCmNdFmDY5UKhwCYgzLmV2cXabreL9qEAw32IKUCYYxc3hubocR+GgYB88zDIebgMxRqI98wcpbdjEjpgAX3f58fxL4F0z+fzQ2Gr1bJY2PwmAJL4ASn3ymSCH/2UAAAAAElFTkSuQmCC"); } .scrollUpButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAfhJREFUOE+tlL1OAkEUhe8sUAkklHQ2dj6Q0crEwsTCWFlZGJ/Aws7amNBR8Bz8mKBBwmJClp+YEGAJkIU1Z8gZdlnWaPQ27A53vnvO3TujSqWSL/8QrutqiiJwNBqJZVkGvVwuRSkliUTCrA2Hw52lZ7OZDAYDabVaa+BkMpFqtSqE+L6vQQBibbVaheBYR45WpJT+tW17oxDAcrlsFALgeZ4kk0m9Ae9QH1S7LRXqjGXYrVQqISAgAACEgox0Oq0foZCFULTdbocVAogERCqVCglAwb2DQ73mNl4kCEVLEJ1ORxaLxbqHaHa9XtcWYZVJUAGF+IIA3p6fysnVteQtT7LZbKgoLBtgs9mUfr8v4/E4doAIRMIuaATY6/XEz+9/O5FQ+Gp/6Jy7+wetNJPJ6H6ihyGFBGITg5u3qziDT730+PSsoehpo9HYfBRYJvD97TVW5c3lheyCoc8Ry47jyHQ6jYU5q6ScHR9FlHGDsVwoFPxutysA8nRsUzE2BNJm7FcOAnmWObScRwJRCD3L5XJmtFjcWCYQPQwG1HLQg+NEZSiKecXsIiJAzCGSeNgx4HgOnmVs5MXAPB6/SA8J5CZeBFDBQrxh6ITXG3Jw28CJomXcZz8JWoSDYMCyAdZqNd2DvwROyXw+XyssFoshFv78TQDE+AI1+8NkGpdDjwAAAABJRU5ErkJgggAA"); } .scrollDownButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAfxJREFUOE+tlMtKY0EQhqtzAdEkuPUBxI0PJO4EF4ILn2AYfItZDLhzBrLLIrjxHXIRIoSQEyGcXBgIuSckOfJ1qPYcY4KD1uZ0OtVf/39XdZt8Ph/IN8RoNLIUo8B+vy+xWMyhl8ulGGMkHo+7uV6v9+HW0+lUut2u1Ov1NXA4HEqpVBKFBEFgQQCZW61WETjz5FhFxtiv53lvCgEWCgWnEMBisZBEImEX8Bv1YbXvpaLOWcZusViMAIEAGI/Hbm0qlXJjFOpGbNpoNKIKAZJAJJNJ+2Wjg+NTOx5VnyQMZA4oR0I0m02Zz+frM+SwK5WKtYhVklAG7PbqQn7+urPATCaztR+w7IC1Wk06nY4DDgYDB3v2XuTPw6MFbgs22gC2222bT4FUGTAC4K4wvmfPMKIQ4Ecwv/tvJ+z3/V85zuxJtVp9KwqWAVIt7PqrhPy4uRaFsWhXnBzub1r2fd9WjVZBKdDL8zPLAXgUW2xlUn1nOZvNBq1WSwDq7dCWUagCP1XlMFDvsjbtZDKxSgkUKpCNtf9UtquyArXKmsAiGp0z1Uin0+7ualPTu8QGkD4kSS87Dc44fJdZqA+D5un12zhDBeoifQiwphvpC6OK9Xkjh9cGN0Yt8559JtQiDsKBZQcsl8v2DL4S3JLZbLZWmMvlIiz+/J8ApPEKFYzOZJBKmFAAAAAASUVORK5CYIIA"); } .throwButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAehJREFUOE+tlLtOAlEQhme5VAK9D2DnG1nZ2VpZG5/Aws7amNBR0PoAFnIxwYQQFhOyXEJCuEOANd/BOeyyG4PRaTZ7Lt/5/5k5xykWi778Q0ynU0NxFDgajSSRSFj0ZrMRx3EkmUzaseFwGHv0YrGQfr8vzWZzB5xMJlKpVEQhvu8bEEDGttttCM44a4wixzFf13X3CgGWSiWrEMB6vZZUKmU28I/6oNpDqaizlrFbLpdDQCAANA0oUjVs5F8PYrzVaoUVAmQBkU6nQwI4kMhkMqFxoKSEaLfbslqtdjkk2bVazVjEqi5iw3w+l5Ozc7NpWn+XXC4XWxgsW2Cj0ZBer2eBgIGiDNjt1aWBXFzfyGliHQuNALvdbuhkChWEfbifZv7u/sFAs9msXY8TchhSqEAmadI4mBIUSk4pHG7q9fq+KFgGSLXG4/GPsDgonRCx7HmezGazo2BAvf5AHp+ejX2UWsv5fN7vdDqCyqDNl9e32GoeDip0MBjschgEetvUUZDDRaiMAA+rTLK10YP3Vm+LNjUtRtgcqkL6MHi9aHA2B++yuavfD4OC9fpFcqhA3aQPAS2hB6lStazPG2t4begQm0Pes2NCLeIgGFi2wGq1anLwl6DCy+Vyp7BQKIRYTP4mAGl8AUQGwWSM9G4HAAAAAElFTkSuQmCC"); } .sprintButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAd1JREFUOE+tlEmKAkEQRSMdwIV6MbeKeEKVciXoORwWLkTUhThtxAFnq/lh/zSzyoaW7gCxDCtf/JjStNvtUP7BDoeDUgyB2+1WEomERd/vdzHGSDKZtL7NZvM29Ol0kvV6LePx+Anc7/fS6/WEkDAMFQQgfI/Hw4PDj3dUkTH6PZlMXgoB7HQ6ViEAt9tNUqmUHsBvqHfVRqVCnU0Z6Xa7XQ8ICAAsAxRRDQ7iNwPBP51OfYUA4gXY8Xi0AvL5vD4jKC2bzeojoCgJbDabyeVyedYQxR4MBpoiUq3VavZwoVAQAOr1uudjIDqRsgWORiNZrVYeMJPJ6LvoYKVSkWq1Kq4PgVxoDLhcLq0CNKnVaikAivEpFovSaDRi0Fwup6mjhp5CAlls1JFQKi2VShIEgReIJRkOh6+mIGUA0a10Oi3X61V2u11MZblc1lq+Sz2W8nw+V+kYFTdlqvsJxo7blIMgCBeLhQDI7Yg24KOmuEAO8bsRQRBatMPw25QJjHaZh9lFlIGGceFQY3bfAjGH7nphVNAkd5d1V78vBq4h1y9WQwJ5iBcBVouBeMNQKa83vIPbBpNhmDLus98YU0QGrqGGFtjv97WofzFsyfl8fipsNpseC39+YgDRvgDSDetkcdqdJgAAAABJRU5ErkJgggAA"); } .perspectiveButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAgxJREFUOE+VlM9L40AUx9+cF8Gbe1D/ld5F6FXv1h8gHirtaltXXGvU1o2g7ura1IJat61haaz4W7AieE6y2P4ZwrLnkfemM0lNl7JDIJnJy+d93zdvhj1d1/jH/kGQgwMHvBhjtIRzBgw45yIEl/Gxdcc46+oOGILwfV//gBdAcawNIrgCLhMg8Oz6Hoz9PLCnmxqJaDiuCvqfB90oAnDuKfKDIrEEsXJZrSuTQFi+LK3puKr0sVgC/v55hQ89vQpkZDW/NcoqPXdMfipQw3GUg5FYEkKhkILU63XIZVeCTgODTaMoEjxeWeRR0/U8Go+nCHRxbsHQcBgQ9H7sZ9K0tJk/oR/DHi6q9Neatq1iJ+cXQdd3wbJKChYOj8BEZJRisOS91SUBKpSF2fc1k0AN21V1T6eWCCRHNDoVULSTXqT4rcOSAN1Wy9h+QB61Gm3mczrg0daXVFsjyqzbRxVR2qX5s6XIUd0aXV4NKNIX5gNdjbDvxyats/PyEceufbFdlTG2kiEQliTv2WS8o6Ld0i/A75l1UuDYB+iRLO3T2lfliQStz812BP2oWAJULR7Qfmw4vwOGJjK6UqTFox27PH9qiT3ZDSS//hfIOK0KsxGEwS92UFHXjQYAPkUF/uw0wayYQiIW3Dp62ue0AdR7MfMdN8aGxrVvhkqOMsVx5gX5lclziiDMO/DeAFwyISmrQZieAAAAAElFTkSuQmCC"); } .screenshotButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAcdJREFUOE+llE1LAkEYx585R3Ssg/WVvEbn3g7RoVBRRJFMS0vBsFLXhHzJlyXcjNJKiBA67264+zUiOk88M+64WxAsLgs7M/vMb/7Pf2YeMhr06JJnBayHAgV8CSFsCPsECFBKeQgOY3PyxTil/wIEQfh/0bM8DWBxxAHhXA63FkDg3WAIUrEMZPTUYyIMTRdBbhoZqQ5A6VTRTCBM30rN1HSR+rovJERJ6YTdkt8WQaZUY34KkKFpzIMNXwi+vz4FaG5+AUrphNNhGy4r1flCb32FeWTqOmz6w27sgWIqDtlyg20MeX3osl0zVRW2g1GHGotaKDXEAlsba6yNSi+TMchWWtzsYU9mIEPVYSccYyBrIk7CtqI0BcjrXQUcR1A+HoXcdZODnrstPH6AHu1G4n8U/QfKxcJwVm3z1B7lm4kiDfYOkq5AmUgQzmsyO6fkvlWleGrHqg6+w5Qrj9IhP1w0bwHnE6VRoXgO0KPA0amrXTsO7EOhrXBQt37F7qOhfbiCWMHljsLv5KwgqdPlZiMI6WN1ZkUV+q6ZILdlLhETnpQeZ59dAPGf92zlRjpJ0EReEv6gTF7OpkF286w6xSBkWvB+AJ1TDClYPe7/AAAAAElFTkSuQmCC"); } .coordinatesButton { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAeZJREFUOE+VlF1LAkEUhs9cR3RZF9avqyiILgwtxRBJttJawT7NNaHS0pZwM/oOIoKud1fSvxHR9cQ544y7rmANgvNx5pn3vHtm2PtDk0+EpkA2DhzwxxijKRwzYMA5FyE4jd3uP8ZZ98/AEITr46HJXgDFMR9EcAVcHoDA64cXMI5KwN4fmySi7bgq6D8d3agAcN5TNAw0E4krfjGrqT6BMH2ZWsdxvan7rJiOxOHn+4s2j4yOAYKkVXrxjPxUoLbjBJ0EBqjEC8E+wrAhMGdUhIC3e4s86riDPZqNJnwgBEjYUSYNuVKVPgx7vW3QV+vY9kCP52JJBZIBhWIVwuF5OFxPQa5cE2a/NE0CtW23v0RgIZFSENwsG0J200mKz59cCNBTo4blB+SRp9AWk2kfBDfLlk8lVEHunNZFanfmeVeRoxbDa+sBiL4aC1Q1Sto/M2me3dROOVbtp+2SooiWCUCy8ehACMYfXFwB7mdWtcyxDtCj5Y3tAGRzZcmXcr+RhbolQI3KMd3HttOCeEYHXT8gK9ATLRoeeltKl5a4k/0gufMvEIw1LhvCbAThxKfdGnr6oACPojL/cDpg1k0hERPuPj3+MV0AtS5GnufG2NK4tmeow1CmeM56QV4l8p0iCOs9eL/YmiwplUjFQgAAAABJRU5ErkJgggAA"); } `; document.documentElement.appendChild(customStyle); }