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() { // State this.level = 1; this.score = 0; this.lives = STARTING_LIVES; this.gameOver = false; this.levelComplete = false; this.levelSpeedMult = 1; // 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 }); // Apply CRT post-processing (scanlines + screen curvature) if (this.renderer.type === Phaser.WEBGL) { this.cameras.main.setPostPipeline('CRTPipeline'); } // Stop level music when this scene shuts down (e.g. returning to menu) this.events.once('shutdown', () => { if (this.music) this.music.stop(); }); // Custom reticle cursor – hide the OS cursor and draw our own this.game.canvas.style.cursor = 'none'; this.reticleOuter = this.add.image(800, 450, 'reticle_outer').setDepth(20); this.reticleInner = this.add.image(800, 450, 'reticle_inner').setDepth(20); this.tweens.add({ targets: this.reticleOuter, angle: 360, duration: 8000, repeat: -1, ease: 'Linear' }); this.tweens.add({ targets: this.reticleInner, angle: -360, duration: 12000, repeat: -1, ease: 'Linear' }); this.events.once('shutdown', () => { this.game.canvas.style.cursor = 'default'; this.reticleOuter.destroy(); this.reticleInner.destroy(); }); } // ─── 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.player.sprite, this.alienBulletsGroup, 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 ──────────────────────────────────────────────────── _playLevelMusic() { if (this.music) { this.music.stop(); this.music.destroy(); this.music = null; } const trackNum = String(this.level).padStart(2, '0'); const key = `track${trackNum}`; // Fall back to the highest available track if this level doesn't have one yet const resolvedKey = this.cache.audio.exists(key) ? key : 'track07'; this.music = this.sound.add(resolvedKey, { loop: true, volume: 0.7 }); this.music.play(); } 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); // Progressive asteroid difficulty: more rocks, mixed sizes, faster per level this.levelSpeedMult = Math.min(1 + (this.level - 1) * 0.12, 2.0); const largeCount = Math.max(3, 2 + this.level); const mediumCount = Math.floor(this.level / 2); const smallCount = Math.floor(this.level / 3); const spawnOne = (size) => { const { x, y } = this.edgePosition(); this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup, this.levelSpeedMult)); }; for (let i = 0; i < largeCount; i++) spawnOne('large'); for (let i = 0; i < mediumCount; i++) spawnOne('medium'); for (let i = 0; i < smallCount; i++) spawnOne('small'); this.updateUI(); this.showMessage(`LEVEL ${this.level}`, 2000); this._playLevelMusic(); } // 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(); // Select alien type by level: missile aliens at 5+, spread at 3+ let type = 'basic'; if (this.level >= 5) { const r = Math.random(); type = r < 0.40 ? 'missile' : r < 0.75 ? 'spread' : 'basic'; } else if (this.level >= 3) { type = Math.random() < 0.5 ? 'spread' : 'basic'; } this.aliens.push(new AlienShip(this, x, y, this.aliensGroup, type)); } spawnPlayerBullet(x, y, angle) { this.sound.play('sfx_player_shoot', { volume: 0.2 }); this.playerBullets.push( new Bullet(this, x, y, angle, 'player', this.playerBulletsGroup) ); } spawnAlienBullet(x, y, angle, ownerShip = null) { this.sound.play('sfx_alien_shoot', { volume: 0.45 }); this.alienBullets.push( new Bullet(this, x, y, angle, 'alien', this.alienBulletsGroup, ownerShip) ); } spawnHomingMissile(x, y, ownerShip) { this.sound.play('sfx_missle', { volume: 0.5 }); const player = this.player; const angle = Phaser.Math.RadToDeg( Math.atan2(player.sprite.y - y, player.sprite.x - x) ); this.alienBullets.push( new Bullet(this, x, y, angle, 'missile', this.alienBulletsGroup, ownerShip) ); } // ─── 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.spawnImpactSparks(bulletSprite.x, bulletSprite.y); 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); this.spawnImpactSparks(bulletSprite.x, bulletSprite.y); this.sound.play('sfx_alien_death', { volume: 0.65 }); alien.explode(); this.aliens = this.aliens.filter(a => a !== alien); bullet.destroy(); this.playerBullets = this.playerBullets.filter(b => b !== bullet); } onAlienBulletHitPlayer(playerSprite, alienBulletSprite) { if (!alienBulletSprite.active) return; const bullet = alienBulletSprite.gameEntity; if (!bullet || !bullet.alive) return; const ownerShip = bullet.ownerEntity; bullet.destroy(); this.alienBullets = this.alienBullets.filter(b => b !== bullet); this.handlePlayerHit(); if (ownerShip && ownerShip.alive) { ownerShip.warpOut(); this.aliens = this.aliens.filter(a => a !== ownerShip); } } 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(); alien.warpOut(); this.aliens = this.aliens.filter(a => a !== alien); } handlePlayerHit() { if (!this.player.hit()) return; // invincible – ignore this.sound.play('sfx_player_death', { volume: 0.7 }); 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, this.levelSpeedMult)); } } else if (asteroid.size === 'medium') { for (let i = 0; i < 2; i++) { this.asteroids.push(new Asteroid(this, x, y, 'small', this.asteroidsGroup, this.levelSpeedMult)); } } // small: fully destroyed – no fragments } // ─── Score / Game Over ──────────────────────────────────────────────────── addScore(pts) { this.score += pts; this.scoreText.setText(`SCORE: ${this.score}`); } spawnImpactSparks(x, y) { this.sound.play('sfx_impact', { volume: 1.0 }); const count = Phaser.Math.Between(3, 5); for (let i = 0; i < count; i++) { const gfx = this.add.graphics(); gfx.setDepth(5); gfx.fillStyle(0xffff00, 1); gfx.fillCircle(0, 0, Phaser.Math.Between(2, 4)); gfx.x = x; gfx.y = y; const angle = Math.random() * Math.PI * 2; const dist = Phaser.Math.Between(20, 50); this.tweens.add({ targets: gfx, x: x + Math.cos(angle) * dist, y: y + Math.sin(angle) * dist, alpha: 0, duration: 250, ease: 'Quad.Out', onComplete: () => gfx.destroy() }); } } 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 return to menu`); this.input.keyboard.once('keydown-R', () => this.scene.start('MenuScene')); } // ─── 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) { // Track reticle to mouse pointer (always, even on game over) const ptr = this.input.activePointer; this.reticleOuter.setPosition(ptr.x, ptr.y); this.reticleInner.setPosition(ptr.x, ptr.y); 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(); } }