refactor(game): 重构项目结构,优化链路, 移动端适配
- 移除 useGameBoardVm 数据层实施说明文档 - 移除核心玩法与前端规则摘要文档 - 移除游戏模块数据与界面分层第一阶段实施稿文档 - 清理与数据层重构相关的技术方案说明 - 删除关于 PC 和 Mobile 界面分离的设计规划 - 移除 view-model hooks 架构设计相关内容
This commit is contained in:
235
src/hooks/use-animal-vm.ts
Normal file
235
src/hooks/use-animal-vm.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { useAudioStore, useAuthStore, useModalStore } from '@/store'
|
||||
import {
|
||||
selectSelectionTotal,
|
||||
useGameRoundStore,
|
||||
useGameSessionStore,
|
||||
} from '@/store/game'
|
||||
import type { DesktopAnimalWarningType } from '@/type'
|
||||
|
||||
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 getNextMarqueeId(ids: number[], currentId: number | null) {
|
||||
if (ids.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (ids.length === 1) {
|
||||
return ids[0] ?? null
|
||||
}
|
||||
|
||||
let nextId = currentId
|
||||
|
||||
while (nextId === currentId) {
|
||||
nextId = ids[Math.floor(Math.random() * ids.length)] ?? currentId
|
||||
}
|
||||
|
||||
return nextId
|
||||
}
|
||||
|
||||
export function useAnimalVm(
|
||||
animalIds: number[],
|
||||
onSelect?: (animalId: number) => void,
|
||||
) {
|
||||
const { t } = useTranslation()
|
||||
const authStatus = useAuthStore((state) => state.status)
|
||||
const currentUser = useAuthStore((state) => state.currentUser)
|
||||
const markSoundPlaybackUnlocked = useAudioStore(
|
||||
(state) => state.markSoundPlaybackUnlocked,
|
||||
)
|
||||
const isLoginModalOpen = useModalStore((state) => state.modals.desktopLogin)
|
||||
const isRegisterModalOpen = useModalStore(
|
||||
(state) => state.modals.desktopRegister,
|
||||
)
|
||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
||||
const activeBetQuantity = useGameRoundStore(
|
||||
(state) => state.activeBetQuantity,
|
||||
)
|
||||
const chips = useGameRoundStore((state) => state.chips)
|
||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||
const roundId = useGameRoundStore((state) => state.round.id)
|
||||
const roundPhase = useGameRoundStore((state) => state.round.phase)
|
||||
const maxSelectionCount = useGameRoundStore(
|
||||
(state) => state.maxSelectionCount,
|
||||
)
|
||||
const placeBet = useGameRoundStore((state) => state.placeBet)
|
||||
const removeSelectionsForCell = useGameRoundStore(
|
||||
(state) => state.removeSelectionsForCell,
|
||||
)
|
||||
const selections = useGameRoundStore((state) => state.selections)
|
||||
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
||||
const connection = useGameSessionStore((state) => state.connection)
|
||||
const tableLimitMax = useGameSessionStore(
|
||||
(state) => state.dashboard.tableLimitMax,
|
||||
)
|
||||
const requestRealtimeConnection = useGameSessionStore(
|
||||
(state) => state.requestRealtimeConnection,
|
||||
)
|
||||
const shouldConnectRealtime = useGameSessionStore(
|
||||
(state) => state.shouldConnectRealtime,
|
||||
)
|
||||
const [marqueeId, setMarqueeId] = useState<number | null>(() =>
|
||||
getNextMarqueeId(animalIds, null),
|
||||
)
|
||||
const [cellWarning, setCellWarning] = useState<{
|
||||
cellId: number
|
||||
type: DesktopAnimalWarningType
|
||||
} | null>(null)
|
||||
|
||||
const activeChip = useMemo(
|
||||
() => chips.find((chip) => chip.id === activeChipId) ?? chips[0] ?? null,
|
||||
[activeChipId, chips],
|
||||
)
|
||||
const balance = parseBalance(currentUser?.coin)
|
||||
const selectionByCell = useMemo(() => {
|
||||
return selections.reduce<Record<number, { amount: number; count: number }>>(
|
||||
(accumulator, selection) => {
|
||||
const current = accumulator[selection.cellId] ?? { amount: 0, count: 0 }
|
||||
|
||||
accumulator[selection.cellId] = {
|
||||
amount: current.amount + selection.amount,
|
||||
count: current.count + 1,
|
||||
}
|
||||
|
||||
return accumulator
|
||||
},
|
||||
{},
|
||||
)
|
||||
}, [selections])
|
||||
|
||||
const isRealtimeConnected = connection.status === 'connected'
|
||||
const isRealtimeConnecting =
|
||||
shouldConnectRealtime &&
|
||||
(connection.status === 'connecting' || connection.status === 'reconnecting')
|
||||
const showStandbyState = !shouldConnectRealtime || !isRealtimeConnected
|
||||
const isAuthModalOpen = isLoginModalOpen || isRegisterModalOpen
|
||||
const shouldAnimateStandby = showStandbyState && !isAuthModalOpen
|
||||
const hasSubmittedCurrentRound =
|
||||
Boolean(roundId) && currentUser?.lastBetPeriodNo === roundId
|
||||
const lockInteraction =
|
||||
showStandbyState || hasSubmittedCurrentRound || roundPhase !== 'betting'
|
||||
const selectedCellCount = Object.keys(selectionByCell).length
|
||||
|
||||
useEffect(() => {
|
||||
if (cellWarning === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const timerId = window.setTimeout(() => {
|
||||
setCellWarning((currentWarning) =>
|
||||
currentWarning?.cellId === cellWarning.cellId &&
|
||||
currentWarning.type === cellWarning.type
|
||||
? null
|
||||
: currentWarning,
|
||||
)
|
||||
}, 1200)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timerId)
|
||||
}
|
||||
}, [cellWarning])
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAnimateStandby) {
|
||||
setMarqueeId(null)
|
||||
return
|
||||
}
|
||||
|
||||
setMarqueeId((currentId) => getNextMarqueeId(animalIds, currentId))
|
||||
|
||||
let timerId = 0
|
||||
|
||||
const loop = () => {
|
||||
setMarqueeId((currentId) => getNextMarqueeId(animalIds, currentId))
|
||||
timerId = window.setTimeout(loop, 180 + Math.floor(Math.random() * 220))
|
||||
}
|
||||
|
||||
timerId = window.setTimeout(loop, 220)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timerId)
|
||||
}
|
||||
}, [animalIds, shouldAnimateStandby])
|
||||
|
||||
const handleStart = () => {
|
||||
if (authStatus !== 'authenticated') {
|
||||
notify.warning(t('commonUi.toast.loginRequired'))
|
||||
setModalOpen('desktopLogin', true)
|
||||
return
|
||||
}
|
||||
|
||||
clearSelections()
|
||||
markSoundPlaybackUnlocked()
|
||||
requestRealtimeConnection()
|
||||
}
|
||||
|
||||
const handleSelect = (animalId: number) => {
|
||||
if (roundPhase !== 'betting' || lockInteraction) {
|
||||
return
|
||||
}
|
||||
|
||||
if (onSelect) {
|
||||
onSelect(animalId)
|
||||
return
|
||||
}
|
||||
|
||||
if (selectionByCell[animalId]) {
|
||||
removeSelectionsForCell(animalId)
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedCellCount >= maxSelectionCount) {
|
||||
setCellWarning({
|
||||
cellId: animalId,
|
||||
type: 'limit',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const nextBetAmount = (activeChip?.amount ?? 0) * activeBetQuantity
|
||||
|
||||
if (tableLimitMax > 0 && totalBetAmount + nextBetAmount > tableLimitMax) {
|
||||
setCellWarning({
|
||||
cellId: animalId,
|
||||
type: 'betLimit',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (totalBetAmount + nextBetAmount > balance) {
|
||||
setCellWarning({
|
||||
cellId: animalId,
|
||||
type: 'balance',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
placeBet(animalId)
|
||||
}
|
||||
|
||||
return {
|
||||
cellWarning,
|
||||
handleSelect,
|
||||
handleStart,
|
||||
isRealtimeConnecting,
|
||||
lockInteraction,
|
||||
marqueeId: shouldAnimateStandby ? marqueeId : null,
|
||||
selectionByCell,
|
||||
showStandbyState,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user