Merge splash and start screen in to menu screen

This commit is contained in:
Peter Stockings
2026-01-04 18:53:57 +11:00
parent 83b7f35e57
commit 64994887dc
6 changed files with 259 additions and 103 deletions

View File

@@ -1,8 +1,7 @@
import Phaser from "phaser";
import GameUI from "./ui/GameUI";
import { GameScene } from "./scenes/GameScene";
import { SplashScene } from "./scenes/SplashScene";
import { StartScene } from "./scenes/StartScene";
import { MenuScene } from "./scenes/MenuScene";
new Phaser.Game({
type: Phaser.AUTO,
@@ -15,5 +14,5 @@ new Phaser.Game({
backgroundColor: "#111",
pixelArt: true,
roundPixels: true,
scene: [SplashScene, StartScene, GameScene, GameUI]
scene: [MenuScene, GameScene, GameUI]
});

View File

@@ -20,7 +20,6 @@ export class GameScene extends Phaser.Scene {
private playerId!: EntityId;
private floorIndex = 1;
private gameState: "playing" | "player-turn" | "enemy-turn" = "player-turn";
private runState: RunState = {
stats: { ...GAME_CONFIG.player.initialStats },
@@ -52,6 +51,7 @@ export class GameScene extends Phaser.Scene {
// Camera
this.cameras.main.setZoom(GAME_CONFIG.rendering.cameraZoom);
this.cameras.main.fadeIn(1000, 0, 0, 0);
// Initialize Sub-systems
this.dungeonRenderer = new DungeonRenderer(this);

255
src/scenes/MenuScene.ts Normal file
View File

@@ -0,0 +1,255 @@
import Phaser from "phaser";
export class MenuScene extends Phaser.Scene {
private background!: Phaser.GameObjects.Image;
constructor() {
super("MenuScene");
}
preload() {
this.load.image('splash_bg', 'splash_bg.png');
}
create() {
const { width, height } = this.scale;
// Restore Splash Background
if (this.textures.exists('splash_bg')) {
this.background = this.add.image(width / 2, height / 2, 'splash_bg');
const scale = Math.max(width / this.background.width, height / this.background.height);
this.background.setScale(scale);
// Add a slight tint to make the UI pop more
this.background.setTint(0xcccccc);
} else {
// Fallback gradient if image fails
const graphics = this.add.graphics();
graphics.fillGradientStyle(0x0a0510, 0x0a0510, 0x1a0a2a, 0x1a0a2a, 1);
graphics.fillRect(0, 0, width, height);
}
// Atmospheric Effects
this.createWindEffect();
this.createSmokeEffect(width, height);
this.createAtmosphere(width, height);
this.cameras.main.fadeIn(1000, 0, 0, 0);
// Stylish Title
const title = this.add.text(width / 2, height * 0.35, "ROGUE", {
fontSize: "96px",
color: "#ff2266",
fontStyle: "bold",
fontFamily: "Georgia, serif",
stroke: "#111",
strokeThickness: 10,
shadow: { blur: 30, color: "#ff0044", fill: true, offsetX: 0, offsetY: 0 }
}).setOrigin(0.5);
// Animate title (Slight float)
this.tweens.add({
targets: title,
y: height * 0.33,
duration: 3000,
ease: 'Sine.easeInOut',
yoyo: true,
loop: -1
});
// Buttons
const buttonYStart = height * 0.65;
const startBtn = this.createButton(width / 2, buttonYStart, "ENTER DUNGEON", 0x2288ff);
const optBtn = this.createButton(width / 2, buttonYStart + 80, "OPTIONS", 0x444444);
startBtn.on("pointerdown", () => {
this.cameras.main.fadeOut(1000, 0, 0, 0);
this.cameras.main.once(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {
this.scene.start("GameScene");
});
});
optBtn.on("pointerdown", () => {
console.log("Options clicked");
});
}
private createWindEffect() {
if (!this.background) return;
// Subtle swaying of the background to simulate wind/heat haze
this.tweens.add({
targets: this.background,
x: (this.scale.width / 2) + 10,
duration: 4000,
ease: 'Sine.easeInOut',
yoyo: true,
loop: -1
});
}
private createSmokeEffect(width: number, height: number) {
// Create many tiny, soft smoke particles instead of big circles
for (let i = 0; i < 60; i++) {
const x = Phaser.Math.Between(0, width);
const y = height + Phaser.Math.Between(0, 400);
const size = Phaser.Math.Between(15, 40);
const smoke = this.add.circle(x, y, size, 0xdddddd, 0.03);
this.tweens.add({
targets: smoke,
y: -200,
x: x + Phaser.Math.Between(-150, 150),
alpha: 0,
scale: 2.5,
duration: Phaser.Math.Between(6000, 12000),
ease: 'Linear',
loop: -1,
delay: Phaser.Math.Between(0, 10000)
});
}
// Add "Heat Haze" / Distant Smoke
for (let i = 0; i < 30; i++) {
const x = Phaser.Math.Between(0, width);
const y = Phaser.Math.Between(height * 0.4, height);
const haze = this.add.circle(x, y, Phaser.Math.Between(40, 80), 0xeeeeee, 0.01);
this.tweens.add({
targets: haze,
alpha: 0.04,
scale: 1.2,
x: x + 20,
duration: Phaser.Math.Between(3000, 6000),
ease: 'Sine.easeInOut',
yoyo: true,
loop: -1,
delay: Phaser.Math.Between(0, 3000)
});
}
}
private createAtmosphere(width: number, height: number) {
// Drifting Embers (Fire sparks)
for (let i = 0; i < 40; i++) {
const x = Phaser.Math.Between(0, width);
const y = height + Phaser.Math.Between(0, 200);
const color = Phaser.Math.RND.pick([0xff4400, 0xffaa00, 0xffffff]);
const ember = this.add.circle(x, y, Phaser.Math.Between(1, 2), color, 0.8);
// Drift diagonally to simulate wind
this.tweens.add({
targets: ember,
y: -100,
x: x - Phaser.Math.Between(100, 300),
alpha: 0,
duration: Phaser.Math.Between(4000, 7000),
ease: 'Cubic.easeOut',
loop: -1,
delay: Phaser.Math.Between(0, 5000)
});
// Add a little flicker/wobble
this.tweens.add({
targets: ember,
alpha: 0.2,
duration: 200,
yoyo: true,
loop: -1,
delay: Phaser.Math.Between(0, 500)
});
}
// Subtle Distant Ash (White particles)
for (let i = 0; i < 20; i++) {
const x = Phaser.Math.Between(0, width);
const y = -10;
const ash = this.add.circle(x, y, 1, 0xffffff, 0.3);
this.tweens.add({
targets: ash,
y: height + 10,
x: x - 100,
duration: Phaser.Math.Between(8000, 15000),
ease: 'Linear',
loop: -1,
delay: Phaser.Math.Between(0, 8000)
});
}
}
private createButton(x: number, y: number, text: string, accentColor: number) {
const width = 280;
const height = 58;
const bg = this.add.graphics();
this.drawButtonShape(bg, width, height, 0x000000);
bg.setAlpha(0.7);
const border = this.add.graphics();
border.lineStyle(2, 0x666666, 0.8);
this.drawButtonBorder(border, width, height);
const accent = this.add.graphics();
accent.fillStyle(accentColor, 1);
accent.fillRect(-width/2, -height/2, 4, height);
accent.setAlpha(0.6);
const txt = this.add.text(0, 0, text, {
fontSize: "18px",
color: "#ffffff",
fontFamily: "Verdana, Geneva, sans-serif",
letterSpacing: 3,
fontStyle: "bold"
}).setOrigin(0.5);
const container = this.add.container(x, y, [bg, border, accent, txt]);
container.setSize(width, height);
container.setInteractive({ useHandCursor: true });
container.on("pointerover", () => {
this.tweens.add({
targets: [bg, border],
alpha: 1,
duration: 200
});
this.tweens.add({
targets: accent,
alpha: 1,
scaleX: 2,
duration: 200
});
border.clear();
border.lineStyle(2, accentColor, 1);
this.drawButtonBorder(border, width, height);
});
container.on("pointerout", () => {
this.tweens.add({
targets: [bg, border],
alpha: 0.7,
duration: 200
});
this.tweens.add({
targets: accent,
alpha: 0.6,
scaleX: 1,
duration: 200
});
border.clear();
border.lineStyle(2, 0x666666, 0.8);
this.drawButtonBorder(border, width, height);
});
return container;
}
private drawButtonShape(g: Phaser.GameObjects.Graphics, w: number, h: number, color: number) {
g.fillStyle(color, 1);
g.fillRect(-w/2, -h/2, w, h);
}
private drawButtonBorder(g: Phaser.GameObjects.Graphics, w: number, h: number) {
g.strokeRect(-w/2, -h/2, w, h);
}
}

View File

@@ -1,47 +0,0 @@
import Phaser from "phaser";
import { Scene } from 'phaser';
export class SplashScene extends Scene {
constructor() {
super("SplashScene");
}
preload() {
this.load.image('splash', 'splash_bg.png');
}
create() {
const { width, height } = this.scale;
// Background (Placeholder for Image)
// If we successfully load the image 'splash', we use it.
if (this.textures.exists('splash')) {
const splash = this.add.image(width / 2, height / 2, 'splash');
// Scale to cover the screen while maintaining aspect ratio
const scaleX = width / splash.width;
const scaleY = height / splash.height;
const scale = Math.max(scaleX, scaleY);
splash.setScale(scale);
} else {
this.add.rectangle(0, 0, width, height, 0x110022).setOrigin(0);
this.add.text(width/2, height/2, "ROGUE LEGACY", {
fontSize: "48px",
color: "#ffffff",
fontStyle: "bold"
}).setOrigin(0.5);
}
// Fade In
this.cameras.main.fadeIn(1000, 0, 0, 0);
// Fade Out after delay
this.time.delayedCall(2500, () => {
this.cameras.main.fadeOut(1000, 0, 0, 0);
});
this.cameras.main.once(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {
this.scene.start("StartScene");
});
}
}

View File

@@ -1,52 +0,0 @@
import Phaser from "phaser";
export class StartScene extends Phaser.Scene {
constructor() {
super("StartScene");
}
create() {
const { width, height } = this.scale;
this.cameras.main.fadeIn(500, 0, 0, 0);
// Title
this.add.text(width / 2, height * 0.3, "ROGUE", {
fontSize: "64px",
color: "#ff0044",
fontStyle: "bold",
stroke: "#ffffff",
strokeThickness: 4
}).setOrigin(0.5);
// Buttons
const startBtn = this.createButton(width / 2, height * 0.55, "Start Game");
const optBtn = this.createButton(width / 2, height * 0.65, "Options");
startBtn.on("pointerdown", () => {
this.scene.start("GameScene");
});
optBtn.on("pointerdown", () => {
console.log("Options clicked");
});
}
private createButton(x: number, y: number, text: string) {
const bg = this.add.rectangle(0, 0, 200, 50, 0x333333).setStrokeStyle(2, 0xffffff);
const txt = this.add.text(0, 0, text, { fontSize: "24px", color: "#ffffff" }).setOrigin(0.5);
const container = this.add.container(x, y, [bg, txt]);
container.setSize(200, 50);
container.setInteractive({ useHandCursor: true });
container.on("pointerover", () => {
bg.setFillStyle(0x555555);
});
container.on("pointerout", () => {
bg.setFillStyle(0x333333);
});
return container;
}
}

View File

@@ -27,6 +27,7 @@ vi.mock('phaser', () => {
setZoom: vi.fn(),
setBounds: vi.fn(),
centerOn: vi.fn(),
fadeIn: vi.fn(),
},
};
scene = {