feat(game): 更新成都麻将游戏页面功能实现
- 移除静态背景图片导入,改为动态获取牌面图片 - 添加 MeldState 类型定义,支持副露状态管理 - 重构牌面图片获取逻辑,为不同座位创建独立配置文件 - 定义 TableTileImageType、WallTileItem 和 WallSeatState 接口 - 移除 selectedTile 响应式变量,优化手牌显示逻辑 - 创建 sortedVisibleHandTiles 计算属性替代原 visibleHandTiles - 添加 normalizeMeldType 和 normalizeMelds 函数处理副露数据标准化 - 在 PlayerState 中新增 handCount 和 hasHu 属性 - 更新房间玩家数据结构,同步处理手牌计数和胡牌状态 - 重构牌墙显示逻辑,实现动态渲染各座位手牌和副露 - 添加胡牌标识显示功能,改进牌面分组展示效果 - 优化 CSS 样式,调整牌墙布局和间距设置
This commit is contained in:
@@ -513,6 +513,11 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wall-live .wall-live-tile {
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.wall-top,
|
.wall-top,
|
||||||
.wall-bottom {
|
.wall-bottom {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -565,6 +570,83 @@
|
|||||||
bottom: 108px;
|
bottom: 108px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wall-top.wall-live .wall-live-tile,
|
||||||
|
.wall-bottom.wall-live .wall-live-tile {
|
||||||
|
width: 36px;
|
||||||
|
height: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-left.wall-live .wall-live-tile {
|
||||||
|
width: 60px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-right.wall-live .wall-live-tile {
|
||||||
|
width: 60px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-top.wall-live .wall-live-tile + .wall-live-tile,
|
||||||
|
.wall-bottom.wall-live .wall-live-tile + .wall-live-tile {
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-left.wall-live .wall-live-tile + .wall-live-tile,
|
||||||
|
.wall-right.wall-live .wall-live-tile + .wall-live-tile {
|
||||||
|
margin-top: -22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-left.wall-live .wall-live-tile + .wall-live-tile{
|
||||||
|
margin-top: -24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-live .wall-live-tile.is-group-start {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-left.wall-live .wall-live-tile.is-group-start,
|
||||||
|
.wall-right.wall-live .wall-live-tile.is-group-start {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-bottom.wall-live {
|
||||||
|
--wall-bottom-live-scale: 1;
|
||||||
|
--wall-bottom-live-width: calc(60px * var(--wall-bottom-live-scale));
|
||||||
|
--wall-bottom-live-height: calc(86px * var(--wall-bottom-live-scale));
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-bottom.wall-live .wall-live-tile {
|
||||||
|
width: var(--wall-bottom-live-width);
|
||||||
|
height: var(--wall-bottom-live-height);
|
||||||
|
filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-bottom.wall-live .wall-live-tile + .wall-live-tile {
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-bottom.wall-live .wall-live-tile.is-group-start {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-hu-flag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #fff3da;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(180deg, rgba(219, 81, 56, 0.92), rgba(146, 32, 20, 0.96));
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
/* 提高特异性,保证本页使用 room.css 配置值 */
|
/* 提高特异性,保证本页使用 room.css 配置值 */
|
||||||
.picture-scene .wall-top {
|
.picture-scene .wall-top {
|
||||||
top: 120px;
|
top: 120px;
|
||||||
@@ -575,7 +657,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.picture-scene .wall-bottom {
|
.picture-scene .wall-bottom {
|
||||||
bottom: 160px;
|
bottom: 124px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.picture-scene .wall-left {
|
.picture-scene .wall-left {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const useGameStore = defineStore('game', {
|
|||||||
if (player.playerId === this.getMyPlayerId()) {
|
if (player.playerId === this.getMyPlayerId()) {
|
||||||
player.handTiles.push(data.tile)
|
player.handTiles.push(data.tile)
|
||||||
}
|
}
|
||||||
|
player.handCount += 1
|
||||||
|
|
||||||
// 剩余牌数减少
|
// 剩余牌数减少
|
||||||
this.remainingTiles = Math.max(0, this.remainingTiles - 1)
|
this.remainingTiles = Math.max(0, this.remainingTiles - 1)
|
||||||
@@ -75,6 +76,7 @@ export const useGameStore = defineStore('game', {
|
|||||||
player.handTiles.splice(index, 1)
|
player.handTiles.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
player.handCount = Math.max(0, player.handCount - 1)
|
||||||
|
|
||||||
// 加入出牌区
|
// 加入出牌区
|
||||||
player.discardTiles.push(data.tile)
|
player.discardTiles.push(data.tile)
|
||||||
@@ -152,8 +154,10 @@ export const useGameStore = defineStore('game', {
|
|||||||
? missingSuitRaw
|
? missingSuitRaw
|
||||||
: previous?.missingSuit,
|
: previous?.missingSuit,
|
||||||
handTiles: previous?.handTiles ?? [],
|
handTiles: previous?.handTiles ?? [],
|
||||||
|
handCount: previous?.handCount ?? 0,
|
||||||
melds: previous?.melds ?? [],
|
melds: previous?.melds ?? [],
|
||||||
discardTiles: previous?.discardTiles ?? [],
|
discardTiles: previous?.discardTiles ?? [],
|
||||||
|
hasHu: previous?.hasHu ?? false,
|
||||||
score: previous?.score ?? 0,
|
score: previous?.score ?? 0,
|
||||||
isReady:
|
isReady:
|
||||||
typeof readyRaw === 'boolean'
|
typeof readyRaw === 'boolean'
|
||||||
@@ -175,8 +179,10 @@ export const useGameStore = defineStore('game', {
|
|||||||
avatarURL: previous?.avatarURL,
|
avatarURL: previous?.avatarURL,
|
||||||
missingSuit: previous?.missingSuit,
|
missingSuit: previous?.missingSuit,
|
||||||
handTiles: previous?.handTiles ?? [],
|
handTiles: previous?.handTiles ?? [],
|
||||||
|
handCount: previous?.handCount ?? 0,
|
||||||
melds: previous?.melds ?? [],
|
melds: previous?.melds ?? [],
|
||||||
discardTiles: previous?.discardTiles ?? [],
|
discardTiles: previous?.discardTiles ?? [],
|
||||||
|
hasHu: previous?.hasHu ?? false,
|
||||||
score: previous?.score ?? 0,
|
score: previous?.score ?? 0,
|
||||||
isReady: previous?.isReady ?? false,
|
isReady: previous?.isReady ?? false,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ export interface PlayerState {
|
|||||||
|
|
||||||
// 手牌(只有自己有完整数据,后端可控制)
|
// 手牌(只有自己有完整数据,后端可控制)
|
||||||
handTiles: Tile[]
|
handTiles: Tile[]
|
||||||
|
handCount: number
|
||||||
|
|
||||||
// 副露(碰/杠)
|
// 副露(碰/杠)
|
||||||
melds: MeldState[]
|
melds: MeldState[]
|
||||||
|
|
||||||
// 出牌区
|
// 出牌区
|
||||||
discardTiles: Tile[]
|
discardTiles: Tile[]
|
||||||
|
hasHu: boolean
|
||||||
|
|
||||||
// 分数
|
// 分数
|
||||||
score: number
|
score: number
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ import tiaoIcon from '../assets/images/flowerClolor/tiao.png'
|
|||||||
import robotIcon from '../assets/images/icons/robot.svg'
|
import robotIcon from '../assets/images/icons/robot.svg'
|
||||||
import exitIcon from '../assets/images/icons/exit.svg'
|
import exitIcon from '../assets/images/icons/exit.svg'
|
||||||
import '../assets/styles/room.css'
|
import '../assets/styles/room.css'
|
||||||
import topBackImage from '../assets/images/tiles/top/tbgs_2.png'
|
|
||||||
import rightBackImage from '../assets/images/tiles/right/tbgs_1.png'
|
|
||||||
import bottomBackImage from '../assets/images/tiles/bottom/tdbgs_4.png'
|
|
||||||
import leftBackImage from '../assets/images/tiles/left/tbgs_3.png'
|
|
||||||
import TopPlayerCard from '../components/game/TopPlayerCard.vue'
|
import TopPlayerCard from '../components/game/TopPlayerCard.vue'
|
||||||
import RightPlayerCard from '../components/game/RightPlayerCard.vue'
|
import RightPlayerCard from '../components/game/RightPlayerCard.vue'
|
||||||
import BottomPlayerCard from '../components/game/BottomPlayerCard.vue'
|
import BottomPlayerCard from '../components/game/BottomPlayerCard.vue'
|
||||||
@@ -35,9 +31,12 @@ import {sendWsMessage} from '../ws/sender'
|
|||||||
import {buildWsUrl} from '../ws/url'
|
import {buildWsUrl} from '../ws/url'
|
||||||
import {useGameStore} from '../store/gameStore'
|
import {useGameStore} from '../store/gameStore'
|
||||||
import {setActiveRoom, useActiveRoomState} from '../store'
|
import {setActiveRoom, useActiveRoomState} from '../store'
|
||||||
import type {PlayerState} from '../types/state'
|
import type {MeldState, PlayerState} from '../types/state'
|
||||||
import type {Tile} from '../types/tile'
|
import type {Tile} from '../types/tile'
|
||||||
import {getTileImage} from "../config/bottomTileMap.ts";
|
import {getTileImage as getBottomTileImage} from '../config/bottomTileMap.ts'
|
||||||
|
import {getTileImage as getTopTileImage} from '../config/topTileMap.ts'
|
||||||
|
import {getTileImage as getRightTileImage} from '../config/rightTileMap.ts'
|
||||||
|
import {getTileImage as getLeftTileImage} from '../config/leftTileMap.ts'
|
||||||
|
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const activeRoom = useActiveRoomState()
|
const activeRoom = useActiveRoomState()
|
||||||
@@ -52,6 +51,20 @@ type DisplayPlayer = PlayerState & {
|
|||||||
|
|
||||||
type GameActionPayload<TType extends GameAction['type']> = Extract<GameAction, { type: TType }>['payload']
|
type GameActionPayload<TType extends GameAction['type']> = Extract<GameAction, { type: TType }>['payload']
|
||||||
type HandSuitLabel = '万' | '筒' | '条'
|
type HandSuitLabel = '万' | '筒' | '条'
|
||||||
|
type TableTileImageType = 'hand' | 'exposed' | 'covered'
|
||||||
|
|
||||||
|
interface WallTileItem {
|
||||||
|
key: string
|
||||||
|
src: string
|
||||||
|
alt: string
|
||||||
|
imageType: TableTileImageType
|
||||||
|
suit?: Tile['suit']
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WallSeatState {
|
||||||
|
tiles: WallTileItem[]
|
||||||
|
hasHu: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface SeatViewModel {
|
interface SeatViewModel {
|
||||||
key: SeatKey
|
key: SeatKey
|
||||||
@@ -64,7 +77,6 @@ const now = ref(Date.now())
|
|||||||
const wsStatus = ref<WsStatus>('idle')
|
const wsStatus = ref<WsStatus>('idle')
|
||||||
const wsMessages = ref<string[]>([])
|
const wsMessages = ref<string[]>([])
|
||||||
const wsError = ref('')
|
const wsError = ref('')
|
||||||
const selectedTile = ref<string | null>(null)
|
|
||||||
const leaveRoomPending = ref(false)
|
const leaveRoomPending = ref(false)
|
||||||
const readyTogglePending = ref(false)
|
const readyTogglePending = ref(false)
|
||||||
const startGamePending = ref(false)
|
const startGamePending = ref(false)
|
||||||
@@ -178,12 +190,8 @@ const visibleHandTileGroups = computed(() => {
|
|||||||
.filter((group) => group.tiles.length > 0)
|
.filter((group) => group.tiles.length > 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
const visibleHandTiles = computed(() => {
|
const sortedVisibleHandTiles = computed(() => {
|
||||||
if (gameStore.phase === 'waiting') {
|
return visibleHandTileGroups.value.flatMap((group) => group.tiles)
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return myHandTiles.value
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const remainingTiles = computed(() => {
|
const remainingTiles = computed(() => {
|
||||||
@@ -465,6 +473,83 @@ function normalizeTiles(value: unknown): Tile[] {
|
|||||||
.filter((item): item is Tile => Boolean(item))
|
.filter((item): item is Tile => Boolean(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMeldType(value: unknown, concealed = false): MeldState['type'] | null {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return concealed ? 'an_gang' : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = value.replace(/[-\s]/g, '_').toLowerCase()
|
||||||
|
if (normalized === 'peng') {
|
||||||
|
return 'peng'
|
||||||
|
}
|
||||||
|
if (normalized === 'ming_gang' || normalized === 'gang' || normalized === 'gang_open') {
|
||||||
|
return concealed ? 'an_gang' : 'ming_gang'
|
||||||
|
}
|
||||||
|
if (normalized === 'an_gang' || normalized === 'angang' || normalized === 'concealed_gang') {
|
||||||
|
return 'an_gang'
|
||||||
|
}
|
||||||
|
|
||||||
|
return concealed ? 'an_gang' : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMelds(value: unknown): PlayerState['melds'] {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.map((item) => {
|
||||||
|
const source = asRecord(item)
|
||||||
|
if (!source) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tiles = normalizeTiles(
|
||||||
|
source.tiles ??
|
||||||
|
source.meld_tiles ??
|
||||||
|
source.meldTiles ??
|
||||||
|
source.cards ??
|
||||||
|
source.card_list,
|
||||||
|
)
|
||||||
|
if (tiles.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const concealed =
|
||||||
|
readBoolean(source, 'concealed', 'is_concealed', 'isConcealed', 'hidden', 'is_hidden') ?? false
|
||||||
|
const type = normalizeMeldType(
|
||||||
|
source.type ?? source.meld_type ?? source.meldType ?? source.kind,
|
||||||
|
concealed,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (type === 'peng') {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
tiles,
|
||||||
|
fromPlayerId: readString(source, 'from_player_id', 'fromPlayerId'),
|
||||||
|
} satisfies MeldState
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'ming_gang') {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
tiles,
|
||||||
|
fromPlayerId: readString(source, 'from_player_id', 'fromPlayerId'),
|
||||||
|
} satisfies MeldState
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'an_gang') {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
tiles,
|
||||||
|
} satisfies MeldState
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter((item): item is MeldState => Boolean(item))
|
||||||
|
}
|
||||||
|
|
||||||
function requestRoomInfo(): void {
|
function requestRoomInfo(): void {
|
||||||
const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
|
const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
|
||||||
const roomId = routeRoomId || gameStore.roomId || activeRoom.value?.roomId || ''
|
const roomId = routeRoomId || gameStore.roomId || activeRoom.value?.roomId || ''
|
||||||
@@ -538,8 +623,10 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
missingSuit?: string | null
|
missingSuit?: string | null
|
||||||
isReady: boolean
|
isReady: boolean
|
||||||
handTiles: Tile[]
|
handTiles: Tile[]
|
||||||
|
handCount: number
|
||||||
melds: PlayerState['melds']
|
melds: PlayerState['melds']
|
||||||
discardTiles: Tile[]
|
discardTiles: Tile[]
|
||||||
|
hasHu: boolean
|
||||||
score: number
|
score: number
|
||||||
}
|
}
|
||||||
}>()
|
}>()
|
||||||
@@ -582,8 +669,10 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
missingSuit,
|
missingSuit,
|
||||||
isReady: ready,
|
isReady: ready,
|
||||||
handTiles: [],
|
handTiles: [],
|
||||||
|
handCount: 0,
|
||||||
melds: [],
|
melds: [],
|
||||||
discardTiles: [],
|
discardTiles: [],
|
||||||
|
hasHu: false,
|
||||||
score: 0,
|
score: 0,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -609,6 +698,13 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
const missingSuit = readString(player, 'missing_suit', 'MissingSuit') || existing?.gamePlayer.missingSuit || null
|
const missingSuit = readString(player, 'missing_suit', 'MissingSuit') || existing?.gamePlayer.missingSuit || null
|
||||||
const handCount = readNumber(player, 'hand_count', 'handCount') ?? 0
|
const handCount = readNumber(player, 'hand_count', 'handCount') ?? 0
|
||||||
const outTiles = normalizeTiles(player.out_tiles ?? player.outTiles)
|
const outTiles = normalizeTiles(player.out_tiles ?? player.outTiles)
|
||||||
|
const melds = normalizeMelds(
|
||||||
|
player.melds ??
|
||||||
|
player.exposed_melds ??
|
||||||
|
player.exposedMelds ??
|
||||||
|
player.claims,
|
||||||
|
)
|
||||||
|
const hasHu = Boolean(player.has_hu ?? player.hasHu)
|
||||||
|
|
||||||
playerMap.set(playerId, {
|
playerMap.set(playerId, {
|
||||||
roomPlayer: {
|
roomPlayer: {
|
||||||
@@ -618,9 +714,9 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
missingSuit,
|
missingSuit,
|
||||||
ready: existing?.roomPlayer.ready ?? false,
|
ready: existing?.roomPlayer.ready ?? false,
|
||||||
hand: Array.from({length: handCount}, () => ''),
|
hand: Array.from({length: handCount}, () => ''),
|
||||||
melds: [],
|
melds: melds.map((meld) => meld.type),
|
||||||
outTiles: outTiles.map((tile) => tileToText(tile)),
|
outTiles: outTiles.map((tile) => tileToText(tile)),
|
||||||
hasHu: Boolean(player.has_hu ?? player.hasHu),
|
hasHu,
|
||||||
},
|
},
|
||||||
gamePlayer: {
|
gamePlayer: {
|
||||||
playerId,
|
playerId,
|
||||||
@@ -630,8 +726,10 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
missingSuit,
|
missingSuit,
|
||||||
isReady: existing?.gamePlayer.isReady ?? false,
|
isReady: existing?.gamePlayer.isReady ?? false,
|
||||||
handTiles: existing?.gamePlayer.handTiles ?? [],
|
handTiles: existing?.gamePlayer.handTiles ?? [],
|
||||||
melds: existing?.gamePlayer.melds ?? [],
|
handCount,
|
||||||
|
melds: melds.length > 0 ? melds : existing?.gamePlayer.melds ?? [],
|
||||||
discardTiles: outTiles,
|
discardTiles: outTiles,
|
||||||
|
hasHu,
|
||||||
score: existing?.gamePlayer.score ?? 0,
|
score: existing?.gamePlayer.score ?? 0,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -643,6 +741,7 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
if (current) {
|
if (current) {
|
||||||
current.roomPlayer.hand = privateHand.map((tile) => tileToText(tile))
|
current.roomPlayer.hand = privateHand.map((tile) => tileToText(tile))
|
||||||
current.gamePlayer.handTiles = privateHand
|
current.gamePlayer.handTiles = privateHand
|
||||||
|
current.gamePlayer.handCount = privateHand.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,8 +761,14 @@ function handleRoomInfoResponse(message: unknown): void {
|
|||||||
avatarURL: gamePlayer.avatarURL ?? previous?.avatarURL,
|
avatarURL: gamePlayer.avatarURL ?? previous?.avatarURL,
|
||||||
missingSuit: gamePlayer.missingSuit ?? previous?.missingSuit,
|
missingSuit: gamePlayer.missingSuit ?? previous?.missingSuit,
|
||||||
handTiles: gamePlayer.handTiles.length > 0 ? gamePlayer.handTiles : previous?.handTiles ?? [],
|
handTiles: gamePlayer.handTiles.length > 0 ? gamePlayer.handTiles : previous?.handTiles ?? [],
|
||||||
|
handCount: gamePlayer.handCount > 0
|
||||||
|
? gamePlayer.handCount
|
||||||
|
: gamePlayer.handTiles.length > 0
|
||||||
|
? gamePlayer.handTiles.length
|
||||||
|
: (previous?.handCount ?? 0),
|
||||||
melds: gamePlayer.melds.length > 0 ? gamePlayer.melds : previous?.melds ?? [],
|
melds: gamePlayer.melds.length > 0 ? gamePlayer.melds : previous?.melds ?? [],
|
||||||
discardTiles: gamePlayer.discardTiles.length > 0 ? gamePlayer.discardTiles : previous?.discardTiles ?? [],
|
discardTiles: gamePlayer.discardTiles.length > 0 ? gamePlayer.discardTiles : previous?.discardTiles ?? [],
|
||||||
|
hasHu: gamePlayer.hasHu || previous?.hasHu || false,
|
||||||
score: typeof score === 'number' ? score : previous?.score ?? gamePlayer.score ?? 0,
|
score: typeof score === 'number' ? score : previous?.score ?? gamePlayer.score ?? 0,
|
||||||
isReady: gamePlayer.isReady,
|
isReady: gamePlayer.isReady,
|
||||||
}
|
}
|
||||||
@@ -764,25 +869,110 @@ const formattedClock = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const wallBacks = computed<Record<SeatKey, string[]>>(() => {
|
function buildWallTileImage(
|
||||||
if (gameStore.phase === 'waiting' || remainingTiles.value <= 0) {
|
seat: SeatKey,
|
||||||
|
tile: Tile | undefined,
|
||||||
|
imageType: TableTileImageType,
|
||||||
|
): string {
|
||||||
|
switch (seat) {
|
||||||
|
case 'top':
|
||||||
|
return getTopTileImage(tile, imageType, 'top')
|
||||||
|
case 'right':
|
||||||
|
return getRightTileImage(tile, imageType, 'right')
|
||||||
|
case 'left':
|
||||||
|
return getLeftTileImage(tile, imageType, 'left')
|
||||||
|
case 'bottom':
|
||||||
|
default:
|
||||||
|
if (!tile) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return getBottomTileImage(tile, imageType, 'bottom')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyWallSeat(): WallSeatState {
|
||||||
return {
|
return {
|
||||||
top: [],
|
tiles: [],
|
||||||
right: [],
|
hasHu: false,
|
||||||
bottom: [],
|
}
|
||||||
left: [],
|
}
|
||||||
|
|
||||||
|
const wallSeats = computed<Record<SeatKey, WallSeatState>>(() => {
|
||||||
|
const emptyState: Record<SeatKey, WallSeatState> = {
|
||||||
|
top: emptyWallSeat(),
|
||||||
|
right: emptyWallSeat(),
|
||||||
|
bottom: emptyWallSeat(),
|
||||||
|
left: emptyWallSeat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameStore.phase === 'waiting') {
|
||||||
|
return emptyState
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const seat of seatViews.value) {
|
||||||
|
if (!seat.player) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const seatTiles: WallTileItem[] = []
|
||||||
|
const targetSeat = seat.key
|
||||||
|
|
||||||
|
if (seat.isSelf) {
|
||||||
|
sortedVisibleHandTiles.value.forEach((tile, index) => {
|
||||||
|
const src = buildWallTileImage(targetSeat, tile, 'hand')
|
||||||
|
if (!src) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seatTiles.push({
|
||||||
|
key: `hand-${tile.id}-${index}`,
|
||||||
|
src,
|
||||||
|
alt: formatTile(tile),
|
||||||
|
imageType: 'hand',
|
||||||
|
suit: tile.suit,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
for (let index = 0; index < seat.player.handCount; index += 1) {
|
||||||
|
const src = buildWallTileImage(targetSeat, undefined, 'hand')
|
||||||
|
if (!src) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
seatTiles.push({
|
||||||
|
key: `concealed-${index}`,
|
||||||
|
src,
|
||||||
|
alt: '手牌背面',
|
||||||
|
imageType: 'hand',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wallSize = remainingTiles.value
|
seat.player.melds.forEach((meld, meldIndex) => {
|
||||||
const perSide = Math.max(6, Math.ceil((wallSize || 48) / 4 / 2))
|
meld.tiles.forEach((tile, tileIndex) => {
|
||||||
|
const imageType: TableTileImageType = meld.type === 'an_gang' ? 'covered' : 'exposed'
|
||||||
return {
|
const src = buildWallTileImage(targetSeat, tile, imageType)
|
||||||
top: Array.from({length: perSide}, (_, index) => `top-${index}`),
|
if (!src) {
|
||||||
right: Array.from({length: perSide}, (_, index) => `right-${index}`),
|
return
|
||||||
bottom: Array.from({length: perSide}, (_, index) => `bottom-${index}`),
|
|
||||||
left: Array.from({length: perSide}, (_, index) => `left-${index}`),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seatTiles.push({
|
||||||
|
key: `${meld.type}-${meldIndex}-${tile.id}-${tileIndex}`,
|
||||||
|
src,
|
||||||
|
alt: formatTile(tile),
|
||||||
|
imageType,
|
||||||
|
suit: tile.suit,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
emptyState[targetSeat] = {
|
||||||
|
tiles: seatTiles,
|
||||||
|
hasHu: seat.player.hasHu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyState
|
||||||
})
|
})
|
||||||
|
|
||||||
const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
|
const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
|
||||||
@@ -860,17 +1050,6 @@ function missingSuitLabel(value: string | null | undefined): string {
|
|||||||
return suitMap[value] ?? value
|
return suitMap[value] ?? value
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBackImage(seat: SeatKey): string {
|
|
||||||
const imageMap: Record<SeatKey, string> = {
|
|
||||||
top: topBackImage,
|
|
||||||
right: rightBackImage,
|
|
||||||
bottom: bottomBackImage,
|
|
||||||
left: leftBackImage,
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageMap[seat]
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMenu(): void {
|
function toggleMenu(): void {
|
||||||
menuTriggerActive.value = true
|
menuTriggerActive.value = true
|
||||||
if (menuTriggerTimer !== null) {
|
if (menuTriggerTimer !== null) {
|
||||||
@@ -900,10 +1079,6 @@ function toggleTrustMode(): void {
|
|||||||
menuOpen.value = false
|
menuOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTile(tile: string): void {
|
|
||||||
selectedTile.value = selectedTile.value === tile ? null : tile
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTile(tile: Tile): string {
|
function formatTile(tile: Tile): string {
|
||||||
return `${tile.suit}${tile.value}`
|
return `${tile.suit}${tile.value}`
|
||||||
}
|
}
|
||||||
@@ -938,6 +1113,7 @@ function handlePlayerHandResponse(message: unknown): void {
|
|||||||
const existingPlayer = gameStore.players[loggedInUserId.value]
|
const existingPlayer = gameStore.players[loggedInUserId.value]
|
||||||
if (existingPlayer) {
|
if (existingPlayer) {
|
||||||
existingPlayer.handTiles = handTiles
|
existingPlayer.handTiles = handTiles
|
||||||
|
existingPlayer.handCount = handTiles.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const room = activeRoom.value
|
const room = activeRoom.value
|
||||||
@@ -1338,8 +1514,10 @@ function hydrateFromActiveRoom(routeRoomId: string): void {
|
|||||||
missingSuit: player.missingSuit ?? previous?.missingSuit,
|
missingSuit: player.missingSuit ?? previous?.missingSuit,
|
||||||
isReady: player.ready,
|
isReady: player.ready,
|
||||||
handTiles: previous?.handTiles ?? [],
|
handTiles: previous?.handTiles ?? [],
|
||||||
|
handCount: previous?.handCount ?? 0,
|
||||||
melds: previous?.melds ?? [],
|
melds: previous?.melds ?? [],
|
||||||
discardTiles: previous?.discardTiles ?? [],
|
discardTiles: previous?.discardTiles ?? [],
|
||||||
|
hasHu: previous?.hasHu ?? false,
|
||||||
score: previous?.score ?? 0,
|
score: previous?.score ?? 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1507,17 +1685,61 @@ onBeforeUnmount(() => {
|
|||||||
<BottomPlayerCard :player="seatDecor.bottom"/>
|
<BottomPlayerCard :player="seatDecor.bottom"/>
|
||||||
<LeftPlayerCard :player="seatDecor.left"/>
|
<LeftPlayerCard :player="seatDecor.left"/>
|
||||||
|
|
||||||
<div class="wall wall-top">
|
<div v-if="wallSeats.top.tiles.length > 0 || wallSeats.top.hasHu" class="wall wall-top wall-live">
|
||||||
<img v-for="key in wallBacks.top" :key="key" :src="getBackImage('top')" alt=""/>
|
<img
|
||||||
|
v-for="(tile, index) in wallSeats.top.tiles"
|
||||||
|
:key="tile.key"
|
||||||
|
class="wall-live-tile"
|
||||||
|
:class="{
|
||||||
|
'is-group-start': index > 0 && tile.suit && wallSeats.top.tiles[index - 1]?.suit !== tile.suit,
|
||||||
|
'is-exposed': tile.imageType !== 'hand',
|
||||||
|
}"
|
||||||
|
:src="tile.src"
|
||||||
|
:alt="tile.alt"
|
||||||
|
/>
|
||||||
|
<span v-if="wallSeats.top.hasHu" class="wall-hu-flag">胡</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wall wall-right">
|
<div v-if="wallSeats.right.tiles.length > 0 || wallSeats.right.hasHu" class="wall wall-right wall-live">
|
||||||
<img v-for="key in wallBacks.right" :key="key" :src="getBackImage('right')" alt=""/>
|
<img
|
||||||
|
v-for="(tile, index) in wallSeats.right.tiles"
|
||||||
|
:key="tile.key"
|
||||||
|
class="wall-live-tile"
|
||||||
|
:class="{
|
||||||
|
'is-group-start': index > 0 && tile.suit && wallSeats.right.tiles[index - 1]?.suit !== tile.suit,
|
||||||
|
'is-exposed': tile.imageType !== 'hand',
|
||||||
|
}"
|
||||||
|
:src="tile.src"
|
||||||
|
:alt="tile.alt"
|
||||||
|
/>
|
||||||
|
<span v-if="wallSeats.right.hasHu" class="wall-hu-flag">胡</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wall wall-bottom">
|
<div v-if="wallSeats.bottom.tiles.length > 0 || wallSeats.bottom.hasHu" class="wall wall-bottom wall-live">
|
||||||
<img v-for="key in wallBacks.bottom" :key="key" :src="getBackImage('bottom')" alt=""/>
|
<img
|
||||||
|
v-for="(tile, index) in wallSeats.bottom.tiles"
|
||||||
|
:key="tile.key"
|
||||||
|
class="wall-live-tile"
|
||||||
|
:class="{
|
||||||
|
'is-group-start': index > 0 && tile.suit && wallSeats.bottom.tiles[index - 1]?.suit !== tile.suit,
|
||||||
|
'is-exposed': tile.imageType !== 'hand',
|
||||||
|
}"
|
||||||
|
:src="tile.src"
|
||||||
|
:alt="tile.alt"
|
||||||
|
/>
|
||||||
|
<span v-if="wallSeats.bottom.hasHu" class="wall-hu-flag">胡</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wall wall-left">
|
<div v-if="wallSeats.left.tiles.length > 0 || wallSeats.left.hasHu" class="wall wall-left wall-live">
|
||||||
<img v-for="key in wallBacks.left" :key="key" :src="getBackImage('left')" alt=""/>
|
<img
|
||||||
|
v-for="(tile, index) in wallSeats.left.tiles"
|
||||||
|
:key="tile.key"
|
||||||
|
class="wall-live-tile"
|
||||||
|
:class="{
|
||||||
|
'is-group-start': index > 0 && tile.suit && wallSeats.left.tiles[index - 1]?.suit !== tile.suit,
|
||||||
|
'is-exposed': tile.imageType !== 'hand',
|
||||||
|
}"
|
||||||
|
:src="tile.src"
|
||||||
|
:alt="tile.alt"
|
||||||
|
/>
|
||||||
|
<span v-if="wallSeats.left.hasHu" class="wall-hu-flag">胡</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="floating-status top">
|
<div class="floating-status top">
|
||||||
@@ -1562,29 +1784,6 @@ onBeforeUnmount(() => {
|
|||||||
<span class="ready-toggle-label">开始游戏</span>
|
<span class="ready-toggle-label">开始游戏</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-hand" v-if="visibleHandTiles.length > 0">
|
|
||||||
<div
|
|
||||||
v-for="group in visibleHandTileGroups"
|
|
||||||
:key="group.suit"
|
|
||||||
class="player-hand-group"
|
|
||||||
:data-suit="group.suit"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-for="tile in group.tiles"
|
|
||||||
:key="tile.id"
|
|
||||||
class="tile-chip"
|
|
||||||
:class="{ selected: selectedTile === formatTile(tile) }"
|
|
||||||
type="button"
|
|
||||||
@click="selectTile(formatTile(tile))"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="tile-chip-image"
|
|
||||||
:src="getTileImage(tile)"
|
|
||||||
:alt="formatTile(tile)"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user