feat(ws): 添加WebSocket客户端及房间状态处理功能

- 实现WebSocket客户端类,支持连接、发送消息、状态监听和自动重连
- 添加房间信息和状态的消息处理器,用于同步游戏状态和玩家数据
- 实现房间快照解析功能,处理玩家信息、游戏阶段、计时器等数据结构
- 集成会话状态适配器,管理房间倒计时、结算截止时间等状态变更
- 添加单例WebSocket客户端实例,统一管理WebSocket连接生命周期
This commit is contained in:
2026-04-06 15:13:43 +08:00
parent cfc65070ea
commit 3c876c4c3d
6 changed files with 50 additions and 0 deletions

View File

@@ -3,9 +3,11 @@ import { parseRoomInfoSnapshot } from '../parsers/roomInfoSnapshot'
import { clearRoomAndRedirect, syncActiveRoomFromRoomInfo } from '../room/roomSnapshotSync' import { clearRoomAndRedirect, syncActiveRoomFromRoomInfo } from '../room/roomSnapshotSync'
import { import {
clearClaimAndTurnPending, clearClaimAndTurnPending,
clearRoomCountdown,
clearDingQuePending, clearDingQuePending,
clearSelfTurnAllowActions, clearSelfTurnAllowActions,
clearTurnPending, clearTurnPending,
setRoomCountdown,
setSettlementDeadline, setSettlementDeadline,
syncCurrentUserId, syncCurrentUserId,
} from '../session/sessionStateAdapter' } from '../session/sessionStateAdapter'
@@ -69,6 +71,11 @@ export function createRoomInfoHandlers(context: SocketHandlerContext) {
} else { } else {
clearTurnPending(context.session) clearTurnPending(context.session)
} }
if (snapshot.actionTimer) {
setRoomCountdown(context.session, snapshot.actionTimer)
} else {
clearRoomCountdown(context.session)
}
if (typeof snapshot.settlementDeadlineMs === 'number' && snapshot.settlementDeadlineMs > 0) { if (typeof snapshot.settlementDeadlineMs === 'number' && snapshot.settlementDeadlineMs > 0) {
setSettlementDeadline(context.session, snapshot.settlementDeadlineMs) setSettlementDeadline(context.session, snapshot.settlementDeadlineMs)
} }

View File

@@ -8,11 +8,13 @@ import { parseRoomStateSnapshot } from '../parsers/roomStateSnapshot'
import { syncActiveRoomFromRoomState } from '../room/roomSnapshotSync' import { syncActiveRoomFromRoomState } from '../room/roomSnapshotSync'
import { import {
clearClaimAndTurnPending, clearClaimAndTurnPending,
clearRoomCountdown,
clearSelfTurnAllowActions, clearSelfTurnAllowActions,
clearStartGamePending, clearStartGamePending,
clearTurnPending, clearTurnPending,
completeDiscard, completeDiscard,
resetSettlementOverlayState, resetSettlementOverlayState,
setRoomCountdown,
setSettlementDeadline, setSettlementDeadline,
} from '../session/sessionStateAdapter' } from '../session/sessionStateAdapter'
import { applyRoomSnapshot } from '../store/gameStoreAdapter' import { applyRoomSnapshot } from '../store/gameStoreAdapter'
@@ -61,6 +63,11 @@ export function createRoomStateHandlers(context: SocketHandlerContext) {
} else if (snapshot.phase !== 'settlement') { } else if (snapshot.phase !== 'settlement') {
setSettlementDeadline(context.session, null) setSettlementDeadline(context.session, null)
} }
if (snapshot.actionTimer) {
setRoomCountdown(context.session, snapshot.actionTimer)
} else {
clearRoomCountdown(context.session)
}
if (!snapshot.pendingClaim) { if (!snapshot.pendingClaim) {
clearClaimAndTurnPending(context.session) clearClaimAndTurnPending(context.session)

View File

@@ -0,0 +1,27 @@
import { asRecord, readNumber, readString, readStringArray } from '../../../../game/chengdu/messageNormalizers'
import type { PlayerActionTimer } from '../../types'
export function parseActionTimerSnapshot(source: unknown): PlayerActionTimer | null {
const timer = asRecord(source)
if (!timer) {
return null
}
const playerIds = readStringArray(timer, 'player_ids', 'playerIds', 'PlayerIDs')
const countdownSeconds = readNumber(timer, 'countdown_seconds', 'countdownSeconds', 'CountdownSeconds') ?? 0
const duration = readNumber(timer, 'duration', 'Duration') ?? countdownSeconds
const remaining = readNumber(timer, 'remaining', 'Remaining') ?? countdownSeconds
const actionDeadlineAt = readString(timer, 'action_deadline_at', 'actionDeadlineAt', 'ActionDeadlineAt') || null
if (playerIds.length === 0 && countdownSeconds <= 0 && remaining <= 0 && !actionDeadlineAt) {
return null
}
return {
playerIds,
actionDeadlineAt,
countdownSeconds,
duration,
remaining,
}
}

View File

@@ -13,6 +13,8 @@ import {
} from '../../../../game/chengdu/messageNormalizers' } from '../../../../game/chengdu/messageNormalizers'
import type { RoomMetaSnapshotState } from '../../../../store/state' import type { RoomMetaSnapshotState } from '../../../../store/state'
import type { PendingClaimState, PlayerState } from '../../../../types/state' import type { PendingClaimState, PlayerState } from '../../../../types/state'
import type { PlayerActionTimer } from '../../types'
import { parseActionTimerSnapshot } from './actionTimerSnapshot'
interface RoomInfoSnapshotPlayerPair { interface RoomInfoSnapshotPlayerPair {
roomPlayer: RoomMetaSnapshotState['players'][number] roomPlayer: RoomMetaSnapshotState['players'][number]
@@ -39,6 +41,7 @@ export interface ParsedRoomInfoSnapshot {
currentRound: number | null currentRound: number | null
totalRounds: number | null totalRounds: number | null
settlementDeadlineMs: number | null settlementDeadlineMs: number | null
actionTimer: PlayerActionTimer | null
} }
interface ParseRoomInfoSnapshotOptions { interface ParseRoomInfoSnapshotOptions {
@@ -273,5 +276,6 @@ export function parseRoomInfoSnapshot(
currentRound: readNumber(gameState ?? {}, 'current_round', 'currentRound'), currentRound: readNumber(gameState ?? {}, 'current_round', 'currentRound'),
totalRounds: readNumber(gameState ?? {}, 'total_rounds', 'totalRounds'), totalRounds: readNumber(gameState ?? {}, 'total_rounds', 'totalRounds'),
settlementDeadlineMs: readNumber(gameState ?? {}, 'settlement_deadline_ms', 'settlementDeadlineMs'), settlementDeadlineMs: readNumber(gameState ?? {}, 'settlement_deadline_ms', 'settlementDeadlineMs'),
actionTimer: parseActionTimerSnapshot(gameState?.action_timer ?? gameState?.actionTimer),
} }
} }

View File

@@ -10,6 +10,8 @@ import {
readStringArray, readStringArray,
} from '../../../../game/chengdu/messageNormalizers' } from '../../../../game/chengdu/messageNormalizers'
import type { PendingClaimState, PlayerState } from '../../../../types/state' import type { PendingClaimState, PlayerState } from '../../../../types/state'
import type { PlayerActionTimer } from '../../types'
import { parseActionTimerSnapshot } from './actionTimerSnapshot'
export interface ParsedRoomStateSnapshot { export interface ParsedRoomStateSnapshot {
roomId: string roomId: string
@@ -25,6 +27,7 @@ export interface ParsedRoomStateSnapshot {
currentRound: number | null currentRound: number | null
totalRounds: number | null totalRounds: number | null
settlementDeadlineMs: number | null settlementDeadlineMs: number | null
actionTimer: PlayerActionTimer | null
} }
interface ParseRoomStateSnapshotOptions { interface ParseRoomStateSnapshotOptions {
@@ -108,5 +111,6 @@ export function parseRoomStateSnapshot(
currentRound: readNumber(payload, 'current_round', 'currentRound'), currentRound: readNumber(payload, 'current_round', 'currentRound'),
totalRounds: readNumber(payload, 'total_rounds', 'totalRounds'), totalRounds: readNumber(payload, 'total_rounds', 'totalRounds'),
settlementDeadlineMs: readNumber(payload, 'settlement_deadline_ms', 'settlementDeadlineMs'), settlementDeadlineMs: readNumber(payload, 'settlement_deadline_ms', 'settlementDeadlineMs'),
actionTimer: parseActionTimerSnapshot(payload.action_timer ?? payload.actionTimer),
} }
} }

View File

@@ -134,6 +134,7 @@ class WsClient {
// 订阅状态变化 // 订阅状态变化
onStatusChange(handler: StatusHandler) { onStatusChange(handler: StatusHandler) {
this.statusHandlers.push(handler) this.statusHandlers.push(handler)
handler(this.status)
return () => { return () => {
this.statusHandlers = this.statusHandlers.filter(fn => fn !== handler) this.statusHandlers = this.statusHandlers.filter(fn => fn !== handler)
} }