diff --git a/QWEN.md b/QWEN.md index b1272e6..612220d 100644 --- a/QWEN.md +++ b/QWEN.md @@ -55,4 +55,17 @@ export class GameHost, 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); ``` \ No newline at end of file diff --git a/packages/onitama-game/src/scenes/OnitamaScene.ts b/packages/onitama-game/src/scenes/OnitamaScene.ts index a26fe88..772c630 100644 --- a/packages/onitama-game/src/scenes/OnitamaScene.ts +++ b/packages/onitama-game/src/scenes/OnitamaScene.ts @@ -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 { private boardContainer!: Phaser.GameObjects.Container; private gridGraphics!: Phaser.GameObjects.Graphics; @@ -19,8 +17,8 @@ export class OnitamaScene extends GameHostScene { private winnerOverlay?: Phaser.GameObjects.Container; private cardLabelContainers: Map = new Map(); - // UI State managed by signal - public uiState!: Signal; + // UI State managed by MutableSignal + public uiState!: MutableSignal; private highlightContainers: Map = new Map(); private highlightDispose?: () => void; @@ -189,10 +187,9 @@ export class OnitamaScene extends GameHostScene { // 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 { // 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, diff --git a/packages/onitama-game/src/spawners/CardSpawner.ts b/packages/onitama-game/src/spawners/CardSpawner.ts index b3d39b1..93b675d 100644 --- a/packages/onitama-game/src/spawners/CardSpawner.ts +++ b/packages/onitama-game/src/spawners/CardSpawner.ts @@ -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 { + 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; diff --git a/packages/onitama-game/src/spawners/PawnSpawner.ts b/packages/onitama-game/src/spawners/PawnSpawner.ts index ab39c2e..3ff3501 100644 --- a/packages/onitama-game/src/spawners/PawnSpawner.ts +++ b/packages/onitama-game/src/spawners/PawnSpawner.ts @@ -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 { private previousPositions = new Map(); @@ -30,13 +38,12 @@ export class PawnSpawner implements Spawner 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 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 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]); diff --git a/packages/onitama-game/src/spawners/index.ts b/packages/onitama-game/src/spawners/index.ts index 352f3e0..cb2d05a 100644 --- a/packages/onitama-game/src/spawners/index.ts +++ b/packages/onitama-game/src/spawners/index.ts @@ -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'; diff --git a/packages/onitama-game/src/state/ui.ts b/packages/onitama-game/src/state/ui.ts index 7e7b9a5..0d0627d 100644 --- a/packages/onitama-game/src/state/ui.ts +++ b/packages/onitama-game/src/state/ui.ts @@ -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 { - return signal({ +export function createOnitamaUIState(): MutableSignal { + return mutableSignal({ selectedPiece: null, selectedCard: null, validMoves: [], }); } -export function clearSelection(uiState: Signal): void { - uiState.value = { - selectedPiece: null, - selectedCard: null, - validMoves: [], - }; +export function clearSelection(uiState: MutableSignal): void { + uiState.produce(state => { + state.selectedPiece = null; + state.selectedCard = null; + state.validMoves = []; + }); } export function selectPiece( - uiState: Signal, + uiState: MutableSignal, 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, + uiState: MutableSignal, 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 + uiState: MutableSignal ): 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, + uiState: MutableSignal, moves: ValidMove[] ): void { - uiState.value = { - ...uiState.value, - validMoves: moves, - }; + uiState.produce(state => { + state.validMoves = moves; + }); }