210 lines
5.4 KiB
TypeScript
210 lines
5.4 KiB
TypeScript
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'
|
|
|
|
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
|
|
}
|
|
|
|
export type DesktopAnimalWarningType = 'balance' | 'limit'
|
|
|
|
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 setModalOpen = useModalStore((state) => state.setModalOpen)
|
|
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
|
const chips = useGameRoundStore((state) => state.chips)
|
|
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
|
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 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 lockInteraction = showStandbyState
|
|
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 (!showStandbyState) {
|
|
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, showStandbyState])
|
|
|
|
const handleStart = () => {
|
|
if (authStatus !== 'authenticated') {
|
|
notify.warning(t('commonUi.toast.loginRequired'))
|
|
setModalOpen('desktopLogin', true)
|
|
return
|
|
}
|
|
|
|
clearSelections()
|
|
markSoundPlaybackUnlocked()
|
|
requestRealtimeConnection()
|
|
}
|
|
|
|
const handleSelect = (animalId: number) => {
|
|
if (showStandbyState) {
|
|
return
|
|
}
|
|
|
|
if (onSelect) {
|
|
onSelect(animalId)
|
|
return
|
|
}
|
|
|
|
if (selectionByCell[animalId]) {
|
|
removeSelectionsForCell(animalId)
|
|
return
|
|
}
|
|
|
|
if (selectedCellCount >= maxSelectionCount) {
|
|
setCellWarning({
|
|
cellId: animalId,
|
|
type: 'limit',
|
|
})
|
|
return
|
|
}
|
|
|
|
if (totalBetAmount + (activeChip?.amount ?? 0) > balance) {
|
|
setCellWarning({
|
|
cellId: animalId,
|
|
type: 'balance',
|
|
})
|
|
return
|
|
}
|
|
|
|
placeBet(animalId)
|
|
}
|
|
|
|
return {
|
|
cellWarning,
|
|
handleSelect,
|
|
handleStart,
|
|
isRealtimeConnecting,
|
|
lockInteraction,
|
|
marqueeId,
|
|
selectionByCell,
|
|
showStandbyState,
|
|
}
|
|
}
|