refactor: 重构中奖和推送大奖事件,中奖过度动画,和开奖动画
This commit is contained in:
252
src/features/game/hooks/use-auto-hosting-runner.ts
Normal file
252
src/features/game/hooks/use-auto-hosting-runner.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { placeGameBet } from '@/features/game'
|
||||
import type { BetSelection } from '@/features/game/shared'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useGameAutoHostingStore, useGameRoundStore } from '@/store/game'
|
||||
|
||||
function parseBalance(value: string | number | null | undefined) {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? value : 0
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return 0
|
||||
}
|
||||
|
||||
const parsed = Number(value)
|
||||
|
||||
return Number.isFinite(parsed) ? parsed : 0
|
||||
}
|
||||
|
||||
function createIdempotencyKey() {
|
||||
if (
|
||||
typeof crypto !== 'undefined' &&
|
||||
typeof crypto.randomUUID === 'function'
|
||||
) {
|
||||
return `auto-bet-${crypto.randomUUID()}`
|
||||
}
|
||||
|
||||
return `auto-bet-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||
}
|
||||
|
||||
function toBetId(chipId: string) {
|
||||
const match = chipId.match(/^chip-(\d+)$/)
|
||||
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
|
||||
const betId = Number(match[1])
|
||||
|
||||
return Number.isInteger(betId) && betId >= 1 && betId <= 6 ? betId : null
|
||||
}
|
||||
|
||||
function groupSelections(selections: BetSelection[]) {
|
||||
return selections.reduce<Map<string, { betId: number; numbers: number[] }>>(
|
||||
(accumulator, selection) => {
|
||||
const betId = toBetId(selection.chipId)
|
||||
|
||||
if (betId === null) {
|
||||
return accumulator
|
||||
}
|
||||
|
||||
const groupKey = String(betId)
|
||||
const current = accumulator.get(groupKey)
|
||||
|
||||
if (current) {
|
||||
current.numbers.push(selection.cellId)
|
||||
return accumulator
|
||||
}
|
||||
|
||||
accumulator.set(groupKey, {
|
||||
betId,
|
||||
numbers: [selection.cellId],
|
||||
})
|
||||
|
||||
return accumulator
|
||||
},
|
||||
new Map(),
|
||||
)
|
||||
}
|
||||
|
||||
export function useAutoHostingRunner() {
|
||||
const { t } = useTranslation()
|
||||
const authStatus = useAuthStore((state) => state.status)
|
||||
const currentUser = useAuthStore((state) => state.currentUser)
|
||||
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
||||
const round = useGameRoundStore((state) => state.round)
|
||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||
const balanceAfterBet = useGameAutoHostingStore(
|
||||
(state) => state.balanceAfterBet,
|
||||
)
|
||||
const isHosting = useGameAutoHostingStore((state) => state.isHosting)
|
||||
const lastSubmittedRoundId = useGameAutoHostingStore(
|
||||
(state) => state.lastSubmittedRoundId,
|
||||
)
|
||||
const rules = useGameAutoHostingStore((state) => state.rules)
|
||||
const selections = useGameAutoHostingStore((state) => state.selections)
|
||||
const markRoundSubmitted = useGameAutoHostingStore(
|
||||
(state) => state.markRoundSubmitted,
|
||||
)
|
||||
const stopHosting = useGameAutoHostingStore((state) => state.stopHosting)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const previousJackpotRef = useRef(currentUser?.isJackpot === true)
|
||||
|
||||
useEffect(() => {
|
||||
const isJackpot = currentUser?.isJackpot === true
|
||||
|
||||
if (!isHosting) {
|
||||
previousJackpotRef.current = isJackpot
|
||||
return
|
||||
}
|
||||
|
||||
const balance = parseBalance(currentUser?.coin)
|
||||
|
||||
if (
|
||||
rules.stopIfBalanceBelow.enabled &&
|
||||
balance < rules.stopIfBalanceBelow.amount
|
||||
) {
|
||||
stopHosting()
|
||||
notify.warning(t('commonUi.toast.autoHostingStoppedBalance'))
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
rules.stopIfSingleWinAbove.enabled &&
|
||||
balanceAfterBet !== null &&
|
||||
balance - balanceAfterBet > rules.stopIfSingleWinAbove.amount
|
||||
) {
|
||||
stopHosting()
|
||||
notify.success(t('commonUi.toast.autoHostingStoppedWin'))
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
rules.stopOnJackpot &&
|
||||
isJackpot &&
|
||||
previousJackpotRef.current === false
|
||||
) {
|
||||
stopHosting()
|
||||
notify.success(t('commonUi.toast.autoHostingStoppedJackpot'))
|
||||
return
|
||||
}
|
||||
|
||||
previousJackpotRef.current = isJackpot
|
||||
}, [
|
||||
balanceAfterBet,
|
||||
currentUser?.coin,
|
||||
currentUser?.isJackpot,
|
||||
isHosting,
|
||||
rules,
|
||||
stopHosting,
|
||||
t,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isHosting ||
|
||||
isSubmitting ||
|
||||
authStatus !== 'authenticated' ||
|
||||
!currentUser ||
|
||||
round.phase !== 'betting' ||
|
||||
!round.id ||
|
||||
lastSubmittedRoundId === round.id
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const groupedSelections = groupSelections(selections)
|
||||
|
||||
if (groupedSelections.size === 0) {
|
||||
stopHosting()
|
||||
notify.warning(t('commonUi.toast.autoHostingStopped'))
|
||||
return
|
||||
}
|
||||
|
||||
const totalBetAmount = selections.reduce(
|
||||
(total, selection) => total + selection.amount,
|
||||
0,
|
||||
)
|
||||
const balance = parseBalance(currentUser.coin)
|
||||
|
||||
if (totalBetAmount > balance) {
|
||||
stopHosting()
|
||||
notify.warning(t('commonUi.toast.autoHostingStoppedBalance'))
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const submitAutoBet = async () => {
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
let latestBalance = currentUser.coin ?? '0'
|
||||
|
||||
for (const group of groupedSelections.values()) {
|
||||
const uniqueNumbers = [...new Set(group.numbers)].sort(
|
||||
(left, right) => left - right,
|
||||
)
|
||||
const result = await placeGameBet({
|
||||
bet_id: group.betId,
|
||||
idempotency_key: createIdempotencyKey(),
|
||||
numbers: uniqueNumbers.join(','),
|
||||
period_no: round.id,
|
||||
})
|
||||
|
||||
if (result.status !== 'accepted') {
|
||||
throw new Error(t('commonUi.toast.betRejected'))
|
||||
}
|
||||
|
||||
latestBalance = result.balance_after
|
||||
}
|
||||
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentUser({
|
||||
...currentUser,
|
||||
coin: latestBalance,
|
||||
lastBetPeriodNo: round.id,
|
||||
})
|
||||
markRoundSubmitted(round.id, parseBalance(latestBalance))
|
||||
clearSelections()
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
stopHosting()
|
||||
notify.error(t('commonUi.toast.autoHostingSubmitFailed'), {
|
||||
description: error instanceof Error ? error.message : undefined,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void submitAutoBet()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [
|
||||
authStatus,
|
||||
clearSelections,
|
||||
currentUser,
|
||||
isHosting,
|
||||
isSubmitting,
|
||||
lastSubmittedRoundId,
|
||||
markRoundSubmitted,
|
||||
round.id,
|
||||
round.phase,
|
||||
selections,
|
||||
setCurrentUser,
|
||||
stopHosting,
|
||||
t,
|
||||
])
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { notify } from '@/lib/notify'
|
||||
import { useAuthStore, useModalStore } from '@/store'
|
||||
import {
|
||||
selectSelectionTotal,
|
||||
useGameAutoHostingStore,
|
||||
useGameRoundStore,
|
||||
useGameSessionStore,
|
||||
} from '@/store/game'
|
||||
@@ -73,6 +74,7 @@ export function useGameControlVm() {
|
||||
const setRecentSuccessfulSelections = useGameRoundStore(
|
||||
(state) => state.setRecentSuccessfulSelections,
|
||||
)
|
||||
const isAutoHosting = useGameAutoHostingStore((state) => state.isHosting)
|
||||
const selectChip = useGameRoundStore((state) => state.selectChip)
|
||||
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
||||
const connectionStatus = useGameSessionStore(
|
||||
@@ -114,13 +116,14 @@ export function useGameControlVm() {
|
||||
const hasSubmittedCurrentRound =
|
||||
Boolean(round.id) && currentUser?.lastBetPeriodNo === round.id
|
||||
const hasInsufficientBalance = hasSelections && totalBetAmount > balance
|
||||
const confirmState: ConfirmState = isSubmitting
|
||||
? 'submitting'
|
||||
: !hasSelections
|
||||
? 'idle'
|
||||
: hasInsufficientBalance
|
||||
? 'insufficient'
|
||||
: 'ready'
|
||||
const confirmState: ConfirmState =
|
||||
isSubmitting || isAutoHosting
|
||||
? 'submitting'
|
||||
: !hasSelections
|
||||
? 'idle'
|
||||
: hasInsufficientBalance
|
||||
? 'insufficient'
|
||||
: 'ready'
|
||||
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (confirmState === 'submitting' || !hasSelections) {
|
||||
@@ -258,13 +261,27 @@ export function useGameControlVm() {
|
||||
])
|
||||
|
||||
const handleOpenAutoSetting = useCallback(() => {
|
||||
if (!hasSelections) {
|
||||
notify.warning(t('commonUi.toast.selectNumbersBeforeAutoHosting'))
|
||||
return
|
||||
}
|
||||
|
||||
setModalOpen('desktopAutoSetting', true)
|
||||
}, [setModalOpen])
|
||||
}, [hasSelections, setModalOpen, t])
|
||||
|
||||
return {
|
||||
acceptingBets: round.phase === 'betting' && !hasSubmittedCurrentRound,
|
||||
actionsEnabled: hasEnteredGame && !hasSubmittedCurrentRound,
|
||||
canClear: selections.length > 0 && !hasSubmittedCurrentRound,
|
||||
acceptingBets:
|
||||
round.phase === 'betting' && !hasSubmittedCurrentRound && !isAutoHosting,
|
||||
actionsEnabled:
|
||||
hasEnteredGame &&
|
||||
round.phase === 'betting' &&
|
||||
!hasSubmittedCurrentRound &&
|
||||
!isAutoHosting,
|
||||
canClear:
|
||||
selections.length > 0 &&
|
||||
round.phase === 'betting' &&
|
||||
!hasSubmittedCurrentRound &&
|
||||
!isAutoHosting,
|
||||
confirmLabel:
|
||||
confirmState === 'idle'
|
||||
? t('gameDesktop.control.selectNumbers')
|
||||
@@ -274,7 +291,7 @@ export function useGameControlVm() {
|
||||
? t('gameDesktop.control.submitting')
|
||||
: t('gameDesktop.control.confirm'),
|
||||
confirmState,
|
||||
isConfirmClickable: confirmState === 'ready',
|
||||
isConfirmClickable: confirmState === 'ready' && !isAutoHosting,
|
||||
onChipSelect: selectChip,
|
||||
onConfirm: handleConfirm,
|
||||
onClearSelections: clearSelections,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user