attention-retro-disorder/src/games/code-bug-invaders/CodeBugInvaders.js

304 lines
10 KiB
JavaScript

import { playMusic, stopMusic } from '../../audio.js';
export default class CodeBugInvaders extends Phaser.Scene {
constructor() { super({ key: 'CodeBugInvaders' }); }
preload() {
this.load.json('cbiConfig', 'src/games/code-bug-invaders/config.json?v=2');
this.load.audio('cbiMusic', 'assets/music/02-code-bug-invaders.mp3');
}
init(data) {
this.globalLives = data.lives;
this.level = data.level || 1;
this.gameActive = false;
}
create() {
const all = this.cache.json.get('cbiConfig');
const lvl = String(this.level);
this.cfg = all.levels ? (all.levels[lvl] ?? all.levels['1']) : all;
this.bugs = [];
this.playerBullets = [];
this.bugBullets = [];
this.bugsHit = 0;
this.gameActive = true;
this.formationDir = 1; // 1 = right, -1 = left
this.formationX = 0;
this.formationY = 0;
this.bugFireTimer = this.cfg.bugFireInterval;
playMusic(this, 'cbiMusic');
this._buildBackground();
this._buildBugs();
this._buildPlayer();
this.mainGfx = this.add.graphics();
this._buildHUD();
this._setupInput();
}
shutdown() {
stopMusic(this);
}
_buildBackground() {
const bg = this.add.graphics();
bg.fillStyle(0x000011);
bg.fillRect(0, 0, 1600, 900);
for (let i = 0; i < 150; i++) {
bg.fillStyle(0xffffff, Math.random() * 0.7 + 0.1);
bg.fillRect(Phaser.Math.Between(0, 1599), Phaser.Math.Between(0, 899), 2, 2);
}
this.add.text(800, 15, 'CODE BUG INVADERS', {
fontSize: '20px', fontFamily: 'monospace', color: '#00ff88', alpha: 0.7,
}).setOrigin(0.5, 0);
}
_buildBugs() {
const { cols, rows, bugSpacingX, bugSpacingY, startX, startY } = this.cfg;
const colors = [0xff2222, 0xff8800, 0xffff00, 0x00ff44];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
this.bugs.push({
col: c, row: r,
x: startX + c * bugSpacingX,
y: startY + r * bugSpacingY,
alive: true,
color: colors[r % colors.length],
frame: 0,
});
}
}
}
_buildPlayer() {
this.player = { x: 800, y: 845, alive: true };
}
_buildHUD() {
this.livesText = this.add.text(800, 40, '♥ '.repeat(this.globalLives).trim(), {
fontSize: '24px', fontFamily: 'monospace', color: '#ff4444',
}).setOrigin(0.5, 0);
this.bugsText = this.add.text(1580, 40, '', {
fontSize: '22px', fontFamily: 'monospace', color: '#00ff88',
}).setOrigin(1, 0);
}
_setupInput() {
this.cursors = this.input.keyboard.createCursorKeys();
this.keyA = this.input.keyboard.addKey('A');
this.keyAPressed = false;
}
update(time, delta) {
if (!this.gameActive) return;
const dt = delta / 1000;
this._movePlayer(dt);
this._moveFormation(dt);
this._moveProjectiles(dt);
this._handleBugFire(delta);
this._checkCollisions();
this._checkWinLose();
this._draw();
}
_movePlayer(dt) {
if (!this.player.alive) return;
const speed = this.cfg.playerSpeed;
if (this.cursors.left.isDown) this.player.x -= speed * dt;
if (this.cursors.right.isDown) this.player.x += speed * dt;
this.player.x = Phaser.Math.Clamp(this.player.x, 50, 1550);
if (this.keyA.isDown && !this.keyAPressed) {
this.keyAPressed = true;
// Limit to 2 bullets at once
if (this.playerBullets.length < 2) {
this.playerBullets.push({ x: this.player.x, y: this.player.y - 20 });
}
}
if (!this.keyA.isDown) this.keyAPressed = false;
}
_moveFormation(dt) {
const aliveBugs = this.bugs.filter(b => b.alive);
if (aliveBugs.length === 0) return;
// Speed scales up as fewer bugs remain
const speedScale = 1 + (1 - aliveBugs.length / (this.cfg.cols * this.cfg.rows)) * 2.5;
const move = this.cfg.formationSpeed * speedScale * dt * this.formationDir;
// Check bounds first
const { rightBound, leftBound } = this.cfg;
let minX = Infinity, maxX = -Infinity;
aliveBugs.forEach(b => { minX = Math.min(minX, b.x); maxX = Math.max(maxX, b.x); });
if (maxX + move > rightBound || minX + move < leftBound) {
// Step down, reverse
this.formationDir *= -1;
aliveBugs.forEach(b => { b.y += this.cfg.formationStepDown; });
} else {
aliveBugs.forEach(b => { b.x += move; });
}
}
_moveProjectiles(dt) {
const pbSpeed = this.cfg.playerBulletSpeed * dt;
this.playerBullets = this.playerBullets.filter(b => {
b.y -= pbSpeed;
return b.y > -10;
});
const bbSpeed = this.cfg.bugBulletSpeed * dt;
this.bugBullets = this.bugBullets.filter(b => {
b.y += bbSpeed;
return b.y < 920;
});
}
_handleBugFire(delta) {
this.bugFireTimer -= delta;
if (this.bugFireTimer <= 0) {
this.bugFireTimer = this.cfg.bugFireInterval * (0.7 + Math.random() * 0.6);
// Pick a random bug from the bottom of each column
const cols = {};
this.bugs.filter(b => b.alive).forEach(b => {
if (!cols[b.col] || b.row > cols[b.col].row) cols[b.col] = b;
});
const shooters = Object.values(cols);
if (shooters.length > 0) {
const shooter = Phaser.Utils.Array.GetRandom(shooters);
this.bugBullets.push({ x: shooter.x, y: shooter.y + 20 });
}
}
}
_checkCollisions() {
// Player bullets vs bugs
for (let bi = this.playerBullets.length - 1; bi >= 0; bi--) {
const bullet = this.playerBullets[bi];
let hit = false;
for (const bug of this.bugs) {
if (!bug.alive) continue;
if (Math.abs(bullet.x - bug.x) < 28 && Math.abs(bullet.y - bug.y) < 22) {
bug.alive = false;
this.bugsHit++;
hit = true;
break;
}
}
if (hit) {
this.playerBullets.splice(bi, 1);
if (this.bugsHit >= this.cfg.bugsToPass) {
this._endGame(true);
return;
}
}
}
// Bug bullets vs player
if (this.player.alive) {
for (const bb of this.bugBullets) {
if (Math.abs(bb.x - this.player.x) < 30 && Math.abs(bb.y - this.player.y) < 25) {
this.player.alive = false;
this._endGame(false);
return;
}
}
}
}
_checkWinLose() {
const alive = this.bugs.filter(b => b.alive);
if (alive.length === 0) { this._endGame(true); return; }
// Bugs reach danger line
if (alive.some(b => b.y >= this.cfg.dangerY)) {
this._endGame(false);
}
}
_endGame(success) {
if (!this.gameActive) return;
this.gameActive = false;
const msg = success ? 'BUGS SQUASHED!' : 'BUGS WIN!';
const color = success ? '#00ff44' : '#ff2222';
this.add.text(800, 440, msg, {
fontSize: '72px', fontFamily: 'monospace',
color, stroke: '#000000', strokeThickness: 5,
}).setOrigin(0.5).setDepth(20);
this.time.delayedCall(2200, () => {
this.game.events.emit('miniGameResult', { success });
});
}
_drawBug(g, x, y, color, frame) {
// Simple bug shape: body + 6 legs + antenna
g.fillStyle(color);
g.fillEllipse(x, y, 36, 26);
// Head
g.fillCircle(x, y - 16, 10);
// Antennae
g.lineStyle(2, color);
g.lineBetween(x - 4, y - 24, x - 10, y - 34);
g.lineBetween(x + 4, y - 24, x + 10, y - 34);
// Legs (animated: offset based on frame)
const legOff = (frame % 2 === 0) ? 4 : -4;
g.lineBetween(x - 18, y - 4 + legOff, x - 30, y - 8);
g.lineBetween(x - 18, y + 4 - legOff, x - 30, y + 8);
g.lineBetween(x + 18, y - 4 + legOff, x + 30, y - 8);
g.lineBetween(x + 18, y + 4 - legOff, x + 30, y + 8);
// Eyes
g.fillStyle(0x000000);
g.fillCircle(x - 4, y - 16, 3);
g.fillCircle(x + 4, y - 16, 3);
g.fillStyle(0xffffff);
g.fillCircle(x - 3, y - 17, 1);
g.fillCircle(x + 5, y - 17, 1);
}
_draw() {
const g = this.mainGfx;
g.clear();
const tick = Math.floor(this.time.now / 300);
// Bugs
this.bugs.forEach(b => {
if (!b.alive) return;
this._drawBug(g, b.x, b.y, b.color, tick);
});
// Player ship
if (this.player.alive) {
const { x, y } = this.player;
g.fillStyle(0x00ff88);
g.fillTriangle(x, y - 30, x - 30, y + 15, x + 30, y + 15);
g.fillStyle(0x00aa55);
g.fillRect(x - 35, y + 10, 70, 12);
g.fillStyle(0x88ffcc);
g.fillRect(x - 6, y - 28, 12, 30);
}
// Ground line for player area
g.lineStyle(2, 0x00ff44, 0.3);
g.lineBetween(0, 870, 1600, 870);
// Player bullets
g.fillStyle(0xffffff);
this.playerBullets.forEach(b => g.fillRect(b.x - 2, b.y - 8, 4, 16));
// Bug bullets
g.fillStyle(0xff4400);
this.bugBullets.forEach(b => g.fillRect(b.x - 3, b.y - 10, 6, 18));
// Danger line
g.lineStyle(1, 0xff0000, 0.3);
g.lineBetween(0, this.cfg.dangerY, 1600, this.cfg.dangerY);
// HUD update
this.bugsText.setText(`BUGS ${this.bugsHit}/${this.cfg.bugsToPass}`);
}
}