From de5b309e0f06e93a963e09ef13c369ae7deeccc9 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Sat, 25 Jul 2026 10:58:28 -0700 Subject: Port a game I wrote a long time ago to JS --- static/games/pong/pong.js | 2 +- .../games/stallman-shooter/assets/binary_blob.webp | Bin 0 -> 1178 bytes static/games/stallman-shooter/assets/gates.webp | Bin 0 -> 2638 bytes .../stallman-shooter/assets/introduction.webp | Bin 0 -> 622752 bytes static/games/stallman-shooter/assets/loss.webp | Bin 0 -> 276316 bytes .../games/stallman-shooter/assets/player_pepp.webp | Bin 0 -> 2094 bytes static/games/stallman-shooter/assets/stallman.webp | Bin 0 -> 3654 bytes static/games/stallman-shooter/assets/victory.webp | Bin 0 -> 418448 bytes static/games/stallman-shooter/constants.js | 2 + static/games/stallman-shooter/gates.js | 143 +++++++++++++++++ static/games/stallman-shooter/main.js | 173 +++++++++++++++++++++ static/games/stallman-shooter/player.js | 88 +++++++++++ static/games/stallman-shooter/state.js | 82 ++++++++++ 13 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 static/games/stallman-shooter/assets/binary_blob.webp create mode 100644 static/games/stallman-shooter/assets/gates.webp create mode 100644 static/games/stallman-shooter/assets/introduction.webp create mode 100644 static/games/stallman-shooter/assets/loss.webp create mode 100644 static/games/stallman-shooter/assets/player_pepp.webp create mode 100644 static/games/stallman-shooter/assets/stallman.webp create mode 100644 static/games/stallman-shooter/assets/victory.webp create mode 100644 static/games/stallman-shooter/constants.js create mode 100644 static/games/stallman-shooter/gates.js create mode 100644 static/games/stallman-shooter/main.js create mode 100644 static/games/stallman-shooter/player.js create mode 100644 static/games/stallman-shooter/state.js (limited to 'static') diff --git a/static/games/pong/pong.js b/static/games/pong/pong.js index b8bdc2d..bdf95ac 100644 --- a/static/games/pong/pong.js +++ b/static/games/pong/pong.js @@ -161,7 +161,7 @@ function updateCanvasSize() { canvas.height = window.innerHeight; } -const pingAudio = new Audio('/static/pong/ping.mp3'); +const pingAudio = new Audio('/static/games/pong/ping.mp3'); function ping() { pingAudio.pause(); diff --git a/static/games/stallman-shooter/assets/binary_blob.webp b/static/games/stallman-shooter/assets/binary_blob.webp new file mode 100644 index 0000000..b769114 Binary files /dev/null and b/static/games/stallman-shooter/assets/binary_blob.webp differ diff --git a/static/games/stallman-shooter/assets/gates.webp b/static/games/stallman-shooter/assets/gates.webp new file mode 100644 index 0000000..6be319e Binary files /dev/null and b/static/games/stallman-shooter/assets/gates.webp differ diff --git a/static/games/stallman-shooter/assets/introduction.webp b/static/games/stallman-shooter/assets/introduction.webp new file mode 100644 index 0000000..f9e2f3d Binary files /dev/null and b/static/games/stallman-shooter/assets/introduction.webp differ diff --git a/static/games/stallman-shooter/assets/loss.webp b/static/games/stallman-shooter/assets/loss.webp new file mode 100644 index 0000000..88bc4d6 Binary files /dev/null and b/static/games/stallman-shooter/assets/loss.webp differ diff --git a/static/games/stallman-shooter/assets/player_pepp.webp b/static/games/stallman-shooter/assets/player_pepp.webp new file mode 100644 index 0000000..a4332a7 Binary files /dev/null and b/static/games/stallman-shooter/assets/player_pepp.webp differ diff --git a/static/games/stallman-shooter/assets/stallman.webp b/static/games/stallman-shooter/assets/stallman.webp new file mode 100644 index 0000000..3b71b82 Binary files /dev/null and b/static/games/stallman-shooter/assets/stallman.webp differ diff --git a/static/games/stallman-shooter/assets/victory.webp b/static/games/stallman-shooter/assets/victory.webp new file mode 100644 index 0000000..17240b9 Binary files /dev/null and b/static/games/stallman-shooter/assets/victory.webp differ diff --git a/static/games/stallman-shooter/constants.js b/static/games/stallman-shooter/constants.js new file mode 100644 index 0000000..dc856d4 --- /dev/null +++ b/static/games/stallman-shooter/constants.js @@ -0,0 +1,2 @@ +export const x_dim = 800; +export const y_dim = 600; diff --git a/static/games/stallman-shooter/gates.js b/static/games/stallman-shooter/gates.js new file mode 100644 index 0000000..c307f1e --- /dev/null +++ b/static/games/stallman-shooter/gates.js @@ -0,0 +1,143 @@ +import * as constants from '/static/games/stallman-shooter/constants.js'; + +export class Gates { + constructor() { + this.x = 200; + this.y = 40; + this.v_x = 0; + this.v_y = 0; + this.a_x = 0; + this.a_y = 0; + this.target_x = 200; + this.target_y = 40; + this.health = 1; + } + + radius() { + return 30; + } + + choose_position() { + const [x, y] = random_position(); + this.target_x = x; + this.target_y = y; + } + + act(count) { + const max_accel = (1 - this.health) * 0.75 + 0.25; + const max_velocity = (1 - this.health) * 5 + 5; + let dx = this.target_x - this.x; + let dy = this.target_y - this.y; + /* If we've arrived (close and stopped) */ + if (Math.sqrt(dx * dx + dy * dy) < max_velocity / max_accel && Math.sqrt(this.v_x * this.v_x + this.v_y * this.v_y) < 0.01) { + /* Then pick a new target */ + this.choose_position(); + + } + + dx = this.target_x - this.x; + dy = this.target_y - this.y; + /* if we're close */ + if (Math.sqrt(dx * dx + dy * dy) < max_velocity / max_accel) { + /* Then slow down */ + const [x, y] = shorten(-this.v_x, -this.v_y, max_accel); + this.a_x = x; + this.a_y = y; + } else { + /* Otherwise, speed up */ + const [x, y] = shorten(dx, dy, max_accel); + this.a_x = x; + this.a_y = y; + } + + /* If we're going too fast */ + if (Math.sqrt(this.v_x * this.v_x + this.v_y * this.v_y) > max_velocity) { + /* Then don't go faster */ + if (Math.sign(this.v_x) == Math.sign(this.a_x)) { + this.a_x = 0; + } + if (Math.sign(this.v_y) == Math.sign(this.a_y)) { + this.a_y = 0; + } + } + + this.v_x += this.a_x; + this.x += this.v_x; + this.v_y += this.a_y; + this.y += this.v_y; + if (count % 20 === 0) { + let num_bullets; + if (this.health >= 0.8) { + num_bullets = 5; + } else if (this.health >= 0.7) { + num_bullets = 6; + } else if (this.health >= 0.5) { + num_bullets = 7; + } else if (this.health >= 0.4) { + num_bullets = 8; + } else if (this.health >= 0.3) { + num_bullets = 9; + } else if (this.health >= 0.2) { + num_bullets = 10; + } else { + num_bullets = 11; + } + const result = []; + for (let i = 0; i < num_bullets; i++) { + result.push(random_bullet(this)); + } + return result; + } else { + return []; + } + } + + injure(damage) { + this.health -= damage; + } + + center() { + return [16, 23]; + } +} + +export class GatesBullet { + constructor(x, d_x, y, d_y) { + this.x = x; + this.y = y; + this.d_x = d_x; + this.d_y = d_y; + } + + radius() { + return 10; + } + + center() { + return [20, 20]; + } +} + +function random_position() { + return [ + Math.random() * (constants.x_dim - 200) + 100, + Math.random() * 100, + ]; +} + +function random_bullet(enemy) { + const multiplier = 2 + 3 * (1 - enemy.health); + const theta = Math.random() * 2 * Math.PI / 3 + Math.PI / 6; + const dx = Math.cos(theta) * multiplier; + const dy = Math.sin(theta) * multiplier; + return new GatesBullet(enemy.x, dx, enemy.y, dy); +} + +function shorten(x, y, maxlen) { + const len = Math.sqrt(x * x + y * y); + if (len > maxlen) { + return [x / len / 2, y / len / 2]; + } else { + return [x, y]; + } +} diff --git a/static/games/stallman-shooter/main.js b/static/games/stallman-shooter/main.js new file mode 100644 index 0000000..d3a3581 --- /dev/null +++ b/static/games/stallman-shooter/main.js @@ -0,0 +1,173 @@ +import * as state from '/static/games/stallman-shooter/state.js'; +import * as constants from '/static/games/stallman-shooter/constants.js'; + +let events = []; + +window.addEventListener('keydown', (e) => events.push(['keydown', e.key])); +window.addEventListener('keyup', (e) => events.push(['keyup', e.key])); + +const duration = 1000 / 60; + +function frame(ctx, st, events, textures) { + const new_bullets = []; + const count = st.count; + for (const enemy of st.enemies) { + new_bullets.push(...enemy.act(count)); + } + for (const bullet of st.bullets) { + state.straight_bullet_act(bullet); + } + for (const bullet of st.player_bullets) { + state.straight_bullet_act(bullet); + } + st.player.act(events); + if (count % st.player.fire_rate === 0) { + st.player_bullets.push(new state.PBullet(st.player.x, 0, st.player.y, -3.5)); + } + for (const bullet of st.bullets) { + if (state.overlaps(st.player, bullet)) { + return 'loss'; + } + } + for (const bullet of new_bullets) { + if (state.overlaps(st.player, bullet)) { + return 'loss'; + } + } + for (const enemy of st.enemies) { + for (const bullet of st.player_bullets) { + if (state.overlaps(enemy, bullet)) { + enemy.injure(bullet.damage()); + state.straight_bullet_delete(bullet); + } + } + } + st.enemies = st.enemies.filter((enemy) => enemy.health > 0); + if (st.enemies.length === 0) { + return 'win'; + } + st.bullets = st.bullets.filter((bullet) => !state.outside(bullet)); + st.bullets.push(...new_bullets); + st.player_bullets = st.player_bullets.filter((bullet) => !state.outside(bullet)); + + for (const bullet of st.player_bullets) { + state.draw(bullet, ctx, textures.pbullet); + } + for (const enemy of st.enemies) { + state.draw(enemy, ctx, textures.enemy); + } + state.draw(st.player, ctx, textures.player); + for (const bullet of st.bullets) { + state.draw(bullet, ctx, textures.ebullet); + } + ctx.strokeStyle = "red"; + ctx.strokeRect(0, 0, Math.round(st.enemies[0].health * constants.x_dim), 20); + st.count++; + return 'nothing'; +} + +async function get_bitmap(id) { + const image = document.getElementById(id); + await image.decode(); + return await window.createImageBitmap(image); +} + +async function get_player_bitmap() { + return get_bitmap('stallman'); +} + +async function get_enemy_bitmap() { + return get_bitmap('gates'); +} + +async function get_ebullet_bitmap() { + return get_bitmap('binary-blob'); +} + +async function get_pbullet_surface() { + return get_bitmap('player-pepp'); +} + +async function generic_message(ctx, image, okay) { + const bitmap = await get_bitmap(image); + while (true) { + await new Promise(window.requestAnimationFrame); + const ev = events; + events = []; + for (const event of ev) { + const [type, keycode] = event; + if (type === 'keydown' && keycode === okay) { + return; + } + } + ctx.drawImage(bitmap, 0, 0); + } +} + +async function introduction(ctx) { + await generic_message(ctx, 'introduction', 'Enter'); +} + +async function loss(ctx) { + await generic_message(ctx, 'loss', 'r'); +} + +async function victory(ctx) { + await generic_message(ctx, 'victory', 'r'); +} + +async function main() { + const canvas = document.getElementById('canvas'); + const ctx = canvas.getContext('2d'); + + await introduction(ctx); + + while (true) { + const st = new state.State(); + const [ + player_bitmap, + enemy_bitmap, + ebullet_bitmap, + pbullet_bitmap, + ] = await Promise.all([ + get_bitmap('stallman'), + get_bitmap('gates'), + get_bitmap('binary-blob'), + get_bitmap('player-pepp'), + ]); + const textures = { + player: player_bitmap, + enemy: enemy_bitmap, + ebullet: ebullet_bitmap, + pbullet: pbullet_bitmap, + }; + let last_frame_time = null; + let accumulator = 0; + game: while (true) { + const now = await new Promise(requestAnimationFrame); + if (last_frame_time !== null) { + accumulator += now - last_frame_time; + } + last_frame_time = now; + + while (accumulator >= duration) { + accumulator -= duration; + + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, constants.x_dim, constants.y_dim); + const result = frame(ctx, st, events, textures); + events = []; + if (result === 'loss') { + await loss(ctx); + break game; + } + if (result === 'win') { + await victory(ctx); + break game; + } + } + } + } +} + +main() diff --git a/static/games/stallman-shooter/player.js b/static/games/stallman-shooter/player.js new file mode 100644 index 0000000..83144e0 --- /dev/null +++ b/static/games/stallman-shooter/player.js @@ -0,0 +1,88 @@ +import * as constants from '/static/games/stallman-shooter/constants.js'; + +export class Player { + constructor() { + this.x = 400; + this.y = 500; + this.x_d = 0; + this.y_d = 0; + this.down_p = false; + this.left_p = false; + this.right_p = false; + this.up_p = false; + this.fire_rate = 15; + this.shift_p = false; + } + + act(events) { + for (const event of events) { + const [kind, key] = event; + if (kind === 'keydown' && key === 'ArrowDown') { + this.down_p = true; + this.y_d = -1; + } else if (kind === 'keyup' && key === 'ArrowDown') { + this.down_p = false; + } else if (kind === 'keydown' && key === 'ArrowUp') { + this.up_p = true; + this.y_d = 1; + } else if (kind === 'keyup' && key === 'ArrowUp') { + this.up_p = false; + } else if (kind === 'keydown' && key === 'ArrowLeft') { + this.left_p = true; + this.x_d = -1; + } else if (kind === 'keyup' && key === 'ArrowLeft') { + this.left_p = false; + } else if (kind === 'keydown' && key === 'ArrowRight') { + this.right_p = true; + this.x_d = 1; + } else if (kind === 'keyup' && key === 'ArrowRight') { + this.right_p = false; + } else if (kind === 'keydown' && key === 'Shift') { + this.shift_p = true; + } else if (kind === 'keyup' && key === 'Shift') { + this.shift_p = false; + } + } + if (this.left_p) { + if (!this.right_p) { + this.x_d = -1; + } + } else if (this.right_p) { + this.x_d = 1; + } else { + this.x_d = 0; + } + if (this.up_p) { + if (!this.down_p) { + this.y_d = -1; + } + } else if (this.down_p) { + this.y_d = 1; + } else { + this.y_d = 0; + } + /* Don't go off the side */ + if (this.x_d < 0 && this.x <= 0 || this.x >= constants.x_dim && this.x_d > 0) { + this.x_d = 0; + } + if (this.y <= 0 && this.y_d < 0 || this.y >= constants.y_dim && this.y_d > 0) { + this.y_d = 0; + } + + if (this.shift_p) { + this.x += this.x_d; + this.y += this.y_d; + } else { + this.x += this.x_d * 3; + this.y += this.y_d * 3; + } + } + + radius() { + return 3; + } + + center() { + return [33, 21]; + } +} diff --git a/static/games/stallman-shooter/state.js b/static/games/stallman-shooter/state.js new file mode 100644 index 0000000..f184c3e --- /dev/null +++ b/static/games/stallman-shooter/state.js @@ -0,0 +1,82 @@ +import * as player from '/static/games/stallman-shooter/player.js'; +import * as gates from '/static/games/stallman-shooter/gates.js'; +import * as constants from '/static/games/stallman-shooter/constants.js'; + +export function straight_bullet_act(straight_bullet) { + const x = straight_bullet.x; + const d_x = straight_bullet.d_x; + straight_bullet.x += d_x; + const y = straight_bullet.y; + const d_y = straight_bullet.d_y; + straight_bullet.y += d_y; +} + +export function straight_bullet_delete(straight_bullet) { + straight_bullet.x = -1000; // LMAO +} + +export class PBullet { + constructor(x, d_x, y, d_y) { + this.x = x; + this.y = y; + this.d_x = d_x; + this.d_y = d_y; + } + + damage() { + return 0.01 + } + + radius() { + return 10; + } + + center() { + return [16, 22]; + } +} + +export class State { + constructor() { + this.player = new player.Player(); + this.enemies = [new gates.Gates()]; + this.bullets = []; + this.count = 0; + this.player_bullets = []; + } +} + +function dist(x1, y1, x2, y2) { + const xdist = x1 - x2; + const ydist = y1 - y2; + return Math.sqrt(xdist * xdist + ydist * ydist); +} + +export function overlaps(self, other) { + const sx = self.x; + const ox = other.x; + const sy = self.y; + const oy = other.y; + const threshold = self.radius() + other.radius(); + if (Math.abs(sx - ox) >= threshold || Math.abs(sy - oy) >= threshold) { + return false; + } + const d = dist(sx, sy, ox, oy); + return d < threshold; +} + +export function outside(positioned) { + const x = positioned.x; + const y = positioned.y; + return x < -100 + || x > constants.x_dim + 100 + || y < -100 + || y > constants.y_dim + 100; +} + +export function draw(positioned, ctx, bitmap) { + const [cx, cy] = positioned.center(); + const x0 = Math.round(positioned.x) - cx; + const y0 = Math.round(positioned.y) - cy; + ctx.drawImage(bitmap, x0, y0); +} -- cgit v1.3.1