diff --git a/src/assets/images/actions/hu.png b/src/assets/images/actions/hu.png new file mode 100644 index 0000000..b92f301 Binary files /dev/null and b/src/assets/images/actions/hu.png differ diff --git a/src/assets/images/actions/pass.png b/src/assets/images/actions/pass.png new file mode 100644 index 0000000..0bba846 Binary files /dev/null and b/src/assets/images/actions/pass.png differ diff --git a/src/assets/images/actions/peng.png b/src/assets/images/actions/peng.png new file mode 100644 index 0000000..1473a3f Binary files /dev/null and b/src/assets/images/actions/peng.png differ diff --git a/src/assets/styles/room.css b/src/assets/styles/room.css index f82b70c..e5dcc70 100644 --- a/src/assets/styles/room.css +++ b/src/assets/styles/room.css @@ -99,8 +99,8 @@ .top-left-tools { position: absolute; - top: 28px; - left: 28px; + top: 40px; + left: 40px; display: flex; align-items: center; gap: 14px; @@ -120,6 +120,7 @@ } .menu-trigger-icon { + left: 40px; display: inline-block; transform-origin: center; } @@ -271,7 +272,7 @@ transform: rotate(0deg); } 100% { - transform: rotate(22deg); + transform: rotate(90deg); } } @@ -298,8 +299,8 @@ .top-right-clock { position: absolute; - top: 30px; - right: 36px; + top: 40px; + right: 40px; display: inline-flex; align-items: center; gap: 12px; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..bd75c8f --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,2 @@ +export * from './suits' +export * from './ws-events' diff --git a/src/constants/suits.ts b/src/constants/suits.ts new file mode 100644 index 0000000..754f2a1 --- /dev/null +++ b/src/constants/suits.ts @@ -0,0 +1,3 @@ +export const MAHJONG_SUITS = ['wan', 'tong', 'tiao'] as const + +export type MahjongSuit = (typeof MAHJONG_SUITS)[number] diff --git a/src/constants/ws-events.ts b/src/constants/ws-events.ts new file mode 100644 index 0000000..820d4a5 --- /dev/null +++ b/src/constants/ws-events.ts @@ -0,0 +1,6 @@ +export const WS_EVENT = { + roomSnapshot: 'room_snapshot', + roomState: 'room_state', + gameState: 'game_state', + myHand: 'my_hand', +} as const diff --git a/src/game/chengdu/index.ts b/src/game/chengdu/index.ts new file mode 100644 index 0000000..1080bd6 --- /dev/null +++ b/src/game/chengdu/index.ts @@ -0,0 +1,2 @@ +export { useChengduGameRoom } from './useChengduGameRoom' +export type { ActionButtonState, ChengduGameRoomModel, SeatKey, SeatView } from './types' diff --git a/src/game/chengdu/parser-utils.ts b/src/game/chengdu/parser-utils.ts new file mode 100644 index 0000000..e8d86a6 --- /dev/null +++ b/src/game/chengdu/parser-utils.ts @@ -0,0 +1,71 @@ +export function humanizeSuit(value: string): string { + const suitMap: Record = { + W: '万', + B: '筒', + T: '条', + } + + return suitMap[value] ?? value +} + +export function toRecord(value: unknown): Record | null { + return typeof value === 'object' && value !== null ? (value as Record) : null +} + +export function toStringOrEmpty(value: unknown): string { + if (typeof value === 'string') { + return value + } + if (typeof value === 'number' && Number.isFinite(value)) { + return String(value) + } + return '' +} + +export function toFiniteNumber(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + return value + } + if (typeof value === 'string' && value.trim()) { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : null + } + return null +} + +export function toBoolean(value: unknown): boolean { + if (typeof value === 'boolean') { + return value + } + if (typeof value === 'number') { + return value !== 0 + } + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase() + return normalized === '1' || normalized === 'true' || normalized === 'yes' + } + return false +} + +export function normalizeActionName(value: unknown): string { + const raw = toStringOrEmpty(value).trim().toLowerCase() + if (!raw) { + return '' + } + const actionMap: Record = { + candraw: 'draw', + draw: 'draw', + candiscard: 'discard', + discard: 'discard', + canpeng: 'peng', + peng: 'peng', + cangang: 'gang', + gang: 'gang', + canhu: 'hu', + hu: 'hu', + canpass: 'pass', + pass: 'pass', + } + + return actionMap[raw.replace(/[_\-\s]/g, '')] ?? raw +} diff --git a/src/game/chengdu/room-normalizers.ts b/src/game/chengdu/room-normalizers.ts new file mode 100644 index 0000000..9b9bf20 --- /dev/null +++ b/src/game/chengdu/room-normalizers.ts @@ -0,0 +1,421 @@ +import { + DEFAULT_MAX_PLAYERS, + type GameState, + type RoomPlayerState, + type RoomState, +} from '../../store/active-room-store' +import { type PendingClaimOption } from './types' +import { + humanizeSuit, + normalizeActionName, + toBoolean, + toFiniteNumber, + toRecord, + toStringOrEmpty, +} from './parser-utils' + +function normalizeScores(value: unknown): Record { + const record = toRecord(value) + if (!record) { + return {} + } + + const scores: Record = {} + for (const [key, score] of Object.entries(record)) { + const parsed = toFiniteNumber(score) + if (parsed !== null) { + scores[key] = parsed + } + } + return scores +} + +export function normalizeTileList(value: unknown): string[] { + if (!Array.isArray(value)) { + return [] + } + + return value + .flatMap((item) => { + if (Array.isArray(item)) { + return item.map((nested) => toStringOrEmpty(nested)).filter(Boolean) + } + const record = toRecord(item) + if (record) { + const explicit = + toStringOrEmpty( + record.tile ?? record.Tile ?? record.code ?? record.Code ?? record.name ?? record.Name, + ) || '' + if (explicit) { + return [explicit] + } + + const suit = toStringOrEmpty(record.suit ?? record.Suit) + const tileValue = toStringOrEmpty(record.value ?? record.Value) + if (suit && tileValue) { + return [`${humanizeSuit(suit)}${tileValue}`] + } + + return [] + } + return [toStringOrEmpty(item)].filter(Boolean) + }) + .filter(Boolean) +} + +function normalizeMeldGroups(value: unknown): string[][] { + if (!Array.isArray(value)) { + return [] + } + + return value + .map((item) => { + if (Array.isArray(item)) { + return normalizeTileList(item) + } + const record = toRecord(item) + if (record) { + const nested = record.tiles ?? record.Tiles ?? record.meld ?? record.Meld + if (nested) { + return normalizeTileList(nested) + } + } + return normalizeTileList([item]) + }) + .filter((group) => group.length > 0) +} + +function normalizePlayer(input: unknown, fallbackIndex: number): RoomPlayerState | null { + const player = toRecord(input) + if (!player) { + return null + } + + const playerId = toStringOrEmpty( + player.playerId ?? + player.player_id ?? + player.PlayerID ?? + player.UserID ?? + player.user_id ?? + player.id, + ) + if (!playerId) { + return null + } + + const seatIndex = toFiniteNumber( + player.index ?? + player.Index ?? + player.seat ?? + player.Seat ?? + player.position ?? + player.Position ?? + player.player_index, + ) + const hand = normalizeTileList(player.hand ?? player.Hand) + + return { + index: seatIndex ?? fallbackIndex, + playerId, + displayName: + toStringOrEmpty( + player.playerName ?? + player.player_name ?? + player.PlayerName ?? + player.username ?? + player.nickname, + ) || undefined, + ready: Boolean(player.ready), + handCount: + toFiniteNumber(player.handCount ?? player.hand_count ?? player.HandCount) ?? + (hand.length > 0 ? hand.length : undefined), + hand, + melds: normalizeMeldGroups(player.melds ?? player.Melds), + outTiles: normalizeTileList(player.outTiles ?? player.out_tiles ?? player.OutTiles), + hasHu: toBoolean(player.hasHu ?? player.has_hu ?? player.HasHu), + missingSuit: + toStringOrEmpty(player.missingSuit ?? player.missing_suit ?? player.MissingSuit) || null, + } +} + +function normalizePublicGameState(source: Record): GameState | null { + const publicPlayers = (Array.isArray(source.players) ? source.players : []) + .map((item, index) => normalizePlayer(item, index)) + .filter((item): item is RoomPlayerState => Boolean(item)) + .sort((a, b) => a.index - b.index) + + const currentTurnPlayerId = toStringOrEmpty( + source.current_turn_player ?? source.currentTurnPlayer ?? source.current_turn_player_id, + ) + const currentTurnIndex = + publicPlayers.find((player) => player.playerId === currentTurnPlayerId)?.index ?? null + + return { + rule: null, + state: { + phase: toStringOrEmpty(source.phase), + dealerIndex: 0, + currentTurn: currentTurnIndex ?? 0, + needDraw: toBoolean(source.need_draw ?? source.needDraw), + players: publicPlayers.map((player) => ({ + playerId: player.playerId, + index: player.index, + ready: player.ready, + })), + wall: Array.from({ + length: toFiniteNumber(source.wall_count ?? source.wallCount) ?? 0, + }).map((_, index) => `wall-${index}`), + lastDiscardTile: + toStringOrEmpty(source.last_discard_tile ?? source.lastDiscardTile) || null, + lastDiscardBy: toStringOrEmpty(source.last_discard_by ?? source.lastDiscardBy), + pendingClaim: toRecord(source.pending_claim ?? source.pendingClaim), + winners: Array.isArray(source.winners) + ? source.winners.map((item) => toStringOrEmpty(item)).filter(Boolean) + : [], + scores: normalizeScores(source.scores), + lastDrawPlayerId: '', + lastDrawFromGang: false, + lastDrawIsLastTile: false, + huWay: '', + }, + } +} + +export function normalizePendingClaimOptions(value: unknown): PendingClaimOption[] { + const pendingClaim = toRecord(value) + if (!pendingClaim) { + return [] + } + + const rawOptions = + (Array.isArray(pendingClaim.options) ? pendingClaim.options : null) ?? + (Array.isArray(pendingClaim.Options) ? pendingClaim.Options : null) ?? + [] + + const optionsFromArray = rawOptions + .map((option) => { + const record = toRecord(option) + if (!record) { + return null + } + const playerId = toStringOrEmpty( + record.playerId ?? record.player_id ?? record.PlayerID ?? record.user_id ?? record.UserID, + ) + if (!playerId) { + return null + } + const actions = new Set() + for (const [key, enabled] of Object.entries(record)) { + if (typeof enabled === 'boolean' && enabled) { + const normalized = normalizeActionName(key) + if (normalized && normalized !== 'playerid' && normalized !== 'userid') { + actions.add(normalized) + } + } + } + if (Array.isArray(record.actions)) { + for (const action of record.actions) { + const normalized = normalizeActionName(action) + if (normalized) { + actions.add(normalized) + } + } + } + + return { playerId, actions: [...actions] } + }) + .filter((item): item is PendingClaimOption => Boolean(item)) + + if (optionsFromArray.length > 0) { + return optionsFromArray + } + + const claimPlayerId = toStringOrEmpty( + pendingClaim.playerId ?? + pendingClaim.player_id ?? + pendingClaim.PlayerID ?? + pendingClaim.user_id ?? + pendingClaim.UserID, + ) + if (!claimPlayerId) { + return [] + } + + const actions = Object.entries(pendingClaim) + .filter(([, enabled]) => typeof enabled === 'boolean' && enabled) + .map(([key]) => normalizeActionName(key)) + .filter(Boolean) + + return actions.length > 0 ? [{ playerId: claimPlayerId, actions }] : [] +} + +function extractCurrentTurnIndex(value: Record): number | null { + const game = toRecord(value.game) + const gameState = toRecord(game?.state) + const keys = [ + gameState?.currentTurn, + gameState?.current_turn, + gameState?.currentTurnIndex, + gameState?.current_turn_index, + value.currentTurnIndex, + value.current_turn_index, + value.currentPlayerIndex, + value.current_player_index, + value.turnIndex, + value.turn_index, + value.activePlayerIndex, + value.active_player_index, + ] + for (const key of keys) { + const parsed = toFiniteNumber(key) + if (parsed !== null) { + return parsed + } + } + return null +} + +function normalizeGame(input: unknown): GameState | null { + const game = toRecord(input) + if (!game) { + return null + } + + const rule = toRecord(game.rule) + const rawState = toRecord(game.state) + const playersRaw = + (Array.isArray(rawState?.players) ? rawState?.players : null) ?? + (Array.isArray(rawState?.playerStates) ? rawState?.playerStates : null) ?? + [] + + const normalizedPlayers = playersRaw + .map((item, index) => normalizePlayer(item, index)) + .filter((item): item is RoomPlayerState => Boolean(item)) + + return { + rule: rule + ? { + name: toStringOrEmpty(rule.name), + isBloodFlow: toBoolean(rule.isBloodFlow ?? rule.is_blood_flow), + hasHongZhong: toBoolean(rule.hasHongZhong ?? rule.has_hong_zhong), + } + : null, + state: rawState + ? { + phase: toStringOrEmpty(rawState.phase), + dealerIndex: toFiniteNumber(rawState.dealerIndex ?? rawState.dealer_index) ?? 0, + currentTurn: toFiniteNumber(rawState.currentTurn ?? rawState.current_turn) ?? 0, + needDraw: toBoolean(rawState.needDraw ?? rawState.need_draw), + players: normalizedPlayers, + wall: Array.isArray(rawState.wall) + ? rawState.wall.map((item) => toStringOrEmpty(item)).filter(Boolean) + : [], + lastDiscardTile: toStringOrEmpty(rawState.lastDiscardTile ?? rawState.last_discard_tile) || null, + lastDiscardBy: toStringOrEmpty(rawState.lastDiscardBy ?? rawState.last_discard_by), + pendingClaim: toRecord(rawState.pendingClaim ?? rawState.pending_claim), + winners: Array.isArray(rawState.winners) + ? rawState.winners.map((item) => toStringOrEmpty(item)).filter(Boolean) + : [], + scores: normalizeScores(rawState.scores), + lastDrawPlayerId: toStringOrEmpty(rawState.lastDrawPlayerID ?? rawState.last_draw_player_id), + lastDrawFromGang: toBoolean(rawState.lastDrawFromGang ?? rawState.last_draw_from_gang), + lastDrawIsLastTile: toBoolean(rawState.lastDrawIsLastTile ?? rawState.last_draw_is_last_tile), + huWay: toStringOrEmpty(rawState.huWay ?? rawState.hu_way), + } + : null, + } +} + +export function normalizeRoom(input: unknown, currentRoomState: RoomState): RoomState | null { + const source = toRecord(input) + if (!source) { + return null + } + + let room = source + let id = toStringOrEmpty(room.roomId ?? room.room_id ?? room.id) + if (!id) { + const nestedRoom = toRecord(room.data) + if (nestedRoom) { + room = nestedRoom + id = toStringOrEmpty(room.roomId ?? room.room_id ?? room.id) + } + } + if (!id) { + return null + } + + const maxPlayers = + toFiniteNumber(room.maxPlayers ?? room.max_players) ?? currentRoomState.maxPlayers ?? DEFAULT_MAX_PLAYERS + const playersRaw = + (Array.isArray(room.players) ? room.players : null) ?? + (Array.isArray(room.playerList) ? room.playerList : null) ?? + (Array.isArray(room.player_list) ? room.player_list : null) ?? + [] + const playerIdsRaw = + (Array.isArray(room.player_ids) ? room.player_ids : null) ?? + (Array.isArray(room.playerIds) ? room.playerIds : null) ?? + [] + + const players = playersRaw + .map((item, index) => normalizePlayer(item, index)) + .filter((item): item is RoomPlayerState => Boolean(item)) + .sort((a, b) => a.index - b.index) + const playersFromIds = playerIdsRaw + .map((item, index) => ({ + index, + playerId: toStringOrEmpty(item), + ready: false, + hand: [], + melds: [], + outTiles: [], + hasHu: false, + })) + .filter((item) => Boolean(item.playerId)) + const resolvedPlayers = players.length > 0 ? players : playersFromIds + const parsedPlayerCount = toFiniteNumber(room.player_count ?? room.playerCount) + const game = normalizeGame(room.game) ?? normalizePublicGameState(room) + const playersFromGame = game?.state?.players + .map((player, index) => + normalizePlayer( + { + player_id: player.playerId, + index: player.index ?? index, + }, + index, + ), + ) + .filter((item): item is RoomPlayerState => Boolean(item)) + const finalPlayers = + resolvedPlayers.length > 0 + ? resolvedPlayers + : playersFromGame && playersFromGame.length > 0 + ? playersFromGame + : [] + const derivedTurnIndex = + extractCurrentTurnIndex(room) ?? + (game?.state + ? finalPlayers.find( + (player) => player.playerId === toStringOrEmpty(room.current_turn_player ?? room.currentTurnPlayer), + )?.index ?? null + : null) + + return { + id, + name: toStringOrEmpty(room.name) || currentRoomState.name, + gameType: toStringOrEmpty(room.gameType ?? room.game_type) || currentRoomState.gameType || 'chengdu', + ownerId: toStringOrEmpty(room.ownerId ?? room.owner_id ?? room.OwnerID ?? room.ownerID), + maxPlayers, + playerCount: + parsedPlayerCount ?? + toFiniteNumber(room.player_count ?? room.playerCount ?? room.playerCount) ?? + finalPlayers.length, + status: toStringOrEmpty(room.status) || currentRoomState.status || 'waiting', + createdAt: toStringOrEmpty(room.createdAt ?? room.created_at) || currentRoomState.createdAt, + updatedAt: toStringOrEmpty(room.updatedAt ?? room.updated_at) || currentRoomState.updatedAt, + game: game ?? currentRoomState.game, + players: finalPlayers, + currentTurnIndex: derivedTurnIndex, + myHand: [], + } +} diff --git a/src/game/chengdu/types.ts b/src/game/chengdu/types.ts new file mode 100644 index 0000000..a4e9acb --- /dev/null +++ b/src/game/chengdu/types.ts @@ -0,0 +1,60 @@ +import type { ComputedRef, Ref } from 'vue' +import type { StoredAuth } from '../../types/session' +import type { RoomPlayerState } from '../../store/active-room-store' +import { activeRoomState } from '../../store/active-room-store' + +export type SeatKey = 'top' | 'right' | 'bottom' | 'left' + +export interface ActionEventLike { + type?: unknown + status?: unknown + requestId?: unknown + request_id?: unknown + roomId?: unknown + room_id?: unknown + payload?: unknown + data?: unknown +} + +export interface PendingClaimOption { + playerId: string + actions: string[] +} + +export interface ActionButtonState { + type: 'draw' | 'discard' | 'peng' | 'gang' | 'hu' | 'pass' + label: string + disabled: boolean +} + +export interface SeatView { + key: SeatKey + player: RoomPlayerState | null + isSelf: boolean + isTurn: boolean + label: string + subLabel: string +} + +export interface ChengduGameRoomModel { + auth: Ref + roomState: typeof activeRoomState + roomId: ComputedRef + roomName: ComputedRef + currentUserId: ComputedRef + loggedInUserName: ComputedRef + wsStatus: Ref<'disconnected' | 'connecting' | 'connected'> + wsError: Ref + wsMessages: Ref + startGamePending: Ref + leaveRoomPending: Ref + canStartGame: ComputedRef + seatViews: ComputedRef + selectedTile: Ref + actionButtons: ComputedRef + connectWs: () => Promise + sendStartGame: () => void + selectTile: (tile: string) => void + sendGameAction: (type: ActionButtonState['type']) => void + backHall: () => void +} diff --git a/src/features/chengdu-game/useChengduGameRoom.ts b/src/game/chengdu/useChengduGameRoom.ts similarity index 53% rename from src/features/chengdu-game/useChengduGameRoom.ts rename to src/game/chengdu/useChengduGameRoom.ts index fd66f34..e5d90e3 100644 --- a/src/features/chengdu-game/useChengduGameRoom.ts +++ b/src/game/chengdu/useChengduGameRoom.ts @@ -1,87 +1,34 @@ -import { computed, onBeforeUnmount, onMounted, ref, watch, type ComputedRef, type Ref } from 'vue' +import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import type { RouteLocationNormalizedLoaded, Router } from 'vue-router' import type { AuthSession } from '../../api/authed-request' import { refreshAccessToken } from '../../api/auth' import { getUserInfo } from '../../api/user' import { - DEFAULT_MAX_PLAYERS, activeRoomState, destroyActiveRoomState, mergeActiveRoomState, resetActiveRoomState, - type GameState, type RoomPlayerState, type RoomState, -} from '../../state/active-room' +} from '../../store/active-room-store' import { readStoredAuth, writeStoredAuth } from '../../utils/auth-storage' -import type { StoredAuth } from '../../types/session' - -export type SeatKey = 'top' | 'right' | 'bottom' | 'left' - -interface ActionEventLike { - type?: unknown - status?: unknown - requestId?: unknown - request_id?: unknown - roomId?: unknown - room_id?: unknown - payload?: unknown - data?: unknown -} - -interface PendingClaimOption { - playerId: string - actions: string[] -} - -export interface ActionButtonState { - type: 'draw' | 'discard' | 'peng' | 'gang' | 'hu' | 'pass' - label: string - disabled: boolean -} - -export interface SeatView { - key: SeatKey - player: RoomPlayerState | null - isSelf: boolean - isTurn: boolean - label: string - subLabel: string -} - -function humanizeSuit(value: string): string { - const suitMap: Record = { - W: '万', - B: '筒', - T: '条', - } - - return suitMap[value] ?? value -} - -export interface ChengduGameRoomModel { - auth: Ref - roomState: typeof activeRoomState - roomId: ComputedRef - roomName: ComputedRef - currentUserId: ComputedRef - loggedInUserName: ComputedRef - wsStatus: Ref<'disconnected' | 'connecting' | 'connected'> - wsError: Ref - wsMessages: Ref - startGamePending: Ref - leaveRoomPending: Ref - canStartGame: ComputedRef - seatViews: ComputedRef - selectedTile: Ref - actionButtons: ComputedRef - connectWs: () => Promise - sendStartGame: () => void - selectTile: (tile: string) => void - sendGameAction: (type: ActionButtonState['type']) => void - backHall: () => void -} +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( @@ -221,6 +168,10 @@ export function useChengduGameRoom( index: 0, playerId: currentUserId.value, ready: false, + hand: [], + melds: [], + outTiles: [], + hasHu: false, }) } @@ -281,16 +232,16 @@ export function useChengduGameRoom( } function logWsSend(message: unknown): void { - console.log('[WS][client] 发送:', message) + 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}`) + console.log(`[WS][${now}] 鏀跺埌${kind}`) return } - console.log(`[WS][${now}] 收到${kind}:`, payload) + console.log(`[WS][${now}] 鏀跺埌${kind}:`, payload) } function disconnectWs(): void { @@ -301,43 +252,6 @@ export function useChengduGameRoom( wsStatus.value = 'disconnected' } - function toRecord(value: unknown): Record | null { - return typeof value === 'object' && value !== null ? (value as Record) : null - } - - function toStringOrEmpty(value: unknown): string { - if (typeof value === 'string') { - return value - } - if (typeof value === 'number' && Number.isFinite(value)) { - return String(value) - } - return '' - } - - function normalizeActionName(value: unknown): string { - const raw = toStringOrEmpty(value).trim().toLowerCase() - if (!raw) { - return '' - } - const actionMap: Record = { - candraw: 'draw', - draw: 'draw', - candiscard: 'discard', - discard: 'discard', - canpeng: 'peng', - peng: 'peng', - cangang: 'gang', - gang: 'gang', - canhu: 'hu', - hu: 'hu', - canpass: 'pass', - pass: 'pass', - } - - return actionMap[raw.replace(/[_\-\s]/g, '')] ?? raw - } - function toSession(source: NonNullable): AuthSession { return { token: source.token, @@ -384,7 +298,7 @@ export function useChengduGameRoom( } writeStoredAuth(auth.value) } catch { - wsError.value = '获取当前用户 ID 失败,部分操作可能不可用' + wsError.value = '鑾峰彇褰撳墠鐢ㄦ埛 ID 澶辫触锛岄儴鍒嗘搷浣滃彲鑳戒笉鍙敤' } } @@ -422,407 +336,6 @@ export function useChengduGameRoom( } } - function toFiniteNumber(value: unknown): number | null { - if (typeof value === 'number' && Number.isFinite(value)) { - return value - } - if (typeof value === 'string' && value.trim()) { - const parsed = Number(value) - return Number.isFinite(parsed) ? parsed : null - } - return null - } - - function toBoolean(value: unknown): boolean { - if (typeof value === 'boolean') { - return value - } - if (typeof value === 'number') { - return value !== 0 - } - if (typeof value === 'string') { - const normalized = value.trim().toLowerCase() - return normalized === '1' || normalized === 'true' || normalized === 'yes' - } - return false - } - - function normalizeScores(value: unknown): Record { - const record = toRecord(value) - if (!record) { - return {} - } - - const scores: Record = {} - for (const [key, score] of Object.entries(record)) { - const parsed = toFiniteNumber(score) - if (parsed !== null) { - scores[key] = parsed - } - } - return scores - } - - function normalizePlayer(input: unknown, fallbackIndex: number): RoomPlayerState | null { - const player = toRecord(input) - if (!player) { - return null - } - - const playerId = toStringOrEmpty( - player.playerId ?? - player.player_id ?? - player.PlayerID ?? - player.UserID ?? - player.user_id ?? - player.id, - ) - if (!playerId) { - return null - } - - const seatIndex = toFiniteNumber( - player.index ?? - player.Index ?? - player.seat ?? - player.Seat ?? - player.position ?? - player.Position ?? - player.player_index, - ) - return { - index: seatIndex ?? fallbackIndex, - playerId, - displayName: - toStringOrEmpty( - player.playerName ?? - player.player_name ?? - player.PlayerName ?? - player.username ?? - player.nickname, - ) || undefined, - ready: Boolean(player.ready), - handCount: - toFiniteNumber(player.handCount ?? player.hand_count ?? player.HandCount) ?? undefined, - melds: normalizeTileList(player.melds ?? player.Melds), - outTiles: normalizeTileList(player.outTiles ?? player.out_tiles ?? player.OutTiles), - hasHu: toBoolean(player.hasHu ?? player.has_hu ?? player.HasHu), - missingSuit: - toStringOrEmpty(player.missingSuit ?? player.missing_suit ?? player.MissingSuit) || null, - } - } - - function normalizeTileList(value: unknown): string[] { - if (!Array.isArray(value)) { - return [] - } - - return value - .flatMap((item) => { - if (Array.isArray(item)) { - return item.map((nested) => toStringOrEmpty(nested)).filter(Boolean) - } - const record = toRecord(item) - if (record) { - const explicit = - toStringOrEmpty( - record.tile ?? record.Tile ?? record.code ?? record.Code ?? record.name ?? record.Name, - ) || '' - if (explicit) { - return [explicit] - } - - const suit = toStringOrEmpty(record.suit ?? record.Suit) - const tileValue = toStringOrEmpty(record.value ?? record.Value) - if (suit && tileValue) { - return [`${humanizeSuit(suit)}${tileValue}`] - } - - return [] - } - return [toStringOrEmpty(item)].filter(Boolean) - }) - .filter(Boolean) - } - - function normalizePublicGameState(source: Record): GameState | null { - const publicPlayers = (Array.isArray(source.players) ? source.players : []) - .map((item, index) => normalizePlayer(item, index)) - .filter((item): item is RoomPlayerState => Boolean(item)) - .sort((a, b) => a.index - b.index) - - const currentTurnPlayerId = toStringOrEmpty( - source.current_turn_player ?? source.currentTurnPlayer ?? source.current_turn_player_id, - ) - const currentTurnIndex = - publicPlayers.find((player) => player.playerId === currentTurnPlayerId)?.index ?? null - - return { - rule: null, - state: { - phase: toStringOrEmpty(source.phase), - dealerIndex: 0, - currentTurn: currentTurnIndex ?? 0, - needDraw: toBoolean(source.need_draw ?? source.needDraw), - players: publicPlayers.map((player) => ({ - playerId: player.playerId, - index: player.index, - ready: player.ready, - })), - wall: Array.from({ - length: toFiniteNumber(source.wall_count ?? source.wallCount) ?? 0, - }).map((_, index) => `wall-${index}`), - lastDiscardTile: - toStringOrEmpty(source.last_discard_tile ?? source.lastDiscardTile) || null, - lastDiscardBy: toStringOrEmpty(source.last_discard_by ?? source.lastDiscardBy), - pendingClaim: toRecord(source.pending_claim ?? source.pendingClaim), - winners: Array.isArray(source.winners) - ? source.winners.map((item) => toStringOrEmpty(item)).filter(Boolean) - : [], - scores: normalizeScores(source.scores), - lastDrawPlayerId: '', - lastDrawFromGang: false, - lastDrawIsLastTile: false, - huWay: '', - }, - } - } - - function normalizePendingClaimOptions(value: unknown): PendingClaimOption[] { - const pendingClaim = toRecord(value) - if (!pendingClaim) { - return [] - } - - const rawOptions = - (Array.isArray(pendingClaim.options) ? pendingClaim.options : null) ?? - (Array.isArray(pendingClaim.Options) ? pendingClaim.Options : null) ?? - [] - - const optionsFromArray = rawOptions - .map((option) => { - const record = toRecord(option) - if (!record) { - return null - } - const playerId = toStringOrEmpty( - record.playerId ?? record.player_id ?? record.PlayerID ?? record.user_id ?? record.UserID, - ) - if (!playerId) { - return null - } - const actions = new Set() - for (const value of Object.values(record)) { - if (typeof value === 'boolean' && value) { - continue - } - } - for (const [key, enabled] of Object.entries(record)) { - if (typeof enabled === 'boolean' && enabled) { - const normalized = normalizeActionName(key) - if (normalized && normalized !== 'playerid' && normalized !== 'userid') { - actions.add(normalized) - } - } - } - if (Array.isArray(record.actions)) { - for (const action of record.actions) { - const normalized = normalizeActionName(action) - if (normalized) { - actions.add(normalized) - } - } - } - - return { playerId, actions: [...actions] } - }) - .filter((item): item is PendingClaimOption => Boolean(item)) - - if (optionsFromArray.length > 0) { - return optionsFromArray - } - - const claimPlayerId = toStringOrEmpty( - pendingClaim.playerId ?? - pendingClaim.player_id ?? - pendingClaim.PlayerID ?? - pendingClaim.user_id ?? - pendingClaim.UserID, - ) - if (!claimPlayerId) { - return [] - } - - const actions = Object.entries(pendingClaim) - .filter(([, enabled]) => typeof enabled === 'boolean' && enabled) - .map(([key]) => normalizeActionName(key)) - .filter(Boolean) - - return actions.length > 0 ? [{ playerId: claimPlayerId, actions }] : [] - } - - function extractCurrentTurnIndex(value: Record): number | null { - const game = toRecord(value.game) - const gameState = toRecord(game?.state) - const keys = [ - gameState?.currentTurn, - gameState?.current_turn, - gameState?.currentTurnIndex, - gameState?.current_turn_index, - value.currentTurnIndex, - value.current_turn_index, - value.currentPlayerIndex, - value.current_player_index, - value.turnIndex, - value.turn_index, - value.activePlayerIndex, - value.active_player_index, - ] - for (const key of keys) { - const parsed = toFiniteNumber(key) - if (parsed !== null) { - return parsed - } - } - return null - } - - function normalizeGame(input: unknown): GameState | null { - const game = toRecord(input) - if (!game) { - return null - } - - const rule = toRecord(game.rule) - const rawState = toRecord(game.state) - const playersRaw = - (Array.isArray(rawState?.players) ? rawState?.players : null) ?? - (Array.isArray(rawState?.playerStates) ? rawState?.playerStates : null) ?? - [] - - const normalizedPlayers = playersRaw - .map((item, index) => normalizePlayer(item, index)) - .filter((item): item is RoomPlayerState => Boolean(item)) - - return { - rule: rule - ? { - name: toStringOrEmpty(rule.name), - isBloodFlow: toBoolean(rule.isBloodFlow ?? rule.is_blood_flow), - hasHongZhong: toBoolean(rule.hasHongZhong ?? rule.has_hong_zhong), - } - : null, - state: rawState - ? { - phase: toStringOrEmpty(rawState.phase), - dealerIndex: toFiniteNumber(rawState.dealerIndex ?? rawState.dealer_index) ?? 0, - currentTurn: toFiniteNumber(rawState.currentTurn ?? rawState.current_turn) ?? 0, - needDraw: toBoolean(rawState.needDraw ?? rawState.need_draw), - players: normalizedPlayers, - wall: Array.isArray(rawState.wall) - ? rawState.wall.map((item) => toStringOrEmpty(item)).filter(Boolean) - : [], - lastDiscardTile: toStringOrEmpty(rawState.lastDiscardTile ?? rawState.last_discard_tile) || null, - lastDiscardBy: toStringOrEmpty(rawState.lastDiscardBy ?? rawState.last_discard_by), - pendingClaim: toRecord(rawState.pendingClaim ?? rawState.pending_claim), - winners: Array.isArray(rawState.winners) - ? rawState.winners.map((item) => toStringOrEmpty(item)).filter(Boolean) - : [], - scores: normalizeScores(rawState.scores), - lastDrawPlayerId: toStringOrEmpty(rawState.lastDrawPlayerID ?? rawState.last_draw_player_id), - lastDrawFromGang: toBoolean(rawState.lastDrawFromGang ?? rawState.last_draw_from_gang), - lastDrawIsLastTile: toBoolean(rawState.lastDrawIsLastTile ?? rawState.last_draw_is_last_tile), - huWay: toStringOrEmpty(rawState.huWay ?? rawState.hu_way), - } - : null, - } - } - - function normalizeRoom(input: unknown): RoomState | null { - const source = toRecord(input) - if (!source) { - return null - } - - let room = source - let id = toStringOrEmpty(room.roomId ?? room.room_id ?? room.id) - if (!id) { - const nestedRoom = toRecord(room.data) - if (nestedRoom) { - room = nestedRoom - id = toStringOrEmpty(room.roomId ?? room.room_id ?? room.id) - } - } - if (!id) { - return null - } - - const maxPlayers = - toFiniteNumber(room.maxPlayers ?? room.max_players) ?? roomState.value.maxPlayers ?? DEFAULT_MAX_PLAYERS - const playersRaw = - (Array.isArray(room.players) ? room.players : null) ?? - (Array.isArray(room.playerList) ? room.playerList : null) ?? - (Array.isArray(room.player_list) ? room.player_list : null) ?? - [] - const playerIdsRaw = - (Array.isArray(room.player_ids) ? room.player_ids : null) ?? - (Array.isArray(room.playerIds) ? room.playerIds : null) ?? - [] - - const players = playersRaw - .map((item, index) => normalizePlayer(item, index)) - .filter((item): item is RoomPlayerState => Boolean(item)) - .sort((a, b) => a.index - b.index) - const playersFromIds = playerIdsRaw - .map((item, index) => ({ - index, - playerId: toStringOrEmpty(item), - ready: false, - })) - .filter((item) => Boolean(item.playerId)) - const resolvedPlayers = players.length > 0 ? players : playersFromIds - const parsedPlayerCount = toFiniteNumber(room.player_count ?? room.playerCount) - const game = normalizeGame(room.game) ?? normalizePublicGameState(room) - const playersFromGame = game?.state?.players - .map((player, index) => - normalizePlayer( - { - player_id: player.playerId, - index: player.index ?? index, - }, - index, - ), - ) - .filter((item): item is RoomPlayerState => Boolean(item)) - const finalPlayers = - resolvedPlayers.length > 0 ? resolvedPlayers : playersFromGame && playersFromGame.length > 0 ? playersFromGame : [] - const derivedTurnIndex = - extractCurrentTurnIndex(room) ?? - (game?.state - ? finalPlayers.find((player) => player.playerId === toStringOrEmpty(room.current_turn_player ?? room.currentTurnPlayer)) - ?.index ?? null - : null) - - return { - id, - name: toStringOrEmpty(room.name) || roomState.value.name, - gameType: toStringOrEmpty(room.gameType ?? room.game_type) || roomState.value.gameType || 'chengdu', - ownerId: toStringOrEmpty(room.ownerId ?? room.owner_id ?? room.OwnerID ?? room.ownerID), - maxPlayers, - playerCount: - parsedPlayerCount ?? - toFiniteNumber(room.player_count ?? room.playerCount ?? room.playerCount) ?? - finalPlayers.length, - status: toStringOrEmpty(room.status) || roomState.value.status || 'waiting', - createdAt: toStringOrEmpty(room.createdAt ?? room.created_at) || roomState.value.createdAt, - updatedAt: toStringOrEmpty(room.updatedAt ?? room.updated_at) || roomState.value.updatedAt, - game: game ?? roomState.value.game, - players: finalPlayers, - currentTurnIndex: derivedTurnIndex, - myHand: [], - } - } - function mergeRoomState(next: RoomState): void { if (roomId.value && next.id !== roomId.value) { return @@ -903,7 +416,7 @@ export function useChengduGameRoom( candidates.push(event) for (const candidate of candidates) { - const normalized = normalizeRoom(candidate) + const normalized = normalizeRoom(candidate, roomState.value) if (normalized) { mergeRoomState(normalized) break @@ -1210,3 +723,4 @@ export function useChengduGameRoom( backHall, } } + diff --git a/src/game/index.ts b/src/game/index.ts new file mode 100644 index 0000000..532305b --- /dev/null +++ b/src/game/index.ts @@ -0,0 +1 @@ +export * from './chengdu' diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..ebd9765 --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,2 @@ +export * from './tile' +export * from './room-state' diff --git a/src/models/room-state/constants.ts b/src/models/room-state/constants.ts new file mode 100644 index 0000000..a6abd72 --- /dev/null +++ b/src/models/room-state/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_MAX_PLAYERS = 4 diff --git a/src/models/room-state/engine-state.ts b/src/models/room-state/engine-state.ts new file mode 100644 index 0000000..783a955 --- /dev/null +++ b/src/models/room-state/engine-state.ts @@ -0,0 +1,19 @@ +import type { GamePlayerState } from './game-player-state' + +export interface EngineState { + phase: string + dealerIndex: number + currentTurn: number + needDraw: boolean + players: GamePlayerState[] + wall: string[] + lastDiscardTile: string | null + lastDiscardBy: string + pendingClaim: Record | null + winners: string[] + scores: Record + lastDrawPlayerId: string + lastDrawFromGang: boolean + lastDrawIsLastTile: boolean + huWay: string +} diff --git a/src/models/room-state/game-player-state.ts b/src/models/room-state/game-player-state.ts new file mode 100644 index 0000000..35f4ada --- /dev/null +++ b/src/models/room-state/game-player-state.ts @@ -0,0 +1,5 @@ +export interface GamePlayerState { + playerId: string + index: number + ready: boolean +} diff --git a/src/models/room-state/game-state.ts b/src/models/room-state/game-state.ts new file mode 100644 index 0000000..c25c764 --- /dev/null +++ b/src/models/room-state/game-state.ts @@ -0,0 +1,7 @@ +import type { EngineState } from './engine-state' +import type { RuleState } from './rule-state' + +export interface GameState { + rule: RuleState | null + state: EngineState | null +} diff --git a/src/models/room-state/index.ts b/src/models/room-state/index.ts new file mode 100644 index 0000000..20475c5 --- /dev/null +++ b/src/models/room-state/index.ts @@ -0,0 +1,9 @@ +export { DEFAULT_MAX_PLAYERS } from './constants' +export type { RoomStatus } from './room-status' +export type { PlayerState } from './player-state' +export type { RoomPlayerState } from './room-player-state' +export type { RuleState } from './rule-state' +export type { GamePlayerState } from './game-player-state' +export type { EngineState } from './engine-state' +export type { GameState } from './game-state' +export type { RoomState } from './room-state' diff --git a/src/models/room-state/player-state.ts b/src/models/room-state/player-state.ts new file mode 100644 index 0000000..e1fc6ca --- /dev/null +++ b/src/models/room-state/player-state.ts @@ -0,0 +1,7 @@ +export interface PlayerState { + playerId: string + hand: string[] + melds: string[][] + outTiles: string[] + hasHu: boolean +} diff --git a/src/models/room-state/room-player-state.ts b/src/models/room-state/room-player-state.ts new file mode 100644 index 0000000..6b6e544 --- /dev/null +++ b/src/models/room-state/room-player-state.ts @@ -0,0 +1,9 @@ +import type { PlayerState } from './player-state' + +export interface RoomPlayerState extends PlayerState { + index: number + displayName?: string + ready: boolean + handCount?: number + missingSuit?: string | null +} diff --git a/src/models/room-state/room-state.ts b/src/models/room-state/room-state.ts new file mode 100644 index 0000000..ded2b57 --- /dev/null +++ b/src/models/room-state/room-state.ts @@ -0,0 +1,19 @@ +import type { GameState } from './game-state' +import type { RoomPlayerState } from './room-player-state' +import type { RoomStatus } from './room-status' + +export interface RoomState { + id: string + name: string + gameType: string + ownerId: string + maxPlayers: number + playerCount: number + status: RoomStatus | string + createdAt: string + updatedAt: string + game: GameState | null + players: RoomPlayerState[] + currentTurnIndex: number | null + myHand: string[] +} diff --git a/src/models/room-state/room-status.ts b/src/models/room-state/room-status.ts new file mode 100644 index 0000000..bec836d --- /dev/null +++ b/src/models/room-state/room-status.ts @@ -0,0 +1 @@ +export type RoomStatus = 'waiting' | 'playing' | 'finished' diff --git a/src/models/room-state/rule-state.ts b/src/models/room-state/rule-state.ts new file mode 100644 index 0000000..615df9c --- /dev/null +++ b/src/models/room-state/rule-state.ts @@ -0,0 +1,5 @@ +export interface RuleState { + name: string + isBloodFlow: boolean + hasHongZhong: boolean +} diff --git a/src/models/tile.ts b/src/models/tile.ts new file mode 100644 index 0000000..5b16263 --- /dev/null +++ b/src/models/tile.ts @@ -0,0 +1,50 @@ +export type Suit = 'W' | 'T' | 'B' + +export interface Tile { + id: number + suit: Suit + value: number +} + + +export class TileModel { + id: number + suit: Suit + value: number + + constructor(tile: Tile) { + this.id = tile.id + this.suit = tile.suit + this.value = tile.value + } + + /** 花色中文 */ + get suitName(): string { + const map: Record = { + W: '万', + T: '筒', + B: '条', + } + return map[this.suit] + } + + /** 显示文本 */ + toString(): string { + return `${this.suitName}${this.value}[#${this.id}]` + } + + /** 是否同一张牌(和后端一致:按ID) */ + equals(other: TileModel): boolean { + return this.id === other.id + } + + /** 排序权重(用于手牌排序) */ + get sortValue(): number { + const suitOrder: Record = { + W: 0, + T: 1, + B: 2, + } + return suitOrder[this.suit] * 10 + this.value + } +} \ No newline at end of file diff --git a/src/state/active-room.ts b/src/store/active-room-store.ts similarity index 66% rename from src/state/active-room.ts rename to src/store/active-room-store.ts index a12be75..be9c7a6 100644 --- a/src/state/active-room.ts +++ b/src/store/active-room-store.ts @@ -1,70 +1,21 @@ import { ref } from 'vue' +import { + DEFAULT_MAX_PLAYERS, + type RoomPlayerState, + type RoomState, +} from '../models/room-state' -export const DEFAULT_MAX_PLAYERS = 4 -export type RoomStatus = 'waiting' | 'playing' | 'finished' - -export interface RoomPlayerState { - index: number - playerId: string - displayName?: string - ready: boolean - handCount?: number - melds?: string[] - outTiles?: string[] - hasHu?: boolean - missingSuit?: string | null -} - -export interface RuleState { - name: string - isBloodFlow: boolean - hasHongZhong: boolean -} - -export interface GamePlayerState { - playerId: string - index: number - ready: boolean -} - -export interface EngineState { - phase: string - dealerIndex: number - currentTurn: number - needDraw: boolean - players: GamePlayerState[] - wall: string[] - lastDiscardTile: string | null - lastDiscardBy: string - pendingClaim: Record | null - winners: string[] - scores: Record - lastDrawPlayerId: string - lastDrawFromGang: boolean - lastDrawIsLastTile: boolean - huWay: string -} - -export interface GameState { - rule: RuleState | null - state: EngineState | null -} - -export interface RoomState { - id: string - name: string - gameType: string - ownerId: string - maxPlayers: number - playerCount: number - status: RoomStatus | string - createdAt: string - updatedAt: string - game: GameState | null - players: RoomPlayerState[] - currentTurnIndex: number | null - myHand: string[] -} +export { + DEFAULT_MAX_PLAYERS, + type EngineState, + type GamePlayerState, + type GameState, + type PlayerState, + type RoomPlayerState, + type RoomState, + type RoomStatus, + type RuleState, +} from '../models/room-state' function createInitialRoomState(): RoomState { return { diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue index cd11f5c..6b10914 100644 --- a/src/views/ChengduGamePage.vue +++ b/src/views/ChengduGamePage.vue @@ -17,28 +17,22 @@ import RightPlayerCard from '../components/game/RightPlayerCard.vue' import BottomPlayerCard from '../components/game/BottomPlayerCard.vue' import LeftPlayerCard from '../components/game/LeftPlayerCard.vue' import type {SeatPlayerCardModel} from '../components/game/seat-player-card' -import {type SeatKey, useChengduGameRoom} from '../features/chengdu-game/useChengduGameRoom' +import {type SeatKey, useChengduGameRoom} from '../game/chengdu' const route = useRoute() const router = useRouter() const { roomState, - roomId, roomName, loggedInUserName, wsStatus, wsError, wsMessages, - startGamePending, leaveRoomPending, - canStartGame, seatViews, selectedTile, - actionButtons, connectWs, - sendStartGame, selectTile, - sendGameAction, backHall, } = useChengduGameRoom(route, router) @@ -57,7 +51,6 @@ const roomStatusText = computed(() => { if (roomState.value.status === 'finished') { return '已结束' } - return '等待中' }) const currentPhaseText = computed(() => { @@ -68,7 +61,6 @@ const currentPhaseText = computed(() => { const phaseLabelMap: Record = { dealing: '发牌', - draw: '摸牌', discard: '出牌', action: '响应', settle: '结算', @@ -152,30 +144,6 @@ const seatDecor = computed>(() => { return result }) -const centerTimer = computed(() => { - const wallLeft = roomState.value.game?.state?.wall.length - if (typeof wallLeft === 'number' && Number.isFinite(wallLeft)) { - return String(wallLeft).padStart(2, '0') - } - - return String(roomState.value.playerCount).padStart(2, '0') -}) - -const selectedTileText = computed(() => selectedTile.value ?? '未选择') - -const pendingClaimText = computed(() => { - const claim = roomState.value.game?.state?.pendingClaim - if (!claim) { - return '无' - } - - try { - return JSON.stringify(claim) - } catch { - return '有待响应动作' - } -}) - const rightMessages = computed(() => wsMessages.value.slice(-16).reverse()) const floatingMissingSuit = computed(() => { @@ -193,10 +161,6 @@ const floatingMissingSuit = computed(() => { }) function missingSuitLabel(value: string | null | undefined): string { - if (!value) { - return '未定' - } - const suitMap: Record = { wan: '万', tong: '筒', @@ -217,16 +181,6 @@ function getBackImage(seat: SeatKey): string { return imageMap[seat] } -function actionTheme(type: string): 'gold' | 'jade' | 'blue' { - if (type === 'hu' || type === 'gang') { - return 'gold' - } - if (type === 'pass') { - return 'jade' - } - return 'blue' -} - function toggleMenu(): void { menuTriggerActive.value = true if (menuTriggerTimer !== null) { @@ -316,7 +270,6 @@ onBeforeUnmount(() => {
-
-
- 四川麻将 - {{ roomState.name || roomName || '成都麻将房' }} - 底注 6 亿 · 封顶 32 倍 -
@@ -392,14 +340,6 @@ onBeforeUnmount(() => {
-
- - 西 - {{ centerTimer }} - - -
-
{{ seatDecor.top.missingSuitLabel }} @@ -413,30 +353,8 @@ onBeforeUnmount(() => { {{ seatDecor.right.missingSuitLabel }}
-
- {{ roomStatusText }} - {{ currentPhaseText }} -
-
-

房间 {{ roomId || '--' }}

- 当前选择:{{ selectedTileText }} · 待响应:{{ pendingClaimText }} -
- -
- -
-

等待服务端下发 `my_hand`。

diff --git a/src/views/HallPage.vue b/src/views/HallPage.vue index fcf005c..e0a7fcd 100644 --- a/src/views/HallPage.vue +++ b/src/views/HallPage.vue @@ -4,8 +4,8 @@ import { useRouter } from 'vue-router' import { AuthExpiredError, type AuthSession } from '../api/authed-request' import { createRoom, joinRoom, listRooms, type RoomItem } from '../api/mahjong' import { getUserInfo, type UserInfo } from '../api/user' -import { hydrateActiveRoomFromSelection } from '../state/active-room' -import type { RoomPlayerState } from '../state/active-room' +import { hydrateActiveRoomFromSelection } from '../store/active-room-store' +import type { RoomPlayerState } from '../store/active-room-store' import type { StoredAuth } from '../types/session' import { clearAuth, readStoredAuth, writeStoredAuth } from '../utils/auth-storage' @@ -129,6 +129,10 @@ function mapRoomPlayers(room: RoomItem): RoomPlayerState[] { index: Number.isFinite(item.index) ? item.index : fallbackIndex, playerId: item.player_id, ready: Boolean(item.ready), + hand: [], + melds: [], + outTiles: [], + hasHu: false, })) .filter((item) => Boolean(item.playerId)) }