From 00ad946ea42a692188519b0facd53f6aab3eb153 Mon Sep 17 00:00:00 2001 From: michael <1+michael@noreply.git.casaderoll.de> Date: Thu, 24 Sep 2026 10:09:55 +0200 Subject: [PATCH] Add Asteroid Field game (asteroids clone with UFOs) --- public/js/games/asteroid.js | 219 ++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 public/js/games/asteroid.js diff --git a/public/js/games/asteroid.js b/public/js/games/asteroid.js new file mode 100644 index 0000000..bcfacf2 --- /dev/null +++ b/public/js/games/asteroid.js @@ -0,0 +1,219 @@ +/* ============================================================ + ASTEROID FIELD — Asteroids clone + ============================================================ */ +(function () { + 'use strict'; + const { Game, STATE } = window.DSP; + const S = window.DSP_SPRITES; + + const W = 800, H = 600; + + class AsteroidField extends Game { + constructor(canvas) { + super({ name: 'asteroid', title: 'ASTEROID FIELD', width: W, height: H, canvas, + loadSteps: ['LOADING ROCKS', 'CHARGING THRUSTERS', 'SCANNING DEBRIS', 'SYNCING GRID'] }); + this.ship = null; + this.bullets = []; + this.asteroids = []; + this.ufos = []; + this.stars = []; + for (let i = 0; i < 80; i++) this.stars.push({ x: Math.random() * W, y: Math.random() * H, s: Math.random() < 0.2 ? 2 : 1, v: 0.2 + Math.random() * 0.5 }); + this.respawnTimer = 0; + } + reset() { + this.bullets = []; this.asteroids = []; this.ufos = []; + this.spawnWave(); + this.respawn(); + } + respawn() { + this.ship = { x: W / 2, y: H / 2, a: -Math.PI / 2, vx: 0, vy: 0, thrust: false, inv: 120 }; + this.respawnTimer = 0; + } + spawnWave() { + const n = 3 + this.level; + for (let i = 0; i < n; i++) this.spawnAsteroid(3); + if (this.level >= 2) this.ufos.push({ x: -30, y: Math.random() * H, vx: 1.2, vy: 0, t: 0, fire: 60 }); + } + spawnAsteroid(size, x, y) { + const speed = (4 - size) * 0.6 + Math.random() * 0.8 + this.level * 0.1; + const a = Math.random() * Math.PI * 2; + this.asteroids.push({ + x: x !== undefined ? x : (Math.random() < 0.5 ? (Math.random() < 0.5 ? -20 : W + 20) : Math.random() * W), + y: y !== undefined ? y : (Math.random() < 0.5 ? (Math.random() < 0.5 ? -20 : H + 20) : Math.random() * H), + vx: Math.cos(a) * speed, vy: Math.sin(a) * speed, + size, rot: Math.random() * Math.PI * 2, vr: (Math.random() - 0.5) * 0.05, + verts: Array.from({ length: 9 }, () => 0.7 + Math.random() * 0.5), + }); + } + wrap(o) { + if (o.x < -30) o.x = W + 30; if (o.x > W + 30) o.x = -30; + if (o.y < -30) o.y = H + 30; if (o.y > H + 30) o.y = -30; + } + update() { + const sh = this.ship; + for (const s of this.stars) { s.x -= s.v; if (s.x < 0) s.x = W; } + + if (this.respawnTimer > 0) { this.respawnTimer--; if (this.respawnTimer === 0) this.respawn(); } + else if (sh) { + const left = this.input.down('ArrowLeft', 'KeyA'); + const right = this.input.down('ArrowRight', 'KeyD'); + const thrust = this.input.down('ArrowUp', 'KeyW'); + if (left) sh.a -= 0.06; + if (right) sh.a += 0.06; + if (thrust) { + sh.vx += Math.cos(sh.a) * 0.12; + sh.vy += Math.sin(sh.a) * 0.12; + if (this.time % 4 === 0) { + window.DSP_AUDIO.sfx('thrust'); + this.particles.burst(sh.x - Math.cos(sh.a) * 12, sh.y - Math.sin(sh.a) * 12, '#ff9d2d', 2, 1.5, 15); + } + } + sh.vx *= 0.99; sh.vy *= 0.99; + sh.x += sh.vx; sh.y += sh.vy; + this.wrap(sh); + if (sh.inv > 0) sh.inv--; + if (this.input.wasPressed('Space') && this.bullets.length < 6) { + this.bullets.push({ x: sh.x + Math.cos(sh.a) * 12, y: sh.y + Math.sin(sh.a) * 12, vx: Math.cos(sh.a) * 6 + sh.vx, vy: Math.sin(sh.a) * 6 + sh.vy, life: 60 }); + window.DSP_AUDIO.sfx('shoot'); + } + if (this.input.wasPressed('KeyX')) { + window.DSP_AUDIO.sfx('jump'); + this.particles.burst(sh.x, sh.y, '#7df9ff', 16, 3, 25); + sh.x = Math.random() * W; sh.y = Math.random() * H; + sh.inv = 90; + } + } + + for (let i = this.bullets.length - 1; i >= 0; i--) { + const b = this.bullets[i]; + b.x += b.vx; b.y += b.vy; + this.wrap(b); + if (--b.life <= 0) { this.bullets.splice(i, 1); continue; } + let hit = false; + for (let j = this.asteroids.length - 1; j >= 0; j--) { + const a = this.asteroids[j]; + const r = a.size * 10; + if ((b.x - a.x) ** 2 + (b.y - a.y) ** 2 < r * r) { + this.hitAsteroid(j); + hit = true; + break; + } + } + if (!hit) for (let j = this.ufos.length - 1; j >= 0; j--) { + const u = this.ufos[j]; + if (Math.abs(b.x - u.x) < 18 && Math.abs(b.y - u.y) < 10) { + this.ufos.splice(j, 1); + this.addScore(200); + window.DSP_AUDIO.sfx('explode'); + this.particles.burst(u.x, u.y, '#9d7dff', 20, 3, 30); + this.shake.trigger(4, 15); + hit = true; + break; + } + } + if (hit) this.bullets.splice(i, 1); + } + + for (const a of this.asteroids) { + a.x += a.vx; a.y += a.vy; a.rot += a.vr; + this.wrap(a); + if (sh && sh.inv <= 0) { + const r = a.size * 10 + 8; + if ((sh.x - a.x) ** 2 + (sh.y - a.y) ** 2 < r * r) this.killShip(); + } + } + + for (let i = this.ufos.length - 1; i >= 0; i--) { + const u = this.ufos[i]; + u.x += u.vx; u.t++; + if (u.x < -40 || u.x > W + 40) this.ufos.splice(i, 1); + if (u.t % 90 === 0 && sh) { + const ang = Math.atan2(sh.y - u.y, sh.x - u.x); + this.bullets.push({ x: u.x, y: u.y, vx: Math.cos(ang) * 3, vy: Math.sin(ang) * 3, life: 120, enemy: true }); + window.DSP_AUDIO.sfx('laser'); + } + } + for (let i = this.bullets.length - 1; i >= 0; i--) { + const b = this.bullets[i]; + if (!b.enemy) continue; + if (sh && sh.inv <= 0 && (b.x - sh.x) ** 2 + (b.y - sh.y) ** 2 < 100) { + this.bullets.splice(i, 1); + this.killShip(); + } + } + + if (this.asteroids.length === 0 && this.ufos.length === 0) { + this.level++; + window.DSP_AUDIO.sfx('levelup'); + this.spawnWave(); + } + } + hitAsteroid(j) { + const a = this.asteroids[j]; + this.asteroids.splice(j, 1); + this.addScore(a.size === 3 ? 20 : a.size === 2 ? 50 : 100); + window.DSP_AUDIO.sfx('explode'); + this.particles.burst(a.x, a.y, '#c9b8ff', 14, 2.5, 25); + this.shake.trigger(a.size === 3 ? 5 : 3, 12); + if (a.size > 1) { + this.spawnAsteroid(a.size - 1, a.x, a.y); + this.spawnAsteroid(a.size - 1, a.x, a.y); + } + } + killShip() { + if (!this.ship) return; + window.DSP_AUDIO.sfx('bigboom'); + this.particles.burst(this.ship.x, this.ship.y, '#ff4d6d', 30, 4, 40); + this.shake.trigger(8, 25); + this.lives--; + this.ship = null; + if (this.lives <= 0) this.gameOver(); + else this.respawnTimer = 90; + } + render() { + const ctx = this.ctx; + ctx.fillStyle = '#05010d'; + ctx.fillRect(0, 0, W, H); + for (const s of this.stars) { + ctx.fillStyle = s.s > 1 ? '#7df9ff' : '#4a3f6b'; + ctx.fillRect(s.x, s.y, s.s, s.s); + } + for (const a of this.asteroids) { + ctx.save(); + ctx.translate(a.x, a.y); + ctx.rotate(a.rot); + ctx.strokeStyle = '#c9b8ff'; + ctx.lineWidth = 2; + ctx.beginPath(); + const r = a.size * 10; + for (let i = 0; i < a.verts.length; i++) { + const ang = (i / a.verts.length) * Math.PI * 2; + const rr = r * a.verts[i]; + const px = Math.cos(ang) * rr, py = Math.sin(ang) * rr; + i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py); + } + ctx.closePath(); + ctx.stroke(); + ctx.restore(); + } + for (const u of this.ufos) { + S.draw(ctx, S.get('ufo', 2), u.x, u.y); + } + for (const b of this.bullets) { + ctx.fillStyle = b.enemy ? '#ff4d6d' : '#ffe14d'; + ctx.fillRect(b.x - 1.5, b.y - 1.5, 3, 3); + } + const sh = this.ship; + if (sh && (sh.inv <= 0 || Math.floor(this.time / 4) % 2 === 0)) { + ctx.save(); + ctx.translate(sh.x, sh.y); + ctx.rotate(sh.a + Math.PI / 2); + S.draw(ctx, S.get('ship', 2), 0, 0); + ctx.restore(); + } + } + } + + window.DSP_ACTIVE_GAME = new AsteroidField(document.getElementById('game')); + window.DSP_AUDIO.playMusic('asteroid'); +})(); \ No newline at end of file