fix: more issues with ui
This commit is contained in:
parent
2beff5c75c
commit
00fd395873
|
|
@ -1,13 +1,15 @@
|
||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import type { OnitamaState, PlayerType, Pawn } from '@/game/onitama';
|
import type { OnitamaState, Pawn } from '@/game/onitama';
|
||||||
import { prompts } from '@/game/onitama';
|
import {getAvailableMoves, prompts} from '@/game/onitama';
|
||||||
import { GameHostScene } from 'boardgame-phaser';
|
import { GameHostScene } from 'boardgame-phaser';
|
||||||
import { spawnEffect } from 'boardgame-phaser';
|
import { spawnEffect } from 'boardgame-phaser';
|
||||||
import { effect } from '@preact/signals-core';
|
|
||||||
import type { MutableSignal } from 'boardgame-core';
|
import type { MutableSignal } from 'boardgame-core';
|
||||||
import { PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT, boardToScreen, BOARD_SIZE } from '@/spawners';
|
import {
|
||||||
|
PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT, boardToScreen, BOARD_SIZE,
|
||||||
|
HighlightSpawner
|
||||||
|
} from '@/spawners';
|
||||||
import type { HighlightData } from '@/spawners/HighlightSpawner';
|
import type { HighlightData } from '@/spawners/HighlightSpawner';
|
||||||
import { createOnitamaUIState, clearSelection, selectPiece, selectCard, deselectCard, setValidMoves } from '@/state';
|
import {createUIState, clearSelection, selectPiece, selectCard, createValidMoves} from '@/state';
|
||||||
import type { OnitamaUIState, ValidMove } from '@/state';
|
import type { OnitamaUIState, ValidMove } from '@/state';
|
||||||
|
|
||||||
export class OnitamaScene extends GameHostScene<OnitamaState> {
|
export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
|
|
@ -19,8 +21,6 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
|
|
||||||
// UI State managed by MutableSignal
|
// UI State managed by MutableSignal
|
||||||
public uiState!: MutableSignal<OnitamaUIState>;
|
public uiState!: MutableSignal<OnitamaUIState>;
|
||||||
private highlightContainers: Map<string, Phaser.GameObjects.GameObject> = new Map();
|
|
||||||
private highlightDispose?: () => void;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('OnitamaScene');
|
super('OnitamaScene');
|
||||||
|
|
@ -30,14 +30,7 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
super.create();
|
super.create();
|
||||||
|
|
||||||
// Create UI state signal
|
// Create UI state signal
|
||||||
this.uiState = createOnitamaUIState();
|
this.uiState = createUIState();
|
||||||
|
|
||||||
// Cleanup effect on scene shutdown
|
|
||||||
this.events.once('shutdown', () => {
|
|
||||||
if (this.highlightDispose) {
|
|
||||||
this.highlightDispose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.boardContainer = this.add.container(0, 0);
|
this.boardContainer = this.add.container(0, 0);
|
||||||
this.gridGraphics = this.add.graphics();
|
this.gridGraphics = this.add.graphics();
|
||||||
|
|
@ -46,16 +39,11 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
// Add spawners
|
// Add spawners
|
||||||
this.disposables.add(spawnEffect(new PawnSpawner(this)));
|
this.disposables.add(spawnEffect(new PawnSpawner(this)));
|
||||||
this.disposables.add(spawnEffect(new CardSpawner(this)));
|
this.disposables.add(spawnEffect(new CardSpawner(this)));
|
||||||
|
this.disposables.add(spawnEffect(new HighlightSpawner(this)));
|
||||||
|
|
||||||
// Create card labels
|
// Create card labels
|
||||||
this.createCardLabels();
|
this.createCardLabels();
|
||||||
|
|
||||||
// Setup highlight effect - react to validMoves changes
|
|
||||||
this.highlightDispose = effect(() => {
|
|
||||||
const validMoves = this.uiState.value.validMoves;
|
|
||||||
this.updateHighlights(validMoves);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Winner overlay effect
|
// Winner overlay effect
|
||||||
this.addEffect(() => {
|
this.addEffect(() => {
|
||||||
const winner = this.state.winner;
|
const winner = this.state.winner;
|
||||||
|
|
@ -201,43 +189,10 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
|
|
||||||
private handleCellClick(x: number, y: number): void {
|
private handleCellClick(x: number, y: number): void {
|
||||||
const pawn = this.getPawnAtPosition(x, y);
|
const pawn = this.getPawnAtPosition(x, y);
|
||||||
|
if(pawn?.owner !== this.state.currentPlayer){
|
||||||
// 如果没有选中卡牌,提示先选卡牌
|
|
||||||
if (!this.uiState.value.selectedCard) {
|
|
||||||
console.log('请先选择一张卡牌');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.uiState.value.selectedPiece) {
|
|
||||||
// 已经选中了棋子
|
|
||||||
if (pawn && pawn.owner === this.state.currentPlayer) {
|
|
||||||
// 点击了自己的另一个棋子,更新选择
|
|
||||||
selectPiece(this.uiState, x, y);
|
selectPiece(this.uiState, x, y);
|
||||||
this.updateValidMoves();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromX = this.uiState.value.selectedPiece.x;
|
|
||||||
const fromY = this.uiState.value.selectedPiece.y;
|
|
||||||
|
|
||||||
if (pawn && pawn.owner === this.state.currentPlayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试移动到目标位置,必须使用选中的卡牌
|
|
||||||
const validMoves = this.getValidMovesForPiece(fromX, fromY, [this.uiState.value.selectedCard]);
|
|
||||||
|
|
||||||
const targetMove = validMoves.find(m => m.toX === x && m.toY === y);
|
|
||||||
if (targetMove) {
|
|
||||||
this.executeMove(targetMove);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 还没有选中棋子
|
|
||||||
if (pawn && pawn.owner === this.state.currentPlayer) {
|
|
||||||
selectPiece(this.uiState, x, y);
|
|
||||||
this.updateValidMoves();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCardClick(cardId: string): void {
|
public onCardClick(cardId: string): void {
|
||||||
|
|
@ -250,23 +205,6 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectCard(this.uiState, cardId);
|
selectCard(this.uiState, cardId);
|
||||||
// 如果已经选中了棋子,更新有效移动
|
|
||||||
if (this.uiState.value.selectedPiece) {
|
|
||||||
this.updateValidMoves();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateValidMoves(): void {
|
|
||||||
const selectedPiece = this.uiState.value.selectedPiece;
|
|
||||||
const selectedCard = this.uiState.value.selectedCard;
|
|
||||||
|
|
||||||
if (!selectedPiece || !selectedCard) {
|
|
||||||
setValidMoves(this.uiState, []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const moves = this.getValidMovesForPiece(selectedPiece.x, selectedPiece.y, [selectedCard]);
|
|
||||||
setValidMoves(this.uiState, moves);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onHighlightClick(data: HighlightData): void {
|
public onHighlightClick(data: HighlightData): void {
|
||||||
|
|
@ -295,80 +233,6 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateHighlights(validMoves: ValidMove[]): void {
|
|
||||||
// Clear old highlights
|
|
||||||
for (const [, circle] of this.highlightContainers) {
|
|
||||||
circle.destroy();
|
|
||||||
}
|
|
||||||
this.highlightContainers.clear();
|
|
||||||
|
|
||||||
// Create new highlights
|
|
||||||
for (const move of validMoves) {
|
|
||||||
const key = `${move.card}-${move.toX}-${move.toY}`;
|
|
||||||
const pos = boardToScreen(move.toX, move.toY);
|
|
||||||
|
|
||||||
const circle = this.add.circle(pos.x, pos.y, CELL_SIZE / 3, 0x3b82f6, 0.3).setDepth(100);
|
|
||||||
circle.setInteractive({ useHandCursor: true });
|
|
||||||
circle.on('pointerdown', () => {
|
|
||||||
this.onHighlightClick({
|
|
||||||
key,
|
|
||||||
x: pos.x,
|
|
||||||
y: pos.y,
|
|
||||||
card: move.card,
|
|
||||||
fromX: move.fromX,
|
|
||||||
fromY: move.fromY,
|
|
||||||
toX: move.toX,
|
|
||||||
toY: move.toY,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.highlightContainers.set(key, circle as Phaser.GameObjects.GameObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getValidMovesForPiece(
|
|
||||||
fromX: number,
|
|
||||||
fromY: number,
|
|
||||||
cardNames: string[]
|
|
||||||
): ValidMove[] {
|
|
||||||
const moves: ValidMove[] = [];
|
|
||||||
const player = this.state.currentPlayer;
|
|
||||||
|
|
||||||
for (const cardName of cardNames) {
|
|
||||||
const card = this.state.cards[cardName];
|
|
||||||
if (!card) continue;
|
|
||||||
|
|
||||||
for (const move of card.moveCandidates) {
|
|
||||||
const toX = fromX + move.dx;
|
|
||||||
const toY = fromY + move.dy;
|
|
||||||
|
|
||||||
if (this.isValidMove(fromX, fromY, toX, toY, player)) {
|
|
||||||
moves.push({ card: cardName, fromX, fromY, toX, toY });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return moves;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isValidMove(fromX: number, fromY: number, toX: number, toY: number, player: PlayerType): boolean {
|
|
||||||
if (toX < 0 || toX >= BOARD_SIZE || toY < 0 || toY >= BOARD_SIZE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetPawn = this.getPawnAtPosition(toX, toY);
|
|
||||||
if (targetPawn && targetPawn.owner === player) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pawn = this.getPawnAtPosition(fromX, fromY);
|
|
||||||
if (!pawn || pawn.owner !== player) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPawnAtPosition(x: number, y: number): Pawn | null {
|
private getPawnAtPosition(x: number, y: number): Pawn | null {
|
||||||
const key = `${x},${y}`;
|
const key = `${x},${y}`;
|
||||||
const pawnId = this.state.regions.board.partMap[key];
|
const pawnId = this.state.regions.board.partMap[key];
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import type { Spawner } from 'boardgame-phaser';
|
import type { Spawner } from 'boardgame-phaser';
|
||||||
import type { OnitamaScene } from '@/scenes/OnitamaScene';
|
import type { OnitamaScene } from '@/scenes/OnitamaScene';
|
||||||
import type { OnitamaUIState } from '@/state/ui';
|
import {getAvailableMoves} from "boardgame-core/samples/onitama";
|
||||||
import { BOARD_OFFSET, CELL_SIZE } from './PawnSpawner';
|
import {boardToScreen, CELL_SIZE} from './PawnSpawner';
|
||||||
|
|
||||||
export interface HighlightData {
|
export interface HighlightData {
|
||||||
key: string;
|
key: string;
|
||||||
|
|
@ -15,42 +15,77 @@ export interface HighlightData {
|
||||||
toY: number;
|
toY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HighlightSpawner implements Spawner<HighlightData, Phaser.GameObjects.GameObject> {
|
export class HighlightSpawner implements Spawner<HighlightData, Phaser.GameObjects.Container> {
|
||||||
constructor(public readonly scene: OnitamaScene) {}
|
constructor(public readonly scene: OnitamaScene) {}
|
||||||
|
|
||||||
*getData(): Iterable<HighlightData> {
|
*getData(): Iterable<HighlightData> {
|
||||||
// HighlightSpawner 的数据由 UI state 控制,不从这里生成
|
const state = this.scene.state;
|
||||||
// 我们会在 scene 中手动调用 spawnEffect 来更新
|
const uiState = this.scene.uiState.value;
|
||||||
|
|
||||||
|
// 如果没有选择卡牌或棋子,不显示高亮
|
||||||
|
if (!uiState.selectedCard || !uiState.selectedPiece) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPlayer = state.currentPlayer;
|
||||||
|
const availableMoves = getAvailableMoves(state, currentPlayer);
|
||||||
|
|
||||||
|
// 过滤出符合当前选择的移动
|
||||||
|
const filteredMoves = availableMoves.filter(move =>
|
||||||
|
move.fromX === uiState.selectedPiece!.x &&
|
||||||
|
move.fromY === uiState.selectedPiece!.y &&
|
||||||
|
move.card === uiState.selectedCard
|
||||||
|
);
|
||||||
|
|
||||||
|
for(const move of filteredMoves){
|
||||||
|
const pos = boardToScreen(move.toX, move.toY);
|
||||||
|
yield {
|
||||||
|
key: `${move.fromX}-${move.fromY}-${move.card}-${move.toX}-${move.toY}`,
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y,
|
||||||
|
card: move.card,
|
||||||
|
fromX: move.fromX,
|
||||||
|
fromY: move.fromY,
|
||||||
|
toX: move.toX,
|
||||||
|
toY: move.toY
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getKey(data: HighlightData): string {
|
getKey(data: HighlightData): string {
|
||||||
return data.key;
|
return data.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate(data: HighlightData, obj: Phaser.GameObjects.GameObject): void {
|
onUpdate(data: HighlightData, obj: Phaser.GameObjects.Container): void {
|
||||||
if (obj instanceof Phaser.GameObjects.Arc) {
|
|
||||||
obj.setPosition(data.x, data.y);
|
obj.setPosition(data.x, data.y);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onSpawn(data: HighlightData): Phaser.GameObjects.GameObject {
|
onSpawn(data: HighlightData): Phaser.GameObjects.Container {
|
||||||
|
const container = this.scene.add.container(data.x, data.y);
|
||||||
|
|
||||||
const circle = this.scene.add.circle(
|
const circle = this.scene.add.circle(
|
||||||
data.x,
|
0,
|
||||||
data.y,
|
0,
|
||||||
CELL_SIZE / 3,
|
CELL_SIZE / 3,
|
||||||
0x3b82f6,
|
0x3b82f6,
|
||||||
0.3
|
0.3
|
||||||
).setDepth(100);
|
);
|
||||||
|
container.add(circle);
|
||||||
|
|
||||||
circle.setInteractive({ useHandCursor: true });
|
const hitArea = new Phaser.Geom.Circle(0, 0, CELL_SIZE / 3);
|
||||||
circle.on('pointerdown', () => {
|
container.setInteractive(hitArea, Phaser.Geom.Circle.Contains);
|
||||||
|
if (container.input) {
|
||||||
|
container.input.cursor = 'pointer';
|
||||||
|
}
|
||||||
|
|
||||||
|
container.on('pointerdown', () => {
|
||||||
this.scene.onHighlightClick(data);
|
this.scene.onHighlightClick(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
return circle;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
onDespawn(obj: Phaser.GameObjects.GameObject): void {
|
onDespawn(obj: Phaser.GameObjects.Container): void {
|
||||||
obj.destroy();
|
obj.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
export { createOnitamaUIState, clearSelection, selectPiece, selectCard, deselectCard, setValidMoves } from './ui';
|
export { createUIState, clearSelection, selectPiece, selectCard, createValidMoves } from './ui';
|
||||||
export type { OnitamaUIState, ValidMove } from './ui';
|
export type { OnitamaUIState, ValidMove } from './ui';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { MutableSignal, mutableSignal } from 'boardgame-core';
|
import { MutableSignal, mutableSignal, computed, ReadonlySignal } from 'boardgame-core';
|
||||||
|
import {getAvailableMoves, OnitamaState} from "boardgame-core/samples/onitama";
|
||||||
|
|
||||||
export interface ValidMove {
|
export interface ValidMove {
|
||||||
card: string;
|
card: string;
|
||||||
|
|
@ -12,14 +13,22 @@ export interface ValidMove {
|
||||||
export interface OnitamaUIState {
|
export interface OnitamaUIState {
|
||||||
selectedPiece: { x: number; y: number } | null;
|
selectedPiece: { x: number; y: number } | null;
|
||||||
selectedCard: string | null;
|
selectedCard: string | null;
|
||||||
validMoves: ValidMove[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOnitamaUIState(): MutableSignal<OnitamaUIState> {
|
export function createUIState(): MutableSignal<OnitamaUIState> {
|
||||||
return mutableSignal<OnitamaUIState>({
|
return mutableSignal<OnitamaUIState>({
|
||||||
selectedPiece: null,
|
selectedPiece: null,
|
||||||
selectedCard: null,
|
selectedCard: null,
|
||||||
validMoves: [],
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createValidMoves(state: ReadonlySignal<OnitamaState>, ui: ReadonlySignal<OnitamaUIState>){
|
||||||
|
return computed(() => {
|
||||||
|
return getAvailableMoves(state.value, state.value.currentPlayer)
|
||||||
|
.filter(move => {
|
||||||
|
const {selectedCard, selectedPiece} = ui.value;
|
||||||
|
return selectedPiece?.x === move.fromX && selectedPiece?.y === move.fromY && selectedCard === move.card;
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,7 +36,6 @@ export function clearSelection(uiState: MutableSignal<OnitamaUIState>): void {
|
||||||
uiState.produce(state => {
|
uiState.produce(state => {
|
||||||
state.selectedPiece = null;
|
state.selectedPiece = null;
|
||||||
state.selectedCard = null;
|
state.selectedCard = null;
|
||||||
state.validMoves = [];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +45,12 @@ export function selectPiece(
|
||||||
y: number
|
y: number
|
||||||
): void {
|
): void {
|
||||||
uiState.produce(state => {
|
uiState.produce(state => {
|
||||||
|
// 如果点击已选中的棋子,取消选择
|
||||||
|
if(state.selectedPiece?.x === x && state.selectedPiece?.y === y){
|
||||||
|
state.selectedPiece = null;
|
||||||
|
}else{
|
||||||
state.selectedPiece = { x, y };
|
state.selectedPiece = { x, y };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,34 +61,10 @@ export function selectCard(
|
||||||
uiState.produce(state => {
|
uiState.produce(state => {
|
||||||
// 如果点击已选中的卡牌,取消选择
|
// 如果点击已选中的卡牌,取消选择
|
||||||
if (state.selectedCard === card) {
|
if (state.selectedCard === card) {
|
||||||
state.selectedPiece = null;
|
|
||||||
state.selectedCard = null;
|
state.selectedCard = null;
|
||||||
state.validMoves = [];
|
|
||||||
} else {
|
} else {
|
||||||
// 选择新卡牌,清除棋子选择
|
// 选择新卡牌,清除棋子选择
|
||||||
state.selectedPiece = null;
|
|
||||||
state.selectedCard = card;
|
state.selectedCard = card;
|
||||||
state.validMoves = [];
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deselectCard(
|
|
||||||
uiState: MutableSignal<OnitamaUIState>
|
|
||||||
): void {
|
|
||||||
uiState.produce(state => {
|
|
||||||
state.selectedCard = null;
|
|
||||||
state.selectedPiece = null;
|
|
||||||
state.validMoves = [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setValidMoves(
|
|
||||||
uiState: MutableSignal<OnitamaUIState>,
|
|
||||||
moves: ValidMove[]
|
|
||||||
): void {
|
|
||||||
uiState.produce(state => {
|
|
||||||
state.validMoves = moves;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue