boardgame-phaser/packages/onitama-game/src/scenes/OnitamaScene.ts

288 lines
7.8 KiB
TypeScript
Raw Normal View History

2026-04-07 16:29:21 +08:00
import Phaser from 'phaser';
2026-04-08 11:06:34 +08:00
import type { OnitamaState, Pawn } from '@/game/onitama';
import {getAvailableMoves, prompts} from '@/game/onitama';
2026-04-07 16:29:21 +08:00
import { GameHostScene } from 'boardgame-phaser';
2026-04-07 17:13:45 +08:00
import { spawnEffect } from 'boardgame-phaser';
2026-04-08 08:50:45 +08:00
import type { MutableSignal } from 'boardgame-core';
2026-04-08 11:06:34 +08:00
import {
PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT, boardToScreen, BOARD_SIZE,
HighlightSpawner
} from '@/spawners';
2026-04-07 17:13:45 +08:00
import type { HighlightData } from '@/spawners/HighlightSpawner';
2026-04-08 11:06:34 +08:00
import {createUIState, clearSelection, selectPiece, selectCard, createValidMoves} from '@/state';
2026-04-07 17:13:45 +08:00
import type { OnitamaUIState, ValidMove } from '@/state';
2026-04-07 16:29:21 +08:00
export class OnitamaScene extends GameHostScene<OnitamaState> {
private boardContainer!: Phaser.GameObjects.Container;
private gridGraphics!: Phaser.GameObjects.Graphics;
private infoText!: Phaser.GameObjects.Text;
private winnerOverlay?: Phaser.GameObjects.Container;
2026-04-07 17:13:45 +08:00
private cardLabelContainers: Map<string, Phaser.GameObjects.Text> = new Map();
2026-04-08 08:50:45 +08:00
// UI State managed by MutableSignal
public uiState!: MutableSignal<OnitamaUIState>;
2026-04-07 16:29:21 +08:00
constructor() {
super('OnitamaScene');
}
create(): void {
super.create();
2026-04-07 17:13:45 +08:00
// Create UI state signal
2026-04-08 11:06:34 +08:00
this.uiState = createUIState();
2026-04-07 17:13:45 +08:00
2026-04-07 16:29:21 +08:00
this.boardContainer = this.add.container(0, 0);
this.gridGraphics = this.add.graphics();
this.drawBoard();
2026-04-07 17:13:45 +08:00
// Add spawners
2026-04-07 16:29:21 +08:00
this.disposables.add(spawnEffect(new PawnSpawner(this)));
2026-04-07 17:13:45 +08:00
this.disposables.add(spawnEffect(new CardSpawner(this)));
2026-04-08 11:06:34 +08:00
this.disposables.add(spawnEffect(new HighlightSpawner(this)));
2026-04-07 16:29:21 +08:00
2026-04-07 17:13:45 +08:00
// Create card labels
this.createCardLabels();
2026-04-07 16:29:21 +08:00
2026-04-07 17:13:45 +08:00
// Winner overlay effect
2026-04-07 16:29:21 +08:00
this.addEffect(() => {
const winner = this.state.winner;
if (winner) {
this.showWinner(winner);
} else if (this.winnerOverlay) {
this.winnerOverlay.destroy();
this.winnerOverlay = undefined;
}
});
2026-04-07 17:13:45 +08:00
// Info text
this.infoText = this.add.text(
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 30,
'',
{
fontSize: '20px',
fontFamily: 'Arial',
color: '#4b5563',
}
).setOrigin(0.5);
2026-04-07 16:29:21 +08:00
2026-04-07 17:13:45 +08:00
// Update info text when UI state changes
2026-04-07 16:29:21 +08:00
this.addEffect(() => {
this.updateInfoText();
});
2026-04-07 17:13:45 +08:00
// Input handling
2026-04-07 16:29:21 +08:00
this.setupInput();
}
2026-04-07 17:13:45 +08:00
private createCardLabels(): void {
const boardLeft = BOARD_OFFSET.x;
const boardTop = BOARD_OFFSET.y;
const boardRight = BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE;
// Red cards label
const redLabel = this.add.text(
boardLeft - CARD_WIDTH - 60 + CARD_WIDTH / 2,
boardTop + 50 + 2 * (CARD_HEIGHT + 15) + 10,
"RED's Cards",
{
fontSize: '16px',
fontFamily: 'Arial',
color: '#ef4444',
}
).setOrigin(0.5, 0);
this.cardLabelContainers.set('red', redLabel);
// Black cards label
const blackLabel = this.add.text(
boardRight + 60 + CARD_WIDTH / 2,
boardTop + 50 + 2 * (CARD_HEIGHT + 15) + 10,
"BLACK's Cards",
{
fontSize: '16px',
fontFamily: 'Arial',
color: '#3b82f6',
}
).setOrigin(0.5, 0);
this.cardLabelContainers.set('black', blackLabel);
// Spare card label
const boardCenterX = boardLeft + (BOARD_SIZE * CELL_SIZE) / 2;
const spareLabel = this.add.text(
boardCenterX,
boardTop - 50,
'Spare Card',
{
fontSize: '16px',
fontFamily: 'Arial',
color: '#6b7280',
}
).setOrigin(0.5, 0);
this.cardLabelContainers.set('spare', spareLabel);
}
2026-04-07 16:29:21 +08:00
private updateInfoText(): void {
const currentPlayer = this.state.currentPlayer;
2026-04-07 17:13:45 +08:00
const selectedCard = this.uiState.value.selectedCard;
const selectedPiece = this.uiState.value.selectedPiece;
2026-04-07 16:29:21 +08:00
if (this.state.winner) {
this.infoText.setText(`${this.state.winner} wins!`);
2026-04-07 17:13:45 +08:00
} else if (!selectedCard) {
this.infoText.setText(`${currentPlayer}'s turn - Select a card first`);
} else if (!selectedPiece) {
this.infoText.setText(`Card: ${selectedCard} - Select a piece to move`);
2026-04-07 16:29:21 +08:00
} else {
this.infoText.setText(`${currentPlayer}'s turn (Turn ${this.state.turn + 1})`);
}
}
private drawBoard(): void {
const g = this.gridGraphics;
g.lineStyle(2, 0x6b7280);
for (let i = 0; i <= BOARD_SIZE; i++) {
g.lineBetween(
BOARD_OFFSET.x + i * CELL_SIZE,
BOARD_OFFSET.y,
BOARD_OFFSET.x + i * CELL_SIZE,
2026-04-07 17:13:45 +08:00
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE
2026-04-07 16:29:21 +08:00
);
g.lineBetween(
BOARD_OFFSET.x,
BOARD_OFFSET.y + i * CELL_SIZE,
BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE,
2026-04-07 17:13:45 +08:00
BOARD_OFFSET.y + i * CELL_SIZE
2026-04-07 16:29:21 +08:00
);
}
g.strokePath();
2026-04-07 17:13:45 +08:00
this.add.text(
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
BOARD_OFFSET.y - 40,
'Onitama',
{
fontSize: '28px',
fontFamily: 'Arial',
color: '#1f2937',
}
).setOrigin(0.5);
2026-04-07 16:29:21 +08:00
}
private setupInput(): void {
2026-04-07 17:13:45 +08:00
// Board cell clicks
2026-04-07 16:29:21 +08:00
for (let row = 0; row < BOARD_SIZE; row++) {
for (let col = 0; col < BOARD_SIZE; col++) {
2026-04-08 08:50:45 +08:00
const pos = boardToScreen(col, row);
2026-04-07 16:29:21 +08:00
2026-04-08 08:50:45 +08:00
const zone = this.add.zone(pos.x, pos.y, CELL_SIZE, CELL_SIZE).setInteractive();
2026-04-07 16:29:21 +08:00
zone.on('pointerdown', () => {
if (this.state.winner) return;
this.handleCellClick(col, row);
});
}
}
}
private handleCellClick(x: number, y: number): void {
const pawn = this.getPawnAtPosition(x, y);
2026-04-08 11:06:34 +08:00
if(pawn?.owner !== this.state.currentPlayer){
2026-04-07 17:13:45 +08:00
return;
}
2026-04-08 11:06:34 +08:00
selectPiece(this.uiState, x, y);
2026-04-07 16:29:21 +08:00
}
2026-04-07 17:13:45 +08:00
public onCardClick(cardId: string): void {
// 只能选择当前玩家的手牌
2026-04-07 16:29:21 +08:00
const currentPlayer = this.state.currentPlayer;
2026-04-07 17:13:45 +08:00
const playerCards = currentPlayer === 'red' ? this.state.redCards : this.state.blackCards;
if (!playerCards.includes(cardId)) {
return;
}
selectCard(this.uiState, cardId);
}
public onHighlightClick(data: HighlightData): void {
clearSelection(this.uiState);
this.executeMove({
card: data.card,
fromX: data.fromX,
fromY: data.fromY,
toX: data.toX,
toY: data.toY,
});
}
private executeMove(move: { card: string; fromX: number; fromY: number; toX: number; toY: number }): void {
const error = this.gameHost.tryAnswerPrompt(
prompts.move,
this.state.currentPlayer,
move.card,
move.fromX,
move.fromY,
move.toX,
move.toY
);
if (error) {
console.warn('Invalid move:', error);
}
}
2026-04-07 16:29:21 +08:00
private getPawnAtPosition(x: number, y: number): Pawn | null {
const key = `${x},${y}`;
const pawnId = this.state.regions.board.partMap[key];
return pawnId ? this.state.pawns[pawnId] : null;
}
private showWinner(winner: string): void {
if (this.winnerOverlay) {
this.winnerOverlay.destroy();
}
this.winnerOverlay = this.add.container();
const text = winner === 'draw' ? "It's a draw!" : `${winner} wins!`;
const bg = 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,
2026-04-07 17:13:45 +08:00
0.6
2026-04-07 16:29:21 +08:00
).setInteractive({ useHandCursor: true });
bg.on('pointerdown', () => {
this.gameHost.start();
});
this.winnerOverlay.add(bg);
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',
2026-04-07 17:13:45 +08:00
}
2026-04-07 16:29:21 +08:00
).setOrigin(0.5);
this.winnerOverlay.add(winText);
this.tweens.add({
targets: winText,
scale: 1.2,
duration: 500,
yoyo: true,
repeat: 1,
});
}
}