Files
mahjong-web/src/store/gameStore.ts
wsy182 2625baf266 feat(game): 实现出牌选择与计时功能
- 添加 PlayerTurnPayload 接口定义和 PLAYER_TURN 动作类型
- 实现选牌、出牌确认逻辑和相关状态管理
- 添加客户端出牌限制检查和错误提示
- 集成 PLAYER_TURN WebSocket 消息处理
- 添加房间状态面板显示游戏信息
- 优化桌面背景图片和样式布局
- 添加马蹄形动画样式文件
- 配置 Vite 别名和端口设置
2026-03-30 17:23:43 +08:00

272 lines
9.1 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'
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: {},
}),
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
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:
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,
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 ''
},
},
})