fix: fix ui bugs
This commit is contained in:
parent
d4819f7cc3
commit
2beff5c75c
13
QWEN.md
13
QWEN.md
|
|
@ -55,4 +55,17 @@ export class GameHost<TState extends Record<string, unknown>, TResult=unknown> {
|
||||||
// 事件侦听
|
// 事件侦听
|
||||||
on(event: 'start' | 'dispose', listener: () => void): () => void {}
|
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 { GameHostScene } from 'boardgame-phaser';
|
||||||
import { spawnEffect } from 'boardgame-phaser';
|
import { spawnEffect } from 'boardgame-phaser';
|
||||||
import { effect } from '@preact/signals-core';
|
import { effect } from '@preact/signals-core';
|
||||||
import type { Signal } from '@preact/signals';
|
import type { MutableSignal } from 'boardgame-core';
|
||||||
import { PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT } from '@/spawners';
|
import { PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT, boardToScreen, BOARD_SIZE } from '@/spawners';
|
||||||
import type { HighlightData } from '@/spawners/HighlightSpawner';
|
import type { HighlightData } from '@/spawners/HighlightSpawner';
|
||||||
import { createOnitamaUIState, clearSelection, selectPiece, selectCard, deselectCard, setValidMoves } from '@/state';
|
import { createOnitamaUIState, clearSelection, selectPiece, selectCard, deselectCard, setValidMoves } from '@/state';
|
||||||
import type { OnitamaUIState, ValidMove } from '@/state';
|
import type { OnitamaUIState, ValidMove } from '@/state';
|
||||||
|
|
||||||
const BOARD_SIZE = 5;
|
|
||||||
|
|
||||||
export class OnitamaScene extends GameHostScene<OnitamaState> {
|
export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
private boardContainer!: Phaser.GameObjects.Container;
|
private boardContainer!: Phaser.GameObjects.Container;
|
||||||
private gridGraphics!: Phaser.GameObjects.Graphics;
|
private gridGraphics!: Phaser.GameObjects.Graphics;
|
||||||
|
|
@ -19,8 +17,8 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
private winnerOverlay?: Phaser.GameObjects.Container;
|
private winnerOverlay?: Phaser.GameObjects.Container;
|
||||||
private cardLabelContainers: Map<string, Phaser.GameObjects.Text> = new Map();
|
private cardLabelContainers: Map<string, Phaser.GameObjects.Text> = new Map();
|
||||||
|
|
||||||
// UI State managed by signal
|
// UI State managed by MutableSignal
|
||||||
public uiState!: Signal<OnitamaUIState>;
|
public uiState!: MutableSignal<OnitamaUIState>;
|
||||||
private highlightContainers: Map<string, Phaser.GameObjects.GameObject> = new Map();
|
private highlightContainers: Map<string, Phaser.GameObjects.GameObject> = new Map();
|
||||||
private highlightDispose?: () => void;
|
private highlightDispose?: () => void;
|
||||||
|
|
||||||
|
|
@ -189,10 +187,9 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
// Board cell clicks
|
// Board cell clicks
|
||||||
for (let row = 0; row < BOARD_SIZE; row++) {
|
for (let row = 0; row < BOARD_SIZE; row++) {
|
||||||
for (let col = 0; col < BOARD_SIZE; col++) {
|
for (let col = 0; col < BOARD_SIZE; col++) {
|
||||||
const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2;
|
const pos = boardToScreen(col, row);
|
||||||
const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2;
|
|
||||||
|
|
||||||
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', () => {
|
zone.on('pointerdown', () => {
|
||||||
if (this.state.winner) return;
|
if (this.state.winner) return;
|
||||||
|
|
@ -308,16 +305,15 @@ export class OnitamaScene extends GameHostScene<OnitamaState> {
|
||||||
// Create new highlights
|
// Create new highlights
|
||||||
for (const move of validMoves) {
|
for (const move of validMoves) {
|
||||||
const key = `${move.card}-${move.toX}-${move.toY}`;
|
const key = `${move.card}-${move.toX}-${move.toY}`;
|
||||||
const x = BOARD_OFFSET.x + move.toX * CELL_SIZE + CELL_SIZE / 2;
|
const pos = boardToScreen(move.toX, move.toY);
|
||||||
const y = BOARD_OFFSET.y + move.toY * CELL_SIZE + CELL_SIZE / 2;
|
|
||||||
|
|
||||||
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.setInteractive({ useHandCursor: true });
|
||||||
circle.on('pointerdown', () => {
|
circle.on('pointerdown', () => {
|
||||||
this.onHighlightClick({
|
this.onHighlightClick({
|
||||||
key,
|
key,
|
||||||
x,
|
x: pos.x,
|
||||||
y,
|
y: pos.y,
|
||||||
card: move.card,
|
card: move.card,
|
||||||
fromX: move.fromX,
|
fromX: move.fromX,
|
||||||
fromY: move.fromY,
|
fromY: move.fromY,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { Card } from '@/game/onitama';
|
||||||
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 { BOARD_OFFSET, CELL_SIZE } from './PawnSpawner';
|
import { BOARD_OFFSET, CELL_SIZE } from './PawnSpawner';
|
||||||
|
import {effect} from "@preact/signals-core";
|
||||||
|
|
||||||
export const CARD_WIDTH = 100;
|
export const CARD_WIDTH = 100;
|
||||||
export const CARD_HEIGHT = 140;
|
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 {
|
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)) {
|
if (!this.hasPositionChanged(data)) {
|
||||||
this.previousData.set(data.cardId, { ...data });
|
this.previousData.set(data.cardId, { ...data });
|
||||||
|
|
@ -175,6 +166,13 @@ export class CardSpawner implements Spawner<CardSpawnData, Phaser.GameObjects.Co
|
||||||
ease: 'Power2',
|
ease: 'Power2',
|
||||||
});
|
});
|
||||||
this.scene.addTweenInterruption(tween);
|
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 });
|
this.previousData.set(data.cardId, { ...data });
|
||||||
return container;
|
return container;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,14 @@ import type { OnitamaScene } from '@/scenes/OnitamaScene';
|
||||||
|
|
||||||
export const CELL_SIZE = 80;
|
export const CELL_SIZE = 80;
|
||||||
export const BOARD_OFFSET = { x: 200, y: 180 };
|
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> {
|
export class PawnSpawner implements Spawner<Pawn, Phaser.GameObjects.Container> {
|
||||||
private previousPositions = new Map<string, [number, number]>();
|
private previousPositions = new Map<string, [number, number]>();
|
||||||
|
|
@ -30,13 +38,12 @@ export class PawnSpawner implements Spawner<Pawn, Phaser.GameObjects.Container>
|
||||||
|
|
||||||
if (hasMoved && prevPos) {
|
if (hasMoved && prevPos) {
|
||||||
// 播放移动动画并添加中断
|
// 播放移动动画并添加中断
|
||||||
const targetX = BOARD_OFFSET.x + x * CELL_SIZE + CELL_SIZE / 2;
|
const targetPos = boardToScreen(x, y);
|
||||||
const targetY = BOARD_OFFSET.y + y * CELL_SIZE + CELL_SIZE / 2;
|
|
||||||
|
|
||||||
const tween = this.scene.tweens.add({
|
const tween = this.scene.tweens.add({
|
||||||
targets: obj,
|
targets: obj,
|
||||||
x: targetX,
|
x: targetPos.x,
|
||||||
y: targetY,
|
y: targetPos.y,
|
||||||
duration: 400,
|
duration: 400,
|
||||||
ease: 'Back.easeOut',
|
ease: 'Back.easeOut',
|
||||||
});
|
});
|
||||||
|
|
@ -44,8 +51,9 @@ export class PawnSpawner implements Spawner<Pawn, Phaser.GameObjects.Container>
|
||||||
this.scene.addTweenInterruption(tween);
|
this.scene.addTweenInterruption(tween);
|
||||||
} else if (!prevPos) {
|
} else if (!prevPos) {
|
||||||
// 初次生成,直接设置位置
|
// 初次生成,直接设置位置
|
||||||
obj.x = BOARD_OFFSET.x + x * CELL_SIZE + CELL_SIZE / 2;
|
const pos = boardToScreen(x, y);
|
||||||
obj.y = BOARD_OFFSET.y + y * CELL_SIZE + CELL_SIZE / 2;
|
obj.x = pos.x;
|
||||||
|
obj.y = pos.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.previousPositions.set(pawn.id, [x, y]);
|
this.previousPositions.set(pawn.id, [x, y]);
|
||||||
|
|
@ -68,8 +76,9 @@ export class PawnSpawner implements Spawner<Pawn, Phaser.GameObjects.Container>
|
||||||
container.add(text);
|
container.add(text);
|
||||||
|
|
||||||
const [x, y] = pawn.position;
|
const [x, y] = pawn.position;
|
||||||
container.x = BOARD_OFFSET.x + x * CELL_SIZE + CELL_SIZE / 2;
|
const pos = boardToScreen(x, y);
|
||||||
container.y = BOARD_OFFSET.y + y * CELL_SIZE + CELL_SIZE / 2;
|
container.x = pos.x;
|
||||||
|
container.y = pos.y;
|
||||||
|
|
||||||
this.previousPositions.set(pawn.id, [x, 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 { CardSpawner, CARD_WIDTH, CARD_HEIGHT, type CardSpawnData } from './CardSpawner';
|
||||||
export { HighlightSpawner, type HighlightData } from './HighlightSpawner';
|
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 {
|
export interface ValidMove {
|
||||||
card: string;
|
card: string;
|
||||||
|
|
@ -8,79 +8,74 @@ export interface ValidMove {
|
||||||
toY: number;
|
toY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先选择牌,然后选择棋子,最后选择移动
|
||||||
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[];
|
validMoves: ValidMove[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOnitamaUIState(): Signal<OnitamaUIState> {
|
export function createOnitamaUIState(): MutableSignal<OnitamaUIState> {
|
||||||
return signal<OnitamaUIState>({
|
return mutableSignal<OnitamaUIState>({
|
||||||
selectedPiece: null,
|
selectedPiece: null,
|
||||||
selectedCard: null,
|
selectedCard: null,
|
||||||
validMoves: [],
|
validMoves: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearSelection(uiState: Signal<OnitamaUIState>): void {
|
export function clearSelection(uiState: MutableSignal<OnitamaUIState>): void {
|
||||||
uiState.value = {
|
uiState.produce(state => {
|
||||||
selectedPiece: null,
|
state.selectedPiece = null;
|
||||||
selectedCard: null,
|
state.selectedCard = null;
|
||||||
validMoves: [],
|
state.validMoves = [];
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectPiece(
|
export function selectPiece(
|
||||||
uiState: Signal<OnitamaUIState>,
|
uiState: MutableSignal<OnitamaUIState>,
|
||||||
x: number,
|
x: number,
|
||||||
y: number
|
y: number
|
||||||
): void {
|
): void {
|
||||||
uiState.value = {
|
uiState.produce(state => {
|
||||||
...uiState.value,
|
state.selectedPiece = { x, y };
|
||||||
selectedPiece: { x, y },
|
});
|
||||||
selectedCard: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectCard(
|
export function selectCard(
|
||||||
uiState: Signal<OnitamaUIState>,
|
uiState: MutableSignal<OnitamaUIState>,
|
||||||
card: string
|
card: string
|
||||||
): void {
|
): void {
|
||||||
// 如果点击已选中的卡牌,取消选择
|
uiState.produce(state => {
|
||||||
if (uiState.value.selectedCard === card) {
|
// 如果点击已选中的卡牌,取消选择
|
||||||
uiState.value = {
|
if (state.selectedCard === card) {
|
||||||
selectedPiece: null,
|
state.selectedPiece = null;
|
||||||
selectedCard: null,
|
state.selectedCard = null;
|
||||||
validMoves: [],
|
state.validMoves = [];
|
||||||
};
|
} else {
|
||||||
} else {
|
// 选择新卡牌,清除棋子选择
|
||||||
// 选择新卡牌,清除棋子选择
|
state.selectedPiece = null;
|
||||||
uiState.value = {
|
state.selectedCard = card;
|
||||||
selectedPiece: null,
|
state.validMoves = [];
|
||||||
selectedCard: card,
|
}
|
||||||
validMoves: [],
|
});
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deselectCard(
|
export function deselectCard(
|
||||||
uiState: Signal<OnitamaUIState>
|
uiState: MutableSignal<OnitamaUIState>
|
||||||
): void {
|
): void {
|
||||||
uiState.value = {
|
uiState.produce(state => {
|
||||||
...uiState.value,
|
state.selectedCard = null;
|
||||||
selectedCard: null,
|
state.selectedPiece = null;
|
||||||
selectedPiece: null,
|
state.validMoves = [];
|
||||||
validMoves: [],
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setValidMoves(
|
export function setValidMoves(
|
||||||
uiState: Signal<OnitamaUIState>,
|
uiState: MutableSignal<OnitamaUIState>,
|
||||||
moves: ValidMove[]
|
moves: ValidMove[]
|
||||||
): void {
|
): void {
|
||||||
uiState.value = {
|
uiState.produce(state => {
|
||||||
...uiState.value,
|
state.validMoves = moves;
|
||||||
validMoves: moves,
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue