refactor: 重构中奖和推送大奖事件,中奖过度动画,和开奖动画
This commit is contained in:
90
src/store/game/game-auto-hosting-store.ts
Normal file
90
src/store/game/game-auto-hosting-store.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
import type { BetSelection } from '@/features/game/shared'
|
||||
|
||||
export interface AutoHostingStopRules {
|
||||
stopIfBalanceBelow: {
|
||||
amount: number
|
||||
enabled: boolean
|
||||
}
|
||||
stopIfSingleWinAbove: {
|
||||
amount: number
|
||||
enabled: boolean
|
||||
}
|
||||
stopOnJackpot: boolean
|
||||
}
|
||||
|
||||
interface StartAutoHostingInput {
|
||||
balanceAfterBet: number | null
|
||||
rules: AutoHostingStopRules
|
||||
selections: BetSelection[]
|
||||
}
|
||||
|
||||
export interface GameAutoHostingStoreState {
|
||||
balanceAfterBet: number | null
|
||||
completedRounds: number
|
||||
isHosting: boolean
|
||||
lastSubmittedRoundId: string | null
|
||||
rules: AutoHostingStopRules
|
||||
selections: BetSelection[]
|
||||
markRoundSubmitted: (roundId: string, balanceAfterBet: number | null) => void
|
||||
startHosting: (input: StartAutoHostingInput) => void
|
||||
stopHosting: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_AUTO_HOSTING_RULES: AutoHostingStopRules = {
|
||||
stopIfBalanceBelow: {
|
||||
amount: 0,
|
||||
enabled: false,
|
||||
},
|
||||
stopIfSingleWinAbove: {
|
||||
amount: 50_000,
|
||||
enabled: false,
|
||||
},
|
||||
stopOnJackpot: false,
|
||||
}
|
||||
|
||||
export const useGameAutoHostingStore = create<GameAutoHostingStoreState>()(
|
||||
(set) => ({
|
||||
balanceAfterBet: null,
|
||||
completedRounds: 0,
|
||||
isHosting: false,
|
||||
lastSubmittedRoundId: null,
|
||||
rules: DEFAULT_AUTO_HOSTING_RULES,
|
||||
selections: [],
|
||||
markRoundSubmitted: (roundId, balanceAfterBet) => {
|
||||
set((state) => {
|
||||
if (!state.isHosting || state.lastSubmittedRoundId === roundId) {
|
||||
return state
|
||||
}
|
||||
|
||||
return {
|
||||
balanceAfterBet,
|
||||
completedRounds: state.completedRounds + 1,
|
||||
lastSubmittedRoundId: roundId,
|
||||
}
|
||||
})
|
||||
},
|
||||
startHosting: ({ balanceAfterBet, rules, selections }) => {
|
||||
set({
|
||||
balanceAfterBet,
|
||||
completedRounds: 0,
|
||||
isHosting: true,
|
||||
lastSubmittedRoundId: null,
|
||||
rules,
|
||||
selections: selections.map((selection) => ({
|
||||
...selection,
|
||||
})),
|
||||
})
|
||||
},
|
||||
stopHosting: () => {
|
||||
set({
|
||||
balanceAfterBet: null,
|
||||
completedRounds: 0,
|
||||
isHosting: false,
|
||||
lastSubmittedRoundId: null,
|
||||
selections: [],
|
||||
})
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -35,9 +35,13 @@ export type RevealAnimationPhase = 'idle' | 'spinning' | 'stopping' | 'result'
|
||||
export type RewardAnimationType = 'none' | 'small' | 'big'
|
||||
|
||||
export interface RevealAnimationState {
|
||||
pendingRewardAmount: string | null
|
||||
pendingRewardKey: string | null
|
||||
pendingRewardRoundId: string | null
|
||||
pendingRewardType: RewardAnimationType
|
||||
phase: RevealAnimationPhase
|
||||
revealKey: string | null
|
||||
rewardAmount: string | null
|
||||
rewardType: RewardAnimationType
|
||||
roundId: string | null
|
||||
winningCellId: number | null
|
||||
@@ -45,15 +49,39 @@ export interface RevealAnimationState {
|
||||
|
||||
function createIdleRevealAnimation(): RevealAnimationState {
|
||||
return {
|
||||
pendingRewardAmount: null,
|
||||
pendingRewardKey: null,
|
||||
pendingRewardRoundId: null,
|
||||
pendingRewardType: 'none',
|
||||
phase: 'idle',
|
||||
revealKey: null,
|
||||
rewardAmount: null,
|
||||
rewardType: 'none',
|
||||
roundId: null,
|
||||
winningCellId: null,
|
||||
}
|
||||
}
|
||||
|
||||
function createIdleRevealAnimationPreservingReward(
|
||||
current: RevealAnimationState,
|
||||
): RevealAnimationState {
|
||||
if (current.rewardType === 'none' && current.pendingRewardType === 'none') {
|
||||
return createIdleRevealAnimation()
|
||||
}
|
||||
|
||||
return {
|
||||
...createIdleRevealAnimation(),
|
||||
pendingRewardAmount: current.pendingRewardAmount,
|
||||
pendingRewardKey: current.pendingRewardKey,
|
||||
pendingRewardRoundId: current.pendingRewardRoundId,
|
||||
pendingRewardType: current.pendingRewardType,
|
||||
revealKey: current.revealKey,
|
||||
rewardAmount: current.rewardAmount,
|
||||
rewardType: current.rewardType,
|
||||
roundId: current.roundId,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRecentActiveChipId(
|
||||
chips: Chip[],
|
||||
selections: BetSelection[],
|
||||
@@ -83,7 +111,6 @@ export interface GameRoundStoreState extends GameRoundSlice {
|
||||
placeBet: (cellId: number) => void
|
||||
playPreparedRevealAnimation: (roundId?: string | null) => void
|
||||
prepareRevealAnimation: (input: {
|
||||
hasSmallReward: boolean
|
||||
revealKey: string
|
||||
roundId: string
|
||||
winningCellId: number
|
||||
@@ -95,7 +122,13 @@ export interface GameRoundStoreState extends GameRoundSlice {
|
||||
setRecentSuccessfulSelections: (selections: BetSelection[]) => void
|
||||
selectChip: (chipId: string) => void
|
||||
setPhase: (phase: RoundPhase) => void
|
||||
showJackpotReward: (roundId?: string | null) => void
|
||||
setPendingBetWinReward: (input: {
|
||||
isJackpot: boolean
|
||||
revealKey: string
|
||||
roundId?: string | null
|
||||
totalWin: string
|
||||
winningCellId?: number | null
|
||||
}) => void
|
||||
syncRound: (round: Partial<RoundSnapshot>) => void
|
||||
upsertSelections: (selections: BetSelection[]) => void
|
||||
}
|
||||
@@ -130,6 +163,11 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
||||
set((state) => ({
|
||||
revealAnimation: {
|
||||
...state.revealAnimation,
|
||||
pendingRewardAmount: null,
|
||||
pendingRewardKey: null,
|
||||
pendingRewardRoundId: null,
|
||||
pendingRewardType: 'none',
|
||||
rewardAmount: null,
|
||||
rewardType: 'none',
|
||||
},
|
||||
}))
|
||||
@@ -143,16 +181,31 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
||||
return state
|
||||
}
|
||||
|
||||
const rewardType =
|
||||
state.revealAnimation.rewardType === 'big'
|
||||
? 'big'
|
||||
: state.revealAnimation.pendingRewardType
|
||||
const hasPendingReward =
|
||||
state.revealAnimation.pendingRewardType !== 'none' &&
|
||||
state.revealAnimation.pendingRewardRoundId ===
|
||||
state.revealAnimation.roundId
|
||||
|
||||
return {
|
||||
revealAnimation: {
|
||||
...state.revealAnimation,
|
||||
pendingRewardAmount: null,
|
||||
pendingRewardKey: null,
|
||||
pendingRewardRoundId: null,
|
||||
pendingRewardType: 'none',
|
||||
phase: 'result',
|
||||
rewardType,
|
||||
revealKey: hasPendingReward
|
||||
? state.revealAnimation.pendingRewardKey
|
||||
: state.revealAnimation.revealKey,
|
||||
rewardAmount: hasPendingReward
|
||||
? state.revealAnimation.pendingRewardAmount
|
||||
: state.revealAnimation.rewardAmount,
|
||||
rewardType: hasPendingReward
|
||||
? state.revealAnimation.pendingRewardType
|
||||
: state.revealAnimation.rewardType,
|
||||
roundId: hasPendingReward
|
||||
? state.revealAnimation.pendingRewardRoundId
|
||||
: state.revealAnimation.roundId,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -170,7 +223,7 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
||||
maxSelectionCount: snapshot.maxSelectionCount,
|
||||
revealAnimation:
|
||||
snapshot.round.phase === 'betting' || snapshot.round.phase === 'waiting'
|
||||
? createIdleRevealAnimation()
|
||||
? createIdleRevealAnimationPreservingReward(state.revealAnimation)
|
||||
: state.revealAnimation,
|
||||
round: snapshot.round,
|
||||
selections: snapshot.selections,
|
||||
@@ -235,34 +288,17 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
||||
}
|
||||
})
|
||||
},
|
||||
prepareRevealAnimation: ({
|
||||
hasSmallReward,
|
||||
revealKey,
|
||||
roundId,
|
||||
winningCellId,
|
||||
}) => {
|
||||
prepareRevealAnimation: ({ revealKey, roundId, winningCellId }) => {
|
||||
set((state) => {
|
||||
if (state.revealAnimation.revealKey === revealKey) {
|
||||
return state
|
||||
}
|
||||
|
||||
const pendingRewardType =
|
||||
state.revealAnimation.pendingRewardType === 'big'
|
||||
? 'big'
|
||||
: hasSmallReward
|
||||
? 'small'
|
||||
: 'none'
|
||||
|
||||
return {
|
||||
revealAnimation: {
|
||||
...state.revealAnimation,
|
||||
pendingRewardType,
|
||||
phase: 'idle',
|
||||
revealKey,
|
||||
rewardType:
|
||||
state.revealAnimation.rewardType === 'big'
|
||||
? 'big'
|
||||
: state.revealAnimation.rewardType,
|
||||
roundId,
|
||||
winningCellId,
|
||||
},
|
||||
@@ -346,24 +382,53 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
||||
}
|
||||
|
||||
if (phase === 'betting' || phase === 'waiting') {
|
||||
nextState.revealAnimation = createIdleRevealAnimation()
|
||||
nextState.revealAnimation = createIdleRevealAnimationPreservingReward(
|
||||
state.revealAnimation,
|
||||
)
|
||||
}
|
||||
|
||||
return nextState
|
||||
})
|
||||
},
|
||||
showJackpotReward: (roundId) => {
|
||||
setPendingBetWinReward: ({
|
||||
isJackpot,
|
||||
revealKey,
|
||||
roundId,
|
||||
totalWin,
|
||||
winningCellId,
|
||||
}) => {
|
||||
set((state) => {
|
||||
const nextRoundId =
|
||||
roundId ?? state.round.id ?? state.revealAnimation.roundId
|
||||
const isResultReady = state.revealAnimation.phase === 'result'
|
||||
const nextWinningCellId =
|
||||
winningCellId ?? state.revealAnimation.winningCellId
|
||||
const rewardType = isJackpot ? 'big' : 'small'
|
||||
const shouldQueueReward =
|
||||
state.revealAnimation.phase !== 'result' ||
|
||||
state.revealAnimation.roundId !== nextRoundId
|
||||
|
||||
if (shouldQueueReward) {
|
||||
return {
|
||||
revealAnimation: {
|
||||
...state.revealAnimation,
|
||||
pendingRewardAmount: totalWin,
|
||||
pendingRewardKey: revealKey,
|
||||
pendingRewardRoundId: nextRoundId,
|
||||
pendingRewardType: rewardType,
|
||||
roundId: nextRoundId,
|
||||
winningCellId: nextWinningCellId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
revealAnimation: {
|
||||
...state.revealAnimation,
|
||||
pendingRewardType: 'big',
|
||||
rewardType: isResultReady ? 'big' : state.revealAnimation.rewardType,
|
||||
revealKey,
|
||||
rewardAmount: totalWin,
|
||||
rewardType,
|
||||
roundId: nextRoundId,
|
||||
winningCellId: nextWinningCellId,
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -392,7 +457,9 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
||||
nextRound.id !== previousRound.id &&
|
||||
(nextRound.phase === 'betting' || nextRound.phase === 'waiting')
|
||||
) {
|
||||
nextState.revealAnimation = createIdleRevealAnimation()
|
||||
nextState.revealAnimation = createIdleRevealAnimationPreservingReward(
|
||||
state.revealAnimation,
|
||||
)
|
||||
}
|
||||
|
||||
return nextState
|
||||
|
||||
@@ -18,10 +18,25 @@ type GameSessionSlice = Pick<
|
||||
'announcements' | 'connection' | 'dashboard'
|
||||
>
|
||||
|
||||
const MAX_JACKPOT_BROADCAST_COUNT = 20
|
||||
|
||||
export interface JackpotBroadcastItem {
|
||||
id: string
|
||||
message: string
|
||||
periodNo: string
|
||||
receivedAt: string
|
||||
totalWin: string
|
||||
userId: number
|
||||
}
|
||||
|
||||
type JackpotBroadcastInput = Omit<JackpotBroadcastItem, 'receivedAt'>
|
||||
|
||||
export interface GameSessionStoreState extends GameSessionSlice {
|
||||
dismissAnnouncement: (announcementId: string) => void
|
||||
hydrateSession: (snapshot: GameSessionSlice) => void
|
||||
jackpotBroadcasts: JackpotBroadcastItem[]
|
||||
markAnnouncementRead: (announcementId: string) => void
|
||||
pushJackpotBroadcasts: (broadcasts: JackpotBroadcastInput[]) => void
|
||||
requestRealtimeConnection: () => void
|
||||
resetRealtimeConnectionRequest: () => void
|
||||
shouldConnectRealtime: boolean
|
||||
@@ -43,6 +58,7 @@ function createInitialSessionState(): GameSessionSlice {
|
||||
|
||||
export const useGameSessionStore = create<GameSessionStoreState>()((set) => ({
|
||||
...createInitialSessionState(),
|
||||
jackpotBroadcasts: [],
|
||||
shouldConnectRealtime: false,
|
||||
dismissAnnouncement: (announcementId) => {
|
||||
set((state) => ({
|
||||
@@ -63,6 +79,7 @@ export const useGameSessionStore = create<GameSessionStoreState>()((set) => ({
|
||||
hydrateSession: (snapshot) => {
|
||||
set((state) => ({
|
||||
...snapshot,
|
||||
jackpotBroadcasts: state.jackpotBroadcasts,
|
||||
shouldConnectRealtime: state.shouldConnectRealtime,
|
||||
}))
|
||||
},
|
||||
@@ -76,6 +93,35 @@ export const useGameSessionStore = create<GameSessionStoreState>()((set) => ({
|
||||
},
|
||||
}))
|
||||
},
|
||||
pushJackpotBroadcasts: (broadcasts) => {
|
||||
if (broadcasts.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const existingIds = new Set(
|
||||
state.jackpotBroadcasts.map((item) => item.id),
|
||||
)
|
||||
const receivedAt = new Date().toISOString()
|
||||
const nextBroadcasts = broadcasts
|
||||
.filter((broadcast) => !existingIds.has(broadcast.id))
|
||||
.map((broadcast) => ({
|
||||
...broadcast,
|
||||
receivedAt,
|
||||
}))
|
||||
|
||||
if (nextBroadcasts.length === 0) {
|
||||
return state
|
||||
}
|
||||
|
||||
return {
|
||||
jackpotBroadcasts: [
|
||||
...nextBroadcasts,
|
||||
...state.jackpotBroadcasts,
|
||||
].slice(0, MAX_JACKPOT_BROADCAST_COUNT),
|
||||
}
|
||||
})
|
||||
},
|
||||
requestRealtimeConnection: () => {
|
||||
set({ shouldConnectRealtime: true })
|
||||
},
|
||||
@@ -138,7 +184,7 @@ export const selectVisibleAnnouncements = (state: GameSessionStoreState) =>
|
||||
export type GameSessionStore = typeof useGameSessionStore
|
||||
export type GameSessionStoreData = Pick<
|
||||
GameSessionStoreState,
|
||||
'announcements' | 'connection' | 'dashboard'
|
||||
'announcements' | 'connection' | 'dashboard' | 'jackpotBroadcasts'
|
||||
>
|
||||
|
||||
export type { AnnouncementState, ConnectionState, DashboardState }
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './game-auto-hosting-store'
|
||||
export * from './game-round-store'
|
||||
export * from './game-session-store'
|
||||
|
||||
Reference in New Issue
Block a user