feat(tiles): 实现麻将牌图像系统并优化游戏界面显示

- 重命名 tileMap.ts 为 bottomTileMap.ts 并扩展支持字牌(东南西北、中发白)
- 新增 leftTileMap.ts、rightTileMap.ts 和 topTileMap.ts 支持多位置牌面渲染
- 实现牌面图像类型区分(手牌、明牌、盖牌)和动态图像键构建
- 添加牌面验证函数支持不同花色的数值范围检查
- 更新 ChengduGamePage.vue 使用新的底部牌面配置文件
- 实现玩家手牌可见性控制仅在非等待阶段显示
- 重构服务器响应解析逻辑适配新的数据结构
- 添加玩家手牌响应处理器实时更新手牌状态
- 将玩家手牌显示从文本改为图像展示提升用户体验
- 重构CSS样式实现牌面图像的响应式布局和阴影效果
This commit is contained in:
2026-03-27 15:34:59 +08:00
parent b1e394d675
commit fd8f6d47fa
8 changed files with 1167 additions and 158 deletions

View File

@@ -37,7 +37,7 @@ import {useGameStore} from '../store/gameStore'
import {setActiveRoom, useActiveRoomState} from '../store'
import type {PlayerState} from '../types/state'
import type {Tile} from '../types/tile'
import {getTileImage} from "../config/tileMap.ts";
import {getTileImage} from "../config/bottomTileMap.ts";
const gameStore = useGameStore()
const activeRoom = useActiveRoomState()
@@ -132,6 +132,14 @@ const myHandTiles = computed(() => {
return myPlayer.value?.handTiles ?? []
})
const visibleHandTiles = computed(() => {
if (gameStore.phase === 'waiting') {
return []
}
return myHandTiles.value
})
const remainingTiles = computed(() => {
return gameStore.remainingTiles
})
@@ -449,22 +457,21 @@ function handleRoomInfoResponse(message: unknown): void {
}
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
console.log("server response payload: " + payload)
const room = asRecord(payload.room)
const gameState = asRecord(payload.game_state)
const playerView = asRecord(payload.player_view)
const roomId =
readString(summary ?? {}, 'room_id', 'roomId') ||
readString(publicState ?? {}, 'room_id', 'roomId') ||
readString(privateState ?? {}, 'room_id', 'roomId') ||
readString(room ?? {}, 'room_id', 'roomId') ||
readString(gameState ?? {}, 'room_id', 'roomId') ||
readString(playerView ?? {}, '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 roomPlayers = Array.isArray(room?.players) ? room.players : []
const gamePlayers = Array.isArray(gameState?.players) ? gameState.players : []
const playerMap = new Map<string, {
roomPlayer: {
index: number
@@ -491,7 +498,7 @@ function handleRoomInfoResponse(message: unknown): void {
}
}>()
summaryPlayers.forEach((item, fallbackIndex) => {
roomPlayers.forEach((item, fallbackIndex) => {
const player = asRecord(item)
if (!player) {
return
@@ -536,7 +543,7 @@ function handleRoomInfoResponse(message: unknown): void {
})
})
publicPlayers.forEach((item, fallbackIndex) => {
gamePlayers.forEach((item, fallbackIndex) => {
const player = asRecord(item)
if (!player) {
return
@@ -584,7 +591,7 @@ function handleRoomInfoResponse(message: unknown): void {
})
})
const privateHand = normalizeTiles(privateState?.hand)
const privateHand = normalizeTiles(playerView?.hand)
if (loggedInUserId.value && playerMap.has(loggedInUserId.value)) {
const current = playerMap.get(loggedInUserId.value)
if (current) {
@@ -599,8 +606,8 @@ function handleRoomInfoResponse(message: unknown): void {
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]
const score = (gameState?.scores && typeof gameState.scores === 'object'
? (gameState.scores as Record<string, unknown>)[gamePlayer.playerId]
: undefined)
nextPlayers[gamePlayer.playerId] = {
playerId: gamePlayer.playerId,
@@ -617,18 +624,18 @@ function handleRoomInfoResponse(message: unknown): void {
})
const status =
readString(publicState ?? {}, 'status') ||
readString(summary ?? {}, 'status') ||
readString(publicState ?? {}, 'phase') ||
readString(gameState ?? {}, 'status') ||
readString(room ?? {}, 'status') ||
readString(gameState ?? {}, 'phase') ||
'waiting'
const phase =
readString(publicState ?? {}, 'phase') ||
readString(summary ?? {}, 'status') ||
readString(gameState ?? {}, 'phase') ||
readString(room ?? {}, '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 wallCount = readNumber(gameState ?? {}, 'wall_count', 'wallCount')
const dealerIndex = readNumber(gameState ?? {}, 'dealer_index', 'dealerIndex')
const currentTurnSeat = readNumber(gameState ?? {}, 'current_turn', 'currentTurn')
const currentTurnPlayerId = readString(gameState ?? {}, 'current_turn_player', 'currentTurnPlayer')
const currentTurn =
currentTurnSeat ??
(currentTurnPlayerId && nextPlayers[currentTurnPlayerId]
@@ -658,24 +665,24 @@ function handleRoomInfoResponse(message: unknown): void {
if (typeof currentTurn === 'number') {
gameStore.currentTurn = currentTurn
}
const scores = asRecord(publicState?.scores)
const scores = asRecord(gameState?.scores)
if (scores) {
gameStore.scores = Object.fromEntries(
Object.entries(scores).filter(([, value]) => typeof value === 'number'),
) as Record<string, number>
}
gameStore.winners = readStringArray(publicState ?? {}, 'winners')
gameStore.winners = readStringArray(gameState ?? {}, '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,
roomName: readString(room ?? {}, 'name', 'room_name') || activeRoom.value?.roomName || roomName.value,
gameType: readString(room ?? {}, 'game_type') || activeRoom.value?.gameType || 'chengdu',
ownerId: readString(room ?? {}, 'owner_id') || activeRoom.value?.ownerId || '',
maxPlayers: readNumber(room ?? {}, 'max_players') ?? activeRoom.value?.maxPlayers ?? 4,
playerCount: readNumber(room ?? {}, 'player_count') ?? players.length,
status,
createdAt: readString(summary ?? {}, 'created_at', 'createdAt') || activeRoom.value?.createdAt || '',
updatedAt: readString(summary ?? {}, 'updated_at', 'updatedAt') || activeRoom.value?.updatedAt || '',
createdAt: readString(room ?? {}, 'created_at') || activeRoom.value?.createdAt || '',
updatedAt: readString(room ?? {}, 'updated_at') || activeRoom.value?.updatedAt || '',
players: players.map((item) => item.roomPlayer),
myHand: privateHand.map((tile) => tileToText(tile)),
game: {
@@ -712,6 +719,15 @@ const formattedClock = computed(() => {
})
const wallBacks = computed<Record<SeatKey, string[]>>(() => {
if (gameStore.phase === 'waiting' || remainingTiles.value <= 0) {
return {
top: [],
right: [],
bottom: [],
left: [],
}
}
const wallSize = remainingTiles.value
const perSide = Math.max(6, Math.ceil((wallSize || 48) / 4 / 2))
@@ -846,6 +862,48 @@ function formatTile(tile: Tile): string {
return `${tile.suit}${tile.value}`
}
function handlePlayerHandResponse(message: unknown): void {
const source = asRecord(message)
if (!source || typeof source.type !== 'string') {
return
}
if (normalizeWsType(source.type) !== 'PLAYER_HAND') {
return
}
const payload = asRecord(source.payload)
if (!payload) {
return
}
const roomId =
readString(payload, 'room_id', 'roomId') ||
readString(source, 'roomId')
if (roomId && gameStore.roomId && roomId !== gameStore.roomId) {
return
}
const handTiles = normalizeTiles(payload.hand)
if (!loggedInUserId.value || handTiles.length === 0) {
return
}
const existingPlayer = gameStore.players[loggedInUserId.value]
if (existingPlayer) {
existingPlayer.handTiles = handTiles
}
const room = activeRoom.value
if (room && room.roomId === (roomId || gameStore.roomId)) {
room.myHand = handTiles.map((tile) => tileToText(tile))
const roomPlayer = room.players.find((item) => item.playerId === loggedInUserId.value)
if (roomPlayer) {
roomPlayer.hand = room.myHand
}
}
}
function toGameAction(message: unknown): GameAction | null {
if (!message || typeof message !== 'object') {
return null
@@ -1265,6 +1323,7 @@ onMounted(() => {
const text = typeof msg === 'string' ? msg : JSON.stringify(msg)
wsMessages.value.push(`[server] ${text}`)
handleRoomInfoResponse(msg)
handlePlayerHandResponse(msg)
handleReadyStateResponse(msg)
const gameAction = toGameAction(msg)
if (gameAction) {
@@ -1457,17 +1516,21 @@ onBeforeUnmount(() => {
<span class="ready-toggle-label">开始游戏</span>
</button>
</div>
<div class="player-hand" v-if="myHandTiles.length > 0">
<div class="player-hand" v-if="visibleHandTiles.length > 0">
<button
v-for="tile in myHandTiles"
v-for="tile in visibleHandTiles"
:key="tile.id"
class="tile-chip"
:class="{ selected: selectedTile === formatTile(tile) }"
type="button"
@click="selectTile(formatTile(tile))"
>
{{ formatTile(tile) }}
<img
class="tile-chip-image"
:src="getTileImage(tile)"
:alt="formatTile(tile)"
/>
</button>
</div>
</div>