Add Asteroid Field game (asteroids clone with UFOs)

This commit is contained in:
michael committed 2026-09-24 10:09:55 +02:00
1 parent 78f75a866b
commit 00ad946ea4
1 file changed
+219
+219
View File
@@ -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');
})();