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 = { W: 0, T: 1, B: 2 } const handSuitLabelMap: Record = { 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 = { 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() 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>(() => { 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 = { 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(() => seatViews.value.find((seat) => seat.isTurn)?.key ?? '') const currentPhaseText = computed(() => { const map: Record = { waiting: '等待中', dealing: '发牌中', playing: '对局中', action: '操作中', settlement: '已结算', } return map[deps.gameStore.phase] ?? deps.gameStore.phase }) const roomStatusText = computed(() => { const map: Record = { 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) .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>(() => { const emptyState: Record = { 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>(() => { const emptyState: Record = { 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>(() => { 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 = { 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, } }