/* ============================================================ 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'); })();