refactor: human effort to reorg the code
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import {DisposableBag, IDisposable} from "@/utils/disposable";
|
||||
import type {GameHost} from "../../../../../boardgame-core/src";
|
||||
import {effect} from "@preact/signals";
|
||||
|
||||
export abstract class GameHostScene<T extends Record<string,unknown>> extends Phaser.Scene implements IDisposable{
|
||||
protected disposables = new DisposableBag();
|
||||
|
||||
protected gameHost!: GameHost<T>;
|
||||
|
||||
init(data: { gameHost: GameHost<T> }): void {
|
||||
this.gameHost = data.gameHost;
|
||||
}
|
||||
create(){
|
||||
this.events.on('shutdown', this.dispose, this);
|
||||
}
|
||||
dispose() {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
public get state(): T {
|
||||
return this.gameHost.state.value;
|
||||
}
|
||||
|
||||
protected watch(fn: () => void | (()=>void)){
|
||||
this.disposables.add(effect(fn));
|
||||
}
|
||||
}
|
||||
@@ -1,126 +1,53 @@
|
||||
import Phaser from 'phaser';
|
||||
import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe';
|
||||
import type { GameHost, CommandSchema, IGameContext, MutableSignal } from 'boardgame-core';
|
||||
import { ReactiveScene, bindRegion, createInputMapper, InputMapper } from 'boardgame-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";
|
||||
|
||||
const CELL_SIZE = 120;
|
||||
const BOARD_OFFSET = { x: 100, y: 100 };
|
||||
const BOARD_SIZE = 3;
|
||||
|
||||
export class GameScene extends ReactiveScene<TicTacToeState> {
|
||||
export class GameScene extends GameHostScene<TicTacToeState> {
|
||||
private boardContainer!: Phaser.GameObjects.Container;
|
||||
private gridGraphics!: Phaser.GameObjects.Graphics;
|
||||
private inputMapper!: InputMapper;
|
||||
private turnText!: Phaser.GameObjects.Text;
|
||||
/** Receives the active prompt schema from main.tsx */
|
||||
promptSchema: { current: CommandSchema | null } = { current: null };
|
||||
/** GameHost instance passed from main.tsx */
|
||||
private gameHost!: GameHost<TicTacToeState>;
|
||||
private winnerOverlay?: Phaser.GameObjects.Container;
|
||||
|
||||
constructor() {
|
||||
super('GameScene');
|
||||
}
|
||||
|
||||
init(data: { gameHost: GameHost<TicTacToeState> } | { gameContext: IGameContext<TicTacToeState> }): void {
|
||||
if ('gameHost' in data) {
|
||||
this.gameHost = data.gameHost;
|
||||
// Create a compatible gameContext from GameHost
|
||||
this.gameContext = {
|
||||
state: this.gameHost.state as MutableSignal<TicTacToeState>,
|
||||
commands: this.gameHost.commands,
|
||||
} as IGameContext<TicTacToeState>;
|
||||
this.state = this.gameContext.state;
|
||||
this.commands = this.gameContext.commands;
|
||||
} else {
|
||||
// Fallback for direct gameContext passing
|
||||
super.init(data);
|
||||
}
|
||||
}
|
||||
|
||||
protected onStateReady(_state: TicTacToeState): void {
|
||||
}
|
||||
|
||||
create(): void {
|
||||
super.create();
|
||||
|
||||
this.boardContainer = this.add.container(0, 0);
|
||||
this.gridGraphics = this.add.graphics();
|
||||
this.drawGrid();
|
||||
|
||||
this.disposables.add(spawnEffect(new TicTacToePartSpawner(this, this.gameHost.state)));
|
||||
|
||||
this.watch(() => {
|
||||
const winner = this.state.value.winner;
|
||||
const winner = this.state.winner;
|
||||
if (winner) {
|
||||
this.showWinner(winner);
|
||||
}
|
||||
});
|
||||
|
||||
this.watch(() => {
|
||||
const currentPlayer = this.state.value.currentPlayer;
|
||||
const currentPlayer = this.state.currentPlayer;
|
||||
this.updateTurnText(currentPlayer);
|
||||
});
|
||||
|
||||
this.setupInput();
|
||||
}
|
||||
|
||||
protected setupBindings(): void {
|
||||
bindRegion<TicTacToeState, { player: PlayerType }>(
|
||||
this.state,
|
||||
(state) => state.parts,
|
||||
(state) => state.board,
|
||||
{
|
||||
cellSize: { x: CELL_SIZE, y: CELL_SIZE },
|
||||
offset: BOARD_OFFSET,
|
||||
factory: (part, pos: Phaser.Math.Vector2) => {
|
||||
const text = this.add.text(pos.x + CELL_SIZE / 2, pos.y + CELL_SIZE / 2, part.player, {
|
||||
fontSize: '64px',
|
||||
fontFamily: 'Arial',
|
||||
color: part.player === 'X' ? '#3b82f6' : '#ef4444',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// 添加落子动画
|
||||
text.setScale(0);
|
||||
this.tweens.add({
|
||||
targets: text,
|
||||
scale: 1,
|
||||
duration: 200,
|
||||
ease: 'Back.easeOut',
|
||||
});
|
||||
|
||||
return text;
|
||||
},
|
||||
update: (part, obj) => {
|
||||
// 可以在这里更新部件的视觉状态
|
||||
},
|
||||
},
|
||||
this.boardContainer,
|
||||
);
|
||||
}
|
||||
|
||||
private isCellOccupied(row: number, col: number): boolean {
|
||||
const state = this.state.value;
|
||||
return !!state.board.partMap[`${row},${col}`];
|
||||
return !!this.state.board.partMap[`${row},${col}`];
|
||||
}
|
||||
|
||||
private setupInput(): void {
|
||||
this.inputMapper = createInputMapper(this, {
|
||||
onSubmit: (cmd: string) => {
|
||||
// Directly submit to GameHost
|
||||
return this.gameHost.onInput(cmd);
|
||||
}
|
||||
});
|
||||
|
||||
this.inputMapper.mapGridClick(
|
||||
{ x: CELL_SIZE, y: CELL_SIZE },
|
||||
BOARD_OFFSET,
|
||||
{ cols: BOARD_SIZE, rows: BOARD_SIZE },
|
||||
(col, row) => {
|
||||
if (this.state.value.winner) return null;
|
||||
|
||||
const currentPlayer = this.state.value.currentPlayer;
|
||||
if (this.isCellOccupied(row, col)) return null;
|
||||
|
||||
return `play ${currentPlayer} ${row} ${col}`;
|
||||
},
|
||||
);
|
||||
// todo
|
||||
}
|
||||
|
||||
private drawGrid(): void {
|
||||
@@ -156,7 +83,7 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
|
||||
color: '#4b5563',
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.updateTurnText(this.state.value.currentPlayer);
|
||||
this.updateTurnText(this.state.currentPlayer);
|
||||
}
|
||||
|
||||
private updateTurnText(player: string): void {
|
||||
@@ -166,15 +93,24 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
|
||||
}
|
||||
|
||||
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!`;
|
||||
|
||||
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,
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
const winText = this.add.text(
|
||||
@@ -188,6 +124,8 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
|
||||
},
|
||||
).setOrigin(0.5);
|
||||
|
||||
this.winnerOverlay.add(winText);
|
||||
|
||||
this.tweens.add({
|
||||
targets: winText,
|
||||
scale: 1.2,
|
||||
@@ -197,3 +135,48 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user