fix: fix ui bugs

This commit is contained in:
hypercross 2026-04-08 08:50:45 +08:00
parent d4819f7cc3
commit 2beff5c75c
6 changed files with 87 additions and 76 deletions

13
QWEN.md
View File

@ -56,3 +56,16 @@ export class GameHost<TState extends Record<string, unknown>, TResult=unknown> {
on(event: 'start' | 'dispose', listener: () => void): () => void {}
}
```
使用`Spawner`来创建动态游戏对象:
- 卡牌
- 棋子
- UI高亮提示对象
使用`MutableSignal`创建状态信号。
```typescript
import {MutableSignal} from "boardgame-core"
export const x = MutableSignal(0);
x.produce(x => x + 1);
```

View File

@ -4,14 +4,12 @@ import { prompts } from '@/game/onitama';
import { GameHostScene } from 'boardgame-phaser';
import { spawnEffect } from 'boardgame-phaser';
import { effect } from '@preact/signals-core';
import type { Signal } from '@preact/signals';
import { PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT } from '@/spawners';
import type { MutableSignal } from 'boardgame-core';
import { PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT, boardToScreen, BOARD_SIZE } from '@/spawners';
import type { HighlightData } from '@/spawners/HighlightSpawner';
import { createOnitamaUIState, clearSelection, selectPiece, selectCard, deselectCard, setValidMoves } from '@/state';
import type { OnitamaUIState, ValidMove } from '@/state';
const BOARD_SIZE = 5;
export class OnitamaScene extends GameHostScene<OnitamaState> {
private boardContainer!: Phaser.GameObjects.Container;
private gridGraphics!: Phaser.GameObjects.Graphics;
@ -19,8 +17,8 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
private winnerOverlay?: Phaser.GameObjects.Container;
private cardLabelContainers: Map<string, Phaser.GameObjects.Text> = new Map();
// UI State managed by signal
public uiState!: Signal<OnitamaUIState>;
// UI State managed by MutableSignal
public uiState!: MutableSignal<OnitamaUIState>;
private highlightContainers: Map<string, Phaser.GameObjects.GameObject> = new Map();
private highlightDispose?: () => void;
@ -189,10 +187,9 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
// Board cell clicks
for (let row = 0; row < BOARD_SIZE; row++) {
for (let col = 0; col < BOARD_SIZE; col++) {
const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
const pos = boardToScreen(col, row);
const zone = this.add.zone(x, y, CELL_SIZE, CELL_SIZE).setInteractive();
const zone = this.add.zone(pos.x, pos.y, CELL_SIZE, CELL_SIZE).setInteractive();
zone.on('pointerdown', () => {
if (this.state.winner) return;
@ -308,16 +305,15 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
// Create new highlights
for (const move of validMoves) {
const key = `${move.card}-${move.toX}-${move.toY}`;
const x = BOARD_OFFSET.x + move.toX * CELL_SIZE + CELL_SIZE / 2;
const y = BOARD_OFFSET.y + move.toY * CELL_SIZE + CELL_SIZE / 2;
const pos = boardToScreen(move.toX, move.toY);
const circle = this.add.circle(x, y, CELL_SIZE / 3, 0x3b82f6, 0.3).setDepth(100);
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,
y,
x: pos.x,
y: pos.y,
card: move.card,
fromX: move.fromX,
fromY: move.fromY,

View File

@ -3,6 +3,7 @@ import type { Card } from '@/game/onitama';
import type { Spawner } from 'boardgame-phaser';
import type { OnitamaScene } from '@/scenes/OnitamaScene';
import { BOARD_OFFSET, CELL_SIZE } from './PawnSpawner';
import {effect} from "@preact/signals-core";
export const CARD_WIDTH = 100;
export const CARD_HEIGHT = 140;
@ -71,16 +72,6 @@ export class CardSpawner implements Spawner<CardSpawnData, Phaser.GameObjects.Co
}
onUpdate(data: CardSpawnData, obj: Phaser.GameObjects.Container): void {
// 检查是否是选中的卡牌
const isSelected = this.scene.uiState.value.selectedCard === data.cardId;
// 高亮选中的卡牌
if (isSelected) {
this.highlightCard(obj, 0xfbbf24, 3);
} else {
this.unhighlightCard(obj);
}
// 只在位置实际变化时才播放移动动画
if (!this.hasPositionChanged(data)) {
this.previousData.set(data.cardId, { ...data });
@ -176,6 +167,13 @@ export class CardSpawner implements Spawner<CardSpawnData, Phaser.GameObjects.Co
});
this.scene.addTweenInterruption(tween);
container.once('destroy', effect(() => {
if(this.scene.uiState.value.selectedCard === data.cardId)
this.highlightCard(container, 0xfbbf24, 3);
else
this.unhighlightCard(container);
}));
this.previousData.set(data.cardId, { ...data });
return container;
}

View File

@ -5,6 +5,14 @@ import type { OnitamaScene } from '@/scenes/OnitamaScene';
export const CELL_SIZE = 80;
export const BOARD_OFFSET = { x: 200, y: 180 };
export const BOARD_SIZE = 5;
export function boardToScreen(boardX: number, boardY: number): { x: number; y: number } {
return {
x: BOARD_OFFSET.x + boardX * CELL_SIZE + CELL_SIZE / 2,
y: BOARD_OFFSET.y + (BOARD_SIZE - 1 - boardY) * CELL_SIZE + CELL_SIZE / 2,
};
}
export class PawnSpawner implements Spawner<Pawn, Phaser.GameObjects.Container> {
private previousPositions = new Map<string, [number, number]>();
@ -30,13 +38,12 @@ export class PawnSpawner implements Spawner<Pawn, Phaser.GameObjects.Container>
if (hasMoved && prevPos) {
// 播放移动动画并添加中断
const targetX = BOARD_OFFSET.x + x * CELL_SIZE + CELL_SIZE / 2;
const targetY = BOARD_OFFSET.y + y * CELL_SIZE + CELL_SIZE / 2;
const targetPos = boardToScreen(x, y);
const tween = this.scene.tweens.add({
targets: obj,
x: targetX,
y: targetY,
x: targetPos.x,
y: targetPos.y,
duration: 400,
ease: 'Back.easeOut',
});
@ -44,8 +51,9 @@ export class PawnSpawner implements Spawner<Pawn, Phaser.GameObjects.Container>
this.scene.addTweenInterruption(tween);
} else if (!prevPos) {
// 初次生成,直接设置位置
obj.x = BOARD_OFFSET.x + x * CELL_SIZE + CELL_SIZE / 2;
obj.y = BOARD_OFFSET.y + y * CELL_SIZE + CELL_SIZE / 2;
const pos = boardToScreen(x, y);
obj.x = pos.x;
obj.y = pos.y;
}
this.previousPositions.set(pawn.id, [x, y]);
@ -68,8 +76,9 @@ export class PawnSpawner implements Spawner<Pawn, Phaser.GameObjects.Container>
container.add(text);
const [x, y] = pawn.position;
container.x = BOARD_OFFSET.x + x * CELL_SIZE + CELL_SIZE / 2;
container.y = BOARD_OFFSET.y + y * CELL_SIZE + CELL_SIZE / 2;
const pos = boardToScreen(x, y);
container.x = pos.x;
container.y = pos.y;
this.previousPositions.set(pawn.id, [x, y]);

View File

@ -1,3 +1,3 @@
export { PawnSpawner, CELL_SIZE, BOARD_OFFSET } from './PawnSpawner';
export { PawnSpawner, CELL_SIZE, BOARD_OFFSET, BOARD_SIZE, boardToScreen } from './PawnSpawner';
export { CardSpawner, CARD_WIDTH, CARD_HEIGHT, type CardSpawnData } from './CardSpawner';
export { HighlightSpawner, type HighlightData } from './HighlightSpawner';

View File

@ -1,4 +1,4 @@
import { signal, type Signal } from '@preact/signals';
import { MutableSignal, mutableSignal } from 'boardgame-core';
export interface ValidMove {
card: string;
@ -8,79 +8,74 @@ export interface ValidMove {
toY: number;
}
// 先选择牌,然后选择棋子,最后选择移动
export interface OnitamaUIState {
selectedPiece: { x: number; y: number } | null;
selectedCard: string | null;
validMoves: ValidMove[];
}
export function createOnitamaUIState(): Signal<OnitamaUIState> {
return signal<OnitamaUIState>({
export function createOnitamaUIState(): MutableSignal<OnitamaUIState> {
return mutableSignal<OnitamaUIState>({
selectedPiece: null,
selectedCard: null,
validMoves: [],
});
}
export function clearSelection(uiState: Signal<OnitamaUIState>): void {
uiState.value = {
selectedPiece: null,
selectedCard: null,
validMoves: [],
};
export function clearSelection(uiState: MutableSignal<OnitamaUIState>): void {
uiState.produce(state => {
state.selectedPiece = null;
state.selectedCard = null;
state.validMoves = [];
});
}
export function selectPiece(
uiState: Signal<OnitamaUIState>,
uiState: MutableSignal<OnitamaUIState>,
x: number,
y: number
): void {
uiState.value = {
...uiState.value,
selectedPiece: { x, y },
selectedCard: null,
};
uiState.produce(state => {
state.selectedPiece = { x, y };
});
}
export function selectCard(
uiState: Signal<OnitamaUIState>,
uiState: MutableSignal<OnitamaUIState>,
card: string
): void {
uiState.produce(state => {
// 如果点击已选中的卡牌,取消选择
if (uiState.value.selectedCard === card) {
uiState.value = {
selectedPiece: null,
selectedCard: null,
validMoves: [],
};
if (state.selectedCard === card) {
state.selectedPiece = null;
state.selectedCard = null;
state.validMoves = [];
} else {
// 选择新卡牌,清除棋子选择
uiState.value = {
selectedPiece: null,
selectedCard: card,
validMoves: [],
};
state.selectedPiece = null;
state.selectedCard = card;
state.validMoves = [];
}
});
}
export function deselectCard(
uiState: Signal<OnitamaUIState>
uiState: MutableSignal<OnitamaUIState>
): void {
uiState.value = {
...uiState.value,
selectedCard: null,
selectedPiece: null,
validMoves: [],
};
uiState.produce(state => {
state.selectedCard = null;
state.selectedPiece = null;
state.validMoves = [];
});
}
export function setValidMoves(
uiState: Signal<OnitamaUIState>,
uiState: MutableSignal<OnitamaUIState>,
moves: ValidMove[]
): void {
uiState.value = {
...uiState.value,
validMoves: moves,
};
uiState.produce(state => {
state.validMoves = moves;
});
}