diff --git a/src/main.ts b/src/main.ts index 1965ef9..9d95945 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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] }); diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 5666f62..fe5f101 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -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); diff --git a/src/scenes/MenuScene.ts b/src/scenes/MenuScene.ts new file mode 100644 index 0000000..f74bc15 --- /dev/null +++ b/src/scenes/MenuScene.ts @@ -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); + } +} diff --git a/src/scenes/SplashScene.ts b/src/scenes/SplashScene.ts deleted file mode 100644 index 5d4a331..0000000 --- a/src/scenes/SplashScene.ts +++ /dev/null @@ -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"); - }); - } -} diff --git a/src/scenes/StartScene.ts b/src/scenes/StartScene.ts deleted file mode 100644 index 8fb0f53..0000000 --- a/src/scenes/StartScene.ts +++ /dev/null @@ -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; - } -} diff --git a/src/scenes/__tests__/GameScene.test.ts b/src/scenes/__tests__/GameScene.test.ts index 50cede9..7760f37 100644 --- a/src/scenes/__tests__/GameScene.test.ts +++ b/src/scenes/__tests__/GameScene.test.ts @@ -27,6 +27,7 @@ vi.mock('phaser', () => { setZoom: vi.fn(), setBounds: vi.fn(), centerOn: vi.fn(), + fadeIn: vi.fn(), }, }; scene = {