feat(game): 更新马来西亚手机号验证和游戏界面优化
- 修改认证模块手机号验证规则适配马来西亚号码格式 - 添加新的投注限制提示文本支持多语言 - 重命名结算阶段标签为Drawing统一显示 - 新增桌面版动物游戏遮罩组件分离功能 - 添加回合开始投注提醒弹窗组件 - 优化开奖动画流程和视觉效果 - 添加奖励动画显示和投注汇总展示 - 新增多种投注限制和状态提示信息
This commit is contained in:
@@ -3,21 +3,18 @@ import { motion, useReducedMotion } from 'motion/react'
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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 refreshIcon from '@/assets/system/refresh.webp'
|
||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
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 { FLOWER_IMAGE_LIST } from '@/features/game/shared'
|
||||
import { cn } from '@/lib/utils'
|
||||
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_MIN_STEP_MS = 90
|
||||
const SETTLEMENT_REVEAL_MAX_STEP_MS = 480
|
||||
@@ -64,7 +61,7 @@ export function DesktopAnimal({
|
||||
imageClassName,
|
||||
onSelect,
|
||||
}: DesktopAnimalProps) {
|
||||
const { i18n, t } = useTranslation()
|
||||
const { t } = useTranslation()
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
const animalIds = useMemo(() => FLOWER_IMAGE_LIST.map((item) => item.id), [])
|
||||
const containerRef = useRef<HTMLElement | null>(null)
|
||||
@@ -77,20 +74,19 @@ export function DesktopAnimal({
|
||||
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,
|
||||
)
|
||||
const revealRewardType = useGameRoundStore(
|
||||
(state) => state.revealAnimation.rewardType,
|
||||
)
|
||||
const roundPhase = useGameRoundStore((state) => state.round.phase)
|
||||
const roundId = useGameRoundStore((state) => state.round.id)
|
||||
const lastBetPeriodNo = useAuthStore(
|
||||
(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(
|
||||
(state) => state.finishRevealAnimation,
|
||||
)
|
||||
@@ -109,31 +105,46 @@ export function DesktopAnimal({
|
||||
revealPhase === 'spinning' ||
|
||||
(revealPhase === 'stopping' && !isRevealHoldingResult)
|
||||
const isRevealResult = revealPhase === 'result'
|
||||
const [hasRewardOverlayStarted, setHasRewardOverlayStarted] = useState(false)
|
||||
const shouldHideRevealHighlight =
|
||||
revealPhase === 'result' &&
|
||||
(revealRewardType !== 'none' || hasRewardOverlayStarted)
|
||||
const hasSubmittedCurrentRound =
|
||||
roundPhase === 'betting' && Boolean(roundId) && lastBetPeriodNo === roundId
|
||||
const showStopOverlay =
|
||||
hasSubmittedCurrentRound ||
|
||||
roundPhase === 'locked' ||
|
||||
roundPhase === 'revealing'
|
||||
const stopImageSrc = i18n.resolvedLanguage?.startsWith('zh')
|
||||
? zhStopImage
|
||||
: enStopImage
|
||||
|
||||
useEffect(() => {
|
||||
if (revealPhase === 'idle') {
|
||||
setHasRewardOverlayStarted(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (revealPhase === 'result' && revealRewardType !== 'none') {
|
||||
setHasRewardOverlayStarted(true)
|
||||
}
|
||||
}, [revealPhase, revealRewardType])
|
||||
|
||||
useEffect(() => {
|
||||
if (revealPhase === 'idle') {
|
||||
setRevealCellId(null)
|
||||
setIsRevealHoldingResult(false)
|
||||
setIsRevealSettlingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (revealPhase === 'result') {
|
||||
setRevealCellId(revealWinningCellId)
|
||||
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(() => {
|
||||
@@ -147,22 +158,28 @@ 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)
|
||||
setIsRevealHoldingResult(true)
|
||||
timeoutId = window.setTimeout(() => {
|
||||
finishRevealAnimation()
|
||||
}, SETTLEMENT_REVEAL_RESULT_HOLD_MS)
|
||||
setIsRevealSettlingResult(false)
|
||||
setIsRevealHoldingResult(true)
|
||||
timeoutId = window.setTimeout(() => {
|
||||
finishRevealAnimation()
|
||||
}, SETTLEMENT_REVEAL_RESULT_HOLD_MS)
|
||||
}, SETTLEMENT_REVEAL_SETTLE_DURATION_MS)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,7 +198,13 @@ export function DesktopAnimal({
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}, [animalIds, finishRevealAnimation, revealPhase, revealWinningCellId])
|
||||
}, [
|
||||
animalIds,
|
||||
finishRevealAnimation,
|
||||
revealPhase,
|
||||
revealWinningCellId,
|
||||
shouldHideRevealHighlight,
|
||||
])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (revealCellId === null) {
|
||||
@@ -230,6 +253,7 @@ export function DesktopAnimal({
|
||||
const hasPlacedSelection = Boolean(selectionMeta)
|
||||
const isMarqueeActive = showStandbyState && item.id === marqueeId
|
||||
const isRevealWinner =
|
||||
!shouldHideRevealHighlight &&
|
||||
(isRevealResult || isRevealHoldingResult) &&
|
||||
revealWinningCellId === item.id
|
||||
const warningType =
|
||||
@@ -238,7 +262,9 @@ export function DesktopAnimal({
|
||||
const warningLabel =
|
||||
warningType === 'balance'
|
||||
? t('gameDesktop.animal.insufficientBalanceRecharge')
|
||||
: t('gameDesktop.animal.selectionLimitReached')
|
||||
: warningType === 'betLimit'
|
||||
? t('gameDesktop.animal.betLimitExceeded')
|
||||
: t('gameDesktop.animal.selectionLimitReached')
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
@@ -409,7 +435,14 @@ export function DesktopAnimal({
|
||||
{revealFrame ? (
|
||||
<div
|
||||
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={{
|
||||
height: revealFrame.height,
|
||||
transform: `translate(${revealFrame.left}px, ${revealFrame.top}px)`,
|
||||
@@ -436,88 +469,8 @@ export function DesktopAnimal({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showStopOverlay && !hostingFlag ? (
|
||||
<div
|
||||
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}
|
||||
<DesktopAnimalOverlay showStopOverlay={showStopOverlay} />
|
||||
<RoundBettingStartAlert />
|
||||
|
||||
{showStandbyState ? (
|
||||
<button
|
||||
@@ -526,29 +479,6 @@ export function DesktopAnimal({
|
||||
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)]"
|
||||
>
|
||||
<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)]">
|
||||
<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" />
|
||||
|
||||
Reference in New Issue
Block a user