36
Asteroids
arrows steer + thrust ยท space shoots
idle
238 lines ยท vanilla
view source
let W = 0, H = 0;
let ship, asteroids, bullets, particles;
let score, lives, gameOver, respawnTimer;
function rand(a, b) { return a + Math.random() * (b - a); }
function makeShip() {
return { x: W / 2, y: H / 2, vx: 0, vy: 0, a: -Math.PI / 2, thrust: false, invuln: 2.0 };
}
function makeAsteroid(x, y, r) {
const a = rand(0, Math.PI * 2);
const sp = rand(20, 60) * (40 / Math.max(20, r));
const verts = 8 + ((Math.random() * 5) | 0);
const shape = [];
for (let i = 0; i < verts; i++) shape.push(rand(0.75, 1.15));
return {
x, y,
vx: Math.cos(a) * sp,
vy: Math.sin(a) * sp,
r,
rot: rand(-1, 1),
ang: rand(0, Math.PI * 2),
shape,
};
}
function spawnWave(n) {
asteroids.length = 0;
for (let i = 0; i < n; i++) {
let x, y;
do {
x = Math.random() * W;
y = Math.random() * H;
} while (Math.hypot(x - W / 2, y - H / 2) < 120);
asteroids.push(makeAsteroid(x, y, 40));
}
}
function reset() {
ship = makeShip();
bullets = [];
particles = [];
score = 0;
lives = 3;
gameOver = false;
respawnTimer = 0;
spawnWave(4);
}
function init({ width, height }) {
W = width; H = height;
reset();
}
function wrap(p) {
if (p.x < 0) p.x += W; else if (p.x > W) p.x -= W;
if (p.y < 0) p.y += H; else if (p.y > H) p.y -= H;
}
function explode(x, y, n, col) {
for (let i = 0; i < n; i++) {
const a = rand(0, Math.PI * 2);
const s = rand(40, 180);
particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life: rand(0.4, 1.0), col });
}
}
function splitAsteroid(i) {
const a = asteroids[i];
score += a.r > 30 ? 20 : a.r > 18 ? 50 : 100;
explode(a.x, a.y, 14, "#fff");
asteroids.splice(i, 1);
if (a.r > 18) {
const nr = a.r * 0.55;
asteroids.push(makeAsteroid(a.x, a.y, nr));
asteroids.push(makeAsteroid(a.x, a.y, nr));
}
}
function tick({ ctx, dt, width, height, input }) {
if (width !== W || height !== H) { W = width; H = height; }
if (dt > 0.05) dt = 0.05;
ctx.fillStyle = "rgba(0,0,0,0.35)";
ctx.fillRect(0, 0, W, H);
if (gameOver) {
for (const c of input.consumeClicks()) { if (c) { reset(); break; } }
} else {
if (input.keyDown("ArrowLeft")) ship.a -= 3.5 * dt;
if (input.keyDown("ArrowRight")) ship.a += 3.5 * dt;
ship.thrust = input.keyDown("ArrowUp");
if (ship.thrust) {
const ax = Math.cos(ship.a) * 180;
const ay = Math.sin(ship.a) * 180;
ship.vx += ax * dt;
ship.vy += ay * dt;
particles.push({
x: ship.x - Math.cos(ship.a) * 10,
y: ship.y - Math.sin(ship.a) * 10,
vx: -Math.cos(ship.a) * 100 + rand(-30, 30),
vy: -Math.sin(ship.a) * 100 + rand(-30, 30),
life: 0.4, col: "#ff9a3c",
});
}
ship.vx *= 0.995;
ship.vy *= 0.995;
ship.x += ship.vx * dt;
ship.y += ship.vy * dt;
wrap(ship);
if (ship.invuln > 0) ship.invuln -= dt;
if (input.justPressed(" ") || input.justPressed("Space") || input.justPressed("Spacebar")) {
bullets.push({
x: ship.x + Math.cos(ship.a) * 12,
y: ship.y + Math.sin(ship.a) * 12,
vx: Math.cos(ship.a) * 420 + ship.vx,
vy: Math.sin(ship.a) * 420 + ship.vy,
life: 1.0,
});
}
}
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.x += b.vx * dt; b.y += b.vy * dt;
wrap(b);
b.life -= dt;
if (b.life <= 0) bullets.splice(i, 1);
}
for (const a of asteroids) {
a.x += a.vx * dt; a.y += a.vy * dt;
a.ang += a.rot * dt;
wrap(a);
}
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt; p.y += p.vy * dt;
p.vx *= 0.96; p.vy *= 0.96;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
// Bullet vs asteroid
for (let i = asteroids.length - 1; i >= 0; i--) {
const a = asteroids[i];
for (let j = bullets.length - 1; j >= 0; j--) {
const b = bullets[j];
if (Math.hypot(a.x - b.x, a.y - b.y) < a.r) {
bullets.splice(j, 1);
splitAsteroid(i);
break;
}
}
}
// Ship vs asteroid
if (!gameOver && ship.invuln <= 0) {
for (const a of asteroids) {
if (Math.hypot(a.x - ship.x, a.y - ship.y) < a.r + 8) {
explode(ship.x, ship.y, 30, "#7ad1ff");
lives -= 1;
if (lives <= 0) {
gameOver = true;
} else {
ship = makeShip();
}
break;
}
}
}
if (!gameOver && asteroids.length === 0) {
spawnWave(Math.min(8, 4 + ((score / 500) | 0)));
}
// --- Render ---
ctx.lineWidth = 1.5;
ctx.strokeStyle = "#cfd6e4";
for (const a of asteroids) {
ctx.beginPath();
for (let k = 0; k < a.shape.length; k++) {
const th = a.ang + (k / a.shape.length) * Math.PI * 2;
const r = a.r * a.shape[k];
const px = a.x + Math.cos(th) * r;
const py = a.y + Math.sin(th) * r;
if (k === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.stroke();
}
for (const b of bullets) {
ctx.fillStyle = "#ffd17a";
ctx.fillRect(b.x - 1.5, b.y - 1.5, 3, 3);
}
for (const p of particles) {
ctx.fillStyle = p.col;
ctx.globalAlpha = Math.max(0, Math.min(1, p.life));
ctx.fillRect(p.x - 1, p.y - 1, 2, 2);
}
ctx.globalAlpha = 1;
if (!gameOver) {
const blink = ship.invuln > 0 ? (Math.floor(ship.invuln * 12) % 2 === 0) : true;
if (blink) {
ctx.save();
ctx.translate(ship.x, ship.y);
ctx.rotate(ship.a);
ctx.strokeStyle = "#7ad1ff";
ctx.beginPath();
ctx.moveTo(12, 0);
ctx.lineTo(-8, 7);
ctx.lineTo(-4, 0);
ctx.lineTo(-8, -7);
ctx.closePath();
ctx.stroke();
if (ship.thrust && Math.random() < 0.7) {
ctx.strokeStyle = "#ff9a3c";
ctx.beginPath();
ctx.moveTo(-4, 0);
ctx.lineTo(-12 - Math.random() * 4, 0);
ctx.stroke();
}
ctx.restore();
}
}
// HUD
ctx.fillStyle = "rgba(0,0,0,0.55)";
ctx.fillRect(10, 10, 180, 56);
ctx.fillStyle = "#fff";
ctx.font = "14px monospace";
ctx.textAlign = "left";
ctx.textBaseline = "alphabetic";
ctx.fillText(`SCORE ${score}`, 20, 30);
ctx.fillText(`LIVES`, 20, 52);
for (let i = 0; i < lives; i++) {
const cx = 80 + i * 18, cy = 47;
ctx.strokeStyle = "#7ad1ff";
ctx.beginPath();
ctx.moveTo(cx + 7, cy);
ctx.lineTo(cx - 5, cy + 5);
ctx.lineTo(cx - 2, cy);
ctx.lineTo(cx - 5, cy - 5);
ctx.closePath();
ctx.stroke();
}
if (gameOver) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(W / 2 - 140, H / 2 - 50, 280, 100);
ctx.strokeStyle = "#7ad1ff";
ctx.strokeRect(W / 2 - 140, H / 2 - 50, 280, 100);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "bold 22px ui-sans-serif, system-ui";
ctx.fillText("GAME OVER", W / 2, H / 2 - 10);
ctx.font = "14px monospace";
ctx.fillText(`final score ${score}`, W / 2, H / 2 + 14);
ctx.fillText("click to restart", W / 2, H / 2 + 34);
}
}
Comments (0)
Log in to comment.