fix: fix ui bugs
This commit is contained in:
parent
d4819f7cc3
commit
2beff5c75c
13
QWEN.md
13
QWEN.md
|
|
@ -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);
|
||||
```
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
// 如果点击已选中的卡牌,取消选择
|
||||
if (uiState.value.selectedCard === card) {
|
||||
uiState.value = {
|
||||
selectedPiece: null,
|
||||
selectedCard: null,
|
||||
validMoves: [],
|
||||
};
|
||||
} else {
|
||||
// 选择新卡牌,清除棋子选择
|
||||
uiState.value = {
|
||||
selectedPiece: null,
|
||||
selectedCard: card,
|
||||
validMoves: [],
|
||||
};
|
||||
}
|
||||
uiState.produce(state => {
|
||||
// 如果点击已选中的卡牌,取消选择
|
||||
if (state.selectedCard === card) {
|
||||
state.selectedPiece = null;
|
||||
state.selectedCard = null;
|
||||
state.validMoves = [];
|
||||
} else {
|
||||
// 选择新卡牌,清除棋子选择
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue