import Player from '../entities/Player.js'; import Asteroid from '../entities/Asteroid.js'; import AlienShip from '../entities/AlienShip.js'; import Bullet from '../entities/Bullet.js'; const STARTING_LIVES = 3; export default class GameScene extends Phaser.Scene { constructor() { super({ key: 'GameScene' }); } create() { this.createTextures(); // State this.level = 1; this.score = 0; this.lives = STARTING_LIVES; this.gameOver = false; this.levelComplete = false; // Physics groups (used for overlap detection) this.asteroidsGroup = this.physics.add.group(); this.aliensGroup = this.physics.add.group(); this.playerBulletsGroup = this.physics.add.group(); this.alienBulletsGroup = this.physics.add.group(); // Entity arrays for custom update logic this.asteroids = []; this.aliens = []; this.playerBullets = []; this.alienBullets = []; this.player = new Player(this, 800, 450); this.setupCollisions(); this.createUI(); this.startLevel(); this.alienTimer = this.time.addEvent({ delay: this.alienSpawnDelay(), callback: this.spawnAlien, callbackScope: this, loop: true }); } // ─── Texture Generation ─────────────────────────────────────────────────── createTextures() { // Player ship – texture 64×32, nose points right (angle 0) // Nose: (56,16) left-wing: (12,4) right-wing: (12,28) let g = this.make.graphics({ add: false }); g.lineStyle(2, 0x00ff44, 1); g.strokeTriangle(56, 16, 12, 4, 12, 28); g.generateTexture('player', 64, 32); g.destroy(); // Player ship with thruster flame g = this.make.graphics({ add: false }); g.lineStyle(2, 0x00ff44, 1); g.strokeTriangle(56, 16, 12, 4, 12, 28); g.lineStyle(2, 0xff6600, 1); // Flame behind the ship: (12,11),(12,21) -> (0,16) g.strokeTriangle(12, 11, 12, 21, 0, 16); g.generateTexture('player_thrust', 64, 32); g.destroy(); // Large asteroid – 120×120 texture, radius 50, 12-sided irregular polygon g = this.make.graphics({ add: false }); g.lineStyle(2, 0xcccccc, 1); g.strokePoints(this.asteroidPoints(60, 60, 50, 12, [1.0, 0.82, 1.08, 0.88, 1.0, 0.78, 1.1, 0.9, 0.94, 1.0, 0.84, 1.06] ), true); g.generateTexture('asteroid_large', 120, 120); g.destroy(); // Medium asteroid – 64×64, radius 26, 10-sided g = this.make.graphics({ add: false }); g.lineStyle(2, 0xcccccc, 1); g.strokePoints(this.asteroidPoints(32, 32, 26, 10, [1.0, 0.78, 1.1, 0.88, 1.0, 0.82, 1.0, 0.92, 1.06, 0.80] ), true); g.generateTexture('asteroid_medium', 64, 64); g.destroy(); // Small asteroid – 32×32, radius 12, 8-sided g = this.make.graphics({ add: false }); g.lineStyle(2, 0xcccccc, 1); g.strokePoints(this.asteroidPoints(16, 16, 12, 8, [1.0, 0.78, 1.12, 0.84, 1.0, 0.9, 0.80, 1.04] ), true); g.generateTexture('asteroid_small', 32, 32); g.destroy(); // Alien saucer – 64×48, drawn as two stacked ellipses g = this.make.graphics({ add: false }); g.lineStyle(2, 0x00ffff, 1); g.strokeEllipse(32, 30, 52, 20); // main body g.strokeEllipse(32, 22, 30, 18); // dome g.generateTexture('alien', 64, 48); g.destroy(); // Player bullet – 8×8 yellow circle g = this.make.graphics({ add: false }); g.fillStyle(0xffff00, 1); g.fillCircle(4, 4, 4); g.generateTexture('bullet', 8, 8); g.destroy(); // Alien bullet – 8×8 red circle g = this.make.graphics({ add: false }); g.fillStyle(0xff4444, 1); g.fillCircle(4, 4, 4); g.generateTexture('alien_bullet', 8, 8); g.destroy(); } // Returns array of {x,y} points forming an irregular polygon asteroidPoints(cx, cy, radius, numPoints, offsets) { return offsets.map((scale, i) => { const angle = (i / numPoints) * Math.PI * 2; return { x: cx + Math.cos(angle) * radius * scale, y: cy + Math.sin(angle) * radius * scale }; }); } // ─── Collision Setup ───────────────────────────────────────────────────── setupCollisions() { this.physics.add.overlap( this.playerBulletsGroup, this.asteroidsGroup, this.onPlayerBulletHitAsteroid, null, this ); this.physics.add.overlap( this.playerBulletsGroup, this.aliensGroup, this.onPlayerBulletHitAlien, null, this ); this.physics.add.overlap( this.alienBulletsGroup, this.player.sprite, this.onAlienBulletHitPlayer, null, this ); this.physics.add.overlap( this.player.sprite, this.asteroidsGroup, this.onPlayerHitAsteroid, null, this ); this.physics.add.overlap( this.player.sprite, this.aliensGroup, this.onPlayerHitAlien, null, this ); } // ─── UI ────────────────────────────────────────────────────────────────── createUI() { const mono = { fontFamily: 'monospace', fill: '#ffffff' }; this.scoreText = this.add.text(16, 16, 'SCORE: 0', { ...mono, fontSize: '20px' }).setDepth(10); this.livesText = this.add.text(16, 44, 'LIVES: 3', { ...mono, fontSize: '20px' }).setDepth(10); this.levelText = this.add.text(800, 16, 'LEVEL 1', { ...mono, fontSize: '24px' }) .setOrigin(0.5, 0).setDepth(10); this.msgText = this.add.text(800, 420, '', { ...mono, fontSize: '52px', align: 'center' }) .setOrigin(0.5).setDepth(10).setVisible(false); } updateUI() { this.scoreText.setText(`SCORE: ${this.score}`); this.livesText.setText(`LIVES: ${this.lives}`); this.levelText.setText(`LEVEL ${this.level}`); } showMessage(text, autohideMs = 0) { if (!text) { this.msgText.setVisible(false); return; } this.msgText.setText(text).setVisible(true); if (autohideMs > 0) { this.time.delayedCall(autohideMs, () => this.msgText.setVisible(false)); } } // ─── Level Management ──────────────────────────────────────────────────── startLevel() { this.levelComplete = false; // Clear all entities this.asteroids.forEach(a => a.destroy()); this.asteroids = []; this.aliens.forEach(a => a.destroy()); this.aliens = []; this.playerBullets.forEach(b => b.destroy()); this.playerBullets = []; this.alienBullets.forEach(b => b.destroy()); this.alienBullets = []; // Respawn player at center this.player.respawn(800, 450); // Spawn large asteroids – more per level const count = 3 + this.level; for (let i = 0; i < count; i++) { const { x, y } = this.edgePosition(); this.asteroids.push(new Asteroid(this, x, y, 'large', this.asteroidsGroup)); } this.updateUI(); this.showMessage(`LEVEL ${this.level}`, 2000); } // Random position along any screen edge, away from center edgePosition() { const W = 1600, H = 900; let x, y; do { const edge = Phaser.Math.Between(0, 3); switch (edge) { case 0: x = Phaser.Math.Between(0, W); y = 0; break; case 1: x = Phaser.Math.Between(0, W); y = H; break; case 2: x = 0; y = Phaser.Math.Between(0, H); break; default: x = W; y = Phaser.Math.Between(0, H); break; } } while (Phaser.Math.Distance.Between(x, y, 800, 450) < 200); return { x, y }; } alienSpawnDelay() { return Math.max(8000, 22000 - this.level * 1500); } // ─── Spawning ──────────────────────────────────────────────────────────── spawnAlien() { if (this.gameOver || this.levelComplete) return; if (this.aliens.length >= 2) return; const { x, y } = this.edgePosition(); this.aliens.push(new AlienShip(this, x, y, this.aliensGroup)); } spawnPlayerBullet(x, y, angle) { this.playerBullets.push( new Bullet(this, x, y, angle, 'player', this.playerBulletsGroup) ); } spawnAlienBullet(x, y, angle) { this.alienBullets.push( new Bullet(this, x, y, angle, 'alien', this.alienBulletsGroup) ); } // ─── Collision Callbacks ───────────────────────────────────────────────── onPlayerBulletHitAsteroid(bulletSprite, asteroidSprite) { if (!bulletSprite.active || !asteroidSprite.active) return; const bullet = bulletSprite.gameEntity; const asteroid = asteroidSprite.gameEntity; if (!bullet || !asteroid || !bullet.alive || !asteroid.alive) return; const scoreTable = { large: 20, medium: 50, small: 100 }; this.addScore(scoreTable[asteroid.size] || 20); this.splitAsteroid(asteroid); bullet.destroy(); this.playerBullets = this.playerBullets.filter(b => b !== bullet); } onPlayerBulletHitAlien(bulletSprite, alienSprite) { if (!bulletSprite.active || !alienSprite.active) return; const bullet = bulletSprite.gameEntity; const alien = alienSprite.gameEntity; if (!bullet || !alien || !bullet.alive || !alien.alive) return; this.addScore(200); alien.destroy(); this.aliens = this.aliens.filter(a => a !== alien); bullet.destroy(); this.playerBullets = this.playerBullets.filter(b => b !== bullet); } onAlienBulletHitPlayer(alienBulletSprite, playerSprite) { if (!alienBulletSprite.active) return; const bullet = alienBulletSprite.gameEntity; if (!bullet || !bullet.alive) return; bullet.destroy(); this.alienBullets = this.alienBullets.filter(b => b !== bullet); this.handlePlayerHit(); } onPlayerHitAsteroid(playerSprite, asteroidSprite) { if (!asteroidSprite.active) return; const asteroid = asteroidSprite.gameEntity; if (!asteroid || !asteroid.alive) return; this.handlePlayerHit(); } onPlayerHitAlien(playerSprite, alienSprite) { if (!alienSprite.active) return; const alien = alienSprite.gameEntity; if (!alien || !alien.alive) return; this.handlePlayerHit(); } handlePlayerHit() { if (!this.player.hit()) return; // invincible – ignore this.player.die(); // hide ship and go invincible immediately this.lives--; this.updateUI(); if (this.lives <= 0) { this.triggerGameOver(); } else { // Brief pause so the destruction is visible before respawning this.time.delayedCall(1500, () => { if (!this.gameOver) this.player.respawn(800, 450); }); } } // ─── Asteroid Splitting ─────────────────────────────────────────────────── splitAsteroid(asteroid) { const { x, y } = asteroid.sprite; asteroid.destroy(); this.asteroids = this.asteroids.filter(a => a !== asteroid); if (asteroid.size === 'large') { const count = Phaser.Math.Between(1, 3); for (let i = 0; i < count; i++) { const size = Math.random() < 0.5 ? 'medium' : 'small'; this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup)); } } else if (asteroid.size === 'medium') { for (let i = 0; i < 2; i++) { this.asteroids.push(new Asteroid(this, x, y, 'small', this.asteroidsGroup)); } } // small: fully destroyed – no fragments } // ─── Score / Game Over ──────────────────────────────────────────────────── addScore(pts) { this.score += pts; this.scoreText.setText(`SCORE: ${this.score}`); } triggerGameOver() { this.gameOver = true; this.player.alive = false; this.player.sprite.setVisible(false); this.showMessage(`GAME OVER\n\nSCORE: ${this.score}\n\nPress R to restart`); this.input.keyboard.once('keydown-R', () => this.scene.restart()); } // ─── Level Completion Check ─────────────────────────────────────────────── checkLevelComplete() { if (this.levelComplete) return; if (this.asteroids.length > 0 || this.aliens.length > 0) return; this.levelComplete = true; this.showMessage('LEVEL COMPLETE!', 2000); this.time.delayedCall(2500, () => { this.level++; this.changeBgColor(); this.startLevel(); // Reset alien spawn timer for new level this.alienTimer.remove(); this.alienTimer = this.time.addEvent({ delay: this.alienSpawnDelay(), callback: this.spawnAlien, callbackScope: this, loop: true }); }); } // ─── Background Color ───────────────────────────────────────────────────── changeBgColor() { // Pick dim, randomised tint – one channel slightly brighter for variety const rgb = [ Phaser.Math.Between(0, 35), Phaser.Math.Between(0, 35), Phaser.Math.Between(0, 35) ]; const boost = Phaser.Math.Between(0, 2); rgb[boost] = Phaser.Math.Between(45, 80); this.cameras.main.setBackgroundColor( Phaser.Display.Color.GetColor(rgb[0], rgb[1], rgb[2]) ); } // ─── Main Update Loop ──────────────────────────────────────────────────── update(time, delta) { if (this.gameOver) return; this.player.update(time, delta); this.asteroids.forEach(a => a.update()); this.aliens.forEach(a => a.update(time, this.player)); // Update bullets; keep only still-alive ones this.playerBullets = this.playerBullets.filter(b => b.update(time)); this.alienBullets = this.alienBullets.filter(b => b.update(time)); this.checkLevelComplete(); } }