554 lines
19 KiB
JavaScript
554 lines
19 KiB
JavaScript
import Minecraft from "./Minecraft.js";
|
|
import FocusStateType from "../util/FocusStateType.js";
|
|
import GuiIngameMenu from "./gui/screens/GuiIngameMenu.js";
|
|
import Keyboard from "../util/Keyboard.js";
|
|
import GuiLoadingScreen from "./gui/screens/GuiLoadingScreen.js";
|
|
|
|
export default class GameWindow {
|
|
|
|
constructor(minecraft, canvasWrapperId) {
|
|
this.minecraft = minecraft;
|
|
|
|
this.width = 0;
|
|
this.height = 0;
|
|
|
|
this.mouseX = 0;
|
|
this.mouseY = 0;
|
|
|
|
this.mouseMotionX = 0;
|
|
this.mouseMotionY = 0;
|
|
|
|
this.mouseInsideWindow = false;
|
|
|
|
this.mouseDownInterval = null;
|
|
this.focusState = FocusStateType.EXITED;
|
|
this.lastIngameSwitchTime = 0;
|
|
|
|
this.mobileDevice = this.detectTouchDevice();
|
|
|
|
// Initialize canvas elements
|
|
this.initializeElements(canvasWrapperId);
|
|
|
|
// Register listeners
|
|
if (this.mobileDevice) {
|
|
this.registerMobileListeners();
|
|
} else {
|
|
this.registerDesktopListeners();
|
|
}
|
|
|
|
// Create keyboard
|
|
Keyboard.create();
|
|
}
|
|
|
|
initializeElements(canvasWrapperId) {
|
|
// Get canvas wrapper
|
|
this.wrapper = document.getElementById(canvasWrapperId);
|
|
|
|
// Remove all children of wrapper
|
|
while (this.wrapper.firstChild) {
|
|
this.wrapper.removeChild(this.wrapper.firstChild);
|
|
}
|
|
|
|
// Create render layers
|
|
this.canvasWorld = document.createElement('canvas');
|
|
this.canvasDebug = document.createElement('canvas');
|
|
this.canvasItems = document.createElement('canvas');
|
|
|
|
// Create canvas renderer
|
|
this.canvas = document.createElement('canvas');
|
|
this.wrapper.appendChild(this.canvas);
|
|
}
|
|
|
|
registerDesktopListeners() {
|
|
this.registerListener(window, 'resize', event => {
|
|
this.updateWindowSize();
|
|
});
|
|
this.registerListener(document, 'mousedown', event => {
|
|
// In-Game mouse click
|
|
this.minecraft.onMouseClicked(event.button);
|
|
|
|
// Start interval to repeat the mouse event
|
|
if (this.mouseDownInterval !== null) {
|
|
clearInterval(this.mouseDownInterval);
|
|
}
|
|
this.mouseDownInterval = setInterval(_ => this.minecraft.onMouseClicked(event.button), 250);
|
|
|
|
// Handle mouse click on screen
|
|
let currentScreen = this.minecraft.currentScreen;
|
|
if (currentScreen !== null) {
|
|
currentScreen.mouseClicked(
|
|
event.x / this.scaleFactor,
|
|
event.y / this.scaleFactor,
|
|
event.code
|
|
);
|
|
}
|
|
|
|
// Fix cursor lock state
|
|
this.requestCursorUpdate();
|
|
|
|
// Request lock on click
|
|
if (this.minecraft.currentScreen === null && this.focusState === FocusStateType.EXITED) {
|
|
this.updateFocusState(FocusStateType.REQUEST_LOCK);
|
|
}
|
|
|
|
this.initialSoundEngine();
|
|
});
|
|
this.registerListener(document, 'mousemove', event => {
|
|
this.mouseX = event.clientX / this.scaleFactor;
|
|
this.mouseY = event.clientY / this.scaleFactor;
|
|
|
|
this.mouseMotionX = event.movementX;
|
|
this.mouseMotionY = -event.movementY;
|
|
|
|
// Handle mouse move on screen
|
|
let currentScreen = this.minecraft.currentScreen;
|
|
if (currentScreen !== null) {
|
|
currentScreen.mouseDragged(event.x / this.scaleFactor, event.y / this.scaleFactor, event.code);
|
|
}
|
|
|
|
this.requestCursorUpdate();
|
|
});
|
|
this.registerListener(document, 'mouseup', event => {
|
|
// Handle mouse release on screen
|
|
let currentScreen = this.minecraft.currentScreen;
|
|
if (currentScreen !== null) {
|
|
currentScreen.mouseReleased(
|
|
event.x / this.scaleFactor,
|
|
event.y / this.scaleFactor,
|
|
event.code
|
|
);
|
|
}
|
|
|
|
if (this.mouseDownInterval !== null) {
|
|
clearInterval(this.mouseDownInterval);
|
|
}
|
|
});
|
|
this.registerListener(document, 'pointerlockchange', event => {
|
|
let intentState = this.focusState.getIntent(); // Get target state we want to switch into
|
|
let isCursorLocked = this.isCursorLockedToCanvas(); // Get current state of the canvas lock
|
|
let isLockIntent = intentState === FocusStateType.LOCKED; // Check if we want to lock the cursor
|
|
|
|
let lastSwitchDuration = Date.now() - this.lastIngameSwitchTime;
|
|
if (this.focusState === FocusStateType.LOCKED && !isCursorLocked && lastSwitchDuration < 200) {
|
|
// If the user exists the inventory by using the escape key, the cursor unlocks from the canvas,
|
|
// so we have to prevent that by switching immediately to the request state
|
|
this.focusState = FocusStateType.REQUEST_LOCK;
|
|
} else {
|
|
if (intentState === null) {
|
|
// The state changed unintentionally, so we have to choose a new state from the current canvas lock
|
|
this.updateFocusState(isCursorLocked ? FocusStateType.LOCKED : FocusStateType.EXITED);
|
|
} else if (isCursorLocked === isLockIntent) {
|
|
// Check if the canvas completed the lock operation like intended and change the state to its final state
|
|
this.updateFocusState(intentState);
|
|
}
|
|
}
|
|
});
|
|
this.registerListener(this.wrapper, 'mouseover', event => {
|
|
// Enable keyboard util handling
|
|
Keyboard.setEnabled(true);
|
|
this.mouseInsideWindow = true;
|
|
|
|
// Update cursor lock
|
|
this.requestCursorUpdate();
|
|
});
|
|
this.registerListener(this.wrapper, 'mouseleave', event => {
|
|
// Disable keyboard util handling
|
|
Keyboard.setEnabled(false);
|
|
this.mouseInsideWindow = false;
|
|
|
|
// Update cursor lock
|
|
this.requestCursorUpdate();
|
|
});
|
|
this.registerListener(document, 'mouseout', event => {
|
|
this.requestCursorUpdate();
|
|
});
|
|
this.registerListener(document, 'mouseenter', event => {
|
|
this.requestCursorUpdate();
|
|
});
|
|
this.registerListener(window, 'keydown', event => {
|
|
// Prevent browser functions except fullscreen
|
|
if (event.key !== 'F11') {
|
|
event.preventDefault();
|
|
}
|
|
|
|
// Ignore key input if mouse is not inside window
|
|
if (!this.mouseInsideWindow) {
|
|
return;
|
|
}
|
|
|
|
// Handle escape press if focus is still in requesting state
|
|
if (event.key === 'Escape' && this.minecraft.currentScreen === null) {
|
|
this.updateFocusState(FocusStateType.REQUEST_EXIT);
|
|
return;
|
|
}
|
|
|
|
let currentScreen = this.minecraft.currentScreen;
|
|
if (currentScreen === null) {
|
|
// Handle in-game key press
|
|
this.minecraft.onKeyPressed(event.code);
|
|
} else {
|
|
// Handle key type on screen
|
|
currentScreen.keyTyped(event.code, event.key);
|
|
}
|
|
|
|
this.requestCursorUpdate();
|
|
}, false);
|
|
this.registerListener(window, 'keyup', event => {
|
|
// Handle key release on screen
|
|
let currentScreen = this.minecraft.currentScreen;
|
|
if (currentScreen !== null) {
|
|
currentScreen.keyReleased(event.code);
|
|
}
|
|
});
|
|
this.registerListener(document, 'contextmenu');
|
|
this.registerListener(this.wrapper, 'wheel', event => {
|
|
event.stopPropagation();
|
|
|
|
// Handle mouse scroll
|
|
let delta = Math.sign(event.deltaY);
|
|
this.minecraft.onMouseScroll(delta);
|
|
});
|
|
}
|
|
|
|
registerMobileListeners() {
|
|
let touchStartTime = 0;
|
|
let prevTouched = false;
|
|
|
|
this.registerListener(window, 'resize', event => {
|
|
this.updateWindowSize();
|
|
});
|
|
this.registerListener(document, 'touchstart', event => {
|
|
for (let i = 0; i < event.touches.length; i++) {
|
|
let touch = event.touches[i];
|
|
let x = touch.pageX;
|
|
let y = touch.pageY;
|
|
|
|
// Handle mouse click on screen
|
|
let currentScreen = this.minecraft.currentScreen;
|
|
if (currentScreen !== null) {
|
|
currentScreen.mouseClicked(
|
|
x / this.scaleFactor,
|
|
y / this.scaleFactor,
|
|
0
|
|
);
|
|
}
|
|
|
|
let isRightHand = x > this.wrapper.offsetWidth / 2;
|
|
|
|
// Handle player movement
|
|
if (isRightHand) {
|
|
touchStartTime = Date.now();
|
|
} else {
|
|
let tileSize = this.wrapper.offsetWidth / 8;
|
|
|
|
let tileX = 0;
|
|
let tileY = this.wrapper.offsetHeight - tileSize * 3;
|
|
|
|
let relX = x - tileX;
|
|
let relY = y - tileY;
|
|
|
|
let tileIndex = Math.floor(relX / tileSize) + Math.floor(relY / tileSize) * 3;
|
|
|
|
// Walk buttons
|
|
switch (tileIndex) {
|
|
case 0:
|
|
case 1:
|
|
case 2:
|
|
Keyboard.setState("KeyW", true);
|
|
break;
|
|
case 3:
|
|
Keyboard.setState("KeyA", true);
|
|
break;
|
|
case 4:
|
|
Keyboard.setState("Space", true);
|
|
break;
|
|
case 5:
|
|
Keyboard.setState("KeyD", true);
|
|
break;
|
|
case 6:
|
|
case 7:
|
|
case 8:
|
|
Keyboard.setState("KeyS", true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}, false);
|
|
this.registerListener(document, 'touchmove', event => {
|
|
for (let i = 0; i < event.touches.length; i++) {
|
|
let touch = event.touches[i];
|
|
let x = touch.pageX;
|
|
let y = touch.pageY;
|
|
|
|
// Handle mouse move on screen
|
|
let currentScreen = this.minecraft.currentScreen;
|
|
if (currentScreen !== null) {
|
|
currentScreen.mouseDragged(
|
|
x / this.scaleFactor,
|
|
y / this.scaleFactor,
|
|
0
|
|
);
|
|
}
|
|
|
|
// Right hand
|
|
let isRightHand = x > this.wrapper.offsetWidth / 2;
|
|
|
|
// Handle player movement
|
|
if (isRightHand) {
|
|
if (prevTouched) {
|
|
this.mouseMotionX = (x - prevTouched.pageX) * 10;
|
|
this.mouseMotionY = -(y - prevTouched.pageY) * 10;
|
|
}
|
|
prevTouched = touch;
|
|
touchStartTime = Date.now();
|
|
}
|
|
}
|
|
}, false);
|
|
this.registerListener(document, 'touchend', event => {
|
|
// Break block
|
|
if (!prevTouched && touchStartTime !== 0 && (Date.now() - touchStartTime) < 1000) {
|
|
this.minecraft.onMouseClicked(2);
|
|
}
|
|
|
|
prevTouched = false;
|
|
touchStartTime = 0;
|
|
|
|
// Handle touches
|
|
for (let i = 0; i < event.changedTouches.length; i++) {
|
|
let touch = event.changedTouches[i];
|
|
let x = touch.pageX;
|
|
let y = touch.pageY;
|
|
|
|
// Handle mouse release on screen
|
|
let currentScreen = this.minecraft.currentScreen;
|
|
if (currentScreen !== null) {
|
|
currentScreen.mouseReleased(
|
|
x / this.scaleFactor,
|
|
y / this.scaleFactor,
|
|
0
|
|
);
|
|
}
|
|
|
|
// Left hand
|
|
let isLeftHand = touch.pageX < this.wrapper.offsetWidth / 2;
|
|
|
|
// Release all keys
|
|
if (isLeftHand) {
|
|
Keyboard.unPressAll();
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.initialSoundEngine();
|
|
}, false);
|
|
this.registerListener(document, 'contextmenu');
|
|
|
|
// Break block listener
|
|
setInterval(() => {
|
|
if (touchStartTime !== 0 && (Date.now() - touchStartTime) > 250) {
|
|
touchStartTime = Date.now();
|
|
this.minecraft.onMouseClicked(0);
|
|
}
|
|
}, 200);
|
|
}
|
|
|
|
updateWindowSize() {
|
|
this.updateScaleFactor();
|
|
|
|
let wrapperWidth = this.width * this.scaleFactor;
|
|
let wrapperHeight = this.height * this.scaleFactor;
|
|
|
|
let worldRenderer = this.minecraft.worldRenderer;
|
|
let itemRenderer = this.minecraft.itemRenderer;
|
|
|
|
// Update world renderer size and camera
|
|
worldRenderer.camera.aspect = this.width / this.height;
|
|
worldRenderer.camera.updateProjectionMatrix();
|
|
worldRenderer.webRenderer.setSize(wrapperWidth, wrapperHeight);
|
|
|
|
// Update item renderer size and camera
|
|
itemRenderer.camera.aspect = this.width / this.height;
|
|
itemRenderer.camera.updateProjectionMatrix();
|
|
itemRenderer.webRenderer.setSize(wrapperWidth, wrapperHeight);
|
|
|
|
// Update canvas 2d size
|
|
this.canvas.style.width = wrapperWidth + "px";
|
|
this.canvas.style.height = wrapperHeight + "px";
|
|
|
|
if (this.canvasDebug.width !== this.canvas.width || this.canvasDebug.height !== this.canvas.height) {
|
|
this.canvasDebug.width = this.canvas.width;
|
|
this.canvasDebug.height = this.canvas.height;
|
|
}
|
|
|
|
// Reinitialize gui
|
|
this.minecraft.screenRenderer.initialize();
|
|
|
|
// Reinitialize current screen
|
|
if (this.minecraft.currentScreen !== null) {
|
|
this.minecraft.currentScreen.setup(this.minecraft, this.width, this.height);
|
|
}
|
|
|
|
// Render first frame
|
|
if (this.minecraft.isInGame()) {
|
|
this.minecraft.worldRenderer.render(0);
|
|
this.minecraft.onRender(0)
|
|
}
|
|
}
|
|
|
|
updateScaleFactor() {
|
|
let wrapperWidth = this.wrapper.offsetWidth;
|
|
let wrapperHeight = this.wrapper.offsetHeight;
|
|
|
|
let scale;
|
|
for (scale = 1; wrapperWidth / (scale + 1) >= 320 && wrapperHeight / (scale + 1) >= 240; scale++) {
|
|
// Empty
|
|
}
|
|
|
|
this.scaleFactor = scale;
|
|
this.width = Math.ceil(wrapperWidth / scale);
|
|
this.height = Math.ceil(wrapperHeight / scale);
|
|
}
|
|
|
|
isCursorLockedToCanvas() {
|
|
// The actual state of the browser cursor lock
|
|
return document.pointerLockElement === this.canvas;
|
|
}
|
|
|
|
isLocked() {
|
|
// The actual definition for the game if the cursor is locked or not
|
|
return this.focusState.isLock() && this.minecraft.currentScreen === null;
|
|
}
|
|
|
|
updateFocusState(state) {
|
|
if (state.getIntent() === this.focusState || state === this.focusState) {
|
|
return;
|
|
}
|
|
|
|
let prevLock = this.focusState.isLock();
|
|
let nextLock = state.isLock();
|
|
|
|
// Update state
|
|
this.focusState = state;
|
|
|
|
// Update cursor visibility
|
|
document.body.style.cursor = nextLock ? 'none' : 'default';
|
|
|
|
// Request lock state
|
|
this.requestCursorUpdate();
|
|
|
|
// Open menu on exit
|
|
if (prevLock !== nextLock) {
|
|
let currentScreen = this.minecraft.currentScreen;
|
|
|
|
// Open in-game menu
|
|
if (currentScreen === null && !nextLock) {
|
|
this.minecraft.displayScreen(new GuiIngameMenu());
|
|
}
|
|
|
|
// Close current screen
|
|
if (!(currentScreen instanceof GuiLoadingScreen) && nextLock) {
|
|
this.minecraft.displayScreen(null);
|
|
this.lastIngameSwitchTime = Date.now();
|
|
}
|
|
}
|
|
}
|
|
|
|
requestCursorUpdate() {
|
|
// Check if the current state doesn't match the canvas lock
|
|
if (this.mouseInsideWindow && this.focusState.isLock() !== this.isCursorLockedToCanvas()) {
|
|
// Request cursor lock depending on the state
|
|
if (this.focusState.isLock()) {
|
|
this.canvas.requestPointerLock();
|
|
} else {
|
|
document.exitPointerLock();
|
|
}
|
|
}
|
|
}
|
|
|
|
detectTouchDevice() {
|
|
let match = window.matchMedia || window.msMatchMedia;
|
|
if (match) {
|
|
let mq = match("(pointer:coarse)");
|
|
return mq.matches;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
getMemoryLimit() {
|
|
return this.getMemoryValue("jsHeapSizeLimit", 1);
|
|
}
|
|
|
|
getMemoryAllocated() {
|
|
return this.getMemoryValue("totalJSHeapSize", 0);
|
|
}
|
|
|
|
getMemoryUsed() {
|
|
return this.getMemoryValue("usedJSHeapSize", 0);
|
|
}
|
|
|
|
getMemoryValue(key, fallbackValue = 0) {
|
|
let performance = window.performance || window.msPerformance || window.webkitPerformance || window.mozPerformance;
|
|
if (performance && performance.memory && performance.memory[key]) {
|
|
return performance.memory[key];
|
|
}
|
|
return fallbackValue;
|
|
}
|
|
|
|
getGPUName() {
|
|
let gl = this.canvasWorld.getContext("webgl2");
|
|
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
}
|
|
|
|
openUrl(url, newTab) {
|
|
if (newTab) {
|
|
window.open(url, '_blank').focus();
|
|
} else {
|
|
window.location = url;
|
|
}
|
|
}
|
|
|
|
close() {
|
|
this.openUrl(Minecraft.URL_GITHUB);
|
|
}
|
|
|
|
async getClipboardText() {
|
|
return navigator.clipboard.readText();
|
|
}
|
|
|
|
isMobileDevice() {
|
|
return this.mobileDevice;
|
|
}
|
|
|
|
pullMouseMotionX() {
|
|
let value = this.mouseMotionX;
|
|
this.mouseMotionX = 0;
|
|
return value;
|
|
}
|
|
|
|
pullMouseMotionY() {
|
|
let value = this.mouseMotionY;
|
|
this.mouseMotionY = 0;
|
|
return value;
|
|
}
|
|
|
|
initialSoundEngine() {
|
|
// Create sound engine (It has to be created after user interaction)
|
|
if (!this.minecraft.soundManager.isCreated()) {
|
|
this.minecraft.soundManager.create(this.minecraft.worldRenderer);
|
|
}
|
|
}
|
|
|
|
registerListener(parent, event, listener = null, preventDefaults = true) {
|
|
parent.addEventListener(event, event => {
|
|
if (preventDefaults) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
if (listener !== null) {
|
|
listener(event);
|
|
}
|
|
});
|
|
}
|
|
}
|