Files
mahjong-web/src/views/chengdu/composables/useChengduTableView.ts
wsy182 e495dc6070 feat(chengdu): 更新结算界面添加准备状态和房间管理功能
- 添加玩家准备状态显示和切换功能
- 实现房主控制下一局开始的游戏流程
- 添加退出房间按钮和相关状态管理
- 集成准备状态的计算逻辑和UI展示
- 更新组件props传递准备和房间状态数据
- 重构结算界面按钮布局和交互逻辑
2026-04-07 13:36:28 +08:00

432 lines
12 KiB
TypeScript

import { computed } from 'vue'
import eastWind from '../../../assets/images/direction/dong.png'
import southWind from '../../../assets/images/direction/nan.png'
import westWind from '../../../assets/images/direction/xi.png'
import northWind from '../../../assets/images/direction/bei.png'
import type { SeatPlayerCardModel } from '../../../components/game/seat-player-card'
import { getTileImage as getBottomTileImage } from '../../../config/bottomTileMap'
import { getTileImage as getLeftTileImage } from '../../../config/leftTileMap'
import { getTileImage as getRightTileImage } from '../../../config/rightTileMap'
import { getTileImage as getTopTileImage } from '../../../config/topTileMap'
import type { SeatKey } from '../../../game/seat'
import type { Tile } from '../../../types/tile'
import type {
DeskSeatState,
DisplayPlayer,
HandSuitLabel,
TableTileImageType,
TableViewDeps,
TableViewResult,
WallSeatState,
WallTileItem,
} from '../types'
const handSuitOrder: Record<Tile['suit'], number> = { W: 0, T: 1, B: 2 }
const handSuitLabelMap: Record<Tile['suit'], HandSuitLabel> = { W: '万', T: '筒', B: '条' }
function buildWallTileImage(
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:
return tile ? getBottomTileImage(tile, imageType, 'bottom') : ''
}
}
export function missingSuitLabel(value: string | null | undefined): string {
const suitMap: Record<string, string> = {
w: '万',
t: '筒',
b: '条',
wan: '万',
tong: '筒',
tiao: '条',
}
if (!value) {
return ''
}
const normalized = value.trim().toLowerCase()
return suitMap[normalized] ?? value
}
export function formatTile(tile: Tile): string {
return `${tile.suit}${tile.value}`
}
function emptyWallSeat(): WallSeatState {
return { tiles: [] }
}
function emptyDeskSeat(): DeskSeatState {
return { tiles: [], hasHu: false }
}
export function useChengduTableView(deps: TableViewDeps): TableViewResult {
const visibleHandTileGroups = computed(() => {
const grouped = new Map<HandSuitLabel, Tile[]>()
deps.myHandTiles.value
.slice()
.sort((left, right) => {
const suitDiff = handSuitOrder[left.suit] - handSuitOrder[right.suit]
if (suitDiff !== 0) {
return suitDiff
}
const valueDiff = left.value - right.value
if (valueDiff !== 0) {
return valueDiff
}
return left.id - right.id
})
.forEach((tile) => {
const label = handSuitLabelMap[tile.suit]
const current = grouped.get(label) ?? []
current.push(tile)
grouped.set(label, current)
})
return (['万', '筒', '条'] as HandSuitLabel[])
.map((suit) => ({
suit,
tiles: grouped.get(suit) ?? [],
}))
.filter((group) => group.tiles.length > 0)
})
const sortedVisibleHandTiles = computed(() => visibleHandTileGroups.value.flatMap((group) => group.tiles))
const roomName = computed(() => {
const activeRoomName =
deps.roomMeta.value && deps.roomMeta.value.roomId === deps.gameStore.roomId
? deps.roomMeta.value.roomName
: ''
return deps.routeRoomName.value || activeRoomName || `房间 ${deps.gameStore.roomId || '--'}`
})
const roomState = computed(() => {
const status =
deps.gameStore.phase === 'waiting'
? 'waiting'
: deps.gameStore.phase === 'settlement'
? 'finished'
: 'playing'
const wall = Array.from({ length: deps.gameStore.remainingTiles }, (_, index) => `wall-${index}`)
const maxPlayers =
deps.roomMeta.value && deps.roomMeta.value.roomId === deps.gameStore.roomId
? deps.roomMeta.value.maxPlayers
: 4
return {
roomId: deps.gameStore.roomId,
name: roomName.value,
playerCount: deps.gamePlayers.value.length,
maxPlayers,
status,
game: {
state: {
wall,
dealerIndex: deps.gameStore.dealerIndex,
currentTurn: deps.gameStore.currentTurn,
phase: deps.gameStore.phase,
},
},
}
})
const seatViews = computed(() => {
const players = deps.gamePlayers.value
const tableOrder: SeatKey[] = ['bottom', 'right', 'top', 'left']
const selfSeatIndex =
deps.myPlayer.value?.seatIndex ??
players.find((player) => player.playerId === deps.loggedInUserId.value)?.seatIndex ??
0
return players.slice(0, 4).map((player) => {
const relativeIndex = (selfSeatIndex - player.seatIndex + 4) % 4
const seatKey = tableOrder[relativeIndex] ?? 'top'
return {
key: seatKey,
player,
isSelf: player.playerId === deps.loggedInUserId.value,
isTurn: player.seatIndex === deps.gameStore.currentTurn,
}
})
})
const seatWinds = computed<Record<SeatKey, string>>(() => {
const tableOrder: SeatKey[] = ['bottom', 'right', 'top', 'left']
const players = deps.gamePlayers.value
const selfSeatIndex =
deps.myPlayer.value?.seatIndex ??
players.find((player) => player.playerId === deps.loggedInUserId.value)?.seatIndex ??
0
const directionBySeatIndex = [eastWind, southWind, westWind, northWind]
const result: Record<SeatKey, string> = {
top: northWind,
right: eastWind,
bottom: southWind,
left: westWind,
}
for (let absoluteSeat = 0; absoluteSeat < 4; absoluteSeat += 1) {
const relativeIndex = (selfSeatIndex - absoluteSeat + 4) % 4
const seatKey = tableOrder[relativeIndex] ?? 'top'
result[seatKey] = directionBySeatIndex[absoluteSeat] ?? northWind
}
return result
})
const currentTurnSeat = computed<SeatKey | ''>(() => seatViews.value.find((seat) => seat.isTurn)?.key ?? '')
const currentPhaseText = computed(() => {
const map: Record<string, string> = {
waiting: '等待中',
dealing: '发牌中',
playing: '对局中',
action: '操作中',
settlement: '已结算',
}
return map[deps.gameStore.phase] ?? deps.gameStore.phase
})
const roomStatusText = computed(() => {
const map: Record<string, string> = {
waiting: '等待玩家',
playing: '游戏中',
finished: '已结束',
}
const status = roomState.value.status
return map[status] ?? status ?? '--'
})
const roundText = computed(() => {
if (deps.gameStore.totalRounds > 0) {
return `${deps.gameStore.currentRound}/${deps.gameStore.totalRounds}`
}
return ''
})
const settlementPlayers = computed(() => {
const winnerSet = new Set(deps.gameStore.winners)
return Object.values(deps.gameStore.players as Record<string, DisplayPlayer>)
.map((player) => ({
playerId: player.playerId,
displayName: player.displayName || `玩家${player.seatIndex + 1}`,
score: deps.gameStore.scores[player.playerId] ?? 0,
isWinner: winnerSet.has(player.playerId),
seatIndex: player.seatIndex,
isReady: Boolean(player.isReady),
}))
.sort((a, b) => b.score - a.score)
})
const wallSeats = computed<Record<SeatKey, WallSeatState>>(() => {
const emptyState: Record<SeatKey, WallSeatState> = {
top: emptyWallSeat(),
right: emptyWallSeat(),
bottom: emptyWallSeat(),
left: emptyWallSeat(),
}
if (deps.gameStore.phase === 'waiting' && deps.myHandTiles.value.length === 0) {
return emptyState
}
for (const seat of seatViews.value) {
if (!seat.player) {
continue
}
const seatTiles: WallTileItem[] = []
const targetSeat = seat.key
if (seat.isSelf) {
const missingSuit = seat.player.missingSuit as Tile['suit'] | null | undefined
sortedVisibleHandTiles.value.forEach((tile, index) => {
const src = buildWallTileImage(targetSeat, tile, 'hand')
if (!src) {
return
}
const previousTile = index > 0 ? sortedVisibleHandTiles.value[index - 1] : undefined
const isMissingSuitGroupStart = Boolean(
missingSuit &&
tile.suit === missingSuit &&
(!previousTile || previousTile.suit !== tile.suit),
)
seatTiles.push({
key: `hand-${tile.id}-${index}`,
src,
alt: formatTile(tile),
imageType: 'hand',
showLackTag: isMissingSuitGroupStart,
suit: tile.suit,
tile,
})
})
} 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',
})
}
}
emptyState[targetSeat] = { tiles: seatTiles }
}
return emptyState
})
const deskSeats = computed<Record<SeatKey, DeskSeatState>>(() => {
const emptyState: Record<SeatKey, DeskSeatState> = {
top: emptyDeskSeat(),
right: emptyDeskSeat(),
bottom: emptyDeskSeat(),
left: emptyDeskSeat(),
}
if (deps.gameStore.phase === 'waiting' && deps.myHandTiles.value.length === 0) {
return emptyState
}
for (const seat of seatViews.value) {
if (!seat.player) {
continue
}
const seatTiles: WallTileItem[] = []
const targetSeat = seat.key
seat.player.discardTiles.forEach((tile, index) => {
const src = buildWallTileImage(targetSeat, tile, 'exposed')
if (!src) {
return
}
seatTiles.push({
key: `discard-${tile.id}-${index}`,
src,
alt: formatTile(tile),
imageType: 'exposed',
suit: tile.suit,
})
})
seat.player.melds.forEach((meld, meldIndex) => {
meld.tiles.forEach((tile, tileIndex) => {
const imageType: TableTileImageType = meld.type === 'an_gang' ? 'covered' : 'exposed'
const src = buildWallTileImage(targetSeat, tile, imageType)
if (!src) {
return
}
seatTiles.push({
key: `desk-${meld.type}-${meldIndex}-${tile.id}-${tileIndex}`,
src,
alt: formatTile(tile),
imageType,
isGroupStart: tileIndex === 0,
suit: tile.suit,
})
})
})
emptyState[targetSeat] = {
tiles: seatTiles,
hasHu: seat.player.hasHu,
}
}
return emptyState
})
const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
const dealerIndex = roomState.value.game?.state?.dealerIndex ?? -1
const defaultMissingSuitLabel = missingSuitLabel(null)
const emptySeat = (): SeatPlayerCardModel => ({
avatarUrl: '',
name: '空位',
dealer: false,
isTurn: false,
isReady: false,
isTrustee: false,
missingSuitLabel: defaultMissingSuitLabel,
})
const result: Record<SeatKey, SeatPlayerCardModel> = {
top: emptySeat(),
right: emptySeat(),
bottom: emptySeat(),
left: emptySeat(),
}
for (const seat of seatViews.value) {
if (!seat.player) {
continue
}
const displayName = seat.player.displayName || `玩家${seat.player.seatIndex + 1}`
const avatarUrl = seat.isSelf
? deps.localCachedAvatarUrl.value || seat.player.avatarURL || ''
: seat.player.avatarURL || ''
const selfDisplayName = seat.player.displayName || deps.loggedInUserName.value || '你自己'
result[seat.key] = {
avatarUrl,
name: Array.from(seat.isSelf ? selfDisplayName : displayName)
.slice(0, 4)
.join(''),
dealer: seat.player.seatIndex === dealerIndex,
isTurn: seat.isTurn,
isReady: Boolean(seat.player.isReady),
isTrustee: Boolean(seat.player.isTrustee),
missingSuitLabel: missingSuitLabel(seat.player.missingSuit),
}
}
return result
})
return {
roomName,
roomState,
seatViews,
seatWinds,
currentTurnSeat,
currentPhaseText,
roomStatusText,
roundText,
visibleHandTileGroups,
sortedVisibleHandTiles,
wallSeats,
deskSeats,
seatDecor,
settlementPlayers,
}
}