244 lines
9.3 KiB
JavaScript
244 lines
9.3 KiB
JavaScript
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();
|
||
|
||
// Apply CRT post-processing (scanlines + screen curvature)
|
||
if (this.renderer.type === Phaser.WEBGL) {
|
||
this.cameras.main.setPostPipeline('CRTPipeline');
|
||
}
|
||
|
||
// Title music — looping; stop when this scene shuts down
|
||
this.music = this.sound.add('title', { loop: true, volume: 0.7 });
|
||
this.music.play();
|
||
this.events.once('shutdown', () => { if (this.music) this.music.stop(); });
|
||
}
|
||
|
||
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');
|
||
}
|
||
}
|