- 在actions.ts中添加is_ready字段支持多种就绪状态标识 - 在ChengduGamePage.vue中更新就绪状态判断逻辑,优先使用is_ready字段 - 修改WebSocket消息类型从SET_READY到PLAYER_READY以保持一致性 - 更新就绪载荷中的用户ID字段从user_id到player_id - 创建新的gameStore.ts文件实现完整的麻将游戏状态管理 - 添加房间玩家更新、托管设置、玩家回合等核心游戏逻辑处理 - 实现摸牌、出牌、操作窗口等游戏状态变更功能 - 统一处理多种数据格式的兼容性问题
301 lines
9.8 KiB
TypeScript
301 lines
9.8 KiB
TypeScript
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 ''
|
||
},
|
||
},
|
||
})
|
||
|
||
|