From 148e21f3b00cdb6ab2cb3740a41553171e8efec6 Mon Sep 17 00:00:00 2001 From: wsy182 <2392948297@qq.com> Date: Wed, 25 Mar 2026 13:34:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor(game):=20=E9=87=8D=E6=9E=84=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E5=8A=A8=E4=BD=9C=E5=A4=84=E7=90=86=E5=92=8CWebSocket?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构sendGameAction函数参数结构,添加上下文支持 - 新增sendStartGame和sendLeaveRoom函数统一处理游戏开始和离开房间逻辑 - 移除路由相关依赖,简化ChengduGamePage组件 - 更新WebSocket客户端实现,添加状态变化订阅功能 - 移除requestId生成函数和相关参数,精简消息结构 - 优化座位玩家卡片数据模型,移除在线状态和金钱字段 - 整理游戏阶段常量定义,添加标签映射 - 移除过期的游戏状态字段如needDraw、lastDiscardTile等 - 添加座位类型定义和改进游戏类型文件组织结构 --- src/common/id.ts | 4 - src/components/game/SeatPlayerCard.vue | 7 +- src/components/game/seat-player-card.ts | 15 +- src/game/actions.ts | 140 +++- src/game/seat.ts | 2 + .../game/engine.ts => game/types/game.ts} | 1 - src/game/useChengduGameRoom.ts | 726 ------------------ src/types/state/gamePhase.ts | 20 +- src/types/state/gamestate.ts | 25 +- src/types/state/pendingClaim.ts | 3 +- src/views/ChengduGamePage.vue | 103 +-- src/ws/client.ts | 56 +- src/ws/message.ts | 1 - 13 files changed, 236 insertions(+), 867 deletions(-) delete mode 100644 src/common/id.ts create mode 100644 src/game/seat.ts rename src/{modules/game/engine.ts => game/types/game.ts} (99%) delete mode 100644 src/game/useChengduGameRoom.ts diff --git a/src/common/id.ts b/src/common/id.ts deleted file mode 100644 index 0fa2dc7..0000000 --- a/src/common/id.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 生成 requestId / traceId -export function createRequestId(prefix: string): string { - return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}` -} \ No newline at end of file diff --git a/src/components/game/SeatPlayerCard.vue b/src/components/game/SeatPlayerCard.vue index 003b993..848e7c9 100644 --- a/src/components/game/SeatPlayerCard.vue +++ b/src/components/game/SeatPlayerCard.vue @@ -26,8 +26,8 @@ const missingSuitIcon = computed(() => { + \ No newline at end of file diff --git a/src/components/game/seat-player-card.ts b/src/components/game/seat-player-card.ts index 284b81e..2f8971f 100644 --- a/src/components/game/seat-player-card.ts +++ b/src/components/game/seat-player-card.ts @@ -1,9 +1,8 @@ +// 玩家卡片展示模型(用于座位UI渲染) export interface SeatPlayerCardModel { - avatar: string - name: string - money: string - dealer: boolean - isTurn: boolean - isOnline: boolean - missingSuitLabel: string -} + avatar: string // 头像 + name: string // 显示名称 + dealer: boolean // 是否庄家 + isTurn: boolean // 是否当前轮到该玩家 + missingSuitLabel: string // 定缺花色(万/筒/条) +} \ No newline at end of file diff --git a/src/game/actions.ts b/src/game/actions.ts index 3d3cb64..351642c 100644 --- a/src/game/actions.ts +++ b/src/game/actions.ts @@ -1,37 +1,145 @@ -import type { ActionButtonState } from "./types.ts" -import { wsClient } from "@/ws/client" // 新增 +import type {ActionButtonState} from "./types.ts" +import {wsClient} from "../ws/client.ts" -export function sendGameAction(type: ActionButtonState['type']): void { - // 原来是判断 ws,这里改成用 wsClient 的状态(简单处理) - if (!currentUserId.value) { + +export function sendGameAction( + params: { + type: ActionButtonState['type'] + userID: string + roomId: string + selectedTile?: string | null + }, + ctx: { + actionPending: { value: boolean } + logWsSend: (msg: any) => void + pushWsMessage: (msg: string) => void + } +): void { + + const {type, userID, roomId, selectedTile} = params + const {actionPending, logWsSend, pushWsMessage} = ctx + + // 简单登录判断 + if (!userID) { + console.log('当前用户未登录') return } - const requestId = createRequestId(type) const payload: Record = {} - if (type === 'discard' && selectedTile.value) { - payload.tile = selectedTile.value - payload.discard_tile = selectedTile.value - payload.code = selectedTile.value + // 出牌 + if (type === 'discard' && selectedTile) { + payload.tile = selectedTile + payload.discard_tile = selectedTile + payload.code = selectedTile } actionPending.value = true const message = { type, - sender: currentUserId.value, + sender: userID, target: 'room', - roomId: roomState.value.id || roomId.value, + roomId, seq: Date.now(), - requestId, - trace_id: createRequestId('trace'), payload, } logWsSend(message) + wsClient.send(message) + pushWsMessage(`[client] 请求${type}`) +} - wsClient.send(message) // ✅ 改这里 - pushWsMessage(`[client] 请求${type} requestId=${requestId}`) +export function sendStartGame(params: { + userID: string + roomId: string + canStartGame: boolean + startGamePending: { value: boolean } + logWsSend: (msg: any) => void + pushWsMessage: (msg: string) => void +}): void { + + const { + userID, + roomId, + canStartGame, + startGamePending, + logWsSend, + pushWsMessage + } = params + + if (!canStartGame || startGamePending.value) { + return + } + + if (!userID) { + return + } + + startGamePending.value = true + + const message = { + type: 'start_game', + sender: userID, + target: 'room', + roomId, + seq: Date.now(), + + payload: {}, + } + + logWsSend(message) + wsClient.send(message) + + pushWsMessage(`[client] 请求开始游戏`) +} + + +export function sendLeaveRoom(params: { + userID: string + roomId: string + wsError: { value: string } + leaveRoomPending: { value: boolean } + logWsSend: (msg: any) => void + pushWsMessage: (msg: string) => void +}): boolean { + + const { + userID, + roomId, + wsError, + leaveRoomPending, + logWsSend, + pushWsMessage + } = params + + if (!userID) { + wsError.value = '缺少当前用户 ID,无法退出房间' + return false + } + + if (!roomId) { + wsError.value = '缺少房间 ID,无法退出房间' + return false + } + + leaveRoomPending.value = true + + const message = { + type: 'leave_room', + sender: userID, + target: 'room', + roomId: roomId, + seq: Date.now(), + payload: {}, + } + + logWsSend(message) + + wsClient.send(message) + + pushWsMessage(`[client] 请求退出房间`) + + return true } \ No newline at end of file diff --git a/src/game/seat.ts b/src/game/seat.ts new file mode 100644 index 0000000..e82d4ae --- /dev/null +++ b/src/game/seat.ts @@ -0,0 +1,2 @@ +export type SeatKey = 'top' | 'right' | 'bottom' | 'left' + diff --git a/src/modules/game/engine.ts b/src/game/types/game.ts similarity index 99% rename from src/modules/game/engine.ts rename to src/game/types/game.ts index 41a3c80..2191ef0 100644 --- a/src/modules/game/engine.ts +++ b/src/game/types/game.ts @@ -1,5 +1,4 @@ // 游戏对象 - import type {GameState} from "../../types/state"; export interface Game { diff --git a/src/game/useChengduGameRoom.ts b/src/game/useChengduGameRoom.ts deleted file mode 100644 index 5d7093c..0000000 --- a/src/game/useChengduGameRoom.ts +++ /dev/null @@ -1,726 +0,0 @@ -import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' -import type { RouteLocationNormalizedLoaded, Router } from 'vue-router' -import type { AuthSession } from '../api/authed-request.ts' -import { refreshAccessToken } from '../api/auth.ts' -import { getUserInfo } from '../api/user.ts' -import { - activeRoomState, - destroyActiveRoomState, - mergeActiveRoomState, - resetActiveRoomState, - type RoomPlayerState, - type RoomState, -} from '../../store/active-room-store' -import { readStoredAuth, writeStoredAuth } from '../utils/auth-storage.ts' -import type { - ActionButtonState, - ActionEventLike, - ChengduGameRoomModel, - PendingClaimOption, - SeatKey, - SeatView, -} from './types' -import { toRecord, toStringOrEmpty } from './parser-utils' -import { normalizePendingClaimOptions, normalizeRoom, normalizeTileList } from './room-normalizers' - -export type { - ActionButtonState, - ChengduGameRoomModel, - SeatKey, - SeatView, -} from './types' -const WS_BASE_URL = import.meta.env.VITE_GAME_WS_URL ?? '/api/v1/ws' - -export function useChengduGameRoom( - route: RouteLocationNormalizedLoaded, - router: Router, -): ChengduGameRoomModel { - const auth = ref(readStoredAuth()) - const ws = ref(null) - const wsStatus = ref<'disconnected' | 'connecting' | 'connected'>('disconnected') - const wsError = ref('') - const wsMessages = ref([]) - const startGamePending = ref(false) - const actionPending = ref(false) - const lastStartRequestId = ref('') - const leaveRoomPending = ref(false) - const lastLeaveRoomRequestId = ref('') - const leaveHallAfterAck = ref(false) - const selectedTile = ref(null) - - const roomId = computed(() => { - return typeof route.params.roomId === 'string' ? route.params.roomId : '' - }) - - const roomName = computed(() => { - return typeof route.query.roomName === 'string' ? route.query.roomName : '' - }) - - const currentUserId = computed(() => { - const user = auth.value?.user as Record | undefined - const candidate = user?.id ?? user?.userID ?? user?.user_id - if (typeof candidate === 'string') { - return candidate - } - if (typeof candidate === 'number' && Number.isFinite(candidate)) { - return String(candidate) - } - return '' - }) - - const loggedInUserName = computed(() => { - if (!auth.value?.user) { - return '' - } - - return auth.value.user.nickname ?? auth.value.user.username ?? '' - }) - - const roomState = activeRoomState - - const isRoomFull = computed(() => { - return ( - roomState.value.maxPlayers > 0 && - roomState.value.playerCount === roomState.value.maxPlayers - ) - }) - - const canStartGame = computed(() => { - return ( - Boolean(roomState.value.id) && - roomState.value.status === 'waiting' && - isRoomFull.value && - Boolean(currentUserId.value) && - roomState.value.ownerId === currentUserId.value - ) - }) - - const myPlayer = computed(() => { - return roomState.value.players.find((player) => player.playerId === currentUserId.value) ?? null - }) - - const isMyTurn = computed(() => { - return myPlayer.value?.index === roomState.value.currentTurnIndex - }) - - const pendingClaimOptions = computed(() => { - return normalizePendingClaimOptions(roomState.value.game?.state?.pendingClaim) - }) - - const myClaimActions = computed(() => { - const claim = pendingClaimOptions.value.find((item) => item.playerId === currentUserId.value) - return new Set(claim?.actions ?? []) - }) - - const canDraw = computed(() => { - return roomState.value.status === 'playing' && isMyTurn.value && Boolean(roomState.value.game?.state?.needDraw) - }) - - const canDiscard = computed(() => { - return ( - roomState.value.status === 'playing' && - isMyTurn.value && - !roomState.value.game?.state?.needDraw && - roomState.value.myHand.length > 0 && - Boolean(selectedTile.value) - ) - }) - - const actionButtons = computed(() => { - return [ - { type: 'draw', label: '摸牌', disabled: !canDraw.value || actionPending.value }, - { type: 'discard', label: '出牌', disabled: !canDiscard.value || actionPending.value }, - { - type: 'peng', - label: '碰', - disabled: !myClaimActions.value.has('peng') || actionPending.value, - }, - { - type: 'gang', - label: '杠', - disabled: !myClaimActions.value.has('gang') || actionPending.value, - }, - { - type: 'hu', - label: '胡', - disabled: !myClaimActions.value.has('hu') || actionPending.value, - }, - { - type: 'pass', - label: '过', - disabled: !myClaimActions.value.has('pass') || actionPending.value, - }, - ] - }) - - const seatViews = computed(() => { - const seats: Record = { - top: null, - right: null, - bottom: null, - left: null, - } - - const players = [...roomState.value.players].sort((a, b) => a.index - b.index) - const hasSelf = players.some((player) => player.playerId === currentUserId.value) - if (currentUserId.value && roomState.value.id && !hasSelf) { - players.unshift({ - index: 0, - playerId: currentUserId.value, - ready: false, - hand: [], - melds: [], - outTiles: [], - hasHu: false, - }) - } - - const me = players.find((player) => player.playerId === currentUserId.value) ?? null - const anchorIndex = me?.index ?? players[0]?.index ?? 0 - const clockwiseSeatByDelta: SeatKey[] = ['bottom', 'left', 'top', 'right'] - - for (const player of players) { - const normalizedDelta = ((player.index - anchorIndex) % 4 + 4) % 4 - const seat = clockwiseSeatByDelta[normalizedDelta] ?? 'top' - seats[seat] = player - } - - const turnSeat = - roomState.value.currentTurnIndex === null - ? null - : clockwiseSeatByDelta[ - ((roomState.value.currentTurnIndex - anchorIndex) % 4 + 4) % 4 - ] ?? null - - const order: SeatKey[] = ['top', 'right', 'bottom', 'left'] - return order.map((seat) => { - const player = seats[seat] - const isSelf = Boolean(player) && player?.playerId === currentUserId.value - return { - key: seat, - player, - isSelf, - isTurn: turnSeat === seat, - label: player ? (isSelf ? '你' : player.displayName || `玩家${player.index + 1}`) : '空位', - subLabel: player ? `座位 ${player.index}` : '', - } - }) - }) - - function backHall(): void { - if (leaveRoomPending.value) { - return - } - - const sent = sendLeaveRoom() - if (!sent) { - pushWsMessage('[client] Leave room request was not sent') - } - - leaveHallAfterAck.value = false - disconnectWs() - destroyActiveRoomState() - void router.push('/hall') - } - - function pushWsMessage(text: string): void { - const line = `[${new Date().toLocaleTimeString()}] ${text}` - wsMessages.value.unshift(line) - if (wsMessages.value.length > 80) { - wsMessages.value.length = 80 - } - } - - function logWsSend(message: unknown): void { - console.log('[WS][client] 鍙戦€?', message) - } - - function logWsReceive(kind: string, payload?: unknown): void { - const now = new Date().toLocaleTimeString() - if (payload === undefined) { - console.log(`[WS][${now}] 鏀跺埌${kind}`) - return - } - console.log(`[WS][${now}] 鏀跺埌${kind}:`, payload) - } - - function disconnectWs(): void { - if (ws.value) { - ws.value.close() - ws.value = null - } - wsStatus.value = 'disconnected' - } - - function toSession(source: NonNullable): AuthSession { - return { - token: source.token, - tokenType: source.tokenType, - refreshToken: source.refreshToken, - expiresIn: source.expiresIn, - } - } - - function syncAuth(next: AuthSession): void { - if (!auth.value) { - return - } - - auth.value = { - ...auth.value, - token: next.token, - tokenType: next.tokenType ?? auth.value.tokenType, - refreshToken: next.refreshToken ?? auth.value.refreshToken, - expiresIn: next.expiresIn, - } - writeStoredAuth(auth.value) - } - - async function ensureCurrentUserId(): Promise { - if (currentUserId.value || !auth.value) { - return - } - - try { - const userInfo = await getUserInfo(toSession(auth.value), syncAuth) - const payload = userInfo as Record - const resolvedId = toStringOrEmpty(payload.userID ?? payload.user_id ?? payload.id) - if (!resolvedId) { - return - } - - auth.value = { - ...auth.value, - user: { - ...(auth.value.user ?? {}), - id: resolvedId, - }, - } - writeStoredAuth(auth.value) - } catch { - wsError.value = '鑾峰彇褰撳墠鐢ㄦ埛 ID 澶辫触锛岄儴鍒嗘搷浣滃彲鑳戒笉鍙敤' - } - } - - async function ensureWsAuth(): Promise { - const currentAuth = auth.value - if (!currentAuth?.token) { - return null - } - - if (!currentAuth.refreshToken) { - return currentAuth.token - } - - try { - const refreshed = await refreshAccessToken({ - token: currentAuth.token, - tokenType: currentAuth.tokenType, - refreshToken: currentAuth.refreshToken, - }) - - const nextAuth = { - ...currentAuth, - token: refreshed.token, - tokenType: refreshed.tokenType ?? currentAuth.tokenType, - refreshToken: refreshed.refreshToken ?? currentAuth.refreshToken, - expiresIn: refreshed.expiresIn, - user: refreshed.user ?? currentAuth.user, - } - - auth.value = nextAuth - writeStoredAuth(nextAuth) - return nextAuth.token - } catch { - return currentAuth.token - } - } - - function mergeRoomState(next: RoomState): void { - if (roomId.value && next.id !== roomId.value) { - return - } - mergeActiveRoomState(next) - } - - function consumeGameEvent(raw: string): void { - let parsed: unknown = null - try { - parsed = JSON.parse(raw) - } catch { - return - } - - const event = toRecord(parsed) as ActionEventLike | null - if (!event) { - return - } - - const payload = toRecord(event.payload) - const data = toRecord(event.data) - const eventType = toStringOrEmpty(event.type) - const eventStatus = toStringOrEmpty(event.status) - const eventRoomId = toStringOrEmpty(event.roomId ?? event.room_id ?? payload?.roomId ?? payload?.room_id) - const eventRequestId = toStringOrEmpty( - event.requestId ?? - event.request_id ?? - payload?.requestId ?? - payload?.request_id ?? - data?.requestId ?? - data?.request_id, - ) - const payloadPlayerIds = Array.isArray(payload?.player_ids) - ? payload.player_ids.map((item) => toStringOrEmpty(item)).filter(Boolean) - : Array.isArray(payload?.playerIds) - ? payload.playerIds.map((item) => toStringOrEmpty(item)).filter(Boolean) - : null - const leaveByRequestIdMatched = Boolean( - eventRequestId && eventRequestId === lastLeaveRoomRequestId.value, - ) - const leaveByPlayerUpdateMatched = - leaveRoomPending.value && - eventType === 'room_player_update' && - eventStatus === 'ok' && - eventRoomId === (roomState.value.id || roomId.value) && - Array.isArray(payloadPlayerIds) && - Boolean(currentUserId.value) && - !payloadPlayerIds.includes(currentUserId.value) - - if (leaveByRequestIdMatched || leaveByPlayerUpdateMatched) { - leaveRoomPending.value = false - lastLeaveRoomRequestId.value = '' - if (event.status === 'error') { - leaveHallAfterAck.value = false - wsError.value = '退出房间失败,请稍后重试' - pushWsMessage(`[client] 退出房间失败 requestId=${eventRequestId}`) - } else { - if (leaveByPlayerUpdateMatched) { - pushWsMessage('[client] 已确认退出房间 player_update') - } else { - pushWsMessage(`[client] 已确认退出房间 requestId=${eventRequestId}`) - } - if (leaveHallAfterAck.value) { - leaveHallAfterAck.value = false - void router.push('/hall') - } - } - } - - const candidates: unknown[] = [event.payload, event.data] - if (payload) { - candidates.push(payload.room, payload.state, payload.roomState, payload.data) - } - if (data) { - candidates.push(data.room, data.state, data.roomState, data.data) - } - candidates.push(event) - - for (const candidate of candidates) { - const normalized = normalizeRoom(candidate, roomState.value) - if (normalized) { - mergeRoomState(normalized) - break - } - } - - if ( - event.status === 'error' && - typeof event.requestId === 'string' && - event.requestId === lastStartRequestId.value - ) { - startGamePending.value = false - } - - if (event.status === 'error') { - actionPending.value = false - } - - if (eventType === 'my_hand') { - const handPayload = payload ?? data - const handRecord = toRecord(handPayload) - const hand = normalizeTileList(handRecord?.hand ?? handRecord?.tiles ?? handRecord?.myHand) - roomState.value = { - ...roomState.value, - myHand: hand, - playerCount: roomState.value.playerCount || roomState.value.players.length, - } - if (!selectedTile.value || !hand.includes(selectedTile.value)) { - selectedTile.value = hand[0] ?? null - } - actionPending.value = false - return - } - - if ( - ['room_state', 'room_player_update', 'room_joined', 'room_member_joined', 'room_member_left'].includes( - eventType, - ) - ) { - actionPending.value = false - } - } - - function createRequestId(prefix: string): string { - return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}` - } - - function sendStartGame(): void { - if ( - !ws.value || - ws.value.readyState !== WebSocket.OPEN || - !canStartGame.value || - startGamePending.value - ) { - return - } - - const sender = currentUserId.value - - if (!sender) { - return - } - - const requestId = createRequestId('start-game') - lastStartRequestId.value = requestId - startGamePending.value = true - - const message = { - type: 'start_game', - sender, - target: 'room', - roomId: roomState.value.id || roomId.value, - seq: Date.now(), - requestId, - trace_id: createRequestId('trace'), - payload: {}, - } - logWsSend(message) - ws.value.send(JSON.stringify(message)) - pushWsMessage(`[client] 请求开始游戏 requestId=${requestId}`) - } - - function selectTile(tile: string): void { - selectedTile.value = selectedTile.value === tile ? null : tile - } - - function sendGameAction(type: ActionButtonState['type']): void { - if (!ws.value || ws.value.readyState !== WebSocket.OPEN || !currentUserId.value) { - return - } - - const requestId = createRequestId(type) - const payload: Record = {} - - if (type === 'discard' && selectedTile.value) { - payload.tile = selectedTile.value - payload.discard_tile = selectedTile.value - payload.code = selectedTile.value - } - - actionPending.value = true - const message = { - type, - sender: currentUserId.value, - target: 'room', - roomId: roomState.value.id || roomId.value, - seq: Date.now(), - requestId, - trace_id: createRequestId('trace'), - payload, - } - - logWsSend(message) - ws.value.send(JSON.stringify(message)) - pushWsMessage(`[client] 请求${type} requestId=${requestId}`) - } - - function sendLeaveRoom(): boolean { - if (!ws.value || ws.value.readyState !== WebSocket.OPEN) { - wsError.value = 'WebSocket 未连接,无法退出房间' - return false - } - - const sender = currentUserId.value - const targetRoomId = roomState.value.id || roomId.value - if (!sender) { - wsError.value = '缺少当前用户 ID,无法退出房间' - return false - } - if (!targetRoomId) { - wsError.value = '缺少房间 ID,无法退出房间' - return false - } - - const requestId = createRequestId('leave-room') - leaveRoomPending.value = true - lastLeaveRoomRequestId.value = requestId - const message = { - type: 'leave_room', - sender, - target: 'room', - roomId: targetRoomId, - seq: Date.now(), - requestId, - trace_id: createRequestId('trace'), - payload: {}, - } - - logWsSend(message) - ws.value.send(JSON.stringify(message)) - pushWsMessage(`[client] 请求退出房间 requestId=${requestId}`) - return true - } - - async function connectWs(): Promise { - wsError.value = '' - const token = await ensureWsAuth() - if (!token) { - wsError.value = '缺少 token,无法建立 WebSocket 连接' - return - } - - disconnectWs() - wsStatus.value = 'connecting' - - const url = buildWsUrl(token) - const socket = new WebSocket(url) - ws.value = socket - - socket.onopen = () => { - wsStatus.value = 'connected' - pushWsMessage('WebSocket 已连接') - } - - socket.onmessage = (event) => { - if (typeof event.data === 'string') { - logWsReceive('文本消息', event.data) - try { - const parsed = JSON.parse(event.data) - logWsReceive('JSON 消息', parsed) - - pushWsMessage(`[server] ${JSON.stringify(parsed, null, 2)}`) - } catch { - pushWsMessage(`[server] ${event.data}`) - } - - consumeGameEvent(event.data) - return - } - - logWsReceive('binary message') - pushWsMessage('[binary] message received') - } - - socket.onerror = () => { - wsError.value = 'WebSocket 连接异常' - } - - socket.onclose = () => { - wsStatus.value = 'disconnected' - startGamePending.value = false - if (leaveRoomPending.value) { - leaveRoomPending.value = false - lastLeaveRoomRequestId.value = '' - leaveHallAfterAck.value = false - wsError.value = '连接已断开,未收到退出房间确认' - pushWsMessage('[client] 连接断开,退出房间请求未确认') - } - pushWsMessage('WebSocket 已断开') - } - } - - function buildWsUrl(token: string): string { - const baseUrl = /^wss?:\/\//.test(WS_BASE_URL) - ? new URL(WS_BASE_URL) - : new URL( - WS_BASE_URL.startsWith('/') ? WS_BASE_URL : `/${WS_BASE_URL}`, - `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}`, - ) - - baseUrl.searchParams.set('token', token) - return baseUrl.toString() - } - - watch( - roomId, - (nextRoomId) => { - const currentRoom = roomState.value - if (!nextRoomId) { - destroyActiveRoomState() - } else if (currentRoom.id !== nextRoomId) { - resetActiveRoomState({ - id: nextRoomId, - name: roomName.value, - }) - } else if (!currentRoom.name && roomName.value) { - roomState.value = { ...currentRoom, name: roomName.value } - } - startGamePending.value = false - lastStartRequestId.value = '' - leaveRoomPending.value = false - lastLeaveRoomRequestId.value = '' - leaveHallAfterAck.value = false - actionPending.value = false - selectedTile.value = null - }, - { immediate: true }, - ) - - watch(roomName, (next) => { - roomState.value = { ...roomState.value, name: next || roomState.value.name } - }) - - watch( - [canStartGame, wsStatus], - ([canStart, status]) => { - if (!canStart || status !== 'connected') { - return - } - sendStartGame() - }, - { immediate: true }, - ) - - watch( - () => roomState.value.status, - (status) => { - if (status === 'playing' || status === 'finished') { - startGamePending.value = false - } - }, - ) - - onMounted(async () => { - await ensureCurrentUserId() - void connectWs() - }) - - onBeforeUnmount(() => { - disconnectWs() - destroyActiveRoomState() - }) - - return { - auth, - roomState, - roomId, - roomName, - currentUserId, - loggedInUserName, - wsStatus, - wsError, - wsMessages, - startGamePending, - leaveRoomPending, - canStartGame, - seatViews, - selectedTile, - actionButtons, - connectWs, - sendStartGame, - selectTile, - sendGameAction, - backHall, - } -} - diff --git a/src/types/state/gamePhase.ts b/src/types/state/gamePhase.ts index d9aa646..1ae36cf 100644 --- a/src/types/state/gamePhase.ts +++ b/src/types/state/gamePhase.ts @@ -1,9 +1,19 @@ +// 游戏阶段常量定义(用于标识当前对局所处阶段) export const GAME_PHASE = { - WAITING: 'waiting', - DEALING: 'dealing', - PLAYING: 'playing', - SETTLEMENT: 'settlement', + WAITING: 'waiting', // 等待玩家准备 / 开始 + DEALING: 'dealing', // 发牌阶段 + PLAYING: 'playing', // 对局进行中 + SETTLEMENT: 'settlement', // 结算阶段 } as const +// 游戏阶段类型(取自 GAME_PHASE 的值) export type GamePhase = - typeof GAME_PHASE[keyof typeof GAME_PHASE] \ No newline at end of file + typeof GAME_PHASE[keyof typeof GAME_PHASE] + + +export const GAME_PHASE_LABEL: Record = { + waiting: '等待中', + dealing: '发牌', + playing: '对局中', + settlement: '结算', +} \ No newline at end of file diff --git a/src/types/state/gamestate.ts b/src/types/state/gamestate.ts index 2d72b9b..1e19a9a 100644 --- a/src/types/state/gamestate.ts +++ b/src/types/state/gamestate.ts @@ -1,7 +1,5 @@ import type {Player} from "./player.ts"; -import type {Tile} from "../../models"; import type {PendingClaim} from "./pendingClaim.ts"; -import type {HuWay} from "./huWay.ts"; import type {GamePhase} from "./gamePhase.ts"; export interface GameState { @@ -14,21 +12,12 @@ export interface GameState { // 当前操作玩家(座位) currentTurn: number - // 是否必须先摸牌 - needDraw: boolean - // 玩家列表 players: Player[] - // ⚠️ 建议:只保留剩余数量(不要完整牌墙) + // 剩余数量 remainingTiles: number - // 最近弃牌 - lastDiscardTile?: Tile - - // 最近弃牌玩家 - lastDiscardBy?: string - // 操作响应窗口(碰/杠/胡) pendingClaim?: PendingClaim @@ -37,16 +26,4 @@ export interface GameState { // 分数(playerId -> score) scores: Record - - // 最近摸牌玩家 - lastDrawPlayerId: string - - // 是否杠后补牌 - lastDrawFromGang: boolean - - // 是否最后一张牌 - lastDrawIsLastTile: boolean - - // 胡牌方式 - huWay: HuWay } \ No newline at end of file diff --git a/src/types/state/pendingClaim.ts b/src/types/state/pendingClaim.ts index 217a7e1..d1a660d 100644 --- a/src/types/state/pendingClaim.ts +++ b/src/types/state/pendingClaim.ts @@ -1,5 +1,6 @@ -import type {Tile} from "../../models"; + import type {ClaimOption} from "./claimOption.ts"; +import type {Tile} from "../tile.ts"; export interface PendingClaim { // 当前被响应的牌 diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue index b767e20..021353c 100644 --- a/src/views/ChengduGamePage.vue +++ b/src/views/ChengduGamePage.vue @@ -1,6 +1,5 @@