feat(game): 添加游戏桌面组件的警告提示和动画效果
- 在动物游戏中添加余额不足和选择限制的警告提示 - 为警告状态添加震动动画和视觉反馈效果 - 实现倒计时警告状态的动画和样式变化 - 添加胜利和失败状态的历史记录显示 - 为筹码和操作按钮添加禁用状态的视觉效果 - 实现用户信息和流程按钮的交互功能 - 添加自动设置弹窗的打开功能 - 优化游戏阶段切换时的选择清空逻辑 - 添加剩余时间变化的回调函数支持 - 为历史记录添加中奖状态的颜色标识
This commit is contained in:
BIN
src/assets/game/chip-lock.webp
Normal file
BIN
src/assets/game/chip-lock.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
769
src/assets/lottie/down5.json
Normal file
769
src/assets/lottie/down5.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,3 +1,5 @@
|
|||||||
|
import { TriangleAlert } from 'lucide-react'
|
||||||
|
import { motion } from 'motion/react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import diamondIcon from '@/assets/system/diamond.webp'
|
import diamondIcon from '@/assets/system/diamond.webp'
|
||||||
@@ -5,7 +7,11 @@ import { SmartImage } from '@/components/smart-image'
|
|||||||
import { notify } from '@/lib/notify'
|
import { notify } from '@/lib/notify'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useAudioStore, useAuthStore, useModalStore } from '@/store'
|
import { useAudioStore, useAuthStore, useModalStore } from '@/store'
|
||||||
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
|
import {
|
||||||
|
selectSelectionTotal,
|
||||||
|
useGameRoundStore,
|
||||||
|
useGameSessionStore,
|
||||||
|
} from '@/store/game'
|
||||||
|
|
||||||
const animalModules = import.meta.glob('../../../../assets/animal/*.webp', {
|
const animalModules = import.meta.glob('../../../../assets/animal/*.webp', {
|
||||||
eager: true,
|
eager: true,
|
||||||
@@ -44,17 +50,22 @@ function getNextMarqueeId(currentId: number | null) {
|
|||||||
return nextId
|
return nextId
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSelectedLog(
|
function parseBalance(value: string | number | null | undefined) {
|
||||||
selectionByCell: Record<number, { amount: number; count: number }>,
|
if (typeof value === 'number') {
|
||||||
) {
|
return Number.isFinite(value) ? value : 0
|
||||||
return Object.entries(selectionByCell)
|
}
|
||||||
.map(([cellId, value]) => ({
|
|
||||||
字花: String(cellId).padStart(2, '0'),
|
if (typeof value !== 'string') {
|
||||||
筹码: value.amount,
|
return 0
|
||||||
}))
|
}
|
||||||
.sort((left, right) => Number(left.字花) - Number(right.字花))
|
|
||||||
|
const parsed = Number(value)
|
||||||
|
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CellWarningType = 'balance' | 'limit'
|
||||||
|
|
||||||
interface DesktopAnimalProps {
|
interface DesktopAnimalProps {
|
||||||
activeId?: number | null
|
activeId?: number | null
|
||||||
className?: string
|
className?: string
|
||||||
@@ -72,6 +83,7 @@ export function DesktopAnimal({
|
|||||||
}: DesktopAnimalProps) {
|
}: DesktopAnimalProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const authStatus = useAuthStore((state) => state.status)
|
const authStatus = useAuthStore((state) => state.status)
|
||||||
|
const currentUser = useAuthStore((state) => state.currentUser)
|
||||||
const markSoundPlaybackUnlocked = useAudioStore(
|
const markSoundPlaybackUnlocked = useAudioStore(
|
||||||
(state) => state.markSoundPlaybackUnlocked,
|
(state) => state.markSoundPlaybackUnlocked,
|
||||||
)
|
)
|
||||||
@@ -87,6 +99,7 @@ export function DesktopAnimal({
|
|||||||
(state) => state.removeSelectionsForCell,
|
(state) => state.removeSelectionsForCell,
|
||||||
)
|
)
|
||||||
const selections = useGameRoundStore((state) => state.selections)
|
const selections = useGameRoundStore((state) => state.selections)
|
||||||
|
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
||||||
const connection = useGameSessionStore((state) => state.connection)
|
const connection = useGameSessionStore((state) => state.connection)
|
||||||
const requestRealtimeConnection = useGameSessionStore(
|
const requestRealtimeConnection = useGameSessionStore(
|
||||||
(state) => state.requestRealtimeConnection,
|
(state) => state.requestRealtimeConnection,
|
||||||
@@ -97,10 +110,15 @@ export function DesktopAnimal({
|
|||||||
const [marqueeId, setMarqueeId] = useState<number | null>(() =>
|
const [marqueeId, setMarqueeId] = useState<number | null>(() =>
|
||||||
getNextMarqueeId(null),
|
getNextMarqueeId(null),
|
||||||
)
|
)
|
||||||
|
const [cellWarning, setCellWarning] = useState<{
|
||||||
|
cellId: number
|
||||||
|
type: CellWarningType
|
||||||
|
} | null>(null)
|
||||||
const activeChip = useMemo(
|
const activeChip = useMemo(
|
||||||
() => chips.find((chip) => chip.id === activeChipId) ?? chips[0] ?? null,
|
() => chips.find((chip) => chip.id === activeChipId) ?? chips[0] ?? null,
|
||||||
[activeChipId, chips],
|
[activeChipId, chips],
|
||||||
)
|
)
|
||||||
|
const balance = parseBalance(currentUser?.coin)
|
||||||
const selectionByCell = useMemo(() => {
|
const selectionByCell = useMemo(() => {
|
||||||
return selections.reduce<Record<number, { amount: number; count: number }>>(
|
return selections.reduce<Record<number, { amount: number; count: number }>>(
|
||||||
(accumulator, selection) => {
|
(accumulator, selection) => {
|
||||||
@@ -150,30 +168,48 @@ export function DesktopAnimal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSelectedCell(animalId)) {
|
if (isSelectedCell(animalId)) {
|
||||||
const nextSelectionByCell = { ...selectionByCell }
|
|
||||||
delete nextSelectionByCell[animalId]
|
|
||||||
console.log('已选', formatSelectedLog(nextSelectionByCell))
|
|
||||||
removeSelectionsForCell(animalId)
|
removeSelectionsForCell(animalId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCellCount >= maxSelectionCount) {
|
if (selectedCellCount >= maxSelectionCount) {
|
||||||
|
setCellWarning({
|
||||||
|
cellId: animalId,
|
||||||
|
type: 'limit',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalBetAmount + (activeChip?.amount ?? 0) > balance) {
|
||||||
|
setCellWarning({
|
||||||
|
cellId: animalId,
|
||||||
|
type: 'balance',
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
'已选',
|
|
||||||
formatSelectedLog({
|
|
||||||
...selectionByCell,
|
|
||||||
[animalId]: {
|
|
||||||
amount: activeChip?.amount ?? 0,
|
|
||||||
count: 1,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
placeBet(animalId)
|
placeBet(animalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (!showStandbyState) {
|
if (!showStandbyState) {
|
||||||
setMarqueeId(null)
|
setMarqueeId(null)
|
||||||
@@ -208,13 +244,41 @@ export function DesktopAnimal({
|
|||||||
const hasPlacedSelection = Boolean(selectionMeta)
|
const hasPlacedSelection = Boolean(selectionMeta)
|
||||||
const isActive = item.id === activeId || hasPlacedSelection
|
const isActive = item.id === activeId || hasPlacedSelection
|
||||||
const isMarqueeActive = showStandbyState && item.id === marqueeId
|
const isMarqueeActive = showStandbyState && item.id === marqueeId
|
||||||
|
const warningType =
|
||||||
|
cellWarning?.cellId === item.id ? cellWarning.type : null
|
||||||
|
const showCellWarning = warningType !== null
|
||||||
|
const warningLabel =
|
||||||
|
warningType === 'balance'
|
||||||
|
? t('gameDesktop.animal.insufficientBalanceRecharge')
|
||||||
|
: t('gameDesktop.animal.selectionLimitReached')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<motion.button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={lockInteraction}
|
disabled={lockInteraction}
|
||||||
onClick={() => handleSelect(item.id)}
|
onClick={() => handleSelect(item.id)}
|
||||||
|
animate={
|
||||||
|
showCellWarning
|
||||||
|
? {
|
||||||
|
rotate: [0, -1.8, 1.4, -1, 0.6, 0],
|
||||||
|
scale: [1, 1.015, 0.992, 1.01, 1],
|
||||||
|
x: [0, -2, 2, -1, 1, 0],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
rotate: 0,
|
||||||
|
scale: 1,
|
||||||
|
x: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
showCellWarning
|
||||||
|
? {
|
||||||
|
duration: 0.46,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}
|
||||||
|
: { duration: 0.16, ease: 'easeOut' }
|
||||||
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex flex-col items-center overflow-hidden rounded-[calc(var(--design-unit)*18)] border border-transparent transition-[transform,border-color,box-shadow,opacity] duration-150',
|
'relative flex flex-col items-center overflow-hidden rounded-[calc(var(--design-unit)*18)] border border-transparent transition-[transform,border-color,box-shadow,opacity] duration-150',
|
||||||
lockInteraction
|
lockInteraction
|
||||||
@@ -224,18 +288,37 @@ export function DesktopAnimal({
|
|||||||
'border-[rgba(121,255,250,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(85,255,247,0.98),0_0_calc(var(--design-unit)*34)_rgba(39,245,255,0.88),inset_0_0_calc(var(--design-unit)*26)_rgba(112,255,248,0.34)]',
|
'border-[rgba(121,255,250,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(85,255,247,0.98),0_0_calc(var(--design-unit)*34)_rgba(39,245,255,0.88),inset_0_0_calc(var(--design-unit)*26)_rgba(112,255,248,0.34)]',
|
||||||
isActive &&
|
isActive &&
|
||||||
'border-[rgba(255,187,61,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(255,175,52,0.82),0_0_calc(var(--design-unit)*30)_rgba(255,151,15,0.46),inset_0_0_calc(var(--design-unit)*20)_rgba(255,177,70,0.58)]',
|
'border-[rgba(255,187,61,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(255,175,52,0.82),0_0_calc(var(--design-unit)*30)_rgba(255,151,15,0.46),inset_0_0_calc(var(--design-unit)*20)_rgba(255,177,70,0.58)]',
|
||||||
|
showCellWarning &&
|
||||||
|
'border-[rgba(255,92,92,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(255,88,88,0.56),0_0_calc(var(--design-unit)*28)_rgba(255,44,44,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(255,126,126,0.3)]',
|
||||||
!showStandbyState && !hasPlacedSelection && 'opacity-95',
|
!showStandbyState && !hasPlacedSelection && 'opacity-95',
|
||||||
itemClassName,
|
itemClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<motion.span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
animate={
|
||||||
|
showCellWarning
|
||||||
|
? {
|
||||||
|
opacity: [0.45, 1, 0.6, 1, 0.82],
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
showCellWarning
|
||||||
|
? {
|
||||||
|
duration: 0.52,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-none absolute inset-[calc(var(--design-unit)*2)] rounded-[calc(var(--design-unit)*15)] opacity-0 transition-opacity duration-150',
|
'pointer-events-none absolute inset-[calc(var(--design-unit)*2)] rounded-[calc(var(--design-unit)*15)] opacity-0 transition-opacity duration-150',
|
||||||
isMarqueeActive &&
|
isMarqueeActive &&
|
||||||
'bg-[radial-gradient(circle_at_center,rgba(129,255,250,0.48)_0%,rgba(94,255,247,0.18)_38%,rgba(43,236,255,0.08)_56%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(119,255,249,0.98),0_0_calc(var(--design-unit)*28)_rgba(53,246,255,0.9),0_0_calc(var(--design-unit)*44)_rgba(37,241,255,0.58),inset_0_0_calc(var(--design-unit)*20)_rgba(163,255,250,0.52)]',
|
'bg-[radial-gradient(circle_at_center,rgba(129,255,250,0.48)_0%,rgba(94,255,247,0.18)_38%,rgba(43,236,255,0.08)_56%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(119,255,249,0.98),0_0_calc(var(--design-unit)*28)_rgba(53,246,255,0.9),0_0_calc(var(--design-unit)*44)_rgba(37,241,255,0.58),inset_0_0_calc(var(--design-unit)*20)_rgba(163,255,250,0.52)]',
|
||||||
isActive &&
|
isActive &&
|
||||||
'bg-[radial-gradient(circle_at_center,rgba(255,207,116,0.42)_0%,rgba(255,181,61,0.16)_42%,transparent_74%)] opacity-100',
|
'bg-[radial-gradient(circle_at_center,rgba(255,207,116,0.42)_0%,rgba(255,181,61,0.16)_42%,transparent_74%)] opacity-100',
|
||||||
|
showCellWarning &&
|
||||||
|
'bg-[radial-gradient(circle_at_center,rgba(255,106,106,0.34)_0%,rgba(255,58,58,0.18)_42%,rgba(108,0,0,0.2)_78%,transparent_100%)] opacity-100',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!showStandbyState && !hasPlacedSelection ? (
|
{!showStandbyState && !hasPlacedSelection ? (
|
||||||
@@ -252,6 +335,43 @@ export function DesktopAnimal({
|
|||||||
imageClassName,
|
imageClassName,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{showCellWarning ? (
|
||||||
|
<motion.span
|
||||||
|
initial={{ opacity: 0, scale: 0.94 }}
|
||||||
|
animate={{
|
||||||
|
opacity: [0.2, 1, 0.92],
|
||||||
|
scale: [0.96, 1.02, 1],
|
||||||
|
boxShadow: [
|
||||||
|
'inset 0 0 calc(var(--design-unit)*10) rgba(255,108,108,0.16), 0 0 calc(var(--design-unit)*8) rgba(255,60,60,0.12)',
|
||||||
|
'inset 0 0 calc(var(--design-unit)*20) rgba(255,108,108,0.28), 0 0 calc(var(--design-unit)*20) rgba(255,60,60,0.28)',
|
||||||
|
'inset 0 0 calc(var(--design-unit)*16) rgba(255,108,108,0.22), 0 0 calc(var(--design-unit)*18) rgba(255,60,60,0.2)',
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||||
|
className="pointer-events-none absolute inset-[calc(var(--design-unit)*3)] z-30 flex flex-col items-center justify-center gap-design-8 rounded-[calc(var(--design-unit)*15)] border border-[rgba(255,126,126,0.9)] bg-[rgba(61,0,0,0.58)] px-design-10 py-design-10 text-center"
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
animate={{
|
||||||
|
opacity: [0.72, 1, 0.92],
|
||||||
|
scale: [0.94, 1.08, 1],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.38, ease: 'easeOut' }}
|
||||||
|
className="flex h-design-28 w-design-28 items-center justify-center"
|
||||||
|
>
|
||||||
|
<TriangleAlert className="h-design-28 w-design-28 text-[#FFD0D0] drop-shadow-[0_0_calc(var(--design-unit)*8)_rgba(255,92,92,0.45)]" />
|
||||||
|
</motion.span>
|
||||||
|
<motion.span
|
||||||
|
animate={{
|
||||||
|
opacity: [0.7, 1, 0.96],
|
||||||
|
scale: [0.98, 1.03, 1],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.42, ease: 'easeOut' }}
|
||||||
|
className="text-design-16 font-bold leading-tight tracking-[0.04em] text-[#FFE0E0] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(255,132,132,0.42)]"
|
||||||
|
>
|
||||||
|
{warningLabel}
|
||||||
|
</motion.span>
|
||||||
|
</motion.span>
|
||||||
|
) : null}
|
||||||
{hasPlacedSelection ? (
|
{hasPlacedSelection ? (
|
||||||
<span className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center">
|
<span className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center">
|
||||||
<span className="flex min-w-design-96 items-center justify-center gap-design-4 rounded-full border border-[rgba(162,242,255,0.48)] bg-[linear-gradient(180deg,rgba(7,23,34,0.88),rgba(5,14,22,0.96))] px-design-10 py-design-6 shadow-[0_0_calc(var(--design-unit)*18)_rgba(70,245,255,0.18)]">
|
<span className="flex min-w-design-96 items-center justify-center gap-design-4 rounded-full border border-[rgba(162,242,255,0.48)] bg-[linear-gradient(180deg,rgba(7,23,34,0.88),rgba(5,14,22,0.96))] px-design-10 py-design-6 shadow-[0_0_calc(var(--design-unit)*18)_rgba(70,245,255,0.18)]">
|
||||||
@@ -266,7 +386,7 @@ export function DesktopAnimal({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</button>
|
</motion.button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import add from '@/assets/game/add.webp'
|
|||||||
import arrow from '@/assets/game/arrow.webp'
|
import arrow from '@/assets/game/arrow.webp'
|
||||||
import chipBg from '@/assets/game/chip-bg.webp'
|
import chipBg from '@/assets/game/chip-bg.webp'
|
||||||
import chipLineBg from '@/assets/game/chip-line-bg.webp'
|
import chipLineBg from '@/assets/game/chip-line-bg.webp'
|
||||||
|
import chipLock from '@/assets/game/chip-lock.webp'
|
||||||
import confirmBg from '@/assets/game/confirm-bg.webp'
|
import confirmBg from '@/assets/game/confirm-bg.webp'
|
||||||
import confirmRedBg from '@/assets/game/confirm-red-bg.png'
|
import confirmRedBg from '@/assets/game/confirm-red-bg.png'
|
||||||
import controlBg from '@/assets/game/control-bg.png'
|
import controlBg from '@/assets/game/control-bg.png'
|
||||||
@@ -20,6 +21,8 @@ import { cn } from '@/lib/utils'
|
|||||||
export function DesktopControl() {
|
export function DesktopControl() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
|
acceptingBets,
|
||||||
|
actionsEnabled,
|
||||||
canClear,
|
canClear,
|
||||||
chips,
|
chips,
|
||||||
confirmLabel,
|
confirmLabel,
|
||||||
@@ -29,6 +32,7 @@ export function DesktopControl() {
|
|||||||
onChipSelect,
|
onChipSelect,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onClearSelections,
|
onClearSelections,
|
||||||
|
onOpenAutoSetting,
|
||||||
onRepeatSelections,
|
onRepeatSelections,
|
||||||
selectedChipAmountLabel,
|
selectedChipAmountLabel,
|
||||||
selectedChipId,
|
selectedChipId,
|
||||||
@@ -40,11 +44,19 @@ export function DesktopControl() {
|
|||||||
const [confirmClicked, setConfirmClicked] = useState(false)
|
const [confirmClicked, setConfirmClicked] = useState(false)
|
||||||
|
|
||||||
const handleChipClick = (chipId: string) => {
|
const handleChipClick = (chipId: string) => {
|
||||||
|
if (!acceptingBets) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
onChipSelect(chipId)
|
onChipSelect(chipId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleActionClick = useCallback(
|
const handleActionClick = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
|
if (!actionsEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (id === 'clear' && canClear) {
|
if (id === 'clear' && canClear) {
|
||||||
onClearSelections()
|
onClearSelections()
|
||||||
}
|
}
|
||||||
@@ -53,6 +65,10 @@ export function DesktopControl() {
|
|||||||
onRepeatSelections()
|
onRepeatSelections()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (id === 'auto-spin') {
|
||||||
|
onOpenAutoSetting()
|
||||||
|
}
|
||||||
|
|
||||||
setClickedId(id)
|
setClickedId(id)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setClickedId(null)
|
setClickedId(null)
|
||||||
@@ -62,7 +78,13 @@ export function DesktopControl() {
|
|||||||
}, 180)
|
}, 180)
|
||||||
}, 200)
|
}, 200)
|
||||||
},
|
},
|
||||||
[canClear, onClearSelections, onRepeatSelections],
|
[
|
||||||
|
actionsEnabled,
|
||||||
|
canClear,
|
||||||
|
onClearSelections,
|
||||||
|
onOpenAutoSetting,
|
||||||
|
onRepeatSelections,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleConfirmClick = useCallback(() => {
|
const handleConfirmClick = useCallback(() => {
|
||||||
@@ -117,6 +139,7 @@ export function DesktopControl() {
|
|||||||
>
|
>
|
||||||
{chips.map((chip) => {
|
{chips.map((chip) => {
|
||||||
const isSelected = chip.id === selectedChipId
|
const isSelected = chip.id === selectedChipId
|
||||||
|
const showLockedState = !acceptingBets
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
@@ -124,7 +147,8 @@ export function DesktopControl() {
|
|||||||
layout
|
layout
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleChipClick(chip.id)}
|
onClick={() => handleChipClick(chip.id)}
|
||||||
whileTap={{ scale: 0.94 }}
|
disabled={showLockedState}
|
||||||
|
whileTap={showLockedState ? undefined : { scale: 0.94 }}
|
||||||
transition={{
|
transition={{
|
||||||
layout: {
|
layout: {
|
||||||
type: 'spring',
|
type: 'spring',
|
||||||
@@ -134,12 +158,20 @@ export function DesktopControl() {
|
|||||||
duration: 0.26,
|
duration: 0.26,
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
'relative flex h-design-70 w-design-70 shrink-0 cursor-pointer items-center justify-center rounded-full'
|
'relative flex h-design-70 w-design-70 shrink-0 items-center justify-center rounded-full'
|
||||||
|
}
|
||||||
|
style={
|
||||||
|
showLockedState
|
||||||
|
? {
|
||||||
|
WebkitFilter: 'grayscale(100%)',
|
||||||
|
filter: 'grayscale(100%)',
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<motion.span
|
<motion.span
|
||||||
animate={
|
animate={
|
||||||
isSelected
|
isSelected && !showLockedState
|
||||||
? {
|
? {
|
||||||
opacity: [0.22, 0.5, 0.22],
|
opacity: [0.22, 0.5, 0.22],
|
||||||
scaleX: [0.82, 1.02, 0.82],
|
scaleX: [0.82, 1.02, 0.82],
|
||||||
@@ -166,7 +198,7 @@ export function DesktopControl() {
|
|||||||
/>
|
/>
|
||||||
<motion.span
|
<motion.span
|
||||||
animate={
|
animate={
|
||||||
isSelected
|
isSelected && !showLockedState
|
||||||
? {
|
? {
|
||||||
opacity: [0.72, 1, 0.72],
|
opacity: [0.72, 1, 0.72],
|
||||||
scale: [0.96, 1.04, 0.96],
|
scale: [0.96, 1.04, 0.96],
|
||||||
@@ -198,7 +230,7 @@ export function DesktopControl() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
layout
|
layout
|
||||||
animate={
|
animate={
|
||||||
isSelected
|
isSelected && !showLockedState
|
||||||
? {
|
? {
|
||||||
y: [-1, -4, -1],
|
y: [-1, -4, -1],
|
||||||
scale: [1.04, 1.1, 1.04],
|
scale: [1.04, 1.1, 1.04],
|
||||||
@@ -211,8 +243,9 @@ export function DesktopControl() {
|
|||||||
: {
|
: {
|
||||||
y: 0,
|
y: 0,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
filter:
|
filter: showLockedState
|
||||||
'drop-shadow(0 6px 10px rgba(0,0,0,0.34)) drop-shadow(0 2px 4px rgba(255,255,255,0.08))',
|
? 'none'
|
||||||
|
: 'drop-shadow(0 6px 10px rgba(0,0,0,0.34)) drop-shadow(0 2px 4px rgba(255,255,255,0.08))',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transition={{ type: 'spring', stiffness: 380, damping: 24 }}
|
transition={{ type: 'spring', stiffness: 380, damping: 24 }}
|
||||||
@@ -224,6 +257,14 @@ export function DesktopControl() {
|
|||||||
draggable={false}
|
draggable={false}
|
||||||
className={'h-design-70 w-design-70 object-contain'}
|
className={'h-design-70 w-design-70 object-contain'}
|
||||||
/>
|
/>
|
||||||
|
{showLockedState && (
|
||||||
|
<img
|
||||||
|
src={chipLock}
|
||||||
|
alt="chip-locked"
|
||||||
|
draggable={false}
|
||||||
|
className="pointer-events-none absolute left-1/2 top-1/2 z-[12] h-design-54 w-design-54 -translate-x-1/2 -translate-y-1/2 object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
'pointer-events-none absolute inset-x-0 top-1/2 z-[8] -translate-y-[calc(50%-1*var(--design-unit))] text-center text-design-16 font-black leading-none tracking-[0.06em] text-[rgba(96,54,0,0.85)] blur-[1px]'
|
'pointer-events-none absolute inset-x-0 top-1/2 z-[8] -translate-y-[calc(50%-1*var(--design-unit))] text-center text-design-16 font-black leading-none tracking-[0.06em] text-[rgba(96,54,0,0.85)] blur-[1px]'
|
||||||
@@ -304,21 +345,31 @@ export function DesktopControl() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'desktop-control-actions relative z-10 flex h-full w-design-385 shrink-0 items-center bg-center bg-no-repeat pl-design-15',
|
'desktop-control-actions relative z-10 flex h-full w-design-385 shrink-0 items-center bg-center bg-no-repeat pl-design-15',
|
||||||
)}
|
)}
|
||||||
|
style={
|
||||||
|
!actionsEnabled
|
||||||
|
? {
|
||||||
|
WebkitFilter: 'grayscale(100%)',
|
||||||
|
filter: 'grayscale(100%)',
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{ACTION_OPTIONS.map(({ id, labelKey, Icon, bg }) => {
|
{ACTION_OPTIONS.map(({ id, labelKey, Icon, bg }) => {
|
||||||
const isClicked = clickedId === id
|
const isClicked = clickedId === id
|
||||||
const isHiding = hidingId === id
|
const isHiding = hidingId === id
|
||||||
const showBg = isClicked || isHiding
|
const showBg = actionsEnabled && (isClicked || isHiding)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={id}
|
key={id}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={!actionsEnabled}
|
||||||
onClick={() => handleActionClick(id)}
|
onClick={() => handleActionClick(id)}
|
||||||
whileHover={{ y: -1, scale: 1.01 }}
|
whileHover={actionsEnabled ? { y: -1, scale: 1.01 } : undefined}
|
||||||
whileTap={{ scale: 0.96 }}
|
whileTap={actionsEnabled ? { scale: 0.96 } : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex h-full flex-1 cursor-pointer items-center justify-center overflow-hidden',
|
'relative flex h-full flex-1 items-center justify-center overflow-hidden',
|
||||||
|
actionsEnabled ? 'cursor-pointer' : 'cursor-not-allowed',
|
||||||
{ '-translate-x-1.5': id === 'auto-spin' },
|
{ '-translate-x-1.5': id === 'auto-spin' },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface DesktopCountdownProps {
|
|||||||
initialMs?: number
|
initialMs?: number
|
||||||
initialSeconds?: number
|
initialSeconds?: number
|
||||||
onComplete?: () => void
|
onComplete?: () => void
|
||||||
|
onRemainingMsChange?: (remainingMs: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DesktopCountdown({
|
export function DesktopCountdown({
|
||||||
@@ -26,6 +27,7 @@ export function DesktopCountdown({
|
|||||||
initialMs,
|
initialMs,
|
||||||
initialSeconds,
|
initialSeconds,
|
||||||
onComplete,
|
onComplete,
|
||||||
|
onRemainingMsChange,
|
||||||
}: DesktopCountdownProps) {
|
}: DesktopCountdownProps) {
|
||||||
const initialCountdownMs = useMemo(() => {
|
const initialCountdownMs = useMemo(() => {
|
||||||
if (typeof initialMs === 'number') {
|
if (typeof initialMs === 'number') {
|
||||||
@@ -43,7 +45,8 @@ export function DesktopCountdown({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRemainingMs(initialCountdownMs)
|
setRemainingMs(initialCountdownMs)
|
||||||
}, [initialCountdownMs])
|
onRemainingMsChange?.(initialCountdownMs)
|
||||||
|
}, [initialCountdownMs, onRemainingMsChange])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialCountdownMs <= 0) {
|
if (initialCountdownMs <= 0) {
|
||||||
@@ -58,6 +61,7 @@ export function DesktopCountdown({
|
|||||||
const nextRemainingMs = Math.max(0, initialCountdownMs - elapsedMs)
|
const nextRemainingMs = Math.max(0, initialCountdownMs - elapsedMs)
|
||||||
|
|
||||||
setRemainingMs(nextRemainingMs)
|
setRemainingMs(nextRemainingMs)
|
||||||
|
onRemainingMsChange?.(nextRemainingMs)
|
||||||
|
|
||||||
if (nextRemainingMs === 0) {
|
if (nextRemainingMs === 0) {
|
||||||
window.clearInterval(timer)
|
window.clearInterval(timer)
|
||||||
@@ -68,12 +72,12 @@ export function DesktopCountdown({
|
|||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(timer)
|
window.clearInterval(timer)
|
||||||
}
|
}
|
||||||
}, [initialCountdownMs, onComplete])
|
}, [initialCountdownMs, onComplete, onRemainingMsChange])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-countdown text-design-48 leading-none tracking-[0.08em] text-[#4BFFFE]',
|
'relative z-10 flex items-center justify-center font-countdown text-design-48 leading-none tracking-[0.08em] text-[#4BFFFE]',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -80,23 +80,23 @@ export function DesktopGameHistory() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={
|
className="common-neon-inset w-full !rounded-b-none text-center text-design-20 font-bold tracking-[0.08em]"
|
||||||
'common-neon-inset w-full !rounded-b-none text-center text-design-20'
|
style={{
|
||||||
}
|
color: item.isWin ? '#FFE375' : '#8DFF98',
|
||||||
|
textShadow: item.isWin
|
||||||
|
? '0 0 calc(var(--design-unit)*10) #FFE375, 0 0 calc(var(--design-unit)*22) rgba(255,227,117,0.48)'
|
||||||
|
: '0 0 calc(var(--design-unit)*10) #8DFF98, 0 0 calc(var(--design-unit)*22) rgba(141,255,152,0.48)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.statusLabel}
|
{item.isWin
|
||||||
|
? t('gameDesktop.history.win')
|
||||||
|
: t('gameDesktop.history.lost')}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex w-full flex-col gap-design-5 px-design-10 py-design-10 text-design-16'
|
'flex w-full flex-col gap-design-5 px-design-10 py-design-10 text-design-16'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<span className={'text-[#84A2A2]'}>
|
|
||||||
{t('gameDesktop.history.orderNo')}:{' '}
|
|
||||||
</span>
|
|
||||||
<span className={'text-[#C0E7EB]'}>{item.orderNo}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span className={'text-[#84A2A2]'}>
|
<span className={'text-[#84A2A2]'}>
|
||||||
{t('gameDesktop.history.roundId')}:{' '}
|
{t('gameDesktop.history.roundId')}:{' '}
|
||||||
@@ -109,12 +109,6 @@ export function DesktopGameHistory() {
|
|||||||
</span>
|
</span>
|
||||||
<span>{item.numbersLabel}</span>
|
<span>{item.numbersLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span className={'text-[#84A2A2]'}>
|
|
||||||
{t('gameDesktop.history.settledAt')}:{' '}
|
|
||||||
</span>
|
|
||||||
<span>{item.createdAtLabel}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span className={'text-[#84A2A2]'}>
|
<span className={'text-[#84A2A2]'}>
|
||||||
{t('gameDesktop.history.totalPoolAmount')}:{' '}
|
{t('gameDesktop.history.totalPoolAmount')}:{' '}
|
||||||
@@ -131,12 +125,6 @@ export function DesktopGameHistory() {
|
|||||||
{item.resultNumberLabel}
|
{item.resultNumberLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span className={'text-[#84A2A2]'}>
|
|
||||||
{t('gameDesktop.history.payout')}:{' '}
|
|
||||||
</span>
|
|
||||||
<span>{item.winAmountLabel}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ export function DesktopHeader() {
|
|||||||
const connection = useGameSessionStore((state) => state.connection)
|
const connection = useGameSessionStore((state) => state.connection)
|
||||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||||
const { currentLanguageLabel, currentLanguageOption } = useAppLanguage()
|
const { currentLanguageLabel, currentLanguageOption } = useAppLanguage()
|
||||||
|
const handleOpenUserInfo = () => {
|
||||||
|
setModalOpen('desktopUserInfo', true)
|
||||||
|
}
|
||||||
|
const handleOpenProcedures = () => {
|
||||||
|
setModalOpen('desktopProcedures', true)
|
||||||
|
}
|
||||||
|
|
||||||
const serverClockOffsetMs = useMemo(() => {
|
const serverClockOffsetMs = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
@@ -368,7 +374,11 @@ export function DesktopHeader() {
|
|||||||
'flex items-center justify-center gap-design-30 pl-design-30 pr-design-10'
|
'flex items-center justify-center gap-design-30 pl-design-30 pr-design-10'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'relative flex items-center justify-center'}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleOpenUserInfo}
|
||||||
|
className="group relative flex items-center justify-center transition-transform duration-150 hover:-translate-y-[1px] active:translate-y-[1px]"
|
||||||
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
src={avatar}
|
src={avatar}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
@@ -377,14 +387,18 @@ export function DesktopHeader() {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'common-neon-inset text-design-16 !py-design-20 flex h-design-36 w-design-180 items-center justify-end'
|
'common-neon-inset text-design-16 !py-design-20 flex h-design-36 w-design-180 items-center justify-end transition-[opacity,transform] duration-150 group-hover:opacity-90 group-active:scale-[0.98]'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{currentUser?.username || '--'}
|
{currentUser?.username || '--'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<div className={'relative flex items-center justify-center'}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleOpenProcedures}
|
||||||
|
className="group relative flex items-center justify-center transition-transform duration-150 hover:-translate-y-[1px] active:translate-y-[1px]"
|
||||||
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
src={diamond}
|
src={diamond}
|
||||||
alt="diamond"
|
alt="diamond"
|
||||||
@@ -393,12 +407,12 @@ export function DesktopHeader() {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'common-neon-inset text-design-16 !py-design-20 box-border flex h-design-36 w-design-180 items-center justify-end'
|
'common-neon-inset text-design-16 !py-design-20 box-border flex h-design-36 w-design-180 items-center justify-end transition-[opacity,transform] duration-150 group-hover:opacity-90 group-active:scale-[0.98]'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{currentUser?.coin || '--'}
|
{currentUser?.coin || '--'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import down5Animation from '@/assets/lottie/down5.json'
|
||||||
import statusCenter from '@/assets/system/status-center.webp'
|
import statusCenter from '@/assets/system/status-center.webp'
|
||||||
import statusLine from '@/assets/system/status-line.webp'
|
import statusLine from '@/assets/system/status-line.webp'
|
||||||
|
import { LottiePlayer } from '@/components/lottie-player.tsx'
|
||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
import { DesktopCountdown } from '@/features/game/components/desktop/desktop-countdown.tsx'
|
import { DesktopCountdown } from '@/features/game/components/desktop/desktop-countdown.tsx'
|
||||||
import { DesktopTitle } from '@/features/game/components/desktop/desktop-title.tsx'
|
import { DesktopTitle } from '@/features/game/components/desktop/desktop-title.tsx'
|
||||||
@@ -18,6 +21,15 @@ export function DesktopStatusLine() {
|
|||||||
roundId,
|
roundId,
|
||||||
streakLabel,
|
streakLabel,
|
||||||
} = useGameStatusVm()
|
} = useGameStatusVm()
|
||||||
|
const [remainingMs, setRemainingMs] = useState(countdownMs)
|
||||||
|
const showWarningCountdown = remainingMs <= 5000 && remainingMs > 0
|
||||||
|
const countdownClassName = useMemo(
|
||||||
|
() =>
|
||||||
|
showWarningCountdown
|
||||||
|
? 'text-[#FF5A5A] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(255,90,90,0.85),0_0_calc(var(--design-unit)*22)_rgba(255,90,90,0.32)]'
|
||||||
|
: 'text-[#4BFFFE] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(75,255,254,0.85),0_0_calc(var(--design-unit)*22)_rgba(75,255,254,0.32)]',
|
||||||
|
[showWarningCountdown],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'relative w-full flex flex-col text-design-22'}>
|
<div className={'relative w-full flex flex-col text-design-22'}>
|
||||||
@@ -39,18 +51,38 @@ export function DesktopStatusLine() {
|
|||||||
{t('gameDesktop.status.limit')}: {limitLabel}
|
{t('gameDesktop.status.limit')}: {limitLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartBackground
|
<div className="relative flex h-[105px] w-design-360 items-center justify-center">
|
||||||
src={statusCenter}
|
<SmartBackground
|
||||||
className="w-design-360 h-[105px] font-countdown bg-no-repeat bg-center bg-contain flex items-center justify-center"
|
src={statusCenter}
|
||||||
size="contain"
|
className="pointer-events-none absolute inset-0 z-0 bg-no-repeat bg-center bg-contain transition-opacity duration-500 ease-out"
|
||||||
>
|
size="contain"
|
||||||
<DesktopCountdown
|
style={{
|
||||||
initialMs={countdownMs}
|
opacity: showWarningCountdown ? 0.18 : 1,
|
||||||
onComplete={() => {
|
transform: showWarningCountdown ? 'scale(0.985)' : 'scale(1)',
|
||||||
console.log('countdown finished')
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SmartBackground>
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0 z-0 overflow-visible transition-all duration-500 ease-out"
|
||||||
|
style={{
|
||||||
|
opacity: showWarningCountdown ? 1 : 0,
|
||||||
|
transform: showWarningCountdown
|
||||||
|
? 'scale(1.1) translateY(calc(var(--design-unit)*1))'
|
||||||
|
: 'scale(1.02) translateY(0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LottiePlayer
|
||||||
|
animationData={down5Animation}
|
||||||
|
className="h-full w-full"
|
||||||
|
loop
|
||||||
|
autoplay
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DesktopCountdown
|
||||||
|
initialMs={countdownMs}
|
||||||
|
onRemainingMsChange={setRemainingMs}
|
||||||
|
className={countdownClassName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className={'flex-1 flex items-center justify-center gap-10'}>
|
<div className={'flex-1 flex items-center justify-center gap-10'}>
|
||||||
<div>
|
<div>
|
||||||
{t('gameDesktop.status.roundId')}:{roundId}
|
{t('gameDesktop.status.roundId')}:{roundId}
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { CHIP_IMAGE_MAP, CHIP_IMAGE_OPTIONS } from '@/constants'
|
|||||||
import { placeGameBet } from '@/features/game'
|
import { placeGameBet } from '@/features/game'
|
||||||
import { notify } from '@/lib/notify'
|
import { notify } from '@/lib/notify'
|
||||||
import { useAuthStore, useModalStore } from '@/store'
|
import { useAuthStore, useModalStore } from '@/store'
|
||||||
import { selectSelectionTotal, useGameRoundStore } from '@/store/game'
|
import {
|
||||||
|
selectSelectionTotal,
|
||||||
|
useGameRoundStore,
|
||||||
|
useGameSessionStore,
|
||||||
|
} from '@/store/game'
|
||||||
|
|
||||||
type ConfirmState = 'idle' | 'ready' | 'insufficient' | 'submitting'
|
type ConfirmState = 'idle' | 'ready' | 'insufficient' | 'submitting'
|
||||||
|
|
||||||
@@ -71,6 +75,12 @@ export function useGameControlVm() {
|
|||||||
)
|
)
|
||||||
const selectChip = useGameRoundStore((state) => state.selectChip)
|
const selectChip = useGameRoundStore((state) => state.selectChip)
|
||||||
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
||||||
|
const connectionStatus = useGameSessionStore(
|
||||||
|
(state) => state.connection.status,
|
||||||
|
)
|
||||||
|
const shouldConnectRealtime = useGameSessionStore(
|
||||||
|
(state) => state.shouldConnectRealtime,
|
||||||
|
)
|
||||||
const authStatus = useAuthStore((state) => state.status)
|
const authStatus = useAuthStore((state) => state.status)
|
||||||
const currentUser = useAuthStore((state) => state.currentUser)
|
const currentUser = useAuthStore((state) => state.currentUser)
|
||||||
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
||||||
@@ -99,6 +109,8 @@ export function useGameControlVm() {
|
|||||||
chipItems.find((chip) => chip.id === activeChipId) ?? chipItems[0] ?? null
|
chipItems.find((chip) => chip.id === activeChipId) ?? chipItems[0] ?? null
|
||||||
const balance = parseBalance(currentUser?.coin)
|
const balance = parseBalance(currentUser?.coin)
|
||||||
const hasSelections = selections.length > 0
|
const hasSelections = selections.length > 0
|
||||||
|
const hasEnteredGame =
|
||||||
|
shouldConnectRealtime && connectionStatus === 'connected'
|
||||||
const hasInsufficientBalance = hasSelections && totalBetAmount > balance
|
const hasInsufficientBalance = hasSelections && totalBetAmount > balance
|
||||||
const confirmState: ConfirmState = isSubmitting
|
const confirmState: ConfirmState = isSubmitting
|
||||||
? 'submitting'
|
? 'submitting'
|
||||||
@@ -235,7 +247,13 @@ export function useGameControlVm() {
|
|||||||
notify.success(t('commonUi.toast.repeatSelectionsRestored'))
|
notify.success(t('commonUi.toast.repeatSelectionsRestored'))
|
||||||
}, [restoreRecentSuccessfulSelections, round.phase, t])
|
}, [restoreRecentSuccessfulSelections, round.phase, t])
|
||||||
|
|
||||||
|
const handleOpenAutoSetting = useCallback(() => {
|
||||||
|
setModalOpen('desktopAutoSetting', true)
|
||||||
|
}, [setModalOpen])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
acceptingBets: round.phase === 'betting',
|
||||||
|
actionsEnabled: hasEnteredGame,
|
||||||
canClear: selections.length > 0,
|
canClear: selections.length > 0,
|
||||||
confirmLabel:
|
confirmLabel:
|
||||||
confirmState === 'idle'
|
confirmState === 'idle'
|
||||||
@@ -250,6 +268,7 @@ export function useGameControlVm() {
|
|||||||
onChipSelect: selectChip,
|
onChipSelect: selectChip,
|
||||||
onConfirm: handleConfirm,
|
onConfirm: handleConfirm,
|
||||||
onClearSelections: clearSelections,
|
onClearSelections: clearSelections,
|
||||||
|
onOpenAutoSetting: handleOpenAutoSetting,
|
||||||
onRepeatSelections: handleRepeatSelections,
|
onRepeatSelections: handleRepeatSelections,
|
||||||
maxSelectionCountLabel: maxSelectionCount,
|
maxSelectionCountLabel: maxSelectionCount,
|
||||||
selectedChipAmountLabel: selectedChip?.valueLabel ?? '--',
|
selectedChipAmountLabel: selectedChip?.valueLabel ?? '--',
|
||||||
|
|||||||
@@ -69,14 +69,17 @@ export function useGameHistoryVm() {
|
|||||||
i18n.resolvedLanguage ?? 'en-US',
|
i18n.resolvedLanguage ?? 'en-US',
|
||||||
),
|
),
|
||||||
id: entry.order_no,
|
id: entry.order_no,
|
||||||
|
isWin:
|
||||||
|
entry.result_number !== null &&
|
||||||
|
entry.numbers.includes(entry.result_number),
|
||||||
numbersLabel: formatNumbers(entry.numbers),
|
numbersLabel: formatNumbers(entry.numbers),
|
||||||
|
numbers: entry.numbers,
|
||||||
orderNo: entry.order_no,
|
orderNo: entry.order_no,
|
||||||
periodNo: entry.period_no,
|
periodNo: entry.period_no,
|
||||||
resultNumberLabel:
|
resultNumberLabel:
|
||||||
entry.result_number === null
|
entry.result_number === null
|
||||||
? '--'
|
? '--'
|
||||||
: String(entry.result_number).padStart(2, '0'),
|
: String(entry.result_number).padStart(2, '0'),
|
||||||
statusLabel: entry.status,
|
|
||||||
winAmountLabel: entry.win_amount,
|
winAmountLabel: entry.win_amount,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -362,12 +362,16 @@ export default {
|
|||||||
announcement: 'Announcement',
|
announcement: 'Announcement',
|
||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
|
insufficientBalanceRecharge: 'Insufficient balance, please top up',
|
||||||
loading: 'Loading',
|
loading: 'Loading',
|
||||||
|
selectionLimitReached: 'Selection limit exceeded',
|
||||||
tapToEnter: 'Tap To Enter',
|
tapToEnter: 'Tap To Enter',
|
||||||
getStart: 'Get Start',
|
getStart: 'Get Start',
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
title: 'History',
|
title: 'History',
|
||||||
|
win: 'WIN',
|
||||||
|
lost: 'LOST',
|
||||||
orderNo: 'Order No.',
|
orderNo: 'Order No.',
|
||||||
roundId: 'Round ID',
|
roundId: 'Round ID',
|
||||||
numbers: 'Bet Numbers',
|
numbers: 'Bet Numbers',
|
||||||
|
|||||||
@@ -361,12 +361,16 @@ export default {
|
|||||||
announcement: 'Pengumuman',
|
announcement: 'Pengumuman',
|
||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
|
insufficientBalanceRecharge: 'Saldo tidak cukup, silakan isi ulang',
|
||||||
loading: 'Memuat',
|
loading: 'Memuat',
|
||||||
|
selectionLimitReached: 'Melebihi pilihan yang diizinkan',
|
||||||
tapToEnter: 'Ketuk Untuk Masuk',
|
tapToEnter: 'Ketuk Untuk Masuk',
|
||||||
getStart: 'Mulai',
|
getStart: 'Mulai',
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
title: 'Riwayat',
|
title: 'Riwayat',
|
||||||
|
win: 'WIN',
|
||||||
|
lost: 'LOST',
|
||||||
orderNo: 'No. Order',
|
orderNo: 'No. Order',
|
||||||
roundId: 'ID Ronde',
|
roundId: 'ID Ronde',
|
||||||
numbers: 'Nomor Taruhan',
|
numbers: 'Nomor Taruhan',
|
||||||
|
|||||||
@@ -365,12 +365,16 @@ export default {
|
|||||||
announcement: 'Pengumuman',
|
announcement: 'Pengumuman',
|
||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
|
insufficientBalanceRecharge: 'Baki tidak mencukupi, sila tambah nilai',
|
||||||
loading: 'Memuatkan',
|
loading: 'Memuatkan',
|
||||||
|
selectionLimitReached: 'Melebihi pilihan aksara yang dibenarkan',
|
||||||
tapToEnter: 'Ketik Untuk Masuk',
|
tapToEnter: 'Ketik Untuk Masuk',
|
||||||
getStart: 'Mula',
|
getStart: 'Mula',
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
title: 'Sejarah',
|
title: 'Sejarah',
|
||||||
|
win: 'WIN',
|
||||||
|
lost: 'LOST',
|
||||||
orderNo: 'No. Pesanan',
|
orderNo: 'No. Pesanan',
|
||||||
roundId: 'ID Pusingan',
|
roundId: 'ID Pusingan',
|
||||||
numbers: 'Nombor Pertaruhan',
|
numbers: 'Nombor Pertaruhan',
|
||||||
|
|||||||
@@ -352,12 +352,16 @@ export default {
|
|||||||
announcement: '公告栏',
|
announcement: '公告栏',
|
||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
|
insufficientBalanceRecharge: '余额不足,请充值',
|
||||||
loading: '加载中',
|
loading: '加载中',
|
||||||
|
selectionLimitReached: '超过可选择字花',
|
||||||
tapToEnter: '点击进入',
|
tapToEnter: '点击进入',
|
||||||
getStart: '开始游戏',
|
getStart: '开始游戏',
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
title: '历史记录',
|
title: '历史记录',
|
||||||
|
win: 'WIN',
|
||||||
|
lost: 'LOST',
|
||||||
orderNo: '订单号',
|
orderNo: '订单号',
|
||||||
roundId: '期号',
|
roundId: '期号',
|
||||||
numbers: '下注号码',
|
numbers: '下注号码',
|
||||||
|
|||||||
@@ -203,20 +203,38 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
setPhase: (phase) => {
|
setPhase: (phase) => {
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
round: {
|
const nextState: Partial<GameRoundStoreState> = {
|
||||||
...state.round,
|
round: {
|
||||||
phase,
|
...state.round,
|
||||||
},
|
phase,
|
||||||
}))
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase === 'settled') {
|
||||||
|
nextState.selections = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextState
|
||||||
|
})
|
||||||
},
|
},
|
||||||
syncRound: (round) => {
|
syncRound: (round) => {
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
round: {
|
const nextRound = {
|
||||||
...state.round,
|
...state.round,
|
||||||
...round,
|
...round,
|
||||||
},
|
}
|
||||||
}))
|
|
||||||
|
const nextState: Partial<GameRoundStoreState> = {
|
||||||
|
round: nextRound,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextRound.phase === 'settled') {
|
||||||
|
nextState.selections = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextState
|
||||||
|
})
|
||||||
},
|
},
|
||||||
upsertSelections: (selections) => {
|
upsertSelections: (selections) => {
|
||||||
set({ selections })
|
set({ selections })
|
||||||
|
|||||||
Reference in New Issue
Block a user