From 93587541cecfa30fd87c507e72d838a11db56a11 Mon Sep 17 00:00:00 2001 From: hyper Date: Sun, 12 Apr 2026 18:47:04 +0800 Subject: [PATCH] refactor: boop to use new api --- packages/boop-game/src/game.ts | 18 +- packages/boop-game/src/main.tsx | 4 +- .../boop-game/src/scenes/BoardRenderer.ts | 8 +- packages/boop-game/src/scenes/GameScene.ts | 29 +-- packages/boop-game/src/scenes/MenuScene.ts | 168 ++++++++++++++++++ packages/boop-game/src/scenes/index.ts | 1 + packages/boop-game/src/ui/App.tsx | 41 ++--- 7 files changed, 213 insertions(+), 56 deletions(-) create mode 100644 packages/boop-game/src/scenes/MenuScene.ts diff --git a/packages/boop-game/src/game.ts b/packages/boop-game/src/game.ts index 92050b3..cd27c68 100644 --- a/packages/boop-game/src/game.ts +++ b/packages/boop-game/src/game.ts @@ -1 +1,17 @@ -export * from 'boardgame-core/samples/boop'; \ No newline at end of file +/** + * Re-export boop game module from boardgame-core + * This provides a convenient import path within boop-game + */ +export { + registry, + prompts, + start, + createInitialState, + type BoopState, + type BoopPart, + type BoopGame, + type PlayerType, + type PieceType, + type WinnerType, + type RegionType, +} from "boardgame-core/samples/boop"; \ No newline at end of file diff --git a/packages/boop-game/src/main.tsx b/packages/boop-game/src/main.tsx index 1a7c105..013e785 100644 --- a/packages/boop-game/src/main.tsx +++ b/packages/boop-game/src/main.tsx @@ -1,13 +1,11 @@ import { h } from 'preact'; import { GameUI } from 'boardgame-phaser'; -import * as boop from './game'; import './style.css'; import App from "@/ui/App"; -import {GameScene} from "@/scenes/GameScene"; const ui = new GameUI({ container: document.getElementById('ui-root')!, - root: , + root: , }); ui.mount(); diff --git a/packages/boop-game/src/scenes/BoardRenderer.ts b/packages/boop-game/src/scenes/BoardRenderer.ts index 6efb793..2708b53 100644 --- a/packages/boop-game/src/scenes/BoardRenderer.ts +++ b/packages/boop-game/src/scenes/BoardRenderer.ts @@ -3,7 +3,7 @@ import type { BoopState, PlayerType, BoopPart } from '@/game'; const BOARD_SIZE = 6; const CELL_SIZE = 80; -const BOARD_OFFSET = { x: 80, y: 100 }; +const BOARD_OFFSET = { x: 80, y: 80 }; export { BOARD_SIZE, CELL_SIZE, BOARD_OFFSET }; @@ -20,7 +20,7 @@ export class BoardRenderer { this.turnText = this.scene.add.text( BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, - BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 30, + BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 40, '', { fontSize: '22px', @@ -31,7 +31,7 @@ export class BoardRenderer { this.infoText = this.scene.add.text( BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, - BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 60, + BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 80, '点击空白处放置小猫,三个大猫连线获胜!', { fontSize: '16px', @@ -62,7 +62,7 @@ export class BoardRenderer { g.strokePath(); - this.scene.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y - 50, 'Boop Game', { + this.scene.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y - 40, 'Boop Game', { fontSize: '32px', fontFamily: 'Arial', color: '#1f2937', diff --git a/packages/boop-game/src/scenes/GameScene.ts b/packages/boop-game/src/scenes/GameScene.ts index 341a292..13d727d 100644 --- a/packages/boop-game/src/scenes/GameScene.ts +++ b/packages/boop-game/src/scenes/GameScene.ts @@ -6,7 +6,6 @@ import { createPieceSpawner } from './PieceSpawner'; import { SupplyUI } from './SupplyUI'; import { PieceTypeSelector } from './PieceTypeSelector'; import { WinnerOverlay } from './WinnerOverlay'; -import { StartOverlay } from './StartOverlay'; import { ErrorOverlay } from './ErrorOverlay'; export class GameScene extends GameHostScene { @@ -14,7 +13,6 @@ export class GameScene extends GameHostScene { private supplyUI!: SupplyUI; private pieceTypeSelector!: PieceTypeSelector; private winnerOverlay!: WinnerOverlay; - private startOverlay!: StartOverlay; private errorOverlay!: ErrorOverlay; constructor() { @@ -28,8 +26,7 @@ export class GameScene extends GameHostScene { this.boardRenderer = new BoardRenderer(this); this.supplyUI = new SupplyUI(this); this.pieceTypeSelector = new PieceTypeSelector(this); - this.winnerOverlay = new WinnerOverlay(this, () => this.restartGame()); - this.startOverlay = new StartOverlay(this, () => this.startGame()); + this.winnerOverlay = new WinnerOverlay(this, () => this.gameHost.start()); this.errorOverlay = new ErrorOverlay(this); // 设置棋子生成器 @@ -43,16 +40,6 @@ export class GameScene extends GameHostScene { () => this.gameHost.status.value !== 'running' || !!this.state.winner ); - // 监听游戏状态变化 - this.addEffect(() => { - const status = this.gameHost.status.value; - if (status === 'running') { - this.startOverlay.hide(); - } else if (status === 'created') { - this.startOverlay.show(); - } - }); - // 监听胜负状态 this.addEffect(() => { const winner = this.state.winner; @@ -69,6 +56,9 @@ export class GameScene extends GameHostScene { this.supplyUI.update(this.state); this.pieceTypeSelector.update(this.state); }); + + // 启动游戏 + this.gameHost.start(); } private handleCellClick(row: number, col: number): void { @@ -81,17 +71,14 @@ export class GameScene extends GameHostScene { private handlePieceClick(row: number, col: number): void { // 棋盘满时,点击棋子触发升级 - const error = this.gameHost.tryAnswerPrompt(prompts.graduate, this.state.currentPlayer, row, col); + const error = this.gameHost.tryAnswerPrompt(prompts.choose, this.state.currentPlayer, row, col); if (error) { this.errorOverlay.show(error); } } - private startGame(): void { - this.gameHost.start(); - } - - private restartGame(): void { - this.gameHost.start(); + /** 跳转到菜单场景 */ + private async goToMenu(): Promise { + await this.sceneController.launch('MenuScene'); } } diff --git a/packages/boop-game/src/scenes/MenuScene.ts b/packages/boop-game/src/scenes/MenuScene.ts new file mode 100644 index 0000000..687a5b2 --- /dev/null +++ b/packages/boop-game/src/scenes/MenuScene.ts @@ -0,0 +1,168 @@ +import { ReactiveScene } from 'boardgame-phaser'; +import Phaser from 'phaser'; + +/** 菜单场景配置 */ +const MENU_CONFIG = { + colors: { + title: '#fbbf24', + buttonText: '#1f2937', + buttonBg: 0xfbbf24, + buttonBgHover: 0xf59e0b, + subtitle: '#6b7280', + }, + fontSize: { + title: '48px', + button: '24px', + subtitle: '16px', + }, + button: { + width: 200, + height: 80, + }, + positions: { + titleY: -120, + buttonY: 40, + subtitleY: 160, + }, +} as const; + +export class MenuScene extends ReactiveScene { + private titleText!: Phaser.GameObjects.Text; + private startButtonContainer!: Phaser.GameObjects.Container; + private startButtonBg!: Phaser.GameObjects.Rectangle; + private startButtonText!: Phaser.GameObjects.Text; + + constructor() { + super('MenuScene'); + } + + create(): void { + super.create(); + + const center = this.getCenterPosition(); + + this.createTitle(center); + this.createStartButton(center); + this.createSubtitle(center); + } + + /** 获取屏幕中心位置 */ + private getCenterPosition(): { x: number; y: number } { + return { + x: this.game.scale.width / 2, + y: this.game.scale.height / 2, + }; + } + + /** 创建标题文本 */ + private createTitle(center: { x: number; y: number }): void { + this.titleText = this.add.text( + center.x, + center.y + MENU_CONFIG.positions.titleY, + '🐱 BOOP 🐾', + { + fontSize: MENU_CONFIG.fontSize.title, + fontFamily: 'Arial', + color: MENU_CONFIG.colors.title, + fontStyle: 'bold', + } + ).setOrigin(0.5); + + // 标题入场动画 + this.titleText.setScale(0); + this.tweens.add({ + targets: this.titleText, + scale: 1, + duration: 600, + ease: 'Back.easeOut', + }); + + // 标题呼吸动画 + this.tweens.add({ + targets: this.titleText, + scale: 1.05, + duration: 1000, + yoyo: true, + repeat: -1, + ease: 'Sine.easeInOut', + }); + } + + /** 创建开始按钮 */ + private createStartButton(center: { x: number; y: number }): void { + const { button, colors } = MENU_CONFIG; + + this.startButtonBg = this.add.rectangle( + 0, + 0, + button.width, + button.height, + colors.buttonBg + ).setOrigin(0.5).setInteractive({ useHandCursor: true }); + + this.startButtonText = this.add.text( + 0, + 0, + 'Start Game', + { + fontSize: MENU_CONFIG.fontSize.button, + fontFamily: 'Arial', + color: colors.buttonText, + fontStyle: 'bold', + } + ).setOrigin(0.5); + + this.startButtonContainer = this.add.container( + center.x, + center.y + MENU_CONFIG.positions.buttonY, + [this.startButtonBg, this.startButtonText] + ); + + // 按钮交互 + this.setupButtonInteraction(); + } + + /** 设置按钮交互效果 */ + private setupButtonInteraction(): void { + this.startButtonBg.on('pointerover', () => { + this.startButtonBg.setFillStyle(MENU_CONFIG.colors.buttonBgHover); + this.tweens.add({ + targets: this.startButtonContainer, + scale: 1.05, + duration: 100, + }); + }); + + this.startButtonBg.on('pointerout', () => { + this.startButtonBg.setFillStyle(MENU_CONFIG.colors.buttonBg); + this.tweens.add({ + targets: this.startButtonContainer, + scale: 1, + duration: 100, + }); + }); + + this.startButtonBg.on('pointerdown', () => { + this.startGame(); + }); + } + + /** 创建副标题 */ + private createSubtitle(center: { x: number; y: number }): void { + this.add.text( + center.x, + center.y + MENU_CONFIG.positions.subtitleY, + 'Click to start playing', + { + fontSize: MENU_CONFIG.fontSize.subtitle, + fontFamily: 'Arial', + color: MENU_CONFIG.colors.subtitle, + } + ).setOrigin(0.5); + } + + /** 开始游戏 */ + private async startGame(): Promise { + await this.sceneController.launch('GameScene'); + } +} diff --git a/packages/boop-game/src/scenes/index.ts b/packages/boop-game/src/scenes/index.ts index 5286101..732f5a0 100644 --- a/packages/boop-game/src/scenes/index.ts +++ b/packages/boop-game/src/scenes/index.ts @@ -1,4 +1,5 @@ export { GameScene } from './GameScene'; +export { MenuScene } from './MenuScene'; export { BoardRenderer, BOARD_SIZE, CELL_SIZE, BOARD_OFFSET } from './BoardRenderer'; export { createPieceSpawner } from './PieceSpawner'; export { SupplyUI } from './SupplyUI'; diff --git a/packages/boop-game/src/ui/App.tsx b/packages/boop-game/src/ui/App.tsx index c97c57a..0fa661f 100644 --- a/packages/boop-game/src/ui/App.tsx +++ b/packages/boop-game/src/ui/App.tsx @@ -1,38 +1,25 @@ -import {useComputed} from '@preact/signals'; -import { createGameHost, type GameModule } from 'boardgame-core'; -import Phaser from 'phaser'; import { h } from 'preact'; -import { PhaserGame, PhaserScene } from 'boardgame-phaser'; +import {PhaserGame, PhaserScene } from 'boardgame-phaser'; +import {MenuScene} from "@/scenes/MenuScene"; +import {useMemo} from "preact/hooks"; +import * as gameModule from '../game'; +import {GameScene} from "@/scenes/GameScene"; +import {createGameHost, type GameModule} from "boardgame-core"; +import type {BoopState} from "@/game"; -export default function App>(props: { gameModule: GameModule, gameScene: { new(): Phaser.Scene } }) { - - const gameHost = useComputed(() => { - const gameHost = createGameHost(props.gameModule); - return { gameHost }; - }); - - const scene = useComputed(() => new props.gameScene()); - - const handleReset = async () => { - gameHost.value.gameHost.start(); - }; - const label = useComputed(() => gameHost.value.gameHost.status.value === 'running' ? 'Restart' : 'Start'); +export default function App() { + const gameHost = useMemo(() => createGameHost(gameModule as unknown as GameModule), []); + const gameScene = useMemo(() => new GameScene(), []); + const menuScene = useMemo(() => new MenuScene(), []); return (
- - + + +
-
- -
); }