Asteroids-2026/js/scenes/GameScene.js

393 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() {
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.lives--;
this.updateUI();
if (this.lives <= 0) {
this.triggerGameOver();
} else {
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.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
});
});
}
// ─── 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();
}
}