393 lines
14 KiB
JavaScript
393 lines
14 KiB
JavaScript
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();
|
||
}
|
||
}
|