|
|
|
|
@@ -34,7 +34,7 @@ import {wsClient} from '../ws/client'
|
|
|
|
|
import {sendWsMessage} from '../ws/sender'
|
|
|
|
|
import {buildWsUrl} from '../ws/url'
|
|
|
|
|
import {useGameStore} from '../store/gameStore'
|
|
|
|
|
import {useActiveRoomState} from '../store'
|
|
|
|
|
import {setActiveRoom, useActiveRoomState} from '../store'
|
|
|
|
|
import type {PlayerState} from '../types/state'
|
|
|
|
|
import type {Tile} from '../types/tile'
|
|
|
|
|
|
|
|
|
|
@@ -65,8 +65,10 @@ const wsError = ref('')
|
|
|
|
|
const selectedTile = ref<string | null>(null)
|
|
|
|
|
const leaveRoomPending = ref(false)
|
|
|
|
|
const readyTogglePending = ref(false)
|
|
|
|
|
const startGamePending = ref(false)
|
|
|
|
|
let clockTimer: number | null = null
|
|
|
|
|
let unsubscribe: (() => void) | null = null
|
|
|
|
|
let pendingRoomInfoRequest = false
|
|
|
|
|
|
|
|
|
|
const menuOpen = ref(false)
|
|
|
|
|
const isTrustMode = ref(false)
|
|
|
|
|
@@ -238,8 +240,46 @@ const myReadyState = computed(() => {
|
|
|
|
|
return Boolean(myPlayer.value?.isReady)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const isRoomOwner = computed(() => {
|
|
|
|
|
const room = activeRoom.value
|
|
|
|
|
return Boolean(
|
|
|
|
|
room &&
|
|
|
|
|
room.roomId === gameStore.roomId &&
|
|
|
|
|
room.ownerId &&
|
|
|
|
|
loggedInUserId.value &&
|
|
|
|
|
room.ownerId === loggedInUserId.value,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const allPlayersReady = computed(() => {
|
|
|
|
|
return (
|
|
|
|
|
gamePlayers.value.length === 4 &&
|
|
|
|
|
gamePlayers.value.every((player) => Boolean(player.isReady))
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const showStartGameButton = computed(() => {
|
|
|
|
|
return gameStore.phase === 'waiting' && allPlayersReady.value
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const showWaitingOwnerTip = computed(() => {
|
|
|
|
|
return showStartGameButton.value && !isRoomOwner.value
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const canStartGame = computed(() => {
|
|
|
|
|
return showStartGameButton.value && isRoomOwner.value && !startGamePending.value
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const showReadyToggle = computed(() => {
|
|
|
|
|
return gameStore.phase === 'waiting' && Boolean(gameStore.roomId)
|
|
|
|
|
if (gameStore.phase !== 'waiting' || !gameStore.roomId) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (showStartGameButton.value) {
|
|
|
|
|
return !isRoomOwner.value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function applyPlayerReadyState(playerId: string, ready: boolean): void {
|
|
|
|
|
@@ -284,6 +324,370 @@ function syncReadyStatesFromRoomUpdate(payload: RoomPlayerUpdatePayload): void {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeWsType(type: string): string {
|
|
|
|
|
return type.replace(/[-\s]/g, '_').toUpperCase()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
|
|
return value && typeof value === 'object' ? value as Record<string, unknown> : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readString(source: Record<string, unknown>, ...keys: string[]): string {
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
const value = source[key]
|
|
|
|
|
if (typeof value === 'string' && value.trim()) {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readNumber(source: Record<string, unknown>, ...keys: string[]): number | null {
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
const value = source[key]
|
|
|
|
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readStringArray(source: Record<string, unknown>, ...keys: string[]): string[] {
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
const value = source[key]
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return value.filter((item): item is string => typeof item === 'string')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readBoolean(source: Record<string, unknown>, ...keys: string[]): boolean | null {
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
const value = source[key]
|
|
|
|
|
if (typeof value === 'boolean') {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function tileToText(tile: Tile): string {
|
|
|
|
|
return `${tile.suit}${tile.value}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeTile(tile: unknown): Tile | null {
|
|
|
|
|
const source = asRecord(tile)
|
|
|
|
|
if (!source) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const id = readNumber(source, 'id')
|
|
|
|
|
const suit = readString(source, 'suit') as Tile['suit'] | ''
|
|
|
|
|
const value = readNumber(source, 'value')
|
|
|
|
|
if (typeof id !== 'number' || !suit || typeof value !== 'number') {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (suit !== 'W' && suit !== 'T' && suit !== 'B') {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
suit,
|
|
|
|
|
value,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeTiles(value: unknown): Tile[] {
|
|
|
|
|
if (!Array.isArray(value)) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
.map((item) => normalizeTile(item))
|
|
|
|
|
.filter((item): item is Tile => Boolean(item))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requestRoomInfo(): void {
|
|
|
|
|
const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
|
|
|
|
|
const roomId = routeRoomId || gameStore.roomId || activeRoom.value?.roomId || ''
|
|
|
|
|
if (!roomId) {
|
|
|
|
|
pendingRoomInfoRequest = true
|
|
|
|
|
wsMessages.value.push('[client] get_room_info pending: missing roomId')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (wsStatus.value !== 'connected') {
|
|
|
|
|
pendingRoomInfoRequest = true
|
|
|
|
|
wsMessages.value.push(`[client] get_room_info pending: ws=${wsStatus.value}`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pendingRoomInfoRequest = false
|
|
|
|
|
wsMessages.value.push(`[client] get_room_info ${roomId}`)
|
|
|
|
|
sendWsMessage({
|
|
|
|
|
type: 'get_room_info',
|
|
|
|
|
roomId,
|
|
|
|
|
payload: {
|
|
|
|
|
room_id: roomId,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleRoomInfoResponse(message: unknown): void {
|
|
|
|
|
const source = asRecord(message)
|
|
|
|
|
if (!source || typeof source.type !== 'string') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalizedType = normalizeWsType(source.type)
|
|
|
|
|
if (normalizedType !== 'GET_ROOM_INFO' && normalizedType !== 'ROOM_INFO') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payload = asRecord(source.payload) ?? source
|
|
|
|
|
const summary = asRecord(payload.summary) ?? asRecord(payload.room_summary) ?? null
|
|
|
|
|
const publicState = asRecord(payload.public) ?? asRecord(payload.public_state) ?? null
|
|
|
|
|
const privateState = asRecord(payload.private) ?? asRecord(payload.private_state) ?? null
|
|
|
|
|
const roomId =
|
|
|
|
|
readString(summary ?? {}, 'room_id', 'roomId') ||
|
|
|
|
|
readString(publicState ?? {}, 'room_id', 'roomId') ||
|
|
|
|
|
readString(privateState ?? {}, 'room_id', 'roomId') ||
|
|
|
|
|
readString(payload, 'room_id', 'roomId') ||
|
|
|
|
|
readString(source, 'roomId')
|
|
|
|
|
if (!roomId) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const summaryPlayers = Array.isArray(summary?.players) ? summary.players : []
|
|
|
|
|
const publicPlayers = Array.isArray(publicState?.players) ? publicState.players : []
|
|
|
|
|
const playerMap = new Map<string, {
|
|
|
|
|
roomPlayer: {
|
|
|
|
|
index: number
|
|
|
|
|
playerId: string
|
|
|
|
|
displayName?: string
|
|
|
|
|
missingSuit?: string | null
|
|
|
|
|
ready: boolean
|
|
|
|
|
hand: string[]
|
|
|
|
|
melds: string[]
|
|
|
|
|
outTiles: string[]
|
|
|
|
|
hasHu: boolean
|
|
|
|
|
}
|
|
|
|
|
gamePlayer: {
|
|
|
|
|
playerId: string
|
|
|
|
|
seatIndex: number
|
|
|
|
|
displayName?: string
|
|
|
|
|
avatarURL?: string
|
|
|
|
|
missingSuit?: string | null
|
|
|
|
|
isReady: boolean
|
|
|
|
|
handTiles: Tile[]
|
|
|
|
|
melds: PlayerState['melds']
|
|
|
|
|
discardTiles: Tile[]
|
|
|
|
|
score: number
|
|
|
|
|
}
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
summaryPlayers.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 === loggedInUserId.value ? loggedInUserName.value : '')
|
|
|
|
|
const ready = readBoolean(player, 'ready', 'Ready') ?? false
|
|
|
|
|
const missingSuit = readString(player, 'missing_suit', 'MissingSuit') || null
|
|
|
|
|
|
|
|
|
|
playerMap.set(playerId, {
|
|
|
|
|
roomPlayer: {
|
|
|
|
|
index: seatIndex,
|
|
|
|
|
playerId,
|
|
|
|
|
displayName: displayName || undefined,
|
|
|
|
|
missingSuit,
|
|
|
|
|
ready,
|
|
|
|
|
hand: [],
|
|
|
|
|
melds: [],
|
|
|
|
|
outTiles: [],
|
|
|
|
|
hasHu: false,
|
|
|
|
|
},
|
|
|
|
|
gamePlayer: {
|
|
|
|
|
playerId,
|
|
|
|
|
seatIndex,
|
|
|
|
|
displayName: displayName || undefined,
|
|
|
|
|
avatarURL: readString(player, 'avatar_url', 'AvatarUrl', 'avatar', 'avatarUrl') || undefined,
|
|
|
|
|
missingSuit,
|
|
|
|
|
isReady: ready,
|
|
|
|
|
handTiles: [],
|
|
|
|
|
melds: [],
|
|
|
|
|
discardTiles: [],
|
|
|
|
|
score: 0,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
publicPlayers.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 === loggedInUserId.value ? loggedInUserName.value : '')
|
|
|
|
|
const missingSuit = readString(player, 'missing_suit', 'MissingSuit') || existing?.gamePlayer.missingSuit || null
|
|
|
|
|
const handCount = readNumber(player, 'hand_count', 'handCount') ?? 0
|
|
|
|
|
const outTiles = normalizeTiles(player.out_tiles ?? player.outTiles)
|
|
|
|
|
|
|
|
|
|
playerMap.set(playerId, {
|
|
|
|
|
roomPlayer: {
|
|
|
|
|
index: seatIndex,
|
|
|
|
|
playerId,
|
|
|
|
|
displayName: displayName || undefined,
|
|
|
|
|
missingSuit,
|
|
|
|
|
ready: existing?.roomPlayer.ready ?? false,
|
|
|
|
|
hand: Array.from({length: handCount}, () => ''),
|
|
|
|
|
melds: [],
|
|
|
|
|
outTiles: outTiles.map((tile) => tileToText(tile)),
|
|
|
|
|
hasHu: Boolean(player.has_hu ?? player.hasHu),
|
|
|
|
|
},
|
|
|
|
|
gamePlayer: {
|
|
|
|
|
playerId,
|
|
|
|
|
seatIndex,
|
|
|
|
|
displayName: displayName || undefined,
|
|
|
|
|
avatarURL: existing?.gamePlayer.avatarURL,
|
|
|
|
|
missingSuit,
|
|
|
|
|
isReady: existing?.gamePlayer.isReady ?? false,
|
|
|
|
|
handTiles: existing?.gamePlayer.handTiles ?? [],
|
|
|
|
|
melds: existing?.gamePlayer.melds ?? [],
|
|
|
|
|
discardTiles: outTiles,
|
|
|
|
|
score: existing?.gamePlayer.score ?? 0,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const privateHand = normalizeTiles(privateState?.hand)
|
|
|
|
|
if (loggedInUserId.value && playerMap.has(loggedInUserId.value)) {
|
|
|
|
|
const current = playerMap.get(loggedInUserId.value)
|
|
|
|
|
if (current) {
|
|
|
|
|
current.roomPlayer.hand = privateHand.map((tile) => tileToText(tile))
|
|
|
|
|
current.gamePlayer.handTiles = privateHand
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const players = Array.from(playerMap.values()).sort((a, b) => a.gamePlayer.seatIndex - b.gamePlayer.seatIndex)
|
|
|
|
|
|
|
|
|
|
const previousPlayers = gameStore.players
|
|
|
|
|
const nextPlayers: typeof gameStore.players = {}
|
|
|
|
|
players.forEach(({gamePlayer}) => {
|
|
|
|
|
const previous = previousPlayers[gamePlayer.playerId]
|
|
|
|
|
const score = (publicState?.scores && typeof publicState.scores === 'object'
|
|
|
|
|
? (publicState.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: gamePlayer.missingSuit ?? previous?.missingSuit,
|
|
|
|
|
handTiles: gamePlayer.handTiles.length > 0 ? gamePlayer.handTiles : previous?.handTiles ?? [],
|
|
|
|
|
melds: gamePlayer.melds.length > 0 ? gamePlayer.melds : previous?.melds ?? [],
|
|
|
|
|
discardTiles: gamePlayer.discardTiles.length > 0 ? gamePlayer.discardTiles : previous?.discardTiles ?? [],
|
|
|
|
|
score: typeof score === 'number' ? score : previous?.score ?? gamePlayer.score ?? 0,
|
|
|
|
|
isReady: gamePlayer.isReady,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const status =
|
|
|
|
|
readString(publicState ?? {}, 'status') ||
|
|
|
|
|
readString(summary ?? {}, 'status') ||
|
|
|
|
|
readString(publicState ?? {}, 'phase') ||
|
|
|
|
|
'waiting'
|
|
|
|
|
const phase =
|
|
|
|
|
readString(publicState ?? {}, 'phase') ||
|
|
|
|
|
readString(summary ?? {}, 'status') ||
|
|
|
|
|
'waiting'
|
|
|
|
|
const wallCount = readNumber(publicState ?? {}, 'wall_count', 'wallCount')
|
|
|
|
|
const dealerIndex = readNumber(publicState ?? {}, 'dealer_index', 'dealerIndex')
|
|
|
|
|
const currentTurnSeat = readNumber(publicState ?? {}, 'current_turn', 'currentTurn')
|
|
|
|
|
const currentTurnPlayerId = readString(publicState ?? {}, 'current_turn_player', 'currentTurnPlayer')
|
|
|
|
|
const currentTurn =
|
|
|
|
|
currentTurnSeat ??
|
|
|
|
|
(currentTurnPlayerId && nextPlayers[currentTurnPlayerId]
|
|
|
|
|
? nextPlayers[currentTurnPlayerId].seatIndex
|
|
|
|
|
: null)
|
|
|
|
|
|
|
|
|
|
gameStore.roomId = roomId
|
|
|
|
|
if (Object.keys(nextPlayers).length > 0) {
|
|
|
|
|
gameStore.players = nextPlayers
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const phaseMap: Record<string, typeof gameStore.phase> = {
|
|
|
|
|
waiting: 'waiting',
|
|
|
|
|
dealing: 'dealing',
|
|
|
|
|
playing: 'playing',
|
|
|
|
|
action: 'action',
|
|
|
|
|
settlement: 'settlement',
|
|
|
|
|
finished: 'settlement',
|
|
|
|
|
}
|
|
|
|
|
gameStore.phase = phaseMap[phase] ?? gameStore.phase
|
|
|
|
|
if (typeof wallCount === 'number') {
|
|
|
|
|
gameStore.remainingTiles = wallCount
|
|
|
|
|
}
|
|
|
|
|
if (typeof dealerIndex === 'number') {
|
|
|
|
|
gameStore.dealerIndex = dealerIndex
|
|
|
|
|
}
|
|
|
|
|
if (typeof currentTurn === 'number') {
|
|
|
|
|
gameStore.currentTurn = currentTurn
|
|
|
|
|
}
|
|
|
|
|
const scores = asRecord(publicState?.scores)
|
|
|
|
|
if (scores) {
|
|
|
|
|
gameStore.scores = Object.fromEntries(
|
|
|
|
|
Object.entries(scores).filter(([, value]) => typeof value === 'number'),
|
|
|
|
|
) as Record<string, number>
|
|
|
|
|
}
|
|
|
|
|
gameStore.winners = readStringArray(publicState ?? {}, 'winners')
|
|
|
|
|
|
|
|
|
|
setActiveRoom({
|
|
|
|
|
roomId,
|
|
|
|
|
roomName: readString(summary ?? {}, 'name', 'room_name', 'roomName') || activeRoom.value?.roomName || roomName.value,
|
|
|
|
|
gameType: readString(summary ?? {}, 'game_type', 'gameType') || activeRoom.value?.gameType || 'chengdu',
|
|
|
|
|
ownerId: readString(summary ?? {}, 'owner_id', 'ownerId') || activeRoom.value?.ownerId || '',
|
|
|
|
|
maxPlayers: readNumber(summary ?? {}, 'max_players', 'maxPlayers') ?? activeRoom.value?.maxPlayers ?? 4,
|
|
|
|
|
playerCount: readNumber(summary ?? {}, 'player_count', 'playerCount') ?? players.length,
|
|
|
|
|
status,
|
|
|
|
|
createdAt: readString(summary ?? {}, 'created_at', 'createdAt') || activeRoom.value?.createdAt || '',
|
|
|
|
|
updatedAt: readString(summary ?? {}, 'updated_at', 'updatedAt') || activeRoom.value?.updatedAt || '',
|
|
|
|
|
players: players.map((item) => item.roomPlayer),
|
|
|
|
|
myHand: privateHand.map((tile) => tileToText(tile)),
|
|
|
|
|
game: {
|
|
|
|
|
state: {
|
|
|
|
|
wall: Array.from({length: wallCount ?? 0}, (_, index) => `wall-${index}`),
|
|
|
|
|
scores: gameStore.scores,
|
|
|
|
|
dealerIndex: typeof dealerIndex === 'number' ? dealerIndex : activeRoom.value?.game?.state?.dealerIndex ?? -1,
|
|
|
|
|
currentTurn: typeof currentTurn === 'number' ? currentTurn : activeRoom.value?.game?.state?.currentTurn ?? -1,
|
|
|
|
|
phase,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const networkLabel = computed(() => {
|
|
|
|
|
const map: Record<WsStatus, string> = {
|
|
|
|
|
connected: '已连接',
|
|
|
|
|
@@ -450,7 +854,7 @@ function toGameAction(message: unknown): GameAction | null {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const type = source.type.replace(/[-\s]/g, '_').toUpperCase()
|
|
|
|
|
const type = normalizeWsType(source.type)
|
|
|
|
|
const payload = source.payload
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
@@ -509,7 +913,7 @@ function handleReadyStateResponse(message: unknown): void {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const type = source.type.replace(/[-\s]/g, '_').toUpperCase()
|
|
|
|
|
const type = normalizeWsType(source.type)
|
|
|
|
|
if (type !== 'SET_READY') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
@@ -754,6 +1158,21 @@ function toggleReadyState(): void {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startGame(): void {
|
|
|
|
|
if (!canStartGame.value) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startGamePending.value = true
|
|
|
|
|
sendWsMessage({
|
|
|
|
|
type: 'start_game',
|
|
|
|
|
roomId: gameStore.roomId,
|
|
|
|
|
payload: {
|
|
|
|
|
room_id: gameStore.roomId,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleLeaveRoom(): void {
|
|
|
|
|
menuOpen.value = false
|
|
|
|
|
backHall()
|
|
|
|
|
@@ -824,24 +1243,33 @@ function hydrateFromActiveRoom(routeRoomId: string): void {
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
|
|
|
|
|
pendingRoomInfoRequest = true
|
|
|
|
|
void ensureCurrentUserLoaded().finally(() => {
|
|
|
|
|
hydrateFromActiveRoom(routeRoomId)
|
|
|
|
|
if (routeRoomId) {
|
|
|
|
|
gameStore.roomId = routeRoomId
|
|
|
|
|
}
|
|
|
|
|
requestRoomInfo()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handler = (status: WsStatus) => {
|
|
|
|
|
wsStatus.value = status
|
|
|
|
|
if (status === 'connected' && pendingRoomInfoRequest) {
|
|
|
|
|
requestRoomInfo()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wsClient.onMessage((msg: unknown) => {
|
|
|
|
|
const text = typeof msg === 'string' ? msg : JSON.stringify(msg)
|
|
|
|
|
wsMessages.value.push(`[server] ${text}`)
|
|
|
|
|
handleRoomInfoResponse(msg)
|
|
|
|
|
handleReadyStateResponse(msg)
|
|
|
|
|
const gameAction = toGameAction(msg)
|
|
|
|
|
if (gameAction) {
|
|
|
|
|
dispatchGameAction(gameAction)
|
|
|
|
|
if (gameAction.type === 'GAME_START') {
|
|
|
|
|
startGamePending.value = false
|
|
|
|
|
}
|
|
|
|
|
if (gameAction.type === 'ROOM_PLAYER_UPDATE') {
|
|
|
|
|
syncReadyStatesFromRoomUpdate(gameAction.payload)
|
|
|
|
|
readyTogglePending.value = false
|
|
|
|
|
@@ -1000,18 +1428,33 @@ onBeforeUnmount(() => {
|
|
|
|
|
|
|
|
|
|
<WindSquare class="center-wind-square" :seat-winds="seatWinds"/>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
v-if="showReadyToggle"
|
|
|
|
|
class="ready-toggle"
|
|
|
|
|
type="button"
|
|
|
|
|
:disabled="readyTogglePending"
|
|
|
|
|
@click="toggleReadyState"
|
|
|
|
|
>
|
|
|
|
|
<span class="ready-toggle-label">{{ myReadyState ? '取 消' : '准 备' }}</span>
|
|
|
|
|
</button>
|
|
|
|
|
<div v-if="showWaitingOwnerTip" class="waiting-owner-tip">
|
|
|
|
|
<span>等待房主开始游戏</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="bottom-control-panel">
|
|
|
|
|
<div v-if="showReadyToggle || showStartGameButton" class="bottom-action-bar">
|
|
|
|
|
<button
|
|
|
|
|
v-if="showReadyToggle"
|
|
|
|
|
class="ready-toggle ready-toggle-inline"
|
|
|
|
|
type="button"
|
|
|
|
|
:disabled="readyTogglePending"
|
|
|
|
|
@click="toggleReadyState"
|
|
|
|
|
>
|
|
|
|
|
<span class="ready-toggle-label">{{ myReadyState ? '取 消' : '准 备' }}</span>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
v-if="showStartGameButton && isRoomOwner"
|
|
|
|
|
class="ready-toggle ready-toggle-inline"
|
|
|
|
|
type="button"
|
|
|
|
|
:disabled="!canStartGame"
|
|
|
|
|
@click="startGame"
|
|
|
|
|
>
|
|
|
|
|
<span class="ready-toggle-label">开始游戏</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="player-hand" v-if="myHandTiles.length > 0">
|
|
|
|
|
<button
|
|
|
|
|
|