feat(game): 更新马来西亚手机号验证和游戏界面优化
- 修改认证模块手机号验证规则适配马来西亚号码格式 - 添加新的投注限制提示文本支持多语言 - 重命名结算阶段标签为Drawing统一显示 - 新增桌面版动物游戏遮罩组件分离功能 - 添加回合开始投注提醒弹窗组件 - 优化开奖动画流程和视觉效果 - 添加奖励动画显示和投注汇总展示 - 新增多种投注限制和状态提示信息
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -1,12 +1,12 @@
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
const mobilePhonePattern = /^1[3-9]\d{9}$/
|
const malaysiaMobilePhonePattern = /^60\d{1,9}$/
|
||||||
|
|
||||||
const usernameSchema = z
|
const usernameSchema = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1, 'auth.validation.username.required')
|
.min(1, 'auth.validation.username.required')
|
||||||
.regex(mobilePhonePattern, 'auth.validation.username.invalidPhone')
|
.regex(malaysiaMobilePhonePattern, 'auth.validation.username.invalidPhone')
|
||||||
|
|
||||||
const passwordSchema = z
|
const passwordSchema = z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
500
src/features/game/components/desktop/desktop-animal-overlay.tsx
Normal file
500
src/features/game/components/desktop/desktop-animal-overlay.tsx
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
import { motion, useReducedMotion } from 'motion/react'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import enStopImage from '@/assets/game/en-stop.webp'
|
||||||
|
import hostingBg from '@/assets/game/hosting-bg.webp'
|
||||||
|
import hostingBtn from '@/assets/game/hosting-btn.webp'
|
||||||
|
import winLogo from '@/assets/game/win.webp'
|
||||||
|
import winBg from '@/assets/game/win-bg.webp'
|
||||||
|
import zhStopImage from '@/assets/game/zh-stop.webp'
|
||||||
|
import bigRewardPath from '@/assets/lottie/pc-big-reward.json?url'
|
||||||
|
import smallRewardPath from '@/assets/lottie/pc-small-reward.json?url'
|
||||||
|
import diamondIcon from '@/assets/system/diamond.webp'
|
||||||
|
import refreshIcon from '@/assets/system/refresh.webp'
|
||||||
|
import type { FullscreenLottieSource } from '@/components/fullscreen-lottie-overlay.types.ts'
|
||||||
|
import { LottiePlayer } from '@/components/lottie-player.tsx'
|
||||||
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
|
import { SmartImage } from '@/components/smart-image'
|
||||||
|
import { REWARD_OVERLAY_DURATION_MS } from '@/constants'
|
||||||
|
import {
|
||||||
|
type BetSelection,
|
||||||
|
FLOWER_IMAGE_BY_ID,
|
||||||
|
groupSelectionsByCell,
|
||||||
|
} from '@/features/game/shared'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
import {
|
||||||
|
type RewardAnimationType,
|
||||||
|
useGameAutoHostingStore,
|
||||||
|
useGameRoundStore,
|
||||||
|
} from '@/store/game'
|
||||||
|
|
||||||
|
const REWARD_OVERLAY_FADE_OUT_MS = 300
|
||||||
|
const REWARD_CHILDREN_FADE_IN_MS = 2_000
|
||||||
|
const REWARD_CHILDREN_VISIBLE_MS = 1_000
|
||||||
|
|
||||||
|
type RewardChildrenStage = 'hidden' | 'visible' | 'exiting'
|
||||||
|
type StopBetItem = {
|
||||||
|
amount: number
|
||||||
|
cellId: number
|
||||||
|
imageUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DesktopAnimalOverlayProps {
|
||||||
|
showStopOverlay: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function easeOutCubic(progress: number) {
|
||||||
|
return 1 - (1 - progress) ** 3
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAmountMeta(amount: string | null) {
|
||||||
|
if (!amount) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedAmount = amount.replace(/,/g, '')
|
||||||
|
const numericAmount = Number(normalizedAmount)
|
||||||
|
|
||||||
|
if (!Number.isFinite(numericAmount)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fractionDigits = normalizedAmount.includes('.')
|
||||||
|
? (normalizedAmount.split('.')[1]?.length ?? 0)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
fractionDigits,
|
||||||
|
numericAmount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRewardAmount(value: number, fractionDigits: number) {
|
||||||
|
return value.toLocaleString('en-US', {
|
||||||
|
maximumFractionDigits: fractionDigits,
|
||||||
|
minimumFractionDigits: fractionDigits,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRewardSource(
|
||||||
|
rewardType: RewardAnimationType,
|
||||||
|
): FullscreenLottieSource | null {
|
||||||
|
if (rewardType === 'small') {
|
||||||
|
return {
|
||||||
|
id: 'pc-small-reward',
|
||||||
|
path: smallRewardPath,
|
||||||
|
loop: false,
|
||||||
|
autoplay: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rewardType === 'big') {
|
||||||
|
return {
|
||||||
|
id: 'pc-big-reward',
|
||||||
|
path: bigRewardPath,
|
||||||
|
loop: false,
|
||||||
|
autoplay: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatStopBetAmount(amount: number) {
|
||||||
|
return amount.toLocaleString('en-US', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
minimumFractionDigits: Number.isInteger(amount) ? 0 : 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStopBetItems(selections: BetSelection[]) {
|
||||||
|
const groupedSelections = groupSelectionsByCell(selections)
|
||||||
|
|
||||||
|
return Object.entries(groupedSelections)
|
||||||
|
.map(([cellId, meta]) => {
|
||||||
|
const normalizedCellId = Number(cellId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: meta.amount,
|
||||||
|
cellId: normalizedCellId,
|
||||||
|
imageUrl: FLOWER_IMAGE_BY_ID[normalizedCellId]?.animalUrl ?? '',
|
||||||
|
} satisfies StopBetItem
|
||||||
|
})
|
||||||
|
.sort((left, right) => left.cellId - right.cellId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StopBetSummary({
|
||||||
|
items,
|
||||||
|
noBetText,
|
||||||
|
}: {
|
||||||
|
items: StopBetItem[]
|
||||||
|
noBetText: string
|
||||||
|
}) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="game-chip-glow rounded-[calc(var(--design-unit)*18)] border border-[rgba(124,232,255,0.22)] bg-[linear-gradient(180deg,rgba(8,34,42,0.94),rgba(3,13,20,0.96))] px-design-42 py-design-14 text-design-28 font-black tracking-[0.14em] text-[#E8FFFF] [text-shadow:0_0_calc(var(--design-unit)*12)_rgba(70,245,255,0.34)]">
|
||||||
|
{noBetText}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex max-h-design-194 max-w-design-850 flex-wrap items-center justify-center gap-design-16 overflow-hidden rounded-[calc(var(--design-unit)*18)] border border-[rgba(124,232,255,0.28)] bg-[linear-gradient(180deg,rgba(10,31,42,0.84),rgba(3,12,18,0.74))] px-design-16 py-design-14 shadow-[0_0_calc(var(--design-unit)*24)_rgba(34,214,255,0.2),0_calc(var(--design-unit)*12)_calc(var(--design-unit)*24)_rgba(0,0,0,0.34),inset_0_calc(var(--design-unit)*1)_0_rgba(255,255,255,0.08),inset_0_calc(var(--design-unit)*-1)_0_rgba(124,232,255,0.18)] ring-1 ring-inset ring-[rgba(3,12,18,0.7)] backdrop-blur-[1px]">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.cellId}
|
||||||
|
className="flex h-design-90 min-w-design-230 items-center gap-design-10 overflow-hidden rounded-[calc(var(--design-unit)*12)] border border-[rgba(124,232,255,0.2)] bg-[linear-gradient(90deg,rgba(7,42,38,0.94),rgba(5,23,34,0.96)_44%,rgba(18,25,25,0.94))] px-design-8 shadow-[0_0_calc(var(--design-unit)*16)_rgba(51,231,255,0.16),inset_0_calc(var(--design-unit)*1)_0_rgba(255,255,255,0.08)]"
|
||||||
|
>
|
||||||
|
<div className="relative h-design-72 w-design-72 shrink-0 overflow-hidden rounded-[calc(var(--design-unit)*10)] bg-[rgba(1,8,13,0.84)] shadow-[0_0_calc(var(--design-unit)*12)_rgba(86,247,255,0.18)]">
|
||||||
|
{item.imageUrl ? (
|
||||||
|
<SmartImage
|
||||||
|
src={item.imageUrl}
|
||||||
|
alt=""
|
||||||
|
showSkeleton={false}
|
||||||
|
className="h-full w-full"
|
||||||
|
imgClassName="object-cover"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="flex h-design-72 min-w-0 flex-1 flex-col items-stretch justify-center gap-design-7 leading-none">
|
||||||
|
<div className="flex h-design-28 w-full items-center justify-center rounded-full bg-[rgba(4,31,31,0.9)] text-design-24 font-black leading-none text-[#78FF7F] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(120,255,127,0.08)] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(120,255,127,0.52)]">
|
||||||
|
{String(item.cellId).padStart(2, '0')}
|
||||||
|
</div>
|
||||||
|
<div className="flex h-design-32 w-full min-w-0 items-center justify-center gap-design-4 rounded-full bg-[linear-gradient(180deg,rgba(7,23,34,0.88),rgba(5,14,22,0.96))] px-design-8 shadow-[0_0_calc(var(--design-unit)*14)_rgba(70,245,255,0.16)]">
|
||||||
|
<SmartImage
|
||||||
|
src={diamondIcon}
|
||||||
|
alt=""
|
||||||
|
showSkeleton={false}
|
||||||
|
className="h-design-20 w-design-20 shrink-0 object-contain"
|
||||||
|
/>
|
||||||
|
<span className="min-w-0 truncate text-design-22 font-black leading-none tabular-nums text-[#FFE58A] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(255,222,130,0.46)]">
|
||||||
|
{formatStopBetAmount(item.amount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DesktopAnimalOverlay({
|
||||||
|
showStopOverlay,
|
||||||
|
}: DesktopAnimalOverlayProps) {
|
||||||
|
const { i18n, t } = useTranslation()
|
||||||
|
const prefersReducedMotion = useReducedMotion()
|
||||||
|
const completedAutoHostingRounds = useGameAutoHostingStore(
|
||||||
|
(state) => state.completedRounds,
|
||||||
|
)
|
||||||
|
const currentRoundId = useGameRoundStore((state) => state.round.id)
|
||||||
|
const lastBetPeriodNo = useAuthStore(
|
||||||
|
(state) => state.currentUser?.lastBetPeriodNo,
|
||||||
|
)
|
||||||
|
const hostingFlag = useGameAutoHostingStore((state) => state.isHosting)
|
||||||
|
const stopHosting = useGameAutoHostingStore((state) => state.stopHosting)
|
||||||
|
const selections = useGameRoundStore((state) => state.selections)
|
||||||
|
const recentSuccessfulSelections = useGameRoundStore(
|
||||||
|
(state) => state.recentSuccessfulSelections,
|
||||||
|
)
|
||||||
|
const rewardType = useGameRoundStore(
|
||||||
|
(state) => state.revealAnimation.rewardType,
|
||||||
|
)
|
||||||
|
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
||||||
|
const rewardAmount = useGameRoundStore(
|
||||||
|
(state) => state.revealAnimation.rewardAmount,
|
||||||
|
)
|
||||||
|
const revealKey = useGameRoundStore(
|
||||||
|
(state) => state.revealAnimation.revealKey,
|
||||||
|
)
|
||||||
|
const roundId = useGameRoundStore((state) => state.revealAnimation.roundId)
|
||||||
|
const clearRewardAnimation = useGameRoundStore(
|
||||||
|
(state) => state.clearRewardAnimation,
|
||||||
|
)
|
||||||
|
const [isRewardFadingOut, setIsRewardFadingOut] = useState(false)
|
||||||
|
const [childrenStage, setChildrenStage] =
|
||||||
|
useState<RewardChildrenStage>('hidden')
|
||||||
|
const [displayRewardAmount, setDisplayRewardAmount] = useState('0')
|
||||||
|
const [hasRenderedReward, setHasRenderedReward] = useState(false)
|
||||||
|
const stopImageSrc = i18n.resolvedLanguage?.startsWith('zh')
|
||||||
|
? zhStopImage
|
||||||
|
: enStopImage
|
||||||
|
const stopBetItems = useMemo(
|
||||||
|
() =>
|
||||||
|
getStopBetItems(
|
||||||
|
selections.length > 0
|
||||||
|
? selections
|
||||||
|
: lastBetPeriodNo === currentRoundId
|
||||||
|
? recentSuccessfulSelections
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
[currentRoundId, lastBetPeriodNo, recentSuccessfulSelections, selections],
|
||||||
|
)
|
||||||
|
const rewardAmountMeta = useMemo(
|
||||||
|
() => getAmountMeta(rewardAmount),
|
||||||
|
[rewardAmount],
|
||||||
|
)
|
||||||
|
const rewardSource = useMemo(() => getRewardSource(rewardType), [rewardType])
|
||||||
|
const shouldRenderReward =
|
||||||
|
revealPhase === 'result' && rewardType !== 'none' && rewardSource !== null
|
||||||
|
const overlayAnimationKey = `${rewardType}-${roundId ?? 'round'}-${revealKey ?? 'pending'}`
|
||||||
|
const childTimelineKey = shouldRenderReward ? overlayAnimationKey : 'closed'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (revealPhase !== 'result') {
|
||||||
|
setHasRenderedReward(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRenderReward) {
|
||||||
|
setHasRenderedReward(true)
|
||||||
|
}
|
||||||
|
}, [revealPhase, shouldRenderReward])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldRenderReward) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRewardFadingOut(false)
|
||||||
|
|
||||||
|
const fadeTimerId = window.setTimeout(() => {
|
||||||
|
setIsRewardFadingOut(true)
|
||||||
|
}, REWARD_OVERLAY_DURATION_MS)
|
||||||
|
const clearTimerId = window.setTimeout(() => {
|
||||||
|
clearRewardAnimation()
|
||||||
|
}, REWARD_OVERLAY_DURATION_MS + REWARD_OVERLAY_FADE_OUT_MS)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(fadeTimerId)
|
||||||
|
window.clearTimeout(clearTimerId)
|
||||||
|
}
|
||||||
|
}, [clearRewardAnimation, shouldRenderReward])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (childTimelineKey === 'closed') {
|
||||||
|
setChildrenStage('hidden')
|
||||||
|
setDisplayRewardAmount('0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setChildrenStage('hidden')
|
||||||
|
setDisplayRewardAmount(
|
||||||
|
rewardAmountMeta
|
||||||
|
? formatRewardAmount(0, rewardAmountMeta.fractionDigits)
|
||||||
|
: (rewardAmount ?? '0'),
|
||||||
|
)
|
||||||
|
|
||||||
|
const enterFrameId = window.requestAnimationFrame(() => {
|
||||||
|
setChildrenStage('visible')
|
||||||
|
})
|
||||||
|
const exitTimerId = window.setTimeout(() => {
|
||||||
|
setChildrenStage('exiting')
|
||||||
|
}, REWARD_CHILDREN_FADE_IN_MS + REWARD_CHILDREN_VISIBLE_MS)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(enterFrameId)
|
||||||
|
window.clearTimeout(exitTimerId)
|
||||||
|
}
|
||||||
|
}, [childTimelineKey, rewardAmount, rewardAmountMeta])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (childTimelineKey === 'closed') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rewardAmountMeta) {
|
||||||
|
setDisplayRewardAmount(rewardAmount ?? '0')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let animationFrameId = 0
|
||||||
|
const startedAt = performance.now()
|
||||||
|
|
||||||
|
const syncRewardAmount = (now: number) => {
|
||||||
|
const progress = Math.min(
|
||||||
|
(now - startedAt) / REWARD_CHILDREN_FADE_IN_MS,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
setDisplayRewardAmount(
|
||||||
|
progress >= 1
|
||||||
|
? formatRewardAmount(
|
||||||
|
rewardAmountMeta.numericAmount,
|
||||||
|
rewardAmountMeta.fractionDigits,
|
||||||
|
)
|
||||||
|
: formatRewardAmount(
|
||||||
|
rewardAmountMeta.numericAmount * easeOutCubic(progress),
|
||||||
|
rewardAmountMeta.fractionDigits,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
animationFrameId = window.requestAnimationFrame(syncRewardAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animationFrameId = window.requestAnimationFrame(syncRewardAmount)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(animationFrameId)
|
||||||
|
}
|
||||||
|
}, [childTimelineKey, rewardAmount, rewardAmountMeta])
|
||||||
|
|
||||||
|
if (shouldRenderReward && rewardSource) {
|
||||||
|
const playerKey = `${overlayAnimationKey}-${rewardSource.id}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute inset-0 z-50 flex items-center justify-center overflow-hidden bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px] transition-opacity duration-300',
|
||||||
|
isRewardFadingOut && 'pointer-events-none opacity-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(49,208,255,0.12),rgba(3,7,15,0.92)_58%,rgba(2,4,10,0.98)_100%)]"
|
||||||
|
/>
|
||||||
|
<LottiePlayer
|
||||||
|
key={playerKey}
|
||||||
|
path={rewardSource.path}
|
||||||
|
renderer={rewardSource.renderer ?? 'svg'}
|
||||||
|
loop={rewardSource.loop ?? false}
|
||||||
|
autoplay={rewardSource.autoplay ?? true}
|
||||||
|
className="absolute inset-0 h-full w-full [&>svg]:h-full [&>svg]:w-full [&>svg]:object-contain [&_canvas]:h-full [&_canvas]:w-full"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none absolute inset-0 z-10 flex items-center justify-center pb-design-220 transition-[opacity,transform,filter] ease-out',
|
||||||
|
childrenStage === 'visible' || childrenStage === 'exiting'
|
||||||
|
? 'duration-[2000ms]'
|
||||||
|
: 'duration-0',
|
||||||
|
childrenStage === 'visible' &&
|
||||||
|
'translate-y-0 scale-100 opacity-75 blur-none',
|
||||||
|
childrenStage === 'hidden' &&
|
||||||
|
'translate-y-[calc(var(--design-unit)*18)] scale-[0.96] opacity-0 blur-[calc(var(--design-unit)*2)]',
|
||||||
|
childrenStage === 'exiting' &&
|
||||||
|
'translate-y-[calc(var(--design-unit)*-14)] scale-[0.97] opacity-0 blur-[calc(var(--design-unit)*1.5)]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SmartBackground
|
||||||
|
className="flex h-design-175 w-design-900 items-center justify-center gap-design-24 pb-design-50"
|
||||||
|
src={winBg}
|
||||||
|
size="contain"
|
||||||
|
>
|
||||||
|
<SmartImage
|
||||||
|
className="h-design-50 w-design-225 drop-shadow-[0_0_calc(var(--design-unit)*10)_rgba(255,218,122,0.72)]"
|
||||||
|
alt="win"
|
||||||
|
src={winLogo}
|
||||||
|
/>
|
||||||
|
<div className="h-design-50 min-w-design-150 animate-bounce text-center font-sans text-design-56 leading-[calc(var(--design-unit)*50)] font-black tabular-nums text-[#FFE89A] [animation-duration:900ms] [-webkit-text-stroke:calc(var(--design-unit)*1)_#8A3A08] [text-shadow:0_0_calc(var(--design-unit)*6)_rgba(255,236,154,0.95),0_calc(var(--design-unit)*3)_0_#7A2F05,0_0_calc(var(--design-unit)*18)_rgba(255,151,15,0.72)]">
|
||||||
|
{displayRewardAmount}
|
||||||
|
</div>
|
||||||
|
</SmartBackground>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (revealPhase === 'result' && hasRenderedReward) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 z-50 bg-[rgba(2,8,14,0.72)] backdrop-blur-[2px]"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostingFlag) {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center gap-design-22 bg-[rgba(2,8,14,0.38)] px-design-24 backdrop-blur-[1px]">
|
||||||
|
{showStopOverlay ? (
|
||||||
|
<div className="relative h-design-170 w-design-520 max-w-[72%] overflow-visible">
|
||||||
|
<SmartImage
|
||||||
|
src={stopImageSrc}
|
||||||
|
alt="stop betting"
|
||||||
|
priority
|
||||||
|
showSkeleton={false}
|
||||||
|
className="h-full w-full overflow-visible"
|
||||||
|
imgClassName="object-contain drop-shadow-[0_0_calc(var(--design-unit)*22)_rgba(60,235,255,0.28)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<SmartBackground
|
||||||
|
src={hostingBg}
|
||||||
|
size="100% 100%"
|
||||||
|
repeat="no-repeat"
|
||||||
|
position="center"
|
||||||
|
className="h-design-350 w-design-930 flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className={'flex flex-col gap-design-44 items-center'}>
|
||||||
|
<div className={'flex items-center gap-design-20'}>
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
animate={prefersReducedMotion ? undefined : { rotate: 360 }}
|
||||||
|
transition={{
|
||||||
|
duration: 1.4,
|
||||||
|
ease: 'linear',
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
}}
|
||||||
|
className="flex h-design-48 w-design-48 items-center justify-center"
|
||||||
|
>
|
||||||
|
<SmartImage
|
||||||
|
src={refreshIcon}
|
||||||
|
alt=""
|
||||||
|
priority
|
||||||
|
showSkeleton={false}
|
||||||
|
className="h-design-40 w-design-40"
|
||||||
|
imgClassName="object-contain drop-shadow-[0_0_calc(var(--design-unit)*22)_rgba(60,235,255,0.36)]"
|
||||||
|
/>
|
||||||
|
</motion.span>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'text-design-22 text-[#ffffff] font-bold [text-shadow:0_0_calc(var(--design-unit)*12)_rgba(76,236,255,0.45)]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('game.autoSpin.runningRounds', {
|
||||||
|
count: completedAutoHostingRounds,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SmartBackground
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
onClick={stopHosting}
|
||||||
|
src={hostingBtn}
|
||||||
|
size="100% 100%"
|
||||||
|
repeat="no-repeat"
|
||||||
|
position="center"
|
||||||
|
className="h-design-70 w-design-220 flex cursor-pointer items-center justify-center pb-design-4 text-design-24 font-bold text-[#EFFFFF] [text-shadow:0_1px_0_rgba(255,255,255,0.18),0_0_calc(var(--design-unit)*10)_rgba(46,220,255,0.5)] transition-transform hover:-translate-y-[1px] active:translate-y-0"
|
||||||
|
>
|
||||||
|
{t('game.actions.stopAuto')}
|
||||||
|
</SmartBackground>
|
||||||
|
</div>
|
||||||
|
</SmartBackground>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showStopOverlay) {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 z-50 flex items-center justify-center bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px]">
|
||||||
|
<div className="flex max-w-[88%] flex-col items-center justify-center gap-design-18">
|
||||||
|
<StopBetSummary
|
||||||
|
items={stopBetItems}
|
||||||
|
noBetText={t('gameDesktop.animal.noBet')}
|
||||||
|
/>
|
||||||
|
<SmartImage
|
||||||
|
src={stopImageSrc}
|
||||||
|
alt="stop betting"
|
||||||
|
priority
|
||||||
|
showSkeleton={false}
|
||||||
|
className="h-design-220 w-design-560 max-w-full overflow-visible"
|
||||||
|
imgClassName="object-contain drop-shadow-[0_0_calc(var(--design-unit)*22)_rgba(60,235,255,0.28)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -3,21 +3,18 @@ import { motion, useReducedMotion } from 'motion/react'
|
|||||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import animalBorderImage from '@/assets/game/animal-border.webp'
|
import animalBorderImage from '@/assets/game/animal-border.webp'
|
||||||
import enStopImage from '@/assets/game/en-stop.webp'
|
|
||||||
import hostingBg from '@/assets/game/hosting-bg.webp'
|
|
||||||
import hostingBtn from '@/assets/game/hosting-btn.webp'
|
|
||||||
import zhStopImage from '@/assets/game/zh-stop.webp'
|
|
||||||
import diamondIcon from '@/assets/system/diamond.webp'
|
import diamondIcon from '@/assets/system/diamond.webp'
|
||||||
import refreshIcon from '@/assets/system/refresh.webp'
|
|
||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
|
||||||
import { SmartImage } from '@/components/smart-image'
|
import { SmartImage } from '@/components/smart-image'
|
||||||
|
import { DesktopAnimalOverlay } from '@/features/game/components/desktop/desktop-animal-overlay.tsx'
|
||||||
|
import { RoundBettingStartAlert } from '@/features/game/components/shared/round-betting-start-alert.tsx'
|
||||||
import { useAnimalVm } from '@/features/game/hooks/use-animal-vm'
|
import { useAnimalVm } from '@/features/game/hooks/use-animal-vm'
|
||||||
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
import { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { useGameAutoHostingStore, useGameRoundStore } from '@/store/game'
|
import { useGameRoundStore } from '@/store/game'
|
||||||
|
|
||||||
const SETTLEMENT_REVEAL_RANDOM_DURATION_MS = 4_000
|
const SETTLEMENT_REVEAL_RANDOM_DURATION_MS = 3_200
|
||||||
|
const SETTLEMENT_REVEAL_SETTLE_DURATION_MS = 800
|
||||||
const SETTLEMENT_REVEAL_RESULT_HOLD_MS = 1_000
|
const SETTLEMENT_REVEAL_RESULT_HOLD_MS = 1_000
|
||||||
const SETTLEMENT_REVEAL_MIN_STEP_MS = 90
|
const SETTLEMENT_REVEAL_MIN_STEP_MS = 90
|
||||||
const SETTLEMENT_REVEAL_MAX_STEP_MS = 480
|
const SETTLEMENT_REVEAL_MAX_STEP_MS = 480
|
||||||
@@ -64,7 +61,7 @@ export function DesktopAnimal({
|
|||||||
imageClassName,
|
imageClassName,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: DesktopAnimalProps) {
|
}: DesktopAnimalProps) {
|
||||||
const { i18n, t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prefersReducedMotion = useReducedMotion()
|
const prefersReducedMotion = useReducedMotion()
|
||||||
const animalIds = useMemo(() => FLOWER_IMAGE_LIST.map((item) => item.id), [])
|
const animalIds = useMemo(() => FLOWER_IMAGE_LIST.map((item) => item.id), [])
|
||||||
const containerRef = useRef<HTMLElement | null>(null)
|
const containerRef = useRef<HTMLElement | null>(null)
|
||||||
@@ -77,20 +74,19 @@ export function DesktopAnimal({
|
|||||||
width: number
|
width: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [isRevealHoldingResult, setIsRevealHoldingResult] = useState(false)
|
const [isRevealHoldingResult, setIsRevealHoldingResult] = useState(false)
|
||||||
|
const [isRevealSettlingResult, setIsRevealSettlingResult] = useState(false)
|
||||||
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
||||||
const revealWinningCellId = useGameRoundStore(
|
const revealWinningCellId = useGameRoundStore(
|
||||||
(state) => state.revealAnimation.winningCellId,
|
(state) => state.revealAnimation.winningCellId,
|
||||||
)
|
)
|
||||||
|
const revealRewardType = useGameRoundStore(
|
||||||
|
(state) => state.revealAnimation.rewardType,
|
||||||
|
)
|
||||||
const roundPhase = useGameRoundStore((state) => state.round.phase)
|
const roundPhase = useGameRoundStore((state) => state.round.phase)
|
||||||
const roundId = useGameRoundStore((state) => state.round.id)
|
const roundId = useGameRoundStore((state) => state.round.id)
|
||||||
const lastBetPeriodNo = useAuthStore(
|
const lastBetPeriodNo = useAuthStore(
|
||||||
(state) => state.currentUser?.lastBetPeriodNo,
|
(state) => state.currentUser?.lastBetPeriodNo,
|
||||||
)
|
)
|
||||||
const completedAutoHostingRounds = useGameAutoHostingStore(
|
|
||||||
(state) => state.completedRounds,
|
|
||||||
)
|
|
||||||
const hostingFlag = useGameAutoHostingStore((state) => state.isHosting)
|
|
||||||
const stopHosting = useGameAutoHostingStore((state) => state.stopHosting)
|
|
||||||
const finishRevealAnimation = useGameRoundStore(
|
const finishRevealAnimation = useGameRoundStore(
|
||||||
(state) => state.finishRevealAnimation,
|
(state) => state.finishRevealAnimation,
|
||||||
)
|
)
|
||||||
@@ -109,31 +105,46 @@ export function DesktopAnimal({
|
|||||||
revealPhase === 'spinning' ||
|
revealPhase === 'spinning' ||
|
||||||
(revealPhase === 'stopping' && !isRevealHoldingResult)
|
(revealPhase === 'stopping' && !isRevealHoldingResult)
|
||||||
const isRevealResult = revealPhase === 'result'
|
const isRevealResult = revealPhase === 'result'
|
||||||
|
const [hasRewardOverlayStarted, setHasRewardOverlayStarted] = useState(false)
|
||||||
|
const shouldHideRevealHighlight =
|
||||||
|
revealPhase === 'result' &&
|
||||||
|
(revealRewardType !== 'none' || hasRewardOverlayStarted)
|
||||||
const hasSubmittedCurrentRound =
|
const hasSubmittedCurrentRound =
|
||||||
roundPhase === 'betting' && Boolean(roundId) && lastBetPeriodNo === roundId
|
roundPhase === 'betting' && Boolean(roundId) && lastBetPeriodNo === roundId
|
||||||
const showStopOverlay =
|
const showStopOverlay =
|
||||||
hasSubmittedCurrentRound ||
|
hasSubmittedCurrentRound ||
|
||||||
roundPhase === 'locked' ||
|
roundPhase === 'locked' ||
|
||||||
roundPhase === 'revealing'
|
roundPhase === 'revealing'
|
||||||
const stopImageSrc = i18n.resolvedLanguage?.startsWith('zh')
|
|
||||||
? zhStopImage
|
useEffect(() => {
|
||||||
: enStopImage
|
if (revealPhase === 'idle') {
|
||||||
|
setHasRewardOverlayStarted(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (revealPhase === 'result' && revealRewardType !== 'none') {
|
||||||
|
setHasRewardOverlayStarted(true)
|
||||||
|
}
|
||||||
|
}, [revealPhase, revealRewardType])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (revealPhase === 'idle') {
|
if (revealPhase === 'idle') {
|
||||||
setRevealCellId(null)
|
setRevealCellId(null)
|
||||||
setIsRevealHoldingResult(false)
|
setIsRevealHoldingResult(false)
|
||||||
|
setIsRevealSettlingResult(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (revealPhase === 'result') {
|
if (revealPhase === 'result') {
|
||||||
setRevealCellId(revealWinningCellId)
|
setRevealCellId(shouldHideRevealHighlight ? null : revealWinningCellId)
|
||||||
setIsRevealHoldingResult(false)
|
setIsRevealHoldingResult(false)
|
||||||
|
setIsRevealSettlingResult(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (revealPhase === 'spinning') {
|
if (revealPhase === 'spinning') {
|
||||||
setIsRevealHoldingResult(false)
|
setIsRevealHoldingResult(false)
|
||||||
|
setIsRevealSettlingResult(false)
|
||||||
setRevealCellId((currentId) => getRandomAnimalId(animalIds, currentId))
|
setRevealCellId((currentId) => getRandomAnimalId(animalIds, currentId))
|
||||||
|
|
||||||
const intervalId = window.setInterval(() => {
|
const intervalId = window.setInterval(() => {
|
||||||
@@ -147,22 +158,28 @@ export function DesktopAnimal({
|
|||||||
|
|
||||||
if (revealWinningCellId === null) {
|
if (revealWinningCellId === null) {
|
||||||
setIsRevealHoldingResult(false)
|
setIsRevealHoldingResult(false)
|
||||||
|
setIsRevealSettlingResult(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const startedAt = performance.now()
|
const startedAt = performance.now()
|
||||||
let timeoutId = 0
|
let timeoutId = 0
|
||||||
setIsRevealHoldingResult(false)
|
setIsRevealHoldingResult(false)
|
||||||
|
setIsRevealSettlingResult(false)
|
||||||
|
|
||||||
const step = () => {
|
const step = () => {
|
||||||
const elapsedMs = performance.now() - startedAt
|
const elapsedMs = performance.now() - startedAt
|
||||||
|
|
||||||
if (elapsedMs >= SETTLEMENT_REVEAL_RANDOM_DURATION_MS) {
|
if (elapsedMs >= SETTLEMENT_REVEAL_RANDOM_DURATION_MS) {
|
||||||
|
setIsRevealSettlingResult(true)
|
||||||
setRevealCellId(revealWinningCellId)
|
setRevealCellId(revealWinningCellId)
|
||||||
setIsRevealHoldingResult(true)
|
|
||||||
timeoutId = window.setTimeout(() => {
|
timeoutId = window.setTimeout(() => {
|
||||||
finishRevealAnimation()
|
setIsRevealSettlingResult(false)
|
||||||
}, SETTLEMENT_REVEAL_RESULT_HOLD_MS)
|
setIsRevealHoldingResult(true)
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
finishRevealAnimation()
|
||||||
|
}, SETTLEMENT_REVEAL_RESULT_HOLD_MS)
|
||||||
|
}, SETTLEMENT_REVEAL_SETTLE_DURATION_MS)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +198,13 @@ export function DesktopAnimal({
|
|||||||
return () => {
|
return () => {
|
||||||
window.clearTimeout(timeoutId)
|
window.clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
}, [animalIds, finishRevealAnimation, revealPhase, revealWinningCellId])
|
}, [
|
||||||
|
animalIds,
|
||||||
|
finishRevealAnimation,
|
||||||
|
revealPhase,
|
||||||
|
revealWinningCellId,
|
||||||
|
shouldHideRevealHighlight,
|
||||||
|
])
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (revealCellId === null) {
|
if (revealCellId === null) {
|
||||||
@@ -230,6 +253,7 @@ export function DesktopAnimal({
|
|||||||
const hasPlacedSelection = Boolean(selectionMeta)
|
const hasPlacedSelection = Boolean(selectionMeta)
|
||||||
const isMarqueeActive = showStandbyState && item.id === marqueeId
|
const isMarqueeActive = showStandbyState && item.id === marqueeId
|
||||||
const isRevealWinner =
|
const isRevealWinner =
|
||||||
|
!shouldHideRevealHighlight &&
|
||||||
(isRevealResult || isRevealHoldingResult) &&
|
(isRevealResult || isRevealHoldingResult) &&
|
||||||
revealWinningCellId === item.id
|
revealWinningCellId === item.id
|
||||||
const warningType =
|
const warningType =
|
||||||
@@ -238,7 +262,9 @@ export function DesktopAnimal({
|
|||||||
const warningLabel =
|
const warningLabel =
|
||||||
warningType === 'balance'
|
warningType === 'balance'
|
||||||
? t('gameDesktop.animal.insufficientBalanceRecharge')
|
? t('gameDesktop.animal.insufficientBalanceRecharge')
|
||||||
: t('gameDesktop.animal.selectionLimitReached')
|
: warningType === 'betLimit'
|
||||||
|
? t('gameDesktop.animal.betLimitExceeded')
|
||||||
|
: t('gameDesktop.animal.selectionLimitReached')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
@@ -409,7 +435,14 @@ export function DesktopAnimal({
|
|||||||
{revealFrame ? (
|
{revealFrame ? (
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="pointer-events-none absolute z-40 transition-[height,transform,width] duration-75 ease-linear"
|
className={cn(
|
||||||
|
'pointer-events-none absolute z-40 transition-[height,transform,width]',
|
||||||
|
prefersReducedMotion
|
||||||
|
? 'duration-0'
|
||||||
|
: isRevealSettlingResult
|
||||||
|
? 'duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)]'
|
||||||
|
: 'duration-75 ease-linear',
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
height: revealFrame.height,
|
height: revealFrame.height,
|
||||||
transform: `translate(${revealFrame.left}px, ${revealFrame.top}px)`,
|
transform: `translate(${revealFrame.left}px, ${revealFrame.top}px)`,
|
||||||
@@ -436,88 +469,8 @@ export function DesktopAnimal({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showStopOverlay && !hostingFlag ? (
|
<DesktopAnimalOverlay showStopOverlay={showStopOverlay} />
|
||||||
<div
|
<RoundBettingStartAlert />
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-0 z-50 flex items-center justify-center bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px]"
|
|
||||||
>
|
|
||||||
<SmartImage
|
|
||||||
src={stopImageSrc}
|
|
||||||
alt="stop betting"
|
|
||||||
priority
|
|
||||||
showSkeleton={false}
|
|
||||||
className="h-design-220 w-design-560 max-w-[78%] overflow-visible"
|
|
||||||
imgClassName="object-contain drop-shadow-[0_0_calc(var(--design-unit)*22)_rgba(60,235,255,0.28)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{hostingFlag ? (
|
|
||||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center gap-design-22 bg-[rgba(2,8,14,0.6)] px-design-24 backdrop-blur-[1px]">
|
|
||||||
{showStopOverlay ? (
|
|
||||||
<SmartImage
|
|
||||||
src={stopImageSrc}
|
|
||||||
alt="stop betting"
|
|
||||||
priority
|
|
||||||
showSkeleton={false}
|
|
||||||
className="h-design-170 w-design-520 max-w-[72%] overflow-visible"
|
|
||||||
imgClassName="object-contain drop-shadow-[0_0_calc(var(--design-unit)*22)_rgba(60,235,255,0.28)]"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<SmartBackground
|
|
||||||
src={hostingBg}
|
|
||||||
size="100% 100%"
|
|
||||||
repeat="no-repeat"
|
|
||||||
position="center"
|
|
||||||
className="h-design-350 w-design-930 flex flex-col items-center justify-center"
|
|
||||||
>
|
|
||||||
<div className={'flex flex-col gap-design-44 items-center'}>
|
|
||||||
<div className={'flex items-center gap-design-20'}>
|
|
||||||
<motion.span
|
|
||||||
aria-hidden="true"
|
|
||||||
animate={prefersReducedMotion ? undefined : { rotate: 360 }}
|
|
||||||
transition={{
|
|
||||||
duration: 1.4,
|
|
||||||
ease: 'linear',
|
|
||||||
repeat: Number.POSITIVE_INFINITY,
|
|
||||||
}}
|
|
||||||
className="flex h-design-48 w-design-48 items-center justify-center"
|
|
||||||
>
|
|
||||||
<SmartImage
|
|
||||||
src={refreshIcon}
|
|
||||||
alt=""
|
|
||||||
priority
|
|
||||||
showSkeleton={false}
|
|
||||||
className="h-design-40 w-design-40"
|
|
||||||
imgClassName="object-contain drop-shadow-[0_0_calc(var(--design-unit)*22)_rgba(60,235,255,0.36)]"
|
|
||||||
/>
|
|
||||||
</motion.span>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'text-design-22 text-[#ffffff] font-bold [text-shadow:0_0_calc(var(--design-unit)*12)_rgba(76,236,255,0.45)]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('game.autoSpin.runningRounds', {
|
|
||||||
count: completedAutoHostingRounds,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SmartBackground
|
|
||||||
as="button"
|
|
||||||
type="button"
|
|
||||||
onClick={stopHosting}
|
|
||||||
src={hostingBtn}
|
|
||||||
size="100% 100%"
|
|
||||||
repeat="no-repeat"
|
|
||||||
position="center"
|
|
||||||
className="h-design-70 w-design-220 flex cursor-pointer items-center justify-center pb-design-4 text-design-24 font-bold text-[#EFFFFF] [text-shadow:0_1px_0_rgba(255,255,255,0.18),0_0_calc(var(--design-unit)*10)_rgba(46,220,255,0.5)] transition-transform hover:-translate-y-[1px] active:translate-y-0"
|
|
||||||
>
|
|
||||||
{t('game.actions.stopAuto')}
|
|
||||||
</SmartBackground>
|
|
||||||
</div>
|
|
||||||
</SmartBackground>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showStandbyState ? (
|
{showStandbyState ? (
|
||||||
<button
|
<button
|
||||||
@@ -526,29 +479,6 @@ export function DesktopAnimal({
|
|||||||
aria-busy={isRealtimeConnecting}
|
aria-busy={isRealtimeConnecting}
|
||||||
className="group absolute inset-0 z-10 flex cursor-pointer items-center justify-center overflow-hidden bg-[rgba(3,13,20,0.66)]"
|
className="group absolute inset-0 z-10 flex cursor-pointer items-center justify-center overflow-hidden bg-[rgba(3,13,20,0.66)]"
|
||||||
>
|
>
|
||||||
<motion.div
|
|
||||||
aria-hidden="true"
|
|
||||||
animate={{ rotate: 360 }}
|
|
||||||
transition={{
|
|
||||||
duration: 18,
|
|
||||||
repeat: Number.POSITIVE_INFINITY,
|
|
||||||
ease: 'linear',
|
|
||||||
}}
|
|
||||||
className="pointer-events-none absolute inset-[12%] rounded-full bg-[conic-gradient(from_0deg,rgba(129,255,250,0)_0deg,rgba(129,255,250,0.26)_60deg,rgba(129,255,250,0)_120deg,rgba(255,255,255,0)_360deg)] opacity-70 blur-[18px]"
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
aria-hidden="true"
|
|
||||||
animate={{
|
|
||||||
scale: [1, 1.03, 1],
|
|
||||||
opacity: [0.42, 0.7, 0.42],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 2.8,
|
|
||||||
repeat: Number.POSITIVE_INFINITY,
|
|
||||||
ease: 'easeInOut',
|
|
||||||
}}
|
|
||||||
className="pointer-events-none absolute inset-[22%] rounded-full border border-[rgba(124,255,248,0.22)] shadow-[0_0_calc(var(--design-unit)*22)_rgba(74,245,255,0.12),inset_0_0_calc(var(--design-unit)*18)_rgba(122,255,250,0.14)]"
|
|
||||||
/>
|
|
||||||
<div className="relative flex min-w-design-260 flex-col items-center gap-design-10 rounded-[calc(var(--design-unit)*22)] border border-[rgba(111,255,247,0.56)] bg-[linear-gradient(180deg,rgba(8,30,42,0.94),rgba(4,14,20,0.96))] px-design-32 py-design-18 text-center shadow-[0_0_calc(var(--design-unit)*18)_rgba(70,245,255,0.38),0_0_calc(var(--design-unit)*42)_rgba(19,210,232,0.22),inset_0_0_calc(var(--design-unit)*18)_rgba(120,255,249,0.12)] transition-[transform,box-shadow,border-color] duration-200 group-hover:-translate-y-[1px] group-hover:border-[rgba(141,255,250,0.82)] group-hover:shadow-[0_0_calc(var(--design-unit)*24)_rgba(88,247,255,0.5),0_0_calc(var(--design-unit)*52)_rgba(32,228,255,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(145,255,251,0.16)]">
|
<div className="relative flex min-w-design-260 flex-col items-center gap-design-10 rounded-[calc(var(--design-unit)*22)] border border-[rgba(111,255,247,0.56)] bg-[linear-gradient(180deg,rgba(8,30,42,0.94),rgba(4,14,20,0.96))] px-design-32 py-design-18 text-center shadow-[0_0_calc(var(--design-unit)*18)_rgba(70,245,255,0.38),0_0_calc(var(--design-unit)*42)_rgba(19,210,232,0.22),inset_0_0_calc(var(--design-unit)*18)_rgba(120,255,249,0.12)] transition-[transform,box-shadow,border-color] duration-200 group-hover:-translate-y-[1px] group-hover:border-[rgba(141,255,250,0.82)] group-hover:shadow-[0_0_calc(var(--design-unit)*24)_rgba(88,247,255,0.5),0_0_calc(var(--design-unit)*52)_rgba(32,228,255,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(145,255,251,0.16)]">
|
||||||
<span className="pointer-events-none absolute inset-[1px] rounded-[calc(var(--design-unit)*22)] border border-[rgba(226,255,255,0.1)]" />
|
<span className="pointer-events-none absolute inset-[1px] rounded-[calc(var(--design-unit)*22)] border border-[rgba(226,255,255,0.1)]" />
|
||||||
<span className="pointer-events-none absolute inset-x-design-14 top-design-10 h-design-18 rounded-full bg-[linear-gradient(180deg,rgba(255,255,255,0.16),rgba(255,255,255,0))] opacity-70" />
|
<span className="pointer-events-none absolute inset-x-design-14 top-design-10 h-design-18 rounded-full bg-[linear-gradient(180deg,rgba(255,255,255,0.16),rgba(255,255,255,0))] opacity-70" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import add from '@/assets/game/add.webp'
|
import reduce 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'
|
||||||
@@ -10,7 +10,7 @@ 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'
|
||||||
import leftBottomBg from '@/assets/game/left-bg.webp'
|
import leftBottomBg from '@/assets/game/left-bg.webp'
|
||||||
import reduce from '@/assets/game/reduce.webp'
|
import add from '@/assets/game/reduce.webp'
|
||||||
import totalBg from '@/assets/game/total-bg.webp'
|
import totalBg from '@/assets/game/total-bg.webp'
|
||||||
import diamond from '@/assets/system/diamond.webp'
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
@@ -45,6 +45,8 @@ export function DesktopControl() {
|
|||||||
const [clickedId, setClickedId] = useState<string | null>(null)
|
const [clickedId, setClickedId] = useState<string | null>(null)
|
||||||
const [hidingId, setHidingId] = useState<string | null>(null)
|
const [hidingId, setHidingId] = useState<string | null>(null)
|
||||||
const [confirmClicked, setConfirmClicked] = useState(false)
|
const [confirmClicked, setConfirmClicked] = useState(false)
|
||||||
|
const isConfirmWarning =
|
||||||
|
confirmState === 'insufficient' || confirmState === 'limit'
|
||||||
|
|
||||||
const handleChipClick = (chipId: string) => {
|
const handleChipClick = (chipId: string) => {
|
||||||
if (!acceptingBets) {
|
if (!acceptingBets) {
|
||||||
@@ -348,7 +350,7 @@ export function DesktopControl() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{t('gameDesktop.control.selected')}:{' '}
|
{t('gameDesktop.control.selected')}:{' '}
|
||||||
<span className={'text-red-500'}>{selectedCountLabel}</span> /{' '}
|
<span className={'text-reduce-500'}>{selectedCountLabel}</span> /{' '}
|
||||||
{maxSelectionCountLabel}
|
{maxSelectionCountLabel}
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex'}>
|
<div className={'flex'}>
|
||||||
@@ -455,7 +457,7 @@ export function DesktopControl() {
|
|||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
<SmartBackground
|
<SmartBackground
|
||||||
as={motion.button}
|
as={motion.button}
|
||||||
src={confirmState === 'insufficient' ? confirmRedBg : confirmBg}
|
src={isConfirmWarning ? confirmRedBg : confirmBg}
|
||||||
size="100% 100%"
|
size="100% 100%"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleConfirmClick}
|
onClick={handleConfirmClick}
|
||||||
@@ -505,7 +507,7 @@ export function DesktopControl() {
|
|||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
className="pointer-events-none absolute inset-0 bg-center bg-no-repeat"
|
className="pointer-events-none absolute inset-0 bg-center bg-no-repeat"
|
||||||
src={confirmState === 'insufficient' ? confirmRedBg : confirmBg}
|
src={isConfirmWarning ? confirmRedBg : confirmBg}
|
||||||
size="100% 100%"
|
size="100% 100%"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -524,10 +526,9 @@ export function DesktopControl() {
|
|||||||
: {
|
: {
|
||||||
opacity: confirmClicked ? 0 : 1,
|
opacity: confirmClicked ? 0 : 1,
|
||||||
y: confirmClicked ? 2 : 0,
|
y: confirmClicked ? 2 : 0,
|
||||||
textShadow:
|
textShadow: isConfirmWarning
|
||||||
confirmState === 'insufficient'
|
? '0 0 10px rgba(255,206,206,0.45)'
|
||||||
? '0 0 10px rgba(255,206,206,0.45)'
|
: 'none',
|
||||||
: 'none',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transition={
|
transition={
|
||||||
@@ -539,10 +540,7 @@ export function DesktopControl() {
|
|||||||
}
|
}
|
||||||
: { duration: 0.15 }
|
: { duration: 0.15 }
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn('relative', isConfirmWarning && 'text-[#FFF1F1]')}
|
||||||
'relative',
|
|
||||||
confirmState === 'insufficient' && 'text-[#FFF1F1]',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{confirmLabel}
|
{confirmLabel}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function DesktopGameHistory() {
|
|||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className={
|
className={
|
||||||
'history-scroll-hidden z-10 flex min-h-0 flex-1 w-full flex-col gap-design-10 overflow-y-auto overflow-x-hidden px-design-20 py-design-20'
|
'history-scroll-hidden z-10 flex min-h-0 flex-1 w-full flex-col gap-design-6 overflow-y-auto overflow-x-hidden px-design-14 py-design-14'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isInitialLoading ? (
|
{isInitialLoading ? (
|
||||||
@@ -142,10 +142,10 @@ export function DesktopGameHistory() {
|
|||||||
: 'linear-gradient(180deg,rgba(31,111,54,0.38),rgba(6,28,20,0.92))'
|
: 'linear-gradient(180deg,rgba(31,111,54,0.38),rgba(6,28,20,0.92))'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className="w-full pb-design-12 last:pb-0">
|
<div key={item.id} className="w-full pb-design-4 last:pb-0">
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'relative isolate flex w-full flex-col items-center overflow-hidden rounded-[calc(var(--design-unit)*14)] border bg-[linear-gradient(180deg,rgba(6,33,45,0.97),rgba(3,14,23,0.94))] text-[#FFE375] shadow-[0_0_calc(var(--design-unit)*16)_rgba(63,226,255,0.14),inset_0_1px_0_rgba(218,255,255,0.12)] transition-colors duration-200'
|
'relative isolate flex w-full flex-col overflow-hidden rounded-[calc(var(--design-unit)*8)] border bg-[linear-gradient(180deg,rgba(6,33,45,0.95),rgba(3,14,23,0.92))] text-[#FFE375] shadow-[0_0_calc(var(--design-unit)*10)_rgba(63,226,255,0.1),inset_0_1px_0_rgba(218,255,255,0.1)] transition-colors duration-200'
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
borderColor: statusBorderColor,
|
borderColor: statusBorderColor,
|
||||||
@@ -153,28 +153,31 @@ export function DesktopGameHistory() {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="pointer-events-none absolute inset-x-design-10 top-0 h-px opacity-80"
|
className="pointer-events-none absolute inset-x-design-8 top-0 h-px opacity-70"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(90deg, transparent, ${statusColor}, transparent)`,
|
background: `linear-gradient(90deg, transparent, ${statusColor}, transparent)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="pointer-events-none absolute -right-design-28 -top-design-30 h-design-90 w-design-90 rounded-full blur-[28px]"
|
className="pointer-events-none absolute -right-design-24 -top-design-28 h-design-72 w-design-72 rounded-full blur-[24px]"
|
||||||
style={{
|
style={{
|
||||||
background: statusColor,
|
background: statusColor,
|
||||||
opacity: isWin ? 0.14 : 0.08,
|
opacity: isWin ? 0.14 : 0.08,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="relative z-10 flex min-h-design-42 w-full items-center justify-center border-b px-design-12 py-design-7"
|
className="relative z-10 grid min-h-design-32 w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-design-8 border-b px-design-9 py-design-4"
|
||||||
style={{
|
style={{
|
||||||
background: statusBg,
|
background: statusBg,
|
||||||
borderColor: statusBorderColor,
|
borderColor: statusBorderColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<span className="min-w-0 truncate text-design-12 font-medium text-[#8DBCC2]">
|
||||||
|
{item.createdAtLabel}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
className="inline-flex min-w-design-118 items-center justify-center gap-design-8 px-design-14 py-design-4 text-design-18 font-bold"
|
className="inline-flex min-w-design-72 items-center justify-center gap-design-5 text-design-15 font-bold"
|
||||||
style={{
|
style={{
|
||||||
borderColor: statusBorderColor,
|
borderColor: statusBorderColor,
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
@@ -183,41 +186,33 @@ export function DesktopGameHistory() {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="h-design-8 w-design-8 rounded-full shadow-[0_0_calc(var(--design-unit)*10)_currentColor]"
|
className="h-design-6 w-design-6 rounded-full shadow-[0_0_calc(var(--design-unit)*8)_currentColor]"
|
||||||
style={{ backgroundColor: statusColor }}
|
style={{ backgroundColor: statusColor }}
|
||||||
/>
|
/>
|
||||||
{statusLabel}
|
{statusLabel}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-right text-design-12 font-medium text-[#C0E7EB]">
|
||||||
|
{t('gameDesktop.history.roundId')}: {item.periodNo}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'relative z-10 flex w-full flex-col gap-design-7 px-design-10 py-design-10 text-design-16'
|
'relative z-10 flex w-full flex-col gap-design-5 px-design-8 py-design-7 text-design-13'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex min-h-design-34 items-center justify-between gap-design-10 rounded-[calc(var(--design-unit)*8)] border border-[rgba(94,212,230,0.14)] bg-[rgba(5,23,33,0.58)] px-design-10 py-design-6">
|
<div className="flex min-h-design-34 items-start justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(94,212,230,0.12)] bg-[rgba(5,23,33,0.52)] px-design-7 py-design-4">
|
||||||
<span className={'shrink-0 text-[#84A2A2]'}>
|
<span className={'shrink-0 pt-design-5 text-[#84A2A2]'}>
|
||||||
{t('gameDesktop.history.roundId')}:{' '}
|
{t('gameDesktop.history.numbers')}
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
'min-w-0 text-right text-design-15 font-medium text-[#C0E7EB]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item.periodNo}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex min-h-design-42 items-start justify-between gap-design-10 rounded-[calc(var(--design-unit)*8)] border border-[rgba(94,212,230,0.14)] bg-[rgba(5,23,33,0.58)] px-design-10 py-design-6">
|
|
||||||
<span className={'shrink-0 pt-design-7 text-[#84A2A2]'}>
|
|
||||||
{t('gameDesktop.history.numbers')}:{' '}
|
|
||||||
</span>
|
</span>
|
||||||
{item.numbers.length === 0 ? (
|
{item.numbers.length === 0 ? (
|
||||||
<span className="pt-design-7 text-right">
|
<span className="pt-design-5 text-right">
|
||||||
{item.numbersLabel}
|
{item.numbersLabel}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex min-w-0 flex-1 flex-wrap items-center justify-end gap-design-5 align-middle">
|
<span className="inline-flex min-w-0 flex-1 flex-wrap items-center justify-end gap-design-3 align-middle">
|
||||||
{item.numbers.map((number) => (
|
{item.numbers.map((number) => (
|
||||||
<HistoryRewardNumber
|
<HistoryRewardNumber
|
||||||
|
className="!h-design-28 !min-w-design-28 !w-design-28 !p-0"
|
||||||
key={`${item.id}-${number}`}
|
key={`${item.id}-${number}`}
|
||||||
number={number}
|
number={number}
|
||||||
/>
|
/>
|
||||||
@@ -225,36 +220,36 @@ export function DesktopGameHistory() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-design-34 items-center justify-between gap-design-10 rounded-[calc(var(--design-unit)*8)] border border-[rgba(255,227,117,0.16)] bg-[linear-gradient(90deg,rgba(5,23,33,0.58),rgba(84,57,8,0.16))] px-design-10 py-design-6">
|
<div className="grid grid-cols-[1fr_1fr] gap-design-5">
|
||||||
<span className={'shrink-0 text-[#84A2A2]'}>
|
<div className="flex min-h-design-28 items-center justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(255,227,117,0.14)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(84,57,8,0.14))] px-design-7 py-design-4">
|
||||||
{t('gameDesktop.history.totalPoolAmount')}:{' '}
|
<span className="shrink-0 text-[#84A2A2]">
|
||||||
</span>
|
{t('gameDesktop.history.payout')}
|
||||||
<span
|
|
||||||
className={
|
|
||||||
'text-right text-design-17 font-bold text-[#FFE375] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(255,227,117,0.22)]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item.amountLabel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex min-h-design-42 items-center justify-between gap-design-10 rounded-[calc(var(--design-unit)*8)] border border-[rgba(255,117,117,0.18)] bg-[linear-gradient(90deg,rgba(5,23,33,0.58),rgba(88,20,28,0.16))] px-design-10 py-design-6">
|
|
||||||
<span className={'shrink-0 text-[#84A2A2]'}>
|
|
||||||
{t('gameDesktop.history.winningResult')}:{' '}
|
|
||||||
</span>
|
|
||||||
{item.resultNumber === null ? (
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
'text-right font-bold text-[#FF7575] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(255,117,117,0.28)]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item.resultNumberLabel}
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
<span className="min-w-0 truncate text-right text-design-14 font-bold text-[#FFE375] [text-shadow:0_0_calc(var(--design-unit)*8)_rgba(255,227,117,0.2)]">
|
||||||
<HistoryRewardNumber
|
{item.winAmountLabel}
|
||||||
className="text-[#FF7575]"
|
</span>
|
||||||
number={item.resultNumber}
|
</div>
|
||||||
/>
|
<div className="flex min-h-design-28 items-center justify-between gap-design-6 rounded-[calc(var(--design-unit)*6)] border border-[rgba(255,117,117,0.16)] bg-[linear-gradient(90deg,rgba(5,23,33,0.54),rgba(88,20,28,0.14))] px-design-7 py-design-4">
|
||||||
)}
|
<span className={'shrink-0 text-[#84A2A2]'}>
|
||||||
|
{t('gameDesktop.history.winningResult')}
|
||||||
|
</span>
|
||||||
|
<span className="flex min-w-0 items-center justify-end">
|
||||||
|
{item.resultNumber === null ? (
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
'text-right font-bold text-[#FF7575] [text-shadow:0_0_calc(var(--design-unit)*8)_rgba(255,117,117,0.24)]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.resultNumberLabel}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<HistoryRewardNumber
|
||||||
|
className="!h-design-28 !min-w-design-28 !w-design-28 !p-0 text-[#FF7575]"
|
||||||
|
number={item.resultNumber}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
Maximize,
|
Maximize,
|
||||||
Minimize,
|
Minimize,
|
||||||
Plus,
|
|
||||||
UserKey,
|
UserKey,
|
||||||
UserRoundPlus,
|
UserRoundPlus,
|
||||||
Volume2,
|
Volume2,
|
||||||
VolumeX,
|
VolumeX,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import add from '@/assets/game/add.webp'
|
||||||
import avatar from '@/assets/system/avatar.webp'
|
import avatar from '@/assets/system/avatar.webp'
|
||||||
import diamond from '@/assets/system/diamond.webp'
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
import logo from '@/assets/system/logo.webp'
|
import logo from '@/assets/system/logo.webp'
|
||||||
@@ -195,8 +195,7 @@ export function DesktopHeader() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenProcedures}
|
className="group relative flex items-center justify-center transition-transform duration-150"
|
||||||
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}
|
||||||
@@ -206,19 +205,17 @@ export function DesktopHeader() {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'common-neon-inset text-design-16 !py-design-20 !pr-design-14 box-border flex h-design-36 w-design-180 items-center justify-end gap-design-8 transition-[opacity,transform] duration-150 group-hover:opacity-90 group-active:scale-[0.98]'
|
'common-neon-inset text-design-16 !py-design-20 box-border flex h-design-36 w-design-180 items-center justify-end gap-design-12 transition-[opacity,transform] duration-150 group-hover:opacity-90 group-active:scale-[0.98]'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="truncate">{currentUser?.coin || '--'}</span>
|
<div className="truncate">{currentUser?.coin || '--'}</div>
|
||||||
<div className={'common-neon-inset !p-design-3'}>
|
|
||||||
<Plus
|
<SmartImage
|
||||||
aria-hidden="true"
|
onClick={onOpenProcedures}
|
||||||
className="shrink-0"
|
className={'w-design-24 h-design-24 cursor-pointer'}
|
||||||
color="#57B8BF"
|
alt={'add'}
|
||||||
size={18}
|
src={add}
|
||||||
strokeWidth={2.5}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
|
||||||
import winLogo from '@/assets/game/win.webp'
|
|
||||||
import winBg from '@/assets/game/win-bg.webp'
|
|
||||||
import { FullscreenLottieOverlay } from '@/components/fullscreen-lottie-overlay.tsx'
|
|
||||||
import type { FullscreenLottieSource } from '@/components/fullscreen-lottie-overlay.types.ts'
|
|
||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
|
||||||
import { SmartImage } from '@/components/smart-image.tsx'
|
|
||||||
import { REWARD_OVERLAY_DURATION_MS } from '@/constants'
|
|
||||||
import { cn } from '@/lib/utils.ts'
|
|
||||||
import { useGameRoundStore } from '@/store'
|
|
||||||
|
|
||||||
const smallRewardPath = new URL(
|
|
||||||
'../../../../assets/lottie/pc-small-reward.json',
|
|
||||||
import.meta.url,
|
|
||||||
).href
|
|
||||||
const bigRewardPath = new URL(
|
|
||||||
'../../../../assets/lottie/pc-big-reward.json',
|
|
||||||
import.meta.url,
|
|
||||||
).href
|
|
||||||
const REWARD_OVERLAY_FADE_OUT_MS = 300
|
|
||||||
const REWARD_CHILDREN_FADE_IN_MS = 2_000
|
|
||||||
const REWARD_CHILDREN_VISIBLE_MS = 1_000
|
|
||||||
|
|
||||||
type RewardChildrenStage = 'hidden' | 'visible' | 'exiting'
|
|
||||||
|
|
||||||
function easeOutCubic(progress: number) {
|
|
||||||
return 1 - (1 - progress) ** 3
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAmountMeta(amount: string | null) {
|
|
||||||
if (!amount) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedAmount = amount.replace(/,/g, '')
|
|
||||||
const numericAmount = Number(normalizedAmount)
|
|
||||||
|
|
||||||
if (!Number.isFinite(numericAmount)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const fractionDigits = normalizedAmount.includes('.')
|
|
||||||
? (normalizedAmount.split('.')[1]?.length ?? 0)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
fractionDigits,
|
|
||||||
numericAmount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRewardAmount(value: number, fractionDigits: number) {
|
|
||||||
return value.toLocaleString('en-US', {
|
|
||||||
maximumFractionDigits: fractionDigits,
|
|
||||||
minimumFractionDigits: fractionDigits,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function DesktopRewardOverlay() {
|
|
||||||
const rewardType = useGameRoundStore(
|
|
||||||
(state) => state.revealAnimation.rewardType,
|
|
||||||
)
|
|
||||||
const rewardAmount = useGameRoundStore(
|
|
||||||
(state) => state.revealAnimation.rewardAmount,
|
|
||||||
)
|
|
||||||
const revealKey = useGameRoundStore(
|
|
||||||
(state) => state.revealAnimation.revealKey,
|
|
||||||
)
|
|
||||||
const roundId = useGameRoundStore((state) => state.revealAnimation.roundId)
|
|
||||||
const clearRewardAnimation = useGameRoundStore(
|
|
||||||
(state) => state.clearRewardAnimation,
|
|
||||||
)
|
|
||||||
const [isFadingOut, setIsFadingOut] = useState(false)
|
|
||||||
const [childrenStage, setChildrenStage] =
|
|
||||||
useState<RewardChildrenStage>('hidden')
|
|
||||||
const [displayRewardAmount, setDisplayRewardAmount] = useState('0')
|
|
||||||
const rewardAmountMeta = useMemo(
|
|
||||||
() => getAmountMeta(rewardAmount),
|
|
||||||
[rewardAmount],
|
|
||||||
)
|
|
||||||
const source = useMemo<FullscreenLottieSource | null>(() => {
|
|
||||||
if (rewardType === 'small') {
|
|
||||||
return {
|
|
||||||
id: 'pc-small-reward',
|
|
||||||
path: smallRewardPath,
|
|
||||||
loop: false,
|
|
||||||
autoplay: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rewardType === 'big') {
|
|
||||||
return {
|
|
||||||
id: 'pc-big-reward',
|
|
||||||
path: bigRewardPath,
|
|
||||||
loop: false,
|
|
||||||
autoplay: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}, [rewardType])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (rewardType === 'none') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsFadingOut(false)
|
|
||||||
|
|
||||||
const fadeTimerId = window.setTimeout(() => {
|
|
||||||
setIsFadingOut(true)
|
|
||||||
}, REWARD_OVERLAY_DURATION_MS)
|
|
||||||
const clearTimerId = window.setTimeout(() => {
|
|
||||||
clearRewardAnimation()
|
|
||||||
}, REWARD_OVERLAY_DURATION_MS + REWARD_OVERLAY_FADE_OUT_MS)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.clearTimeout(fadeTimerId)
|
|
||||||
window.clearTimeout(clearTimerId)
|
|
||||||
}
|
|
||||||
}, [clearRewardAnimation, rewardType])
|
|
||||||
|
|
||||||
const shouldRenderOverlay = rewardType !== 'none'
|
|
||||||
const overlayAnimationKey = `${rewardType}-${roundId ?? 'round'}-${revealKey ?? 'pending'}`
|
|
||||||
const childTimelineKey = shouldRenderOverlay ? overlayAnimationKey : 'closed'
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (childTimelineKey === 'closed') {
|
|
||||||
setChildrenStage('hidden')
|
|
||||||
setDisplayRewardAmount('0')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setChildrenStage('hidden')
|
|
||||||
setDisplayRewardAmount(
|
|
||||||
rewardAmountMeta
|
|
||||||
? formatRewardAmount(0, rewardAmountMeta.fractionDigits)
|
|
||||||
: (rewardAmount ?? '0'),
|
|
||||||
)
|
|
||||||
|
|
||||||
const enterFrameId = window.requestAnimationFrame(() => {
|
|
||||||
setChildrenStage('visible')
|
|
||||||
})
|
|
||||||
const exitTimerId = window.setTimeout(() => {
|
|
||||||
setChildrenStage('exiting')
|
|
||||||
}, REWARD_CHILDREN_FADE_IN_MS + REWARD_CHILDREN_VISIBLE_MS)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.cancelAnimationFrame(enterFrameId)
|
|
||||||
window.clearTimeout(exitTimerId)
|
|
||||||
}
|
|
||||||
}, [childTimelineKey, rewardAmount, rewardAmountMeta])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (childTimelineKey === 'closed') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rewardAmountMeta) {
|
|
||||||
setDisplayRewardAmount(rewardAmount ?? '0')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let animationFrameId = 0
|
|
||||||
const startedAt = performance.now()
|
|
||||||
|
|
||||||
const syncRewardAmount = (now: number) => {
|
|
||||||
const progress = Math.min(
|
|
||||||
(now - startedAt) / REWARD_CHILDREN_FADE_IN_MS,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
setDisplayRewardAmount(
|
|
||||||
progress >= 1
|
|
||||||
? formatRewardAmount(
|
|
||||||
rewardAmountMeta.numericAmount,
|
|
||||||
rewardAmountMeta.fractionDigits,
|
|
||||||
)
|
|
||||||
: formatRewardAmount(
|
|
||||||
rewardAmountMeta.numericAmount * easeOutCubic(progress),
|
|
||||||
rewardAmountMeta.fractionDigits,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
animationFrameId = window.requestAnimationFrame(syncRewardAmount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
animationFrameId = window.requestAnimationFrame(syncRewardAmount)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.cancelAnimationFrame(animationFrameId)
|
|
||||||
}
|
|
||||||
}, [childTimelineKey, rewardAmount, rewardAmountMeta])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FullscreenLottieOverlay
|
|
||||||
open={shouldRenderOverlay}
|
|
||||||
source={source}
|
|
||||||
animationKey={overlayAnimationKey}
|
|
||||||
zIndex={120}
|
|
||||||
loop={false}
|
|
||||||
autoplay
|
|
||||||
lockBodyScroll={!isFadingOut}
|
|
||||||
backdropClassName={cn(
|
|
||||||
'bg-black/70 transition-opacity duration-300',
|
|
||||||
isFadingOut && 'pointer-events-none opacity-0',
|
|
||||||
)}
|
|
||||||
viewportClassName="px-0 py-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'absolute inset-0 flex items-center justify-center pb-design-220 transition-[opacity,transform,filter] ease-out',
|
|
||||||
childrenStage === 'visible' || childrenStage === 'exiting'
|
|
||||||
? 'duration-[2000ms]'
|
|
||||||
: 'duration-0',
|
|
||||||
childrenStage === 'visible' &&
|
|
||||||
'translate-y-0 scale-100 opacity-75 blur-none',
|
|
||||||
childrenStage === 'hidden' &&
|
|
||||||
'translate-y-[calc(var(--design-unit)*18)] scale-[0.96] opacity-0 blur-[calc(var(--design-unit)*2)]',
|
|
||||||
childrenStage === 'exiting' &&
|
|
||||||
'translate-y-[calc(var(--design-unit)*-14)] scale-[0.97] opacity-0 blur-[calc(var(--design-unit)*1.5)]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SmartBackground
|
|
||||||
className="flex h-design-175 w-design-900 items-center justify-center gap-design-24 pb-design-50"
|
|
||||||
src={winBg}
|
|
||||||
size="contain"
|
|
||||||
>
|
|
||||||
<SmartImage
|
|
||||||
className="h-design-50 w-design-225 drop-shadow-[0_0_calc(var(--design-unit)*10)_rgba(255,218,122,0.72)]"
|
|
||||||
alt="win"
|
|
||||||
src={winLogo}
|
|
||||||
/>
|
|
||||||
<div className="h-design-50 min-w-design-150 animate-bounce text-center font-sans text-design-56 leading-[calc(var(--design-unit)*50)] font-black tabular-nums text-[#FFE89A] [animation-duration:900ms] [-webkit-text-stroke:calc(var(--design-unit)*1)_#8A3A08] [text-shadow:0_0_calc(var(--design-unit)*6)_rgba(255,236,154,0.95),0_calc(var(--design-unit)*3)_0_#7A2F05,0_0_calc(var(--design-unit)*18)_rgba(255,151,15,0.72)]">
|
|
||||||
{displayRewardAmount}
|
|
||||||
</div>
|
|
||||||
</SmartBackground>
|
|
||||||
</div>
|
|
||||||
</FullscreenLottieOverlay>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DesktopRewardOverlay
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { motion, useReducedMotion } from 'motion/react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import streakBg from '@/assets/game/pc-streak.webp'
|
import streakBg from '@/assets/game/pc-streak.webp'
|
||||||
@@ -17,6 +18,7 @@ import { cn } from '@/lib/utils.ts'
|
|||||||
|
|
||||||
export function DesktopStatusLine() {
|
export function DesktopStatusLine() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const prefersReducedMotion = useReducedMotion()
|
||||||
const {
|
const {
|
||||||
countdownMs,
|
countdownMs,
|
||||||
limitLabel,
|
limitLabel,
|
||||||
@@ -165,22 +167,63 @@ export function DesktopStatusLine() {
|
|||||||
className={countdownClassName}
|
className={countdownClassName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="flex flex-1 items-center justify-center gap-design-88">
|
||||||
className={'flex-1 flex items-center justify-center gap-design-100'}
|
<div className="flex items-center gap-design-9">
|
||||||
>
|
<span className="relative flex h-design-24 w-design-24 items-center justify-center">
|
||||||
<div className={'flex items-center gap-2'}>
|
<motion.span
|
||||||
<div className={'flex items-center gap-2'}>
|
aria-hidden="true"
|
||||||
<div
|
className="absolute h-design-24 w-design-24 rounded-full bg-[rgba(120,255,127,0.22)] blur-[calc(var(--design-unit)*3)]"
|
||||||
className={'w-design-20 h-design-20 bg-[#78FF7F] rounded-[50%]'}
|
animate={
|
||||||
></div>
|
prefersReducedMotion
|
||||||
<div className={cn(phaseToneClassName, 'font-bold')}>
|
? undefined
|
||||||
{phaseLabel}
|
: { opacity: [0.36, 0.88, 0.42], scale: [0.9, 1.28, 0.98] }
|
||||||
</div>
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
duration: 1.25,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="relative h-design-14 w-design-14 rounded-full bg-[#78FF7F] shadow-[0_0_calc(var(--design-unit)*8)_rgba(120,255,127,0.78),0_0_calc(var(--design-unit)*16)_rgba(120,255,127,0.34)]"
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: { opacity: [0.78, 1, 0.86], scale: [0.96, 1.12, 1] }
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
duration: 1.25,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
phaseToneClassName,
|
||||||
|
'font-black tracking-[0.08em] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(120,255,127,0.44)]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{phaseLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'text-[#CBD3D5] font-bold'}>
|
<div className="flex items-baseline gap-design-7 font-bold leading-none">
|
||||||
{t('gameDesktop.status.roundId')}:{roundId}
|
<span className="text-design-20 tracking-[0.06em] text-[#9FBAC0]">
|
||||||
|
{t('gameDesktop.status.roundId')}:
|
||||||
|
</span>
|
||||||
|
<span className="text-design-24 font-black tracking-[0.06em] text-[#E7FFFF] [text-shadow:0_0_calc(var(--design-unit)*9)_rgba(75,255,254,0.38)]">
|
||||||
|
{roundId}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useGameRoundStore } from '@/store/game'
|
||||||
|
|
||||||
|
const BETTING_START_ALERT_DURATION_MS = 2000
|
||||||
|
|
||||||
|
interface RoundBettingStartAlertProps {
|
||||||
|
className?: string
|
||||||
|
placement?: 'absolute' | 'fixed'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoundBettingStartAlert({
|
||||||
|
className,
|
||||||
|
placement = 'absolute',
|
||||||
|
}: RoundBettingStartAlertProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prefersReducedMotion = useReducedMotion()
|
||||||
|
const roundId = useGameRoundStore((state) => state.round.id)
|
||||||
|
const roundPhase = useGameRoundStore((state) => state.round.phase)
|
||||||
|
const lastShownRoundIdRef = useRef<string | null>(null)
|
||||||
|
const [visibleRoundId, setVisibleRoundId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (roundPhase !== 'betting' || !roundId) {
|
||||||
|
setVisibleRoundId(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastShownRoundIdRef.current === roundId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastShownRoundIdRef.current = roundId
|
||||||
|
setVisibleRoundId(roundId)
|
||||||
|
|
||||||
|
const timerId = window.setTimeout(() => {
|
||||||
|
setVisibleRoundId((currentRoundId) =>
|
||||||
|
currentRoundId === roundId ? null : currentRoundId,
|
||||||
|
)
|
||||||
|
}, BETTING_START_ALERT_DURATION_MS)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timerId)
|
||||||
|
}
|
||||||
|
}, [roundId, roundPhase])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{visibleRoundId ? (
|
||||||
|
<motion.div
|
||||||
|
aria-live="polite"
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none left-1/2 top-1/2 z-50 w-[min(calc(var(--design-unit)*560),88vw)] -translate-x-1/2 -translate-y-1/2',
|
||||||
|
placement === 'fixed' ? 'fixed' : 'absolute',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
initial={
|
||||||
|
prefersReducedMotion
|
||||||
|
? { opacity: 0 }
|
||||||
|
: { opacity: 0, scale: 0.82, y: 'calc(var(--design-unit)*18)' }
|
||||||
|
}
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? { opacity: 1 }
|
||||||
|
: { opacity: 1, scale: 1, y: 0 }
|
||||||
|
}
|
||||||
|
exit={
|
||||||
|
prefersReducedMotion
|
||||||
|
? { opacity: 0 }
|
||||||
|
: { opacity: 0, scale: 0.92, y: 'calc(var(--design-unit)*-12)' }
|
||||||
|
}
|
||||||
|
transition={{ duration: 0.28, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="relative overflow-hidden rounded-[calc(var(--design-unit)*22)] border border-[rgba(126,255,248,0.76)] bg-[linear-gradient(180deg,rgba(7,33,43,0.96),rgba(4,16,24,0.98))] px-design-36 py-design-28 text-center shadow-[0_0_calc(var(--design-unit)*20)_rgba(70,245,255,0.4),0_0_calc(var(--design-unit)*52)_rgba(18,206,232,0.28),inset_0_0_calc(var(--design-unit)*22)_rgba(126,255,248,0.16)]"
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
boxShadow: [
|
||||||
|
'0 0 calc(var(--design-unit)*14) rgba(70,245,255,0.32), 0 0 calc(var(--design-unit)*34) rgba(18,206,232,0.2), inset 0 0 calc(var(--design-unit)*18) rgba(126,255,248,0.14)',
|
||||||
|
'0 0 calc(var(--design-unit)*24) rgba(87,250,255,0.54), 0 0 calc(var(--design-unit)*58) rgba(25,223,255,0.34), inset 0 0 calc(var(--design-unit)*24) rgba(146,255,251,0.22)',
|
||||||
|
'0 0 calc(var(--design-unit)*18) rgba(70,245,255,0.38), 0 0 calc(var(--design-unit)*46) rgba(18,206,232,0.26), inset 0 0 calc(var(--design-unit)*20) rgba(126,255,248,0.16)',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
duration: 0.72,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute inset-[1px] rounded-[calc(var(--design-unit)*22)] border border-[rgba(229,255,255,0.12)]" />
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute inset-x-design-20 top-design-10 h-design-24 rounded-full bg-[linear-gradient(180deg,rgba(255,255,255,0.18),rgba(255,255,255,0))]"
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: { opacity: [0.54, 0.9, 0.58], x: ['-7%', '7%', '-2%'] }
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: { duration: 1.1, ease: 'easeInOut' }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="relative z-10 flex flex-col items-center gap-design-28">
|
||||||
|
<motion.div
|
||||||
|
className="text-design-22 font-semibold leading-tight tracking-[0.18em] text-[#78FF7F]"
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion ? undefined : { opacity: [0.78, 1, 0.9] }
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: { duration: 0.72, ease: 'easeOut' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('game.roundBettingStart.title', {
|
||||||
|
roundId: visibleRoundId,
|
||||||
|
})}
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="relative flex items-center justify-center px-design-12 text-design-46 font-bold leading-tight tracking-[0.14em] text-[#F2FFFF] [text-shadow:0_0_calc(var(--design-unit)*16)_rgba(92,249,255,0.66)]"
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
opacity: [0.88, 1, 0.92],
|
||||||
|
scale: [0.98, 1.07, 0.99],
|
||||||
|
textShadow: [
|
||||||
|
'0 0 calc(var(--design-unit)*12) rgba(92,249,255,0.52), 0 0 calc(var(--design-unit)*22) rgba(120,255,127,0.22)',
|
||||||
|
'0 0 calc(var(--design-unit)*22) rgba(111,255,255,0.9), 0 0 calc(var(--design-unit)*42) rgba(120,255,127,0.44)',
|
||||||
|
'0 0 calc(var(--design-unit)*15) rgba(92,249,255,0.62), 0 0 calc(var(--design-unit)*28) rgba(120,255,127,0.28)',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
duration: 1.05,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-0 top-1/2 h-design-30 -translate-y-1/2 rounded-full bg-[rgba(120,255,248,0.24)] blur-[calc(var(--design-unit)*10)]"
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
opacity: [0.28, 0.72, 0.34],
|
||||||
|
scaleX: [0.82, 1.12, 0.9],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
duration: 1.05,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="relative z-10">
|
||||||
|
{t('game.roundBettingStart.action')}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
<div className="mt-design-8 flex items-center gap-design-7">
|
||||||
|
{[0, 1, 2, 3, 4].map((index) => (
|
||||||
|
<motion.span
|
||||||
|
key={index}
|
||||||
|
className="h-design-5 w-design-22 rounded-full bg-[rgba(130,255,248,0.88)] shadow-[0_0_calc(var(--design-unit)*10)_rgba(91,248,255,0.52)]"
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: { opacity: [0.36, 1, 0.42], y: [0, -2, 0] }
|
||||||
|
}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
delay: index * 0.06,
|
||||||
|
duration: 1.1,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
repeat: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
) : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MobileHeader } from '@/features/game/components/mobile/mobile-header.tsx'
|
import { MobileHeader } from '@/features/game/components/mobile/mobile-header.tsx'
|
||||||
|
import { RoundBettingStartAlert } from '@/features/game/components/shared/round-betting-start-alert.tsx'
|
||||||
import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.ts'
|
import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.ts'
|
||||||
|
|
||||||
export function MobileEntry() {
|
export function MobileEntry() {
|
||||||
@@ -7,6 +8,7 @@ export function MobileEntry() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MobileHeader />
|
<MobileHeader />
|
||||||
|
<RoundBettingStartAlert placement="fixed" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { DesktopHeader, EntryNoticeGateModal } from '@/features/game/components'
|
|||||||
import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal.tsx'
|
import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal.tsx'
|
||||||
import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
|
import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
|
||||||
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
|
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
|
||||||
import DesktopRewardOverlay from '@/features/game/components/desktop/desktop-reward-overlay.tsx'
|
|
||||||
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx'
|
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx'
|
||||||
import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.ts'
|
import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.ts'
|
||||||
import DesktopAutoSettingModal from '@/features/game/modal/desktop/desktop-auto-setting-modal.tsx'
|
import DesktopAutoSettingModal from '@/features/game/modal/desktop/desktop-auto-setting-modal.tsx'
|
||||||
@@ -67,8 +66,6 @@ export function PcEntry() {
|
|||||||
<DesktopProceduresModal />
|
<DesktopProceduresModal />
|
||||||
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
|
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
|
||||||
<DesktopWithdrawTopupModal />
|
<DesktopWithdrawTopupModal />
|
||||||
{/* 大奖/小奖动画展示 */}
|
|
||||||
<DesktopRewardOverlay />
|
|
||||||
{/* 强制弹窗 */}
|
{/* 强制弹窗 */}
|
||||||
<EntryNoticeGateModal />
|
<EntryNoticeGateModal />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function parseBalance(value: string | number | null | undefined) {
|
|||||||
return Number.isFinite(parsed) ? parsed : 0
|
return Number.isFinite(parsed) ? parsed : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DesktopAnimalWarningType = 'balance' | 'limit'
|
export type DesktopAnimalWarningType = 'balance' | 'betLimit' | 'limit'
|
||||||
|
|
||||||
function getNextMarqueeId(ids: number[], currentId: number | null) {
|
function getNextMarqueeId(ids: number[], currentId: number | null) {
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
@@ -60,6 +60,7 @@ export function useAnimalVm(
|
|||||||
const chips = useGameRoundStore((state) => state.chips)
|
const chips = useGameRoundStore((state) => state.chips)
|
||||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||||
const roundId = useGameRoundStore((state) => state.round.id)
|
const roundId = useGameRoundStore((state) => state.round.id)
|
||||||
|
const roundPhase = useGameRoundStore((state) => state.round.phase)
|
||||||
const maxSelectionCount = useGameRoundStore(
|
const maxSelectionCount = useGameRoundStore(
|
||||||
(state) => state.maxSelectionCount,
|
(state) => state.maxSelectionCount,
|
||||||
)
|
)
|
||||||
@@ -70,6 +71,9 @@ export function useAnimalVm(
|
|||||||
const selections = useGameRoundStore((state) => state.selections)
|
const selections = useGameRoundStore((state) => state.selections)
|
||||||
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
||||||
const connection = useGameSessionStore((state) => state.connection)
|
const connection = useGameSessionStore((state) => state.connection)
|
||||||
|
const tableLimitMax = useGameSessionStore(
|
||||||
|
(state) => state.dashboard.tableLimitMax,
|
||||||
|
)
|
||||||
const requestRealtimeConnection = useGameSessionStore(
|
const requestRealtimeConnection = useGameSessionStore(
|
||||||
(state) => state.requestRealtimeConnection,
|
(state) => state.requestRealtimeConnection,
|
||||||
)
|
)
|
||||||
@@ -112,7 +116,8 @@ export function useAnimalVm(
|
|||||||
const showStandbyState = !shouldConnectRealtime || !isRealtimeConnected
|
const showStandbyState = !shouldConnectRealtime || !isRealtimeConnected
|
||||||
const hasSubmittedCurrentRound =
|
const hasSubmittedCurrentRound =
|
||||||
Boolean(roundId) && currentUser?.lastBetPeriodNo === roundId
|
Boolean(roundId) && currentUser?.lastBetPeriodNo === roundId
|
||||||
const lockInteraction = showStandbyState || hasSubmittedCurrentRound
|
const lockInteraction =
|
||||||
|
showStandbyState || hasSubmittedCurrentRound || roundPhase !== 'betting'
|
||||||
const selectedCellCount = Object.keys(selectionByCell).length
|
const selectedCellCount = Object.keys(selectionByCell).length
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -169,7 +174,7 @@ export function useAnimalVm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (animalId: number) => {
|
const handleSelect = (animalId: number) => {
|
||||||
if (showStandbyState) {
|
if (roundPhase !== 'betting' || lockInteraction) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,6 +198,14 @@ export function useAnimalVm(
|
|||||||
|
|
||||||
const nextBetAmount = (activeChip?.amount ?? 0) * activeBetQuantity
|
const nextBetAmount = (activeChip?.amount ?? 0) * activeBetQuantity
|
||||||
|
|
||||||
|
if (tableLimitMax > 0 && totalBetAmount + nextBetAmount > tableLimitMax) {
|
||||||
|
setCellWarning({
|
||||||
|
cellId: animalId,
|
||||||
|
type: 'betLimit',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (totalBetAmount + nextBetAmount > balance) {
|
if (totalBetAmount + nextBetAmount > balance) {
|
||||||
setCellWarning({
|
setCellWarning({
|
||||||
cellId: animalId,
|
cellId: animalId,
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import { placeGameBet } from '@/features/game'
|
|||||||
import type { BetSelection } from '@/features/game/shared'
|
import type { BetSelection } from '@/features/game/shared'
|
||||||
import { notify } from '@/lib/notify'
|
import { notify } from '@/lib/notify'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { useGameAutoHostingStore, useGameRoundStore } from '@/store/game'
|
import {
|
||||||
|
useGameAutoHostingStore,
|
||||||
|
useGameRoundStore,
|
||||||
|
useGameSessionStore,
|
||||||
|
} from '@/store/game'
|
||||||
|
|
||||||
function parseBalance(value: string | number | null | undefined) {
|
function parseBalance(value: string | number | null | undefined) {
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
@@ -87,6 +91,9 @@ export function useAutoHostingRunner() {
|
|||||||
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
||||||
const round = useGameRoundStore((state) => state.round)
|
const round = useGameRoundStore((state) => state.round)
|
||||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||||
|
const tableLimitMax = useGameSessionStore(
|
||||||
|
(state) => state.dashboard.tableLimitMax,
|
||||||
|
)
|
||||||
const lastSingleWinAmount = useGameAutoHostingStore(
|
const lastSingleWinAmount = useGameAutoHostingStore(
|
||||||
(state) => state.lastSingleWinAmount,
|
(state) => state.lastSingleWinAmount,
|
||||||
)
|
)
|
||||||
@@ -171,6 +178,12 @@ export function useAutoHostingRunner() {
|
|||||||
)
|
)
|
||||||
const balance = parseBalance(currentUser.coin)
|
const balance = parseBalance(currentUser.coin)
|
||||||
|
|
||||||
|
if (tableLimitMax > 0 && totalBetAmount > tableLimitMax) {
|
||||||
|
stopHosting()
|
||||||
|
notify.warning(t('commonUi.toast.autoHostingStoppedBetLimit'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (totalBetAmount > balance) {
|
if (totalBetAmount > balance) {
|
||||||
stopHosting()
|
stopHosting()
|
||||||
notify.warning(t('commonUi.toast.autoHostingStoppedBalance'))
|
notify.warning(t('commonUi.toast.autoHostingStoppedBalance'))
|
||||||
@@ -250,6 +263,7 @@ export function useAutoHostingRunner() {
|
|||||||
selections,
|
selections,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
stopHosting,
|
stopHosting,
|
||||||
|
tableLimitMax,
|
||||||
t,
|
t,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
useGameSessionStore,
|
useGameSessionStore,
|
||||||
} from '@/store/game'
|
} from '@/store/game'
|
||||||
|
|
||||||
type ConfirmState = 'idle' | 'ready' | 'insufficient' | 'submitting'
|
type ConfirmState = 'idle' | 'ready' | 'insufficient' | 'limit' | 'submitting'
|
||||||
|
|
||||||
function formatChipDisplayValue(amount: number) {
|
function formatChipDisplayValue(amount: number) {
|
||||||
if (Number.isInteger(amount)) {
|
if (Number.isInteger(amount)) {
|
||||||
@@ -86,6 +86,9 @@ export function useGameControlVm() {
|
|||||||
const connectionStatus = useGameSessionStore(
|
const connectionStatus = useGameSessionStore(
|
||||||
(state) => state.connection.status,
|
(state) => state.connection.status,
|
||||||
)
|
)
|
||||||
|
const tableLimitMax = useGameSessionStore(
|
||||||
|
(state) => state.dashboard.tableLimitMax,
|
||||||
|
)
|
||||||
const shouldConnectRealtime = useGameSessionStore(
|
const shouldConnectRealtime = useGameSessionStore(
|
||||||
(state) => state.shouldConnectRealtime,
|
(state) => state.shouldConnectRealtime,
|
||||||
)
|
)
|
||||||
@@ -122,14 +125,18 @@ export function useGameControlVm() {
|
|||||||
const hasSubmittedCurrentRound =
|
const hasSubmittedCurrentRound =
|
||||||
Boolean(round.id) && currentUser?.lastBetPeriodNo === round.id
|
Boolean(round.id) && currentUser?.lastBetPeriodNo === round.id
|
||||||
const hasInsufficientBalance = hasSelections && totalBetAmount > balance
|
const hasInsufficientBalance = hasSelections && totalBetAmount > balance
|
||||||
|
const hasExceededBetLimit =
|
||||||
|
hasSelections && tableLimitMax > 0 && totalBetAmount > tableLimitMax
|
||||||
const confirmState: ConfirmState =
|
const confirmState: ConfirmState =
|
||||||
isSubmitting || isAutoHosting
|
isSubmitting || isAutoHosting
|
||||||
? 'submitting'
|
? 'submitting'
|
||||||
: !hasSelections
|
: !hasSelections
|
||||||
? 'idle'
|
? 'idle'
|
||||||
: hasInsufficientBalance
|
: hasExceededBetLimit
|
||||||
? 'insufficient'
|
? 'limit'
|
||||||
: 'ready'
|
: hasInsufficientBalance
|
||||||
|
? 'insufficient'
|
||||||
|
: 'ready'
|
||||||
|
|
||||||
const handleConfirm = useCallback(async () => {
|
const handleConfirm = useCallback(async () => {
|
||||||
if (confirmState === 'submitting' || !hasSelections) {
|
if (confirmState === 'submitting' || !hasSelections) {
|
||||||
@@ -142,6 +149,11 @@ export function useGameControlVm() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasExceededBetLimit) {
|
||||||
|
notify.warning(t('commonUi.toast.betLimitExceeded'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (hasInsufficientBalance) {
|
if (hasInsufficientBalance) {
|
||||||
notify.warning(t('commonUi.toast.insufficientBalance'))
|
notify.warning(t('commonUi.toast.insufficientBalance'))
|
||||||
return
|
return
|
||||||
@@ -214,6 +226,7 @@ export function useGameControlVm() {
|
|||||||
confirmState,
|
confirmState,
|
||||||
currentUser,
|
currentUser,
|
||||||
hasInsufficientBalance,
|
hasInsufficientBalance,
|
||||||
|
hasExceededBetLimit,
|
||||||
hasSelections,
|
hasSelections,
|
||||||
hasSubmittedCurrentRound,
|
hasSubmittedCurrentRound,
|
||||||
round.id,
|
round.id,
|
||||||
@@ -275,9 +288,11 @@ export function useGameControlVm() {
|
|||||||
? t('gameDesktop.control.selectNumbers')
|
? t('gameDesktop.control.selectNumbers')
|
||||||
: confirmState === 'insufficient'
|
: confirmState === 'insufficient'
|
||||||
? t('gameDesktop.control.insufficientBalance')
|
? t('gameDesktop.control.insufficientBalance')
|
||||||
: confirmState === 'submitting'
|
: confirmState === 'limit'
|
||||||
? t('gameDesktop.control.submitting')
|
? t('gameDesktop.control.betLimitExceeded')
|
||||||
: t('gameDesktop.control.confirm'),
|
: confirmState === 'submitting'
|
||||||
|
? t('gameDesktop.control.submitting')
|
||||||
|
: t('gameDesktop.control.confirm'),
|
||||||
confirmState,
|
confirmState,
|
||||||
isConfirmClickable: confirmState === 'ready' && !isAutoHosting,
|
isConfirmClickable: confirmState === 'ready' && !isAutoHosting,
|
||||||
onChipSelect: selectChip,
|
onChipSelect: selectChip,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
selectSelectionTotal,
|
selectSelectionTotal,
|
||||||
useGameAutoHostingStore,
|
useGameAutoHostingStore,
|
||||||
useGameRoundStore,
|
useGameRoundStore,
|
||||||
|
useGameSessionStore,
|
||||||
} from '@/store/game'
|
} from '@/store/game'
|
||||||
|
|
||||||
function parseAmount(value: string) {
|
function parseAmount(value: string) {
|
||||||
@@ -43,6 +44,9 @@ function DesktopAutoSettingModal() {
|
|||||||
const round = useGameRoundStore((state) => state.round)
|
const round = useGameRoundStore((state) => state.round)
|
||||||
const selections = useGameRoundStore((state) => state.selections)
|
const selections = useGameRoundStore((state) => state.selections)
|
||||||
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
const totalBetAmount = useGameRoundStore(selectSelectionTotal)
|
||||||
|
const tableLimitMax = useGameSessionStore(
|
||||||
|
(state) => state.dashboard.tableLimitMax,
|
||||||
|
)
|
||||||
const startHosting = useGameAutoHostingStore((state) => state.startHosting)
|
const startHosting = useGameAutoHostingStore((state) => state.startHosting)
|
||||||
const [balanceLimitEnabled, setBalanceLimitEnabled] = useState(false)
|
const [balanceLimitEnabled, setBalanceLimitEnabled] = useState(false)
|
||||||
const [balanceLimitValue, setBalanceLimitValue] = useState('0')
|
const [balanceLimitValue, setBalanceLimitValue] = useState('0')
|
||||||
@@ -69,6 +73,11 @@ function DesktopAutoSettingModal() {
|
|||||||
|
|
||||||
const balance = parseBalance(currentUser?.coin)
|
const balance = parseBalance(currentUser?.coin)
|
||||||
|
|
||||||
|
if (tableLimitMax > 0 && totalBetAmount > tableLimitMax) {
|
||||||
|
notify.warning(t('commonUi.toast.betLimitExceeded'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (totalBetAmount > balance) {
|
if (totalBetAmount > balance) {
|
||||||
notify.warning(t('commonUi.toast.insufficientBalance'))
|
notify.warning(t('commonUi.toast.insufficientBalance'))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -89,9 +89,12 @@ export default {
|
|||||||
phases: {
|
phases: {
|
||||||
betting: 'Betting',
|
betting: 'Betting',
|
||||||
locked: 'Locked',
|
locked: 'Locked',
|
||||||
revealing: 'Revealing',
|
|
||||||
settled: 'Settled',
|
settled: 'Settled',
|
||||||
},
|
},
|
||||||
|
roundBettingStart: {
|
||||||
|
title: 'Round {{roundId}}',
|
||||||
|
action: 'Start Betting',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
unifiedBetHint: 'Unified bet',
|
unifiedBetHint: 'Unified bet',
|
||||||
totalBet: 'Total bet',
|
totalBet: 'Total bet',
|
||||||
@@ -253,6 +256,7 @@ export default {
|
|||||||
inviteLinkCopyFailed:
|
inviteLinkCopyFailed:
|
||||||
'Failed to copy invite link. Please copy it manually.',
|
'Failed to copy invite link. Please copy it manually.',
|
||||||
insufficientBalance: 'Insufficient balance. Please adjust your bet.',
|
insufficientBalance: 'Insufficient balance. Please adjust your bet.',
|
||||||
|
betLimitExceeded: 'Single bet limit exceeded',
|
||||||
betUnavailable: 'Betting is not available for this round',
|
betUnavailable: 'Betting is not available for this round',
|
||||||
betPlaced: 'Bet placed successfully',
|
betPlaced: 'Bet placed successfully',
|
||||||
noRecentSuccessfulBet:
|
noRecentSuccessfulBet:
|
||||||
@@ -266,6 +270,8 @@ export default {
|
|||||||
autoHostingStopped: 'Auto spin stopped',
|
autoHostingStopped: 'Auto spin stopped',
|
||||||
autoHostingStoppedBalance:
|
autoHostingStoppedBalance:
|
||||||
'Balance condition reached. Auto spin has stopped.',
|
'Balance condition reached. Auto spin has stopped.',
|
||||||
|
autoHostingStoppedBetLimit:
|
||||||
|
'Single bet limit exceeded. Auto spin has stopped.',
|
||||||
autoHostingStoppedWin:
|
autoHostingStoppedWin:
|
||||||
'Single-win condition reached. Auto spin has stopped.',
|
'Single-win condition reached. Auto spin has stopped.',
|
||||||
autoHostingStoppedJackpot: 'Jackpot reached. Auto spin has stopped.',
|
autoHostingStoppedJackpot: 'Jackpot reached. Auto spin has stopped.',
|
||||||
@@ -378,6 +384,7 @@ export default {
|
|||||||
confirm: 'Confirm',
|
confirm: 'Confirm',
|
||||||
selectNumbers: 'Select Numbers',
|
selectNumbers: 'Select Numbers',
|
||||||
insufficientBalance: 'Insufficient Balance',
|
insufficientBalance: 'Insufficient Balance',
|
||||||
|
betLimitExceeded: 'Limit Exceeded',
|
||||||
submitting: 'Submitting...',
|
submitting: 'Submitting...',
|
||||||
actions: {
|
actions: {
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
@@ -404,7 +411,7 @@ export default {
|
|||||||
description: '(Revealing Result)',
|
description: '(Revealing Result)',
|
||||||
},
|
},
|
||||||
settled: {
|
settled: {
|
||||||
label: 'Settled',
|
label: 'Drawing',
|
||||||
description: '(Round Complete)',
|
description: '(Round Complete)',
|
||||||
},
|
},
|
||||||
waiting: {
|
waiting: {
|
||||||
@@ -418,10 +425,12 @@ export default {
|
|||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
insufficientBalanceRecharge: 'Insufficient balance, please top up',
|
insufficientBalanceRecharge: 'Insufficient balance, please top up',
|
||||||
|
betLimitExceeded: 'Single bet limit exceeded',
|
||||||
loading: 'Loading',
|
loading: 'Loading',
|
||||||
selectionLimitReached: 'Selection limit exceeded',
|
selectionLimitReached: 'Selection limit exceeded',
|
||||||
tapToEnter: 'Tap To Enter',
|
tapToEnter: 'Tap To Enter',
|
||||||
getStart: 'Get Start',
|
getStart: 'Get Start',
|
||||||
|
noBet: 'No Bet',
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
title: 'History',
|
title: 'History',
|
||||||
@@ -431,6 +440,7 @@ export default {
|
|||||||
orderNo: 'Order No.',
|
orderNo: 'Order No.',
|
||||||
roundId: 'Round ID',
|
roundId: 'Round ID',
|
||||||
numbers: 'Bet Numbers',
|
numbers: 'Bet Numbers',
|
||||||
|
createdAt: 'Time',
|
||||||
settledAt: 'Settled At',
|
settledAt: 'Settled At',
|
||||||
totalPoolAmount: 'Bet Amount',
|
totalPoolAmount: 'Bet Amount',
|
||||||
winningResult: 'Winning Result',
|
winningResult: 'Winning Result',
|
||||||
|
|||||||
@@ -88,9 +88,12 @@ export default {
|
|||||||
phases: {
|
phases: {
|
||||||
betting: 'Betting',
|
betting: 'Betting',
|
||||||
locked: 'Terkunci',
|
locked: 'Terkunci',
|
||||||
revealing: 'Mengungkap',
|
|
||||||
settled: 'Selesai',
|
settled: 'Selesai',
|
||||||
},
|
},
|
||||||
|
roundBettingStart: {
|
||||||
|
title: 'Ronde {{roundId}}',
|
||||||
|
action: 'Mulai Bertaruh',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
unifiedBetHint: 'Bet seragam',
|
unifiedBetHint: 'Bet seragam',
|
||||||
totalBet: 'Total bet',
|
totalBet: 'Total bet',
|
||||||
@@ -252,6 +255,7 @@ export default {
|
|||||||
inviteLinkCopyFailed:
|
inviteLinkCopyFailed:
|
||||||
'Gagal menyalin tautan undangan. Silakan salin secara manual.',
|
'Gagal menyalin tautan undangan. Silakan salin secara manual.',
|
||||||
insufficientBalance: 'Saldo tidak cukup. Silakan sesuaikan taruhan.',
|
insufficientBalance: 'Saldo tidak cukup. Silakan sesuaikan taruhan.',
|
||||||
|
betLimitExceeded: 'Melebihi batas taruhan tunggal',
|
||||||
betUnavailable: 'Taruhan tidak tersedia untuk ronde ini',
|
betUnavailable: 'Taruhan tidak tersedia untuk ronde ini',
|
||||||
betPlaced: 'Taruhan berhasil dikirim',
|
betPlaced: 'Taruhan berhasil dikirim',
|
||||||
noRecentSuccessfulBet:
|
noRecentSuccessfulBet:
|
||||||
@@ -265,6 +269,8 @@ export default {
|
|||||||
autoHostingStopped: 'Auto spin berhenti',
|
autoHostingStopped: 'Auto spin berhenti',
|
||||||
autoHostingStoppedBalance:
|
autoHostingStoppedBalance:
|
||||||
'Kondisi saldo tercapai. Auto spin telah berhenti.',
|
'Kondisi saldo tercapai. Auto spin telah berhenti.',
|
||||||
|
autoHostingStoppedBetLimit:
|
||||||
|
'Melebihi batas taruhan tunggal. Auto spin telah berhenti.',
|
||||||
autoHostingStoppedWin:
|
autoHostingStoppedWin:
|
||||||
'Kondisi kemenangan tunggal tercapai. Auto spin telah berhenti.',
|
'Kondisi kemenangan tunggal tercapai. Auto spin telah berhenti.',
|
||||||
autoHostingStoppedJackpot: 'Jackpot tercapai. Auto spin telah berhenti.',
|
autoHostingStoppedJackpot: 'Jackpot tercapai. Auto spin telah berhenti.',
|
||||||
@@ -378,6 +384,7 @@ export default {
|
|||||||
confirm: 'Konfirmasi',
|
confirm: 'Konfirmasi',
|
||||||
selectNumbers: 'Pilih Nombor',
|
selectNumbers: 'Pilih Nombor',
|
||||||
insufficientBalance: 'Saldo Tidak Cukup',
|
insufficientBalance: 'Saldo Tidak Cukup',
|
||||||
|
betLimitExceeded: 'Batas Terlampaui',
|
||||||
submitting: 'Mengirim...',
|
submitting: 'Mengirim...',
|
||||||
actions: {
|
actions: {
|
||||||
clear: 'Hapus',
|
clear: 'Hapus',
|
||||||
@@ -404,7 +411,7 @@ export default {
|
|||||||
description: '(Mengungkap Hasil)',
|
description: '(Mengungkap Hasil)',
|
||||||
},
|
},
|
||||||
settled: {
|
settled: {
|
||||||
label: 'Selesai',
|
label: 'Drawing',
|
||||||
description: '(Ronde Selesai)',
|
description: '(Ronde Selesai)',
|
||||||
},
|
},
|
||||||
waiting: {
|
waiting: {
|
||||||
@@ -418,10 +425,12 @@ export default {
|
|||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
insufficientBalanceRecharge: 'Saldo tidak cukup, silakan isi ulang',
|
insufficientBalanceRecharge: 'Saldo tidak cukup, silakan isi ulang',
|
||||||
|
betLimitExceeded: 'Melebihi batas taruhan tunggal',
|
||||||
loading: 'Memuat',
|
loading: 'Memuat',
|
||||||
selectionLimitReached: 'Melebihi pilihan yang diizinkan',
|
selectionLimitReached: 'Melebihi pilihan yang diizinkan',
|
||||||
tapToEnter: 'Ketuk Untuk Masuk',
|
tapToEnter: 'Ketuk Untuk Masuk',
|
||||||
getStart: 'Mulai',
|
getStart: 'Mulai',
|
||||||
|
noBet: 'Belum Bertaruh',
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
title: 'Riwayat',
|
title: 'Riwayat',
|
||||||
@@ -431,6 +440,7 @@ export default {
|
|||||||
orderNo: 'No. Order',
|
orderNo: 'No. Order',
|
||||||
roundId: 'ID Ronde',
|
roundId: 'ID Ronde',
|
||||||
numbers: 'Nomor Taruhan',
|
numbers: 'Nomor Taruhan',
|
||||||
|
createdAt: 'Waktu',
|
||||||
settledAt: 'Waktu Selesai',
|
settledAt: 'Waktu Selesai',
|
||||||
totalPoolAmount: 'Jumlah Taruhan',
|
totalPoolAmount: 'Jumlah Taruhan',
|
||||||
winningResult: 'Hasil Menang',
|
winningResult: 'Hasil Menang',
|
||||||
|
|||||||
@@ -91,9 +91,12 @@ export default {
|
|||||||
phases: {
|
phases: {
|
||||||
betting: 'Taruhan',
|
betting: 'Taruhan',
|
||||||
locked: 'Dikunci',
|
locked: 'Dikunci',
|
||||||
revealing: 'Cabutan',
|
|
||||||
settled: 'Selesai',
|
settled: 'Selesai',
|
||||||
},
|
},
|
||||||
|
roundBettingStart: {
|
||||||
|
title: 'Pusingan {{roundId}}',
|
||||||
|
action: 'Mula Bertaruh',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
unifiedBetHint: 'Taruhan seragam',
|
unifiedBetHint: 'Taruhan seragam',
|
||||||
totalBet: 'Jumlah taruhan',
|
totalBet: 'Jumlah taruhan',
|
||||||
@@ -256,6 +259,7 @@ export default {
|
|||||||
inviteLinkCopyFailed:
|
inviteLinkCopyFailed:
|
||||||
'Gagal menyalin pautan jemputan. Sila salin secara manual.',
|
'Gagal menyalin pautan jemputan. Sila salin secara manual.',
|
||||||
insufficientBalance: 'Baki tidak mencukupi. Sila laraskan taruhan.',
|
insufficientBalance: 'Baki tidak mencukupi. Sila laraskan taruhan.',
|
||||||
|
betLimitExceeded: 'Melebihi had taruhan tunggal',
|
||||||
betUnavailable: 'Taruhan tidak tersedia untuk pusingan ini',
|
betUnavailable: 'Taruhan tidak tersedia untuk pusingan ini',
|
||||||
betPlaced: 'Taruhan berjaya dihantar',
|
betPlaced: 'Taruhan berjaya dihantar',
|
||||||
noRecentSuccessfulBet:
|
noRecentSuccessfulBet:
|
||||||
@@ -269,6 +273,8 @@ export default {
|
|||||||
autoHostingStopped: 'Putaran auto dihentikan',
|
autoHostingStopped: 'Putaran auto dihentikan',
|
||||||
autoHostingStoppedBalance:
|
autoHostingStoppedBalance:
|
||||||
'Syarat baki dicapai. Putaran auto telah dihentikan.',
|
'Syarat baki dicapai. Putaran auto telah dihentikan.',
|
||||||
|
autoHostingStoppedBetLimit:
|
||||||
|
'Melebihi had taruhan tunggal. Putaran auto telah dihentikan.',
|
||||||
autoHostingStoppedWin:
|
autoHostingStoppedWin:
|
||||||
'Syarat kemenangan tunggal dicapai. Putaran auto telah dihentikan.',
|
'Syarat kemenangan tunggal dicapai. Putaran auto telah dihentikan.',
|
||||||
autoHostingStoppedJackpot:
|
autoHostingStoppedJackpot:
|
||||||
@@ -383,6 +389,7 @@ export default {
|
|||||||
confirm: 'Sahkan',
|
confirm: 'Sahkan',
|
||||||
selectNumbers: 'Pilih Nombor',
|
selectNumbers: 'Pilih Nombor',
|
||||||
insufficientBalance: 'Baki Tidak Mencukupi',
|
insufficientBalance: 'Baki Tidak Mencukupi',
|
||||||
|
betLimitExceeded: 'Melebihi Had',
|
||||||
submitting: 'Menghantar...',
|
submitting: 'Menghantar...',
|
||||||
actions: {
|
actions: {
|
||||||
clear: 'Kosongkan',
|
clear: 'Kosongkan',
|
||||||
@@ -409,7 +416,7 @@ export default {
|
|||||||
description: '(Mendedahkan Hasil)',
|
description: '(Mendedahkan Hasil)',
|
||||||
},
|
},
|
||||||
settled: {
|
settled: {
|
||||||
label: 'Selesai',
|
label: 'Cabutan',
|
||||||
description: '(Pusingan Tamat)',
|
description: '(Pusingan Tamat)',
|
||||||
},
|
},
|
||||||
waiting: {
|
waiting: {
|
||||||
@@ -423,10 +430,12 @@ export default {
|
|||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
insufficientBalanceRecharge: 'Baki tidak mencukupi, sila tambah nilai',
|
insufficientBalanceRecharge: 'Baki tidak mencukupi, sila tambah nilai',
|
||||||
|
betLimitExceeded: 'Melebihi had taruhan tunggal',
|
||||||
loading: 'Memuatkan',
|
loading: 'Memuatkan',
|
||||||
selectionLimitReached: 'Melebihi pilihan aksara yang dibenarkan',
|
selectionLimitReached: 'Melebihi pilihan aksara yang dibenarkan',
|
||||||
tapToEnter: 'Ketik Untuk Masuk',
|
tapToEnter: 'Ketik Untuk Masuk',
|
||||||
getStart: 'Mula',
|
getStart: 'Mula',
|
||||||
|
noBet: 'Belum Bertaruh',
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
title: 'Sejarah',
|
title: 'Sejarah',
|
||||||
@@ -436,6 +445,7 @@ export default {
|
|||||||
orderNo: 'No. Pesanan',
|
orderNo: 'No. Pesanan',
|
||||||
roundId: 'ID Pusingan',
|
roundId: 'ID Pusingan',
|
||||||
numbers: 'Nombor Pertaruhan',
|
numbers: 'Nombor Pertaruhan',
|
||||||
|
createdAt: 'Masa',
|
||||||
settledAt: 'Masa Selesai',
|
settledAt: 'Masa Selesai',
|
||||||
totalPoolAmount: 'Jumlah Pertaruhan',
|
totalPoolAmount: 'Jumlah Pertaruhan',
|
||||||
winningResult: 'Keputusan Menang',
|
winningResult: 'Keputusan Menang',
|
||||||
|
|||||||
@@ -87,9 +87,12 @@ export default {
|
|||||||
phases: {
|
phases: {
|
||||||
betting: '下注中',
|
betting: '下注中',
|
||||||
locked: '已封盘',
|
locked: '已封盘',
|
||||||
revealing: '开奖中',
|
|
||||||
settled: '已结算',
|
settled: '已结算',
|
||||||
},
|
},
|
||||||
|
roundBettingStart: {
|
||||||
|
title: '{{roundId}}期',
|
||||||
|
action: '开始下注',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
unifiedBetHint: '统一下注额',
|
unifiedBetHint: '统一下注额',
|
||||||
totalBet: '总下注',
|
totalBet: '总下注',
|
||||||
@@ -246,6 +249,7 @@ export default {
|
|||||||
inviteLinkCopied: '邀请链接已复制',
|
inviteLinkCopied: '邀请链接已复制',
|
||||||
inviteLinkCopyFailed: '邀请链接复制失败,请手动复制',
|
inviteLinkCopyFailed: '邀请链接复制失败,请手动复制',
|
||||||
insufficientBalance: '余额不足,请调整下注金额',
|
insufficientBalance: '余额不足,请调整下注金额',
|
||||||
|
betLimitExceeded: '超过单次投注限额',
|
||||||
betUnavailable: '当前期不可下注',
|
betUnavailable: '当前期不可下注',
|
||||||
betPlaced: '下注成功',
|
betPlaced: '下注成功',
|
||||||
noRecentSuccessfulBet: '暂无上一局成功下注记录',
|
noRecentSuccessfulBet: '暂无上一局成功下注记录',
|
||||||
@@ -256,6 +260,7 @@ export default {
|
|||||||
autoHostingStarted: '自动托管已开始',
|
autoHostingStarted: '自动托管已开始',
|
||||||
autoHostingStopped: '自动托管已停止',
|
autoHostingStopped: '自动托管已停止',
|
||||||
autoHostingStoppedBalance: '余额低于条件,自动托管已停止',
|
autoHostingStoppedBalance: '余额低于条件,自动托管已停止',
|
||||||
|
autoHostingStoppedBetLimit: '超过单次投注限额,自动托管已停止',
|
||||||
autoHostingStoppedWin: '单次盈利达到条件,自动托管已停止',
|
autoHostingStoppedWin: '单次盈利达到条件,自动托管已停止',
|
||||||
autoHostingStoppedJackpot: '出现 Jackpot 大奖,自动托管已停止',
|
autoHostingStoppedJackpot: '出现 Jackpot 大奖,自动托管已停止',
|
||||||
autoHostingSubmitFailed: '自动托管下注失败,已停止托管',
|
autoHostingSubmitFailed: '自动托管下注失败,已停止托管',
|
||||||
@@ -365,6 +370,7 @@ export default {
|
|||||||
confirm: '确认',
|
confirm: '确认',
|
||||||
selectNumbers: '请选择号码',
|
selectNumbers: '请选择号码',
|
||||||
insufficientBalance: '余额不足',
|
insufficientBalance: '余额不足',
|
||||||
|
betLimitExceeded: '超过限额',
|
||||||
submitting: '提交中...',
|
submitting: '提交中...',
|
||||||
actions: {
|
actions: {
|
||||||
clear: '清空',
|
clear: '清空',
|
||||||
@@ -391,7 +397,7 @@ export default {
|
|||||||
description: '(正在开奖)',
|
description: '(正在开奖)',
|
||||||
},
|
},
|
||||||
settled: {
|
settled: {
|
||||||
label: '已结算',
|
label: '开奖中',
|
||||||
description: '(本轮结束)',
|
description: '(本轮结束)',
|
||||||
},
|
},
|
||||||
waiting: {
|
waiting: {
|
||||||
@@ -405,10 +411,12 @@ export default {
|
|||||||
},
|
},
|
||||||
animal: {
|
animal: {
|
||||||
insufficientBalanceRecharge: '余额不足,请充值',
|
insufficientBalanceRecharge: '余额不足,请充值',
|
||||||
|
betLimitExceeded: '超过单次投注限额',
|
||||||
loading: '加载中',
|
loading: '加载中',
|
||||||
selectionLimitReached: '超过可选择字花',
|
selectionLimitReached: '超过可选择字花',
|
||||||
tapToEnter: '点击进入',
|
tapToEnter: '点击进入',
|
||||||
getStart: '开始游戏',
|
getStart: '开始游戏',
|
||||||
|
noBet: '未下注',
|
||||||
},
|
},
|
||||||
history: {
|
history: {
|
||||||
title: '历史记录',
|
title: '历史记录',
|
||||||
@@ -418,6 +426,7 @@ export default {
|
|||||||
orderNo: '订单号',
|
orderNo: '订单号',
|
||||||
roundId: '期号',
|
roundId: '期号',
|
||||||
numbers: '下注号码',
|
numbers: '下注号码',
|
||||||
|
createdAt: '时间',
|
||||||
settledAt: '结算时间',
|
settledAt: '结算时间',
|
||||||
totalPoolAmount: '下注金额',
|
totalPoolAmount: '下注金额',
|
||||||
winningResult: '中奖字花',
|
winningResult: '中奖字花',
|
||||||
|
|||||||
Reference in New Issue
Block a user