boardgame-phaser/packages/sample-game/src/scenes/GameScene.ts

182 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-04-03 15:18:47 +08:00
import Phaser from 'phaser';
import type {TicTacToeState, TicTacToePart} from '@/game/tic-tac-toe';
import {ReadonlySignal} from "@preact/signals";
import {GameHostScene} from "@/scenes/GameHostScene";
import {spawnEffect, Spawner} from "@/utils/spawner";
2026-04-03 15:18:47 +08:00
const CELL_SIZE = 120;
const BOARD_OFFSET = { x: 100, y: 100 };
const BOARD_SIZE = 3;
export class GameScene extends GameHostScene<TicTacToeState> {
2026-04-03 15:18:47 +08:00
private boardContainer!: Phaser.GameObjects.Container;
private gridGraphics!: Phaser.GameObjects.Graphics;
private turnText!: Phaser.GameObjects.Text;
private winnerOverlay?: Phaser.GameObjects.Container;
2026-04-03 15:18:47 +08:00
constructor() {
super('GameScene');
}
create(): void {
2026-04-03 16:18:44 +08:00
super.create();
2026-04-03 15:18:47 +08:00
this.boardContainer = this.add.container(0, 0);
this.gridGraphics = this.add.graphics();
this.drawGrid();
this.disposables.add(spawnEffect(new TicTacToePartSpawner(this, this.gameHost.state)));
2026-04-03 15:18:47 +08:00
this.watch(() => {
const winner = this.state.winner;
2026-04-03 15:18:47 +08:00
if (winner) {
this.showWinner(winner);
}
});
this.watch(() => {
const currentPlayer = this.state.currentPlayer;
2026-04-03 15:18:47 +08:00
this.updateTurnText(currentPlayer);
});
this.setupInput();
}
2026-04-03 19:39:07 +08:00
private isCellOccupied(row: number, col: number): boolean {
return !!this.state.board.partMap[`${row},${col}`];
2026-04-03 19:39:07 +08:00
}
2026-04-03 15:18:47 +08:00
private setupInput(): void {
// todo
2026-04-04 00:16:30 +08:00
}
2026-04-03 15:18:47 +08:00
private drawGrid(): void {
const g = this.gridGraphics;
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);
2026-04-03 15:18:47 +08:00
}
private updateTurnText(player: string): void {
if (this.turnText) {
this.turnText.setText(`${player}'s turn`);
}
}
private showWinner(winner: string): void {
// 清理旧的覆盖层防止叠加
if (this.winnerOverlay) {
this.winnerOverlay.destroy();
}
this.winnerOverlay = this.add.container();
2026-04-03 15:18:47 +08:00
const text = winner === 'draw' ? "It's a draw!" : `${winner} wins!`;
this.winnerOverlay.add(
this.add.rectangle(
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
BOARD_SIZE * CELL_SIZE,
BOARD_SIZE * CELL_SIZE,
0x000000,
0.6,
),
2026-04-03 15:18:47 +08:00
);
const winText = this.add.text(
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
text,
{
fontSize: '36px',
fontFamily: 'Arial',
color: '#fbbf24',
},
).setOrigin(0.5);
this.winnerOverlay.add(winText);
2026-04-03 15:18:47 +08:00
this.tweens.add({
targets: winText,
scale: 1.2,
duration: 500,
yoyo: true,
repeat: 1,
});
}
}
class TicTacToePartSpawner implements Spawner<TicTacToePart, Phaser.GameObjects.Text> {
constructor(public readonly scene: GameScene, public readonly state: ReadonlySignal<TicTacToeState>) {}
*getData() {
for (const part of Object.values(this.state.value.parts)) {
yield part;
}
}
getKey(part: TicTacToePart): string {
return part.id;
}
onUpdate(part: TicTacToePart, obj: Phaser.GameObjects.Text): void {
const [xIndex, yIndex] = part.position;
const x = xIndex * CELL_SIZE + BOARD_OFFSET.x;
const y = yIndex * CELL_SIZE + BOARD_OFFSET.y;
obj.x = x;
obj.y = y;
}
onSpawn(part: TicTacToePart) {
const [xIndex, yIndex] = part.position;
const x = xIndex * CELL_SIZE + BOARD_OFFSET.x;
const y = yIndex * CELL_SIZE + BOARD_OFFSET.y;
const text = this.scene.add.text(x + CELL_SIZE / 2, y + CELL_SIZE / 2, part.player, {
fontSize: '64px',
fontFamily: 'Arial',
color: part.player === 'X' ? '#3b82f6' : '#ef4444',
}).setOrigin(0.5);
// 添加落子动画
text.setScale(0);
this.scene.tweens.add({
targets: text,
scale: 1,
duration: 200,
ease: 'Back.easeOut',
});
return text;
}
onDespawn(obj: Phaser.GameObjects.Text) {
obj.removedFromScene();
}
}