Compare commits

..

No commits in common. "cc80cbad06aec6305574ca739b159430b493af01" and "fe57583a8f63700928958859922aacaea792a30d" have entirely different histories.

7 changed files with 148 additions and 353 deletions

View File

@ -57,12 +57,6 @@ export class FadeScene extends ReactiveScene<FadeSceneData> {
* *
*/ */
private fadeTo(targetAlpha: number, duration: number): Promise<void> { private fadeTo(targetAlpha: number, duration: number): Promise<void> {
// 如果 overlay 还未初始化,直接返回 resolved promise
if (!this.overlay) {
console.warn('FadeScene: overlay 未初始化,跳过过渡动画');
return Promise.resolve();
}
if (this.isFading) { if (this.isFading) {
console.warn('FadeScene: 正在进行过渡动画'); console.warn('FadeScene: 正在进行过渡动画');
} }

View File

@ -10,14 +10,7 @@ export abstract class GameHostScene<TState extends Record<string, unknown>>
extends ReactiveScene<GameHostSceneOptions<TState>> extends ReactiveScene<GameHostSceneOptions<TState>>
{ {
public get gameHost(): GameHost<TState> { public get gameHost(): GameHost<TState> {
const gameHost = this.initData.gameHost as GameHost<TState>; return this.initData.gameHost as GameHost<TState>;
if (!gameHost) {
throw new Error(
`GameHostScene (${this.scene.key}): gameHost 未提供。` +
`确保在 PhaserScene 组件的 data 属性中传入 gameHost。`
);
}
return gameHost;
} }
public get state(): TState { public get state(): TState {

View File

@ -29,19 +29,13 @@ export abstract class ReactiveScene<TData extends Record<string, unknown> = {}>
implements IDisposable implements IDisposable
{ {
protected disposables = new DisposableBag(); protected disposables = new DisposableBag();
private _initData?: TData & ReactiveScenePhaserData; private _initData!: TData & ReactiveScenePhaserData;
/** /**
* init() * init()
* create() * create()
*/ */
public get initData(): TData & ReactiveScenePhaserData { public get initData(): TData & ReactiveScenePhaserData {
if (!this._initData) {
throw new Error(
`ReactiveScene (${this.scene.key}): initData 尚未初始化。` +
`确保场景通过 PhaserScene 组件注册,并在 create() 阶段访问。`
);
}
return this._initData; return this._initData;
} }
@ -49,14 +43,14 @@ export abstract class ReactiveScene<TData extends Record<string, unknown> = {}>
* Phaser game * Phaser game
*/ */
public get phaserGame(): ReadonlySignal<{ game: Phaser.Game }> { public get phaserGame(): ReadonlySignal<{ game: Phaser.Game }> {
return this.initData.phaserGame; return this._initData.phaserGame;
} }
/** /**
* *
*/ */
public get sceneController(): SceneController { public get sceneController(): SceneController {
return this.initData.sceneController; return this._initData.sceneController;
} }
constructor(key?: string) { constructor(key?: string) {

View File

@ -1,7 +1,7 @@
import Phaser from 'phaser'; import Phaser from 'phaser';
import { signal, useSignal, useSignalEffect } from '@preact/signals'; import { signal, useSignal, useSignalEffect } from '@preact/signals';
import { createContext, h } from 'preact'; import { createContext, h } from 'preact';
import { useContext, useEffect, useRef } from 'preact/hooks'; import { useContext } from 'preact/hooks';
import {ReadonlySignal} from "@preact/signals-core"; import {ReadonlySignal} from "@preact/signals-core";
import type { ReactiveScene } from '../scenes'; import type { ReactiveScene } from '../scenes';
import { FadeScene as FadeSceneClass, FADE_SCENE_KEY } from '../scenes/FadeScene'; import { FadeScene as FadeSceneClass, FADE_SCENE_KEY } from '../scenes/FadeScene';
@ -40,7 +40,7 @@ export interface PhaserGameProps {
export function PhaserGame(props: PhaserGameProps) { export function PhaserGame(props: PhaserGameProps) {
const gameSignal = useSignal<PhaserGameContext>({ game: undefined!, sceneController: undefined! }); const gameSignal = useSignal<PhaserGameContext>({ game: undefined!, sceneController: undefined! });
const initialSceneLaunched = useRef(false); const initialSceneLaunched = useSignal(false);
useSignalEffect(() => { useSignalEffect(() => {
const config: Phaser.Types.Core.GameConfig = { const config: Phaser.Types.Core.GameConfig = {
@ -49,9 +49,9 @@ export function PhaserGame(props: PhaserGameProps) {
}; };
const phaserGame = new Phaser.Game(config); const phaserGame = new Phaser.Game(config);
// 添加 FadeScene 并启动它来初始化 overlay // 添加 FadeScene
const fadeScene = new FadeSceneClass(); const fadeScene = new FadeSceneClass();
phaserGame.scene.add(FADE_SCENE_KEY, fadeScene, true); // 改为 true 以触发 create phaserGame.scene.add(FADE_SCENE_KEY, fadeScene, false);
// 创建 SceneController // 创建 SceneController
const currentScene = signal<string | null>(null); const currentScene = signal<string | null>(null);
@ -64,24 +64,10 @@ export function PhaserGame(props: PhaserGameProps) {
return; return;
} }
// 等待场景注册完成(最多等待 100ms
let retries = 0;
while (!phaserGame.scene.getScene(sceneKey) && retries < 10) {
await new Promise(resolve => setTimeout(resolve, 10));
retries++;
}
// 验证场景是否已注册
if (!phaserGame.scene.getScene(sceneKey)) {
console.error(`SceneController: 场景 "${sceneKey}" 未注册`);
return;
}
isTransitioning.value = true; isTransitioning.value = true;
const fade = phaserGame.scene.getScene(FADE_SCENE_KEY) as FadeSceneClass; const fade = phaserGame.scene.getScene(FADE_SCENE_KEY) as FadeSceneClass;
// 淡出到黑色 // 淡出到黑色
phaserGame.scene.bringToTop(FADE_SCENE_KEY);
await fade.fadeOut(300); await fade.fadeOut(300);
// 停止当前场景 // 停止当前场景
@ -89,14 +75,6 @@ export function PhaserGame(props: PhaserGameProps) {
phaserGame.scene.stop(currentScene.value); phaserGame.scene.stop(currentScene.value);
} }
// 确保场景已注册后再启动
// (场景应该已经在 PhaserScene 组件中注册)
if (!phaserGame.scene.getScene(sceneKey)) {
console.error(`SceneController: 场景 "${sceneKey}" 在切换时仍未注册`);
isTransitioning.value = false;
return;
}
// 启动新场景 // 启动新场景
phaserGame.scene.start(sceneKey); phaserGame.scene.start(sceneKey);
currentScene.value = sceneKey; currentScene.value = sceneKey;
@ -113,22 +91,19 @@ export function PhaserGame(props: PhaserGameProps) {
return () => { return () => {
gameSignal.value = { game: undefined!, sceneController: undefined! }; gameSignal.value = { game: undefined!, sceneController: undefined! };
initialSceneLaunched.current = false; initialSceneLaunched.value = false;
phaserGame.destroy(true); phaserGame.destroy(true);
}; };
}); });
// 启动初始场景(仅一次) // 启动初始场景
useEffect(() => { useSignalEffect(() => {
const ctx = gameSignal.value; const ctx = gameSignal.value;
if (!initialSceneLaunched.current && props.initialScene && ctx?.sceneController) { if (!initialSceneLaunched.value && props.initialScene && ctx?.sceneController) {
initialSceneLaunched.current = true; initialSceneLaunched.value = true;
// 使用 microtask 确保所有子组件的场景注册已完成 ctx.sceneController.launch(props.initialScene);
Promise.resolve().then(() => {
ctx.sceneController.launch(props.initialScene!);
});
} }
}, [gameSignal.value, props.initialScene]); });
return ( return (
<div id="phaser-container" className="w-full h-full"> <div id="phaser-container" className="w-full h-full">
@ -147,11 +122,9 @@ export interface PhaserSceneProps<TData extends Record<string, unknown> = {}> {
} }
export const phaserSceneContext = createContext<ReadonlySignal<ReactiveScene> | null>(null); export const phaserSceneContext = createContext<ReadonlySignal<ReactiveScene> | null>(null);
export function PhaserScene<TData extends Record<string, unknown> = {}>(props: PhaserSceneProps<TData>) { export function PhaserScene<TData extends Record<string, unknown> = {}>(props: PhaserSceneProps<TData>) {
const phaserGameSignal = useContext(phaserContext); const phaserGameSignal = useContext(phaserContext);
const sceneSignal = useSignal<ReactiveScene<TData>>(); const sceneSignal = useSignal<ReactiveScene<TData>>();
const registered = useRef(false);
useSignalEffect(() => { useSignalEffect(() => {
if (!phaserGameSignal) return; if (!phaserGameSignal) return;
@ -159,23 +132,20 @@ export function PhaserScene<TData extends Record<string, unknown> = {}>(props: P
if (!ctx?.game) return; if (!ctx?.game) return;
const game = ctx.game; const game = ctx.game;
// 注册场景到 Phaser但不启动
if (!game.scene.getScene(props.sceneKey)) {
const initData = { const initData = {
...props.data, ...props.data,
phaserGame: phaserGameSignal, phaserGame: phaserGameSignal,
sceneController: ctx.sceneController, sceneController: ctx.sceneController,
}; };
// 注册场景但不启动
if (!game.scene.getScene(props.sceneKey)) {
game.scene.add(props.sceneKey, props.scene, false, initData); game.scene.add(props.sceneKey, props.scene, false, initData);
} }
sceneSignal.value = props.scene; sceneSignal.value = props.scene;
registered.current = true;
return () => { return () => {
sceneSignal.value = undefined; sceneSignal.value = undefined;
registered.current = false;
// 不在这里移除场景,让 SceneController 管理生命周期 // 不在这里移除场景,让 SceneController 管理生命周期
}; };
}); });

View File

@ -1,19 +1 @@
/** export * from "boardgame-core/samples/tic-tac-toe";
* Re-export tic-tac-toe game module from boardgame-core
* This provides a convenient import path within sample-game
*/
export {
registry,
prompts,
start,
createInitialState,
isCellOccupied,
hasWinningLine,
checkWinner,
placePiece,
type TicTacToePart,
type TicTacToeState,
type TicTacToeGame,
type PlayerType,
type WinnerType,
} from "boardgame-core/samples/tic-tac-toe";

View File

@ -4,39 +4,18 @@ import { GameHostScene } from 'boardgame-phaser';
import { spawnEffect, type Spawner } from 'boardgame-phaser'; import { spawnEffect, type Spawner } from 'boardgame-phaser';
import {prompts} from "@/game/tic-tac-toe"; import {prompts} from "@/game/tic-tac-toe";
// 棋盘配置常量 const CELL_SIZE = 120;
export const BOARD_CONFIG = { const BOARD_OFFSET = { x: 100, y: 100 };
cellSize: 120, const BOARD_SIZE = 3;
boardOffset: { x: 100, y: 100 },
boardSize: 3,
colors: {
grid: 0x6b7280,
x: '#3b82f6',
o: '#ef4444',
title: '#1f2937',
turn: '#4b5563',
menuButton: 0x6b7280,
menuButtonHover: 0x4b5563,
overlay: 0x000000,
winText: '#fbbf24',
},
fontSize: {
title: '28px',
turn: '20px',
cell: '64px',
menuButton: '18px',
winText: '36px',
},
} as const;
export class GameScene extends GameHostScene<TicTacToeState> { export class GameScene extends GameHostScene<TicTacToeState> {
private boardContainer!: Phaser.GameObjects.Container; private boardContainer!: Phaser.GameObjects.Container;
private gridGraphics!: Phaser.GameObjects.Graphics; private gridGraphics!: Phaser.GameObjects.Graphics;
private turnText!: Phaser.GameObjects.Text; private turnText!: Phaser.GameObjects.Text;
private winnerOverlay?: Phaser.GameObjects.Container; private winnerOverlay?: Phaser.GameObjects.Container;
private menuButtonContainer!: Phaser.GameObjects.Container; private menuButton!: Phaser.GameObjects.Container;
private menuButtonBg!: Phaser.GameObjects.Rectangle;
private menuButtonText!: Phaser.GameObjects.Text; private menuButtonText!: Phaser.GameObjects.Text;
private menuButtonBg!: Phaser.GameObjects.Rectangle;
constructor() { constructor() {
super('GameScene'); super('GameScene');
@ -47,10 +26,8 @@ export class GameScene extends GameHostScene<TicTacToeState> {
this.boardContainer = this.add.container(0, 0); this.boardContainer = this.add.container(0, 0);
this.gridGraphics = this.add.graphics(); this.gridGraphics = this.add.graphics();
this.drawGrid();
this.createBoardVisuals();
this.createMenuButton(); this.createMenuButton();
this.createInputZones();
this.disposables.add(spawnEffect(new TicTacToePartSpawner(this))); this.disposables.add(spawnEffect(new TicTacToePartSpawner(this)));
@ -69,87 +46,38 @@ export class GameScene extends GameHostScene<TicTacToeState> {
this.updateTurnText(currentPlayer); this.updateTurnText(currentPlayer);
}); });
this.gameHost.start(); this.setupInput();
} }
/** 创建棋盘视觉元素(网格、标题、回合提示) */ private isCellOccupied(row: number, col: number): boolean {
private createBoardVisuals(): void { return !!this.state.board.partMap[`${row},${col}`];
this.drawGrid();
const { boardSize, cellSize, boardOffset } = BOARD_CONFIG;
const centerX = boardOffset.x + (boardSize * cellSize) / 2;
this.add.text(centerX, boardOffset.y - 40, 'Tic-Tac-Toe', {
fontSize: BOARD_CONFIG.fontSize.title,
fontFamily: 'Arial',
color: BOARD_CONFIG.colors.title,
}).setOrigin(0.5);
this.turnText = this.add.text(
centerX,
boardOffset.y + boardSize * cellSize + 20,
'',
{
fontSize: BOARD_CONFIG.fontSize.turn,
fontFamily: 'Arial',
color: BOARD_CONFIG.colors.turn,
}
).setOrigin(0.5);
this.updateTurnText(this.state.currentPlayer);
} }
/** 绘制棋盘网格 */
private drawGrid(): void {
const g = this.gridGraphics;
const { boardSize, cellSize, boardOffset } = BOARD_CONFIG;
g.lineStyle(3, BOARD_CONFIG.colors.grid);
for (let i = 1; i < boardSize; i++) {
g.lineBetween(
boardOffset.x + i * cellSize,
boardOffset.y,
boardOffset.x + i * cellSize,
boardOffset.y + boardSize * cellSize,
);
g.lineBetween(
boardOffset.x,
boardOffset.y + i * cellSize,
boardOffset.x + boardSize * cellSize,
boardOffset.y + i * cellSize,
);
}
g.strokePath();
}
/** 创建菜单按钮 */
private createMenuButton(): void { private createMenuButton(): void {
const buttonX = this.game.scale.width - 80; const buttonX = this.game.scale.width - 80;
const buttonY = 30; const buttonY = 30;
this.menuButtonBg = this.add.rectangle(buttonX, buttonY, 120, 40, BOARD_CONFIG.colors.menuButton) this.menuButtonBg = this.add.rectangle(buttonX, buttonY, 120, 40, 0x6b7280)
.setInteractive({ useHandCursor: true }); .setInteractive({ useHandCursor: true });
this.menuButtonText = this.add.text(buttonX, buttonY, 'Menu', { this.menuButtonText = this.add.text(buttonX, buttonY, 'Menu', {
fontSize: BOARD_CONFIG.fontSize.menuButton, fontSize: '18px',
fontFamily: 'Arial', fontFamily: 'Arial',
color: '#ffffff', color: '#ffffff',
}).setOrigin(0.5); }).setOrigin(0.5);
this.menuButtonContainer = this.add.container(buttonX, buttonY, [ this.menuButton = this.add.container(buttonX, buttonY, [
this.menuButtonBg, this.menuButtonBg,
this.menuButtonText, this.menuButtonText,
]); ]);
// 按钮交互 // 按钮交互
this.menuButtonBg.on('pointerover', () => { this.menuButtonBg.on('pointerover', () => {
this.menuButtonBg.setFillStyle(BOARD_CONFIG.colors.menuButtonHover); this.menuButtonBg.setFillStyle(0x4b5563);
}); });
this.menuButtonBg.on('pointerout', () => { this.menuButtonBg.on('pointerout', () => {
this.menuButtonBg.setFillStyle(BOARD_CONFIG.colors.menuButton); this.menuButtonBg.setFillStyle(0x6b7280);
}); });
this.menuButtonBg.on('pointerdown', () => { this.menuButtonBg.on('pointerdown', () => {
@ -157,26 +85,19 @@ export class GameScene extends GameHostScene<TicTacToeState> {
}); });
} }
/** 创建输入区域 */ private async goToMenu(): Promise<void> {
private createInputZones(): void { await this.sceneController.launch('MenuScene');
const { boardSize, cellSize, boardOffset } = BOARD_CONFIG; }
for (let row = 0; row < boardSize; row++) { private setupInput(): void {
for (let col = 0; col < boardSize; col++) { for (let row = 0; row < BOARD_SIZE; row++) {
const x = boardOffset.x + col * cellSize + cellSize / 2; for (let col = 0; col < BOARD_SIZE; col++) {
const y = boardOffset.y + row * cellSize + cellSize / 2; const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
const zone = this.add.zone(x, y, cellSize, cellSize).setInteractive(); const zone = this.add.zone(x, y, CELL_SIZE, CELL_SIZE).setInteractive();
zone.on('pointerdown', () => { zone.on('pointerdown', () => {
this.handleCellClick(row, col);
});
}
}
}
/** 处理格子点击 */
private handleCellClick(row: number, col: number): void {
if (this.state.winner) return; if (this.state.winner) return;
if (this.isCellOccupied(row, col)) return; if (this.isCellOccupied(row, col)) return;
@ -184,21 +105,53 @@ export class GameScene extends GameHostScene<TicTacToeState> {
if (error) { if (error) {
console.warn('Invalid move:', error); console.warn('Invalid move:', error);
} }
});
}
}
} }
/** 检查格子是否被占用 */ private drawGrid(): void {
private isCellOccupied(row: number, col: number): boolean { const g = this.gridGraphics;
return !!this.state.board.partMap[`${row},${col}`]; g.lineStyle(3, 0x6b7280);
for (let i = 1; i < BOARD_SIZE; i++) {
g.lineBetween(
BOARD_OFFSET.x + i * CELL_SIZE,
BOARD_OFFSET.y,
BOARD_OFFSET.x + i * CELL_SIZE,
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE,
);
g.lineBetween(
BOARD_OFFSET.x,
BOARD_OFFSET.y + i * CELL_SIZE,
BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE,
BOARD_OFFSET.y + i * CELL_SIZE,
);
}
g.strokePath();
this.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y - 40, 'Tic-Tac-Toe', {
fontSize: '28px',
fontFamily: 'Arial',
color: '#1f2937',
}).setOrigin(0.5);
this.turnText = this.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 20, '', {
fontSize: '20px',
fontFamily: 'Arial',
color: '#4b5563',
}).setOrigin(0.5);
this.updateTurnText(this.state.currentPlayer);
} }
/** 更新回合提示文本 */
private updateTurnText(player: string): void { private updateTurnText(player: string): void {
if (this.turnText) { if (this.turnText) {
this.turnText.setText(`${player}'s turn`); this.turnText.setText(`${player}'s turn`);
} }
} }
/** 显示获胜者 */
private showWinner(winner: string): void { private showWinner(winner: string): void {
// 清理旧的覆盖层防止叠加 // 清理旧的覆盖层防止叠加
if (this.winnerOverlay) { if (this.winnerOverlay) {
@ -208,16 +161,13 @@ export class GameScene extends GameHostScene<TicTacToeState> {
this.winnerOverlay = this.add.container(); this.winnerOverlay = this.add.container();
const text = winner === 'draw' ? "It's a draw!" : `${winner} wins!`; const text = winner === 'draw' ? "It's a draw!" : `${winner} wins!`;
const { boardSize, cellSize, boardOffset } = BOARD_CONFIG;
const centerX = boardOffset.x + (boardSize * cellSize) / 2;
const centerY = boardOffset.y + (boardSize * cellSize) / 2;
const bg = this.add.rectangle( const bg = this.add.rectangle(
centerX, BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
centerY, BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
boardSize * cellSize, BOARD_SIZE * CELL_SIZE,
boardSize * cellSize, BOARD_SIZE * CELL_SIZE,
BOARD_CONFIG.colors.overlay, 0x000000,
0.6, 0.6,
).setInteractive({ useHandCursor: true }); ).setInteractive({ useHandCursor: true });
@ -227,11 +177,16 @@ export class GameScene extends GameHostScene<TicTacToeState> {
this.winnerOverlay.add(bg); this.winnerOverlay.add(bg);
const winText = this.add.text(centerX, centerY, text, { const winText = this.add.text(
fontSize: BOARD_CONFIG.fontSize.winText, BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
text,
{
fontSize: '36px',
fontFamily: 'Arial', fontFamily: 'Arial',
color: BOARD_CONFIG.colors.winText, color: '#fbbf24',
}).setOrigin(0.5); },
).setOrigin(0.5);
this.winnerOverlay.add(winText); this.winnerOverlay.add(winText);
@ -243,14 +198,8 @@ export class GameScene extends GameHostScene<TicTacToeState> {
repeat: 1, repeat: 1,
}); });
} }
/** 跳转到菜单场景 */
private async goToMenu(): Promise<void> {
await this.sceneController.launch('MenuScene');
}
} }
/** 棋子生成器 */
class TicTacToePartSpawner implements Spawner<TicTacToePart, Phaser.GameObjects.Text> { class TicTacToePartSpawner implements Spawner<TicTacToePart, Phaser.GameObjects.Text> {
constructor(public readonly scene: GameScene) {} constructor(public readonly scene: GameScene) {}
@ -259,23 +208,24 @@ class TicTacToePartSpawner implements Spawner<TicTacToePart, Phaser.GameObjects.
yield part; yield part;
} }
} }
getKey(part: TicTacToePart): string { getKey(part: TicTacToePart): string {
return part.id; return part.id;
} }
onUpdate(part: TicTacToePart, obj: Phaser.GameObjects.Text): void { onUpdate(part: TicTacToePart, obj: Phaser.GameObjects.Text): void {
this.updatePosition(part, obj); const [yIndex, xIndex] = part.position;
const x = xIndex * CELL_SIZE + BOARD_OFFSET.x;
const y = yIndex * CELL_SIZE + BOARD_OFFSET.y;
obj.x = x + CELL_SIZE / 2;
obj.y = y + CELL_SIZE / 2;
} }
onSpawn(part: TicTacToePart) { onSpawn(part: TicTacToePart) {
const { cellSize, boardOffset } = BOARD_CONFIG; const [yIndex, xIndex] = part.position;
const pos = this.calculatePosition(part.position); const x = xIndex * CELL_SIZE + BOARD_OFFSET.x;
const y = yIndex * CELL_SIZE + BOARD_OFFSET.y;
const text = this.scene.add.text(pos.x, pos.y, part.player, { const text = this.scene.add.text(x + CELL_SIZE / 2, y + CELL_SIZE / 2, part.player, {
fontSize: BOARD_CONFIG.fontSize.cell, fontSize: '64px',
fontFamily: 'Arial', fontFamily: 'Arial',
color: part.player === 'X' ? BOARD_CONFIG.colors.x : BOARD_CONFIG.colors.o, color: part.player === 'X' ? '#3b82f6' : '#ef4444',
}).setOrigin(0.5); }).setOrigin(0.5);
// 添加落子动画 // 添加落子动画
@ -299,21 +249,4 @@ class TicTacToePartSpawner implements Spawner<TicTacToePart, Phaser.GameObjects.
onComplete: () => obj.destroy(), onComplete: () => obj.destroy(),
}); });
} }
/** 计算格子的屏幕位置 */
private calculatePosition(position: number[]): { x: number; y: number } {
const { cellSize, boardOffset } = BOARD_CONFIG;
const [yIndex, xIndex] = position;
return {
x: xIndex * cellSize + boardOffset.x + cellSize / 2,
y: yIndex * cellSize + boardOffset.y + cellSize / 2,
};
}
/** 更新对象位置 */
private updatePosition(part: TicTacToePart, obj: Phaser.GameObjects.Text): void {
const pos = this.calculatePosition(part.position);
obj.x = pos.x;
obj.y = pos.y;
}
} }

View File

@ -1,35 +1,10 @@
import { ReactiveScene } from 'boardgame-phaser'; import { ReactiveScene } from 'boardgame-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 { export class MenuScene extends ReactiveScene {
private titleText!: Phaser.GameObjects.Text; private titleText!: Phaser.GameObjects.Text;
private startButtonContainer!: Phaser.GameObjects.Container; private startButton!: Phaser.GameObjects.Container;
private startButtonBg!: Phaser.GameObjects.Rectangle;
private startButtonText!: Phaser.GameObjects.Text; private startButtonText!: Phaser.GameObjects.Text;
private startButtonBg!: Phaser.GameObjects.Rectangle;
constructor() { constructor() {
super('MenuScene'); super('MenuScene');
@ -38,35 +13,17 @@ export class MenuScene extends ReactiveScene {
create(): void { create(): void {
super.create(); super.create();
const center = this.getCenterPosition(); const centerX = this.game.scale.width / 2;
const centerY = this.game.scale.height / 2;
this.createTitle(center); // 标题
this.createStartButton(center); this.titleText = this.add.text(centerX, centerY - 100, 'Tic-Tac-Toe', {
this.createSubtitle(center); fontSize: '48px',
}
/** 获取屏幕中心位置 */
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,
'Tic-Tac-Toe',
{
fontSize: MENU_CONFIG.fontSize.title,
fontFamily: 'Arial', fontFamily: 'Arial',
color: MENU_CONFIG.colors.title, color: '#1f2937',
} }).setOrigin(0.5);
).setOrigin(0.5);
// 标题入场动画 // 添加标题动画
this.titleText.setScale(0); this.titleText.setScale(0);
this.tweens.add({ this.tweens.add({
targets: this.titleText, targets: this.titleText,
@ -74,56 +31,36 @@ export class MenuScene extends ReactiveScene {
duration: 600, duration: 600,
ease: 'Back.easeOut', ease: 'Back.easeOut',
}); });
}
/** 创建开始按钮 */ // 开始按钮
private createStartButton(center: { x: number; y: number }): void { this.startButtonBg = this.add.rectangle(centerX, centerY + 40, 200, 60, 0x3b82f6)
const { button, colors } = MENU_CONFIG; .setInteractive({ useHandCursor: true });
this.startButtonBg = this.add.rectangle( this.startButtonText = this.add.text(centerX, centerY + 40, 'Start Game', {
-button.width/2, fontSize: '24px',
-button.height/2,
button.width,
button.height,
colors.buttonBg
).setOrigin(0.5).setInteractive({ useHandCursor: true });
this.startButtonText = this.add.text(
-button.width/2,
-button.height/2,
'Start Game',
{
fontSize: MENU_CONFIG.fontSize.button,
fontFamily: 'Arial', fontFamily: 'Arial',
color: colors.buttonText, color: '#ffffff',
} }).setOrigin(0.5);
).setOrigin(0.5);
this.startButtonContainer = this.add.container( this.startButton = this.add.container(centerX, centerY + 40, [
center.x, this.startButtonBg,
center.y + MENU_CONFIG.positions.buttonY, this.startButtonText,
[this.startButtonBg, this.startButtonText] ]);
);
// 按钮交互 // 按钮交互
this.setupButtonInteraction();
}
/** 设置按钮交互效果 */
private setupButtonInteraction(): void {
this.startButtonBg.on('pointerover', () => { this.startButtonBg.on('pointerover', () => {
this.startButtonBg.setFillStyle(MENU_CONFIG.colors.buttonBgHover); this.startButtonBg.setFillStyle(0x2563eb);
this.tweens.add({ this.tweens.add({
targets: this.startButtonContainer, targets: this.startButton,
scale: 1.05, scale: 1.05,
duration: 100, duration: 100,
}); });
}); });
this.startButtonBg.on('pointerout', () => { this.startButtonBg.on('pointerout', () => {
this.startButtonBg.setFillStyle(MENU_CONFIG.colors.buttonBg); this.startButtonBg.setFillStyle(0x3b82f6);
this.tweens.add({ this.tweens.add({
targets: this.startButtonContainer, targets: this.startButton,
scale: 1, scale: 1,
duration: 100, duration: 100,
}); });
@ -132,23 +69,15 @@ export class MenuScene extends ReactiveScene {
this.startButtonBg.on('pointerdown', () => { this.startButtonBg.on('pointerdown', () => {
this.startGame(); this.startGame();
}); });
}
/** 创建副标题 */ // 副标题
private createSubtitle(center: { x: number; y: number }): void { this.add.text(centerX, centerY + 140, 'Click to start playing', {
this.add.text( fontSize: '16px',
center.x,
center.y + MENU_CONFIG.positions.subtitleY,
'Click to start playing',
{
fontSize: MENU_CONFIG.fontSize.subtitle,
fontFamily: 'Arial', fontFamily: 'Arial',
color: MENU_CONFIG.colors.subtitle, color: '#6b7280',
} }).setOrigin(0.5);
).setOrigin(0.5);
} }
/** 开始游戏 */
private async startGame(): Promise<void> { private async startGame(): Promise<void> {
await this.sceneController.launch('GameScene'); await this.sceneController.launch('GameScene');
} }