refactor: 重构中奖和推送大奖事件,中奖过度动画,和开奖动画

This commit is contained in:
JiaJun
2026-05-27 14:57:08 +08:00
parent 046f250ce3
commit 2b2b86a73d
48 changed files with 2471 additions and 422 deletions

View File

@@ -15,7 +15,12 @@ import {
import { getAuthDeviceId, useAuthStore } from '@/store/auth'
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
import { getGameLobbyInit, normalizePeriodTickRound } from '../api/game-api'
import type { GamePeriodTickDto } from '../api/types'
import type {
BetWinEventDataDto,
GamePeriodTickDto,
JackpotHitEventDataDto,
JackpotHitItemDto,
} from '../api/types'
type UserStreakMessageData = {
currentStreak: number
@@ -29,6 +34,10 @@ type PeriodEventData = {
resultNumber: number | null
}
type WalletChangedData = {
coin: string
}
let sharedSocketClient: GameSocketClient | null = null
let sharedSocketKey: string | null = null
let sharedSocketDisconnectTimerId: number | null = null
@@ -55,6 +64,34 @@ function toOptionalNumber(value: unknown) {
return undefined
}
function toOptionalString(value: unknown) {
if (typeof value === 'string') {
return value
}
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value)
}
return undefined
}
function toOptionalBoolean(value: unknown) {
if (typeof value === 'boolean') {
return value
}
if (value === 1 || value === '1' || value === 'true') {
return true
}
if (value === 0 || value === '0' || value === 'false') {
return false
}
return undefined
}
function getNestedRecord(
value: unknown,
key: string,
@@ -101,10 +138,11 @@ function extractServerTime(message: GameSocketMessage) {
function extractUserStreakMessageData(
message: GameSocketMessage,
): UserStreakMessageData | null {
const direct = getNestedRecord(message, 'user_snapshot')
const data = getNestedRecord(message, 'data')
const direct = getNestedRecord(message, 'user_snapshot')
const nested = getNestedRecord(data, 'user_snapshot')
const source = direct ?? nested ?? data
const source =
data && 'current_streak' in data ? data : (nested ?? direct ?? data)
if (!source || typeof source.current_streak !== 'number') {
return null
@@ -184,23 +222,163 @@ function extractPeriodEventData(
}
}
function extractWalletCoin(message: GameSocketMessage) {
function extractWalletChangedData(
message: GameSocketMessage,
): WalletChangedData | null {
const data = getNestedRecord(message, 'data')
const source = data ?? (message as Record<string, unknown>)
const coin = source.coin ?? source.balance ?? source.balance_after
const normalizedCoin =
typeof coin === 'string'
? coin
: typeof coin === 'number' && Number.isFinite(coin)
? String(coin)
: null
if (typeof coin === 'string') {
return coin
if (normalizedCoin === null) {
return null
}
return typeof coin === 'number' && Number.isFinite(coin) ? String(coin) : null
return {
coin: normalizedCoin,
}
}
function extractJackpotStatus(message: GameSocketMessage) {
const data = getNestedRecord(message, 'data')
const source = data ?? (message as Record<string, unknown>)
function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null {
if (!value || typeof value !== 'object') {
return null
}
return typeof source.is_jackpot === 'boolean' ? source.is_jackpot : true
const source = value as Record<string, unknown>
if (
typeof source.user_id !== 'number' ||
typeof source.period_no !== 'string' ||
typeof source.total_win !== 'string'
) {
return null
}
const resultNumber = toOptionalNumber(source.result_number)
if (typeof resultNumber !== 'number' || !Number.isInteger(resultNumber)) {
return null
}
return {
period_no: source.period_no,
result_number: resultNumber,
total_win: source.total_win,
user_id: source.user_id,
}
}
function extractJackpotHitData(
message: GameSocketMessage,
): JackpotHitEventDataDto | null {
const data = getNestedRecord(message, 'data')
if (!data || typeof data.period_no !== 'string') {
return null
}
const serverTime = toOptionalNumber(data.server_time)
if (typeof serverTime !== 'number') {
return null
}
const sourceHits = Array.isArray(data.hits) ? data.hits : [data]
const hits = sourceHits
.map((item) => extractJackpotHitItem(item))
.filter((item): item is JackpotHitItemDto => item !== null)
const resultNumber = toOptionalNumber(data.result_number)
return {
hits,
period_id:
typeof data.period_id === 'number' && Number.isInteger(data.period_id)
? data.period_id
: null,
period_no: data.period_no,
result_number:
typeof resultNumber === 'number' && Number.isInteger(resultNumber)
? resultNumber
: null,
server_time: serverTime,
}
}
function extractBetWinData(
message: GameSocketMessage,
): BetWinEventDataDto | null {
const data = getNestedRecord(message, 'data')
if (!data) {
return null
}
const root = message as Record<string, unknown>
const userId = toOptionalNumber(data.user_id)
const periodId = toOptionalNumber(data.period_id)
const resultNumber = toOptionalNumber(data.result_number)
const totalWin = toOptionalString(data.total_win)
const balanceAfter = toOptionalString(data.balance_after)
const serverTime = toOptionalNumber(data.server_time ?? root.server_time)
const currentStreak = toOptionalNumber(data.current_streak)
const streakLevel = toOptionalNumber(data.streak_level)
const oddsFactor = toOptionalNumber(data.odds_factor)
const isJackpot = toOptionalBoolean(data.is_jackpot)
if (
typeof totalWin !== 'string' ||
typeof data.period_no !== 'string' ||
typeof isJackpot !== 'boolean'
) {
return null
}
return {
balance_after: balanceAfter,
bets: Array.isArray(data.bets)
? data.bets
.map((item) => {
if (!item || typeof item !== 'object') {
return null
}
const bet = item as Record<string, unknown>
const betId = toOptionalNumber(bet.bet_id)
const winAmount = toOptionalString(bet.win_amount)
return typeof betId === 'number' && typeof winAmount === 'string'
? {
bet_id: betId,
win_amount: winAmount,
}
: null
})
.filter(
(item): item is BetWinEventDataDto['bets'][number] => item !== null,
)
: [],
current_streak: currentStreak,
is_jackpot: isJackpot,
is_win: toOptionalBoolean(data.is_win) ?? true,
odds_factor: typeof oddsFactor === 'number' ? oddsFactor : undefined,
payout_pending_review:
toOptionalBoolean(data.payout_pending_review) ?? false,
period_id: periodId,
period_no: data.period_no,
result_number:
typeof resultNumber === 'number' && Number.isInteger(resultNumber)
? resultNumber
: null,
server_time: serverTime,
streak_level: typeof streakLevel === 'number' ? streakLevel : undefined,
total_win: totalWin,
user_id: userId,
}
}
function applyLobbySync(result: Awaited<ReturnType<typeof getGameLobbyInit>>) {
@@ -271,7 +449,7 @@ function applyPeriodMessage(
revealingAt: round.revealingAt,
settledAt: round.settledAt,
startedAt: round.startedAt,
winningCellId: round.winningCellId,
winningCellId: previousRound.winningCellId,
})
useGameSessionStore.getState().syncDashboard({
countdownMs: period.countdown * 1000,
@@ -323,9 +501,6 @@ function applyPeriodOpenedMessage(
const openedAt = toIsoFromUnixSeconds(
period.openTime ?? serverTime ?? Math.floor(Date.now() / 1000),
)
const hasSmallReward = roundState.selections.some(
(selection) => selection.cellId === period.resultNumber,
)
const revealKey = `${period.periodNo}:${period.resultNumber}`
roundState.syncRound({
@@ -335,7 +510,6 @@ function applyPeriodOpenedMessage(
winningCellId: period.resultNumber,
})
useGameRoundStore.getState().prepareRevealAnimation({
hasSmallReward,
revealKey,
roundId: period.periodNo,
winningCellId: period.resultNumber,
@@ -354,10 +528,11 @@ function applyPeriodPayoutMessage(
const roundState = useGameRoundStore.getState()
const revealKey = `${period.periodNo}:${period.resultNumber}`
roundState.syncRound({
id: period.periodNo,
winningCellId: period.resultNumber,
})
roundState.prepareRevealAnimation({
hasSmallReward: roundState.selections.some(
(selection) => selection.cellId === period.resultNumber,
),
revealKey,
roundId: period.periodNo,
winningCellId: period.resultNumber,
@@ -381,29 +556,56 @@ function applyUserStreakMessage(message: GameSocketMessage) {
useAuthStore.getState().setCurrentUser({
...currentUser,
currentStreak: streakData.currentStreak,
oddsFactor: streakData.oddsFactor,
streakLevel: streakData.streakLevel,
oddsFactor: streakData.oddsFactor ?? currentUser.oddsFactor,
streakLevel: streakData.streakLevel ?? currentUser.streakLevel,
})
}
function applyWalletChangedMessage(message: GameSocketMessage) {
const coin = extractWalletCoin(message)
const walletData = extractWalletChangedData(message)
const currentUser = useAuthStore.getState().currentUser
if (coin === null || !currentUser) {
if (!walletData || !currentUser) {
return
}
useAuthStore.getState().setCurrentUser({
...currentUser,
coin,
coin: walletData.coin,
})
}
function applyJackpotHitMessage(message: GameSocketMessage) {
const jackpotHitData = extractJackpotHitData(message)
if (jackpotHitData?.hits.length) {
useGameSessionStore.getState().pushJackpotBroadcasts(
jackpotHitData.hits.map((hit) => ({
id: `${jackpotHitData.period_no}:${hit.result_number}:${hit.user_id}:${hit.total_win}`,
message: `恭喜${hit.user_id} 用户中奖,获得${hit.total_win}`,
periodNo: hit.period_no,
totalWin: hit.total_win,
userId: hit.user_id,
})),
)
}
}
function applyBetWinMessage(message: GameSocketMessage) {
const betWinData = extractBetWinData(message)
const currentUser = useAuthStore.getState().currentUser
const period = extractPeriodEventData(message)
const isJackpot = extractJackpotStatus(message)
if (!betWinData) {
return
}
useGameRoundStore.getState().setPendingBetWinReward({
isJackpot: betWinData.is_jackpot,
revealKey: `${betWinData.period_no}:${betWinData.result_number ?? 'pending'}:${betWinData.total_win}`,
roundId: betWinData.period_no,
totalWin: betWinData.total_win,
winningCellId: betWinData.result_number,
})
if (!currentUser) {
return
@@ -411,12 +613,12 @@ function applyJackpotHitMessage(message: GameSocketMessage) {
useAuthStore.getState().setCurrentUser({
...currentUser,
isJackpot,
coin: betWinData.balance_after ?? currentUser.coin,
currentStreak: betWinData.current_streak ?? currentUser.currentStreak,
isJackpot: betWinData.is_jackpot,
oddsFactor: betWinData.odds_factor ?? currentUser.oddsFactor,
streakLevel: betWinData.streak_level ?? currentUser.streakLevel,
})
if (isJackpot) {
useGameRoundStore.getState().showJackpotReward(period?.periodNo ?? null)
}
}
function applyRealtimeMessage(message: GameSocketMessage) {
@@ -424,33 +626,9 @@ function applyRealtimeMessage(message: GameSocketMessage) {
const topic = getMessageTopic(message)
switch (topic) {
case GAME_SOCKET_TOPICS.periodTick: {
const period = extractPeriodTick(message)
const resultNumber =
typeof period?.result_number === 'number' ? period.result_number : null
const shouldStartSettledReveal =
period?.status === 'settled' && resultNumber !== null
const hasSmallReward = shouldStartSettledReveal
? useGameRoundStore
.getState()
.selections.some((selection) => selection.cellId === resultNumber)
: false
case GAME_SOCKET_TOPICS.periodTick:
applyPeriodMessage(message, serverTime)
if (shouldStartSettledReveal) {
useGameRoundStore.getState().prepareRevealAnimation({
hasSmallReward,
revealKey: `${period.period_no}:${resultNumber}`,
roundId: period.period_no,
winningCellId: resultNumber,
})
useGameRoundStore
.getState()
.playPreparedRevealAnimation(period.period_no)
}
break
}
case GAME_SOCKET_TOPICS.periodLocked:
applyPeriodLockedMessage(message, serverTime)
break
@@ -469,6 +647,9 @@ function applyRealtimeMessage(message: GameSocketMessage) {
case GAME_SOCKET_TOPICS.jackpotHit:
applyJackpotHitMessage(message)
break
case GAME_SOCKET_TOPICS.betWin:
applyBetWinMessage(message)
break
case GAME_SOCKET_TOPICS.betAccepted:
case GAME_SOCKET_TOPICS.autoSpinProgress:
break