Files
mahjong-web/src/views/chengdu/composables/useChengduGameSocket.ts
wsy182 e96c45739e feat(game): 添加成都麻将游戏页面和大厅功能
- 实现 ChengduGamePage.vue 组件,包含完整的麻将游戏界面
- 实现 HallPage.vue 组件,支持房间列表展示、创建和加入功能
- 添加 mahjong API 接口用于房间管理操作
- 集成 store 状态管理和本地存储功能
- 实现 ChengduBottomActions 等游戏控制组件
- 添加 websocket 连接和游戏会话管理逻辑
- 实现游戏倒计时、结算等功能模块
2026-04-03 20:46:50 +08:00

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,
}
}