Files
36-character-flower/src/store/game/game-round-store.ts
JiaJun 85b4d9481f feat(game): 添加游戏大厅音频控制和用户协议功能
- 实现音频资源配置和音频商店状态管理
- 添加用户协议和游戏规则的多语言支持
- 集成音频播放解锁机制和声音开关功能
- 更新API客户端以支持根路径候选
- 优化游戏历史记录组件的滚动加载逻辑
- 添加桌面端控制按钮的动画效果和交互反馈
- 实现语言切换和音效控制的UI组件
- 增加下注相关的状态管理和错误提示
- 完善应用偏好设置的存储和持久化逻辑
2026-05-16 18:02:59 +08:00

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 }