42
Space Invaders
arrows + space to shoot
idle
253 lines ยท vanilla
view source
let W = 0, H = 0;
let cannon, aliens, dir, bullets, eBullets, bunkers, ufo, particles;
let score, lives, gameOver, win, stepTimer, shootTimer, ufoTimer, formationDrop;
const COLS = 10, ROWS = 5;
const ALIEN_W = 22, ALIEN_H = 14, ALIEN_SX = 32, ALIEN_SY = 22;
const ALIEN_POINTS = [40, 20, 20, 10, 10];
function rand(a, b) { return a + Math.random() * (b - a); }
function buildFormation() {
aliens = [];
const startX = (W - (COLS - 1) * ALIEN_SX) / 2;
const startY = 60;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
aliens.push({ x: startX + c * ALIEN_SX, y: startY + r * ALIEN_SY, row: r, alive: true });
}
}
dir = 1;
stepTimer = 0;
shootTimer = rand(0.8, 1.6);
}
function buildBunkers() {
bunkers = [];
const N = 4, BW = 44, BH = 22;
const gap = (W - N * BW) / (N + 1);
const by = H - 90;
for (let i = 0; i < N; i++) {
const bx = gap + i * (BW + gap);
const cells = new Uint8Array(11 * 6);
for (let y = 0; y < 6; y++) for (let x = 0; x < 11; x++) {
// notch out the bottom-center to look like the original
if (y >= 4 && x >= 4 && x <= 6) continue;
cells[y * 11 + x] = 1;
}
bunkers.push({ x: bx, y: by, cells });
}
}
function reset() {
cannon = { x: W / 2, y: H - 30, w: 28, h: 10, cool: 0 };
bullets = [];
eBullets = [];
particles = [];
ufo = null;
ufoTimer = rand(12, 20);
score = 0;
lives = 3;
gameOver = false;
win = false;
formationDrop = 0;
buildFormation();
buildBunkers();
}
function init({ width, height }) {
W = width; H = height;
reset();
}
function explode(x, y, n, col) {
for (let i = 0; i < n; i++) {
const a = rand(0, Math.PI * 2), s = rand(30, 140);
particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, life: rand(0.3, 0.7), col });
}
}
function aliveCount() { let n = 0; for (const a of aliens) if (a.alive) n++; return n; }
function hitBunker(bx, by) {
for (const b of bunkers) {
if (bx < b.x || bx > b.x + 44 || by < b.y || by > b.y + 22) continue;
const cx = Math.floor((bx - b.x) / 4);
const cy = Math.floor((by - b.y) / 4);
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
const x = cx + dx, y = cy + dy;
if (x >= 0 && x < 11 && y >= 0 && y < 6) b.cells[y * 11 + x] = 0;
}
return true;
}
return false;
}
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.55)";
ctx.fillRect(0, 0, W, H);
if (gameOver || win) {
for (const c of input.consumeClicks()) { if (c) { reset(); break; } }
} else {
const sp = 220;
if (input.keyDown("ArrowLeft")) cannon.x -= sp * dt;
if (input.keyDown("ArrowRight")) cannon.x += sp * dt;
cannon.x = Math.max(cannon.w / 2 + 4, Math.min(W - cannon.w / 2 - 4, cannon.x));
cannon.cool -= dt;
if (cannon.cool <= 0 && (input.justPressed(" ") || input.justPressed("Space") || input.justPressed("Spacebar"))) {
bullets.push({ x: cannon.x, y: cannon.y - 8, vy: -380 });
cannon.cool = 0.45;
}
// formation step
const n = aliveCount();
const speed = 0.9 - 0.75 * (1 - n / (COLS * ROWS));
stepTimer -= dt;
if (stepTimer <= 0) {
stepTimer = Math.max(0.06, speed * 0.55);
let minX = 1e9, maxX = -1e9, maxY = -1e9;
for (const a of aliens) if (a.alive) {
a.x += dir * 6;
if (a.x < minX) minX = a.x;
if (a.x + ALIEN_W > maxX) maxX = a.x + ALIEN_W;
if (a.y + ALIEN_H > maxY) maxY = a.y + ALIEN_H;
}
if (minX < 12 || maxX > W - 12) {
dir = -dir;
for (const a of aliens) if (a.alive) a.y += 12;
formationDrop++;
}
if (maxY >= cannon.y - 8) { gameOver = true; }
}
// alien shooting (front-row only)
shootTimer -= dt;
if (shootTimer <= 0) {
shootTimer = rand(0.5, 1.4) * Math.max(0.4, n / (COLS * ROWS));
const front = new Map();
for (const a of aliens) if (a.alive) {
const k = Math.round(a.x);
const prev = front.get(k);
if (!prev || a.y > prev.y) front.set(k, a);
}
const arr = [...front.values()];
if (arr.length) {
const s = arr[(Math.random() * arr.length) | 0];
eBullets.push({ x: s.x + ALIEN_W / 2, y: s.y + ALIEN_H, vy: 180 + formationDrop * 8 });
}
}
// UFO
if (!ufo) {
ufoTimer -= dt;
if (ufoTimer <= 0) {
const fromLeft = Math.random() < 0.5;
ufo = { x: fromLeft ? -30 : W + 30, y: 28, vx: fromLeft ? 110 : -110, value: [50, 100, 150, 200, 300][(Math.random() * 5) | 0] };
ufoTimer = rand(14, 24);
}
} else {
ufo.x += ufo.vx * dt;
if (ufo.x < -40 || ufo.x > W + 40) ufo = null;
}
if (n === 0) win = true;
}
// bullets up
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.y += b.vy * dt;
if (b.y < 0) { bullets.splice(i, 1); continue; }
if (hitBunker(b.x, b.y)) { bullets.splice(i, 1); explode(b.x, b.y, 4, "#9fe870"); continue; }
let hit = false;
for (const a of aliens) {
if (!a.alive) continue;
if (b.x >= a.x && b.x <= a.x + ALIEN_W && b.y >= a.y && b.y <= a.y + ALIEN_H) {
a.alive = false;
score += ALIEN_POINTS[a.row];
explode(a.x + ALIEN_W / 2, a.y + ALIEN_H / 2, 12, "#9fe870");
hit = true; break;
}
}
if (hit) { bullets.splice(i, 1); continue; }
if (ufo && b.y <= ufo.y + 8 && b.y >= ufo.y - 8 && b.x >= ufo.x - 18 && b.x <= ufo.x + 18) {
score += ufo.value;
explode(ufo.x, ufo.y, 24, "#ff6ab0");
ufo = null;
bullets.splice(i, 1);
}
}
// enemy bullets down
for (let i = eBullets.length - 1; i >= 0; i--) {
const b = eBullets[i];
b.y += b.vy * dt;
if (b.y > H) { eBullets.splice(i, 1); continue; }
if (hitBunker(b.x, b.y)) { eBullets.splice(i, 1); explode(b.x, b.y, 4, "#ffd17a"); continue; }
if (!gameOver && b.x >= cannon.x - cannon.w / 2 && b.x <= cannon.x + cannon.w / 2 && b.y >= cannon.y - cannon.h / 2 && b.y <= cannon.y + cannon.h / 2) {
eBullets.splice(i, 1);
explode(cannon.x, cannon.y, 28, "#7ad1ff");
lives -= 1;
if (lives <= 0) gameOver = true;
}
}
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.95; p.vy *= 0.95;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
// --- Render ---
// bunkers
ctx.fillStyle = "#9fe870";
for (const b of bunkers) {
for (let y = 0; y < 6; y++) for (let x = 0; x < 11; x++) {
if (b.cells[y * 11 + x]) ctx.fillRect(b.x + x * 4, b.y + y * 4, 4, 4);
}
}
// aliens
const blink = Math.floor(stepTimer * 6) % 2 === 0;
for (const a of aliens) {
if (!a.alive) continue;
ctx.fillStyle = a.row === 0 ? "#ff6ab0" : a.row < 3 ? "#7ad1ff" : "#9fe870";
ctx.fillRect(a.x + 2, a.y + 2, ALIEN_W - 4, ALIEN_H - 4);
ctx.fillRect(a.x, a.y + 4, ALIEN_W, ALIEN_H - 8);
if (blink) {
ctx.fillRect(a.x - 2, a.y + ALIEN_H, 4, 3);
ctx.fillRect(a.x + ALIEN_W - 2, a.y + ALIEN_H, 4, 3);
} else {
ctx.fillRect(a.x + 4, a.y + ALIEN_H, 4, 3);
ctx.fillRect(a.x + ALIEN_W - 8, a.y + ALIEN_H, 4, 3);
}
ctx.fillStyle = "#0b0b0f";
ctx.fillRect(a.x + 6, a.y + 5, 3, 3);
ctx.fillRect(a.x + ALIEN_W - 9, a.y + 5, 3, 3);
}
if (ufo) {
ctx.fillStyle = "#ff6ab0";
ctx.fillRect(ufo.x - 16, ufo.y - 3, 32, 6);
ctx.fillRect(ufo.x - 10, ufo.y - 7, 20, 4);
ctx.fillRect(ufo.x - 20, ufo.y + 3, 40, 3);
}
// bullets
ctx.fillStyle = "#ffd17a";
for (const b of bullets) ctx.fillRect(b.x - 1, b.y - 6, 2, 6);
ctx.fillStyle = "#ff6ab0";
for (const b of eBullets) ctx.fillRect(b.x - 1, b.y, 2, 6);
for (const p of particles) {
ctx.fillStyle = p.col;
ctx.globalAlpha = Math.max(0, Math.min(1, p.life * 1.5));
ctx.fillRect(p.x - 1, p.y - 1, 2, 2);
}
ctx.globalAlpha = 1;
// cannon
if (!gameOver) {
ctx.fillStyle = "#7ad1ff";
ctx.fillRect(cannon.x - cannon.w / 2, cannon.y - 2, cannon.w, 6);
ctx.fillRect(cannon.x - 2, cannon.y - 8, 4, 6);
}
// ground line
ctx.fillStyle = "#9fe870";
ctx.fillRect(0, H - 16, W, 2);
// HUD
ctx.fillStyle = "rgba(0,0,0,0.55)";
ctx.fillRect(10, 10, 200, 40);
ctx.fillStyle = "#fff";
ctx.font = "14px monospace";
ctx.textAlign = "left";
ctx.textBaseline = "alphabetic";
ctx.fillText(`SCORE ${score}`, 20, 28);
ctx.fillText(`LIVES`, 20, 44);
for (let i = 0; i < lives; i++) {
ctx.fillStyle = "#7ad1ff";
ctx.fillRect(80 + i * 18, 36, 12, 6);
ctx.fillRect(84 + i * 18, 32, 4, 4);
}
if (gameOver || win) {
ctx.fillStyle = "rgba(0,0,0,0.78)";
ctx.fillRect(W / 2 - 150, H / 2 - 56, 300, 112);
ctx.strokeStyle = win ? "#9fe870" : "#ff6ab0";
ctx.strokeRect(W / 2 - 150, H / 2 - 56, 300, 112);
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "bold 22px ui-sans-serif, system-ui";
ctx.fillText(win ? "EARTH SAVED" : "GAME OVER", W / 2, H / 2 - 14);
ctx.font = "14px monospace";
ctx.fillText(`final score ${score}`, W / 2, H / 2 + 10);
ctx.fillText("click to restart", W / 2, H / 2 + 32);
}
}
Comments (0)
Log in to comment.