import {computed} from 'vue' import {sendWsMessage} from '../../../ws/sender' import {formatTile} from './useChengduTableView' import type {ClaimOptionState} from '../../../types/state' import type {Tile} from '../../../types/tile' import type {DisplayPlayer} from '../types' interface UseChengduGameActionsOptions { gameStore: { roomId: string phase: string currentTurn: number needDraw: boolean currentRound: number totalRounds: number pendingClaim?: { tile?: Tile options: ClaimOptionState[] } } roomMeta: { value: { roomId: string; ownerId: string } | null } gamePlayers: { value: DisplayPlayer[] } myPlayer: { value: DisplayPlayer | undefined } session: any } export function useChengduGameActions(options: UseChengduGameActionsOptions) { const isLastRound = computed( () => options.gameStore.currentRound >= options.gameStore.totalRounds && options.gameStore.totalRounds > 0, ) const myReadyState = computed(() => Boolean(options.myPlayer.value?.isReady)) const isRoomOwner = computed(() => { const room = options.roomMeta.value return Boolean( room && room.roomId === options.gameStore.roomId && room.ownerId && options.session.loggedInUserId.value && room.ownerId === options.session.loggedInUserId.value, ) }) const allPlayersReady = computed( () => options.gamePlayers.value.length === 4 && options.gamePlayers.value.every((player) => Boolean(player.isReady)), ) const hasRoundStarted = computed(() => options.gamePlayers.value.some( (player) => player.handCount > 0 || player.handTiles.length > 0 || player.melds.length > 0 || player.discardTiles.length > 0, ), ) const showStartGameButton = computed( () => options.gameStore.phase === 'waiting' && allPlayersReady.value && !hasRoundStarted.value, ) const showWaitingOwnerTip = computed(() => showStartGameButton.value && !isRoomOwner.value) const canStartGame = computed( () => showStartGameButton.value && isRoomOwner.value && !options.session.startGamePending.value, ) const showReadyToggle = computed(() => { if (options.gameStore.phase !== 'waiting' || !options.gameStore.roomId || hasRoundStarted.value) { return false } if (showStartGameButton.value) { return !isRoomOwner.value } return true }) const showDingQueChooser = computed(() => { const player = options.myPlayer.value if (!player || options.gameStore.phase === 'settlement') { return false } return player.handTiles.length > 0 && !player.missingSuit }) const selectedDiscardTile = computed(() => { const player = options.myPlayer.value if (!player || options.session.selectedDiscardTileId.value === null) { return null } return player.handTiles.find((tile) => tile.id === options.session.selectedDiscardTileId.value) ?? null }) const hasMissingSuitTiles = computed(() => { const player = options.myPlayer.value const missingSuit = player?.missingSuit as Tile['suit'] | null | undefined if (!player || !missingSuit) { return false } return player.handTiles.some((tile) => tile.suit === missingSuit) }) function missingSuitLabel(value: string | null | undefined): string { const suitMap: Record = { w: '万', t: '筒', b: '条', wan: '万', tong: '筒', tiao: '条', } if (!value) { return '' } return suitMap[value.trim().toLowerCase()] ?? value } const discardBlockedReason = computed(() => { const player = options.myPlayer.value if (!player || !options.gameStore.roomId) { return '未进入房间' } if (options.session.wsStatus.value !== 'connected') { return 'WebSocket 未连接' } if (showDingQueChooser.value) { return '请先完成定缺' } if (options.gameStore.phase !== 'playing') { return '当前不是出牌阶段' } if (player.seatIndex !== options.gameStore.currentTurn) { return '未轮到你出牌' } if (options.gameStore.needDraw) { return '请先摸牌' } if (options.gameStore.pendingClaim) { return '等待当前操作结算' } if (player.handTiles.length === 0) { return '当前没有可出的手牌' } if (options.session.discardPending.value) { return '正在提交出牌' } return '' }) function discardTileBlockedReason(tile: Tile): string { if (discardBlockedReason.value) { return discardBlockedReason.value } const player = options.myPlayer.value const missingSuit = player?.missingSuit as Tile['suit'] | null | undefined if (player && missingSuit && hasMissingSuitTiles.value && tile.suit !== missingSuit) { return `当前必须先打${missingSuitLabel(missingSuit)}牌` } return '' } const canConfirmDiscard = computed(() => { const tile = selectedDiscardTile.value return Boolean(tile && !discardTileBlockedReason(tile)) }) const confirmDiscardLabel = computed(() => { const tile = selectedDiscardTile.value return tile ? `出牌 ${formatTile(tile)}` : '出牌' }) const canDrawTile = computed(() => { const player = options.myPlayer.value if (!player || !options.gameStore.roomId) { return false } return ( options.gameStore.phase === 'playing' && options.gameStore.needDraw && player.seatIndex === options.gameStore.currentTurn ) }) const myClaimState = computed(() => options.gameStore.pendingClaim) const visibleClaimOptions = computed(() => { const current = myClaimState.value?.options ?? [] const order: ClaimOptionState[] = ['hu', 'gang', 'peng', 'pass'] return order.filter((option) => current.includes(option)) }) const showClaimActions = computed(() => visibleClaimOptions.value.length > 0) const canSelfHu = computed(() => { const player = options.myPlayer.value if (!player || !options.gameStore.roomId || options.session.wsStatus.value !== 'connected') { return false } if ( showDingQueChooser.value || options.gameStore.phase !== 'playing' || options.gameStore.needDraw || options.gameStore.pendingClaim ) { return false } if (player.seatIndex !== options.gameStore.currentTurn || options.session.turnActionPending.value) { return false } return options.session.selfTurnAllowActions.value.includes('hu') }) const canSelfGang = computed(() => { const player = options.myPlayer.value if (!player || !options.gameStore.roomId || options.session.wsStatus.value !== 'connected') { return false } if ( showDingQueChooser.value || options.gameStore.phase !== 'playing' || options.gameStore.needDraw || options.gameStore.pendingClaim ) { return false } if (player.seatIndex !== options.gameStore.currentTurn || options.session.turnActionPending.value) { return false } return options.session.selfTurnAllowActions.value.includes('gang') }) function toggleReadyState(): void { if (options.session.readyTogglePending.value) { return } const nextReady = !myReadyState.value options.session.readyTogglePending.value = true sendWsMessage({ type: 'set_ready', roomId: options.gameStore.roomId, payload: { ready: nextReady, isReady: nextReady, }, }) } function startGame(): void { if (!canStartGame.value) { return } options.session.startGamePending.value = true sendWsMessage({ type: 'start_game', roomId: options.gameStore.roomId, payload: { room_id: options.gameStore.roomId, }, }) } function nextRound(): void { if ( options.session.nextRoundPending.value || !options.gameStore.roomId || options.gameStore.phase !== 'settlement' ) { return } options.session.settlementOverlayDismissed.value = true options.session.nextRoundPending.value = true sendWsMessage({ type: 'next_round', roomId: options.gameStore.roomId, payload: {}, }) } function chooseDingQue(suit: Tile['suit']): void { if (options.session.dingQuePending.value || !showDingQueChooser.value) { return } options.session.dingQuePending.value = true sendWsMessage({ type: 'ding_que', roomId: options.gameStore.roomId, payload: {suit}, }) } function selectDiscardTile(tile: Tile): void { const blockedReason = discardTileBlockedReason(tile) if (blockedReason) { options.session.wsError.value = blockedReason options.session.wsMessages.value.push(`[client-blocked] select ${formatTile(tile)}: ${blockedReason}`) options.session.selectedDiscardTileId.value = null return } options.session.wsError.value = '' options.session.selectedDiscardTileId.value = options.session.selectedDiscardTileId.value === tile.id ? null : tile.id } function confirmDiscard(): void { const tile = selectedDiscardTile.value if (!tile) { return } const blockedReason = discardTileBlockedReason(tile) if (blockedReason || !options.gameStore.roomId) { if (blockedReason) { options.session.wsError.value = blockedReason options.session.wsMessages.value.push(`[client-blocked] discard ${formatTile(tile)}: ${blockedReason}`) } return } options.session.wsError.value = '' options.session.markDiscardPendingWithFallback() sendWsMessage({ type: 'discard', roomId: options.gameStore.roomId, payload: { tile: { id: tile.id, suit: tile.suit, value: tile.value, }, }, }) } function drawTile(): void { if (!canDrawTile.value) { return } sendWsMessage({ type: 'draw', roomId: options.gameStore.roomId, payload: { room_id: options.gameStore.roomId, }, }) } function submitSelfGang(): void { if (!options.gameStore.roomId || !canSelfGang.value || options.session.turnActionPending.value) { return } options.session.markTurnActionPending('gang') sendWsMessage({ type: 'gang', roomId: options.gameStore.roomId, payload: { room_id: options.gameStore.roomId, }, }) } function submitSelfHu(): void { if (!options.gameStore.roomId || !canSelfHu.value || options.session.turnActionPending.value) { return } options.session.markTurnActionPending('hu') sendWsMessage({ type: 'hu', roomId: options.gameStore.roomId, payload: { room_id: options.gameStore.roomId, }, }) } function submitClaim(action: ClaimOptionState): void { if ( options.session.claimActionPending.value || !options.gameStore.roomId || !visibleClaimOptions.value.includes(action) ) { return } const claimTile = options.gameStore.pendingClaim?.tile options.session.claimActionPending.value = true sendWsMessage({ type: action, roomId: options.gameStore.roomId, payload: { room_id: options.gameStore.roomId, ...(action !== 'pass' && claimTile ? { tile: { id: claimTile.id, suit: claimTile.suit, value: claimTile.value, }, } : {}), }, }) } return { isLastRound, myReadyState, isRoomOwner, showStartGameButton, showWaitingOwnerTip, canStartGame, showReadyToggle, showDingQueChooser, selectedDiscardTile, discardBlockedReason, discardTileBlockedReason, canConfirmDiscard, confirmDiscardLabel, canDrawTile, visibleClaimOptions, showClaimActions, canSelfHu, canSelfGang, toggleReadyState, startGame, nextRound, chooseDingQue, selectDiscardTile, confirmDiscard, drawTile, submitSelfGang, submitSelfHu, submitClaim, } }