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:
parent
33d340ef10
commit
9a40e9a23a
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ───────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue