(function() { "use strict"; var canvas = document.getElementById("game"); if (!canvas.getContext) { alert("Your browser is unsupported (does not support canvas.getContext)."); return; } var ctx = canvas.getContext("2d"); var playfieldWidth = canvas.width; var playfieldHeight = canvas.height; var BALL_RADIUS = 20; var GRAVITY_ACCELERATION = 10; function message(text, expire) { document.getElementById("message").innerHTML = ""; var div = document.createElement("div"); div.innerText = text; document.getElementById("message").appendChild(div); div.className = "visible"; if (expire === false) { return; } if (!expire) { expire = 2000; } window.setTimeout(function() { div.className = ""; window.setTimeout(function() { try { div.parentNode.removeChild(div); } catch(e) {} }, 1000); }, expire); } function Vector(x, y) { this.x = x; this.y = y; } Vector.prototype.add = function(other) { return new Vector( this.x + other.x, this.y + other.y ); }; Vector.prototype.iadd = function(other) { this.x += other.x; this.y += other.y; }; Vector.prototype.sub = function(other) { return new Vector( this.x - other.x, this.y - other.y ); }; Vector.prototype.mul = function(scalar) { return new Vector( this.x * scalar, this.y * scalar ); }; Vector.prototype.dot = function(other) { return this.x * other.x + this.y * other.y; }; Vector.prototype.normSquared = function() { return this.dot(this); }; Vector.prototype.norm = function() { return Math.sqrt(this.normSquared()); }; Vector.prototype.distance_squared = function(other) { var diffX = this.x - other.x; var diffY = this.y - other.y; return diffX * diffX + diffY * diffY; }; Vector.prototype.toString = function() { return "Vector(" + this.x + ", " + this.y + ")"; }; function Circle(centre, radius) { this.centre = centre; this.radius = radius; } Circle.prototype.intersects = function(other) { return ( this.centre.distance_squared(other.centre) <= 2 * this.radius * 2 * this.radius ); } Circle.prototype.toString = function(other) { return "Circle(centre=" + this.centre.toString() + ", radius=" + this.radius + ")"; } function Ball(outline, velocity, colour, ghost) { this.outline = outline; this.velocity = velocity; this.colour = colour; this.ghost = ghost; this.audioBall = new Audio("ball.mp3"); this.audioEdge = new Audio("edge.mp3"); } Ball.prototype.toString = function() { return ( "Ball(outline=" + this.outline.toString() + "," + "velocity=" + this.velocity.toString() + ", " + "colour=" + this.colour + ", ghost=" + this.ghost + ")" ); } function intersectsExistingBall(circle) { for (var i = 0; i < balls.length; i++) { if (circle.intersects(balls[i].outline)) { return true; } } return false; } function bogoGenerateNewBallOutline() { var circle; do { circle = new Circle( new Vector( Math.random() * (playfieldWidth - 2*BALL_RADIUS) + BALL_RADIUS, Math.random() * (playfieldHeight - 2*BALL_RADIUS) + BALL_RADIUS ), BALL_RADIUS ); } while (intersectsExistingBall(circle)); return circle; } function bogoGenerateVelocity() { var vec; do { vec = new Vector( Math.random() * 10 - 2, Math.random() * 10 - 2 ); } while (vec.normSquared < 2); return vec; } var playerBall; var balls = []; function addBall(ghost) { var newBall = new Ball( bogoGenerateNewBallOutline(), bogoGenerateVelocity(), "red", ghost ); balls.push(newBall); } var isGameOver = true; function advanceBall(ball, timePassed) { if (ball.ghost != 0) { ball.ghost -= timePassed / 1000; if (ball.ghost <= 0) { ball.ghost = 0; } if (!isGameOver) { return; } } ball.outline.centre.iadd( ball.velocity.mul(timePassed / 20) ); } function bounceBalls(ball1, ball2) { var x1 = ball1.outline.centre; var x2 = ball2.outline.centre; var v1 = ball1.velocity; var v2 = ball2.velocity; var x_diff1 = x1.sub(x2); var x_diff2 = x2.sub(x1); var collisionForce = v1.sub(v2).norm(); ball1.audioBall.pause(); ball1.audioBall.currentTime = 0; ball1.audioBall.volume = clamp(collisionForce / 10 + 0.1, 0, 1); ball1.audioBall.play(); // https://en.wikipedia.org/w/index.php?title=Elastic_collision&oldid=1057236793#Two-dimensional_collision_with_two_moving_objects ball1.velocity = v1.sub(x_diff1.mul((v1.sub(v2).dot(x_diff1)) / x_diff1.normSquared())); ball2.velocity = v2.sub(x_diff2.mul((v2.sub(v1).dot(x_diff2)) / x_diff2.normSquared())); // Make balls non-intersecting var c = x1.add(x2).mul(1/2) ball1.outline.centre = c.add(x1.sub(c).mul((ball1.outline.radius + 0.05) / x1.sub(c).norm())); ball2.outline.centre = c.add(x2.sub(c).mul((ball2.outline.radius + 0.05) / x2.sub(c).norm())); } function handleBounceEdge(ball, ignoreBottom) { var bounce = false; if (ball.outline.centre.x <= ball.outline.radius) { ball.velocity.x *= -1; ball.outline.centre.x = ball.outline.radius + (ball.outline.radius - ball.outline.centre.x); bounce = true; } if (ball.outline.centre.y <= ball.outline.radius) { ball.velocity.y *= -1; ball.outline.centre.y = ball.outline.radius + (ball.outline.radius - ball.outline.centre.y); bounce = true; } var maxX = playfieldWidth - ball.outline.radius; var maxY = playfieldHeight - ball.outline.radius; if (ball.outline.centre.x >= maxX) { ball.velocity.x *= -1; ball.outline.centre.x = maxX + (maxX - ball.outline.centre.x); bounce = true; } if (ball.outline.centre.y >= maxY && !ignoreBottom) { ball.velocity.y *= -1; ball.outline.centre.y = maxY + (maxY - ball.outline.centre.y); bounce = true; } if (bounce) { ball.audioEdge.pause(); ball.audioEdge.currentTime = 0; ball.audioEdge.volume = clamp(ball.velocity.norm() / 3 + 0.1, 0, 1); ball.audioEdge.play(); } } function checkPlayerCollision() { for (var i = 1; i < balls.length; i++) { if (balls[i].ghost == 0 && playerBall.outline.intersects(balls[i].outline)) { bounceBalls(playerBall, balls[i]); gameOver(); return; } } } function checkBallCollision(includingPlayerBall) { var start = includingPlayerBall ? 0 : 1; for (var i = start; i < balls.length; i++) for (var j = i + 1; j < balls.length; j++) { if ( balls[i].outline.intersects(balls[j].outline) && ( (balls[i].ghost == 0 && balls[j].ghost == 0) || isGameOver ) ) { bounceBalls(balls[i], balls[j]); } } } function gravity(ball, timePassed) { ball.velocity.y += GRAVITY_ACCELERATION * timePassed / 1000; } function allFallen() { for (var i = 0; i < balls.length; i++) { if (balls[i].outline.centre.y < playfieldHeight + balls[i].outline.radius) { return false; } } return true; } var colorGradients = { "blue": [ [0, "#2b2bff"], [0.5, "#1212eb"], [1, "#000092"] ], "red": [ [0, "#ff2b2b"], [0.5, "#eb1212"], [1, "#920000"] ] }; function drawBall(ball) { var x = ball.outline.centre.x; var y = ball.outline.centre.y; var r = ball.outline.radius; ctx.beginPath(); ctx.globalAlpha = 1 - ball.ghost; ctx.arc(x, y, ball.outline.radius, 0, Math.PI * 2, true); var gradient = ctx.createRadialGradient(x+r*.75,y-r*.75,0, x,y,r*1.1); for (var i in colorGradients[ball.colour]) { gradient.addColorStop( colorGradients[ball.colour][i][0], colorGradients[ball.colour][i][1] ); } ctx.fillStyle = gradient; ctx.closePath(); ctx.fill(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (var i = balls.length - 1; i >= 0; i--) { drawBall(balls[i]); } } var gameStarted = null; var previousTime = -1; function gameLoop(timestamp) { if (previousTime == -1) { previousTime = timestamp; } var timePassed = timestamp - previousTime; checkPlayerCollision(); for (var i = 1; i < balls.length; i++) { advanceBall(balls[i], timePassed); handleBounceEdge(balls[i]); } checkBallCollision(false); draw(); previousTime = timestamp; if (!isGameOver) { requestAnimationFrame(gameLoop); } else { requestAnimationFrame(gameOverLoop); } } function gameOver() { console.log("Game over"); var gameDuration = new Date() - gameStarted; message( "You reached " + (balls.length - 1) + " balls\n" + "You lasted " + Math.floor(gameDuration / 1000) + " seconds", false); document.getElementById("playbutton").innerHTML = "Play again"; if (ballAddTimeout !== null) { window.clearTimeout(ballAddTimeout); } isGameOver = true; fadeOutMusic(); document.body.className = "gameover"; } function fadeOutMusic() { if (!isGameOver) return; if (document.getElementById("music").volume > 0.01) { document.getElementById("music").volume *= 0.8; window.setTimeout(fadeOutMusic, 100); } else { document.getElementById("music").pause(); } } function gameOverLoop(timestamp) { var timePassed = timestamp - previousTime; for (var i = 0; i < balls.length; i++) { gravity(balls[i], timePassed); advanceBall(balls[i], timePassed); handleBounceEdge(balls[i], true); } checkBallCollision(true); draw(); previousTime = timestamp; if (!allFallen(balls)) { requestAnimationFrame(gameOverLoop); } else { prepareStartButton(); } } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function ballPositionFromEvent(event) { return new Vector( clamp(event.clientX - document.getElementById("gamecontainer").offsetLeft, BALL_RADIUS, playfieldWidth - BALL_RADIUS), clamp(event.clientY - document.getElementById("gamecontainer").offsetTop, BALL_RADIUS, playfieldHeight - BALL_RADIUS) ); } function moveMouseCallback(event) { if (isGameOver) return; // move player var previousPosition = playerBall.outline.centre; var newPosition = ballPositionFromEvent(event); playerBall.outline.centre = newPosition; playerBall.velocity = newPosition.sub(previousPosition).mul(0.1); } document.body.addEventListener("mousemove", moveMouseCallback); document.body.addEventListener("mousein", moveMouseCallback); var ballAddTimeout = null; var ballAddMessages = [ ["3 balls\nEasy peasy", 15], ["4 balls\nStill quite easy", 20], ["5 balls\nFun begins now", 30], ["6 balls\nQuite challenging", 35], ["7 balls\nPretty tough", 40], ["8 balls\nVery tricky", 50], ["9 balls\nTerribly hard", 60], ["10 balls\nIncredibly tough", 60], ["11 balls\nInsanely difficult", 60], ["12 balls\nAbsolutely crazy", 60], ["13 balls\nCompletely mad", 60], ["14 balls\nAlmost impossible", 60], ["15 balls\nMaxed out", 60] ]; function ballAddTimeoutFunction() { if (balls.length - 2 > ballAddMessages.length) { return; } addBall(1); var addMessage = ballAddMessages[balls.length - 4]; message(addMessage[0]); ballAddTimeout = window.setTimeout(ballAddTimeoutFunction, addMessage[1] * 1000); } function startGame(mouseEvent) { document.getElementById("music").currentTime = 0; document.getElementById("music").volume = 0.5; document.getElementById("music").play(); document.getElementById("playbutton").style.display = "none"; playerBall = new Ball( new Circle(ballPositionFromEvent(mouseEvent), BALL_RADIUS), new Vector(0, 0), "blue", 0 ); balls = [playerBall]; for (var i = 0; i < 2; i++) { addBall(1); } console.log(`${balls.length} balls`); ballAddTimeoutFunction(); isGameOver = false; previousTime = -1; requestAnimationFrame(gameLoop); gameStarted = new Date(); document.body.className = "gameon"; } function prepareStartButton() { document.getElementById("playbutton").style.display = "block"; document.getElementById("playbutton").onclick = startGame; } prepareStartButton(); })();