"use strict"; var boardRatio = 1.5; var ballRadius = 0.01, padWidth = 0.01, padHeight = 0.15, padX = 0.01; var padBaseSpeed = 0.8; var gameId; var cvs, ctx, cvsW, cvsH, bdW, bdH, bdltX, bdltY; var socket; var playing = false, flip = false; var ballX, ballY, ballVX, ballVY; var padPos, padVel, pad2Pos, pad2Vel; function redraw() { ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, cvsW, cvsH); redrawFast(); } function calcBallX() {return bdltX + bdW * (flip ? 1 - ballX : ballX);} function calcBallY() {return bdltY + bdH * ballY;} function calcPadY() {return bdltY + bdH * ((flip ? pad2Pos : padPos) - padHeight / 2);} function calcPad2Y() {return bdltY + bdH * ((flip ? padPos : pad2Pos) - padHeight / 2);} function redrawFast() { ctx.strokeStyle = "#ffffff"; ctx.lineWidth = 2; ctx.strokeRect(bdltX - 1, bdltY - 1, bdW + 2, bdH + 2); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(bdltX + bdW / 2 | 0, bdltY); ctx.lineTo(bdltX + bdW / 2 | 0, bdltY + bdH); ctx.stroke(); if (playing) { ctx.fillStyle = "#ffffff"; ctx.beginPath(); ctx.arc(calcBallX(), calcBallY(), ballRadius * bdW, 0, 2 * Math.PI); ctx.rect(bdltX + bdW * padX, calcPadY(), bdW * padWidth, bdH * padHeight); ctx.rect(bdltX + bdW * (1 - padX - padWidth), calcPad2Y(), bdW * padWidth, bdH * padHeight); ctx.fill(); } } function undrawElements() { ctx.fillStyle = "#000000"; ctx.beginPath(); ctx.rect( calcBallX() - ballRadius * bdW - 2, calcBallY() - ballRadius * bdW - 2, 2 * ballRadius * bdW + 4, 2 * ballRadius * bdW + 4); ctx.rect( bdltX + bdW * padX - 2, calcPadY() - 2, bdW * padWidth + 4, bdH * padHeight + 4); ctx.rect( bdltX + bdW * (1 - padX - padWidth) - 2, calcPad2Y() - 2, bdW * padWidth + 4, bdH * padHeight + 4); ctx.fill(); } function resizeHandler() { var rect = cvs.getBoundingClientRect(); cvsW = cvs.width = rect.width; cvsH = cvs.height = rect.height; // I believe this stuff is correct. if (cvsH * boardRatio > cvsW) { bdW = cvsW; bdH = bdW / boardRatio | 0; bdltX = 2; bdltY = (cvsH - bdH) / 2 + 2 | 0; bdW -= 4; bdH -= 4; } else { bdH = cvsH; bdW = boardRatio * bdH | 0; bdltX = (cvsW - bdW) / 2 + 2 | 0; bdltY = 2; bdW -= 4; bdH -= 4; } redraw(); } function displayStatus(msg) { document.getElementById("status").innerHTML = msg; } function displayScore(score) { document.getElementById("score").classList.remove("invisible"); document.getElementById("score-left").innerHTML = score[0]; document.getElementById("score-right").innerHTML = score[1]; setTimeout(function() { document.getElementById("score").classList.add("invisible"); }, 2000); } function openGame() { socket = io(); socket.on("redirect", function(url) { location.href = url; }); socket.on("status", function(msg) { displayStatus(msg); }); socket.on("score", function(score) { displayScore(score); }); socket.on("join", function() { playSound("join"); }); socket.on("leave", function() { playSound("leave"); }); socket.on("othermiss", function() { playSound("score"); }); socket.on("bounce", function() { playSound("bounce"); }); socket.on("serve", function(side, x, y, initServe) { playing = true; flip = side == "right"; ballX = x; ballY = y; padPos = pad2Pos = 0.5; redraw(); if (!initServe) playSound("ready"); }); socket.on("stop", function() { playing = false; stopPhysicsLoop(); stopGraphicsLoop(); redraw(); }); socket.on("startBall", function() { initPhysics(); ballVX = -0.5 / boardRatio; ballVY = -0.5; startPhysicsLoop(); startGraphicsLoop(); playSound("start"); }); socket.on("start", function() { initPhysics(); ballVX = 0.5 / boardRatio; ballVY = -0.5; startPhysicsLoop(); startGraphicsLoop(); playSound("start"); }); socket.on("padvec", function(pos, vel) { pad2Pos = pos; pad2Vel = vel; }); socket.on("ballvec", function(x, y, vx, vy) { ballX = x; ballY = y; ballVX = vx; ballVY = vy; }); socket.emit("open", gameId); } var graphicsLoopId = null, loopPhysics = false; function startGraphicsLoop() { // undrawElements(); redraw(); // TODO: redrawFast graphicsLoopId = requestAnimationFrame(startGraphicsLoop); } function stopGraphicsLoop() { if (graphicsLoopId == null) return; cancelAnimationFrame(graphicsLoopId); graphicsLoopId = null; } function startPhysicsLoop() { let prev = performance.now(); loopPhysics = true; function tick() { if (!loopPhysics) { return; } const now = performance.now(); var info = advancePhysics((now - prev) / 1e3); prev = now; if (info.bounce || info.otherBounce) { playSound("bounce"); } if (info.bounce || info.ourSide) { socket.emit("ballvec", ballX, ballY, ballVX, ballVY); } if (info.bounce) { socket.emit("bounce"); } if (ballX < -ballRadius) { socket.emit("ballout"); stopPhysicsLoop(); playSound("miss"); } requestAnimationFrame(tick); }; requestAnimationFrame(tick); } function stopPhysicsLoop() { loopPhysics = false; } function initPhysics() { ballX = ballY = 0.5; ballVX = ballVY = 0; padPos = pad2Pos = 0.5; padVel = pad2Vel = 0; } // Returns {bounce, otherBounce, ourSide : Bool} function advancePhysics(deltaT) { var ret = {bounce: false, otherBounce: false, ourSide: false}; var newballX = ballX + ballVX * deltaT; var newballY = ballY + ballVY * deltaT; var newpadPos = padPos + padVel * deltaT; var newpad2Pos = pad2Pos + pad2Vel * deltaT; var t; if (newballY <= ballRadius) { t = (ballRadius - ballY) / ballVY; } else if (newballY >= 1 - ballRadius) { t = (1 - ballRadius - ballY) / ballVY; } else { t = -1; } if (t >= 0) { newballY = ballY + ballVY * (2 * t - deltaT); ballVY = -ballVY; } if (newballX <= padX + padWidth + ballRadius && Math.abs(newballY - padPos) < padHeight / 2 + ballRadius) { t = (padX + padWidth + ballRadius - ballX) / ballVX; ret.bounce = true; } else if (newballX >= 1 - (padX + padWidth + ballRadius) && Math.abs(newballY - pad2Pos) < padHeight / 2 + ballRadius) { t = (1 - (padX + padWidth + ballRadius) - ballX) / ballVX; ret.otherBounce = true; } else { t = -1; } if (t >= 0) { newballX = ballX + ballVX * (2 * t - deltaT); ballVX = -ballVX; } // If the ball has come to our side, send a ballvec if (ballX >= 0.5 && newballX < 0.5) { ret.ourSide = true; } ballX = newballX; ballY = newballY; padPos = newpadPos; pad2Pos = newpad2Pos; return ret; } var soundMap = {}; function preloadSounds() { var names = ["bounce", "leave", "ready", "start", "join", "miss", "score"]; var types = [["audio/ogg", "ogg"], ["audio/mpeg", "mp3"], ["audio/wav", "wav"]]; for (var i = 0; i < names.length; i++) { var audio = new Audio(); for (var j = 0; j < types.length; j++) { var source = document.createElement("source"); source.type = types[j][0]; source.src = "/snd/" + names[i] + "." + types[j][1]; audio.appendChild(source); } soundMap[names[i]] = audio; audio.preload = "auto"; } } var lastSoundStampMap = {}; function playSound(name) { var now = performance.now(); console.log("playSound(\"" + name + "\")"); if (lastSoundStampMap[name] == null || now - lastSoundStampMap[name] >= 500) { soundMap[name].play(); } lastSoundStampMap[name] = now; } function setupBindings() { var up = false, down = false; function padUpdate() { padVel = (down - up) * padBaseSpeed; socket.emit("padvec", padPos, padVel); } window.addEventListener("keydown", function(ev) { if (ev.key == "w" || ev.key == "ArrowUp") up = true; else if (ev.key == "s" || ev.key == "ArrowDown") down = true; padUpdate(); }); window.addEventListener("keyup", function(ev) { if (ev.key == "w" || ev.key == "ArrowUp") up = false; else if (ev.key == "s" || ev.key == "ArrowDown") down = false; padUpdate(); }); } window.addEventListener("load", function() { gameId = document.location.href.replace(/.*\//, ""); cvs = document.getElementById("cvs"); ctx = cvs.getContext("2d"); resizeHandler(); setupBindings(); preloadSounds(); openGame(); }); var resizeDebounceTimeout = null; window.addEventListener("resize", function() { if (resizeDebounceTimeout != null) return; resizeDebounceTimeout = setTimeout(function() { resizeDebounceTimeout = null; resizeHandler(); }, 50); });