- 实现音频资源配置和音频商店状态管理 - 添加用户协议和游戏规则的多语言支持 - 集成音频播放解锁机制和声音开关功能 - 更新API客户端以支持根路径候选 - 优化游戏历史记录组件的滚动加载逻辑 - 添加桌面端控制按钮的动画效果和交互反馈 - 实现语言切换和音效控制的UI组件 - 增加下注相关的状态管理和错误提示 - 完善应用偏好设置的存储和持久化逻辑
265 lines
6.7 KiB
TypeScript
265 lines
6.7 KiB
TypeScript
import { create } from 'zustand'
|
|
|
|
import type {
|
|
BetSelection,
|
|
Chip,
|
|
GameBootstrapSnapshot,
|
|
GameCell,
|
|
HistoryEntry,
|
|
RoundPhase,
|
|
RoundSnapshot,
|
|
TrendEntry,
|
|
} from '@/features/game/shared'
|
|
import {
|
|
buildGameCellViewModels,
|
|
createEmptyGameBootstrapSnapshot,
|
|
DEFAULT_ACTIVE_CHIP_ID,
|
|
getChipById,
|
|
getRecentWinningCellIds,
|
|
getSelectionTotal,
|
|
groupSelectionsByCell,
|
|
} from '@/features/game/shared'
|
|
|
|
type GameRoundSlice = Pick<
|
|
GameBootstrapSnapshot,
|
|
| 'cells'
|
|
| 'chips'
|
|
| 'history'
|
|
| 'maxSelectionCount'
|
|
| 'round'
|
|
| 'selections'
|
|
| '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
|
|
recentSuccessfulSelections: BetSelection[]
|
|
} {
|
|
const snapshot = createEmptyGameBootstrapSnapshot()
|
|
|
|
return {
|
|
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,
|
|
}
|
|
}
|
|
|
|
export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|
...createInitialRoundState(),
|
|
clearSelections: () => {
|
|
set({ selections: [] })
|
|
},
|
|
hydrateRound: (snapshot) => {
|
|
set((state) => ({
|
|
activeChipId: getChipById(snapshot.chips, state.activeChipId)
|
|
? state.activeChipId
|
|
: (snapshot.chips.find((chip) => chip.isDefault)?.id ??
|
|
snapshot.chips[0]?.id ??
|
|
DEFAULT_ACTIVE_CHIP_ID),
|
|
cells: snapshot.cells,
|
|
chips: snapshot.chips,
|
|
history: snapshot.history,
|
|
maxSelectionCount: snapshot.maxSelectionCount,
|
|
round: snapshot.round,
|
|
selections: snapshot.selections,
|
|
trends: snapshot.trends,
|
|
}))
|
|
},
|
|
placeBet: (cellId) => {
|
|
set((state) => {
|
|
const activeChip =
|
|
getChipById(state.chips, state.activeChipId) ??
|
|
state.chips.find((chip) => chip.isDefault) ??
|
|
state.chips[0]
|
|
const hasExistingSelection = state.selections.some(
|
|
(selection) => selection.cellId === cellId,
|
|
)
|
|
const selectedCellCount = new Set(
|
|
state.selections.map((selection) => selection.cellId),
|
|
).size
|
|
|
|
if (
|
|
!activeChip ||
|
|
state.round.phase !== 'betting' ||
|
|
hasExistingSelection ||
|
|
selectedCellCount >= state.maxSelectionCount
|
|
) {
|
|
return state
|
|
}
|
|
|
|
return {
|
|
selections: [
|
|
...state.selections,
|
|
{
|
|
amount: activeChip.amount,
|
|
cellId,
|
|
chipId: activeChip.id,
|
|
id: `bet-${cellId}-${state.selections.length + 1}-${Date.now()}`,
|
|
placedAt: new Date().toISOString(),
|
|
source: 'local',
|
|
},
|
|
],
|
|
}
|
|
})
|
|
},
|
|
recentSuccessfulSelections: [],
|
|
removeSelectionsForCell: (cellId) => {
|
|
set((state) => ({
|
|
selections: state.selections.filter(
|
|
(selection) => selection.cellId !== cellId,
|
|
),
|
|
}))
|
|
},
|
|
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)) {
|
|
return state
|
|
}
|
|
|
|
return { activeChipId: chipId }
|
|
})
|
|
},
|
|
setPhase: (phase) => {
|
|
set((state) => ({
|
|
round: {
|
|
...state.round,
|
|
phase,
|
|
},
|
|
}))
|
|
},
|
|
syncRound: (round) => {
|
|
set((state) => ({
|
|
round: {
|
|
...state.round,
|
|
...round,
|
|
},
|
|
}))
|
|
},
|
|
upsertSelections: (selections) => {
|
|
set({ selections })
|
|
},
|
|
}))
|
|
|
|
export const selectActiveChip = (state: GameRoundStoreState): Chip | null =>
|
|
getChipById(state.chips, state.activeChipId) ??
|
|
state.chips.find((chip) => chip.isDefault) ??
|
|
state.chips[0] ??
|
|
null
|
|
|
|
export const selectBoardCells = (state: GameRoundStoreState) =>
|
|
buildGameCellViewModels({
|
|
cells: state.cells,
|
|
round: state.round,
|
|
selections: state.selections,
|
|
trends: state.trends,
|
|
})
|
|
|
|
export const selectCanPlaceBets = (state: GameRoundStoreState) =>
|
|
state.round.phase === 'betting'
|
|
|
|
export const selectRecentResults = (state: GameRoundStoreState) =>
|
|
getRecentWinningCellIds(state.history)
|
|
|
|
export const selectSelectionTotal = (state: GameRoundStoreState) =>
|
|
getSelectionTotal(state.selections)
|
|
|
|
export const selectSelectionsByCell = (state: GameRoundStoreState) =>
|
|
groupSelectionsByCell(state.selections)
|
|
|
|
export type GameRoundStore = typeof useGameRoundStore
|
|
export type GameRoundStoreData = Pick<
|
|
GameRoundStoreState,
|
|
| 'cells'
|
|
| 'chips'
|
|
| 'history'
|
|
| 'maxSelectionCount'
|
|
| 'round'
|
|
| 'selections'
|
|
| 'trends'
|
|
>
|
|
|
|
export type { BetSelection, GameCell, HistoryEntry, RoundSnapshot, TrendEntry }
|