Asteroids-2026/js/scenes/MenuScene.js

244 lines
9.3 KiB
JavaScript
Raw Permalink 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 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');
}
}