- 添加玩家准备状态显示和切换功能 - 实现房主控制下一局开始的游戏流程 - 添加退出房间按钮和相关状态管理 - 集成准备状态的计算逻辑和UI展示 - 更新组件props传递准备和房间状态数据 - 重构结算界面按钮布局和交互逻辑
432 lines
12 KiB
TypeScript
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,
|
|
}
|
|
}
|