feat(game): 添加游戏大厅音频控制和用户协议功能
- 实现音频资源配置和音频商店状态管理 - 添加用户协议和游戏规则的多语言支持 - 集成音频播放解锁机制和声音开关功能 - 更新API客户端以支持根路径候选 - 优化游戏历史记录组件的滚动加载逻辑 - 添加桌面端控制按钮的动画效果和交互反馈 - 实现语言切换和音效控制的UI组件 - 增加下注相关的状态管理和错误提示 - 完善应用偏好设置的存储和持久化逻辑
This commit is contained in:
42
src/store/audio/audio-store.ts
Normal file
42
src/store/audio/audio-store.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware'
|
||||
|
||||
import { AUDIO_PREFERENCES_STORAGE_KEY } from '@/constants'
|
||||
|
||||
interface AudioPreferenceState {
|
||||
hasUnlockedSoundPlayback: boolean
|
||||
markSoundPlaybackUnlocked: () => void
|
||||
isSoundEnabled: boolean
|
||||
setSoundEnabled: (enabled: boolean) => void
|
||||
toggleSoundEnabled: () => void
|
||||
}
|
||||
|
||||
export const useAudioStore = create<AudioPreferenceState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
hasUnlockedSoundPlayback: false,
|
||||
isSoundEnabled: true,
|
||||
markSoundPlaybackUnlocked: () => {
|
||||
set({ hasUnlockedSoundPlayback: true })
|
||||
},
|
||||
setSoundEnabled: (enabled) => {
|
||||
set({ isSoundEnabled: enabled })
|
||||
},
|
||||
toggleSoundEnabled: () => {
|
||||
set((state) => ({ isSoundEnabled: !state.isSoundEnabled }))
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: AUDIO_PREFERENCES_STORAGE_KEY,
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
merge: (persistedState, currentState) => ({
|
||||
...currentState,
|
||||
...(persistedState as Partial<AudioPreferenceState>),
|
||||
hasUnlockedSoundPlayback: false,
|
||||
}),
|
||||
partialize: (state) => ({
|
||||
isSoundEnabled: state.isSoundEnabled,
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
1
src/store/audio/index.ts
Normal file
1
src/store/audio/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './audio-store'
|
||||
@@ -50,6 +50,7 @@ interface PersistedAuthState {
|
||||
interface PersistedAppPreferenceState {
|
||||
appLanguage: string | null
|
||||
deviceId: string | null
|
||||
hasAcceptedProtocol: boolean
|
||||
}
|
||||
|
||||
interface AuthState extends PersistedAuthState {
|
||||
@@ -217,8 +218,11 @@ export const useAuthStore = create<AuthState>()(
|
||||
)
|
||||
|
||||
interface AppPreferenceStoreState extends PersistedAppPreferenceState {
|
||||
finishHydration: () => void
|
||||
getOrCreateDeviceId: () => string
|
||||
isHydrated: boolean
|
||||
setAppLanguage: (language: string) => void
|
||||
setProtocolAccepted: (accepted: boolean) => void
|
||||
}
|
||||
|
||||
export const useAppPreferenceStore = create<AppPreferenceStoreState>()(
|
||||
@@ -226,6 +230,11 @@ export const useAppPreferenceStore = create<AppPreferenceStoreState>()(
|
||||
(set, get) => ({
|
||||
appLanguage: null,
|
||||
deviceId: null,
|
||||
hasAcceptedProtocol: false,
|
||||
isHydrated: false,
|
||||
finishHydration: () => {
|
||||
set({ isHydrated: true })
|
||||
},
|
||||
getOrCreateDeviceId: () => {
|
||||
const deviceId = get().deviceId
|
||||
|
||||
@@ -242,6 +251,9 @@ export const useAppPreferenceStore = create<AppPreferenceStoreState>()(
|
||||
setAppLanguage: (language) => {
|
||||
set({ appLanguage: language })
|
||||
},
|
||||
setProtocolAccepted: (accepted) => {
|
||||
set({ hasAcceptedProtocol: accepted })
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: APP_PREFERENCES_STORAGE_KEY,
|
||||
@@ -249,7 +261,11 @@ export const useAppPreferenceStore = create<AppPreferenceStoreState>()(
|
||||
partialize: (state) => ({
|
||||
appLanguage: state.appLanguage,
|
||||
deviceId: state.deviceId,
|
||||
hasAcceptedProtocol: state.hasAcceptedProtocol,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
state?.finishHydration()
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -265,3 +281,11 @@ export function getStoredAppLanguage() {
|
||||
export function setStoredAppLanguage(language: string) {
|
||||
useAppPreferenceStore.getState().setAppLanguage(language)
|
||||
}
|
||||
|
||||
export function getStoredProtocolAccepted() {
|
||||
return useAppPreferenceStore.getState().hasAcceptedProtocol
|
||||
}
|
||||
|
||||
export function setStoredProtocolAccepted(accepted: boolean) {
|
||||
useAppPreferenceStore.getState().setProtocolAccepted(accepted)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
} from '@/features/game/shared'
|
||||
import {
|
||||
buildGameCellViewModels,
|
||||
createMockGameBootstrapSnapshot,
|
||||
createEmptyGameBootstrapSnapshot,
|
||||
DEFAULT_ACTIVE_CHIP_ID,
|
||||
getChipById,
|
||||
getRecentWinningCellIds,
|
||||
@@ -31,29 +31,54 @@ type GameRoundSlice = Pick<
|
||||
| 'trends'
|
||||
>
|
||||
|
||||
function resolveRecentActiveChipId(
|
||||
chips: Chip[],
|
||||
selections: BetSelection[],
|
||||
fallbackChipId: string,
|
||||
) {
|
||||
for (let index = selections.length - 1; index >= 0; index -= 1) {
|
||||
const chipId = selections[index]?.chipId
|
||||
|
||||
if (chipId && getChipById(chips, chipId)) {
|
||||
return chipId
|
||||
}
|
||||
}
|
||||
|
||||
return getChipById(chips, fallbackChipId)
|
||||
? fallbackChipId
|
||||
: (chips.find((chip) => chip.isDefault)?.id ??
|
||||
chips[0]?.id ??
|
||||
DEFAULT_ACTIVE_CHIP_ID)
|
||||
}
|
||||
|
||||
export interface GameRoundStoreState extends GameRoundSlice {
|
||||
activeChipId: string
|
||||
clearSelections: () => void
|
||||
hydrateRound: (snapshot: GameRoundSlice) => void
|
||||
placeBet: (cellId: number) => void
|
||||
recentSuccessfulSelections: BetSelection[]
|
||||
removeSelectionsForCell: (cellId: number) => void
|
||||
restoreRecentSuccessfulSelections: () => boolean
|
||||
setRecentSuccessfulSelections: (selections: BetSelection[]) => void
|
||||
selectChip: (chipId: string) => void
|
||||
setPhase: (phase: RoundPhase) => void
|
||||
syncRound: (round: Partial<RoundSnapshot>) => void
|
||||
upsertSelections: (selections: BetSelection[]) => void
|
||||
}
|
||||
|
||||
function createInitialRoundState(): GameRoundSlice & { activeChipId: string } {
|
||||
const snapshot = createMockGameBootstrapSnapshot()
|
||||
function createInitialRoundState(): GameRoundSlice & {
|
||||
activeChipId: string
|
||||
recentSuccessfulSelections: BetSelection[]
|
||||
} {
|
||||
const snapshot = createEmptyGameBootstrapSnapshot()
|
||||
|
||||
return {
|
||||
activeChipId:
|
||||
snapshot.chips.find((chip) => chip.isDefault)?.id ??
|
||||
DEFAULT_ACTIVE_CHIP_ID,
|
||||
activeChipId: DEFAULT_ACTIVE_CHIP_ID,
|
||||
cells: snapshot.cells,
|
||||
chips: snapshot.chips,
|
||||
history: snapshot.history,
|
||||
maxSelectionCount: snapshot.maxSelectionCount,
|
||||
recentSuccessfulSelections: [],
|
||||
round: snapshot.round,
|
||||
selections: snapshot.selections,
|
||||
trends: snapshot.trends,
|
||||
@@ -118,6 +143,7 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
||||
}
|
||||
})
|
||||
},
|
||||
recentSuccessfulSelections: [],
|
||||
removeSelectionsForCell: (cellId) => {
|
||||
set((state) => ({
|
||||
selections: state.selections.filter(
|
||||
@@ -125,6 +151,48 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
||||
),
|
||||
}))
|
||||
},
|
||||
restoreRecentSuccessfulSelections: () => {
|
||||
const state = useGameRoundStore.getState()
|
||||
|
||||
if (
|
||||
state.round.phase !== 'betting' ||
|
||||
state.recentSuccessfulSelections.length === 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const nextSelections = state.recentSuccessfulSelections
|
||||
.filter((selection) => getChipById(state.chips, selection.chipId))
|
||||
.slice(0, state.maxSelectionCount)
|
||||
.map((selection, index) => ({
|
||||
...selection,
|
||||
id: `bet-repeat-${selection.cellId}-${index + 1}-${Date.now()}`,
|
||||
placedAt: new Date().toISOString(),
|
||||
source: 'local' as const,
|
||||
}))
|
||||
|
||||
if (nextSelections.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
set({
|
||||
activeChipId: resolveRecentActiveChipId(
|
||||
state.chips,
|
||||
nextSelections,
|
||||
state.activeChipId,
|
||||
),
|
||||
selections: nextSelections,
|
||||
})
|
||||
|
||||
return true
|
||||
},
|
||||
setRecentSuccessfulSelections: (selections) => {
|
||||
set({
|
||||
recentSuccessfulSelections: selections.map((selection) => ({
|
||||
...selection,
|
||||
})),
|
||||
})
|
||||
},
|
||||
selectChip: (chipId) => {
|
||||
set((state) => {
|
||||
if (!getChipById(state.chips, chipId)) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
GameBootstrapSnapshot,
|
||||
} from '@/features/game/shared'
|
||||
import {
|
||||
createMockGameBootstrapSnapshot,
|
||||
createEmptyGameBootstrapSnapshot,
|
||||
getUnreadAnnouncementCount,
|
||||
getVisibleAnnouncements,
|
||||
} from '@/features/game/shared'
|
||||
@@ -32,7 +32,7 @@ export interface GameSessionStoreState extends GameSessionSlice {
|
||||
}
|
||||
|
||||
function createInitialSessionState(): GameSessionSlice {
|
||||
const snapshot = createMockGameBootstrapSnapshot()
|
||||
const snapshot = createEmptyGameBootstrapSnapshot()
|
||||
|
||||
return {
|
||||
announcements: snapshot.announcements,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './audio'
|
||||
export * from './auth'
|
||||
export * from './game'
|
||||
export * from './modal'
|
||||
|
||||
@@ -6,6 +6,12 @@ export const MODAL_KEYS = [
|
||||
'desktopLogin',
|
||||
/**@description 桌面端注册弹窗*/
|
||||
'desktopRegister',
|
||||
/**@description 桌面端多语言弹窗*/
|
||||
'desktopLanguage',
|
||||
/**@description 桌面端协议弹窗*/
|
||||
'desktopProtocol',
|
||||
/**@description 桌面端规则弹窗*/
|
||||
'desktopRules',
|
||||
/**@description 桌面端用户信息弹窗*/
|
||||
'desktopUserInfo',
|
||||
/**@description 桌面端公告弹窗*/
|
||||
@@ -25,6 +31,9 @@ type ModalVisibilityMap = Record<ModalKey, boolean>
|
||||
const INITIAL_MODAL_VISIBILITY: ModalVisibilityMap = {
|
||||
desktopLogin: false,
|
||||
desktopRegister: false,
|
||||
desktopLanguage: false,
|
||||
desktopProtocol: false,
|
||||
desktopRules: false,
|
||||
desktopUserInfo: false,
|
||||
desktopNotice: false,
|
||||
desktopAutoSetting: false,
|
||||
|
||||
Reference in New Issue
Block a user