Version 1.40

This commit is contained in:
voxonoly 2024-07-17 10:40:59 -05:00
parent 3269490eae
commit f7efafafd5
4 changed files with 919 additions and 450 deletions

View File

@ -11,6 +11,9 @@
<meta property="og:title" content="EaglercraftX 1.8" /> <meta property="og:title" content="EaglercraftX 1.8" />
<meta property="og:description" content="Play minecraft 1.8 in your browser" /> <meta property="og:description" content="Play minecraft 1.8 in your browser" />
<script type="text/javascript"> <script type="text/javascript">
// 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() { function isMobile() {
try { try {
document.createEvent("TouchEvent"); document.createEvent("TouchEvent");
@ -22,39 +25,55 @@ function isMobile() {
if(!isMobile()) { if(!isMobile()) {
alert("WARNING: This script was created for mobile, and may break functionality in non-mobile browsers!"); alert("WARNING: This script was created for mobile, and may break functionality in non-mobile browsers!");
} }
window.keyboardEnabled = false; // TODO: consolidate all of these into a single object?
window.crouchLock = false; window.crouchLock = false; // Used for crouch mobile control
window.sprintLock = false; window.sprintLock = false; // Used for sprint mobile control
window.keyboardFix = false; // temporarily set to true until I can figure out whats going wrong with the event listener in charge of switching it window.keyboardFix = false; // keyboardFix ? "Standard Keyboard" : "Compatibility Mode"
// Used for changing touchmove events to mousemove events window.inputFix = false; // If true, Duplicate Mode
var previousTouchX = null; window.blockNextInput = false; // Used for Duplicate Mode
var previousTouchY = null; window.hiddenInputFocused = false; // Used for keyboard display on mobile
var startTouchX = null; window.canvasTouchMode = 0; // Used for canvas touch handling
// better charCodeAt function /*
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() { 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, "`": 192, "[": 219, "\\": 220, "]": 221, "\"": 222}; 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]; return keyCodeList[this];
} }
// Ignores keydown events that don't have the isValid parameter set to true // 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; const _addEventListener = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, "addEventListener", { Object.defineProperty(EventTarget.prototype, "addEventListener", {
value: function (type, fn, ...rest) { value: function (type, fn, ...rest) {
if(type == 'keydown') { if(type == 'keydown') { // Check if a keydown event is being added
_addEventListener.call(this, type, function(...args) { _addEventListener.call(this, type, function(...args) {
if(!args[0].isValid && window.keyboardFix) { if(args[0].isTrusted && window.keyboardFix) { // When we are in compatibility mode, we ignore all trusted keyboard events
return; return;
} }
return fn.apply(this, args); return fn.apply(this, args); // Appends the rest of the function specified by addEventListener
}, ...rest); }, ...rest);
} else { } else { // If it's not a keydown event, behave like normal (hopefully)
_addEventListener.call(this, type, fn, ...rest); _addEventListener.call(this, type, fn, ...rest);
} }
} }
}); });
// Allows typing in #hiddenInput // Overrides preventDefault, because on some (Android) devices you couldn't type into hiddenInput
const _preventDefault = Event.prototype.preventDefault; const _preventDefault = Event.prototype.preventDefault;
Event.prototype.preventDefault = function() { Event.prototype.preventDefault = function(shouldBypass) {
if(document.activeElement.id != "hiddenInput") { if(document.activeElement.id != "hiddenInput" || shouldBypass) { // activeElement is what element is currently focused
this._preventDefault = _preventDefault; this._preventDefault = _preventDefault;
this._preventDefault(); this._preventDefault();
} }
@ -67,18 +86,24 @@ Event.prototype.preventDefault = function() {
function keyEvent(name, state) { function keyEvent(name, state) {
const charCode = name.toKeyCode(); const charCode = name.toKeyCode();
let evt = new KeyboardEvent(state, { let evt = new KeyboardEvent(state, {
key: name, "key": name,
keyCode: charCode, "keyCode": charCode,
which: charCode "which": charCode
}); });
evt.isValid = true; // Disables fix for bad keyboard input
window.dispatchEvent(evt); window.dispatchEvent(evt);
} }
function mouseEvent(number, state, canvas) { function mouseEvent(number, state, element, event = {"clientX": 0, "clientY" : 0, "screenX": 0, "screenY": 0}) {
canvas.dispatchEvent(new PointerEvent(state, {"button": number})) element.dispatchEvent(new PointerEvent(state, {
"button": number,
"buttons": number,
"clientX": event.clientX,
"clientY" : event.clientY,
"screenX": event.screenX,
"screenY": event.screenY
}));
} }
function wheelEvent(canvas, delta) { function wheelEvent(element, delta) {
canvas.dispatchEvent(new WheelEvent("wheel", { element.dispatchEvent(new WheelEvent("wheel", {
"wheelDeltaY": delta "wheelDeltaY": delta
})); }));
} }
@ -148,8 +173,8 @@ const _createElement = document.createElement;
document.createElement = function(type, ignore) { document.createElement = function(type, ignore) {
this._createElement = _createElement; this._createElement = _createElement;
var element = this._createElement(type); var element = this._createElement(type);
if(type == "input" && !ignore) { 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)); document.querySelectorAll('#fileUpload').forEach(e => e.parentNode.removeChild(e)); // Get rid of any left over fileUpload inputs
element.id = "fileUpload"; element.id = "fileUpload";
element.addEventListener('change', function(e) { element.addEventListener('change', function(e) {
element.hidden = true; element.hidden = true;
@ -184,7 +209,7 @@ inMenuStyle.textContent = `
document.documentElement.appendChild(inMenuStyle); 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 // 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) { function waitForElm(selector) {
return new Promise(resolve => { return new Promise(resolve => {
if (document.querySelector(selector)) { if (document.querySelector(selector)) {
@ -208,48 +233,109 @@ function createTouchButton(buttonClass, buttonDisplay, elementName) {
touchButton.classList.add(buttonDisplay); touchButton.classList.add(buttonDisplay);
touchButton.classList.add("mobileControl"); touchButton.classList.add("mobileControl");
touchButton.addEventListener("touchmove", function(e){e.preventDefault()}, false); touchButton.addEventListener("touchmove", function(e){e.preventDefault()}, false);
touchButton.addEventListener("contextmenu", function(e){e.preventDefault()});
return touchButton; return touchButton;
} }
function toggleKeyboard() {
const keyboardInput = document.getElementById('hiddenInput');
if (window.keyboardEnabled) {
window.keyboardEnabled = false;
keyboardInput.blur();
} else {
window.keyboardEnabled = true;
keyboardInput.select();
}
}
waitForElm('canvas').then(() => {insertCanvasElements()}); waitForElm('canvas').then(() => {insertCanvasElements()});
function insertCanvasElements() { function insertCanvasElements() {
// Translates touchmove events to mousemove events when inGame, and touchmove events to wheele events when inMenu // Translates touchmove events to mousemove events when inGame, and touchmove events to wheele events when inMenu
var canvas = document.querySelector('canvas'); var canvas = document.querySelector('canvas');
canvas.addEventListener("touchmove", function(e) { canvas.addEventListener("touchstart", function(e) {
e.preventDefault(); if(window.canvasTouchMode < 2) { // If a touch is initiated but not assigned
const touch = e.targetTouches[0]; // We can get away with this because every other touch event will be on different elements 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
if (!previousTouchX) { window.touchTimer = setTimeout(function(e) {
previousTouchX = touch.pageX; // If our touch is still set to initiaited, set it to secondary touch
previousTouchY = touch.pageY; 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
}
} }
e.movementX = touch.pageX - previousTouchX;
e.movementY = touch.pageY - previousTouchY;
var evt = window.fakelock ? new MouseEvent("mousemove", {movementX: e.movementX, movementY: e.movementY}) : new WheelEvent("wheel", {"wheelDeltaY": e.movementY});
canvas.dispatchEvent(evt);
previousTouchX = touch.pageX;
previousTouchY = touch.pageY;
}, false); }, false);
canvas.addEventListener("touchend", function(e) { canvas.addEventListener("touchmove", function(e) {
previousTouchX = null; e.preventDefault() // Prevents window zoom when using two fingers
previousTouchY = null; var primaryTouch;
}, false) for (let touchIndex = 0; touchIndex < e.targetTouches.length; touchIndex++) { // Iterate through our touches to find a touch event matching the primary touch ID
//Updates button visibility on load if(e.targetTouches[touchIndex].identifier == window.canvasPrimaryID) {
setButtonVisibility(window.fakelock != null); primaryTouch = e.targetTouches[touchIndex];
// Adds all of the touch screen controls break;
// Theres probably a better way to do this but this works for now }
}
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) {
let 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
let strafeRightButton = createTouchButton("strafeRightButton", "inGame", "div"); let strafeRightButton = createTouchButton("strafeRightButton", "inGame", "div");
strafeRightButton.classList.add("strafeSize"); strafeRightButton.classList.add("strafeSize");
strafeRightButton.style.cssText = "left:24vh;bottom:22vh;" strafeRightButton.style.cssText = "left:24vh;bottom:22vh;"
@ -259,7 +345,7 @@ function insertCanvasElements() {
strafeLeftButton.style.cssText = "left:5.5vh;bottom:22vh;" strafeLeftButton.style.cssText = "left:5.5vh;bottom:22vh;"
document.body.appendChild(strafeLeftButton); document.body.appendChild(strafeLeftButton);
let forwardButton = createTouchButton("forwardButton", "inGame", "div"); let 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.style.cssText = "left:14vh;bottom:22vh;"
forwardButton.addEventListener("touchstart", function(e){ forwardButton.addEventListener("touchstart", function(e){
keyEvent("w", "keydown"); keyEvent("w", "keydown");
@ -269,12 +355,12 @@ function insertCanvasElements() {
}, false); }, false);
forwardButton.addEventListener("touchmove", function(e) { forwardButton.addEventListener("touchmove", function(e) {
e.preventDefault(); e.preventDefault();
const touch = e.targetTouches[0]; // We can get away with this because every other touch event will be on different elements const touch = e.targetTouches[0]; // We are just hoping that the user will only ever use one finger on the forward button
if (!startTouchX) { if (!buttonTouchStartX) { // TODO: move this to a touchstart event handler
startTouchX = touch.pageX; buttonTouchStartX = touch.pageX;
} }
let movementX = touch.pageX - startTouchX; let movementX = touch.pageX - buttonTouchStartX;
if((movementX * 10) > window.innerHeight) { if((movementX * 10) > window.innerHeight) {
strafeRightButton.classList.add("active"); strafeRightButton.classList.add("active");
strafeLeftButton.classList.remove("active"); strafeLeftButton.classList.remove("active");
@ -293,7 +379,7 @@ function insertCanvasElements() {
} }
}, false); }, false);
forwardButton.addEventListener("touchend", function(e) { forwardButton.addEventListener("touchend", function(e) {
keyEvent("w", "keyup"); 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("d", "keyup");
keyEvent("a", "keyup"); keyEvent("a", "keyup");
strafeRightButton.classList.remove("active"); strafeRightButton.classList.remove("active");
@ -302,7 +388,7 @@ function insertCanvasElements() {
strafeLeftButton.classList.add("hide"); strafeLeftButton.classList.add("hide");
forwardButton.classList.remove("active"); forwardButton.classList.remove("active");
startTouchX = null; buttonTouchStartX = null;
}, false); }, false);
strafeRightButton.classList.add("hide"); strafeRightButton.classList.add("hide");
strafeLeftButton.classList.add("hide"); strafeLeftButton.classList.add("hide");
@ -335,7 +421,7 @@ function insertCanvasElements() {
crouchButton.addEventListener("touchstart", function(e){ crouchButton.addEventListener("touchstart", function(e){
keyEvent("shift", "keydown") keyEvent("shift", "keydown")
window.crouchLock = window.crouchLock ? null : false window.crouchLock = window.crouchLock ? null : false
crouchTimer = setTimeout(function(e) { window.crouchTimer = setTimeout(function(e) { // Allows us to lock the button after a long press
window.crouchLock = (window.crouchLock != null); window.crouchLock = (window.crouchLock != null);
crouchButton.classList.toggle('active'); crouchButton.classList.toggle('active');
}, 1000); }, 1000);
@ -347,14 +433,20 @@ function insertCanvasElements() {
crouchButton.classList.remove('active'); crouchButton.classList.remove('active');
window.crouchLock = false window.crouchLock = false
} }
clearTimeout(crouchTimer); clearTimeout(window.crouchTimer);
}, false); }, false);
document.body.appendChild(crouchButton); document.body.appendChild(crouchButton);
let inventoryButton = createTouchButton("inventoryButton", "inGame"); let inventoryButton = createTouchButton("inventoryButton", "inGame");
inventoryButton.classList.add("smallMobileControl"); inventoryButton.classList.add("smallMobileControl");
inventoryButton.style.cssText = "right:19.5vh;bottom:0vh;" inventoryButton.style.cssText = "right:19.5vh;bottom:0vh;"
inventoryButton.addEventListener("touchstart", function(e){keyEvent("e", "keydown")}, false); inventoryButton.addEventListener("touchstart", function(e) {
inventoryButton.addEventListener("touchend", function(e){keyEvent("e", "keyup")}, false); 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); document.body.appendChild(inventoryButton);
let exitButton = createTouchButton("exitButton", "inMenu"); let exitButton = createTouchButton("exitButton", "inMenu");
exitButton.classList.add("smallMobileControl"); exitButton.classList.add("smallMobileControl");
@ -362,51 +454,106 @@ function insertCanvasElements() {
exitButton.addEventListener("touchstart", function(e){keyEvent("`", "keydown")}, false); exitButton.addEventListener("touchstart", function(e){keyEvent("`", "keydown")}, false);
exitButton.addEventListener("touchend", function(e){keyEvent("`", "keyup")}, false); exitButton.addEventListener("touchend", function(e){keyEvent("`", "keyup")}, false);
document.body.appendChild(exitButton); document.body.appendChild(exitButton);
// input for keyboard button // ---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---
let hiddenInput = document.createElement('input', true); let hiddenInput = document.createElement('input', true);
hiddenInput.id = "hiddenInput" hiddenInput.id = "hiddenInput"
hiddenInput.classList.add("inMenu") hiddenInput.classList.add("inMenu")
// We are hiding the text input behind button because opacity was causing problems. 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.style.cssText = "position:absolute;top: 0vh; margin: auto; left: 8vh; right:0vh;font-size:20px;z-index:-10;color: transparent;text-shadow: 0 0 0 black;"; hiddenInput.addEventListener("beforeinput", function(e) { // For some reason beforeinput doesn't have the same deletion problems that input has on Android
hiddenInput.value = " " //Allows delete to be detected before input is changed e.stopImmediatePropagation(); // Android ignores this and the prevent default, so this will probably be removed in the future
hiddenInput.addEventListener("input", function(e) { e.preventDefault(true); // We pass a value because we've hijacked the prevent default function to have a "should bypass" boolean value
let inputData = e.data ?? "delete"; // backspace makes null let inputData = (e.inputType == "insertLineBreak") ? "return" : (e.data == null ? "delete" : e.data.slice(-1)); // Saves the last key press.
window.lastKey = inputData if(!window.lastKey) { // When we first set hiddenInput's text contents to " " we can use this to check if Duplicate Mode is needed
hiddenInput.value = " "; // We need a character to detect deleting window.console.warn("Enabling blocking duplicate key events. Some functionality may be lost.")
window.inputFix = true;
}
if(window.keyboardFix) { if(window.keyboardFix) {
if(e.inputType == 'insertText') { 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 {
let isShift = (inputData.toLowerCase() != inputData); let isShift = (inputData.toLowerCase() != inputData);
if(isShift) { 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("shift", "keydown");
keyEvent(inputData, "keydown"); keyEvent(inputData, "keydown");
keyEvent(inputData, "keyup"); keyEvent(inputData, "keyup");
keyEvent("shift", "keyup") keyEvent("shift", "keyup");
} else { } else {
keyEvent(inputData, "keydown"); keyEvent(inputData, "keydown");
keyEvent(inputData, "keyup"); keyEvent(inputData, "keyup");
} }
} else if (e.inputType == 'deleteContentForward' || e.inputType == 'deleteContentBackward') { window.blockNextInput = true;
keyEvent("backspace", "keydown")
keyEvent("backspace", "keyup")
} }
} 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); }, false);
hiddenInput.addEventListener("keydown", function(e) { hiddenInput.addEventListener("keydown", function(e) { // Enables Compatibility Mode if we receive an invalid key press event
if(!(e.key && e.keyCode && e.which) && !window.keyboardFix) { if((e.keyCode == 229 || e.which == 229) && !window.keyboardFix) {
console.warn("Switching from keydown to input events due to invalid KeyboardEvent. Some functionality will be lost.") window.console.warn("Switching from keydown to input events due to invalid KeyboardEvent. Some functionality will be lost.")
window.keyboardFix = true; window.keyboardFix = true;
if(window.lastKey) { 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, "keydown");
keyEvent(window.lastKey, "keyup") keyEvent(window.lastKey, "keyup");
} }
} }
}, false); }, false);
hiddenInput.addEventListener("blur", function(e) { // Updates window.hiddenInputFocused to reflect the actual state of the focus
window.hiddenInputFocused = false;
});
document.body.appendChild(hiddenInput); document.body.appendChild(hiddenInput);
let keyboardButton = createTouchButton("keyboardButton", "inMenu"); let keyboardButton = createTouchButton("keyboardButton", "inMenu");
keyboardButton.classList.add("smallMobileControl"); keyboardButton.classList.add("smallMobileControl");
keyboardButton.style.cssText = "top: 0.5vh; margin: auto; left: 6vh; right:0vh;" keyboardButton.style.cssText = "top: 0.5vh; margin: auto; left: 6vh; right:0vh;"
keyboardButton.addEventListener("touchstart", function(e){e.preventDefault();hiddenInput.blur()}, false); keyboardButton.addEventListener("touchstart", function(e){
keyboardButton.addEventListener("touchend", function(e){e.preventDefault();toggleKeyboard()}, false); 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); document.body.appendChild(keyboardButton);
let placeButton = createTouchButton("placeButton", "inGame"); let placeButton = createTouchButton("placeButton", "inGame");
placeButton.style.cssText = "right:6vh;bottom:37vh;" placeButton.style.cssText = "right:6vh;bottom:37vh;"
@ -444,7 +591,7 @@ function insertCanvasElements() {
sprintButton.addEventListener("touchstart", function(e) { sprintButton.addEventListener("touchstart", function(e) {
keyEvent("r", "keydown"); keyEvent("r", "keydown");
window.sprintLock = window.sprintLock ? null : false window.sprintLock = window.sprintLock ? null : false
sprintTimer = setTimeout(function(e) { window.sprintTimer = setTimeout(function(e) {
window.sprintLock = (window.sprintLock != null); window.sprintLock = (window.sprintLock != null);
sprintButton.classList.toggle('active'); sprintButton.classList.toggle('active');
}, 1000); }, 1000);
@ -456,7 +603,7 @@ function insertCanvasElements() {
sprintButton.classList.remove('active'); sprintButton.classList.remove('active');
window.sprintLock = false window.sprintLock = false
} }
clearTimeout(sprintTimer); clearTimeout(window.sprintTimer);
}, false); }, false);
document.body.appendChild(sprintButton); document.body.appendChild(sprintButton);
let pauseButton = createTouchButton("pauseButton", "inGame"); let pauseButton = createTouchButton("pauseButton", "inGame");
@ -468,7 +615,7 @@ function insertCanvasElements() {
let chatButton = createTouchButton("chatButton", "inGame"); let chatButton = createTouchButton("chatButton", "inGame");
chatButton.classList.add("smallMobileControl"); chatButton.classList.add("smallMobileControl");
chatButton.style.cssText = "top: 0.5vh; margin: auto; left: 0vh; right: 14vh;" chatButton.style.cssText = "top: 0.5vh; margin: auto; left: 0vh; right: 14vh;"
chatButton.addEventListener("touchstart", function(e){keyEvent("t", "keydown")}, false); 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); document.body.appendChild(chatButton);
let perspectiveButton = createTouchButton("perspectiveButton", "inGame"); let perspectiveButton = createTouchButton("perspectiveButton", "inGame");
perspectiveButton.classList.add("smallMobileControl"); perspectiveButton.classList.add("smallMobileControl");
@ -510,6 +657,19 @@ function insertCanvasElements() {
// 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 :( // 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 :(
let customStyle = document.createElement("style"); let customStyle = document.createElement("style");
customStyle.textContent = ` 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 { .mobileControl {
position: absolute; position: absolute;
width: 9vh; width: 9vh;
@ -540,10 +700,6 @@ customStyle.textContent = `
height: 6vh; height: 6vh;
margin: 1vh 0vh; margin: 1vh 0vh;
} }
html, body {
height: -webkit-fill-available !important;
touch-action: pan-x pan-y;
}
.hide { .hide {
display: none; display: none;
} }

View File

@ -1,4 +1,4 @@
# <img src=".github/assets/logo.png" alt="EPM Logo" align="right" width="120px">Eagler Pocket Mobile v1.31 # <img src=".github/assets/logo.png" alt="EPM Logo" align="right" width="120px">Eagler Pocket Mobile v1.40
A revamp of FlamedDogo99's eaglercraft mobile UI using pocked edition UI.<br> A revamp of FlamedDogo99's eaglercraft mobile UI using pocked edition UI.<br>
Now currently just a reskin with different positions! Now currently just a reskin with different positions!
@ -8,10 +8,11 @@
<hr> <hr>
## Versions ## Versions
__v1.40__ - Updated to the most modern version from FlamedDogo.<br>
__v1.31__ - Added offline file (code update in the future).<br> __v1.31__ - Added offline file (code update in the future).<br>
__v1.3__ - Condensed code into one file and updated to match original.<br> __v1.30__ - Condensed code into one file and updated to match original.<br>
[eaglercraft.com](https://eaglercraft.com) is using __v1.3__ [eaglercraft.com](https://eaglercraft.com) is using __v1.31__
<hr> <hr>
@ -36,11 +37,11 @@
- [x] Crouch lock on hold - [x] Crouch lock on hold
- [x] Sprint button (sperate from double tapping forward) - [x] Sprint button (sperate from double tapping forward)
- [ ] Misc Platform support (Controller, Keyboard arrows, etc) - [ ] Misc Platform support (Controller, Keyboard arrows, etc)
- [ ] Cancel button for file upload - [x] Cancel button for file upload
- [ ] Styling for file upload - [x] Styling for file upload
- [x] Back button for Kiwi browser? - [x] Back button for Kiwi browser?
- [x] Re-orginize button layout (With new icons) - [x] Re-orginize button layout (With new icons)
- [ ] Redo the display button functions - [x] Redo the display button functions
- [x] Pocket Edition UI - [x] Pocket Edition UI
- [x] Updated code from main - [x] Updated code from main
- [x] Bug fixes from original - [x] Bug fixes from original

View File

@ -1,3 +1,6 @@
// 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() { function isMobile() {
try { try {
document.createEvent("TouchEvent"); document.createEvent("TouchEvent");
@ -9,39 +12,55 @@ function isMobile() {
if(!isMobile()) { if(!isMobile()) {
alert("WARNING: This script was created for mobile, and may break functionality in non-mobile browsers!"); alert("WARNING: This script was created for mobile, and may break functionality in non-mobile browsers!");
} }
window.keyboardEnabled = false; // TODO: consolidate all of these into a single object?
window.crouchLock = false; window.crouchLock = false; // Used for crouch mobile control
window.sprintLock = false; window.sprintLock = false; // Used for sprint mobile control
window.keyboardFix = false; // temporarily set to true until I can figure out whats going wrong with the event listener in charge of switching it window.keyboardFix = false; // keyboardFix ? "Standard Keyboard" : "Compatibility Mode"
// Used for changing touchmove events to mousemove events window.inputFix = false; // If true, Duplicate Mode
var previousTouchX = null; window.blockNextInput = false; // Used for Duplicate Mode
var previousTouchY = null; window.hiddenInputFocused = false; // Used for keyboard display on mobile
var startTouchX = null; window.canvasTouchMode = 0; // Used for canvas touch handling
// better charCodeAt function /*
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() { 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, "`": 192, "[": 219, "\\": 220, "]": 221, "\"": 222}; 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]; return keyCodeList[this];
} }
// Ignores keydown events that don't have the isValid parameter set to true // 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; const _addEventListener = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, "addEventListener", { Object.defineProperty(EventTarget.prototype, "addEventListener", {
value: function (type, fn, ...rest) { value: function (type, fn, ...rest) {
if(type == 'keydown') { if(type == 'keydown') { // Check if a keydown event is being added
_addEventListener.call(this, type, function(...args) { _addEventListener.call(this, type, function(...args) {
if(!args[0].isValid && window.keyboardFix) { if(args[0].isTrusted && window.keyboardFix) { // When we are in compatibility mode, we ignore all trusted keyboard events
return; return;
} }
return fn.apply(this, args); return fn.apply(this, args); // Appends the rest of the function specified by addEventListener
}, ...rest); }, ...rest);
} else { } else { // If it's not a keydown event, behave like normal (hopefully)
_addEventListener.call(this, type, fn, ...rest); _addEventListener.call(this, type, fn, ...rest);
} }
} }
}); });
// Allows typing in #hiddenInput // Overrides preventDefault, because on some (Android) devices you couldn't type into hiddenInput
const _preventDefault = Event.prototype.preventDefault; const _preventDefault = Event.prototype.preventDefault;
Event.prototype.preventDefault = function() { Event.prototype.preventDefault = function(shouldBypass) {
if(document.activeElement.id != "hiddenInput") { if(document.activeElement.id != "hiddenInput" || shouldBypass) { // activeElement is what element is currently focused
this._preventDefault = _preventDefault; this._preventDefault = _preventDefault;
this._preventDefault(); this._preventDefault();
} }
@ -54,18 +73,24 @@ Event.prototype.preventDefault = function() {
function keyEvent(name, state) { function keyEvent(name, state) {
const charCode = name.toKeyCode(); const charCode = name.toKeyCode();
let evt = new KeyboardEvent(state, { let evt = new KeyboardEvent(state, {
key: name, "key": name,
keyCode: charCode, "keyCode": charCode,
which: charCode "which": charCode
}); });
evt.isValid = true; // Disables fix for bad keyboard input
window.dispatchEvent(evt); window.dispatchEvent(evt);
} }
function mouseEvent(number, state, canvas) { function mouseEvent(number, state, element, event = {"clientX": 0, "clientY" : 0, "screenX": 0, "screenY": 0}) {
canvas.dispatchEvent(new PointerEvent(state, {"button": number})) element.dispatchEvent(new PointerEvent(state, {
"button": number,
"buttons": number,
"clientX": event.clientX,
"clientY" : event.clientY,
"screenX": event.screenX,
"screenY": event.screenY
}));
} }
function wheelEvent(canvas, delta) { function wheelEvent(element, delta) {
canvas.dispatchEvent(new WheelEvent("wheel", { element.dispatchEvent(new WheelEvent("wheel", {
"wheelDeltaY": delta "wheelDeltaY": delta
})); }));
} }
@ -135,8 +160,8 @@ const _createElement = document.createElement;
document.createElement = function(type, ignore) { document.createElement = function(type, ignore) {
this._createElement = _createElement; this._createElement = _createElement;
var element = this._createElement(type); var element = this._createElement(type);
if(type == "input" && !ignore) { 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)); document.querySelectorAll('#fileUpload').forEach(e => e.parentNode.removeChild(e)); // Get rid of any left over fileUpload inputs
element.id = "fileUpload"; element.id = "fileUpload";
element.addEventListener('change', function(e) { element.addEventListener('change', function(e) {
element.hidden = true; element.hidden = true;
@ -171,7 +196,7 @@ inMenuStyle.textContent = `
document.documentElement.appendChild(inMenuStyle); 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 // 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) { function waitForElm(selector) {
return new Promise(resolve => { return new Promise(resolve => {
if (document.querySelector(selector)) { if (document.querySelector(selector)) {
@ -195,48 +220,109 @@ function createTouchButton(buttonClass, buttonDisplay, elementName) {
touchButton.classList.add(buttonDisplay); touchButton.classList.add(buttonDisplay);
touchButton.classList.add("mobileControl"); touchButton.classList.add("mobileControl");
touchButton.addEventListener("touchmove", function(e){e.preventDefault()}, false); touchButton.addEventListener("touchmove", function(e){e.preventDefault()}, false);
touchButton.addEventListener("contextmenu", function(e){e.preventDefault()});
return touchButton; return touchButton;
} }
function toggleKeyboard() {
const keyboardInput = document.getElementById('hiddenInput');
if (window.keyboardEnabled) {
window.keyboardEnabled = false;
keyboardInput.blur();
} else {
window.keyboardEnabled = true;
keyboardInput.select();
}
}
waitForElm('canvas').then(() => {insertCanvasElements()}); waitForElm('canvas').then(() => {insertCanvasElements()});
function insertCanvasElements() { function insertCanvasElements() {
// Translates touchmove events to mousemove events when inGame, and touchmove events to wheele events when inMenu // Translates touchmove events to mousemove events when inGame, and touchmove events to wheele events when inMenu
var canvas = document.querySelector('canvas'); var canvas = document.querySelector('canvas');
canvas.addEventListener("touchmove", function(e) { canvas.addEventListener("touchstart", function(e) {
e.preventDefault(); if(window.canvasTouchMode < 2) { // If a touch is initiated but not assigned
const touch = e.targetTouches[0]; // We can get away with this because every other touch event will be on different elements 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
if (!previousTouchX) { window.touchTimer = setTimeout(function(e) {
previousTouchX = touch.pageX; // If our touch is still set to initiaited, set it to secondary touch
previousTouchY = touch.pageY; 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
}
} }
e.movementX = touch.pageX - previousTouchX;
e.movementY = touch.pageY - previousTouchY;
var evt = window.fakelock ? new MouseEvent("mousemove", {movementX: e.movementX, movementY: e.movementY}) : new WheelEvent("wheel", {"wheelDeltaY": e.movementY});
canvas.dispatchEvent(evt);
previousTouchX = touch.pageX;
previousTouchY = touch.pageY;
}, false); }, false);
canvas.addEventListener("touchend", function(e) { canvas.addEventListener("touchmove", function(e) {
previousTouchX = null; e.preventDefault() // Prevents window zoom when using two fingers
previousTouchY = null; var primaryTouch;
}, false) for (let touchIndex = 0; touchIndex < e.targetTouches.length; touchIndex++) { // Iterate through our touches to find a touch event matching the primary touch ID
//Updates button visibility on load if(e.targetTouches[touchIndex].identifier == window.canvasPrimaryID) {
setButtonVisibility(window.fakelock != null); primaryTouch = e.targetTouches[touchIndex];
// Adds all of the touch screen controls break;
// Theres probably a better way to do this but this works for now }
}
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) {
let 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
let strafeRightButton = createTouchButton("strafeRightButton", "inGame", "div"); let strafeRightButton = createTouchButton("strafeRightButton", "inGame", "div");
strafeRightButton.classList.add("strafeSize"); strafeRightButton.classList.add("strafeSize");
strafeRightButton.style.cssText = "left:24vh;bottom:22vh;" strafeRightButton.style.cssText = "left:24vh;bottom:22vh;"
@ -246,7 +332,7 @@ function insertCanvasElements() {
strafeLeftButton.style.cssText = "left:5.5vh;bottom:22vh;" strafeLeftButton.style.cssText = "left:5.5vh;bottom:22vh;"
document.body.appendChild(strafeLeftButton); document.body.appendChild(strafeLeftButton);
let forwardButton = createTouchButton("forwardButton", "inGame", "div"); let 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.style.cssText = "left:14vh;bottom:22vh;"
forwardButton.addEventListener("touchstart", function(e){ forwardButton.addEventListener("touchstart", function(e){
keyEvent("w", "keydown"); keyEvent("w", "keydown");
@ -256,12 +342,12 @@ function insertCanvasElements() {
}, false); }, false);
forwardButton.addEventListener("touchmove", function(e) { forwardButton.addEventListener("touchmove", function(e) {
e.preventDefault(); e.preventDefault();
const touch = e.targetTouches[0]; // We can get away with this because every other touch event will be on different elements const touch = e.targetTouches[0]; // We are just hoping that the user will only ever use one finger on the forward button
if (!startTouchX) { if (!buttonTouchStartX) { // TODO: move this to a touchstart event handler
startTouchX = touch.pageX; buttonTouchStartX = touch.pageX;
} }
let movementX = touch.pageX - startTouchX; let movementX = touch.pageX - buttonTouchStartX;
if((movementX * 10) > window.innerHeight) { if((movementX * 10) > window.innerHeight) {
strafeRightButton.classList.add("active"); strafeRightButton.classList.add("active");
strafeLeftButton.classList.remove("active"); strafeLeftButton.classList.remove("active");
@ -280,7 +366,7 @@ function insertCanvasElements() {
} }
}, false); }, false);
forwardButton.addEventListener("touchend", function(e) { forwardButton.addEventListener("touchend", function(e) {
keyEvent("w", "keyup"); 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("d", "keyup");
keyEvent("a", "keyup"); keyEvent("a", "keyup");
strafeRightButton.classList.remove("active"); strafeRightButton.classList.remove("active");
@ -289,7 +375,7 @@ function insertCanvasElements() {
strafeLeftButton.classList.add("hide"); strafeLeftButton.classList.add("hide");
forwardButton.classList.remove("active"); forwardButton.classList.remove("active");
startTouchX = null; buttonTouchStartX = null;
}, false); }, false);
strafeRightButton.classList.add("hide"); strafeRightButton.classList.add("hide");
strafeLeftButton.classList.add("hide"); strafeLeftButton.classList.add("hide");
@ -322,7 +408,7 @@ function insertCanvasElements() {
crouchButton.addEventListener("touchstart", function(e){ crouchButton.addEventListener("touchstart", function(e){
keyEvent("shift", "keydown") keyEvent("shift", "keydown")
window.crouchLock = window.crouchLock ? null : false window.crouchLock = window.crouchLock ? null : false
crouchTimer = setTimeout(function(e) { window.crouchTimer = setTimeout(function(e) { // Allows us to lock the button after a long press
window.crouchLock = (window.crouchLock != null); window.crouchLock = (window.crouchLock != null);
crouchButton.classList.toggle('active'); crouchButton.classList.toggle('active');
}, 1000); }, 1000);
@ -334,14 +420,20 @@ function insertCanvasElements() {
crouchButton.classList.remove('active'); crouchButton.classList.remove('active');
window.crouchLock = false window.crouchLock = false
} }
clearTimeout(crouchTimer); clearTimeout(window.crouchTimer);
}, false); }, false);
document.body.appendChild(crouchButton); document.body.appendChild(crouchButton);
let inventoryButton = createTouchButton("inventoryButton", "inGame"); let inventoryButton = createTouchButton("inventoryButton", "inGame");
inventoryButton.classList.add("smallMobileControl"); inventoryButton.classList.add("smallMobileControl");
inventoryButton.style.cssText = "right:19.5vh;bottom:0vh;" inventoryButton.style.cssText = "right:19.5vh;bottom:0vh;"
inventoryButton.addEventListener("touchstart", function(e){keyEvent("e", "keydown")}, false); inventoryButton.addEventListener("touchstart", function(e) {
inventoryButton.addEventListener("touchend", function(e){keyEvent("e", "keyup")}, false); 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); document.body.appendChild(inventoryButton);
let exitButton = createTouchButton("exitButton", "inMenu"); let exitButton = createTouchButton("exitButton", "inMenu");
exitButton.classList.add("smallMobileControl"); exitButton.classList.add("smallMobileControl");
@ -349,51 +441,106 @@ function insertCanvasElements() {
exitButton.addEventListener("touchstart", function(e){keyEvent("`", "keydown")}, false); exitButton.addEventListener("touchstart", function(e){keyEvent("`", "keydown")}, false);
exitButton.addEventListener("touchend", function(e){keyEvent("`", "keyup")}, false); exitButton.addEventListener("touchend", function(e){keyEvent("`", "keyup")}, false);
document.body.appendChild(exitButton); document.body.appendChild(exitButton);
// input for keyboard button // ---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---
let hiddenInput = document.createElement('input', true); let hiddenInput = document.createElement('input', true);
hiddenInput.id = "hiddenInput" hiddenInput.id = "hiddenInput"
hiddenInput.classList.add("inMenu") hiddenInput.classList.add("inMenu")
// We are hiding the text input behind button because opacity was causing problems. 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.style.cssText = "position:absolute;top: 0vh; margin: auto; left: 8vh; right:0vh;font-size:20px;z-index:-10;color: transparent;text-shadow: 0 0 0 black;"; hiddenInput.addEventListener("beforeinput", function(e) { // For some reason beforeinput doesn't have the same deletion problems that input has on Android
hiddenInput.value = " " //Allows delete to be detected before input is changed e.stopImmediatePropagation(); // Android ignores this and the prevent default, so this will probably be removed in the future
hiddenInput.addEventListener("input", function(e) { e.preventDefault(true); // We pass a value because we've hijacked the prevent default function to have a "should bypass" boolean value
let inputData = e.data ?? "delete"; // backspace makes null let inputData = (e.inputType == "insertLineBreak") ? "return" : (e.data == null ? "delete" : e.data.slice(-1)); // Saves the last key press.
window.lastKey = inputData if(!window.lastKey) { // When we first set hiddenInput's text contents to " " we can use this to check if Duplicate Mode is needed
hiddenInput.value = " "; // We need a character to detect deleting window.console.warn("Enabling blocking duplicate key events. Some functionality may be lost.")
window.inputFix = true;
}
if(window.keyboardFix) { if(window.keyboardFix) {
if(e.inputType == 'insertText') { 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 {
let isShift = (inputData.toLowerCase() != inputData); let isShift = (inputData.toLowerCase() != inputData);
if(isShift) { 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("shift", "keydown");
keyEvent(inputData, "keydown"); keyEvent(inputData, "keydown");
keyEvent(inputData, "keyup"); keyEvent(inputData, "keyup");
keyEvent("shift", "keyup") keyEvent("shift", "keyup");
} else { } else {
keyEvent(inputData, "keydown"); keyEvent(inputData, "keydown");
keyEvent(inputData, "keyup"); keyEvent(inputData, "keyup");
} }
} else if (e.inputType == 'deleteContentForward' || e.inputType == 'deleteContentBackward') { window.blockNextInput = true;
keyEvent("backspace", "keydown")
keyEvent("backspace", "keyup")
} }
} 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); }, false);
hiddenInput.addEventListener("keydown", function(e) { hiddenInput.addEventListener("keydown", function(e) { // Enables Compatibility Mode if we receive an invalid key press event
if(!(e.key && e.keyCode && e.which) && !window.keyboardFix) { if((e.keyCode == 229 || e.which == 229) && !window.keyboardFix) {
console.warn("Switching from keydown to input events due to invalid KeyboardEvent. Some functionality will be lost.") window.console.warn("Switching from keydown to input events due to invalid KeyboardEvent. Some functionality will be lost.")
window.keyboardFix = true; window.keyboardFix = true;
if(window.lastKey) { 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, "keydown");
keyEvent(window.lastKey, "keyup") keyEvent(window.lastKey, "keyup");
} }
} }
}, false); }, false);
hiddenInput.addEventListener("blur", function(e) { // Updates window.hiddenInputFocused to reflect the actual state of the focus
window.hiddenInputFocused = false;
});
document.body.appendChild(hiddenInput); document.body.appendChild(hiddenInput);
let keyboardButton = createTouchButton("keyboardButton", "inMenu"); let keyboardButton = createTouchButton("keyboardButton", "inMenu");
keyboardButton.classList.add("smallMobileControl"); keyboardButton.classList.add("smallMobileControl");
keyboardButton.style.cssText = "top: 0.5vh; margin: auto; left: 6vh; right:0vh;" keyboardButton.style.cssText = "top: 0.5vh; margin: auto; left: 6vh; right:0vh;"
keyboardButton.addEventListener("touchstart", function(e){e.preventDefault();hiddenInput.blur()}, false); keyboardButton.addEventListener("touchstart", function(e){
keyboardButton.addEventListener("touchend", function(e){e.preventDefault();toggleKeyboard()}, false); 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); document.body.appendChild(keyboardButton);
let placeButton = createTouchButton("placeButton", "inGame"); let placeButton = createTouchButton("placeButton", "inGame");
placeButton.style.cssText = "right:6vh;bottom:37vh;" placeButton.style.cssText = "right:6vh;bottom:37vh;"
@ -431,7 +578,7 @@ function insertCanvasElements() {
sprintButton.addEventListener("touchstart", function(e) { sprintButton.addEventListener("touchstart", function(e) {
keyEvent("r", "keydown"); keyEvent("r", "keydown");
window.sprintLock = window.sprintLock ? null : false window.sprintLock = window.sprintLock ? null : false
sprintTimer = setTimeout(function(e) { window.sprintTimer = setTimeout(function(e) {
window.sprintLock = (window.sprintLock != null); window.sprintLock = (window.sprintLock != null);
sprintButton.classList.toggle('active'); sprintButton.classList.toggle('active');
}, 1000); }, 1000);
@ -443,7 +590,7 @@ function insertCanvasElements() {
sprintButton.classList.remove('active'); sprintButton.classList.remove('active');
window.sprintLock = false window.sprintLock = false
} }
clearTimeout(sprintTimer); clearTimeout(window.sprintTimer);
}, false); }, false);
document.body.appendChild(sprintButton); document.body.appendChild(sprintButton);
let pauseButton = createTouchButton("pauseButton", "inGame"); let pauseButton = createTouchButton("pauseButton", "inGame");
@ -455,7 +602,7 @@ function insertCanvasElements() {
let chatButton = createTouchButton("chatButton", "inGame"); let chatButton = createTouchButton("chatButton", "inGame");
chatButton.classList.add("smallMobileControl"); chatButton.classList.add("smallMobileControl");
chatButton.style.cssText = "top: 0.5vh; margin: auto; left: 0vh; right: 14vh;" chatButton.style.cssText = "top: 0.5vh; margin: auto; left: 0vh; right: 14vh;"
chatButton.addEventListener("touchstart", function(e){keyEvent("t", "keydown")}, false); 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); document.body.appendChild(chatButton);
let perspectiveButton = createTouchButton("perspectiveButton", "inGame"); let perspectiveButton = createTouchButton("perspectiveButton", "inGame");
perspectiveButton.classList.add("smallMobileControl"); perspectiveButton.classList.add("smallMobileControl");
@ -497,6 +644,19 @@ function insertCanvasElements() {
// 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 :( // 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 :(
let customStyle = document.createElement("style"); let customStyle = document.createElement("style");
customStyle.textContent = ` 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 { .mobileControl {
position: absolute; position: absolute;
width: 9vh; width: 9vh;
@ -527,10 +687,6 @@ customStyle.textContent = `
height: 6vh; height: 6vh;
margin: 1vh 0vh; margin: 1vh 0vh;
} }
html, body {
height: -webkit-fill-available !important;
touch-action: pan-x pan-y;
}
.hide { .hide {
display: none; display: none;
} }

View File

@ -1,3 +1,6 @@
// 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() { function isMobile() {
try { try {
document.createEvent("TouchEvent"); document.createEvent("TouchEvent");
@ -9,39 +12,55 @@ function isMobile() {
if(!isMobile()) { if(!isMobile()) {
alert("WARNING: This script was created for mobile, and may break functionality in non-mobile browsers!"); alert("WARNING: This script was created for mobile, and may break functionality in non-mobile browsers!");
} }
window.keyboardEnabled = false; // TODO: consolidate all of these into a single object?
window.crouchLock = false; window.crouchLock = false; // Used for crouch mobile control
window.sprintLock = false; window.sprintLock = false; // Used for sprint mobile control
window.keyboardFix = false; // temporarily set to true until I can figure out whats going wrong with the event listener in charge of switching it window.keyboardFix = false; // keyboardFix ? "Standard Keyboard" : "Compatibility Mode"
// Used for changing touchmove events to mousemove events window.inputFix = false; // If true, Duplicate Mode
var previousTouchX = null; window.blockNextInput = false; // Used for Duplicate Mode
var previousTouchY = null; window.hiddenInputFocused = false; // Used for keyboard display on mobile
var startTouchX = null; window.canvasTouchMode = 0; // Used for canvas touch handling
// better charCodeAt function /*
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() { 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, "`": 192, "[": 219, "\\": 220, "]": 221, "\"": 222}; 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]; return keyCodeList[this];
} }
// Ignores keydown events that don't have the isValid parameter set to true // 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; const _addEventListener = EventTarget.prototype.addEventListener;
Object.defineProperty(EventTarget.prototype, "addEventListener", { Object.defineProperty(EventTarget.prototype, "addEventListener", {
value: function (type, fn, ...rest) { value: function (type, fn, ...rest) {
if(type == 'keydown') { if(type == 'keydown') { // Check if a keydown event is being added
_addEventListener.call(this, type, function(...args) { _addEventListener.call(this, type, function(...args) {
if(!args[0].isValid && window.keyboardFix) { if(args[0].isTrusted && window.keyboardFix) { // When we are in compatibility mode, we ignore all trusted keyboard events
return; return;
} }
return fn.apply(this, args); return fn.apply(this, args); // Appends the rest of the function specified by addEventListener
}, ...rest); }, ...rest);
} else { } else { // If it's not a keydown event, behave like normal (hopefully)
_addEventListener.call(this, type, fn, ...rest); _addEventListener.call(this, type, fn, ...rest);
} }
} }
}); });
// Allows typing in #hiddenInput // Overrides preventDefault, because on some (Android) devices you couldn't type into hiddenInput
const _preventDefault = Event.prototype.preventDefault; const _preventDefault = Event.prototype.preventDefault;
Event.prototype.preventDefault = function() { Event.prototype.preventDefault = function(shouldBypass) {
if(document.activeElement.id != "hiddenInput") { if(document.activeElement.id != "hiddenInput" || shouldBypass) { // activeElement is what element is currently focused
this._preventDefault = _preventDefault; this._preventDefault = _preventDefault;
this._preventDefault(); this._preventDefault();
} }
@ -54,18 +73,24 @@ Event.prototype.preventDefault = function() {
function keyEvent(name, state) { function keyEvent(name, state) {
const charCode = name.toKeyCode(); const charCode = name.toKeyCode();
let evt = new KeyboardEvent(state, { let evt = new KeyboardEvent(state, {
key: name, "key": name,
keyCode: charCode, "keyCode": charCode,
which: charCode "which": charCode
}); });
evt.isValid = true; // Disables fix for bad keyboard input
window.dispatchEvent(evt); window.dispatchEvent(evt);
} }
function mouseEvent(number, state, canvas) { function mouseEvent(number, state, element, event = {"clientX": 0, "clientY" : 0, "screenX": 0, "screenY": 0}) {
canvas.dispatchEvent(new PointerEvent(state, {"button": number})) element.dispatchEvent(new PointerEvent(state, {
"button": number,
"buttons": number,
"clientX": event.clientX,
"clientY" : event.clientY,
"screenX": event.screenX,
"screenY": event.screenY
}));
} }
function wheelEvent(canvas, delta) { function wheelEvent(element, delta) {
canvas.dispatchEvent(new WheelEvent("wheel", { element.dispatchEvent(new WheelEvent("wheel", {
"wheelDeltaY": delta "wheelDeltaY": delta
})); }));
} }
@ -135,8 +160,8 @@ const _createElement = document.createElement;
document.createElement = function(type, ignore) { document.createElement = function(type, ignore) {
this._createElement = _createElement; this._createElement = _createElement;
var element = this._createElement(type); var element = this._createElement(type);
if(type == "input" && !ignore) { 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)); document.querySelectorAll('#fileUpload').forEach(e => e.parentNode.removeChild(e)); // Get rid of any left over fileUpload inputs
element.id = "fileUpload"; element.id = "fileUpload";
element.addEventListener('change', function(e) { element.addEventListener('change', function(e) {
element.hidden = true; element.hidden = true;
@ -171,7 +196,7 @@ inMenuStyle.textContent = `
document.documentElement.appendChild(inMenuStyle); 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 // 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) { function waitForElm(selector) {
return new Promise(resolve => { return new Promise(resolve => {
if (document.querySelector(selector)) { if (document.querySelector(selector)) {
@ -195,48 +220,109 @@ function createTouchButton(buttonClass, buttonDisplay, elementName) {
touchButton.classList.add(buttonDisplay); touchButton.classList.add(buttonDisplay);
touchButton.classList.add("mobileControl"); touchButton.classList.add("mobileControl");
touchButton.addEventListener("touchmove", function(e){e.preventDefault()}, false); touchButton.addEventListener("touchmove", function(e){e.preventDefault()}, false);
touchButton.addEventListener("contextmenu", function(e){e.preventDefault()});
return touchButton; return touchButton;
} }
function toggleKeyboard() {
const keyboardInput = document.getElementById('hiddenInput');
if (window.keyboardEnabled) {
window.keyboardEnabled = false;
keyboardInput.blur();
} else {
window.keyboardEnabled = true;
keyboardInput.select();
}
}
waitForElm('canvas').then(() => {insertCanvasElements()}); waitForElm('canvas').then(() => {insertCanvasElements()});
function insertCanvasElements() { function insertCanvasElements() {
// Translates touchmove events to mousemove events when inGame, and touchmove events to wheele events when inMenu // Translates touchmove events to mousemove events when inGame, and touchmove events to wheele events when inMenu
var canvas = document.querySelector('canvas'); var canvas = document.querySelector('canvas');
canvas.addEventListener("touchmove", function(e) { canvas.addEventListener("touchstart", function(e) {
e.preventDefault(); if(window.canvasTouchMode < 2) { // If a touch is initiated but not assigned
const touch = e.targetTouches[0]; // We can get away with this because every other touch event will be on different elements 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
if (!previousTouchX) { window.touchTimer = setTimeout(function(e) {
previousTouchX = touch.pageX; // If our touch is still set to initiaited, set it to secondary touch
previousTouchY = touch.pageY; 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
}
} }
e.movementX = touch.pageX - previousTouchX;
e.movementY = touch.pageY - previousTouchY;
var evt = window.fakelock ? new MouseEvent("mousemove", {movementX: e.movementX, movementY: e.movementY}) : new WheelEvent("wheel", {"wheelDeltaY": e.movementY});
canvas.dispatchEvent(evt);
previousTouchX = touch.pageX;
previousTouchY = touch.pageY;
}, false); }, false);
canvas.addEventListener("touchend", function(e) { canvas.addEventListener("touchmove", function(e) {
previousTouchX = null; e.preventDefault() // Prevents window zoom when using two fingers
previousTouchY = null; var primaryTouch;
}, false) for (let touchIndex = 0; touchIndex < e.targetTouches.length; touchIndex++) { // Iterate through our touches to find a touch event matching the primary touch ID
//Updates button visibility on load if(e.targetTouches[touchIndex].identifier == window.canvasPrimaryID) {
setButtonVisibility(window.fakelock != null); primaryTouch = e.targetTouches[touchIndex];
// Adds all of the touch screen controls break;
// Theres probably a better way to do this but this works for now }
}
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) {
let 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
let strafeRightButton = createTouchButton("strafeRightButton", "inGame", "div"); let strafeRightButton = createTouchButton("strafeRightButton", "inGame", "div");
strafeRightButton.classList.add("strafeSize"); strafeRightButton.classList.add("strafeSize");
strafeRightButton.style.cssText = "left:24vh;bottom:22vh;" strafeRightButton.style.cssText = "left:24vh;bottom:22vh;"
@ -246,7 +332,7 @@ function insertCanvasElements() {
strafeLeftButton.style.cssText = "left:5.5vh;bottom:22vh;" strafeLeftButton.style.cssText = "left:5.5vh;bottom:22vh;"
document.body.appendChild(strafeLeftButton); document.body.appendChild(strafeLeftButton);
let forwardButton = createTouchButton("forwardButton", "inGame", "div"); let 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.style.cssText = "left:14vh;bottom:22vh;"
forwardButton.addEventListener("touchstart", function(e){ forwardButton.addEventListener("touchstart", function(e){
keyEvent("w", "keydown"); keyEvent("w", "keydown");
@ -256,12 +342,12 @@ function insertCanvasElements() {
}, false); }, false);
forwardButton.addEventListener("touchmove", function(e) { forwardButton.addEventListener("touchmove", function(e) {
e.preventDefault(); e.preventDefault();
const touch = e.targetTouches[0]; // We can get away with this because every other touch event will be on different elements const touch = e.targetTouches[0]; // We are just hoping that the user will only ever use one finger on the forward button
if (!startTouchX) { if (!buttonTouchStartX) { // TODO: move this to a touchstart event handler
startTouchX = touch.pageX; buttonTouchStartX = touch.pageX;
} }
let movementX = touch.pageX - startTouchX; let movementX = touch.pageX - buttonTouchStartX;
if((movementX * 10) > window.innerHeight) { if((movementX * 10) > window.innerHeight) {
strafeRightButton.classList.add("active"); strafeRightButton.classList.add("active");
strafeLeftButton.classList.remove("active"); strafeLeftButton.classList.remove("active");
@ -280,7 +366,7 @@ function insertCanvasElements() {
} }
}, false); }, false);
forwardButton.addEventListener("touchend", function(e) { forwardButton.addEventListener("touchend", function(e) {
keyEvent("w", "keyup"); 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("d", "keyup");
keyEvent("a", "keyup"); keyEvent("a", "keyup");
strafeRightButton.classList.remove("active"); strafeRightButton.classList.remove("active");
@ -289,7 +375,7 @@ function insertCanvasElements() {
strafeLeftButton.classList.add("hide"); strafeLeftButton.classList.add("hide");
forwardButton.classList.remove("active"); forwardButton.classList.remove("active");
startTouchX = null; buttonTouchStartX = null;
}, false); }, false);
strafeRightButton.classList.add("hide"); strafeRightButton.classList.add("hide");
strafeLeftButton.classList.add("hide"); strafeLeftButton.classList.add("hide");
@ -322,7 +408,7 @@ function insertCanvasElements() {
crouchButton.addEventListener("touchstart", function(e){ crouchButton.addEventListener("touchstart", function(e){
keyEvent("shift", "keydown") keyEvent("shift", "keydown")
window.crouchLock = window.crouchLock ? null : false window.crouchLock = window.crouchLock ? null : false
crouchTimer = setTimeout(function(e) { window.crouchTimer = setTimeout(function(e) { // Allows us to lock the button after a long press
window.crouchLock = (window.crouchLock != null); window.crouchLock = (window.crouchLock != null);
crouchButton.classList.toggle('active'); crouchButton.classList.toggle('active');
}, 1000); }, 1000);
@ -334,14 +420,20 @@ function insertCanvasElements() {
crouchButton.classList.remove('active'); crouchButton.classList.remove('active');
window.crouchLock = false window.crouchLock = false
} }
clearTimeout(crouchTimer); clearTimeout(window.crouchTimer);
}, false); }, false);
document.body.appendChild(crouchButton); document.body.appendChild(crouchButton);
let inventoryButton = createTouchButton("inventoryButton", "inGame"); let inventoryButton = createTouchButton("inventoryButton", "inGame");
inventoryButton.classList.add("smallMobileControl"); inventoryButton.classList.add("smallMobileControl");
inventoryButton.style.cssText = "right:19.5vh;bottom:0vh;" inventoryButton.style.cssText = "right:19.5vh;bottom:0vh;"
inventoryButton.addEventListener("touchstart", function(e){keyEvent("e", "keydown")}, false); inventoryButton.addEventListener("touchstart", function(e) {
inventoryButton.addEventListener("touchend", function(e){keyEvent("e", "keyup")}, false); 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); document.body.appendChild(inventoryButton);
let exitButton = createTouchButton("exitButton", "inMenu"); let exitButton = createTouchButton("exitButton", "inMenu");
exitButton.classList.add("smallMobileControl"); exitButton.classList.add("smallMobileControl");
@ -349,51 +441,106 @@ function insertCanvasElements() {
exitButton.addEventListener("touchstart", function(e){keyEvent("`", "keydown")}, false); exitButton.addEventListener("touchstart", function(e){keyEvent("`", "keydown")}, false);
exitButton.addEventListener("touchend", function(e){keyEvent("`", "keyup")}, false); exitButton.addEventListener("touchend", function(e){keyEvent("`", "keyup")}, false);
document.body.appendChild(exitButton); document.body.appendChild(exitButton);
// input for keyboard button // ---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---
let hiddenInput = document.createElement('input', true); let hiddenInput = document.createElement('input', true);
hiddenInput.id = "hiddenInput" hiddenInput.id = "hiddenInput"
hiddenInput.classList.add("inMenu") hiddenInput.classList.add("inMenu")
// We are hiding the text input behind button because opacity was causing problems. 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.style.cssText = "position:absolute;top: 0vh; margin: auto; left: 8vh; right:0vh;font-size:20px;z-index:-10;color: transparent;text-shadow: 0 0 0 black;"; hiddenInput.addEventListener("beforeinput", function(e) { // For some reason beforeinput doesn't have the same deletion problems that input has on Android
hiddenInput.value = " " //Allows delete to be detected before input is changed e.stopImmediatePropagation(); // Android ignores this and the prevent default, so this will probably be removed in the future
hiddenInput.addEventListener("input", function(e) { e.preventDefault(true); // We pass a value because we've hijacked the prevent default function to have a "should bypass" boolean value
let inputData = e.data ?? "delete"; // backspace makes null let inputData = (e.inputType == "insertLineBreak") ? "return" : (e.data == null ? "delete" : e.data.slice(-1)); // Saves the last key press.
window.lastKey = inputData if(!window.lastKey) { // When we first set hiddenInput's text contents to " " we can use this to check if Duplicate Mode is needed
hiddenInput.value = " "; // We need a character to detect deleting window.console.warn("Enabling blocking duplicate key events. Some functionality may be lost.")
window.inputFix = true;
}
if(window.keyboardFix) { if(window.keyboardFix) {
if(e.inputType == 'insertText') { 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 {
let isShift = (inputData.toLowerCase() != inputData); let isShift = (inputData.toLowerCase() != inputData);
if(isShift) { 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("shift", "keydown");
keyEvent(inputData, "keydown"); keyEvent(inputData, "keydown");
keyEvent(inputData, "keyup"); keyEvent(inputData, "keyup");
keyEvent("shift", "keyup") keyEvent("shift", "keyup");
} else { } else {
keyEvent(inputData, "keydown"); keyEvent(inputData, "keydown");
keyEvent(inputData, "keyup"); keyEvent(inputData, "keyup");
} }
} else if (e.inputType == 'deleteContentForward' || e.inputType == 'deleteContentBackward') { window.blockNextInput = true;
keyEvent("backspace", "keydown")
keyEvent("backspace", "keyup")
} }
} 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); }, false);
hiddenInput.addEventListener("keydown", function(e) { hiddenInput.addEventListener("keydown", function(e) { // Enables Compatibility Mode if we receive an invalid key press event
if(!(e.key && e.keyCode && e.which) && !window.keyboardFix) { if((e.keyCode == 229 || e.which == 229) && !window.keyboardFix) {
console.warn("Switching from keydown to input events due to invalid KeyboardEvent. Some functionality will be lost.") window.console.warn("Switching from keydown to input events due to invalid KeyboardEvent. Some functionality will be lost.")
window.keyboardFix = true; window.keyboardFix = true;
if(window.lastKey) { 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, "keydown");
keyEvent(window.lastKey, "keyup") keyEvent(window.lastKey, "keyup");
} }
} }
}, false); }, false);
hiddenInput.addEventListener("blur", function(e) { // Updates window.hiddenInputFocused to reflect the actual state of the focus
window.hiddenInputFocused = false;
});
document.body.appendChild(hiddenInput); document.body.appendChild(hiddenInput);
let keyboardButton = createTouchButton("keyboardButton", "inMenu"); let keyboardButton = createTouchButton("keyboardButton", "inMenu");
keyboardButton.classList.add("smallMobileControl"); keyboardButton.classList.add("smallMobileControl");
keyboardButton.style.cssText = "top: 0.5vh; margin: auto; left: 6vh; right:0vh;" keyboardButton.style.cssText = "top: 0.5vh; margin: auto; left: 6vh; right:0vh;"
keyboardButton.addEventListener("touchstart", function(e){e.preventDefault();hiddenInput.blur()}, false); keyboardButton.addEventListener("touchstart", function(e){
keyboardButton.addEventListener("touchend", function(e){e.preventDefault();toggleKeyboard()}, false); 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); document.body.appendChild(keyboardButton);
let placeButton = createTouchButton("placeButton", "inGame"); let placeButton = createTouchButton("placeButton", "inGame");
placeButton.style.cssText = "right:6vh;bottom:37vh;" placeButton.style.cssText = "right:6vh;bottom:37vh;"
@ -431,7 +578,7 @@ function insertCanvasElements() {
sprintButton.addEventListener("touchstart", function(e) { sprintButton.addEventListener("touchstart", function(e) {
keyEvent("r", "keydown"); keyEvent("r", "keydown");
window.sprintLock = window.sprintLock ? null : false window.sprintLock = window.sprintLock ? null : false
sprintTimer = setTimeout(function(e) { window.sprintTimer = setTimeout(function(e) {
window.sprintLock = (window.sprintLock != null); window.sprintLock = (window.sprintLock != null);
sprintButton.classList.toggle('active'); sprintButton.classList.toggle('active');
}, 1000); }, 1000);
@ -443,7 +590,7 @@ function insertCanvasElements() {
sprintButton.classList.remove('active'); sprintButton.classList.remove('active');
window.sprintLock = false window.sprintLock = false
} }
clearTimeout(sprintTimer); clearTimeout(window.sprintTimer);
}, false); }, false);
document.body.appendChild(sprintButton); document.body.appendChild(sprintButton);
let pauseButton = createTouchButton("pauseButton", "inGame"); let pauseButton = createTouchButton("pauseButton", "inGame");
@ -455,7 +602,7 @@ function insertCanvasElements() {
let chatButton = createTouchButton("chatButton", "inGame"); let chatButton = createTouchButton("chatButton", "inGame");
chatButton.classList.add("smallMobileControl"); chatButton.classList.add("smallMobileControl");
chatButton.style.cssText = "top: 0.5vh; margin: auto; left: 0vh; right: 14vh;" chatButton.style.cssText = "top: 0.5vh; margin: auto; left: 0vh; right: 14vh;"
chatButton.addEventListener("touchstart", function(e){keyEvent("t", "keydown")}, false); 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); document.body.appendChild(chatButton);
let perspectiveButton = createTouchButton("perspectiveButton", "inGame"); let perspectiveButton = createTouchButton("perspectiveButton", "inGame");
perspectiveButton.classList.add("smallMobileControl"); perspectiveButton.classList.add("smallMobileControl");
@ -497,6 +644,19 @@ function insertCanvasElements() {
// 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 :( // 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 :(
let customStyle = document.createElement("style"); let customStyle = document.createElement("style");
customStyle.textContent = ` 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 { .mobileControl {
position: absolute; position: absolute;
width: 9vh; width: 9vh;
@ -527,10 +687,6 @@ customStyle.textContent = `
height: 6vh; height: 6vh;
margin: 1vh 0vh; margin: 1vh 0vh;
} }
html, body {
height: -webkit-fill-available !important;
touch-action: pan-x pan-y;
}
.hide { .hide {
display: none; display: none;
} }