diff --git a/frontend/static/js/index.js b/frontend/static/js/index.js index 10e0878..3d58006 100644 --- a/frontend/static/js/index.js +++ b/frontend/static/js/index.js @@ -6,6 +6,9 @@ import Chat from "./views/Chat.js"; import HomeView from "./views/HomeView.js"; import RegisterView from "./views/accounts/RegisterView.js"; import LogoutView from "./views/accounts/LogoutView.js"; +import GameView from "./views/Game.js" + + import AbstractRedirectView from "./views/AbstractRedirectView.js"; import MeView from "./views/MeView.js"; import ProfilePageView from "./views/profiles/ProfilePageView.js"; @@ -44,6 +47,7 @@ const router = async (uri) => { { path: "/home", view: HomeView }, { path: "/me", view: MeView }, { path: "/matchmaking", view: MatchMakingView }, + { path: "/game", view: GameView }, ]; // Test each route for potential match @@ -96,4 +100,4 @@ document.addEventListener("DOMContentLoaded", () => { router(location.pathname); }); -export { client, navigateTo } \ No newline at end of file +export { client, navigateTo } diff --git a/frontend/static/js/views/Game.js b/frontend/static/js/views/Game.js new file mode 100644 index 0000000..87dd0cb --- /dev/null +++ b/frontend/static/js/views/Game.js @@ -0,0 +1,250 @@ +import AbstractView from './AbstractView.js' + +export default class extends AbstractView { + constructor(params) { + super(params, 'Game'); + this.game = null; + } + + async getHtml() { + return ` +

Game

+ + + `; + } + + async postInit() { + document.getElementById('startGameButton').onclick = this.startGame.bind(this); + document.getElementById('stopGameButton').onclick = this.stopGame.bind(this); + } + + startGame() { + if (this.game == null) { + document.getElementById('startGameButton').innerHTML = 'Reset Game'; + this.game = new Game; + } + else { + document.getElementById('app').removeChild(this.game.canvas); + this.game.cleanup(); + this.game = new Game; + } + } + + stopGame() { + if (!this.game) + return; + document.getElementById('app').removeChild(this.game.canvas); + document.getElementById('app').removeChild(this.game.scoresDisplay); + this.game.cleanup(); + this.game = null; + document.getElementById('startGameButton').innerHTML = 'Start Game'; + } +} + +class Game { + constructor() { + //Global variables + this.def = { + CANVASHEIGHT: 270, + CANVASWIDTH: 480, + PADDLEHEIGHT: 70, + PADDLEWIDTH: 10, + PADDLEMARGIN: 5, + PADDLESPEED: 3, + BALLRADIUS: 5, + BALLSPEED: 2, + BALLSPEEDINCR: 0.15, + MAXBOUNCEANGLE: 10 * (Math.PI / 12), + MAXSCORE: 6 + }; + + this.canvas = document.createElement('canvas'); + this.canvas.id = 'gameCanvas'; + this.canvas.width = this.def.CANVASWIDTH; + this.canvas.height = this.def.CANVASHEIGHT; + this.canvas.style.border = '1px solid #d3d3d3'; + this.canvas.style.backgroundColor = '#f1f1f1'; + this.context = this.canvas.getContext('2d'); + document.getElementById('app').appendChild(this.canvas); + this.scoresDisplay = document.createElement('p'); + this.scoresDisplay.innerHTML = 'Scores: 0 - 0'; + document.getElementById('app').appendChild(this.scoresDisplay); + + this.players = [ + { + paddle: new Paddle(this.context, + this.def.PADDLEMARGIN, + this.def), + score: 0 + }, + { + paddle: new Paddle(this.context, + this.def.CANVASWIDTH - this.def.PADDLEMARGIN - this.def.PADDLEWIDTH, + this.def), + score: 0 + } + ]; + this.ballStartSide = 0; + this.ballRespawned = false; + this.ball = new Ball(this.context, this.def, this.ballStartSide); + + this.interval = setInterval(this.updateGame.bind(this), 10); + + this.keys = []; + this.keyUpHandler = this.keyUpHandler.bind(this); + this.keyDownHandler = this.keyDownHandler.bind(this); + document.addEventListener('keydown', this.keyDownHandler); + document.addEventListener('keyup', this.keyUpHandler); + } + + cleanup() { + clearInterval(this.interval); + document.removeEventListener('keydown', this.keyDownHandler); + document.removeEventListener('keyup', this.keyUpHandler); + this.canvas.style.display = 'none'; + } + + clear() { + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + + updateGame() { + //Paddle movement + if (this.keys.includes('s') && + this.players[0].paddle.y + this.def.PADDLEHEIGHT < this.def.CANVASHEIGHT - this.def.PADDLEMARGIN) + this.players[0].paddle.y += this.def.PADDLESPEED; + if (this.keys.includes('w') && + this.players[0].paddle.y > 0 + this.def.PADDLEMARGIN) + this.players[0].paddle.y -= this.def.PADDLESPEED; + + if (this.keys.includes('ArrowDown') && + this.players[1].paddle.y + this.def.PADDLEHEIGHT < this.def.CANVASHEIGHT - this.def.PADDLEMARGIN) + this.players[1].paddle.y += this.def.PADDLESPEED; + if (this.keys.includes('ArrowUp') && + this.players[1].paddle.y > 0 + this.def.PADDLEMARGIN) + this.players[1].paddle.y -= this.def.PADDLESPEED; + + //GOOAAAAL + if (this.ball.x <= 0) + this.updateScore(this.players[0].score, ++this.players[1].score); + else if (this.ball.x >= this.def.CANVASWIDTH) + this.updateScore(++this.players[0].score, this.players[1].score); + + //Ball collisions + if (this.detectCollision(this.players[0].paddle, this.ball)) + this.calculateBallVelocity(this.players[0].paddle.getCenter().y, this.ball); + else if (this.detectCollision(this.players[1].paddle, this.ball)) + this.calculateBallVelocity(this.players[1].paddle.getCenter().y, this.ball, -1); + + if (this.ball.y - this.ball.radius <= 0) + this.ball.vy *= -1; + else if (this.ball.y + this.ball.radius >= this.canvas.height) + this.ball.vy *= -1; + + if (!this.ballRespawned) { + this.ball.x += this.ball.vx; + this.ball.y += this.ball.vy; + } + + this.clear(); + this.players[0].paddle.update(); + this.players[1].paddle.update(); + this.ball.update(); + } + + updateScore(p1Score, p2Score) { + if (p1Score > this.def.MAXSCORE) { + this.scoresDisplay.innerHTML = 'Player 1 wins!! GGS'; + this.cleanup(); + } + else if (p2Score > this.def.MAXSCORE) { + this.scoresDisplay.innerHTML = 'Player 2 wins!! GGS'; + this.cleanup(); + } else { + this.scoresDisplay.innerHTML = `Scores: ${p1Score} - ${p2Score}`; + this.ballStartSide = 1 - this.ballStartSide; + this.ball = new Ball(this.context, this.def, this.ballStartSide); + this.ballRespawned = true; + new Promise(r => setTimeout(r, 300)) + .then(_ => this.ballRespawned = false); + } + } + + detectCollision(paddle, ball) { + let paddleCenter = paddle.getCenter(); + let dx = Math.abs(ball.x - paddleCenter.x); + let dy = Math.abs(ball.y - paddleCenter.y); + if (dx <= ball.radius + paddle.width / 2 && + dy <= ball.radius + paddle.height / 2) + return true; + return false; + } + + calculateBallVelocity(paddleCenterY, ball, side = 1) { + let relativeIntersectY = paddleCenterY - ball.y; + let normRelIntersectY = relativeIntersectY / this.def.PADDLEHEIGHT / 2; + let bounceAngle = normRelIntersectY * this.def.MAXBOUNCEANGLE; + + ball.speed += this.def.BALLSPEEDINCR; + ball.vx = ball.speed * side * Math.cos(bounceAngle); + ball.vy = ball.speed * -Math.sin(bounceAngle); + } + + keyUpHandler(ev) { + const idx = this.keys.indexOf(ev.key); + if (idx != -1) + this.keys.splice(idx, 1); + } + + keyDownHandler(ev) { + if (!this.keys.includes(ev.key)) + this.keys.push(ev.key); + } +} + +class Paddle { + constructor(context, paddleSide, def) { + this.width = def.PADDLEWIDTH; + this.height = def.PADDLEHEIGHT; + this.x = paddleSide; + this.y = def.CANVASHEIGHT / 2 - this.height / 2; + this.ctx = context; + this.update(); + } + + update() { + this.ctx.fillStyle = 'black'; + this.ctx.fillRect(this.x, this.y, this.width, this.height); + } + + getCenter() { + return { + x: this.x + this.width / 2, + y: this.y + this.height / 2 + }; + } +} + +class Ball { + constructor(context, def, startSide) { + this.radius = def.BALLRADIUS; + this.speed = def.BALLSPEED; + this.x = def.CANVASWIDTH / 2; + this.y = def.CANVASHEIGHT / 2; + this.vy = 0; + if (startSide === 0) + this.vx = -this.speed; + else + this.vx = this.speed; + this.ctx = context; + this.update(); + } + + update() { + this.ctx.fillStyle = 'black'; + this.ctx.beginPath(); + this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI); + this.ctx.fill(); + } +}