From 66834d8a7a28622f4232ed6254484b5292634aaf Mon Sep 17 00:00:00 2001 From: wsy182 <2392948297@qq.com> Date: Wed, 25 Mar 2026 17:26:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(game):=20=E6=B7=BB=E5=8A=A0=E6=88=BF?= =?UTF-8?q?=E9=97=B4=E7=8E=A9=E5=AE=B6=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 定义 RoomPlayerUpdatePayload 接口用于处理房间状态更新 - 在游戏动作中新增 ROOM_PLAYER_UPDATE 类型支持 - 实现游戏状态管理器中的房间玩家更新逻辑 - 重构成都麻将页面以使用新的状态管理机制 - 添加从 WebSocket 消息转换为游戏动作的功能 - 更新房间离开时的 WebSocket 消息发送逻辑 - 优化玩家手牌显示和选择逻辑 - 调整房间状态显示逻辑以匹配新状态模型 - 修复座位索引计算和庄家标识逻辑 - 更新全局样式中的图标按钮样式 - 替换大厅页面的刷新图标为 SVG 图像 - 升级 pnpm 包管理器版本 - 扩展玩家状态类型定义以支持显示名称和缺门信息 --- package.json | 2 +- src/assets/images/icons/avatar.svg | 1 + src/assets/images/icons/refresh.svg | 1 + src/assets/styles/global.css | 13 ++ src/game/actions.ts | 27 ++- src/game/dispatcher.ts | 10 +- src/store/gameStore.ts | 91 ++++++++- src/types/state/playerState.ts | 2 + src/views/ChengduGamePage.vue | 282 ++++++++++++++++++++-------- src/views/HallPage.vue | 5 +- 10 files changed, 352 insertions(+), 82 deletions(-) create mode 100644 src/assets/images/icons/avatar.svg create mode 100644 src/assets/images/icons/refresh.svg diff --git a/package.json b/package.json index 5f79224..ebd3a6f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", - "packageManager": "pnpm@9.0.0", + "packageManager": "pnpm@10.28.2", "scripts": { "dev": "vite", "build": "vue-tsc -b && vite build", diff --git a/src/assets/images/icons/avatar.svg b/src/assets/images/icons/avatar.svg new file mode 100644 index 0000000..f013996 --- /dev/null +++ b/src/assets/images/icons/avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/icons/refresh.svg b/src/assets/images/icons/refresh.svg new file mode 100644 index 0000000..f809f38 --- /dev/null +++ b/src/assets/images/icons/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css index 95cadbd..e553a9d 100644 --- a/src/assets/styles/global.css +++ b/src/assets/styles/global.css @@ -335,6 +335,9 @@ button:disabled { .icon-btn { width: 34px; height: 34px; + display: inline-flex; + align-items: center; + justify-content: center; border: 1px solid rgba(176, 216, 194, 0.35); border-radius: 8px; color: #d3efdf; @@ -342,6 +345,16 @@ button:disabled { cursor: pointer; } +.icon-btn-image { + width: 18px; + height: 18px; + display: block; +} + +.icon-btn:disabled .icon-btn-image { + opacity: 0.65; +} + .room-list { list-style: none; margin: 0; diff --git a/src/game/actions.ts b/src/game/actions.ts index 21d7728..8c03851 100644 --- a/src/game/actions.ts +++ b/src/game/actions.ts @@ -1,6 +1,25 @@ import type {GameState, PendingClaimState} from "../types/state"; import type {Tile} from "../types/tile.ts"; +export interface RoomPlayerUpdatePayload { + room_id?: string + status?: string + player_count?: number + player_ids?: string[] + players?: Array<{ + Index?: number + index?: number + PlayerID?: string + player_id?: string + PlayerName?: string + player_name?: string + Ready?: boolean + ready?: boolean + MissingSuit?: string | null + missing_suit?: string | null + }> +} + /** * 游戏动作定义(只描述“发生了什么”) @@ -52,4 +71,10 @@ export type GameAction = playerId: string action: 'peng' | 'gang' | 'hu' | 'pass' } -} \ No newline at end of file +} + + // 房间玩家更新(等待房间人数变化) + | { + type: 'ROOM_PLAYER_UPDATE' + payload: RoomPlayerUpdatePayload +} diff --git a/src/game/dispatcher.ts b/src/game/dispatcher.ts index dd32025..1571eec 100644 --- a/src/game/dispatcher.ts +++ b/src/game/dispatcher.ts @@ -29,5 +29,13 @@ export function dispatchGameAction(action: GameAction) { case 'CLAIM_RESOLVED': store.clearPendingClaim() break + + case 'ROOM_PLAYER_UPDATE': + store.onRoomPlayerUpdate(action.payload) + break + + + default: + throw new Error('Invalid game action') } -} \ No newline at end of file +} diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts index 986aa93..484862c 100644 --- a/src/store/gameStore.ts +++ b/src/store/gameStore.ts @@ -4,6 +4,7 @@ import { type GameState, type PendingClaimState, } from '../types/state' +import type { RoomPlayerUpdatePayload } from '../game/actions' import type { Tile } from '../types/tile' @@ -91,6 +92,94 @@ export const useGameStore = defineStore('game', { this.phase = GAME_PHASE.ACTION }, + onRoomPlayerUpdate(payload: RoomPlayerUpdatePayload) { + if (typeof payload.room_id === 'string' && payload.room_id) { + this.roomId = payload.room_id + } + + if (typeof payload.status === 'string' && payload.status) { + const phaseMap: Record = { + waiting: GAME_PHASE.WAITING, + dealing: GAME_PHASE.DEALING, + playing: GAME_PHASE.PLAYING, + action: GAME_PHASE.ACTION, + settlement: GAME_PHASE.SETTLEMENT, + } + this.phase = phaseMap[payload.status] ?? this.phase + } + + const hasPlayerList = + Array.isArray(payload.players) || Array.isArray(payload.player_ids) + if (!hasPlayerList) { + return + } + + const nextPlayers: GameState['players'] = {} + const players = Array.isArray(payload.players) ? payload.players : [] + const playerIds = Array.isArray(payload.player_ids) ? payload.player_ids : [] + + players.forEach((raw, index) => { + const playerId = + (typeof raw.PlayerID === 'string' && raw.PlayerID) || + (typeof raw.player_id === 'string' && raw.player_id) || + playerIds[index] + if (!playerId) { + return + } + + const previous = this.players[playerId] + const seatRaw = raw.Index ?? raw.index ?? index + const seatIndex = + typeof seatRaw === 'number' && Number.isFinite(seatRaw) ? seatRaw : index + const readyRaw = raw.Ready ?? raw.ready + const displayNameRaw = raw.PlayerName ?? raw.player_name + const missingSuitRaw = raw.MissingSuit ?? raw.missing_suit + + nextPlayers[playerId] = { + playerId, + seatIndex, + displayName: + typeof displayNameRaw === 'string' && displayNameRaw + ? displayNameRaw + : previous?.displayName, + missingSuit: + typeof missingSuitRaw === 'string' || missingSuitRaw === null + ? missingSuitRaw + : previous?.missingSuit, + handTiles: previous?.handTiles ?? [], + melds: previous?.melds ?? [], + discardTiles: previous?.discardTiles ?? [], + score: previous?.score ?? 0, + isReady: + typeof readyRaw === 'boolean' + ? readyRaw + : (previous?.isReady ?? false), + } + }) + + if (players.length === 0) { + playerIds.forEach((playerId, index) => { + if (typeof playerId !== 'string' || !playerId) { + return + } + const previous = this.players[playerId] + nextPlayers[playerId] = { + playerId, + seatIndex: previous?.seatIndex ?? index, + displayName: previous?.displayName ?? playerId, + missingSuit: previous?.missingSuit, + handTiles: previous?.handTiles ?? [], + melds: previous?.melds ?? [], + discardTiles: previous?.discardTiles ?? [], + score: previous?.score ?? 0, + isReady: previous?.isReady ?? false, + } + }) + } + + this.players = nextPlayers + }, + // 清理操作窗口 clearPendingClaim() { this.pendingClaim = undefined @@ -102,4 +191,4 @@ export const useGameStore = defineStore('game', { return Object.keys(this.players)[0] || '' }, }, -}) \ No newline at end of file +}) diff --git a/src/types/state/playerState.ts b/src/types/state/playerState.ts index a596e02..dd347fb 100644 --- a/src/types/state/playerState.ts +++ b/src/types/state/playerState.ts @@ -4,6 +4,8 @@ import type { MeldState } from './meldState.ts' export interface PlayerState { playerId: string seatIndex: number + displayName?: string + missingSuit?: string | null // 手牌(只有自己有完整数据,后端可控制) handTiles: Tile[] diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue index 967a46e..19b23ff 100644 --- a/src/views/ChengduGamePage.vue +++ b/src/views/ChengduGamePage.vue @@ -1,6 +1,6 @@