From e5b53c6332979142c0eaa2c98b02884cd03949c1 Mon Sep 17 00:00:00 2001 From: hyper Date: Sun, 12 Apr 2026 19:09:41 +0800 Subject: [PATCH] refactor: fix onitama api usage --- packages/onitama-game/src/game/onitama.ts | 26 ++- packages/onitama-game/src/main.tsx | 4 +- packages/onitama-game/src/scenes/MenuScene.ts | 156 ++++++++++++++++++ .../onitama-game/src/scenes/OnitamaScene.ts | 48 +++++- packages/onitama-game/src/scenes/index.ts | 2 + packages/onitama-game/src/ui/App.tsx | 46 ++---- 6 files changed, 245 insertions(+), 37 deletions(-) create mode 100644 packages/onitama-game/src/scenes/MenuScene.ts create mode 100644 packages/onitama-game/src/scenes/index.ts diff --git a/packages/onitama-game/src/game/onitama.ts b/packages/onitama-game/src/game/onitama.ts index e9f7571..8a53254 100644 --- a/packages/onitama-game/src/game/onitama.ts +++ b/packages/onitama-game/src/game/onitama.ts @@ -1 +1,25 @@ -export * from "boardgame-core/samples/onitama"; +/** + * Re-export onitama game module from boardgame-core + * This provides a convenient import path within onitama-game + */ +export { + registry, + prompts, + start, + createInitialState, + initializeCards, + getAvailableMoves, + isValidMove, + getCardMoveCandidates, + createRegions, + createCards, + createPawns, + createGameInfo, + type OnitamaState, + type OnitamaGame, + type PlayerType, + type Card, + type CardData, + type Pawn, + type RegionType, +} from "boardgame-core/samples/onitama"; diff --git a/packages/onitama-game/src/main.tsx b/packages/onitama-game/src/main.tsx index 8804b0e..013e785 100644 --- a/packages/onitama-game/src/main.tsx +++ b/packages/onitama-game/src/main.tsx @@ -1,13 +1,11 @@ import { h } from 'preact'; import { GameUI } from 'boardgame-phaser'; -import * as gameModule from './game/onitama'; import './style.css'; import App from "@/ui/App"; -import {OnitamaScene} from "@/scenes/OnitamaScene"; const ui = new GameUI({ container: document.getElementById('ui-root')!, - root: , + root: , }); ui.mount(); diff --git a/packages/onitama-game/src/scenes/MenuScene.ts b/packages/onitama-game/src/scenes/MenuScene.ts new file mode 100644 index 0000000..ce95674 --- /dev/null +++ b/packages/onitama-game/src/scenes/MenuScene.ts @@ -0,0 +1,156 @@ +import { ReactiveScene } from 'boardgame-phaser'; +import Phaser from 'phaser'; + +/** 菜单场景配置 */ +const MENU_CONFIG = { + colors: { + title: '#1f2937', + buttonText: '#ffffff', + buttonBg: 0x3b82f6, + buttonBgHover: 0x2563eb, + subtitle: '#6b7280', + }, + fontSize: { + title: '48px', + button: '24px', + subtitle: '16px', + }, + button: { + width: 200, + height: 60, + }, + positions: { + titleY: -100, + buttonY: 40, + subtitleY: 140, + }, +} 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, + 'Onitama', + { + fontSize: MENU_CONFIG.fontSize.title, + fontFamily: 'Arial', + color: MENU_CONFIG.colors.title, + } + ).setOrigin(0.5); + + // 标题入场动画 + this.titleText.setScale(0); + this.tweens.add({ + targets: this.titleText, + scale: 1, + duration: 600, + ease: 'Back.easeOut', + }); + } + + /** 创建开始按钮 */ + 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, + } + ).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('OnitamaScene'); + } +} diff --git a/packages/onitama-game/src/scenes/OnitamaScene.ts b/packages/onitama-game/src/scenes/OnitamaScene.ts index 57c5670..51df91d 100644 --- a/packages/onitama-game/src/scenes/OnitamaScene.ts +++ b/packages/onitama-game/src/scenes/OnitamaScene.ts @@ -18,7 +18,10 @@ export class OnitamaScene extends GameHostScene { private infoText!: Phaser.GameObjects.Text; private winnerOverlay?: Phaser.GameObjects.Container; private cardLabelContainers: Map = new Map(); - + private menuButtonContainer!: Phaser.GameObjects.Container; + private menuButtonBg!: Phaser.GameObjects.Rectangle; + private menuButtonText!: Phaser.GameObjects.Text; + // UI State managed by MutableSignal public uiState!: MutableSignal; @@ -74,6 +77,12 @@ export class OnitamaScene extends GameHostScene { // Input handling this.setupInput(); + + // Menu button + this.createMenuButton(); + + // Start the game + this.gameHost.start(); } private createCardLabels(): void { @@ -220,6 +229,43 @@ export class OnitamaScene extends GameHostScene { return pawnId ? this.state.pawns[pawnId] : null; } + /** 创建菜单按钮 */ + private createMenuButton(): void { + const buttonX = this.game.scale.width - 80; + const buttonY = 30; + + this.menuButtonBg = this.add.rectangle(buttonX, buttonY, 120, 40, 0x6b7280) + .setInteractive({ useHandCursor: true }); + + this.menuButtonText = this.add.text(buttonX, buttonY, 'Menu', { + fontSize: '18px', + fontFamily: 'Arial', + color: '#ffffff', + }).setOrigin(0.5); + + this.menuButtonContainer = this.add.container(buttonX, buttonY, [ + this.menuButtonBg, + this.menuButtonText, + ]); + + this.menuButtonBg.on('pointerover', () => { + this.menuButtonBg.setFillStyle(0x4b5563); + }); + + this.menuButtonBg.on('pointerout', () => { + this.menuButtonBg.setFillStyle(0x6b7280); + }); + + this.menuButtonBg.on('pointerdown', () => { + this.goToMenu(); + }); + } + + /** 跳转到菜单场景 */ + private async goToMenu(): Promise { + await this.sceneController.launch('MenuScene'); + } + private showWinner(winner: string): void { if (this.winnerOverlay) { this.winnerOverlay.destroy(); diff --git a/packages/onitama-game/src/scenes/index.ts b/packages/onitama-game/src/scenes/index.ts new file mode 100644 index 0000000..c34ddd4 --- /dev/null +++ b/packages/onitama-game/src/scenes/index.ts @@ -0,0 +1,2 @@ +export { OnitamaScene } from './OnitamaScene'; +export { MenuScene } from './MenuScene'; diff --git a/packages/onitama-game/src/ui/App.tsx b/packages/onitama-game/src/ui/App.tsx index 9347a0f..928e166 100644 --- a/packages/onitama-game/src/ui/App.tsx +++ b/packages/onitama-game/src/ui/App.tsx @@ -1,41 +1,23 @@ -import {useComputed} from '@preact/signals'; -import { createGameHost } 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/onitama'; +import {OnitamaScene} from "@/scenes/OnitamaScene"; +import {createGameHost, type GameModule} from "boardgame-core"; +import type {OnitamaState} from "@/game/onitama"; -export default function App(props: { gameModule: any, gameScene: { new(): Phaser.Scene } }) { - - const gameHost = useComputed(() => { - const gameHost = createGameHost(props.gameModule); - return { gameHost }; - }); - - const scene = useComputed(() => new props.gameScene()); - - const handleReset = () => { - gameHost.value.gameHost.start(); - }; - const label = useComputed(() => gameHost.value.gameHost.status.value === 'running' ? 'Restart' : 'Start'); - - const phaserConfig: Partial = { - width: 700, - height: 800, - }; +export default function App() { + const gameHost = useMemo(() => createGameHost(gameModule as unknown as GameModule), []); + const gameScene = useMemo(() => new OnitamaScene(), []); + const menuScene = useMemo(() => new MenuScene(), []); return (
-
- -
- - + + +