Files
mahjong-web/src/store/gameStore.ts
wsy182 e6cba75f9b feat(game): 添加游戏状态管理store并统一就绪状态字段
- 在actions.ts中添加is_ready字段支持多种就绪状态标识
- 在ChengduGamePage.vue中更新就绪状态判断逻辑,优先使用is_ready字段
- 修改WebSocket消息类型从SET_READY到PLAYER_READY以保持一致性
- 更新就绪载荷中的用户ID字段从user_id到player_id
- 创建新的gameStore.ts文件实现完整的麻将游戏状态管理
- 添加房间玩家更新、托管设置、玩家回合等核心游戏逻辑处理
- 实现摸牌、出牌、操作窗口等游戏状态变更功能
- 统一处理多种数据格式的兼容性问题
2026-04-03 15:24:46 +08:00

301 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { defineStore } from 'pinia'
import {
GAME_PHASE,
type GameState,
type PendingClaimState,
} from '../types/state'
import type { PlayerTurnPayload, RoomPlayerUpdatePayload, RoomTrusteePayload } from '../game/actions'
import { readStoredAuth } from '../utils/auth-storage'
import type { Tile } from '../types/tile'
function parseBooleanish(value: unknown): boolean | null {
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'number') {
if (value === 1) {
return true
}
if (value === 0) {
return false
}
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase()
if (normalized === 'true' || normalized === '1') {
return true
}
if (normalized === 'false' || normalized === '0') {
return false
}
}
return null
}
export const useGameStore = defineStore('game', {
state: (): GameState => ({
roomId: '',
phase: GAME_PHASE.WAITING,
dealerIndex: 0,
currentTurn: 0,
currentPlayerId: '',
needDraw: false,
players: {},
remainingTiles: 0,
pendingClaim: undefined,
winners: [],
scores: {},
currentRound: 0,
totalRounds: 0,
}),
actions: {
resetGame() {
this.$reset()
},
// 初始<E5889D>?
initGame(data: GameState) {
Object.assign(this, data)
},
// 摸牌
onDrawTile(data: { playerId: string; tile: Tile }) {
const player = this.players[data.playerId]
if (!player) return
// 只更新自己的手牌
if (player.playerId === this.getMyPlayerId()) {
player.handTiles.push(data.tile)
}
player.handCount += 1
// 剩余牌数减少
this.remainingTiles = Math.max(0, this.remainingTiles - 1)
// 更新回合seatIndex<65>?
this.currentTurn = player.seatIndex
this.currentPlayerId = player.playerId
// 清除操作窗口
this.pendingClaim = undefined
this.needDraw = false
// 进入出牌阶段
this.phase = GAME_PHASE.PLAYING
},
// 出牌
onPlayTile(data: {
playerId: string
tile: Tile
nextSeat: number
}) {
const player = this.players[data.playerId]
if (!player) return
// 如果是自己,移除手牌
if (player.playerId === this.getMyPlayerId()) {
const index = player.handTiles.findIndex(
(t) => t.id === data.tile.id
)
if (index !== -1) {
player.handTiles.splice(index, 1)
}
}
player.handCount = Math.max(0, player.handCount - 1)
// 加入出牌<E587BA>?
player.discardTiles.push(data.tile)
// 更新回合
this.currentTurn = data.nextSeat
this.needDraw = true
// 等待其他玩家响应
this.phase = GAME_PHASE.ACTION
},
// 触发操作窗口(碰/<2F>?胡)
onPendingClaim(data: PendingClaimState) {
this.pendingClaim = data
this.needDraw = false
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<string, GameState['phase']> = {
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 ?? raw.is_ready
const ready = parseBooleanish(readyRaw)
const displayNameRaw = raw.PlayerName ?? raw.player_name
const avatarUrlRaw = raw.AvatarUrl ?? raw.avatar_url
const missingSuitRaw = raw.MissingSuit ?? raw.missing_suit
nextPlayers[playerId] = {
playerId,
seatIndex,
displayName:
typeof displayNameRaw === 'string' && displayNameRaw
? displayNameRaw
: previous?.displayName,
avatarURL:
typeof avatarUrlRaw === 'string'
? avatarUrlRaw
: previous?.avatarURL,
isTrustee: previous?.isTrustee ?? false,
missingSuit:
typeof missingSuitRaw === 'string' || missingSuitRaw === null
? missingSuitRaw
: previous?.missingSuit,
handTiles: previous?.handTiles ?? [],
handCount: previous?.handCount ?? 0,
melds: previous?.melds ?? [],
discardTiles: previous?.discardTiles ?? [],
hasHu: previous?.hasHu ?? false,
score: previous?.score ?? 0,
isReady:
ready !== null
? ready
: (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,
avatarURL: previous?.avatarURL,
isTrustee: previous?.isTrustee ?? false,
missingSuit: previous?.missingSuit,
handTiles: previous?.handTiles ?? [],
handCount: previous?.handCount ?? 0,
melds: previous?.melds ?? [],
discardTiles: previous?.discardTiles ?? [],
hasHu: previous?.hasHu ?? false,
score: previous?.score ?? 0,
isReady: previous?.isReady ?? false,
}
})
}
this.players = nextPlayers
},
onRoomTrustee(payload: RoomTrusteePayload) {
const playerId =
(typeof payload.player_id === 'string' && payload.player_id) ||
(typeof payload.playerId === 'string' && payload.playerId) ||
''
if (!playerId) {
return
}
const player = this.players[playerId]
if (!player) {
return
}
player.isTrustee = typeof payload.trustee === 'boolean' ? payload.trustee : true
},
// 清理操作窗口
onPlayerTurn(payload: PlayerTurnPayload) {
const playerId =
(typeof payload.player_id === 'string' && payload.player_id) ||
(typeof payload.playerId === 'string' && payload.playerId) ||
(typeof payload.PlayerID === 'string' && payload.PlayerID) ||
''
if (!playerId) {
return
}
const player = this.players[playerId]
if (player) {
this.currentTurn = player.seatIndex
}
this.currentPlayerId = playerId
this.needDraw = false
this.pendingClaim = undefined
this.phase = GAME_PHASE.PLAYING
},
clearPendingClaim() {
this.pendingClaim = undefined
this.phase = GAME_PHASE.PLAYING
},
// 获取当前玩家ID后续建议放<E8AEAE>?userStore<72>?
getMyPlayerId(): string {
const auth = readStoredAuth()
const source = auth?.user as Record<string, unknown> | undefined
const rawId =
source?.id ??
source?.userID ??
source?.user_id
if (typeof rawId === 'string' && rawId.trim()) {
return rawId
}
if (typeof rawId === 'number') {
return String(rawId)
}
return ''
},
},
})