feat(game): 添加动物精灵图像变体支持并优化揭示动画
- 引入 light-animal-sprite 图像资源和 variant 属性支持 - 在动物组件中实现正常和浅色精灵图像切换功能 - 重构桌面和移动版动物组件的揭示动画逻辑 - 移除冗余的运动偏好检测和布局效果钩子 - 调整揭示动画的时间参数和平滑度曲线 - 更新资源预加载配置以包含浅色动物图像 - 修改控制组件中的投注数量调整状态管理 - 优化脚本生成精灵图像的复用性
BIN
src/assets/light-animal/1.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/light-animal/10.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/light-animal/11.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/light-animal/12.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/light-animal/13.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/light-animal/14.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/light-animal/15.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/light-animal/16.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/light-animal/17.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/assets/light-animal/18.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/light-animal/19.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/light-animal/2.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/light-animal/20.webp
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src/assets/light-animal/21.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/light-animal/22.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/light-animal/23.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/light-animal/24.webp
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
src/assets/light-animal/25.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/light-animal/26.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/light-animal/27.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/light-animal/28.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/light-animal/29.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/light-animal/3.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/light-animal/30.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/light-animal/31.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/assets/light-animal/32.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/light-animal/33.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/light-animal/34.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/light-animal/35.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/light-animal/36.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/light-animal/4.webp
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
src/assets/light-animal/5.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/light-animal/6.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/light-animal/7.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/light-animal/8.webp
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
src/assets/light-animal/9.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/light-animal/light-animal-sprite.webp
Normal file
|
After Width: | Height: | Size: 522 KiB |
@@ -7,6 +7,9 @@ const bootImageModules = import.meta.glob(
|
||||
'!../assets/animal/[1-9].webp',
|
||||
'!../assets/animal/[12][0-9].webp',
|
||||
'!../assets/animal/3[0-6].webp',
|
||||
'!../assets/light-animal/[1-9].webp',
|
||||
'!../assets/light-animal/[12][0-9].webp',
|
||||
'!../assets/light-animal/3[0-6].webp',
|
||||
'!../assets/reward/[1-9].webp',
|
||||
'!../assets/reward/[12][0-9].webp',
|
||||
'!../assets/reward/3[0-6].webp',
|
||||
@@ -21,7 +24,7 @@ const BOOT_MIN_VISIBLE_MS = 900
|
||||
const PC_LOADING_BG_URL = '/loading/pc-loading-bg.webp'
|
||||
const MOBILE_LOADING_BG_URL = '/loading/mobile-loading-bg.webp'
|
||||
const SPRITE_SOURCE_IMAGE_PATTERN =
|
||||
/^\.\.\/assets\/(?:animal|reward)\/(?:[1-9]|[12]\d|3[0-6])\.webp$/
|
||||
/^\.\.\/assets\/(?:animal|light-animal|reward)\/(?:[1-9]|[12]\d|3[0-6])\.webp$/
|
||||
|
||||
function preloadImage(url: string) {
|
||||
return new Promise<void>((resolve) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
import { motion, useReducedMotion } from 'motion/react'
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { motion } from 'motion/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import animalBorderImage from '@/assets/game/animal-border.webp'
|
||||
import diamondIcon from '@/assets/system/diamond.webp'
|
||||
@@ -17,8 +17,9 @@ import { useGameRoundStore } from '@/store/game'
|
||||
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_MIN_STEP_MS = 180
|
||||
const SETTLEMENT_REVEAL_MAX_STEP_MS = 960
|
||||
const SETTLEMENT_REVEAL_SPIN_STEP_MS = 160
|
||||
const SETTLEMENT_REVEAL_MIN_STEP_MS = 300
|
||||
const SETTLEMENT_REVEAL_MAX_STEP_MS = 760
|
||||
|
||||
function getRandomAnimalId(ids: number[], currentId: number | null) {
|
||||
if (ids.length === 0) {
|
||||
@@ -40,7 +41,7 @@ function getRandomAnimalId(ids: number[], currentId: number | null) {
|
||||
|
||||
function getSettlementRevealStepDelay(progress: number) {
|
||||
const clampedProgress = Math.min(Math.max(progress, 0), 1)
|
||||
const easedProgress = clampedProgress ** 1.65
|
||||
const easedProgress = clampedProgress ** 1.35
|
||||
|
||||
return (
|
||||
SETTLEMENT_REVEAL_MIN_STEP_MS +
|
||||
@@ -63,19 +64,8 @@ export function DesktopAnimal({
|
||||
onSelect,
|
||||
}: DesktopAnimalProps) {
|
||||
const { t } = useTranslation()
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
const animalIds = useMemo(() => FLOWER_IMAGE_LIST.map((item) => item.id), [])
|
||||
const containerRef = useRef<HTMLElement | null>(null)
|
||||
const cellRefs = useRef(new Map<number, HTMLButtonElement>())
|
||||
const [revealCellId, setRevealCellId] = useState<number | null>(null)
|
||||
const [revealFrame, setRevealFrame] = useState<{
|
||||
height: number
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
} | null>(null)
|
||||
const [isRevealHoldingResult, setIsRevealHoldingResult] = useState(false)
|
||||
const [isRevealSettlingResult, setIsRevealSettlingResult] = useState(false)
|
||||
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
||||
const revealWinningCellId = useGameRoundStore(
|
||||
(state) => state.revealAnimation.winningCellId,
|
||||
@@ -102,10 +92,6 @@ export function DesktopAnimal({
|
||||
showStandbyState,
|
||||
} = useAnimalVm(animalIds, onSelect)
|
||||
|
||||
const isRevealRunning =
|
||||
revealPhase === 'spinning' ||
|
||||
(revealPhase === 'stopping' && !isRevealHoldingResult)
|
||||
const isRevealResult = revealPhase === 'result'
|
||||
const [hasRewardOverlayStarted, setHasRewardOverlayStarted] = useState(false)
|
||||
const shouldHideRevealHighlight =
|
||||
revealPhase === 'result' &&
|
||||
@@ -131,26 +117,20 @@ export function DesktopAnimal({
|
||||
useEffect(() => {
|
||||
if (revealPhase === 'idle') {
|
||||
setRevealCellId(null)
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (revealPhase === 'result') {
|
||||
setRevealCellId(shouldHideRevealHighlight ? null : revealWinningCellId)
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (revealPhase === 'spinning') {
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
setRevealCellId((currentId) => getRandomAnimalId(animalIds, currentId))
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
setRevealCellId((currentId) => getRandomAnimalId(animalIds, currentId))
|
||||
}, 70)
|
||||
}, SETTLEMENT_REVEAL_SPIN_STEP_MS)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(intervalId)
|
||||
@@ -158,25 +138,18 @@ export function DesktopAnimal({
|
||||
}
|
||||
|
||||
if (revealWinningCellId === null) {
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
const startedAt = performance.now()
|
||||
let timeoutId = 0
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
|
||||
const step = () => {
|
||||
const elapsedMs = performance.now() - startedAt
|
||||
|
||||
if (elapsedMs >= SETTLEMENT_REVEAL_RANDOM_DURATION_MS) {
|
||||
setIsRevealSettlingResult(true)
|
||||
setRevealCellId(revealWinningCellId)
|
||||
timeoutId = window.setTimeout(() => {
|
||||
setIsRevealSettlingResult(false)
|
||||
setIsRevealHoldingResult(true)
|
||||
timeoutId = window.setTimeout(() => {
|
||||
finishRevealAnimation()
|
||||
}, SETTLEMENT_REVEAL_RESULT_HOLD_MS)
|
||||
@@ -207,45 +180,11 @@ export function DesktopAnimal({
|
||||
shouldHideRevealHighlight,
|
||||
])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (revealCellId === null) {
|
||||
setRevealFrame(null)
|
||||
return
|
||||
}
|
||||
|
||||
const syncRevealFrame = () => {
|
||||
const container = containerRef.current
|
||||
const cell = cellRefs.current.get(revealCellId)
|
||||
|
||||
if (!container || !cell) {
|
||||
setRevealFrame(null)
|
||||
return
|
||||
}
|
||||
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const cellRect = cell.getBoundingClientRect()
|
||||
|
||||
setRevealFrame({
|
||||
height: cellRect.height,
|
||||
left: cellRect.left - containerRect.left,
|
||||
top: cellRect.top - containerRect.top,
|
||||
width: cellRect.width,
|
||||
})
|
||||
}
|
||||
|
||||
syncRevealFrame()
|
||||
window.addEventListener('resize', syncRevealFrame)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', syncRevealFrame)
|
||||
}
|
||||
}, [revealCellId])
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
'relative grid w-full grid-cols-6 gap-design-5 overflow-hidden common-neon-inset',
|
||||
'relative grid w-full grid-cols-6 gap-design-5 common-neon-inset',
|
||||
!revealCellId && 'overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -253,13 +192,13 @@ export function DesktopAnimal({
|
||||
const selectionMeta = selectionByCell[item.id]
|
||||
const hasPlacedSelection = Boolean(selectionMeta)
|
||||
const isMarqueeActive = showStandbyState && item.id === marqueeId
|
||||
const isRevealWinner =
|
||||
!shouldHideRevealHighlight &&
|
||||
(isRevealResult || isRevealHoldingResult) &&
|
||||
revealWinningCellId === item.id
|
||||
const isRevealActive =
|
||||
!shouldHideRevealHighlight && revealCellId === item.id
|
||||
const warningType =
|
||||
cellWarning?.cellId === item.id ? cellWarning.type : null
|
||||
const showCellWarning = warningType !== null
|
||||
const animalSpriteVariant =
|
||||
hasPlacedSelection || isMarqueeActive ? 'light' : 'normal'
|
||||
const warningLabel =
|
||||
warningType === 'balance'
|
||||
? t('gameDesktop.animal.insufficientBalanceRecharge')
|
||||
@@ -270,13 +209,6 @@ export function DesktopAnimal({
|
||||
return (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
cellRefs.current.set(item.id, node)
|
||||
} else {
|
||||
cellRefs.current.delete(item.id)
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
disabled={lockInteraction || showStopOverlay}
|
||||
onClick={() => handleSelect(item.id)}
|
||||
@@ -302,22 +234,16 @@ export function DesktopAnimal({
|
||||
: { duration: 0.16, ease: 'easeOut' }
|
||||
}
|
||||
className={cn(
|
||||
'relative flex h-design-112 flex-col items-center justify-center overflow-hidden rounded-[calc(var(--design-unit)*18)] border border-transparent transition-[transform,border-color,box-shadow,opacity] duration-150',
|
||||
'relative flex h-design-112 flex-col items-center justify-center rounded-[calc(var(--design-unit)*18)] border border-transparent transition-[transform,border-color,box-shadow,opacity] duration-150',
|
||||
!isRevealActive && 'overflow-hidden',
|
||||
lockInteraction
|
||||
? 'cursor-not-allowed opacity-90'
|
||||
: 'cursor-pointer hover:-translate-y-[1px]',
|
||||
isMarqueeActive &&
|
||||
'border-[rgba(121,255,250,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(85,255,247,0.98),0_0_calc(var(--design-unit)*34)_rgba(39,245,255,0.88),inset_0_0_calc(var(--design-unit)*26)_rgba(112,255,248,0.34)]',
|
||||
isRevealRunning &&
|
||||
'border-[rgba(104,255,249,0.76)] shadow-[0_0_calc(var(--design-unit)*10)_rgba(68,244,255,0.46),0_0_calc(var(--design-unit)*22)_rgba(37,214,255,0.28),inset_0_0_calc(var(--design-unit)*16)_rgba(115,255,247,0.18)] brightness-115 saturate-125',
|
||||
isRevealWinner &&
|
||||
'border-[rgba(121,255,250,0.72)] shadow-[0_0_calc(var(--design-unit)*12)_rgba(81,248,255,0.54),0_0_calc(var(--design-unit)*22)_rgba(30,199,255,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(125,255,249,0.24)] brightness-110 saturate-120',
|
||||
showCellWarning &&
|
||||
'border-[rgba(255,92,92,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(255,88,88,0.56),0_0_calc(var(--design-unit)*28)_rgba(255,44,44,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(255,126,126,0.3)]',
|
||||
!showStandbyState &&
|
||||
!hasPlacedSelection &&
|
||||
!isRevealWinner &&
|
||||
'opacity-95',
|
||||
!showStandbyState && !hasPlacedSelection && 'opacity-95',
|
||||
itemClassName,
|
||||
)}
|
||||
>
|
||||
@@ -354,15 +280,11 @@ export function DesktopAnimal({
|
||||
'pointer-events-none absolute inset-[calc(var(--design-unit)*2)] rounded-[calc(var(--design-unit)*15)] opacity-0 transition-opacity duration-150',
|
||||
isMarqueeActive &&
|
||||
'bg-[radial-gradient(circle_at_center,rgba(129,255,250,0.48)_0%,rgba(94,255,247,0.18)_38%,rgba(43,236,255,0.08)_56%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(119,255,249,0.98),0_0_calc(var(--design-unit)*28)_rgba(53,246,255,0.9),0_0_calc(var(--design-unit)*44)_rgba(37,241,255,0.58),inset_0_0_calc(var(--design-unit)*20)_rgba(163,255,250,0.52)]',
|
||||
isRevealRunning &&
|
||||
'bg-[radial-gradient(circle_at_center,rgba(128,255,250,0.36)_0%,rgba(77,244,255,0.16)_40%,rgba(27,183,255,0.07)_68%,transparent_88%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(95,249,255,0.46),inset_0_0_calc(var(--design-unit)*18)_rgba(151,255,250,0.24)]',
|
||||
isRevealWinner &&
|
||||
'bg-[radial-gradient(circle_at_center,rgba(128,255,250,0.34)_0%,rgba(67,226,255,0.16)_38%,rgba(25,131,255,0.07)_58%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(92,248,255,0.42),inset_0_0_calc(var(--design-unit)*18)_rgba(126,255,250,0.24)]',
|
||||
showCellWarning &&
|
||||
'bg-[radial-gradient(circle_at_center,rgba(255,106,106,0.34)_0%,rgba(255,58,58,0.18)_42%,rgba(108,0,0,0.2)_78%,transparent_100%)] opacity-100',
|
||||
)}
|
||||
/>
|
||||
{!showStandbyState && !hasPlacedSelection && !isRevealWinner ? (
|
||||
{!showStandbyState && !hasPlacedSelection ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-[calc(var(--design-unit)*2)] z-20 rounded-[calc(var(--design-unit)*15)] bg-[rgba(4,16,24,0.52)] shadow-[inset_0_0_calc(var(--design-unit)*20)_rgba(3,9,14,0.56)]"
|
||||
@@ -371,15 +293,34 @@ export function DesktopAnimal({
|
||||
<AnimalSpriteImage
|
||||
animalId={item.id}
|
||||
alt={`animal-${item.id}`}
|
||||
variant={animalSpriteVariant}
|
||||
className={cn(
|
||||
'absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*14)]',
|
||||
isRevealRunning &&
|
||||
'brightness-115 saturate-125 drop-shadow-[0_0_calc(var(--design-unit)*8)_rgba(101,250,255,0.42)]',
|
||||
isRevealWinner &&
|
||||
'brightness-110 saturate-120 drop-shadow-[0_0_calc(var(--design-unit)*9)_rgba(106,250,255,0.44)]',
|
||||
imageClassName,
|
||||
)}
|
||||
/>
|
||||
{isRevealActive ? (
|
||||
<span className="pointer-events-none absolute inset-0 z-40 overflow-visible rounded-[calc(var(--design-unit)*18)] border border-[rgba(121,255,250,0.95)] shadow-[0_0_calc(var(--design-unit)*4)_calc(var(--design-unit)*1)_rgba(121,255,250,0.59),0_0_calc(var(--design-unit)*12)_calc(var(--design-unit)*3)_rgba(81,248,255,0.35),0_0_calc(var(--design-unit)*24)_calc(var(--design-unit)*4)_rgba(30,199,255,0.19)] transition-opacity duration-150">
|
||||
<SmartImage
|
||||
src={animalBorderImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
priority
|
||||
showSkeleton={false}
|
||||
className="pointer-events-none absolute inset-0 z-20 h-full w-full"
|
||||
imgClassName="object-fill"
|
||||
/>
|
||||
<span className="pointer-events-none absolute left-design-24 top-design-16 z-30 text-design-32 font-bold leading-none text-[#4BFFFE]">
|
||||
{String(item.id).padStart(2, '0')}
|
||||
</span>
|
||||
<AnimalSpriteImage
|
||||
animalId={item.id}
|
||||
variant="light"
|
||||
alt={`animal-${item.id}`}
|
||||
className="absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*14)]"
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
{showCellWarning ? (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, scale: 0.94 }}
|
||||
@@ -435,43 +376,6 @@ export function DesktopAnimal({
|
||||
)
|
||||
})}
|
||||
|
||||
{revealFrame ? (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'pointer-events-none absolute z-40 transform-gpu transition-[height,transform,width]',
|
||||
prefersReducedMotion
|
||||
? 'duration-0'
|
||||
: isRevealSettlingResult
|
||||
? 'duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)]'
|
||||
: 'duration-[160ms] ease-[cubic-bezier(0.22,1,0.36,1)]',
|
||||
)}
|
||||
style={{
|
||||
height: revealFrame.height,
|
||||
transform: `translate(${revealFrame.left}px, ${revealFrame.top}px)`,
|
||||
width: revealFrame.width,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="gold-reveal-glow rounded-[calc(var(--design-unit)*16)]"
|
||||
style={
|
||||
prefersReducedMotion
|
||||
? {
|
||||
animation: 'none',
|
||||
opacity: 0.36,
|
||||
transform: 'scale(1)',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="gold-reveal-static-border rounded-[calc(var(--design-unit)*16)]" />
|
||||
<div
|
||||
className="gold-reveal-shell rounded-[calc(var(--design-unit)*16)]"
|
||||
style={prefersReducedMotion ? { animation: 'none' } : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<DesktopAnimalOverlay showStopOverlay={showStopOverlay} />
|
||||
<RoundBettingStartAlert className="z-50" />
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export function DesktopControl() {
|
||||
actionsEnabled,
|
||||
canClear,
|
||||
canDecreaseBetQuantity,
|
||||
canIncreaseBetQuantity,
|
||||
chips,
|
||||
confirmLabel,
|
||||
confirmState,
|
||||
@@ -185,7 +186,7 @@ export function DesktopControl() {
|
||||
>
|
||||
{chips.map((chip) => {
|
||||
const isSelected = chip.id === selectedChipId
|
||||
const showLockedState = !acceptingBets
|
||||
const showLockedState = !acceptingBets || chip.isDisabled
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
@@ -367,11 +368,13 @@ export function DesktopControl() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!acceptingBets}
|
||||
disabled={!acceptingBets || !canIncreaseBetQuantity}
|
||||
onClick={onIncreaseBetQuantity}
|
||||
className={cn(
|
||||
'flex h-design-40 w-design-40 shrink-0 items-center justify-center',
|
||||
acceptingBets ? 'cursor-pointer' : 'cursor-not-allowed',
|
||||
acceptingBets && canIncreaseBetQuantity
|
||||
? 'cursor-pointer'
|
||||
: 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
<SmartImage
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
import { motion, useReducedMotion } from 'motion/react'
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { motion } from 'motion/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import animalBorderImage from '@/assets/game/animal-border.webp'
|
||||
import diamondIcon from '@/assets/system/diamond.webp'
|
||||
@@ -17,8 +17,9 @@ import { useGameRoundStore } from '@/store/game'
|
||||
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_MIN_STEP_MS = 180
|
||||
const SETTLEMENT_REVEAL_MAX_STEP_MS = 960
|
||||
const SETTLEMENT_REVEAL_SPIN_STEP_MS = 160
|
||||
const SETTLEMENT_REVEAL_MIN_STEP_MS = 300
|
||||
const SETTLEMENT_REVEAL_MAX_STEP_MS = 760
|
||||
|
||||
function getRandomAnimalId(ids: number[], currentId: number | null) {
|
||||
if (ids.length === 0) {
|
||||
@@ -40,7 +41,7 @@ function getRandomAnimalId(ids: number[], currentId: number | null) {
|
||||
|
||||
function getSettlementRevealStepDelay(progress: number) {
|
||||
const clampedProgress = Math.min(Math.max(progress, 0), 1)
|
||||
const easedProgress = clampedProgress ** 1.65
|
||||
const easedProgress = clampedProgress ** 1.35
|
||||
|
||||
return (
|
||||
SETTLEMENT_REVEAL_MIN_STEP_MS +
|
||||
@@ -63,20 +64,8 @@ export function MobileAnimal({
|
||||
onSelect,
|
||||
}: MobileAnimalProps) {
|
||||
const { t } = useTranslation()
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
const animalIds = useMemo(() => FLOWER_IMAGE_LIST.map((item) => item.id), [])
|
||||
const containerRef = useRef<HTMLElement | null>(null)
|
||||
const animalGridRef = useRef<HTMLDivElement | null>(null)
|
||||
const cellRefs = useRef(new Map<number, HTMLButtonElement>())
|
||||
const [revealCellId, setRevealCellId] = useState<number | null>(null)
|
||||
const [revealFrame, setRevealFrame] = useState<{
|
||||
height: number
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
} | null>(null)
|
||||
const [isRevealHoldingResult, setIsRevealHoldingResult] = useState(false)
|
||||
const [isRevealSettlingResult, setIsRevealSettlingResult] = useState(false)
|
||||
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
||||
const revealWinningCellId = useGameRoundStore(
|
||||
(state) => state.revealAnimation.winningCellId,
|
||||
@@ -103,10 +92,6 @@ export function MobileAnimal({
|
||||
showStandbyState,
|
||||
} = useAnimalVm(animalIds, onSelect)
|
||||
|
||||
const isRevealRunning =
|
||||
revealPhase === 'spinning' ||
|
||||
(revealPhase === 'stopping' && !isRevealHoldingResult)
|
||||
const isRevealResult = revealPhase === 'result'
|
||||
const [hasRewardOverlayStarted, setHasRewardOverlayStarted] = useState(false)
|
||||
const shouldHideRevealHighlight =
|
||||
revealPhase === 'result' &&
|
||||
@@ -132,26 +117,20 @@ export function MobileAnimal({
|
||||
useEffect(() => {
|
||||
if (revealPhase === 'idle') {
|
||||
setRevealCellId(null)
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (revealPhase === 'result') {
|
||||
setRevealCellId(shouldHideRevealHighlight ? null : revealWinningCellId)
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (revealPhase === 'spinning') {
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
setRevealCellId((currentId) => getRandomAnimalId(animalIds, currentId))
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
setRevealCellId((currentId) => getRandomAnimalId(animalIds, currentId))
|
||||
}, 70)
|
||||
}, SETTLEMENT_REVEAL_SPIN_STEP_MS)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(intervalId)
|
||||
@@ -159,25 +138,18 @@ export function MobileAnimal({
|
||||
}
|
||||
|
||||
if (revealWinningCellId === null) {
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
const startedAt = performance.now()
|
||||
let timeoutId = 0
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
|
||||
const step = () => {
|
||||
const elapsedMs = performance.now() - startedAt
|
||||
|
||||
if (elapsedMs >= SETTLEMENT_REVEAL_RANDOM_DURATION_MS) {
|
||||
setIsRevealSettlingResult(true)
|
||||
setRevealCellId(revealWinningCellId)
|
||||
timeoutId = window.setTimeout(() => {
|
||||
setIsRevealSettlingResult(false)
|
||||
setIsRevealHoldingResult(true)
|
||||
timeoutId = window.setTimeout(() => {
|
||||
finishRevealAnimation()
|
||||
}, SETTLEMENT_REVEAL_RESULT_HOLD_MS)
|
||||
@@ -208,63 +180,29 @@ export function MobileAnimal({
|
||||
shouldHideRevealHighlight,
|
||||
])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (revealCellId === null) {
|
||||
setRevealFrame(null)
|
||||
return
|
||||
}
|
||||
|
||||
const syncRevealFrame = () => {
|
||||
const container = animalGridRef.current
|
||||
const cell = cellRefs.current.get(revealCellId)
|
||||
|
||||
if (!container || !cell) {
|
||||
setRevealFrame(null)
|
||||
return
|
||||
}
|
||||
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const cellRect = cell.getBoundingClientRect()
|
||||
|
||||
setRevealFrame({
|
||||
height: cellRect.height,
|
||||
left: cellRect.left - containerRect.left,
|
||||
top: cellRect.top - containerRect.top,
|
||||
width: cellRect.width,
|
||||
})
|
||||
}
|
||||
|
||||
syncRevealFrame()
|
||||
window.addEventListener('resize', syncRevealFrame)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', syncRevealFrame)
|
||||
}
|
||||
}, [revealCellId])
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
'common-neon-inset-glow relative flex w-full flex-col gap-design-5 overflow-hidden !rounded-[calc(var(--design-unit)*6)] !px-design-5 !py-design-5',
|
||||
'common-neon-inset-glow relative flex w-full flex-col gap-design-5 !rounded-[calc(var(--design-unit)*6)] !px-design-5 !py-design-5',
|
||||
!revealCellId && 'overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<MobileStatusLine />
|
||||
|
||||
<div ref={animalGridRef} className="relative z-10 w-full">
|
||||
<div className="relative z-10 w-full">
|
||||
<div className="grid w-full grid-cols-4 gap-design-3">
|
||||
{FLOWER_IMAGE_LIST.map((item) => {
|
||||
const selectionMeta = selectionByCell[item.id]
|
||||
const hasPlacedSelection = Boolean(selectionMeta)
|
||||
const isMarqueeActive = showStandbyState && item.id === marqueeId
|
||||
const isRevealWinner =
|
||||
!shouldHideRevealHighlight &&
|
||||
(isRevealResult || isRevealHoldingResult) &&
|
||||
revealWinningCellId === item.id
|
||||
const isRevealActive =
|
||||
!shouldHideRevealHighlight && revealCellId === item.id
|
||||
const warningType =
|
||||
cellWarning?.cellId === item.id ? cellWarning.type : null
|
||||
const showCellWarning = warningType !== null
|
||||
const animalSpriteVariant =
|
||||
hasPlacedSelection || isMarqueeActive ? 'light' : 'normal'
|
||||
const warningLabel =
|
||||
warningType === 'balance'
|
||||
? t('gameDesktop.animal.insufficientBalanceRecharge')
|
||||
@@ -275,13 +213,6 @@ export function MobileAnimal({
|
||||
return (
|
||||
<motion.button
|
||||
key={item.id}
|
||||
ref={(node) => {
|
||||
if (node) {
|
||||
cellRefs.current.set(item.id, node)
|
||||
} else {
|
||||
cellRefs.current.delete(item.id)
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
disabled={lockInteraction || showStopOverlay}
|
||||
onClick={() => handleSelect(item.id)}
|
||||
@@ -307,22 +238,16 @@ export function MobileAnimal({
|
||||
: { duration: 0.16, ease: 'easeOut' }
|
||||
}
|
||||
className={cn(
|
||||
'relative flex h-design-36 flex-col items-center justify-center overflow-hidden rounded-[calc(var(--design-unit)*6)] border border-transparent transition-[transform,border-color,box-shadow,opacity] duration-150',
|
||||
'relative flex h-design-36 flex-col items-center justify-center rounded-[calc(var(--design-unit)*6)] border border-transparent transition-[transform,border-color,box-shadow,opacity] duration-150',
|
||||
!isRevealActive && 'overflow-hidden',
|
||||
lockInteraction
|
||||
? 'cursor-not-allowed opacity-90'
|
||||
: 'cursor-pointer hover:-translate-y-[1px]',
|
||||
isMarqueeActive &&
|
||||
'border-[rgba(121,255,250,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(85,255,247,0.98),0_0_calc(var(--design-unit)*34)_rgba(39,245,255,0.88),inset_0_0_calc(var(--design-unit)*26)_rgba(112,255,248,0.34)]',
|
||||
isRevealRunning &&
|
||||
'border-[rgba(104,255,249,0.76)] shadow-[0_0_calc(var(--design-unit)*10)_rgba(68,244,255,0.46),0_0_calc(var(--design-unit)*22)_rgba(37,214,255,0.28),inset_0_0_calc(var(--design-unit)*16)_rgba(115,255,247,0.18)] brightness-115 saturate-125',
|
||||
isRevealWinner &&
|
||||
'border-[rgba(121,255,250,0.72)] shadow-[0_0_calc(var(--design-unit)*12)_rgba(81,248,255,0.54),0_0_calc(var(--design-unit)*22)_rgba(30,199,255,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(125,255,249,0.24)] brightness-110 saturate-120',
|
||||
showCellWarning &&
|
||||
'border-[rgba(255,92,92,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(255,88,88,0.56),0_0_calc(var(--design-unit)*28)_rgba(255,44,44,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(255,126,126,0.3)]',
|
||||
!showStandbyState &&
|
||||
!hasPlacedSelection &&
|
||||
!isRevealWinner &&
|
||||
'opacity-95',
|
||||
!showStandbyState && !hasPlacedSelection && 'opacity-95',
|
||||
itemClassName,
|
||||
)}
|
||||
>
|
||||
@@ -359,15 +284,11 @@ export function MobileAnimal({
|
||||
'pointer-events-none absolute inset-[calc(var(--design-unit)*1)] rounded-[calc(var(--design-unit)*6)] opacity-0 transition-opacity duration-150',
|
||||
isMarqueeActive &&
|
||||
'bg-[radial-gradient(circle_at_center,rgba(129,255,250,0.48)_0%,rgba(94,255,247,0.18)_38%,rgba(43,236,255,0.08)_56%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(119,255,249,0.98),0_0_calc(var(--design-unit)*28)_rgba(53,246,255,0.9),0_0_calc(var(--design-unit)*44)_rgba(37,241,255,0.58),inset_0_0_calc(var(--design-unit)*20)_rgba(163,255,250,0.52)]',
|
||||
isRevealRunning &&
|
||||
'bg-[radial-gradient(circle_at_center,rgba(128,255,250,0.36)_0%,rgba(77,244,255,0.16)_40%,rgba(27,183,255,0.07)_68%,transparent_88%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(95,249,255,0.46),inset_0_0_calc(var(--design-unit)*18)_rgba(151,255,250,0.24)]',
|
||||
isRevealWinner &&
|
||||
'bg-[radial-gradient(circle_at_center,rgba(128,255,250,0.34)_0%,rgba(67,226,255,0.16)_38%,rgba(25,131,255,0.07)_58%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(92,248,255,0.42),inset_0_0_calc(var(--design-unit)*18)_rgba(126,255,250,0.24)]',
|
||||
showCellWarning &&
|
||||
'bg-[radial-gradient(circle_at_center,rgba(255,106,106,0.34)_0%,rgba(255,58,58,0.18)_42%,rgba(108,0,0,0.2)_78%,transparent_100%)] opacity-100',
|
||||
)}
|
||||
/>
|
||||
{!showStandbyState && !hasPlacedSelection && !isRevealWinner ? (
|
||||
{!showStandbyState && !hasPlacedSelection ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-[calc(var(--design-unit)*1)] z-20 rounded-[calc(var(--design-unit)*6)] bg-[rgba(4,16,24,0.52)] shadow-[inset_0_0_calc(var(--design-unit)*12)_rgba(3,9,14,0.56)]"
|
||||
@@ -376,15 +297,34 @@ export function MobileAnimal({
|
||||
<AnimalSpriteImage
|
||||
animalId={item.id}
|
||||
alt={`animal-${item.id}`}
|
||||
variant={animalSpriteVariant}
|
||||
className={cn(
|
||||
'absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*6)]',
|
||||
isRevealRunning &&
|
||||
'brightness-115 saturate-125 drop-shadow-[0_0_calc(var(--design-unit)*8)_rgba(101,250,255,0.42)]',
|
||||
isRevealWinner &&
|
||||
'brightness-110 saturate-120 drop-shadow-[0_0_calc(var(--design-unit)*9)_rgba(106,250,255,0.44)]',
|
||||
imageClassName,
|
||||
)}
|
||||
/>
|
||||
{isRevealActive ? (
|
||||
<span className="pointer-events-none absolute inset-0 z-40 overflow-visible rounded-[calc(var(--design-unit)*6)] border border-[rgba(121,255,250,0.95)] shadow-[0_0_calc(var(--design-unit)*3)_calc(var(--design-unit)*1)_rgba(121,255,250,0.59),0_0_calc(var(--design-unit)*9)_calc(var(--design-unit)*2)_rgba(81,248,255,0.35),0_0_calc(var(--design-unit)*18)_calc(var(--design-unit)*3)_rgba(30,199,255,0.19)] transition-opacity duration-150">
|
||||
<SmartImage
|
||||
src={animalBorderImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
priority
|
||||
showSkeleton={false}
|
||||
className="pointer-events-none absolute inset-0 z-20 h-full w-full"
|
||||
imgClassName="object-fill"
|
||||
/>
|
||||
<span className="pointer-events-none absolute left-design-6 top-design-4 z-30 text-design-10 font-bold leading-none text-[#4BFFFE]">
|
||||
{String(item.id).padStart(2, '0')}
|
||||
</span>
|
||||
<AnimalSpriteImage
|
||||
animalId={item.id}
|
||||
variant="light"
|
||||
alt=""
|
||||
className="absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*5)]"
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
{showCellWarning ? (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, scale: 0.94 }}
|
||||
@@ -441,52 +381,6 @@ export function MobileAnimal({
|
||||
})}
|
||||
</div>
|
||||
|
||||
{revealFrame ? (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'pointer-events-none absolute left-0 top-0 z-30 transform-gpu transition-[height,transform,width]',
|
||||
prefersReducedMotion
|
||||
? 'duration-0'
|
||||
: isRevealSettlingResult
|
||||
? 'duration-[800ms] ease-[cubic-bezier(0.16,1,0.3,1)]'
|
||||
: 'duration-[160ms] ease-[cubic-bezier(0.22,1,0.36,1)]',
|
||||
)}
|
||||
style={{
|
||||
height: revealFrame.height,
|
||||
transform: `translate(${revealFrame.left}px, ${revealFrame.top}px)`,
|
||||
width: revealFrame.width,
|
||||
}}
|
||||
>
|
||||
<SmartImage
|
||||
src={animalBorderImage}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
priority
|
||||
showSkeleton={false}
|
||||
className="pointer-events-none absolute inset-0 z-30 h-full w-full"
|
||||
imgClassName="object-fill"
|
||||
/>
|
||||
<div
|
||||
className="mobile-gold-reveal-glow z-10 rounded-[calc(var(--design-unit)*7)]"
|
||||
style={
|
||||
prefersReducedMotion
|
||||
? {
|
||||
animation: 'none',
|
||||
opacity: 0.36,
|
||||
transform: 'scale(1)',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="mobile-gold-reveal-static-border z-20 rounded-[calc(var(--design-unit)*7)]" />
|
||||
<div
|
||||
className="mobile-gold-reveal-shell z-20 rounded-[calc(var(--design-unit)*7)]"
|
||||
style={prefersReducedMotion ? { animation: 'none' } : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<MobileAnimalOverlay showStopOverlay={showStopOverlay} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export function MobileControl() {
|
||||
actionsEnabled,
|
||||
canClear,
|
||||
canDecreaseBetQuantity,
|
||||
canIncreaseBetQuantity,
|
||||
chips,
|
||||
confirmLabel,
|
||||
confirmState,
|
||||
@@ -157,7 +158,7 @@ export function MobileControl() {
|
||||
>
|
||||
{chips.map((chip) => {
|
||||
const isSelected = chip.id === selectedChipId
|
||||
const showLockedState = !acceptingBets
|
||||
const showLockedState = !acceptingBets || chip.isDisabled
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
@@ -314,11 +315,11 @@ export function MobileControl() {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!acceptingBets}
|
||||
disabled={!acceptingBets || !canIncreaseBetQuantity}
|
||||
onClick={onIncreaseBetQuantity}
|
||||
className={cn(
|
||||
'flex h-design-24 w-design-24 shrink-0 items-center justify-center',
|
||||
acceptingBets
|
||||
acceptingBets && canIncreaseBetQuantity
|
||||
? 'cursor-pointer'
|
||||
: 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import animalSpriteUrl from '@/assets/animal/animal-sprite.webp'
|
||||
import lightAnimalSpriteUrl from '@/assets/light-animal/light-animal-sprite.webp'
|
||||
import {
|
||||
ANIMAL_SPRITE_CELL_ASPECT_RATIO,
|
||||
ANIMAL_SPRITE_COLUMNS,
|
||||
@@ -14,6 +15,7 @@ interface AnimalSpriteImageProps
|
||||
alt?: string
|
||||
fit?: 'cover' | 'fill'
|
||||
imageClassName?: string
|
||||
variant?: 'normal' | 'light'
|
||||
}
|
||||
|
||||
export function AnimalSpriteImage({
|
||||
@@ -23,9 +25,11 @@ export function AnimalSpriteImage({
|
||||
fit = 'fill',
|
||||
imageClassName,
|
||||
style,
|
||||
variant = 'normal',
|
||||
...props
|
||||
}: AnimalSpriteImageProps) {
|
||||
const position = getAnimalSpritePosition(animalId)
|
||||
const spriteUrl = variant === 'light' ? lightAnimalSpriteUrl : animalSpriteUrl
|
||||
const accessibilityProps = alt
|
||||
? ({ 'aria-label': alt, role: 'img' } as const)
|
||||
: ({ 'aria-hidden': 'true' } as const)
|
||||
@@ -50,7 +54,7 @@ export function AnimalSpriteImage({
|
||||
style={{
|
||||
aspectRatio:
|
||||
fit === 'cover' ? ANIMAL_SPRITE_CELL_ASPECT_RATIO : undefined,
|
||||
backgroundImage: `url(${animalSpriteUrl})`,
|
||||
backgroundImage: `url(${spriteUrl})`,
|
||||
backgroundPosition: `${position.xPercent}% ${position.yPercent}%`,
|
||||
backgroundSize: `${ANIMAL_SPRITE_COLUMNS * 100}% ${ANIMAL_SPRITE_ROWS * 100}%`,
|
||||
}}
|
||||
|
||||
@@ -102,6 +102,7 @@ export function useGameControlVm() {
|
||||
amount: chip.amount,
|
||||
id: chip.id,
|
||||
isSelected: chip.id === activeChipId,
|
||||
isDisabled: tableLimitMax > 0 && chip.amount > tableLimitMax,
|
||||
src: CHIP_IMAGE_MAP.get(chip.id) ?? CHIP_IMAGE_OPTIONS[0]?.src ?? '',
|
||||
valueLabel: formatChipDisplayValue(chip.amount),
|
||||
}))
|
||||
@@ -113,7 +114,7 @@ export function useGameControlVm() {
|
||||
|
||||
return left.isSelected ? 1 : -1
|
||||
})
|
||||
}, [activeChipId, chips])
|
||||
}, [activeChipId, chips, tableLimitMax])
|
||||
|
||||
const selectedChip =
|
||||
chipItems.find((chip) => chip.id === activeChipId) ?? chipItems[0] ?? null
|
||||
@@ -126,6 +127,30 @@ export function useGameControlVm() {
|
||||
const hasInsufficientBalance = hasSelections && totalBetAmount > balance
|
||||
const hasExceededBetLimit =
|
||||
hasSelections && tableLimitMax > 0 && totalBetAmount > tableLimitMax
|
||||
const canIncreaseBetQuantity = useMemo(() => {
|
||||
if (!hasSelections) {
|
||||
return true
|
||||
}
|
||||
|
||||
const activeChip = chips.find((chip) => chip.id === activeChipId)
|
||||
|
||||
if (!activeChip) {
|
||||
return false
|
||||
}
|
||||
|
||||
const nextQuantity = activeBetQuantity + 1
|
||||
const nextTotalPerSelection = activeChip.amount * nextQuantity
|
||||
const nextTotal = nextTotalPerSelection * selections.length
|
||||
|
||||
return tableLimitMax <= 0 || nextTotal <= tableLimitMax
|
||||
}, [
|
||||
hasSelections,
|
||||
chips,
|
||||
activeChipId,
|
||||
activeBetQuantity,
|
||||
selections.length,
|
||||
tableLimitMax,
|
||||
])
|
||||
const confirmState: ConfirmState =
|
||||
isSubmitting || isAutoHosting
|
||||
? 'submitting'
|
||||
@@ -292,6 +317,7 @@ export function useGameControlVm() {
|
||||
!hasSubmittedCurrentRound &&
|
||||
!isAutoHosting,
|
||||
canDecreaseBetQuantity: activeBetQuantity > 1,
|
||||
canIncreaseBetQuantity,
|
||||
confirmLabel:
|
||||
confirmState === 'idle'
|
||||
? t('gameDesktop.control.selectNumbers')
|
||||
@@ -306,7 +332,14 @@ export function useGameControlVm() {
|
||||
isConfirmClickable: confirmState === 'ready' && !isAutoHosting,
|
||||
onChipSelect: selectChip,
|
||||
onDecreaseBetQuantity: () => adjustBetQuantity(-1),
|
||||
onIncreaseBetQuantity: () => adjustBetQuantity(1),
|
||||
onIncreaseBetQuantity: () => {
|
||||
if (!canIncreaseBetQuantity) {
|
||||
notify.warning(t('commonUi.toast.betLimitExceeded'))
|
||||
return
|
||||
}
|
||||
|
||||
adjustBetQuantity(1)
|
||||
},
|
||||
onConfirm: handleConfirm,
|
||||
onClearSelections: clearSelections,
|
||||
onOpenAutoSetting: handleOpenAutoSetting,
|
||||
|
||||