mirror of
https://github.com/PlayableIntelligence/game-creator.git
synced 2026-09-19 07:34:10 +08:00
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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,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],
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
+1
-7
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
+1
-5
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user