madstuff-website/js/tennis.js
2026-04-15 14:25:23 +01:00

228 lines
8.5 KiB
JavaScript

/**
* @file Implements a tennis game.
*
* @licstart The following is the entire license notice for the JavaScript code
* in this page.
*
* Copyright (C) 2026 Madison L.
*
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License (GNU GPL)
* as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
*
* As additional permission under GNU GPL version 3 section 7, you may
* distribute non-source (e.g., minimized or compacted) forms of that code
* without the copy of the GNU GPL normally required by section 4, provided you
* include this license notice and a URL through which recipients can access the
* Corresponding Source.
*
* @licend The above is the entire license notice for the JavaScript code in
* this page.
*
* Behold! My latest contribution to the site's growing technical debt, this
* time with 80% more duct tape and glue. Bask in the constant recomputations of
* the same values, repeated logic, and magic numbers galore! I just want to get
* this working, I'll fix it later.
*/
const gameState = {
START_MENU: 0,
IN_GAME: 1,
PAUSED: 2,
}
function rectsIntersecting(a, b) {
return (a.x-(a.w/2) < b.x+(b.w/2)) && (a.x+(a.w/2) > b.x-(b.w/2))
&& (a.y-(a.w/2) < b.y+(b.w/2)) && (a.y+(a.w/2) > b.y-(b.w/2));
}
function initGame() {
let canvas = document.getElementById("canvas");
let gameData = {
canvas: canvas,
canvasContext: canvas.getContext("2d"),
paddleLeft: {
rectangle: { x: 10, y: canvas.height/2, w: 10, h: 20 }, // px
controller: updatePaddleP,
},
paddleRight: {
rectangle: {
x: canvas.width-10, // px
y: canvas.height/2, // px
w: 10, // px
h: 20, // px
},
controller: updatePaddleCPU,
},
paddleSpeed: 1, // px/s
ball: {
rectangle: {
x: canvas.width/2,
y: canvas.height/2,
w: 10,
h: 10
},
direction: { x: -1, y: -1 },
speed: 0.5, // px/s
increment: 0.01, // px/collision
paddleHit: false,
},
score: { left: 0, right: 0 },
gameState: gameState.START_MENU,
targetFrameTime: 1/30, // 30 FPS
upPressed: false,
downPressed: false,
};
window.addEventListener("keydown", (event) => {
if (event.code == "KeyW")
gameData.upPressed = true;
else if (event.code == "KeyS")
gameData.downPressed = true;
else if ((event.code == "Escape")
&& (gameData.gameState != gameState.START_MENU))
gameData.gameState = (gameData.gameState == gameState.IN_GAME)
? gameState.PAUSED : gameState.IN_GAME;
else if ((event.code == "Enter")
&& (gameData.gameState == gameState.START_MENU))
gameData.gameState = gameState.IN_GAME;
});
window.addEventListener("keyup", (event) => {
if (event.code == "KeyW")
gameData.upPressed = false;
else if (event.code == "KeyS")
gameData.downPressed = false;
});
return gameData;
}
function updatePaddleP(paddle, gameData) {
if ((gameData.upPressed) && ((paddle.rectangle.y-paddle.rectangle.w) >= 0))
paddle.rectangle.y -= gameData.paddleSpeed;
if ((gameData.downPressed)
&& ((paddle.rectangle.y+paddle.rectangle.w) <= gameData.canvas.height))
paddle.rectangle.y += gameData.paddleSpeed;
}
function updatePaddleCPU(paddle, gameData) {
if (gameData.ball.rectangle.x < gameData.canvas.width/2)
return;
if (paddle.rectangle.y < gameData.ball.rectangle.y) {
paddle.rectangle.y += gameData.paddleSpeed;
}
if (paddle.rectangle.y > gameData.ball.rectangle.y) {
paddle.rectangle.y -= gameData.paddleSpeed;
}
}
function resetBall(ball) {
ball.rectangle.x = canvas.width/2;
ball.rectangle.y = canvas.height/2;
ball.direction.x *= -1;
ball.direction.y *= -1;
ball.speed = 0.5;
}
function updateBall(gameData) {
gameData.ball.rectangle.x += gameData.ball.direction.x*gameData.ball.speed;
gameData.ball.rectangle.y += gameData.ball.direction.y*gameData.ball.speed;
// r/programminghorror:
if ((gameData.ball.rectangle.y <= 0)
|| (gameData.ball.rectangle.y >= gameData.canvas.height)) {
gameData.ball.direction.y *= -1;
gameData.ball.paddleHit = false;
gameData.ball.speed += 0.01;
} else if ((rectsIntersecting(gameData.ball.rectangle,
gameData.paddleLeft.rectangle)
|| rectsIntersecting(gameData.ball.rectangle,
gameData.paddleRight.rectangle))
&& !gameData.ball.paddleHit) {
/*
* HACK: Ball gets stuck in paddle sometimes. This kinda fixes it, but
* it results in occasional weird behaviour where the ball is
* reflected when it probably shouldn't be. I think that's the
* least of the problems with this function though.
*/
gameData.ball.paddleHit = true;
gameData.ball.direction.x *= -1;
gameData.ball.speed += 0.01;
} else if (gameData.ball.rectangle.x <= 0) {
++gameData.score.right;
resetBall(gameData.ball);
} else if (gameData.ball.rectangle.x >= gameData.canvas.width) {
++gameData.score.left;
resetBall(gameData.ball);
}
}
function drawPaddle(paddle, gameData) {
gameData.canvasContext.fillStyle = "white";
const halfWidth = paddle.rectangle.w/2;
const halfHeight = paddle.rectangle.h/2;
gameData.canvasContext.fillRect(paddle.rectangle.x-halfWidth,
paddle.rectangle.y-halfHeight,
paddle.rectangle.w,
paddle.rectangle.h);
}
function drawBall(gameData) {
gameData.canvasContext.fillStyle = "white";
const halfWidth = gameData.ball.rectangle.w/2;
gameData.canvasContext.fillRect(gameData.ball.rectangle.x-halfWidth,
gameData.ball.rectangle.y-halfWidth,
gameData.ball.rectangle.w,
gameData.ball.rectangle.h);
}
function drawUI(gameData) {
gameData.canvasContext.fillStyle = "white";
gameData.canvasContext.font = "20px 'Courier New'";
if (gameData.gameState == gameState.START_MENU) {
gameData.canvasContext.fillText("Press Enter to Play", 45, 90, 250);
} else if (gameData.gameState == gameState.PAUSED) {
gameData.canvasContext.fillText("PAUSED", 120, 100, 100);
} else {
gameData.canvasContext.fillRect(gameData.canvas.width/2, 0, 2,
gameData.canvas.height);
gameData.canvasContext.fillText(gameData.score.left,
(gameData.canvas.width/2)-(20*2)-5,
20,
80);
gameData.canvasContext.fillText(gameData.score.right,
(gameData.canvas.width/2)+(20*2),
20,
80);
}
}
function updateGame(gameData) {
gameData.paddleLeft.controller(gameData.paddleLeft, gameData);
gameData.paddleRight.controller(gameData.paddleRight, gameData);
updateBall(gameData);
}
function drawGame(gameData) {
gameData.canvasContext.fillStyle = "black";
gameData.canvasContext.fillRect(0, 0, gameData.canvas.width,
gameData.canvas.height);
if (gameData.gameState == gameState.IN_GAME) {
drawPaddle(gameData.paddleLeft, gameData);
drawPaddle(gameData.paddleRight, gameData);
drawBall(gameData);
}
drawUI(gameData);
}
function runGame() {
let gameData = initGame();
setInterval(function() {
if (gameData.gameState == gameState.IN_GAME)
updateGame(gameData);
drawGame(gameData);
}, gameData.targetFrameTime);
}
runGame();