feat(game): 添加长按取消下注功能和优化控制组件
- 在桌面和移动版动物游戏组件中添加长按取消下注功能 - 新增 LongPressProgress 组件显示长按进度动画 - 重构游戏控制组件,移除增加/减少投注数量按钮 - 更新投注逻辑,优化单笔投注金额计算方式 - 修复移动端触摸目标选择器样式问题 - 添加右键菜单禁用和键盘删除操作支持 - 更新多语言文件中的游戏说明和取消投注标签 - 优化自动托管运行器中的投注分组逻辑
This commit is contained in:
@@ -1,14 +1,21 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
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 { useGameRoundStore, useGameSessionStore } from '@/store/game'
|
||||
import type { DesktopAnimalWarningType } from '@/type'
|
||||
|
||||
const LONG_PRESS_DURATION_MS = 500
|
||||
const LONG_PRESS_CLICK_SUPPRESSION_MS = 1_000
|
||||
const LONG_PRESS_MOVE_TOLERANCE_PX = 8
|
||||
|
||||
function parseBalance(value: string | number | null | undefined) {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? value : 0
|
||||
@@ -57,9 +64,6 @@ export function useAnimalVm(
|
||||
)
|
||||
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)
|
||||
@@ -72,7 +76,6 @@ export function useAnimalVm(
|
||||
(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,
|
||||
@@ -90,6 +93,17 @@ export function useAnimalVm(
|
||||
cellId: number
|
||||
type: DesktopAnimalWarningType
|
||||
} | null>(null)
|
||||
const [longPressCellId, setLongPressCellId] = useState<number | null>(null)
|
||||
const longPressTimerRef = useRef<number | null>(null)
|
||||
const longPressOriginRef = useRef<{
|
||||
pointerId: number
|
||||
x: number
|
||||
y: number
|
||||
} | null>(null)
|
||||
const suppressedClickRef = useRef<{
|
||||
cellId: number
|
||||
expiresAt: number
|
||||
} | null>(null)
|
||||
|
||||
const activeChip = useMemo(
|
||||
() => chips.find((chip) => chip.id === activeChipId) ?? chips[0] ?? null,
|
||||
@@ -125,6 +139,13 @@ export function useAnimalVm(
|
||||
showStandbyState || hasSubmittedCurrentRound || roundPhase !== 'betting'
|
||||
const selectedCellCount = Object.keys(selectionByCell).length
|
||||
|
||||
const clearLongPressTimer = useCallback(() => {
|
||||
if (longPressTimerRef.current !== null) {
|
||||
window.clearTimeout(longPressTimerRef.current)
|
||||
longPressTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (cellWarning === null) {
|
||||
return
|
||||
@@ -166,6 +187,12 @@ export function useAnimalVm(
|
||||
}
|
||||
}, [animalIds, shouldAnimateStandby])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearLongPressTimer()
|
||||
}
|
||||
}, [clearLongPressTimer])
|
||||
|
||||
const handleStart = () => {
|
||||
if (authStatus !== 'authenticated') {
|
||||
notify.warning(t('commonUi.toast.loginRequired'))
|
||||
@@ -179,6 +206,19 @@ export function useAnimalVm(
|
||||
}
|
||||
|
||||
const handleSelect = (animalId: number) => {
|
||||
const suppressedClick = suppressedClickRef.current
|
||||
|
||||
if (suppressedClick !== null) {
|
||||
suppressedClickRef.current = null
|
||||
|
||||
if (
|
||||
suppressedClick.cellId === animalId &&
|
||||
Date.now() <= suppressedClick.expiresAt
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (roundPhase !== 'betting' || lockInteraction) {
|
||||
return
|
||||
}
|
||||
@@ -188,12 +228,9 @@ export function useAnimalVm(
|
||||
return
|
||||
}
|
||||
|
||||
if (selectionByCell[animalId]) {
|
||||
removeSelectionsForCell(animalId)
|
||||
return
|
||||
}
|
||||
const hasExistingSelection = Boolean(selectionByCell[animalId])
|
||||
|
||||
if (selectedCellCount >= maxSelectionCount) {
|
||||
if (!hasExistingSelection && selectedCellCount >= maxSelectionCount) {
|
||||
setCellWarning({
|
||||
cellId: animalId,
|
||||
type: 'limit',
|
||||
@@ -201,9 +238,16 @@ export function useAnimalVm(
|
||||
return
|
||||
}
|
||||
|
||||
const nextBetAmount = (activeChip?.amount ?? 0) * activeBetQuantity
|
||||
const currentSingleBetAmount = selections[0]?.amount ?? 0
|
||||
const nextSingleBetAmount = hasExistingSelection
|
||||
? currentSingleBetAmount + (activeChip?.amount ?? 0)
|
||||
: (activeChip?.amount ?? 0)
|
||||
const nextSelectedCellCount = hasExistingSelection
|
||||
? selectedCellCount
|
||||
: selectedCellCount + 1
|
||||
const nextTotalBetAmount = nextSingleBetAmount * nextSelectedCellCount
|
||||
|
||||
if (tableLimitMax > 0 && totalBetAmount + nextBetAmount > tableLimitMax) {
|
||||
if (tableLimitMax > 0 && nextSingleBetAmount > tableLimitMax) {
|
||||
setCellWarning({
|
||||
cellId: animalId,
|
||||
type: 'betLimit',
|
||||
@@ -211,7 +255,7 @@ export function useAnimalVm(
|
||||
return
|
||||
}
|
||||
|
||||
if (totalBetAmount + nextBetAmount > balance) {
|
||||
if (nextTotalBetAmount > balance) {
|
||||
setCellWarning({
|
||||
cellId: animalId,
|
||||
type: 'balance',
|
||||
@@ -222,12 +266,94 @@ export function useAnimalVm(
|
||||
placeBet(animalId)
|
||||
}
|
||||
|
||||
const handleClearCell = (animalId: number) => {
|
||||
if (lockInteraction || !selectionByCell[animalId]) {
|
||||
return
|
||||
}
|
||||
|
||||
removeSelectionsForCell(animalId)
|
||||
}
|
||||
|
||||
const handleLongPressStart = (
|
||||
animalId: number,
|
||||
event: ReactPointerEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
if (
|
||||
lockInteraction ||
|
||||
!selectionByCell[animalId] ||
|
||||
(event.pointerType === 'mouse' && event.button !== 0)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
clearLongPressTimer()
|
||||
suppressedClickRef.current = null
|
||||
setLongPressCellId(animalId)
|
||||
longPressOriginRef.current = {
|
||||
pointerId: event.pointerId,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
}
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
longPressTimerRef.current = window.setTimeout(() => {
|
||||
suppressedClickRef.current = {
|
||||
cellId: animalId,
|
||||
expiresAt: Date.now() + LONG_PRESS_CLICK_SUPPRESSION_MS,
|
||||
}
|
||||
longPressTimerRef.current = null
|
||||
longPressOriginRef.current = null
|
||||
setLongPressCellId(null)
|
||||
handleClearCell(animalId)
|
||||
|
||||
if (typeof navigator.vibrate === 'function') {
|
||||
navigator.vibrate(30)
|
||||
}
|
||||
}, LONG_PRESS_DURATION_MS)
|
||||
}
|
||||
|
||||
const handleLongPressMove = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
const origin = longPressOriginRef.current
|
||||
|
||||
if (!origin || origin.pointerId !== event.pointerId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(event.clientX - origin.x) > LONG_PRESS_MOVE_TOLERANCE_PX ||
|
||||
Math.abs(event.clientY - origin.y) > LONG_PRESS_MOVE_TOLERANCE_PX
|
||||
) {
|
||||
clearLongPressTimer()
|
||||
longPressOriginRef.current = null
|
||||
setLongPressCellId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLongPressEnd = () => {
|
||||
clearLongPressTimer()
|
||||
longPressOriginRef.current = null
|
||||
setLongPressCellId(null)
|
||||
}
|
||||
|
||||
const handleLongPressCancel = () => {
|
||||
clearLongPressTimer()
|
||||
longPressOriginRef.current = null
|
||||
suppressedClickRef.current = null
|
||||
setLongPressCellId(null)
|
||||
}
|
||||
|
||||
return {
|
||||
cellWarning,
|
||||
handleClearCell,
|
||||
handleLongPressCancel,
|
||||
handleLongPressEnd,
|
||||
handleLongPressMove,
|
||||
handleLongPressStart,
|
||||
handleSelect,
|
||||
handleStart,
|
||||
isRealtimeConnecting,
|
||||
lockInteraction,
|
||||
longPressCellId,
|
||||
longPressDurationMs: LONG_PRESS_DURATION_MS,
|
||||
marqueeId: shouldAnimateStandby ? marqueeId : null,
|
||||
selectionByCell,
|
||||
showStandbyState,
|
||||
|
||||
Reference in New Issue
Block a user