feat(game): 添加成都麻将游戏页面和大厅功能
- 实现 ChengduGamePage.vue 组件,包含完整的麻将游戏界面 - 实现 HallPage.vue 组件,支持房间列表展示、创建和加入功能 - 添加 mahjong API 接口用于房间管理操作 - 集成 store 状态管理和本地存储功能 - 实现 ChengduBottomActions 等游戏控制组件 - 添加 websocket 连接和游戏会话管理逻辑 - 实现游戏倒计时、结算等功能模块
This commit is contained in:
274
src/views/chengdu/socket/parsers/roomInfoSnapshot.ts
Normal file
274
src/views/chengdu/socket/parsers/roomInfoSnapshot.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import {
|
||||
asRecord,
|
||||
normalizeMelds,
|
||||
normalizePendingClaim,
|
||||
normalizeTiles,
|
||||
readBoolean,
|
||||
readMissingSuit,
|
||||
readMissingSuitWithPresence,
|
||||
readNumber,
|
||||
readString,
|
||||
readStringArray,
|
||||
tileToText,
|
||||
} from '../../../../game/chengdu/messageNormalizers'
|
||||
import type { RoomMetaSnapshotState } from '../../../../store/state'
|
||||
import type { PendingClaimState, PlayerState } from '../../../../types/state'
|
||||
|
||||
interface RoomInfoSnapshotPlayerPair {
|
||||
roomPlayer: RoomMetaSnapshotState['players'][number]
|
||||
gamePlayer: PlayerState
|
||||
}
|
||||
|
||||
export interface ParsedRoomInfoSnapshot {
|
||||
room: Record<string, unknown> | null
|
||||
gameState: Record<string, unknown> | null
|
||||
playerView: Record<string, unknown> | null
|
||||
roomId: string
|
||||
roomPlayers: RoomInfoSnapshotPlayerPair[]
|
||||
nextPlayers: Record<string, PlayerState>
|
||||
status: string
|
||||
phase: string
|
||||
wallCount: number | null
|
||||
dealerIndex: number | null
|
||||
currentTurnPlayerId: string
|
||||
currentTurn: number | null
|
||||
needDraw: boolean
|
||||
pendingClaim?: PendingClaimState
|
||||
scores?: Record<string, number>
|
||||
winners: string[]
|
||||
currentRound: number | null
|
||||
totalRounds: number | null
|
||||
settlementDeadlineMs: number | null
|
||||
}
|
||||
|
||||
interface ParseRoomInfoSnapshotOptions {
|
||||
message: Record<string, unknown>
|
||||
loggedInUserId: string
|
||||
loggedInUserName: string
|
||||
previousPlayers: Record<string, PlayerState>
|
||||
}
|
||||
|
||||
function buildPlayerPairs(options: ParseRoomInfoSnapshotOptions, payload: Record<string, unknown>) {
|
||||
const room = asRecord(payload.room)
|
||||
const gameState = asRecord(payload.game_state)
|
||||
const playerView = asRecord(payload.player_view)
|
||||
const roomPlayers = Array.isArray(room?.players) ? room.players : []
|
||||
const gamePlayers = Array.isArray(gameState?.players) ? gameState.players : []
|
||||
const playerMap = new Map<string, RoomInfoSnapshotPlayerPair>()
|
||||
|
||||
roomPlayers.forEach((item, fallbackIndex) => {
|
||||
const player = asRecord(item)
|
||||
if (!player) {
|
||||
return
|
||||
}
|
||||
|
||||
const playerId = readString(player, 'player_id', 'PlayerID', 'id', 'user_id')
|
||||
if (!playerId) {
|
||||
return
|
||||
}
|
||||
|
||||
const seatIndex = readNumber(player, 'index', 'Index', 'seat_index', 'seatIndex') ?? fallbackIndex
|
||||
const displayName =
|
||||
readString(player, 'player_name', 'PlayerName', 'display_name', 'displayName', 'nickname', 'username') ||
|
||||
(playerId === options.loggedInUserId ? options.loggedInUserName : '')
|
||||
const ready = readBoolean(player, 'ready', 'Ready') ?? false
|
||||
const missingSuit = readMissingSuit(player)
|
||||
|
||||
playerMap.set(playerId, {
|
||||
roomPlayer: {
|
||||
index: seatIndex,
|
||||
playerId,
|
||||
displayName: displayName || undefined,
|
||||
missingSuit,
|
||||
ready,
|
||||
trustee: false,
|
||||
hand: [],
|
||||
melds: [],
|
||||
outTiles: [],
|
||||
hasHu: false,
|
||||
},
|
||||
gamePlayer: {
|
||||
playerId,
|
||||
seatIndex,
|
||||
displayName: displayName || undefined,
|
||||
avatarURL: readString(player, 'avatar_url', 'AvatarUrl', 'avatar', 'avatarUrl') || undefined,
|
||||
missingSuit,
|
||||
isReady: ready,
|
||||
isTrustee: false,
|
||||
handTiles: [],
|
||||
handCount: 0,
|
||||
melds: [],
|
||||
discardTiles: [],
|
||||
hasHu: false,
|
||||
score: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
gamePlayers.forEach((item, fallbackIndex) => {
|
||||
const player = asRecord(item)
|
||||
if (!player) {
|
||||
return
|
||||
}
|
||||
|
||||
const playerId = readString(player, 'player_id', 'PlayerID')
|
||||
if (!playerId) {
|
||||
return
|
||||
}
|
||||
|
||||
const existing = playerMap.get(playerId)
|
||||
const seatIndex =
|
||||
existing?.gamePlayer.seatIndex ??
|
||||
readNumber(player, 'index', 'Index', 'seat_index', 'seatIndex') ??
|
||||
fallbackIndex
|
||||
const displayName =
|
||||
existing?.gamePlayer.displayName || (playerId === options.loggedInUserId ? options.loggedInUserName : '')
|
||||
const missingSuit = readMissingSuitWithPresence(player)
|
||||
const handCount = readNumber(player, 'hand_count', 'handCount') ?? 0
|
||||
const outTiles = normalizeTiles(player.out_tiles ?? player.outTiles)
|
||||
const melds = normalizeMelds(player.melds ?? player.exposed_melds ?? player.exposedMelds ?? player.claims)
|
||||
const hasHu = Boolean(player.has_hu ?? player.hasHu)
|
||||
|
||||
playerMap.set(playerId, {
|
||||
roomPlayer: {
|
||||
index: seatIndex,
|
||||
playerId,
|
||||
displayName: displayName || undefined,
|
||||
missingSuit: missingSuit.present ? missingSuit.value : (existing?.gamePlayer.missingSuit ?? null),
|
||||
ready: existing?.roomPlayer.ready ?? false,
|
||||
trustee: existing?.roomPlayer.trustee ?? false,
|
||||
hand: Array.from({ length: handCount }, () => ''),
|
||||
melds: melds.map((meld) => meld.type),
|
||||
outTiles: outTiles.map((tile) => tileToText(tile)),
|
||||
hasHu,
|
||||
},
|
||||
gamePlayer: {
|
||||
playerId,
|
||||
seatIndex,
|
||||
displayName: displayName || undefined,
|
||||
avatarURL: existing?.gamePlayer.avatarURL,
|
||||
missingSuit: missingSuit.present ? missingSuit.value : (existing?.gamePlayer.missingSuit ?? null),
|
||||
isReady: existing?.gamePlayer.isReady ?? false,
|
||||
isTrustee: existing?.gamePlayer.isTrustee ?? false,
|
||||
handTiles: existing?.gamePlayer.handTiles ?? [],
|
||||
handCount,
|
||||
melds: melds.length > 0 ? melds : existing?.gamePlayer.melds ?? [],
|
||||
discardTiles: outTiles,
|
||||
hasHu,
|
||||
score: existing?.gamePlayer.score ?? 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const privateHandTiles = normalizeTiles(playerView?.hand)
|
||||
const privateHand = privateHandTiles.map((tile) => tileToText(tile))
|
||||
if (options.loggedInUserId && playerMap.has(options.loggedInUserId)) {
|
||||
const current = playerMap.get(options.loggedInUserId)
|
||||
if (current) {
|
||||
const selfMissingSuit = readMissingSuitWithPresence(playerView)
|
||||
current.roomPlayer.hand = privateHand
|
||||
if (selfMissingSuit.present) {
|
||||
current.roomPlayer.missingSuit = selfMissingSuit.value
|
||||
}
|
||||
current.gamePlayer.handTiles = privateHandTiles
|
||||
current.gamePlayer.handCount = privateHandTiles.length
|
||||
if (selfMissingSuit.present) {
|
||||
current.gamePlayer.missingSuit = selfMissingSuit.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
room,
|
||||
gameState,
|
||||
playerView,
|
||||
roomPlayers: Array.from(playerMap.values()).sort((a, b) => a.gamePlayer.seatIndex - b.gamePlayer.seatIndex),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRoomInfoSnapshot(
|
||||
options: ParseRoomInfoSnapshotOptions,
|
||||
): ParsedRoomInfoSnapshot | null {
|
||||
const payload = asRecord(options.message.payload) ?? options.message
|
||||
const { room, gameState, playerView, roomPlayers } = buildPlayerPairs(options, payload)
|
||||
|
||||
const roomId =
|
||||
readString(room ?? {}, 'room_id', 'roomId') ||
|
||||
readString(gameState ?? {}, 'room_id', 'roomId') ||
|
||||
readString(playerView ?? {}, 'room_id', 'roomId') ||
|
||||
readString(payload, 'room_id', 'roomId') ||
|
||||
readString(options.message, 'roomId')
|
||||
if (!roomId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nextPlayers: Record<string, PlayerState> = {}
|
||||
roomPlayers.forEach(({ gamePlayer }) => {
|
||||
const previous = options.previousPlayers[gamePlayer.playerId]
|
||||
const score =
|
||||
gameState?.scores && typeof gameState.scores === 'object'
|
||||
? (gameState.scores as Record<string, unknown>)[gamePlayer.playerId]
|
||||
: undefined
|
||||
nextPlayers[gamePlayer.playerId] = {
|
||||
playerId: gamePlayer.playerId,
|
||||
seatIndex: gamePlayer.seatIndex,
|
||||
displayName: gamePlayer.displayName ?? previous?.displayName,
|
||||
avatarURL: gamePlayer.avatarURL ?? previous?.avatarURL,
|
||||
missingSuit:
|
||||
typeof gamePlayer.missingSuit === 'undefined' ? (previous?.missingSuit ?? null) : gamePlayer.missingSuit,
|
||||
isTrustee: previous?.isTrustee ?? gamePlayer.isTrustee,
|
||||
handTiles: gamePlayer.handTiles.length > 0 ? gamePlayer.handTiles : previous?.handTiles ?? [],
|
||||
handCount:
|
||||
gamePlayer.handCount > 0
|
||||
? gamePlayer.handCount
|
||||
: gamePlayer.handTiles.length > 0
|
||||
? gamePlayer.handTiles.length
|
||||
: (previous?.handCount ?? 0),
|
||||
melds: gamePlayer.melds.length > 0 ? gamePlayer.melds : previous?.melds ?? [],
|
||||
discardTiles: gamePlayer.discardTiles.length > 0 ? gamePlayer.discardTiles : previous?.discardTiles ?? [],
|
||||
hasHu: gamePlayer.hasHu || previous?.hasHu || false,
|
||||
score: typeof score === 'number' ? score : previous?.score ?? gamePlayer.score ?? 0,
|
||||
isReady: gamePlayer.isReady,
|
||||
}
|
||||
})
|
||||
|
||||
const status =
|
||||
readString(gameState ?? {}, 'status') ||
|
||||
readString(room ?? {}, 'status') ||
|
||||
readString(gameState ?? {}, 'phase') ||
|
||||
'waiting'
|
||||
const phase = readString(gameState ?? {}, 'phase') || readString(room ?? {}, 'status') || 'waiting'
|
||||
const wallCount = readNumber(gameState ?? {}, 'wall_count', 'wallCount')
|
||||
const dealerIndex = readNumber(gameState ?? {}, 'dealer_index', 'dealerIndex')
|
||||
const currentTurnSeat = readNumber(gameState ?? {}, 'current_turn', 'currentTurn')
|
||||
const currentTurnPlayerId = readString(gameState ?? {}, 'current_turn_player', 'currentTurnPlayer') || ''
|
||||
const currentTurn =
|
||||
currentTurnSeat ??
|
||||
(currentTurnPlayerId && nextPlayers[currentTurnPlayerId] ? nextPlayers[currentTurnPlayerId].seatIndex : null)
|
||||
|
||||
return {
|
||||
room,
|
||||
gameState,
|
||||
playerView,
|
||||
roomId,
|
||||
roomPlayers,
|
||||
nextPlayers,
|
||||
status,
|
||||
phase,
|
||||
wallCount,
|
||||
dealerIndex,
|
||||
currentTurnPlayerId,
|
||||
currentTurn,
|
||||
needDraw: readBoolean(gameState ?? {}, 'need_draw', 'needDraw') ?? false,
|
||||
pendingClaim: normalizePendingClaim(gameState, options.loggedInUserId),
|
||||
scores: asRecord(gameState?.scores)
|
||||
? (Object.fromEntries(
|
||||
Object.entries(asRecord(gameState?.scores) ?? {}).filter(([, value]) => typeof value === 'number'),
|
||||
) as Record<string, number>)
|
||||
: undefined,
|
||||
winners: readStringArray(gameState ?? {}, 'winners'),
|
||||
currentRound: readNumber(gameState ?? {}, 'current_round', 'currentRound'),
|
||||
totalRounds: readNumber(gameState ?? {}, 'total_rounds', 'totalRounds'),
|
||||
settlementDeadlineMs: readNumber(gameState ?? {}, 'settlement_deadline_ms', 'settlementDeadlineMs'),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user