feat(game): 添加成都麻将游戏核心功能实现
- 定义游戏动作类型和载荷接口,包括摸牌、出牌、碰杠胡等操作 - 创建成都麻将游戏页面组件,集成桌面视图和玩家交互界面 - 实现游戏动作消息解析器,处理WebSocket消息转换为游戏动作 - 构建游戏状态管理store,管理玩家信息、回合状态和游戏流程 - 开发房间信息快照解析器,同步房间状态和玩家数据 - 实现房间状态快照解析,处理游戏阶段转换和玩家操作
This commit is contained in:
@@ -30,6 +30,22 @@ export interface RoomTrusteePayload {
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export interface DiscardActionPayload {
|
||||
player_id?: string
|
||||
playerId?: string
|
||||
PlayerID?: string
|
||||
tile?: Tile
|
||||
next_seat?: number
|
||||
nextSeat?: number
|
||||
}
|
||||
|
||||
export interface DrawActionPayload {
|
||||
player_id?: string
|
||||
playerId?: string
|
||||
PlayerID?: string
|
||||
tile?: Tile
|
||||
}
|
||||
|
||||
export interface PlayerTurnPayload {
|
||||
player_id?: string
|
||||
playerId?: string
|
||||
@@ -69,20 +85,13 @@ export type GameAction =
|
||||
// 摸牌
|
||||
| {
|
||||
type: 'DRAW_TILE'
|
||||
payload: {
|
||||
playerId: string
|
||||
tile: Tile
|
||||
}
|
||||
payload: DrawActionPayload
|
||||
}
|
||||
|
||||
// 出牌
|
||||
| {
|
||||
type: 'PLAY_TILE'
|
||||
payload: {
|
||||
playerId: string
|
||||
tile: Tile
|
||||
nextSeat: number
|
||||
}
|
||||
payload: DiscardActionPayload
|
||||
}
|
||||
|
||||
// 进入操作窗口(碰/杠/胡)
|
||||
|
||||
@@ -70,13 +70,21 @@ export const useGameStore = defineStore('game', {
|
||||
},
|
||||
|
||||
// 摸牌
|
||||
onDrawTile(data: { playerId: string; tile: Tile }) {
|
||||
const player = this.players[data.playerId]
|
||||
onDrawTile(data: { playerId?: string; player_id?: string; PlayerID?: string; tile?: Tile }) {
|
||||
const playerId =
|
||||
(typeof data.playerId === 'string' && data.playerId) ||
|
||||
(typeof data.player_id === 'string' && data.player_id) ||
|
||||
(typeof data.PlayerID === 'string' && data.PlayerID) ||
|
||||
''
|
||||
const tile = data.tile
|
||||
if (!playerId || !tile) return
|
||||
|
||||
const player = this.players[playerId]
|
||||
if (!player) return
|
||||
|
||||
// 只更新自己的手牌
|
||||
if (player.playerId === this.getMyPlayerId()) {
|
||||
player.handTiles.push(data.tile)
|
||||
player.handTiles.push(tile)
|
||||
}
|
||||
player.handCount += 1
|
||||
|
||||
@@ -97,17 +105,28 @@ export const useGameStore = defineStore('game', {
|
||||
|
||||
// 出牌
|
||||
onPlayTile(data: {
|
||||
playerId: string
|
||||
tile: Tile
|
||||
nextSeat: number
|
||||
playerId?: string
|
||||
player_id?: string
|
||||
PlayerID?: string
|
||||
tile?: Tile
|
||||
nextSeat?: number
|
||||
next_seat?: number
|
||||
}) {
|
||||
const player = this.players[data.playerId]
|
||||
const playerId =
|
||||
(typeof data.playerId === 'string' && data.playerId) ||
|
||||
(typeof data.player_id === 'string' && data.player_id) ||
|
||||
(typeof data.PlayerID === 'string' && data.PlayerID) ||
|
||||
''
|
||||
const tile = data.tile
|
||||
if (!playerId || !tile) return
|
||||
|
||||
const player = this.players[playerId]
|
||||
if (!player) return
|
||||
|
||||
// 如果是自己,移除手牌
|
||||
if (player.playerId === this.getMyPlayerId()) {
|
||||
const index = player.handTiles.findIndex(
|
||||
(t) => t.id === data.tile.id
|
||||
(t) => t.id === tile.id
|
||||
)
|
||||
if (index !== -1) {
|
||||
player.handTiles.splice(index, 1)
|
||||
@@ -116,10 +135,16 @@ export const useGameStore = defineStore('game', {
|
||||
player.handCount = Math.max(0, player.handCount - 1)
|
||||
|
||||
// 加入出牌<E587BA>?
|
||||
player.discardTiles.push(data.tile)
|
||||
player.discardTiles.push(tile)
|
||||
|
||||
// 更新回合
|
||||
this.currentTurn = data.nextSeat
|
||||
const nextSeat =
|
||||
typeof data.nextSeat === 'number'
|
||||
? data.nextSeat
|
||||
: typeof data.next_seat === 'number'
|
||||
? data.next_seat
|
||||
: this.currentTurn
|
||||
this.currentTurn = nextSeat
|
||||
this.needDraw = true
|
||||
|
||||
// 等待其他玩家响应
|
||||
@@ -297,4 +322,3 @@ export const useGameStore = defineStore('game', {
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -34,20 +34,55 @@ const session = useChengduGameSession({
|
||||
gameStore,
|
||||
roomMeta,
|
||||
})
|
||||
const {
|
||||
now,
|
||||
wsStatus,
|
||||
wsError,
|
||||
roomCountdown,
|
||||
leaveRoomPending,
|
||||
readyTogglePending,
|
||||
dingQuePending,
|
||||
discardPending,
|
||||
claimActionPending,
|
||||
turnActionPending,
|
||||
nextRoundPending,
|
||||
selectedDiscardTileId,
|
||||
menuOpen,
|
||||
isTrustMode,
|
||||
menuTriggerActive,
|
||||
loggedInUserId,
|
||||
networkLabel,
|
||||
formattedClock,
|
||||
toggleMenu,
|
||||
toggleTrustMode,
|
||||
backHall,
|
||||
} = session
|
||||
|
||||
const routeRoomName = computed(() => (typeof route.query.roomName === 'string' ? route.query.roomName : ''))
|
||||
const myPlayer = computed(() => gameStore.players[session.loggedInUserId.value] as DisplayPlayer | undefined)
|
||||
const myPlayer = computed(() => gameStore.players[loggedInUserId.value] as DisplayPlayer | undefined)
|
||||
const myHandTiles = computed(() => myPlayer.value?.handTiles ?? [])
|
||||
const gamePlayers = computed<DisplayPlayer[]>(() =>
|
||||
Object.values(gameStore.players).sort((a, b) => a.seatIndex - b.seatIndex) as DisplayPlayer[],
|
||||
)
|
||||
|
||||
const tableView = useChengduTableView({
|
||||
const {
|
||||
roomName,
|
||||
roomState,
|
||||
seatWinds,
|
||||
currentTurnSeat,
|
||||
currentPhaseText,
|
||||
roomStatusText,
|
||||
roundText,
|
||||
wallSeats,
|
||||
deskSeats,
|
||||
seatDecor,
|
||||
settlementPlayers,
|
||||
} = useChengduTableView({
|
||||
roomMeta,
|
||||
gamePlayers,
|
||||
gameStore,
|
||||
localCachedAvatarUrl: session.localCachedAvatarUrl,
|
||||
loggedInUserId: session.loggedInUserId,
|
||||
loggedInUserId,
|
||||
loggedInUserName: session.loggedInUserName,
|
||||
myHandTiles,
|
||||
myPlayer,
|
||||
@@ -59,13 +94,43 @@ const socket = useChengduGameSocket({
|
||||
router,
|
||||
gameStore,
|
||||
roomMeta,
|
||||
roomName: tableView.roomName,
|
||||
roomName,
|
||||
myHandTiles,
|
||||
myPlayer,
|
||||
session,
|
||||
})
|
||||
const { showSettlementOverlay, settlementCountdown } = socket
|
||||
|
||||
const actions = useChengduGameActions({
|
||||
const {
|
||||
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,
|
||||
} = useChengduGameActions({
|
||||
gameStore,
|
||||
roomMeta,
|
||||
gamePlayers,
|
||||
@@ -74,7 +139,7 @@ const actions = useChengduGameActions({
|
||||
})
|
||||
|
||||
const actionCountdown = computed<ActionCountdownView | null>(() => {
|
||||
const countdown = session.roomCountdown.value
|
||||
const countdown = roomCountdown.value
|
||||
if (!countdown) {
|
||||
return null
|
||||
}
|
||||
@@ -82,7 +147,7 @@ const actionCountdown = computed<ActionCountdownView | null>(() => {
|
||||
const deadlineAt = countdown.actionDeadlineAt ? Date.parse(countdown.actionDeadlineAt) : Number.NaN
|
||||
const fallbackRemaining = countdown.remaining > 0 ? countdown.remaining : countdown.countdownSeconds
|
||||
const derivedRemaining = Number.isFinite(deadlineAt)
|
||||
? Math.ceil((deadlineAt - session.now.value) / 1000)
|
||||
? Math.ceil((deadlineAt - now.value) / 1000)
|
||||
: fallbackRemaining
|
||||
const remaining = Math.max(0, derivedRemaining)
|
||||
|
||||
@@ -97,7 +162,7 @@ const actionCountdown = computed<ActionCountdownView | null>(() => {
|
||||
|
||||
const playerLabel = targetPlayerIds
|
||||
.map((playerId) => {
|
||||
if (playerId === session.loggedInUserId.value) {
|
||||
if (playerId === loggedInUserId.value) {
|
||||
return '你'
|
||||
}
|
||||
const targetPlayer = gameStore.players[playerId]
|
||||
@@ -111,7 +176,7 @@ const actionCountdown = computed<ActionCountdownView | null>(() => {
|
||||
})
|
||||
.join('、')
|
||||
const duration = countdown.duration > 0 ? countdown.duration : Math.max(remaining, fallbackRemaining, 1)
|
||||
const includesSelf = targetPlayerIds.includes(session.loggedInUserId.value)
|
||||
const includesSelf = targetPlayerIds.includes(loggedInUserId.value)
|
||||
|
||||
return {
|
||||
playerLabel,
|
||||
@@ -123,8 +188,8 @@ const actionCountdown = computed<ActionCountdownView | null>(() => {
|
||||
})
|
||||
|
||||
function handleLeaveRoom(): void {
|
||||
session.menuOpen.value = false
|
||||
session.backHall()
|
||||
menuOpen.value = false
|
||||
backHall()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -140,24 +205,24 @@ function handleLeaveRoom(): void {
|
||||
<div class="inner-outline mid"></div>
|
||||
|
||||
<ChengduTableHeader
|
||||
:leave-room-pending="session.leaveRoomPending"
|
||||
:menu-open="session.menuOpen"
|
||||
:menu-trigger-active="session.menuTriggerActive"
|
||||
:is-trust-mode="session.isTrustMode"
|
||||
:wall-count="tableView.roomState.game.state.wall.length || 48"
|
||||
:network-label="session.networkLabel"
|
||||
:ws-status="session.wsStatus"
|
||||
:formatted-clock="session.formattedClock"
|
||||
:room-name="tableView.roomState.name || tableView.roomName"
|
||||
:current-phase-text="tableView.currentPhaseText"
|
||||
:player-count="tableView.roomState.playerCount"
|
||||
:max-players="tableView.roomState.maxPlayers"
|
||||
:round-text="tableView.roundText"
|
||||
:room-status-text="tableView.roomStatusText"
|
||||
:ws-error="session.wsError"
|
||||
:leave-room-pending="leaveRoomPending"
|
||||
:menu-open="menuOpen"
|
||||
:menu-trigger-active="menuTriggerActive"
|
||||
:is-trust-mode="isTrustMode"
|
||||
:wall-count="roomState.game.state.wall.length || 48"
|
||||
:network-label="networkLabel"
|
||||
:ws-status="wsStatus"
|
||||
:formatted-clock="formattedClock"
|
||||
:room-name="roomState.name || roomName"
|
||||
:current-phase-text="currentPhaseText"
|
||||
:player-count="roomState.playerCount"
|
||||
:max-players="roomState.maxPlayers"
|
||||
:round-text="roundText"
|
||||
:room-status-text="roomStatusText"
|
||||
:ws-error="wsError"
|
||||
:action-countdown="actionCountdown"
|
||||
@toggle-menu="session.toggleMenu"
|
||||
@toggle-trust-mode="session.toggleTrustMode"
|
||||
@toggle-menu="toggleMenu"
|
||||
@toggle-trust-mode="toggleTrustMode"
|
||||
@leave-room="handleLeaveRoom"
|
||||
>
|
||||
<template #robot-icon>
|
||||
@@ -172,67 +237,67 @@ function handleLeaveRoom(): void {
|
||||
</template>
|
||||
</ChengduTableHeader>
|
||||
|
||||
<TopPlayerCard :player="tableView.seatDecor.top"/>
|
||||
<RightPlayerCard :player="tableView.seatDecor.right"/>
|
||||
<BottomPlayerCard :player="tableView.seatDecor.bottom"/>
|
||||
<LeftPlayerCard :player="tableView.seatDecor.left"/>
|
||||
<TopPlayerCard :player="seatDecor.top"/>
|
||||
<RightPlayerCard :player="seatDecor.right"/>
|
||||
<BottomPlayerCard :player="seatDecor.bottom"/>
|
||||
<LeftPlayerCard :player="seatDecor.left"/>
|
||||
|
||||
<ChengduDeskZones :desk-seats="tableView.deskSeats"/>
|
||||
<ChengduDeskZones :desk-seats="deskSeats"/>
|
||||
|
||||
<ChengduWallSeats
|
||||
:wall-seats="tableView.wallSeats"
|
||||
:selected-discard-tile-id="session.selectedDiscardTileId"
|
||||
:wall-seats="wallSeats"
|
||||
:selected-discard-tile-id="selectedDiscardTileId"
|
||||
:discard-blocked-reason="discardBlockedReason"
|
||||
:discard-tile-blocked-reason="discardTileBlockedReason"
|
||||
:format-tile="formatTile"
|
||||
@select-discard-tile="selectDiscardTile"
|
||||
/>
|
||||
|
||||
<WindSquare class="center-wind-square" :seat-winds="tableView.seatWinds"
|
||||
:active-position="tableView.currentTurnSeat"/>
|
||||
<WindSquare class="center-wind-square" :seat-winds="seatWinds"
|
||||
:active-position="currentTurnSeat"/>
|
||||
|
||||
<ChengduSettlementOverlay
|
||||
:show="socket.showSettlementOverlay"
|
||||
:is-last-round="actions.isLastRound"
|
||||
:show="showSettlementOverlay"
|
||||
:is-last-round="isLastRound"
|
||||
:current-round="gameStore.currentRound"
|
||||
:total-rounds="gameStore.totalRounds"
|
||||
:settlement-players="tableView.settlementPlayers"
|
||||
:logged-in-user-id="session.loggedInUserId"
|
||||
:next-round-pending="session.nextRoundPending"
|
||||
:settlement-countdown="socket.settlementCountdown"
|
||||
@next-round="actions.nextRound"
|
||||
@back-hall="session.backHall"
|
||||
:settlement-players="settlementPlayers"
|
||||
:logged-in-user-id="loggedInUserId"
|
||||
:next-round-pending="nextRoundPending"
|
||||
:settlement-countdown="settlementCountdown"
|
||||
@next-round="nextRound"
|
||||
@back-hall="backHall"
|
||||
/>
|
||||
|
||||
<ChengduBottomActions
|
||||
:show-ding-que-chooser="actions.showDingQueChooser"
|
||||
:show-ready-toggle="actions.showReadyToggle"
|
||||
:show-start-game-button="actions.showStartGameButton"
|
||||
:selected-discard-tile="actions.selectedDiscardTile"
|
||||
:ding-que-pending="session.dingQuePending"
|
||||
:can-confirm-discard="actions.canConfirmDiscard"
|
||||
:discard-pending="session.discardPending"
|
||||
:confirm-discard-label="actions.confirmDiscardLabel"
|
||||
:ready-toggle-pending="session.readyTogglePending"
|
||||
:my-ready-state="actions.myReadyState"
|
||||
:can-draw-tile="actions.canDrawTile"
|
||||
:can-start-game="actions.canStartGame"
|
||||
:is-room-owner="actions.isRoomOwner"
|
||||
:can-self-gang="actions.canSelfGang"
|
||||
:can-self-hu="actions.canSelfHu"
|
||||
:show-claim-actions="actions.showClaimActions"
|
||||
:turn-action-pending="session.turnActionPending"
|
||||
:visible-claim-options="actions.visibleClaimOptions"
|
||||
:claim-action-pending="session.claimActionPending"
|
||||
:show-waiting-owner-tip="actions.showWaitingOwnerTip"
|
||||
@choose-ding-que="actions.chooseDingQue"
|
||||
@confirm-discard="actions.confirmDiscard"
|
||||
@toggle-ready-state="actions.toggleReadyState"
|
||||
@draw-tile="actions.drawTile"
|
||||
@start-game="actions.startGame"
|
||||
@submit-self-gang="actions.submitSelfGang"
|
||||
@submit-self-hu="actions.submitSelfHu"
|
||||
@submit-claim="actions.submitClaim"
|
||||
:show-ding-que-chooser="showDingQueChooser"
|
||||
:show-ready-toggle="showReadyToggle"
|
||||
:show-start-game-button="showStartGameButton"
|
||||
:selected-discard-tile="selectedDiscardTile"
|
||||
:ding-que-pending="dingQuePending"
|
||||
:can-confirm-discard="canConfirmDiscard"
|
||||
:discard-pending="discardPending"
|
||||
:confirm-discard-label="confirmDiscardLabel"
|
||||
:ready-toggle-pending="readyTogglePending"
|
||||
:my-ready-state="myReadyState"
|
||||
:can-draw-tile="canDrawTile"
|
||||
:can-start-game="canStartGame"
|
||||
:is-room-owner="isRoomOwner"
|
||||
:can-self-gang="canSelfGang"
|
||||
:can-self-hu="canSelfHu"
|
||||
:show-claim-actions="showClaimActions"
|
||||
:turn-action-pending="turnActionPending"
|
||||
:visible-claim-options="visibleClaimOptions"
|
||||
:claim-action-pending="claimActionPending"
|
||||
:show-waiting-owner-tip="showWaitingOwnerTip"
|
||||
@choose-ding-que="chooseDingQue"
|
||||
@confirm-discard="confirmDiscard"
|
||||
@toggle-ready-state="toggleReadyState"
|
||||
@draw-tile="drawTile"
|
||||
@start-game="startGame"
|
||||
@submit-self-gang="submitSelfGang"
|
||||
@submit-self-hu="submitSelfHu"
|
||||
@submit-claim="submitClaim"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,10 +3,36 @@ import {
|
||||
normalizeWsType,
|
||||
readString,
|
||||
} from '../../../../game/chengdu/messageNormalizers'
|
||||
import { clearClaimAndTurnPending, pushWsMessage, setWsError } from '../session/sessionStateAdapter'
|
||||
import { clearClaimAndTurnPending, clearRoomCountdown, pushWsMessage, setWsError } from '../session/sessionStateAdapter'
|
||||
import type { SocketHandlerContext, StatusHandlerApi } from '../types'
|
||||
|
||||
export function createStatusHandlers(context: SocketHandlerContext): StatusHandlerApi {
|
||||
function handleActionAck(message: unknown): void {
|
||||
const source = asRecord(message)
|
||||
if (!source || typeof source.type !== 'string' || normalizeWsType(source.type) !== 'ACTION_ACK') {
|
||||
return
|
||||
}
|
||||
|
||||
const payload = asRecord(source.payload)
|
||||
const roomId = readString(payload ?? {}, 'room_id', 'roomId') || readString(source, 'roomId')
|
||||
if (roomId && context.gameStore.roomId && roomId !== context.gameStore.roomId) {
|
||||
return
|
||||
}
|
||||
|
||||
const action = normalizeWsType(readString(payload ?? {}, 'action') || readString(source, 'action'))
|
||||
if (
|
||||
action === 'DISCARD' ||
|
||||
action === 'DRAW' ||
|
||||
action === 'PENG' ||
|
||||
action === 'GANG' ||
|
||||
action === 'HU' ||
|
||||
action === 'PASS' ||
|
||||
action === 'DING_QUE'
|
||||
) {
|
||||
clearRoomCountdown(context.session)
|
||||
}
|
||||
}
|
||||
|
||||
function handleActionError(message: unknown): void {
|
||||
const source = asRecord(message)
|
||||
if (!source || typeof source.type !== 'string' || normalizeWsType(source.type) !== 'ACTION_ERROR') {
|
||||
@@ -27,6 +53,7 @@ export function createStatusHandlers(context: SocketHandlerContext): StatusHandl
|
||||
}
|
||||
|
||||
return {
|
||||
handleActionAck,
|
||||
handleActionError,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,46 @@ import { resetRoundResolutionState } from '../store/gameStoreAdapter'
|
||||
import type { SocketHandlerContext, TurnHandlerApi, TurnPayloadRecord } from '../types'
|
||||
|
||||
export function createTurnHandlers(context: SocketHandlerContext): TurnHandlerApi {
|
||||
function handlePlayerAllowAction(message: unknown): void {
|
||||
const source = asRecord(message)
|
||||
if (!source || typeof source.type !== 'string' || normalizeWsType(source.type) !== 'PLAYER_ALLOW_ACTION') {
|
||||
return
|
||||
}
|
||||
|
||||
const payload = asRecord(source.payload) ?? source
|
||||
const roomId = readString(payload, 'room_id', 'roomId') || readString(source, 'roomId', 'room_id')
|
||||
if (roomId && context.gameStore.roomId && roomId !== context.gameStore.roomId) {
|
||||
return
|
||||
}
|
||||
|
||||
const playerId = readPlayerTurnPlayerId(payload)
|
||||
const timeout =
|
||||
readNumber(payload, 'timeout', 'Timeout') ??
|
||||
readNumber(source, 'timeout', 'Timeout') ??
|
||||
0
|
||||
const startAtRaw =
|
||||
readNumber(payload, 'start_at', 'startAt', 'StartAt') ??
|
||||
readNumber(source, 'start_at', 'startAt', 'StartAt')
|
||||
|
||||
if (!playerId || timeout <= 0) {
|
||||
clearRoomCountdown(context.session)
|
||||
return
|
||||
}
|
||||
|
||||
const startAtMs = normalizeTimestampMs(startAtRaw)
|
||||
const deadlineAtMs = startAtMs !== null ? startAtMs + timeout * 1000 : null
|
||||
const remaining =
|
||||
deadlineAtMs !== null ? Math.max(0, Math.ceil((deadlineAtMs - context.session.now.value) / 1000)) : timeout
|
||||
|
||||
setRoomCountdown(context.session, {
|
||||
playerIds: [playerId],
|
||||
actionDeadlineAt: deadlineAtMs !== null ? new Date(deadlineAtMs).toISOString() : null,
|
||||
countdownSeconds: timeout,
|
||||
duration: timeout,
|
||||
remaining,
|
||||
})
|
||||
}
|
||||
|
||||
function handleDingQueCountdown(message: unknown): void {
|
||||
const source = asRecord(message)
|
||||
if (!source || typeof source.type !== 'string' || normalizeWsType(source.type) !== 'DING_QUE_COUNTDOWN') {
|
||||
@@ -151,6 +191,7 @@ export function createTurnHandlers(context: SocketHandlerContext): TurnHandlerAp
|
||||
|
||||
return {
|
||||
handleDingQueCountdown,
|
||||
handlePlayerAllowAction,
|
||||
handlePlayerTurn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GameAction, PlayerTurnPayload, RoomTrusteePayload } from '../../../../game/actions'
|
||||
import type { DiscardActionPayload, DrawActionPayload, GameAction, PlayerTurnPayload, RoomTrusteePayload } from '../../../../game/actions'
|
||||
import type { ClaimOptionState } from '../../../../types/state'
|
||||
import { asRecord, normalizeWsType, readString } from '../../../../game/chengdu/messageNormalizers'
|
||||
import { asRecord, normalizeTile, normalizeWsType, readNumber, readString } from '../../../../game/chengdu/messageNormalizers'
|
||||
|
||||
export function parseGameActionMessage(message: unknown): GameAction | null {
|
||||
if (!message || typeof message !== 'object') {
|
||||
@@ -29,9 +29,46 @@ export function parseGameActionMessage(message: unknown): GameAction | null {
|
||||
? ({ type: 'ROOM_PLAYER_UPDATE', payload } as GameAction)
|
||||
: null
|
||||
case 'ROOM_TRUSTEE':
|
||||
case 'PLAYER_TRUSTEE':
|
||||
return payload && typeof payload === 'object'
|
||||
? ({ type: 'ROOM_TRUSTEE', payload } as GameAction)
|
||||
: ({ type: 'ROOM_TRUSTEE', payload: source as unknown as RoomTrusteePayload } as GameAction)
|
||||
case 'DRAW': {
|
||||
const resolvedPayload = asRecord(payload)
|
||||
const playerId =
|
||||
readString(resolvedPayload ?? {}, 'player_id', 'playerId', 'PlayerID') ||
|
||||
readString(source, 'target')
|
||||
if (!playerId) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
type: 'DRAW_TILE',
|
||||
payload: {
|
||||
...(resolvedPayload as DrawActionPayload | null),
|
||||
player_id: playerId,
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'DISCARD': {
|
||||
const resolvedPayload = asRecord(payload)
|
||||
const playerId =
|
||||
readString(resolvedPayload ?? {}, 'player_id', 'playerId', 'PlayerID') ||
|
||||
readString(source, 'target')
|
||||
const tile = normalizeTile(resolvedPayload?.tile)
|
||||
if (!playerId || !tile) {
|
||||
return null
|
||||
}
|
||||
const nextSeat = readNumber(resolvedPayload ?? {}, 'next_seat', 'nextSeat')
|
||||
return {
|
||||
type: 'PLAY_TILE',
|
||||
payload: {
|
||||
...(resolvedPayload as DiscardActionPayload | null),
|
||||
player_id: playerId,
|
||||
tile,
|
||||
...(typeof nextSeat === 'number' ? { next_seat: nextSeat } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'PLAYER_TURN':
|
||||
case 'NEXT_TURN':
|
||||
return payload && typeof payload === 'object'
|
||||
|
||||
@@ -237,7 +237,10 @@ export function parseRoomInfoSnapshot(
|
||||
readString(room ?? {}, 'status') ||
|
||||
readString(gameState ?? {}, 'phase') ||
|
||||
'waiting'
|
||||
const phase = readString(gameState ?? {}, 'phase') || readString(room ?? {}, 'status') || 'waiting'
|
||||
const rawPendingClaim = asRecord(gameState?.pending_claim ?? gameState?.pendingClaim)
|
||||
const hasPendingClaimWindow = Boolean(rawPendingClaim && Object.keys(rawPendingClaim).length > 0)
|
||||
const phase =
|
||||
hasPendingClaimWindow ? 'action' : readString(gameState ?? {}, 'phase') || readString(room ?? {}, 'status') || 'waiting'
|
||||
const wallCount = readNumber(gameState ?? {}, 'wall_count', 'wallCount')
|
||||
const dealerIndex = readNumber(gameState ?? {}, 'dealer_index', 'dealerIndex')
|
||||
const currentTurnSeat = readNumber(gameState ?? {}, 'current_turn', 'currentTurn')
|
||||
|
||||
@@ -79,7 +79,10 @@ export function parseRoomStateSnapshot(
|
||||
}
|
||||
})
|
||||
|
||||
const phase = readString(payload, 'phase') || readString(payload, 'status') || 'waiting'
|
||||
const rawPendingClaim = asRecord(payload.pending_claim ?? payload.pendingClaim)
|
||||
const hasPendingClaimWindow = Boolean(rawPendingClaim && Object.keys(rawPendingClaim).length > 0)
|
||||
const phase =
|
||||
hasPendingClaimWindow ? 'action' : readString(payload, 'phase') || readString(payload, 'status') || 'waiting'
|
||||
const wallCount = readNumber(payload, 'wall_count', 'wallCount')
|
||||
const currentTurnSeat = readNumber(payload, 'current_turn', 'currentTurn')
|
||||
const currentTurnPlayerId = readString(payload, 'current_turn_player', 'currentTurnPlayer') || ''
|
||||
|
||||
@@ -15,8 +15,10 @@ export function createSocketMessageRouter(deps: SocketMessageRouterDeps) {
|
||||
['ROOM_INFO', [deps.roomHandlers.handleRoomInfoResponse]],
|
||||
['ROOM_STATE', [deps.roomHandlers.handleRoomStateResponse]],
|
||||
['PLAYER_HAND', [deps.playerHandlers.handlePlayerHandResponse]],
|
||||
['PLAYER_ALLOW_ACTION', [deps.turnHandlers.handlePlayerAllowAction]],
|
||||
['PLAYER_TURN', [deps.turnHandlers.handlePlayerTurn]],
|
||||
['NEXT_TURN', [deps.turnHandlers.handlePlayerTurn]],
|
||||
['ACTION_ACK', [deps.statusHandlers.handleActionAck]],
|
||||
['ACTION_ERROR', [deps.statusHandlers.handleActionError]],
|
||||
['DING_QUE_COUNTDOWN', [deps.turnHandlers.handleDingQueCountdown]],
|
||||
['PLAYER_READY', [deps.playerHandlers.handleReadyStateResponse]],
|
||||
|
||||
@@ -51,10 +51,12 @@ export interface PlayerHandlerApi extends ReadyStateApi {
|
||||
|
||||
export interface TurnHandlerApi {
|
||||
handleDingQueCountdown: (message: unknown) => void
|
||||
handlePlayerAllowAction: (message: unknown) => void
|
||||
handlePlayerTurn: (message: unknown) => void
|
||||
}
|
||||
|
||||
export interface StatusHandlerApi {
|
||||
handleActionAck: (message: unknown) => void
|
||||
handleActionError: (message: unknown) => void
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user