Remove MenuScene from example games

Examples contradicted the plugin's own instructions by including
MenuScene files. Games now boot directly into gameplay as intended.

Changes per example:
- Delete MenuScene.js
- BootScene transitions to GameScene (not MenuScene)
- GameOverScene restarts to GameScene (not MenuScene)
- GameScene handles audio init on first user interaction
- Tests updated to match new boot-into-gameplay flow
- Barn-defense: removed menu/level-select buttons from
  GameOverScene and LevelCompleteScene

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
glitchtrend
2026-02-21 07:15:51 -08:00
parent b379e26e8b
commit 42a10021f3
25 changed files with 66 additions and 672 deletions
+4 -6
View File
@@ -104,12 +104,10 @@ tests/
### Audio integration
Strudel audio requires user interaction to start (browser autoplay policy). The flow:
1. MenuScene first tap`AUDIO_INIT` event → `initStrudel()` called
2. MenuScene first tap → `MUSIC_MENU` event → menu theme plays
3. MenuScene second tap → `MUSIC_STOP` → transition to GameScene
4. GameScene `startPlaying()``MUSIC_GAMEPLAY`gameplay BGM
5. Bird dies → `BIRD_DIED` (death SFX) + `MUSIC_STOP`
6. GameOverScene create → `MUSIC_GAMEOVER` → somber theme
1. GameScene first input`AUDIO_INIT` event → `initStrudel()` called
2. GameScene `startPlaying()``MUSIC_GAMEPLAY` → gameplay BGM
3. Bird dies → `BIRD_DIED` (death SFX) + `MUSIC_STOP`
4. GameOverScene create`MUSIC_GAMEOVER`somber theme
SFX fires on `BIRD_FLAP`, `SCORE_CHANGED`, `BIRD_DIED` via AudioBridge listeners.
+1 -2
View File
@@ -6,7 +6,6 @@
import Phaser from 'phaser';
import { GAME, COLORS } from './Constants.js';
import { BootScene } from '../scenes/BootScene.js';
import { MenuScene } from '../scenes/MenuScene.js';
import { GameScene } from '../scenes/GameScene.js';
import { UIScene } from '../scenes/UIScene.js';
import { GameOverScene } from '../scenes/GameOverScene.js';
@@ -18,5 +17,5 @@ export const GameConfig = {
height: GAME.HEIGHT,
parent: 'game-container',
backgroundColor: 0x1a3a0e,
scene: [BootScene, MenuScene, GameScene, UIScene, GameOverScene, LevelCompleteScene],
scene: [BootScene, GameScene, UIScene, GameOverScene, LevelCompleteScene],
};
@@ -1,7 +1,7 @@
// =============================================================================
// Barn Defense - BootScene
// Boots the game, generates pixel art textures programmatically, then
// transitions to menu.
// transitions directly to gameplay.
// =============================================================================
import Phaser from 'phaser';
@@ -10,6 +10,7 @@ import { ENEMY_SPRITES } from '../sprites/enemies.js';
import { TOWER_SPRITES } from '../sprites/towers.js';
import { PROJECTILE_SPRITES } from '../sprites/projectiles.js';
import { TILE_SPRITES, DECORATION_SPRITES } from '../sprites/tiles.js';
import { gameState } from '../core/GameState.js';
export class BootScene extends Phaser.Scene {
constructor() {
@@ -59,7 +60,9 @@ export class BootScene extends Phaser.Scene {
}
}
// Transition to menu
this.scene.start('MenuScene');
// Boot directly into gameplay
gameState.setLevel(0);
this.scene.start('GameScene');
this.scene.launch('UIScene');
}
}
@@ -119,11 +119,11 @@ export class GameOverScene extends Phaser.Scene {
});
// Retry button with hover scale
const retryBtn = this.add.rectangle(cx - 80, cy + 110, 140, 44, COLORS.BUTTON, 0.9);
const retryBtn = this.add.rectangle(cx, cy + 110, 140, 44, COLORS.BUTTON, 0.9);
retryBtn.setStrokeStyle(2, COLORS.BUTTON_HOVER);
retryBtn.setInteractive({ useHandCursor: true });
const retryText = this.add.text(cx - 80, cy + 110, 'RETRY', {
const retryText = this.add.text(cx, cy + 110, 'RETRY', {
fontSize: UI.FONT_SIZE_LARGE,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
@@ -154,41 +154,6 @@ export class GameOverScene extends Phaser.Scene {
});
retryBtn.on('pointerdown', () => this.retryLevel());
// Menu button with hover scale
const menuBtn = this.add.rectangle(cx + 80, cy + 110, 140, 44, 0x555555, 0.9);
menuBtn.setStrokeStyle(2, 0x777777);
menuBtn.setInteractive({ useHandCursor: true });
const menuText = this.add.text(cx + 80, cy + 110, 'MENU', {
fontSize: UI.FONT_SIZE_LARGE,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
fontStyle: 'bold',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: EFFECTS.TEXT_STROKE_THICKNESS,
}).setOrigin(0.5);
menuBtn.on('pointerover', () => {
menuBtn.setFillStyle(0x777777, 1);
this.tweens.add({
targets: [menuBtn, menuText],
scaleX: EFFECTS.BUTTON_HOVER_SCALE,
scaleY: EFFECTS.BUTTON_HOVER_SCALE,
duration: EFFECTS.BUTTON_HOVER_DURATION,
ease: 'Quad.easeOut',
});
});
menuBtn.on('pointerout', () => {
menuBtn.setFillStyle(0x555555, 0.9);
this.tweens.add({
targets: [menuBtn, menuText],
scaleX: 1,
scaleY: 1,
duration: EFFECTS.BUTTON_HOVER_DURATION,
ease: 'Quad.easeOut',
});
});
menuBtn.on('pointerdown', () => this.goToMenu());
// Fade in
this.cameras.main.fadeIn(TRANSITION.FADE_DURATION);
@@ -237,13 +202,4 @@ export class GameOverScene extends Phaser.Scene {
});
}
goToMenu() {
eventBus.emit(Events.MUSIC_STOP);
eventBus.emit(Events.GAME_RESTART);
this.cameras.main.fadeOut(TRANSITION.FADE_DURATION, 0, 0, 0, (camera, progress) => {
if (progress === 1) {
this.scene.start('MenuScene');
}
});
}
}
@@ -42,6 +42,11 @@ export class GameScene extends Phaser.Scene {
// Particle system (visual effects driven by EventBus)
this.particleSystem = new ParticleSystem(this);
// Init audio on first user interaction (browser autoplay policy)
this.input.once('pointerdown', () => {
eventBus.emit(Events.AUDIO_INIT);
});
// Game state
gameState.started = true;
@@ -134,11 +134,11 @@ export class LevelCompleteScene extends Phaser.Scene {
const hasNextLevel = gameState.currentLevel + 1 < LEVELS.length;
if (hasNextLevel) {
const nextBtn = this.add.rectangle(cx - 80, cy + 110, 150, 44, COLORS.BUTTON, 0.9);
const nextBtn = this.add.rectangle(cx, cy + 110, 150, 44, COLORS.BUTTON, 0.9);
nextBtn.setStrokeStyle(2, COLORS.BUTTON_HOVER);
nextBtn.setInteractive({ useHandCursor: true });
const nextText = this.add.text(cx - 80, cy + 110, 'NEXT LEVEL', {
const nextText = this.add.text(cx, cy + 110, 'NEXT LEVEL', {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
@@ -170,42 +170,6 @@ export class LevelCompleteScene extends Phaser.Scene {
nextBtn.on('pointerdown', () => this.nextLevel());
}
// Menu button with hover scale
const menuX = hasNextLevel ? cx + 80 : cx;
const menuBtn = this.add.rectangle(menuX, cy + 110, 140, 44, 0x555555, 0.9);
menuBtn.setStrokeStyle(2, 0x777777);
menuBtn.setInteractive({ useHandCursor: true });
const menuText = this.add.text(menuX, cy + 110, 'MENU', {
fontSize: UI.FONT_SIZE_LARGE,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
fontStyle: 'bold',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: EFFECTS.TEXT_STROKE_THICKNESS,
}).setOrigin(0.5);
menuBtn.on('pointerover', () => {
menuBtn.setFillStyle(0x777777, 1);
this.tweens.add({
targets: [menuBtn, menuText],
scaleX: EFFECTS.BUTTON_HOVER_SCALE,
scaleY: EFFECTS.BUTTON_HOVER_SCALE,
duration: EFFECTS.BUTTON_HOVER_DURATION,
ease: 'Quad.easeOut',
});
});
menuBtn.on('pointerout', () => {
menuBtn.setFillStyle(0x555555, 0.9);
this.tweens.add({
targets: [menuBtn, menuText],
scaleX: 1,
scaleY: 1,
duration: EFFECTS.BUTTON_HOVER_DURATION,
ease: 'Quad.easeOut',
});
});
menuBtn.on('pointerdown', () => this.goToMenu());
// Confetti celebration effect
this.createCelebration();
@@ -291,13 +255,4 @@ export class LevelCompleteScene extends Phaser.Scene {
});
}
goToMenu() {
eventBus.emit(Events.MUSIC_STOP);
eventBus.emit(Events.GAME_RESTART);
this.cameras.main.fadeOut(TRANSITION.FADE_DURATION, 0, 0, 0, (camera, progress) => {
if (progress === 1) {
this.scene.start('MenuScene');
}
});
}
}
@@ -1,271 +0,0 @@
// =============================================================================
// Barn Defense - MenuScene
// Main menu with title, instructions, and level select buttons.
// =============================================================================
import Phaser from 'phaser';
import { GAME, COLORS, UI, TRANSITION, PARTICLES, EFFECTS } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
import { LEVELS } from '../systems/MapSystem.js';
export class MenuScene extends Phaser.Scene {
constructor() {
super('MenuScene');
}
create() {
const cx = GAME.WIDTH / 2;
const cy = GAME.HEIGHT / 2;
// Background
this.cameras.main.setBackgroundColor(COLORS.MENU_BG);
// Gradient background (dark green top to darker bottom)
const bg = this.add.graphics();
bg.fillGradientStyle(0x0e2a08, 0x0e2a08, 0x2d5a1a, 0x2d5a1a, 1);
bg.fillRect(0, 0, GAME.WIDTH, GAME.HEIGHT);
// First click inits audio (browser autoplay policy)
this.input.once('pointerdown', () => {
eventBus.emit(Events.AUDIO_INIT);
// Small delay then start menu music
this.time.delayedCall(200, () => {
eventBus.emit(Events.MUSIC_MENU);
});
});
// Floating firefly particles in background
this.createFireflies();
// Title with bounce-in animation
const title = this.add.text(cx, 80, 'BARN DEFENSE', {
fontSize: UI.FONT_SIZE_TITLE,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
fontStyle: 'bold',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 4,
shadow: {
offsetX: 2,
offsetY: 2,
color: EFFECTS.TEXT_SHADOW_COLOR,
blur: EFFECTS.TEXT_SHADOW_BLUR,
fill: true,
},
}).setOrigin(0.5);
// Title bounce-in from scale 0
title.setScale(EFFECTS.TITLE_BOUNCE.FROM_SCALE);
this.tweens.add({
targets: title,
scaleX: EFFECTS.TITLE_BOUNCE.TO_SCALE,
scaleY: EFFECTS.TITLE_BOUNCE.TO_SCALE,
duration: EFFECTS.TITLE_BOUNCE.DURATION,
ease: EFFECTS.TITLE_BOUNCE.EASE,
});
// Subtitle
const subtitle = this.add.text(cx, 130, 'Defend your barn from the farm animals!', {
fontSize: UI.FONT_SIZE_MEDIUM,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_GOLD_TEXT,
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
}).setOrigin(0.5);
subtitle.setAlpha(0);
this.tweens.add({
targets: subtitle,
alpha: 1,
duration: 400,
delay: 300,
});
// Decorative barn with glow pulse
this.drawDecorativeBarn(cx, 200);
// Level select label
this.add.text(cx, 280, 'SELECT LEVEL', {
fontSize: UI.FONT_SIZE_LARGE,
fontFamily: UI.FONT_FAMILY,
color: COLORS.UI_TEXT,
fontStyle: 'bold',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: EFFECTS.TEXT_STROKE_THICKNESS,
}).setOrigin(0.5);
// Level buttons
const buttonY = 340;
const buttonSpacing = 80;
const startX = cx - (LEVELS.length - 1) * buttonSpacing / 2;
for (let i = 0; i < LEVELS.length; i++) {
const bx = startX + i * buttonSpacing;
const unlocked = i < gameState.levelsUnlocked;
const btn = this.add.rectangle(
bx, buttonY, 64, 64,
unlocked ? COLORS.BUTTON : COLORS.UI_BUTTON_DISABLED,
0.9
);
btn.setStrokeStyle(2, unlocked ? COLORS.BUTTON_HOVER : 0x666666);
// Level number
const levelNum = this.add.text(bx, buttonY - 8, String(i + 1), {
fontSize: UI.FONT_SIZE_LARGE,
fontFamily: UI.FONT_FAMILY,
color: unlocked ? COLORS.UI_TEXT : '#666666',
fontStyle: 'bold',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
}).setOrigin(0.5);
// Level name (small)
const levelName = this.add.text(bx, buttonY + 14, LEVELS[i].name.split(' ').slice(1).join(' '), {
fontSize: '9px',
fontFamily: UI.FONT_FAMILY,
color: unlocked ? COLORS.UI_GOLD_TEXT : '#555555',
}).setOrigin(0.5);
if (unlocked) {
btn.setInteractive({ useHandCursor: true });
btn.on('pointerover', () => {
btn.setFillStyle(COLORS.BUTTON_HOVER, 1);
this.tweens.add({
targets: [btn, levelNum, levelName],
scaleX: EFFECTS.BUTTON_HOVER_SCALE,
scaleY: EFFECTS.BUTTON_HOVER_SCALE,
duration: EFFECTS.BUTTON_HOVER_DURATION,
ease: 'Quad.easeOut',
});
});
btn.on('pointerout', () => {
btn.setFillStyle(COLORS.BUTTON, 0.9);
this.tweens.add({
targets: [btn, levelNum, levelName],
scaleX: 1,
scaleY: 1,
duration: EFFECTS.BUTTON_HOVER_DURATION,
ease: 'Quad.easeOut',
});
});
btn.on('pointerdown', () => {
this.startLevel(i);
});
}
}
// Instructions with text shadows
this.add.text(cx, 440, 'Click towers to place them on grass tiles', {
fontSize: UI.FONT_SIZE_SMALL,
fontFamily: UI.FONT_FAMILY,
color: '#aaaaaa',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
}).setOrigin(0.5);
this.add.text(cx, 460, 'Stop animals from reaching the barn!', {
fontSize: UI.FONT_SIZE_SMALL,
fontFamily: UI.FONT_FAMILY,
color: '#aaaaaa',
stroke: EFFECTS.TEXT_STROKE_COLOR,
strokeThickness: 1,
}).setOrigin(0.5);
// Version info
this.add.text(GAME.WIDTH - 10, GAME.HEIGHT - 10, 'v1.0', {
fontSize: '10px',
fontFamily: UI.FONT_FAMILY,
color: '#555555',
}).setOrigin(1);
// Fade in
this.cameras.main.fadeIn(TRANSITION.FADE_DURATION);
}
createFireflies() {
const cfg = PARTICLES.MENU_FIREFLIES;
for (let i = 0; i < cfg.COUNT; i++) {
const x = Math.random() * GAME.WIDTH;
const y = Math.random() * GAME.HEIGHT;
const size = cfg.MIN_SIZE + Math.random() * (cfg.MAX_SIZE - cfg.MIN_SIZE);
const firefly = this.add.circle(x, y, size, cfg.COLOR, 0.4 + Math.random() * 0.4);
firefly.setDepth(1);
// Gentle floating animation
const duration = cfg.MIN_DURATION + Math.random() * (cfg.MAX_DURATION - cfg.MIN_DURATION);
const driftX = (Math.random() - 0.5) * cfg.DRIFT * 2;
const driftY = (Math.random() - 0.5) * cfg.DRIFT * 2;
this.tweens.add({
targets: firefly,
x: x + driftX,
y: y + driftY,
alpha: { from: 0.2, to: 0.8 },
duration: duration,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
delay: Math.random() * 2000,
});
}
}
drawDecorativeBarn(cx, cy) {
// Glow behind barn
const glow = this.add.circle(cx, cy, 50, 0xffee88, 0.1);
glow.setDepth(2);
this.tweens.add({
targets: glow,
alpha: { from: 0.05, to: 0.18 },
scaleX: { from: 1, to: 1.15 },
scaleY: { from: 1, to: 1.15 },
duration: 1500,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
const g = this.add.graphics();
g.setDepth(3);
const w = 60;
const h = 50;
// Barn body
g.fillStyle(COLORS.BARN_COLOR, 1);
g.fillRect(cx - w / 2, cy - h / 4, w, h * 0.75);
// Roof
g.fillStyle(COLORS.BARN_ROOF, 1);
g.fillTriangle(
cx - w / 2 - 5, cy - h / 4,
cx, cy - h / 2 - 10,
cx + w / 2 + 5, cy - h / 4
);
// Door
g.fillStyle(0x663322, 1);
g.fillRect(cx - 8, cy + h * 0.15, 16, h * 0.35);
// Windows
g.fillStyle(0xffee88, 0.7);
g.fillRect(cx - w / 2 + 6, cy - h / 8, 10, 10);
g.fillRect(cx + w / 2 - 16, cy - h / 8, 10, 10);
}
startLevel(levelIndex) {
eventBus.emit(Events.MUSIC_STOP);
gameState.setLevel(levelIndex);
eventBus.emit(Events.LEVEL_SELECT, { level: levelIndex });
eventBus.emit(Events.GAME_START);
this.cameras.main.fadeOut(TRANSITION.FADE_DURATION, 0, 0, 0, (camera, progress) => {
if (progress === 1) {
this.scene.start('GameScene');
this.scene.launch('UIScene');
}
});
}
}
+1 -2
View File
@@ -1,7 +1,6 @@
import Phaser from 'phaser';
import { GAME } from './Constants.js';
import { BootScene } from '../scenes/BootScene.js';
import { MenuScene } from '../scenes/MenuScene.js';
import { GameScene } from '../scenes/GameScene.js';
import { UIScene } from '../scenes/UIScene.js';
import { GameOverScene } from '../scenes/GameOverScene.js';
@@ -19,5 +18,5 @@ export const GameConfig = {
debug: false,
},
},
scene: [BootScene, MenuScene, GameScene, UIScene, GameOverScene],
scene: [BootScene, GameScene, UIScene, GameOverScene],
};
+1 -1
View File
@@ -6,6 +6,6 @@ export class BootScene extends Phaser.Scene {
}
create() {
this.scene.start('MenuScene');
this.scene.start('GameScene');
}
}
@@ -122,6 +122,6 @@ export class GameOverScene extends Phaser.Scene {
restartGame() {
eventBus.emit(Events.MUSIC_STOP);
eventBus.emit(Events.GAME_RESTART);
this.scene.start('MenuScene');
this.scene.start('GameScene');
}
}
@@ -11,11 +11,13 @@ import { ParticleSystem } from '../systems/Particles.js';
export class GameScene extends Phaser.Scene {
constructor() {
super('GameScene');
this.audioInitialized = false;
}
create() {
gameState.reset();
this.playing = false;
this.audioInitialized = false;
// Background (sky, clouds, ground)
this.background = new Background(this);
@@ -111,6 +113,11 @@ export class GameScene extends Phaser.Scene {
handleInput() {
if (gameState.gameOver) return;
if (!this.audioInitialized) {
this.audioInitialized = true;
eventBus.emit(Events.AUDIO_INIT);
}
if (!this.playing) {
this.startPlaying();
return;
@@ -1,125 +0,0 @@
import Phaser from 'phaser';
import { GAME, BIRD, SKY, GROUND, COLORS } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { Background } from '../systems/Background.js';
export class MenuScene extends Phaser.Scene {
constructor() {
super('MenuScene');
this.audioInitialized = false;
}
create() {
const cx = GAME.WIDTH / 2;
const cy = GAME.HEIGHT / 2;
// Background with sky gradient, clouds, ground
this.background = new Background(this);
// Title
this.add.text(cx, cy - 120, 'FLAPPY', {
fontSize: '48px',
fontFamily: 'Arial Black, Arial, sans-serif',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 6,
fontStyle: 'bold',
}).setOrigin(0.5).setDepth(30);
this.add.text(cx, cy - 70, 'BIRD', {
fontSize: '48px',
fontFamily: 'Arial Black, Arial, sans-serif',
color: '#f5d742',
stroke: '#000000',
strokeThickness: 6,
fontStyle: 'bold',
}).setOrigin(0.5).setDepth(30);
// Draw a preview bird
this.drawPreviewBird(cx, cy + 10);
// Instruction
const prompt = this.add.text(cx, cy + 80, 'TAP or SPACE to Start', {
fontSize: '18px',
fontFamily: 'Arial, sans-serif',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 3,
}).setOrigin(0.5).setDepth(30);
// Blink
this.tweens.add({
targets: prompt,
alpha: 0.3,
duration: 600,
yoyo: true,
repeat: -1,
});
// Best score display
const bestScore = window.__GAME_STATE__?.bestScore || 0;
if (bestScore > 0) {
this.add.text(cx, cy + 130, `Best: ${bestScore}`, {
fontSize: '20px',
fontFamily: 'Arial, sans-serif',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 3,
}).setOrigin(0.5).setDepth(30);
}
// Input: first tap inits audio, second tap starts game
this.input.keyboard.on('keydown-SPACE', () => this.handleTap());
this.input.on('pointerdown', () => this.handleTap());
}
drawPreviewBird(x, y) {
const gfx = this.add.graphics();
gfx.setDepth(30);
gfx.setPosition(x, y);
// Body
gfx.fillStyle(BIRD.BODY_COLOR, 1);
gfx.fillEllipse(0, 0, BIRD.WIDTH * 1.5, BIRD.HEIGHT * 1.5);
gfx.fillStyle(BIRD.BODY_LIGHT, 1);
gfx.fillEllipse(2, 4, BIRD.WIDTH, BIRD.HEIGHT * 0.7);
gfx.fillStyle(BIRD.WING_COLOR, 1);
gfx.fillEllipse(-6, -1, 22, 14);
gfx.fillStyle(BIRD.EYE_COLOR, 1);
gfx.fillCircle(12, -6, 7);
gfx.fillStyle(BIRD.PUPIL_COLOR, 1);
gfx.fillCircle(14, -6, 3.5);
gfx.fillStyle(BIRD.BEAK_COLOR, 1);
gfx.fillTriangle(20, 0, 32, 4, 20, 8);
// Bob animation
this.tweens.add({
targets: gfx,
y: y - 8,
duration: 800,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut',
});
}
handleTap() {
if (!this.audioInitialized) {
this.audioInitialized = true;
eventBus.emit(Events.AUDIO_INIT);
eventBus.emit(Events.MUSIC_MENU);
return;
}
eventBus.emit(Events.MUSIC_STOP);
eventBus.emit(Events.GAME_START);
this.scene.start('GameScene');
this.scene.launch('UIScene');
}
update(time, delta) {
if (this.background) {
this.background.update(delta);
}
}
}
+11 -14
View File
@@ -7,27 +7,24 @@ test.describe('Flappy Bird — Game Tests', () => {
await expect(canvas).toBeVisible();
});
test('starts on MenuScene', async ({ gamePage: page }) => {
test('starts on GameScene', async ({ gamePage: page }) => {
const sceneKey = await page.evaluate(() => {
const scenes = window.__GAME__.scene.getScenes(true);
return scenes[0]?.scene?.key;
});
expect(sceneKey).toBe('MenuScene');
expect(sceneKey).toBe('GameScene');
});
test('transitions to GameScene on input', async ({ gamePage: page }) => {
// First tap: audio init
await page.keyboard.press('Space');
await page.waitForTimeout(200);
// Second tap: start game
test('starts playing on first input', async ({ gamePage: page }) => {
await page.keyboard.press('Space');
await page.waitForTimeout(500);
const sceneKey = await page.evaluate(() => {
const scenes = window.__GAME__.scene.getScenes(true);
return scenes.map(s => s.scene.key);
});
expect(sceneKey).toContain('GameScene');
const state = await page.evaluate(() => ({
started: window.__GAME_STATE__.started,
gameOver: window.__GAME_STATE__.gameOver,
}));
expect(state.started).toBe(true);
expect(state.gameOver).toBe(false);
});
test('GameScene shows GET READY before first input', async ({ gamePage: page }) => {
@@ -117,7 +114,7 @@ test.describe('Flappy Bird — Game Tests', () => {
expect(sceneKeys).toContain('GameOverScene');
});
test('restart returns to MenuScene', async ({ gamePage: page }) => {
test('restart returns to GameScene', async ({ gamePage: page }) => {
await startPlaying(page);
await page.evaluate(() => {
@@ -134,7 +131,7 @@ test.describe('Flappy Bird — Game Tests', () => {
const scenes = window.__GAME__.scene.getScenes(true);
return scenes.map(s => s.scene.key);
});
expect(sceneKeys).toContain('MenuScene');
expect(sceneKeys).toContain('GameScene');
});
test('best score persists across restarts', async ({ gamePage: page }) => {
@@ -2,9 +2,9 @@ import { expect } from '@playwright/test';
import { test, startPlaying } from '../fixtures/game-test.js';
test.describe('Flappy Bird — Visual Regression', () => {
test('menu scene screenshot', async ({ gamePage: page }) => {
test('initial gameplay screenshot', async ({ gamePage: page }) => {
await page.waitForTimeout(500);
await expect(page).toHaveScreenshot('menu-scene.png', {
await expect(page).toHaveScreenshot('initial-gameplay.png', {
maxDiffPixels: 3000,
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

+1 -7
View File
@@ -14,13 +14,7 @@ export const test = base.extend({
});
export async function startPlaying(page) {
// First tap: init audio (or skip to game if audio already inited)
await page.keyboard.press('Space');
await page.waitForTimeout(200);
// Second tap: start game from menu
await page.keyboard.press('Space');
await page.waitForTimeout(200);
// Third tap: start playing from GET READY state
// First tap: dismiss GET READY and start playing (also inits audio)
await page.keyboard.press('Space');
await page.waitForTimeout(300);
}
@@ -1,7 +1,6 @@
import Phaser from 'phaser';
import { GAME, COLORS } from './Constants.js';
import { BootScene } from '../scenes/BootScene.js';
import { MenuScene } from '../scenes/MenuScene.js';
import { GameScene } from '../scenes/GameScene.js';
import { UIScene } from '../scenes/UIScene.js';
import { GameOverScene } from '../scenes/GameOverScene.js';
@@ -19,5 +18,5 @@ export const GameConfig = {
debug: false,
},
},
scene: [BootScene, MenuScene, GameScene, UIScene, GameOverScene],
scene: [BootScene, GameScene, UIScene, GameOverScene],
};
@@ -6,6 +6,7 @@ export class BootScene extends Phaser.Scene {
}
create() {
this.scene.start('MenuScene');
this.scene.start('GameScene');
this.scene.launch('UIScene');
}
}
@@ -89,7 +89,8 @@ export class GameOverScene extends Phaser.Scene {
restartGame() {
eventBus.emit(Events.MUSIC_STOP);
eventBus.emit(Events.GAME_RESTART);
this.scene.start('MenuScene');
this.scene.start('GameScene');
this.scene.launch('UIScene');
}
}
@@ -23,6 +23,14 @@ export class GameScene extends Phaser.Scene {
this.xpGems = [];
this.levelingUp = false;
// Init audio on first user interaction (browser autoplay policy)
this.input.once('pointerdown', () => {
eventBus.emit(Events.AUDIO_INIT);
});
this.input.keyboard.once('keydown', () => {
eventBus.emit(Events.AUDIO_INIT);
});
// World bounds
this.physics.world.setBounds(0, 0, GAME.WORLD_WIDTH, GAME.WORLD_HEIGHT);
@@ -1,127 +0,0 @@
import Phaser from 'phaser';
import { GAME, COLORS, PLAYER } from '../core/Constants.js';
import { eventBus, Events } from '../core/EventBus.js';
import { gameState } from '../core/GameState.js';
export class MenuScene extends Phaser.Scene {
constructor() {
super('MenuScene');
this.audioInitialized = false;
}
create() {
const cx = GAME.WIDTH / 2;
const cy = GAME.HEIGHT / 2;
this.cameras.main.setBackgroundColor(COLORS.MENU_BG);
// Floating particles background
for (let i = 0; i < 30; i++) {
const px = Math.random() * GAME.WIDTH;
const py = Math.random() * GAME.HEIGHT;
const dot = this.add.circle(px, py, 1 + Math.random() * 2, 0x6644cc, 0.3 + Math.random() * 0.4);
this.tweens.add({
targets: dot,
y: py - 20 - Math.random() * 30,
alpha: 0,
duration: 2000 + Math.random() * 3000,
repeat: -1,
yoyo: true,
ease: 'Sine.easeInOut',
});
}
// Title
this.add.text(cx, cy - 120, 'VAMPIRE', {
fontSize: '52px',
fontFamily: 'Arial Black, Arial, sans-serif',
color: '#ff4444',
stroke: '#000000',
strokeThickness: 6,
fontStyle: 'bold',
}).setOrigin(0.5);
this.add.text(cx, cy - 60, 'SURVIVORS', {
fontSize: '40px',
fontFamily: 'Arial Black, Arial, sans-serif',
color: '#ffcc00',
stroke: '#000000',
strokeThickness: 5,
fontStyle: 'bold',
}).setOrigin(0.5);
// Subtitle
this.add.text(cx, cy + 10, 'Survive the night. Slay the horde.', {
fontSize: '16px',
fontFamily: 'Arial, sans-serif',
color: '#aaaacc',
stroke: '#000000',
strokeThickness: 2,
}).setOrigin(0.5);
// Controls
this.add.text(cx, cy + 60, 'WASD / Arrow Keys to move', {
fontSize: '14px',
fontFamily: 'Arial, sans-serif',
color: '#8888aa',
}).setOrigin(0.5);
this.add.text(cx, cy + 82, 'Weapons attack automatically!', {
fontSize: '14px',
fontFamily: 'Arial, sans-serif',
color: '#8888aa',
}).setOrigin(0.5);
// Start prompt
const prompt = this.add.text(cx, cy + 140, 'TAP or SPACE to Start', {
fontSize: '20px',
fontFamily: 'Arial, sans-serif',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 3,
}).setOrigin(0.5);
this.tweens.add({
targets: prompt,
alpha: 0.3,
duration: 600,
yoyo: true,
repeat: -1,
});
// Best stats
if (gameState.bestScore > 0) {
this.add.text(cx, cy + 190, `Best: ${gameState.bestScore} kills | ${formatTime(gameState.bestTime)}`, {
fontSize: '16px',
fontFamily: 'Arial, sans-serif',
color: '#aaaacc',
stroke: '#000000',
strokeThickness: 2,
}).setOrigin(0.5);
}
// Input
this.input.keyboard.on('keydown-SPACE', () => this.handleTap());
this.input.on('pointerdown', () => this.handleTap());
}
handleTap() {
if (!this.audioInitialized) {
this.audioInitialized = true;
eventBus.emit(Events.AUDIO_INIT);
eventBus.emit(Events.MUSIC_MENU);
return;
}
eventBus.emit(Events.MUSIC_STOP);
eventBus.emit(Events.GAME_START);
this.scene.start('GameScene');
this.scene.launch('UIScene');
}
}
function formatTime(seconds) {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
@@ -6,13 +6,13 @@ test.describe('Vampire Survivors — Gameplay', () => {
await expect(canvas).toBeVisible();
});
test('starts on MenuScene (BootScene)', async ({ page }) => {
test('starts on GameScene', async ({ page }) => {
const sceneName = await page.evaluate(() => {
const scenes = window.__GAME__.scene.scenes;
const active = scenes.find(s => s.sys.isActive());
return active?.sys?.settings?.key;
const active = scenes.filter(s => s.sys.isActive());
return active.map(s => s.sys.settings.key);
});
expect(['BootScene', 'MenuScene']).toContain(sceneName);
expect(sceneName).toContain('GameScene');
});
test('transitions to GameScene on double-tap', async ({ page }) => {
@@ -101,10 +101,10 @@ test.describe('Vampire Survivors — Gameplay', () => {
expect(isOver).toBe(true);
});
test('game has 5 registered scenes', async ({ page }) => {
test('game has 4 registered scenes', async ({ page }) => {
const sceneCount = await page.evaluate(() => {
return window.__GAME__.scene.scenes.length;
});
expect(sceneCount).toBe(5);
expect(sceneCount).toBe(4);
});
});
@@ -1,10 +1,9 @@
import { test, expect, startPlaying } from '../fixtures/game-test.js';
test.describe('Vampire Survivors — Visual Regression', () => {
test('menu scene screenshot', async ({ page }) => {
// Wait for menu particles to settle
test('initial gameplay screenshot', async ({ page }) => {
await page.waitForTimeout(1000);
await expect(page).toHaveScreenshot('menu-scene.png', {
await expect(page).toHaveScreenshot('initial-gameplay.png', {
maxDiffPixels: 3000,
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

+1 -5
View File
@@ -14,11 +14,7 @@ export const test = base.extend({
});
export async function startPlaying(page) {
// First tap — audio init
await page.click('canvas');
await page.waitForTimeout(300);
// Second tap — start game
await page.click('canvas');
// Game boots directly into gameplay — just wait for scene to be ready
await page.waitForTimeout(500);
}