Add Invader Dock game (space invaders clone with bunkers)
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
/* ============================================================
|
||||
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');
|
||||
})();
|
||||
Reference in New Issue
Block a user