const x_dim = 800; const y_dim = 600; 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]; } } 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() * (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]; } } 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 >= x_dim && this.x_d > 0) { this.x_d = 0; } if (this.y <= 0 && this.y_d < 0 || this.y >= 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]; } } 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; } function straight_bullet_delete(straight_bullet) { straight_bullet.x = -1000; // LMAO } 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]; } } class State { constructor() { this.player = new Player(); this.enemies = [new 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); } 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; } function outside(positioned) { const x = positioned.x; const y = positioned.y; return x < -100 || x > x_dim + 100 || y < -100 || y > y_dim + 100; } 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); } 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) { straight_bullet_act(bullet); } for (const bullet of st.player_bullets) { straight_bullet_act(bullet); } st.player.act(events); if (count % st.player.fire_rate === 0) { st.player_bullets.push(new PBullet(st.player.x, 0, st.player.y, -3.5)); } for (const bullet of st.bullets) { if (overlaps(st.player, bullet)) { return 'loss'; } } for (const bullet of new_bullets) { if (overlaps(st.player, bullet)) { return 'loss'; } } for (const enemy of st.enemies) { for (const bullet of st.player_bullets) { if (overlaps(enemy, bullet)) { enemy.injure(bullet.damage()); 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) => !outside(bullet)); st.bullets.push(...new_bullets); st.player_bullets = st.player_bullets.filter((bullet) => !outside(bullet)); for (const bullet of st.player_bullets) { draw(bullet, ctx, textures.pbullet); } for (const enemy of st.enemies) { draw(enemy, ctx, textures.enemy); } draw(st.player, ctx, textures.player); for (const bullet of st.bullets) { draw(bullet, ctx, textures.ebullet); } ctx.strokeStyle = "red"; ctx.strokeRect(0, 0, Math.round(st.enemies[0].health * x_dim), 20); st.count++; return 'nothing'; } const sprite_sheet = document.getElementById('sprite-sheet'); async function get_bitmap(off, w, h) { await sprite_sheet.decode(); return await window.createImageBitmap(sprite_sheet, off, 0, w, h); } async function get_player_bitmap() { return get_bitmap(1707, 61, 46); } async function get_enemy_bitmap() { return get_bitmap(38, 32, 40); } async function get_ebullet_bitmap() { return get_bitmap(0, 37, 39); } async function get_pbullet_bitmap() { return get_bitmap(1673, 33, 40); } async function generic_message(ctx, image, okay) { const bitmap = await get_bitmap(image, 800, 600); await new Promise((resolve) => { const loop = () => { const ev = events; events = []; for (const event of ev) { const [type, keycode] = event; if (type === 'keydown' && keycode === okay) { resolve(); return; } } ctx.drawImage(bitmap, 0, 0); window.requestAnimationFrame(loop); }; window.requestAnimationFrame(loop); }); } async function introduction(ctx) { await generic_message(ctx, 71, 'Enter'); } async function loss(ctx) { await generic_message(ctx, 872, 'r'); } async function victory(ctx) { await generic_message(ctx, 1769, 'r'); } async function main() { const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); await introduction(ctx); while (true) { const st = new State(); const [ player_bitmap, enemy_bitmap, ebullet_bitmap, pbullet_bitmap, ] = await Promise.all([ get_player_bitmap(), get_enemy_bitmap(), get_ebullet_bitmap(), get_pbullet_bitmap(), ]); const textures = { player: player_bitmap, enemy: enemy_bitmap, ebullet: ebullet_bitmap, pbullet: pbullet_bitmap, }; let last_frame_time = null; let accumulator = 0; const result = await new Promise((resolve) => { const loop = (now) => { 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, x_dim, y_dim); const result = frame(ctx, st, events, textures); events = []; if (result === 'loss' || result === 'win') { resolve(result); return; } } window.requestAnimationFrame(loop); }; window.requestAnimationFrame(loop); }); if (result === 'loss') { await loss(ctx); } else if (result === 'win') { await victory(ctx); } } } main()