- 实现 ChengduGamePage.vue 组件,包含完整的麻将游戏界面 - 实现 HallPage.vue 组件,支持房间列表展示、创建和加入功能 - 添加 mahjong API 接口用于房间管理操作 - 集成 store 状态管理和本地存储功能 - 实现 ChengduBottomActions 等游戏控制组件 - 添加 websocket 连接和游戏会话管理逻辑 - 实现游戏倒计时、结算等功能模块
144 lines
4.4 KiB
TypeScript
144 lines
4.4 KiB
TypeScript
import { computed, onBeforeUnmount, onMounted, type ComputedRef } from 'vue'
|
|
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
|
|
import type { RoomMetaSnapshotState } from '../../../store/state'
|
|
import type { PlayerState, Tile } from '../../../types/state'
|
|
import { wsClient, type WsStatus } from '../../../ws/client'
|
|
import { buildWsUrl } from '../../../ws/url'
|
|
import type { DisplayPlayer } from '../types'
|
|
import { useChengduGameSession } from './useChengduGameSession'
|
|
import { createChengduMessageHandlers } from '../socket/createMessageHandlers'
|
|
|
|
interface UseChengduGameSocketOptions {
|
|
route: RouteLocationNormalizedLoaded
|
|
router: Router
|
|
gameStore: {
|
|
roomId: string
|
|
phase: string
|
|
players: Record<string, PlayerState>
|
|
dealerIndex: number
|
|
currentTurn: number
|
|
remainingTiles: number
|
|
needDraw: boolean
|
|
pendingClaim?: any
|
|
scores: Record<string, number>
|
|
winners: string[]
|
|
currentRound: number
|
|
totalRounds: number
|
|
resetGame: () => void
|
|
}
|
|
roomMeta: { value: RoomMetaSnapshotState | null }
|
|
roomName: ComputedRef<string>
|
|
myHandTiles: ComputedRef<Tile[]>
|
|
myPlayer: ComputedRef<DisplayPlayer | undefined>
|
|
session: ReturnType<typeof useChengduGameSession>
|
|
}
|
|
|
|
export function useChengduGameSocket(options: UseChengduGameSocketOptions) {
|
|
let unsubscribe: (() => void) | null = null
|
|
let needsInitialRoomInfo = false
|
|
|
|
const showSettlementOverlay = computed(
|
|
() => options.gameStore.phase === 'settlement' && !options.session.settlementOverlayDismissed.value,
|
|
)
|
|
|
|
const settlementCountdown = computed(() => {
|
|
if (!showSettlementOverlay.value || !options.session.settlementDeadlineMs.value) {
|
|
return null
|
|
}
|
|
return Math.max(
|
|
0,
|
|
Math.ceil((options.session.settlementDeadlineMs.value - options.session.now.value) / 1000),
|
|
)
|
|
})
|
|
|
|
const handlers = createChengduMessageHandlers({
|
|
router: options.router,
|
|
gameStore: options.gameStore,
|
|
roomMeta: options.roomMeta,
|
|
roomName: options.roomName,
|
|
myHandTiles: options.myHandTiles,
|
|
myPlayer: options.myPlayer,
|
|
session: options.session,
|
|
})
|
|
|
|
function requestRoomInfo(): void {
|
|
const routeRoomId = typeof options.route.params.roomId === 'string' ? options.route.params.roomId : ''
|
|
const roomId = routeRoomId || options.gameStore.roomId || options.roomMeta.value?.roomId || ''
|
|
if (!roomId || options.session.wsStatus.value !== 'connected') {
|
|
return
|
|
}
|
|
|
|
needsInitialRoomInfo = false
|
|
options.session.wsMessages.value.push(`[client] get_room_info ${roomId}`)
|
|
wsClient.send({
|
|
type: 'get_room_info',
|
|
roomId,
|
|
payload: {
|
|
room_id: roomId,
|
|
},
|
|
})
|
|
}
|
|
|
|
function handleSocketError(message: string): void {
|
|
options.session.markDiscardCompleted()
|
|
options.session.clearTurnActionPending()
|
|
options.session.wsError.value = message
|
|
options.session.wsMessages.value.push(`[error] ${message}`)
|
|
|
|
const nowMs = Date.now()
|
|
if (nowMs - options.session.lastForcedRefreshAtRef.value > 5000) {
|
|
options.session.lastForcedRefreshAtRef.value = nowMs
|
|
void options.session
|
|
.resolveWsToken(true, true)
|
|
.then((refreshedToken) => {
|
|
if (!refreshedToken) {
|
|
return
|
|
}
|
|
options.session.wsError.value = ''
|
|
wsClient.reconnect(buildWsUrl(), refreshedToken)
|
|
})
|
|
.catch(() => {
|
|
options.session.logoutToLogin()
|
|
})
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
const routeRoomId = typeof options.route.params.roomId === 'string' ? options.route.params.roomId : ''
|
|
needsInitialRoomInfo = true
|
|
void options.session.ensureCurrentUserLoaded().finally(() => {
|
|
handlers.hydrateFromActiveRoom(routeRoomId)
|
|
if (routeRoomId) {
|
|
options.gameStore.roomId = routeRoomId
|
|
}
|
|
if (options.session.wsStatus.value === 'connected' && needsInitialRoomInfo) {
|
|
requestRoomInfo()
|
|
}
|
|
})
|
|
|
|
const statusHandler = (status: WsStatus) => {
|
|
options.session.wsStatus.value = status
|
|
if (status === 'connected' && needsInitialRoomInfo) {
|
|
requestRoomInfo()
|
|
}
|
|
}
|
|
|
|
wsClient.onMessage(handlers.handleSocketMessage)
|
|
wsClient.onError(handleSocketError)
|
|
unsubscribe = wsClient.onStatusChange(statusHandler)
|
|
void options.session.ensureWsConnected()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (unsubscribe) {
|
|
unsubscribe()
|
|
unsubscribe = null
|
|
}
|
|
})
|
|
|
|
return {
|
|
showSettlementOverlay,
|
|
settlementCountdown,
|
|
}
|
|
}
|