summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2024-10-18 08:37:36 -0700
committerRose Hogenson <rosehogenson@posteo.net>2024-10-18 17:06:58 -0700
commit6f2519f5229ed8f037f0c062490cd2191e57da9c (patch)
tree1b2511a52251cfcea801d917e1ae4ae3bfd11bf9
parent81c25beb38c87678a52145ef8fcb97631bfa8322 (diff)
downloadroseh.moe-6f2519f5229ed8f037f0c062490cd2191e57da9c.tar.zst
Add a game.
-rw-r--r--ping.aup3bin0 -> 57344 bytes
-rw-r--r--roseh.moe.go12
-rw-r--r--static/ping.mp3bin0 -> 4016 bytes
-rw-r--r--static/pong.js270
-rw-r--r--templates/pong.html.template11
5 files changed, 293 insertions, 0 deletions
diff --git a/ping.aup3 b/ping.aup3
new file mode 100644
index 0000000..da7c415
--- /dev/null
+++ b/ping.aup3
Binary files differ
diff --git a/roseh.moe.go b/roseh.moe.go
index ce66f73..5ce4258 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -47,6 +47,17 @@ func index(w http.ResponseWriter, _ *http.Request) {
}
}
+//go:embed templates/pong.html.template
+var pongString string
+
+var pongTemplate = template.Must(template.New("pong").Parse(pongString))
+
+func pong(w http.ResponseWriter, _ *http.Request) {
+ if err := pongTemplate.Execute(w, nil); err != nil {
+ log.Printf("Warning: pong: %s", err)
+ }
+}
+
//go:embed static
var staticFiles embed.FS
@@ -65,6 +76,7 @@ func main() {
flag.Parse()
mux := http.NewServeMux()
+ mux.HandleFunc("/pong", pong)
jellyfinURL, err := url.Parse("http://localhost:8096")
if err != nil {
log.Fatalf("Jellyfin URL: %s", err)
diff --git a/static/ping.mp3 b/static/ping.mp3
new file mode 100644
index 0000000..87145b8
--- /dev/null
+++ b/static/ping.mp3
Binary files differ
diff --git a/static/pong.js b/static/pong.js
new file mode 100644
index 0000000..583585b
--- /dev/null
+++ b/static/pong.js
@@ -0,0 +1,270 @@
+const ticksPerSecond = 300;
+const ballSpeed = 10 * 60 / ticksPerSecond;
+const paddleSpeed = 10 * 60 / ticksPerSecond;
+
+const canvas = document.getElementById("pong");
+const ctx = canvas.getContext("2d");
+
+class Vector2 {
+ constructor(x, y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ add(v) {
+ return new Vector2(this.x + v.x, this.y + v.y);
+ }
+}
+
+class Rect {
+ constructor(x, y, w, h) {
+ this.x = x;
+ this.y = y;
+ this.w = w;
+ this.h = h;
+ }
+
+ bottom() {
+ return this.y + this.h;
+ }
+
+ right() {
+ return this.x + this.w;
+ }
+
+ intersects(r) {
+ return r.x <= this.right() && r.right() >= this.x && r.y <= this.bottom() && r.bottom() >= this.y;
+ }
+
+ fill() {
+ ctx.clearRect(this.x, this.y, this.w, this.h);
+ }
+}
+
+const digitSegments = [0x77, 0x24, 0x5d, 0x6d, 0x2e, 0x6b, 0x7b, 0x25, 0x7f, 0x6f];
+
+function drawNumber(n, x, y) {
+ const width = 60;
+ const height = 100;
+ const thickness = 10;
+ const padding = 20;
+
+ const digits = n.toString();
+ for (let i = 0; i < digits.length; i++) {
+ const segments = digitSegments[digits[i] - '0'];
+ for (let j = 0; j < 7; j++) {
+ if ((segments & 1 << j) === 0) {
+ continue;
+ }
+ switch (j) {
+ case 0:
+ new Rect(x, y, width, thickness).fill();
+ break;
+ case 1:
+ new Rect(x, y, thickness, height / 2).fill();
+ break;
+ case 2:
+ new Rect(x + width - thickness, y, thickness, height / 2).fill();
+ break;
+ case 3:
+ new Rect(x, y + height / 2 - thickness / 2, width, thickness).fill();
+ break;
+ case 4:
+ new Rect(x, y + height / 2, thickness, height / 2).fill();
+ break;
+ case 5:
+ new Rect(x + width - thickness, y + height / 2, thickness, height / 2).fill();
+ break;
+ case 6:
+ new Rect(x, y + height - thickness, width, thickness).fill();
+ break;
+ }
+ }
+ x += width + padding;
+ }
+}
+
+class Ball {
+ constructor() {
+ this.reset('left');
+ }
+
+ bounds() {
+ const ballSize = 30;
+
+ return new Rect(
+ this.pos.x - ballSize / 2,
+ this.pos.y - ballSize / 2,
+ ballSize,
+ ballSize,
+ );
+ }
+
+ predictIntersection(targetX) {
+ if (this.velocity.x <= 0) {
+ return null;
+ }
+ const dist = targetX - this.pos.x;
+ const prediction = this.pos.y + this.velocity.y * dist / this.velocity.x;
+ if (prediction < 0) {
+ const bounces = Math.floor(-prediction / canvas.height);
+ if (bounces % 2 === 0) {
+ return -prediction - canvas.height * bounces;
+ }
+ return prediction + canvas.height * (bounces + 1);
+ }
+ if (prediction > canvas.height) {
+ const bounces = Math.floor(prediction / canvas.height);
+ if (bounces % 2 === 0) {
+ return prediction - canvas.height * bounces;
+ }
+ return -prediction + canvas.height * (bounces + 1);
+ }
+ return prediction;
+ }
+
+ reset(side) {
+ this.pos = new Vector2(canvas.width / 2, canvas.height / 2);
+ this.velocity = new Vector2(ballSpeed, 0);
+ if (side === 'left') {
+ this.velocity.x = -this.velocity.x;
+ }
+ }
+}
+
+class Paddle {
+ constructor(side) {
+ this.pos = canvas.height / 2;
+ this.side = side;
+ }
+
+ bounds() {
+ const paddleHeight = 200;
+ const thickness = 10;
+ const padding = 100;
+
+ let r = new Rect(
+ padding - thickness / 2,
+ this.pos - paddleHeight / 2,
+ thickness,
+ paddleHeight,
+ );
+ if (this.side === 'right') {
+ r.x = canvas.width - padding - thickness / 2;
+ }
+ return r;
+ }
+}
+
+function updateCanvasSize() {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+}
+
+const pingAudio = new Audio('/static/ping.mp3');
+
+function ping() {
+ pingAudio.pause();
+ pingAudio.currentTime = 0;
+ pingAudio.play();
+}
+
+function main() {
+ updateCanvasSize();
+
+ let playerScore = 0;
+ let aiScore = 0;
+
+ const ball = new Ball();
+
+ const paddle1 = new Paddle('left');
+ const paddle2 = new Paddle('right');
+
+ const keys = {};
+ let mousePressed = false;
+ let mouseY = 0;
+ window.addEventListener('keydown', (e) => keys[e.key] = true);
+ window.addEventListener('keyup', (e) => delete keys[e.key]);
+ window.addEventListener('mousedown', () => mousePressed = true);
+ window.addEventListener('mouseup', () => mousePressed = false);
+ window.addEventListener('mousemove', (e) => mouseY = e.y);
+
+ let lastFrameTime = performance.now();
+ let accumulator = 0;
+
+ function gameLoop() {
+ updateCanvasSize();
+
+ const now = performance.now();
+ const frameTime = now - lastFrameTime;
+ lastFrameTime = now;
+ accumulator += frameTime;
+
+ const simulationSpeed = 1000 / ticksPerSecond;
+ for (; accumulator >= simulationSpeed; accumulator -= simulationSpeed) {
+ if ((keys["w"] || keys["ArrowUp"] || mousePressed && paddle1.pos >= mouseY + paddleSpeed) && paddle1.bounds().y >= 0) {
+ paddle1.pos -= paddleSpeed;
+ } else if ((keys["s"] || keys["ArrowDown"] || mousePressed && paddle1.pos <= mouseY - paddleSpeed) && paddle1.bounds().bottom() <= canvas.height) {
+ paddle1.pos += paddleSpeed;
+ }
+
+ const prediction = ball.predictIntersection(paddle2.bounds().x);
+ if (prediction !== null) {
+ if (paddle2.pos <= prediction - paddleSpeed && paddle2.bounds().bottom() <= canvas.height) {
+ paddle2.pos += paddleSpeed;
+ } else if (paddle2.pos >= prediction + paddleSpeed && paddle2.bounds().y >= 0) {
+ paddle2.pos -= paddleSpeed;
+ }
+ }
+
+ ball.pos = ball.pos.add(ball.velocity);
+ if (ball.bounds().intersects(paddle1.bounds()) && ball.velocity.x <= 0) {
+ ping();
+ let d = (ball.pos.y - paddle1.pos) / (paddle1.bounds().h / 2 + ball.bounds().h);
+ if (d < 0) {
+ d = -d;
+ }
+ let angle = d * 75 * 2 * Math.PI / 360;
+ if (ball.pos.y < paddle1.pos) {
+ angle = -angle;
+ }
+ const speed = ballSpeed * (1 + 4 * d);
+ ball.velocity = new Vector2(speed * Math.cos(angle), speed * Math.sin(angle));
+ } else if (ball.bounds().intersects(paddle2.bounds()) && ball.velocity.x >= 0) {
+ ping();
+ let d = (ball.pos.y - paddle2.pos) / (paddle2.bounds().h / 2 + ball.bounds().h);
+ if (d < 0) {
+ d = -d;
+ }
+ let angle = d * 75 * 2 * Math.PI / 360;
+ if (ball.pos.y < paddle2.pos) {
+ angle = -angle;
+ }
+ angle += Math.PI;
+ const speed = ballSpeed * (1 + 4 * d);
+ ball.velocity = new Vector2(speed * Math.cos(angle), speed * Math.sin(angle));
+ } else if (ball.bounds().right() <= 0) {
+ aiScore++;
+ ball.reset('right');
+ } else if (ball.bounds().x >= canvas.width) {
+ playerScore++;
+ ball.reset('left');
+ } else if (ball.bounds().y <= 0 && ball.velocity.y < 0 || ball.bounds().bottom() >= canvas.height && ball.velocity.y > 0) {
+ ping();
+ ball.velocity.y = -ball.velocity.y;
+ }
+ }
+
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ paddle1.bounds().fill();
+ paddle2.bounds().fill();
+ ball.bounds().fill();
+ drawNumber(playerScore, 150, 30)
+ drawNumber(aiScore, canvas.width - 600, 30);
+
+ requestAnimationFrame(gameLoop);
+ }
+ gameLoop();
+}
+
+main();
diff --git a/templates/pong.html.template b/templates/pong.html.template
new file mode 100644
index 0000000..e7a00c4
--- /dev/null
+++ b/templates/pong.html.template
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en" style="width:100%;height:100%;margin:0;padding:0">
+ <head>
+ <title>Pong</title>
+ <!-- TODO: page icon? -->
+ <script defer src="/static/pong.js"></script>
+ </head>
+ <body style="width:100%;height:100%;margin:0;padding:0">
+ <canvas id="pong" style="display:block;margin:0;padding:0"></canvas>
+ </body>
+</html>