Asteroids-2026/js/scenes/GameScene.js

365 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
// 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
});
}
// ─── 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 ────────────────────────────────────────────────────
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, ownerShip = null) {
this.alienBullets.push(
new Bullet(this, x, y, angle, 'alien', 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);
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.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}`);
}
spawnImpactSparks(x, y) {
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) {
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();
}
}