Add BootScene and MenuScene, update game initialization and UI

- Import and register new `BootScene` and `MenuScene` in `main.js`.
- Create `BootScene` to generate all shared textures once at startup and then start the menu.
- Implement full-featured `MenuScene` with background asteroids, wandering alien, title, controls panel, and start button (including ENTER key support).
- Remove texture generation logic and helper from `GameScene`; it now relies on textures created by `BootScene`.
- Change game‑over handling to return to the menu instead of restarting the game.
This commit is contained in:
Brian Fertig 2026-02-21 08:27:35 -07:00
parent 33d340ef10
commit 9a40e9a23a
4 changed files with 331 additions and 87 deletions

View File

@ -1,3 +1,5 @@
import BootScene from './scenes/BootScene.js';
import MenuScene from './scenes/MenuScene.js';
import GameScene from './scenes/GameScene.js';
const config = {
@ -12,7 +14,7 @@ const config = {
debug: false
}
},
scene: [GameScene]
scene: [BootScene, MenuScene, GameScene]
};
new Phaser.Game(config);

93
js/scenes/BootScene.js Normal file
View File

@ -0,0 +1,93 @@
// BootScene runs once at startup to generate all shared textures, then hands
// off to MenuScene. Textures live in Phaser's global TextureManager so every
// subsequent scene can use them without re-creating them.
export default class BootScene extends Phaser.Scene {
constructor() {
super({ key: 'BootScene' });
}
create() {
this._createTextures();
this.scene.start('MenuScene');
}
// ─── Texture Generation ───────────────────────────────────────────────────
_createTextures() {
// Player ship 64×32, nose points right (angle 0)
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);
g.strokeTriangle(12, 11, 12, 21, 0, 16);
g.generateTexture('player_thrust', 64, 32);
g.destroy();
// Large asteroid 120×120, radius 50, 12-sided
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
g = this.make.graphics({ add: false });
g.lineStyle(2, 0x00ffff, 1);
g.strokeEllipse(32, 30, 52, 20);
g.strokeEllipse(32, 22, 30, 18);
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();
}
_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
};
});
}
}

View File

@ -11,8 +11,6 @@ export default class GameScene extends Phaser.Scene {
}
create() {
this.createTextures();
// State
this.level = 1;
this.score = 0;
@ -46,88 +44,6 @@ export default class GameScene extends Phaser.Scene {
});
}
// ─── 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() {
@ -358,8 +274,8 @@ export default class GameScene extends Phaser.Scene {
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());
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 ───────────────────────────────────────────────

233
js/scenes/MenuScene.js Normal file
View File

@ -0,0 +1,233 @@
import Asteroid from '../entities/Asteroid.js';
const W = 1600;
const H = 900;
export default class MenuScene extends Phaser.Scene {
constructor() {
super({ key: 'MenuScene' });
}
// ─── Lifecycle ────────────────────────────────────────────────────────────
create() {
this.cameras.main.setBackgroundColor('#000011');
// Background asteroids (no collision needed just decoration)
this.asteroidsGroup = this.physics.add.group();
this.asteroids = [];
for (let i = 0; i < 9; i++) {
this._spawnAsteroid();
}
// Wandering alien plain physics sprite, no collision setup
this.alienSprite = this.physics.add.sprite(
Phaser.Math.Between(300, 1300),
Phaser.Math.Between(300, 600),
'alien'
);
this.alienSprite.body.setAllowGravity(false);
this._setAlienVelocity();
// Periodically change alien direction
this.time.addEvent({
delay: 2600,
callback: this._setAlienVelocity,
callbackScope: this,
loop: true
});
// UI layers (rendered on top of background entities)
this._drawTitle();
this._drawControlsPanel();
this._drawStartButton();
}
update() {
this.asteroids.forEach(a => a.update());
if (this.alienSprite && this.alienSprite.active) {
this.physics.world.wrap(this.alienSprite, 40);
}
}
// ─── Background Entities ──────────────────────────────────────────────────
_spawnAsteroid() {
const x = Phaser.Math.Between(40, W - 40);
const y = Phaser.Math.Between(40, H - 40);
// Weight toward large to fill the screen nicely
const roll = Math.random();
const size = roll < 0.5 ? 'large' : roll < 0.78 ? 'medium' : 'small';
this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup));
}
_setAlienVelocity() {
if (!this.alienSprite || !this.alienSprite.active) return;
const angle = Math.random() * Math.PI * 2;
const speed = 75 + Math.random() * 65;
this.alienSprite.setVelocity(Math.cos(angle) * speed, Math.sin(angle) * speed);
}
// ─── Title ────────────────────────────────────────────────────────────────
//
// "3D block letter" look: stack several offset copies of the text in
// progressively lighter shades (back → front), finishing with the bright
// face on top. Each copy is offset by (i, i) pixels, so the layers
// form a visible extrusion going down-right.
_drawTitle() {
const cx = W / 2;
const cy = 155;
const titleText = 'AIsteroids 2026';
const style = {
fontFamily: '"Courier New", Courier, monospace',
fontSize: '98px',
fontStyle: 'bold',
align: 'center'
};
// Extrusion layers back (darkest, largest offset) to front
const extrusionLayers = [
{ offset: 9, color: '#2a0c00' },
{ offset: 7, color: '#3e1400' },
{ offset: 5, color: '#5c2200' },
{ offset: 4, color: '#7a3300' },
{ offset: 3, color: '#9a4800' },
{ offset: 2, color: '#bb6200' },
{ offset: 1, color: '#d97e00' },
];
extrusionLayers.forEach(({ offset, color }) => {
this.add.text(cx + offset, cy + offset, titleText, { ...style, color })
.setOrigin(0.5)
.setDepth(5);
});
// Bright front face
const front = this.add.text(cx, cy, titleText, {
...style,
color: '#ffee44',
stroke: '#ffbb00',
strokeThickness: 2
}).setOrigin(0.5).setDepth(6);
// Gentle pulse so it feels alive
this.tweens.add({
targets: front,
alpha: { from: 1, to: 0.75 },
duration: 1900,
ease: 'Sine.InOut',
yoyo: true,
repeat: -1
});
// Thin decorative lines flanking the title
const lineY = cy + 68;
const gfx = this.add.graphics().setDepth(5);
gfx.lineStyle(1, 0x886622, 0.7);
gfx.beginPath();
gfx.moveTo(80, lineY); gfx.lineTo(560, lineY);
gfx.moveTo(1040, lineY); gfx.lineTo(1520, lineY);
gfx.strokePath();
}
// ─── Controls Panel ───────────────────────────────────────────────────────
_drawControlsPanel() {
const panelX = W / 2;
const panelY = 490;
const panelW = 820;
const panelH = 320;
// Semi-transparent backing so text is readable over floating asteroids
const gfx = this.add.graphics().setDepth(7);
gfx.fillStyle(0x000018, 0.82);
gfx.fillRoundedRect(panelX - panelW / 2, panelY - panelH / 2, panelW, panelH, 12);
gfx.lineStyle(1, 0x224466, 0.9);
gfx.strokeRoundedRect(panelX - panelW / 2, panelY - panelH / 2, panelW, panelH, 12);
// Header
this.add.text(panelX, panelY - panelH / 2 + 26, '── HOW TO PLAY ──', {
fontFamily: 'monospace',
fontSize: '20px',
color: '#44ddaa',
align: 'center'
}).setOrigin(0.5, 0).setDepth(8);
// Control entries
const entries = [
['Mouse', 'Aim your ship'],
['Hold LMB', 'Thrust / Accelerate'],
['A or SPACE', 'Fire weapon'],
];
const entryBaseY = panelY - panelH / 2 + 68;
const keyStyle = { fontFamily: 'monospace', fontSize: '19px', color: '#ffdd66', align: 'right' };
const descStyle = { fontFamily: 'monospace', fontSize: '19px', color: '#aaccff', align: 'left' };
entries.forEach(([key, desc], i) => {
const ey = entryBaseY + i * 36;
this.add.text(panelX - 20, ey, key, keyStyle).setOrigin(1, 0).setDepth(8);
this.add.text(panelX - 10, ey, '→', { ...descStyle, color: '#556677' }).setOrigin(0, 0).setDepth(8);
this.add.text(panelX + 10, ey, desc, descStyle).setOrigin(0, 0).setDepth(8);
});
// Flavour text
const noteStyle = { fontFamily: 'monospace', fontSize: '17px', color: '#778899', align: 'center' };
this.add.text(panelX, panelY + panelH / 2 - 62,
'Destroy all asteroids and alien ships to advance levels.',
noteStyle
).setOrigin(0.5, 0).setDepth(8);
this.add.text(panelX, panelY + panelH / 2 - 36,
'Beware — aliens will hunt you and fire back!',
{ ...noteStyle, color: '#cc6655' }
).setOrigin(0.5, 0).setDepth(8);
}
// ─── Start Button ─────────────────────────────────────────────────────────
_drawStartButton() {
const cx = W / 2;
const btn = this.add.text(cx, 790, '[ START GAME ]', {
fontFamily: 'monospace',
fontSize: '52px',
fontStyle: 'bold',
color: '#00ff88',
stroke: '#007744',
strokeThickness: 4
}).setOrigin(0.5).setDepth(9).setInteractive({ useHandCursor: true });
btn.on('pointerover', () => btn.setStyle({ color: '#ffffff', stroke: '#00cc66' }));
btn.on('pointerout', () => btn.setStyle({ color: '#00ff88', stroke: '#007744' }));
btn.on('pointerdown', () => this._startGame());
// Also allow Enter key
this.input.keyboard.once('keydown-ENTER', () => this._startGame());
// Slow blink so it draws the eye
this.tweens.add({
targets: btn,
alpha: { from: 1, to: 0.35 },
duration: 700,
ease: 'Sine.InOut',
yoyo: true,
repeat: -1
});
this.add.text(cx, 848, 'or press ENTER', {
fontFamily: 'monospace',
fontSize: '16px',
color: '#445566'
}).setOrigin(0.5).setDepth(9);
}
// ─── Transition ───────────────────────────────────────────────────────────
_startGame() {
// Phaser shuts down this scene automatically when the next one starts;
// all timers, tweens, and physics objects are cleaned up by the engine.
this.scene.start('GameScene');
}
}