refactor: human effort to reorg the code

This commit is contained in:
2026-04-04 12:14:26 +08:00
parent e8c98d3ad3
commit f83e031c9a
8 changed files with 283 additions and 255 deletions
@@ -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));
}
}
+77 -94
View File
@@ -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();
}
}