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}`); } }