304 lines
10 KiB
JavaScript
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}`);
|
|
}
|
|
}
|