/* ============================================================ INVADER DOCK — Space Invaders clone ============================================================ */ (function () { 'use strict'; const { Game, STATE } = window.DSP; const S = window.DSP_SPRITES; const W = 800, H = 600; const COLS = 10, ROWS = 5, IW = 40, IH = 34; const TOP = 70; class InvaderDock extends Game { constructor(canvas) { super({ name: 'invader', title: 'INVADER DOCK', width: W, height: H, canvas, loadSteps: ['DETECTING FORMATION', 'CHARGING CANNON', 'RAISING BUNKERS', 'SYNCING GRID'] }); this.invaders = []; this.dir = 1; this.drop = false; this.bullets = []; this.enemyBullets = []; this.cannon = { x: W / 2, w: 36 }; this.bunkers = []; this.fireCd = 0; this.stepTimer = 0; this.stars = Array.from({ length: 60 }, () => ({ x: Math.random() * W, y: Math.random() * H, s: Math.random() < 0.2 ? 2 : 1 })); } reset() { this.invaders = []; for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { this.invaders.push({ c, r, x: (W - COLS * IW) / 2 + c * IW, y: TOP + r * IH, type: r === 0 ? 3 : r < 3 ? 1 : 2, alive: true, }); } } this.dir = 1; this.drop = false; this.bullets = []; this.enemyBullets = []; this.fireCd = 0; this.buildBunkers(); } buildBunkers() { this.bunkers = []; const bw = 60, bh = 30; const positions = [W * 0.2, W * 0.4, W * 0.6, W * 0.8]; for (const px of positions) { const cells = []; for (let r = 0; r < 4; r++) { for (let c = 0; c < 6; c++) { if (r === 3 && (c === 0 || c === 5)) continue; if (r === 2 && (c === 0 || c === 5)) continue; cells.push({ x: px - bw / 2 + c * 10, y: H - 90 + r * 8, hp: 2 }); } } this.bunkers.push(cells); } } aliveInvaders() { return this.invaders.filter(i => i.alive); } update() { const c = this.cannon; const left = this.input.down('ArrowLeft', 'KeyA'); const right = this.input.down('ArrowRight', 'KeyD'); if (left) c.x -= 5; if (right) c.x += 5; c.x = Math.max(c.w / 2, Math.min(W - c.w / 2, c.x)); if (this.fireCd > 0) this.fireCd--; if (this.input.wasPressed('Space') && this.fireCd === 0) { this.bullets.push({ x: c.x, y: H - 60, vy: -8 }); this.fireCd = 18; window.DSP_AUDIO.sfx('laser'); } for (let i = this.bullets.length - 1; i >= 0; i--) { const b = this.bullets[i]; b.y += b.vy; if (b.y < 0) { this.bullets.splice(i, 1); continue; } let hit = false; for (const inv of this.invaders) { if (!inv.alive) continue; if (b.x > inv.x - IW / 2 + 4 && b.x < inv.x + IW / 2 - 4 && b.y > inv.y - IH / 2 + 4 && b.y < inv.y + IH / 2 - 4) { inv.alive = false; const pts = inv.type === 3 ? 30 : inv.type === 1 ? 20 : 10; this.addScore(pts); window.DSP_AUDIO.sfx('explode'); this.particles.burst(inv.x, inv.y, inv.type === 3 ? '#ffe14d' : inv.type === 1 ? '#7dff5e' : '#ff7de9', 12, 2.5, 25); hit = true; break; } } if (!hit) { outer: for (const bunker of this.bunkers) { for (const cell of bunker) { if (cell.hp > 0 && b.x > cell.x && b.x < cell.x + 10 && b.y > cell.y && b.y < cell.y + 8) { cell.hp--; this.bullets.splice(i, 1); hit = true; break outer; } } } } if (hit) this.bullets.splice(i, 1); } const alive = this.aliveInvaders(); const speed = Math.max(0.5, 1 + (this.invaders.length - alive.length) * 0.08) * (1 + this.level * 0.15); this.stepTimer++; const stepEvery = Math.max(4, 14 - this.level * 2); if (this.stepTimer >= stepEvery) { this.stepTimer = 0; let edge = false; for (const inv of alive) { inv.x += this.dir * 8; if (inv.x < IW / 2 || inv.x > W - IW / 2) edge = true; } if (edge) { this.dir *= -1; for (const inv of alive) { inv.x += this.dir * 8; inv.y += 14; } } for (const inv of alive) { if (inv.y > H - 100) { this.gameOver(); return; } } } if (alive.length && Math.random() < 0.02 + this.level * 0.008) { const cols = {}; for (const inv of alive) { if (!cols[inv.c] || inv.y > cols[inv.c].y) cols[inv.c] = inv; } const colList = Object.values(cols); const shooter = colList[Math.floor(Math.random() * colList.length)]; if (shooter) { this.enemyBullets.push({ x: shooter.x, y: shooter.y + IH / 2, vy: 3 + this.level * 0.3 }); window.DSP_AUDIO.sfx('hit'); } } for (let i = this.enemyBullets.length - 1; i >= 0; i--) { const b = this.enemyBullets[i]; b.y += b.vy; if (b.y > H) { this.enemyBullets.splice(i, 1); continue; } let hit = false; for (const bunker of this.bunkers) { for (const cell of bunker) { if (cell.hp > 0 && b.x > cell.x && b.x < cell.x + 10 && b.y > cell.y && b.y < cell.y + 8) { cell.hp--; hit = true; break; } } if (hit) break; } if (hit) { this.enemyBullets.splice(i, 1); continue; } if (Math.abs(b.x - c.x) < c.w / 2 && b.y > H - 60 && b.y < H - 30) { this.enemyBullets.splice(i, 1); window.DSP_AUDIO.sfx('bigboom'); this.particles.burst(c.x, H - 45, '#ff4d6d', 24, 3.5, 35); this.shake.trigger(7, 20); this.lives--; if (this.lives <= 0) this.gameOver(); } } if (alive.length === 0) { this.level++; window.DSP_AUDIO.sfx('levelup'); this.reset(); } } 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' : '#2a2145'; ctx.fillRect(s.x, s.y, s.s, s.s); } const frame = Math.floor(this.time / 20) % 2; for (const inv of this.invaders) { if (!inv.alive) continue; const spriteName = inv.type === 3 ? 'invader3' : inv.type === 1 ? 'invader1' : 'invader2'; S.draw(ctx, S.get(spriteName, 3), inv.x, inv.y); } for (const bunker of this.bunkers) { for (const cell of bunker) { if (cell.hp <= 0) continue; ctx.fillStyle = cell.hp === 2 ? '#7dff5e' : '#3d8a3d'; ctx.fillRect(cell.x, cell.y, 10, 8); } } const c = this.cannon; ctx.fillStyle = '#7df9ff'; ctx.fillRect(c.x - c.w / 2, H - 40, c.w, 14); ctx.fillRect(c.x - 4, H - 56, 8, 18); ctx.fillStyle = '#ffffff'; ctx.fillRect(c.x - c.w / 2, H - 40, c.w, 4); ctx.fillStyle = '#ffe14d'; for (const b of this.bullets) ctx.fillRect(b.x - 1.5, b.y - 8, 3, 10); ctx.fillStyle = '#ff4d6d'; for (const b of this.enemyBullets) ctx.fillRect(b.x - 1.5, b.y - 6, 3, 8); } } window.DSP_ACTIVE_GAME = new InvaderDock(document.getElementById('game')); window.DSP_AUDIO.playMusic('invader'); })();