diff --git a/index.html b/index.html index 4295bec..d80b1ab 100644 --- a/index.html +++ b/index.html @@ -17,11 +17,19 @@

GAME OVER

+ +
+ 📺️ + 🎮 + 🖱 +
+
+ @@ -37,6 +45,8 @@

GAME OVER

const playButton = document.getElementById('playButton'); const pauseButton = document.getElementById('pauseButton'); const restartButton = document.getElementById('restartButton'); + const gamepadButton = document.getElementById('gamepad'); + const mouseButton = document.getElementById('mouse'); // captions const greetingCaption = document.getElementById('greetingCaption'); @@ -52,31 +62,69 @@

GAME OVER

}); engine.onGameOver(() => { - gameButtons.classList.remove('hide'); - greetingCaption.classList.add('hide'); - gameOverCaption.classList.remove('hide'); - playButton.classList.add('hide'); - restartButton.classList.remove('hide'); + hide(greetingCaption, playButton); + show(gameButtons, gameOverCaption, restartButton); }); playButton.addEventListener('click', () => { - gameButtons.classList.add('hide'); - inGameButtons.classList.remove('hide'); + hide(gameButtons); + show(inGameButtons); engine.play(); }); pauseButton.addEventListener('click', () => { - gameButtons.classList.remove('hide'); - restartButton.classList.remove('hide'); + show(gameButtons, restartButton, playButton); engine.pause(); }); restartButton.addEventListener('click', () => { - greetingCaption.classList.add('hide'); - gameOverCaption.classList.remove('hide'); - gameButtons.classList.add('hide'); + hide(gameOverCaption, gameButtons); + show(greetingCaption); engine.restart(); }); + + gamepadButton.addEventListener('click', () => controls.switchControlsMode(CONTROLS_MODES.GAMEPAD_MODE)); + + mouseButton.addEventListener('click', () => controls.switchControlsMode(CONTROLS_MODES.MOUSE_MODE)); + + controls.on('gamepadconnected', () => { + controls.switchControlsMode(CONTROLS_MODES.MOUSE_MODE); + }); + + controls.on('gamepaddisconnected', () => { + controls.switchControlsMode(CONTROLS_MODES.NO_MODE); + }); + + controls.on('controlsModeSwitched', (mode) => { + switch (mode) { + case CONTROLS_MODES.MOUSE_MODE: { + hide(mouseButton); + show(gamepadButton); + break; + } + + case CONTROLS_MODES.GAMEPAD_MODE: { + hide(gamepadButton); + show(mouseButton); + break; + } + + case CONTROLS_MODES.NO_MODE: { + hide(mouseButton, gamepadButton); + break; + } + } + }); + + function toggleFullScreen() { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } + } + } diff --git a/main.css b/main.css index 2a02bfc..200e571 100644 --- a/main.css +++ b/main.css @@ -11,6 +11,7 @@ body { -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* Internet Explorer/Edge */ user-select: none; + overflow: hidden; } button { @@ -56,6 +57,24 @@ button { background: rgba(102, 51, 153, 0.6); } +#generalButtons { + display: flex; + right: 10px; + bottom: 10px; + font-size: 24px; +} + +#generalButtons span { + margin-left: 15px; +} + +.aim { + font-size: 72px; + margin: 0; + padding: 0; + line-height: 0; +} + .position-absolute { position: absolute; } diff --git a/src/constants.js b/src/constants.js index 5b2f671..27eb372 100644 --- a/src/constants.js +++ b/src/constants.js @@ -14,3 +14,11 @@ const DEADLINE = 10; const RANGE_X = 10; const RANGE_Y = 2; + +const CONTROLS_MODES = { + NO_MODE: 'NO_MODE', + MOUSE_MODE: 'MOUSE_MODE', + GAMEPAD_MODE: 'GAMEPAD_MODE', + VR_MODE: 'VR_MODE', // for future + AR_MODE: 'AR_MODE', // for future +} diff --git a/src/controls.js b/src/controls.js index 7e5abbb..7236bc2 100644 --- a/src/controls.js +++ b/src/controls.js @@ -4,11 +4,18 @@ * you can simply subscribe on some of AVAILABLE_DOM_EVENTS */ class Controls { - rootObject; constructor(rootObject = document.body) { this.eventStorage = {}; this.rootObject = rootObject; + this.currentControlsMode = CONTROLS_MODES.MOUSE_MODE; + + this.aim = document.createElement('p'); + this.aim.innerText = '+'; + this.aim.classList.add('aim', 'position-absolute'); + hide(this.aim); + this.rootObject.appendChild(this.aim); + this.init(); } @@ -16,6 +23,14 @@ class Controls { * @method init */ init() { + window.addEventListener("gamepadconnected", ({ gamepad }) => { + this.dispatch('gamepadconnected', gamepad); + }); + + window.addEventListener("gamepaddisconnected", ({ gamepad }) => { + this.dispatch('gamepaddisconnected', gamepad); + }); + AVAILABLE_DOM_EVENTS.forEach((eventName) => { this.rootObject.addEventListener(eventName, (event) => { this.dispatch(eventName, event); @@ -23,6 +38,59 @@ class Controls { }); } + switchControlsMode(mode) { + if (mode in CONTROLS_MODES) { + this.currentControlsMode = mode; + this.handleSwitchControlsMode(mode); + this.dispatch('controlsModeSwitched', mode); + } + } + + handleSwitchControlsMode(mode) { + if(mode === CONTROLS_MODES.GAMEPAD_MODE) { + show(this.aim); + this.aim.style.marginLeft = `-${39.59 / 2}px`; + this.moveAimToCoordinates(0, 0); + } + + else if(mode === CONTROLS_MODES.MOUSE_MODE || mode === CONTROLS_MODES.NO_MODE) { + hide(this.aim); + } + } + + moveAimToCoordinates(x, y) { + const pos = convertCoordinatesToPixels( + this.rootObject.offsetWidth, + this.rootObject.offsetHeight, + x, y + ); + + this.aim.style.left = `${pos.x}px`; + this.aim.style.top = `${pos.y}px`; + } + + handleGamepadButtons() { + if (this.currentControlsMode === CONTROLS_MODES.GAMEPAD_MODE) { + + const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; + if (!gamepads) { + return; + } + + const gp = gamepads[0]; + if (gp.buttons[0].pressed) { + this.dispatch('shot1', convertCoordinatesToPixels( + this.rootObject.offsetWidth, + this.rootObject.offsetHeight, + gp.axes[0].toFixed(1), + gp.axes[1].toFixed(1) + )); + } + + this.moveAimToCoordinates(gp.axes[0].toFixed(1), gp.axes[1].toFixed(1)) + } + } + /** * @method on * @description this method should assign some callback to eventName and prevent secondary assignment @@ -41,11 +109,11 @@ class Controls { * @method dispatch * @description this method should provide way for trigger some callback by eventName * @param eventName - * @param DOMEvent + * @param data */ - dispatch(eventName, DOMEvent) { + dispatch(eventName, data) { if (eventName in this.eventStorage) { - this.eventStorage[eventName](DOMEvent); + this.eventStorage[eventName](data); } else { throw(`there is no callback for ${eventName} event name`); } diff --git a/src/engine.js b/src/engine.js index 4070d66..4d212f9 100644 --- a/src/engine.js +++ b/src/engine.js @@ -6,7 +6,8 @@ class Engine { _generateEnemiesIntervalId; _onGameOverCallback; gameEventCallbackMap; - __defaultAnimationAction = (enemyMeshes) => this.updateExistingEnemiesPosition(enemyMeshes); + __updateEnemiesDefaultAnimationAction = (enemyMeshes) => this.updateExistingEnemiesPosition(enemyMeshes); + __handleGamepadButtonsDefaultAnimationAction = () => this.controls.handleGamepadButtons(); /** * @constructor throw dependencies here @@ -19,8 +20,10 @@ class Engine { this.controls = controls; this.raycaster = raycaster; + this.viewer.addAnimationActions(this.__handleGamepadButtonsDefaultAnimationAction); + this.gameEventCallbackMap = { - 'shot': ({ clientX, clientY }) => this.onShot({clientX, clientY}), + 'shot': ({ clientX, clientY }) => this.onShot({x: clientX, y: clientY}), }; this.init(); @@ -35,6 +38,10 @@ class Engine { }); } + this.controls.on('shot1', (pos) => { + this.onShot(pos); + }); + this.raycaster.onIntersection((UUID) => { this.onHit(UUID); }); @@ -63,7 +70,11 @@ class Engine { } play() { - this.viewer.setAnimationAction(this.__defaultAnimationAction); + this.viewer.addAnimationActions( + this.__updateEnemiesDefaultAnimationAction, + this.__handleGamepadButtonsDefaultAnimationAction + ); + this._generateEnemiesIntervalId = setInterval( () => this.generateEnemies(), GENERATE_ENEMIES_INTERVAL_TIME @@ -72,7 +83,7 @@ class Engine { pause() { clearInterval(this._generateEnemiesIntervalId); - this.viewer.removeAnimationAction(); + this.viewer.clearAnimationActions(); } onShot(coordinates) { @@ -89,7 +100,7 @@ class Engine { gameOver() { clearInterval(this._generateEnemiesIntervalId); - this.viewer.removeAnimationAction(); + this.viewer.clearAnimationActions(); this._onGameOverCallback(); } diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000..e43a366 --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,24 @@ +function getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1) + min); +} + +function getRandomFloat(min, max) { + return Math.random() * (max - min) + min; +} + +function hide(...elements) { + elements.forEach(element => element.classList.add('hide')); +} + +function show(...elements) { + elements.forEach(element => element.classList.remove('hide')); +} + +function convertCoordinatesToPixels(width, height, x = 0, y = 0) { + return { + x: ((width / 2) + ((width / 2) * x)), + y: ((height / 2) + ((height / 2) * y)), + }; +} diff --git a/src/raycaster.js b/src/raycaster.js index f9678e9..6e77dec 100644 --- a/src/raycaster.js +++ b/src/raycaster.js @@ -12,8 +12,8 @@ class Raycaster { * @param intersectionParams - object with three fields: coordinates, container & types */ intersect(intersectionParams) { - this.mouse.x = (intersectionParams.coordinates.clientX / window.innerWidth) * 2 - 1; - this.mouse.y = -(intersectionParams.coordinates.clientY / window.innerHeight) * 2 + 1; + this.mouse.x = (intersectionParams.coordinates.x / window.innerWidth) * 2 - 1; + this.mouse.y = -(intersectionParams.coordinates.y / window.innerHeight) * 2 + 1; this.raycaster.setFromCamera(this.mouse, this.camera); const intersects = this.raycaster.intersectObjects(intersectionParams.container.children); diff --git a/src/viewer.js b/src/viewer.js index 53b96a4..040d2f5 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -2,7 +2,7 @@ class Viewer { _objContainer; _animationFrameId; - _animationAction = null; + _animationActions = []; constructor(HTMLContainer) { this.HTMLContainer = HTMLContainer; @@ -33,17 +33,19 @@ class Viewer { window.addEventListener('resize', this.onWindowResize); } - setAnimationAction(callback) { - this._animationAction = callback; + addAnimationActions(...callbacks) { + this._animationActions.push(...callbacks); } - removeAnimationAction() { - this._animationAction = null; + clearAnimationActions() { + this._animationActions.length = 0; } animate() { this.__animationFrameId = requestAnimationFrame(() => { - if (this._animationAction) { this._animationAction(this._objContainer.children); } + if (this._animationActions.length) { + this._animationActions.forEach(animationAction => animationAction(this._objContainer.children)); + } this.renderFrame(); this.animate(); }); @@ -71,8 +73,8 @@ class Viewer { const obj = this._makeEmojiSprite(config.look); obj.position.set( - this.getRandomInt(-RANGE_X, RANGE_X), - this.getRandomFloat(0, RANGE_Y), + getRandomInt(-RANGE_X, RANGE_X), + getRandomFloat(0, RANGE_Y), -DEADLINE ); @@ -121,14 +123,4 @@ class Viewer { this._objContainer.remove(this._objContainer.children[0]); } } - - getRandomInt(min, max) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1) + min); - } - - getRandomFloat(min, max) { - return Math.random() * (max - min) + min; - } }