feat(game): 优化加载动画并添加无奖励结算显示
- 替换粒子动画为预加载背景图片,提升加载性能 - 添加桌面端无奖励结算界面显示玩家选择和开奖结果 - 实现消息广播组件的动画效果和状态管理 - 添加代码审查报告文档 - 更新GitNexus索引统计信息 - 调整加载界面布局和样式优化
This commit is contained in:
@@ -28,6 +28,7 @@ import type { BetSelection, RewardAnimationType } from '@/type'
|
||||
const REWARD_OVERLAY_FADE_OUT_MS = 300
|
||||
const REWARD_CHILDREN_FADE_IN_MS = 2_000
|
||||
const REWARD_CHILDREN_VISIBLE_MS = 1_000
|
||||
const SETTLEMENT_OVERLAY_FADE_OUT_MS = 300
|
||||
|
||||
type RewardChildrenStage = 'hidden' | 'visible' | 'exiting'
|
||||
type StopBetItem = {
|
||||
@@ -175,6 +176,79 @@ function StopBetSummary({
|
||||
)
|
||||
}
|
||||
|
||||
function NoRewardSettlement({
|
||||
betItems,
|
||||
message,
|
||||
selectedLabel,
|
||||
resultLabel,
|
||||
resultNumber,
|
||||
}: {
|
||||
betItems: StopBetItem[]
|
||||
message: string
|
||||
selectedLabel: string
|
||||
resultLabel: string
|
||||
resultNumber: number
|
||||
}) {
|
||||
const winningImageUrl = FLOWER_IMAGE_BY_ID[resultNumber]?.animalUrl ?? ''
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex w-design-720 max-w-[84%] flex-col items-center justify-center gap-design-18 rounded-[calc(var(--design-unit)*22)] border border-[rgba(124,232,255,0.34)] bg-[linear-gradient(180deg,rgba(7,28,40,0.96),rgba(3,12,20,0.98))] px-design-28 py-design-24 text-center shadow-[0_0_calc(var(--design-unit)*28)_rgba(70,245,255,0.28),0_0_calc(var(--design-unit)*64)_rgba(20,112,210,0.18),inset_0_0_calc(var(--design-unit)*24)_rgba(124,232,255,0.12)]">
|
||||
<div className="text-design-34 font-black tracking-[0.14em] text-[#D8FBFF] [text-shadow:0_0_calc(var(--design-unit)*16)_rgba(84,245,255,0.42)]">
|
||||
{message}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-stretch justify-center gap-design-14">
|
||||
<div className="flex w-design-180 shrink-0 flex-col items-center gap-design-8 rounded-[calc(var(--design-unit)*14)] border border-[rgba(120,255,127,0.3)] bg-[linear-gradient(180deg,rgba(5,42,36,0.82),rgba(3,16,20,0.9))] px-design-12 py-design-10">
|
||||
<div className="text-design-18 font-bold text-[rgba(216,251,255,0.78)]">
|
||||
{resultLabel}
|
||||
</div>
|
||||
<div className="relative h-design-76 w-design-76 overflow-hidden rounded-[calc(var(--design-unit)*10)] border border-[rgba(120,255,127,0.36)] bg-[rgba(1,8,13,0.84)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(120,255,127,0.2)]">
|
||||
{winningImageUrl ? (
|
||||
<img
|
||||
src={winningImageUrl}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="rounded-full border border-[rgba(120,255,127,0.38)] bg-[rgba(4,31,31,0.78)] px-design-16 py-design-4 text-design-26 font-black leading-none text-[#78FF7F] [text-shadow:0_0_calc(var(--design-unit)*12)_rgba(120,255,127,0.52)]">
|
||||
{String(resultNumber).padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-design-430 min-w-0 flex-col items-center gap-design-8 rounded-[calc(var(--design-unit)*14)] border border-[rgba(124,232,255,0.2)] bg-[rgba(3,13,20,0.38)] px-design-10 py-design-10">
|
||||
<div className="text-design-18 font-bold text-[rgba(216,251,255,0.78)]">
|
||||
{selectedLabel}
|
||||
</div>
|
||||
<div className="flex max-h-design-132 w-full flex-wrap items-center justify-center gap-design-8 overflow-hidden">
|
||||
{betItems.map((item) => (
|
||||
<div
|
||||
key={item.cellId}
|
||||
className="flex h-design-104 w-design-88 flex-col items-center justify-center gap-design-4 overflow-hidden rounded-[calc(var(--design-unit)*10)] border border-[rgba(124,232,255,0.22)] bg-[linear-gradient(180deg,rgba(8,34,42,0.9),rgba(3,13,20,0.96))] p-design-4"
|
||||
>
|
||||
<div className="relative h-design-76 w-design-76 overflow-hidden rounded-[calc(var(--design-unit)*10)] bg-[rgba(1,8,13,0.84)]">
|
||||
{item.imageUrl ? (
|
||||
<img
|
||||
src={item.imageUrl}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-design-20 font-black leading-none text-[#4BFFFE] [text-shadow:0_0_calc(var(--design-unit)*8)_rgba(75,255,254,0.46)]">
|
||||
{String(item.cellId).padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DesktopAnimalOverlay({
|
||||
showStopOverlay,
|
||||
}: DesktopAnimalOverlayProps) {
|
||||
@@ -197,6 +271,9 @@ export function DesktopAnimalOverlay({
|
||||
(state) => state.revealAnimation.rewardType,
|
||||
)
|
||||
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
||||
const revealWinningCellId = useGameRoundStore(
|
||||
(state) => state.revealAnimation.winningCellId,
|
||||
)
|
||||
const rewardAmount = useGameRoundStore(
|
||||
(state) => state.revealAnimation.rewardAmount,
|
||||
)
|
||||
@@ -212,6 +289,13 @@ export function DesktopAnimalOverlay({
|
||||
useState<RewardChildrenStage>('hidden')
|
||||
const [displayRewardAmount, setDisplayRewardAmount] = useState('0')
|
||||
const [hasRenderedReward, setHasRenderedReward] = useState(false)
|
||||
const [activeNoRewardKey, setActiveNoRewardKey] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
const [dismissedNoRewardKey, setDismissedNoRewardKey] = useState<
|
||||
string | null
|
||||
>(null)
|
||||
const [isNoRewardFadingOut, setIsNoRewardFadingOut] = useState(false)
|
||||
const stopImageSrc = i18n.resolvedLanguage?.startsWith('zh')
|
||||
? zhStopImage
|
||||
: enStopImage
|
||||
@@ -235,10 +319,30 @@ export function DesktopAnimalOverlay({
|
||||
revealPhase === 'result' && rewardType !== 'none' && rewardSource !== null
|
||||
const overlayAnimationKey = `${rewardType}-${roundId ?? 'round'}-${revealKey ?? 'pending'}`
|
||||
const childTimelineKey = shouldRenderReward ? overlayAnimationKey : 'closed'
|
||||
const hasWinningSelection =
|
||||
revealWinningCellId !== null &&
|
||||
recentSuccessfulSelections.some(
|
||||
(selection) => selection.cellId === revealWinningCellId,
|
||||
)
|
||||
const noRewardKey =
|
||||
roundId && revealWinningCellId !== null
|
||||
? `${roundId}:${revealWinningCellId}`
|
||||
: null
|
||||
const shouldRenderNoReward =
|
||||
revealPhase === 'result' &&
|
||||
rewardType === 'none' &&
|
||||
noRewardKey !== null &&
|
||||
lastBetPeriodNo === roundId &&
|
||||
recentSuccessfulSelections.length > 0 &&
|
||||
!hasWinningSelection &&
|
||||
activeNoRewardKey === noRewardKey
|
||||
|
||||
useEffect(() => {
|
||||
if (revealPhase !== 'result') {
|
||||
setHasRenderedReward(false)
|
||||
setActiveNoRewardKey(null)
|
||||
setDismissedNoRewardKey(null)
|
||||
setIsNoRewardFadingOut(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -247,6 +351,54 @@ export function DesktopAnimalOverlay({
|
||||
}
|
||||
}, [revealPhase, shouldRenderReward])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
revealPhase !== 'result' ||
|
||||
rewardType !== 'none' ||
|
||||
noRewardKey === null ||
|
||||
lastBetPeriodNo !== roundId ||
|
||||
recentSuccessfulSelections.length === 0 ||
|
||||
hasWinningSelection ||
|
||||
activeNoRewardKey === noRewardKey ||
|
||||
dismissedNoRewardKey === noRewardKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setActiveNoRewardKey(noRewardKey)
|
||||
setIsNoRewardFadingOut(false)
|
||||
}, [
|
||||
activeNoRewardKey,
|
||||
dismissedNoRewardKey,
|
||||
hasWinningSelection,
|
||||
lastBetPeriodNo,
|
||||
noRewardKey,
|
||||
recentSuccessfulSelections.length,
|
||||
revealPhase,
|
||||
rewardType,
|
||||
roundId,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeNoRewardKey) {
|
||||
return
|
||||
}
|
||||
|
||||
const fadeTimerId = window.setTimeout(() => {
|
||||
setIsNoRewardFadingOut(true)
|
||||
}, REWARD_OVERLAY_DURATION_MS)
|
||||
const clearTimerId = window.setTimeout(() => {
|
||||
setDismissedNoRewardKey(activeNoRewardKey)
|
||||
setActiveNoRewardKey(null)
|
||||
setIsNoRewardFadingOut(false)
|
||||
}, REWARD_OVERLAY_DURATION_MS + SETTLEMENT_OVERLAY_FADE_OUT_MS)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(fadeTimerId)
|
||||
window.clearTimeout(clearTimerId)
|
||||
}
|
||||
}, [activeNoRewardKey])
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldRenderReward) {
|
||||
return
|
||||
@@ -400,6 +552,29 @@ export function DesktopAnimalOverlay({
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldRenderNoReward && revealWinningCellId !== null) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-40 flex items-center justify-center overflow-hidden bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px] transition-opacity duration-300',
|
||||
isNoRewardFadingOut && '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%)]"
|
||||
/>
|
||||
<NoRewardSettlement
|
||||
betItems={stopBetItems}
|
||||
message={t('gameDesktop.animal.noReward')}
|
||||
selectedLabel={t('gameDesktop.animal.selectedResult')}
|
||||
resultLabel={t('gameDesktop.animal.winningResult')}
|
||||
resultNumber={revealWinningCellId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (hostingFlag) {
|
||||
return (
|
||||
<div className="absolute inset-0 z-40 flex flex-col items-center justify-center gap-design-22 bg-[rgba(2,8,14,0.38)] px-design-24 backdrop-blur-[1px]">
|
||||
|
||||
@@ -9,10 +9,10 @@ import lock from '@/assets/system/lock.webp'
|
||||
import statusCenter from '@/assets/system/status-center.webp'
|
||||
import statusLine from '@/assets/system/status-line.webp'
|
||||
import { LottiePlayer } from '@/components/lottie-player.tsx'
|
||||
import { MessageBroadcast } from '@/components/message-broadcast.tsx'
|
||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { DesktopCountdown } from '@/features/game/components/desktop/desktop-countdown.tsx'
|
||||
import { MessageBroadcast } from '@/features/game/components/desktop/desktop-title.tsx'
|
||||
import { useGameStatusVm } from '@/hooks/use-game-status-vm.ts'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import AutoScroll from 'embla-carousel-auto-scroll'
|
||||
import useEmblaCarousel from 'embla-carousel-react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import broadcast from '@/assets/system/broadcast.webp'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { cn } from '@/lib/utils.ts'
|
||||
import { useGameSessionStore } from '@/store/game'
|
||||
|
||||
const winAmountFormatter = new Intl.NumberFormat('en-US', {
|
||||
maximumFractionDigits: 6,
|
||||
})
|
||||
function getDesktopTitleCarouselCycleCount(titleCount: number) {
|
||||
if (titleCount >= 8) {
|
||||
return 2
|
||||
}
|
||||
|
||||
if (titleCount >= 5) {
|
||||
return 3
|
||||
}
|
||||
|
||||
if (titleCount >= 3) {
|
||||
return 5
|
||||
}
|
||||
|
||||
return 8
|
||||
}
|
||||
|
||||
function formatWinAmount(value: string) {
|
||||
const amount = Number(value)
|
||||
|
||||
return Number.isFinite(amount) ? winAmountFormatter.format(amount) : value
|
||||
}
|
||||
|
||||
type MessageBroadcastProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function MessageBroadcast({ className }: MessageBroadcastProps) {
|
||||
const jackpotBroadcasts = useGameSessionStore(
|
||||
(state) => state.jackpotBroadcasts,
|
||||
)
|
||||
const hasBroadcasts = jackpotBroadcasts.length > 0
|
||||
const titles = useMemo(
|
||||
() =>
|
||||
hasBroadcasts
|
||||
? jackpotBroadcasts.map((broadcast) => ({
|
||||
id: broadcast.id,
|
||||
message: `Player ${broadcast.nickname} won ${formatWinAmount(
|
||||
broadcast.totalWin,
|
||||
)}`,
|
||||
}))
|
||||
: [{ id: 'empty', message: '' }],
|
||||
[hasBroadcasts, jackpotBroadcasts],
|
||||
)
|
||||
const carouselTitles = useMemo(
|
||||
() =>
|
||||
hasBroadcasts
|
||||
? Array.from(
|
||||
{ length: getDesktopTitleCarouselCycleCount(titles.length) },
|
||||
(_, cycleIndex) =>
|
||||
titles.map((title) => ({
|
||||
...title,
|
||||
cycleIndex,
|
||||
id: `${title.id}:cycle-${cycleIndex}`,
|
||||
})),
|
||||
).flat()
|
||||
: titles.map((title) => ({ ...title, cycleIndex: 0 })),
|
||||
[hasBroadcasts, titles],
|
||||
)
|
||||
const carouselTitleCount = carouselTitles.length
|
||||
const autoScrollPlugin = useMemo(
|
||||
() =>
|
||||
AutoScroll({
|
||||
playOnInit: hasBroadcasts,
|
||||
speed: 0.7,
|
||||
startDelay: 0,
|
||||
stopOnFocusIn: false,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: false,
|
||||
}),
|
||||
[hasBroadcasts],
|
||||
)
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel(
|
||||
{
|
||||
align: 'start',
|
||||
dragFree: true,
|
||||
loop: hasBroadcasts,
|
||||
watchDrag: false,
|
||||
},
|
||||
[autoScrollPlugin],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!emblaApi) {
|
||||
return
|
||||
}
|
||||
|
||||
if (carouselTitleCount === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
emblaApi.reInit()
|
||||
const autoScroll = emblaApi.plugins().autoScroll
|
||||
|
||||
if (!hasBroadcasts) {
|
||||
autoScroll?.stop()
|
||||
return
|
||||
}
|
||||
|
||||
autoScroll?.reset()
|
||||
autoScroll?.play()
|
||||
}, [emblaApi, hasBroadcasts, carouselTitleCount])
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label="Jackpot broadcast"
|
||||
className={cn(
|
||||
'common-neon-inset flex w-full min-w-0 items-center overflow-hidden',
|
||||
'h-design-24 gap-design-5 !rounded-[3px] !px-design-7 !py-0 text-design-8',
|
||||
'md:h-design-65 md:gap-design-10 md:!rounded-[5px] md:!px-design-20 md:text-design-16',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<SmartImage
|
||||
className="h-design-12 w-design-12 shrink-0 md:h-design-24 md:w-design-24"
|
||||
alt={'broadcast'}
|
||||
src={broadcast}
|
||||
/>
|
||||
<div className="relative h-design-16 min-w-0 flex-1 overflow-hidden md:h-design-28">
|
||||
<div className="h-full overflow-hidden" ref={emblaRef}>
|
||||
<div className="flex h-full items-center gap-design-36 md:gap-design-80">
|
||||
{carouselTitles.map((title) => (
|
||||
<div
|
||||
aria-hidden={title.cycleIndex > 0}
|
||||
className="flex h-design-16 min-w-max shrink-0 items-center whitespace-nowrap !text-[#FF970F] md:h-design-28"
|
||||
key={title.id}
|
||||
>
|
||||
{title.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function DesktopTitle(props: MessageBroadcastProps) {
|
||||
return <MessageBroadcast {...props} />
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import type { BetSelection, RewardAnimationType } from '@/type'
|
||||
const REWARD_OVERLAY_FADE_OUT_MS = 300
|
||||
const REWARD_CHILDREN_FADE_IN_MS = 2_000
|
||||
const REWARD_CHILDREN_VISIBLE_MS = 1_000
|
||||
const SETTLEMENT_OVERLAY_FADE_OUT_MS = 300
|
||||
|
||||
type RewardChildrenStage = 'hidden' | 'visible' | 'exiting'
|
||||
type StopBetItem = {
|
||||
@@ -173,6 +174,79 @@ function StopBetSummary({
|
||||
)
|
||||
}
|
||||
|
||||
function NoRewardSettlement({
|
||||
betItems,
|
||||
message,
|
||||
selectedLabel,
|
||||
resultLabel,
|
||||
resultNumber,
|
||||
}: {
|
||||
betItems: StopBetItem[]
|
||||
message: string
|
||||
selectedLabel: string
|
||||
resultLabel: string
|
||||
resultNumber: number
|
||||
}) {
|
||||
const winningImageUrl = FLOWER_IMAGE_BY_ID[resultNumber]?.animalUrl ?? ''
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex w-design-336 max-w-[96%] flex-col items-center justify-center gap-design-9 rounded-[calc(var(--design-unit)*13)] border border-[rgba(124,232,255,0.34)] bg-[linear-gradient(180deg,rgba(7,28,40,0.96),rgba(3,12,20,0.98))] px-design-8 py-design-10 text-center shadow-[0_0_calc(var(--design-unit)*18)_rgba(70,245,255,0.28),0_0_calc(var(--design-unit)*34)_rgba(20,112,210,0.18),inset_0_0_calc(var(--design-unit)*16)_rgba(124,232,255,0.12)]">
|
||||
<div className="text-design-20 font-black tracking-[0.12em] text-[#D8FBFF] [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(84,245,255,0.42)]">
|
||||
{message}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-stretch justify-center gap-design-6">
|
||||
<div className="flex w-design-92 shrink-0 flex-col items-center gap-design-5 rounded-[calc(var(--design-unit)*9)] border border-[rgba(120,255,127,0.3)] bg-[linear-gradient(180deg,rgba(5,42,36,0.82),rgba(3,16,20,0.9))] px-design-5 py-design-6">
|
||||
<div className="text-design-10 font-bold text-[rgba(216,251,255,0.72)]">
|
||||
{resultLabel}
|
||||
</div>
|
||||
<div className="relative h-design-46 w-design-46 overflow-hidden rounded-[calc(var(--design-unit)*6)] border border-[rgba(120,255,127,0.32)] bg-[rgba(1,8,13,0.84)]">
|
||||
{winningImageUrl ? (
|
||||
<img
|
||||
src={winningImageUrl}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="rounded-full border border-[rgba(120,255,127,0.36)] bg-[rgba(4,31,31,0.72)] px-design-8 py-design-2 text-design-15 font-black leading-none text-[#78FF7F] [text-shadow:0_0_calc(var(--design-unit)*7)_rgba(120,255,127,0.52)]">
|
||||
{String(resultNumber).padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col items-center gap-design-5 rounded-[calc(var(--design-unit)*9)] border border-[rgba(124,232,255,0.2)] bg-[rgba(3,13,20,0.38)] px-design-4 py-design-5">
|
||||
<div className="text-design-10 font-bold text-[rgba(216,251,255,0.72)]">
|
||||
{selectedLabel}
|
||||
</div>
|
||||
<div className="flex max-h-design-92 w-full flex-wrap items-center justify-center gap-design-4 overflow-hidden">
|
||||
{betItems.map((item) => (
|
||||
<div
|
||||
key={item.cellId}
|
||||
className="flex h-design-52 w-design-42 flex-col items-center justify-center gap-design-2 overflow-hidden rounded-[calc(var(--design-unit)*6)] border border-[rgba(124,232,255,0.2)] bg-[linear-gradient(180deg,rgba(8,34,42,0.88),rgba(3,13,20,0.94))] p-design-2"
|
||||
>
|
||||
<div className="relative h-design-30 w-design-30 overflow-hidden rounded-[calc(var(--design-unit)*4)] bg-[rgba(1,8,13,0.84)]">
|
||||
{item.imageUrl ? (
|
||||
<img
|
||||
src={item.imageUrl}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-design-11 font-black leading-none text-[#4BFFFE] [text-shadow:0_0_calc(var(--design-unit)*6)_rgba(75,255,254,0.46)]">
|
||||
{String(item.cellId).padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobileAnimalOverlay({
|
||||
showStopOverlay,
|
||||
}: MobileAnimalOverlayProps) {
|
||||
@@ -195,6 +269,9 @@ export function MobileAnimalOverlay({
|
||||
(state) => state.revealAnimation.rewardType,
|
||||
)
|
||||
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
||||
const revealWinningCellId = useGameRoundStore(
|
||||
(state) => state.revealAnimation.winningCellId,
|
||||
)
|
||||
const rewardAmount = useGameRoundStore(
|
||||
(state) => state.revealAnimation.rewardAmount,
|
||||
)
|
||||
@@ -210,6 +287,13 @@ export function MobileAnimalOverlay({
|
||||
useState<RewardChildrenStage>('hidden')
|
||||
const [displayRewardAmount, setDisplayRewardAmount] = useState('0')
|
||||
const [hasRenderedReward, setHasRenderedReward] = useState(false)
|
||||
const [activeNoRewardKey, setActiveNoRewardKey] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
const [dismissedNoRewardKey, setDismissedNoRewardKey] = useState<
|
||||
string | null
|
||||
>(null)
|
||||
const [isNoRewardFadingOut, setIsNoRewardFadingOut] = useState(false)
|
||||
const stopImageSrc = i18n.resolvedLanguage?.startsWith('zh')
|
||||
? zhStopImage
|
||||
: enStopImage
|
||||
@@ -233,10 +317,30 @@ export function MobileAnimalOverlay({
|
||||
revealPhase === 'result' && rewardType !== 'none' && rewardSource !== null
|
||||
const overlayAnimationKey = `${rewardType}-${roundId ?? 'round'}-${revealKey ?? 'pending'}`
|
||||
const childTimelineKey = shouldRenderReward ? overlayAnimationKey : 'closed'
|
||||
const hasWinningSelection =
|
||||
revealWinningCellId !== null &&
|
||||
recentSuccessfulSelections.some(
|
||||
(selection) => selection.cellId === revealWinningCellId,
|
||||
)
|
||||
const noRewardKey =
|
||||
roundId && revealWinningCellId !== null
|
||||
? `${roundId}:${revealWinningCellId}`
|
||||
: null
|
||||
const shouldRenderNoReward =
|
||||
revealPhase === 'result' &&
|
||||
rewardType === 'none' &&
|
||||
noRewardKey !== null &&
|
||||
lastBetPeriodNo === roundId &&
|
||||
recentSuccessfulSelections.length > 0 &&
|
||||
!hasWinningSelection &&
|
||||
activeNoRewardKey === noRewardKey
|
||||
|
||||
useEffect(() => {
|
||||
if (revealPhase !== 'result') {
|
||||
setHasRenderedReward(false)
|
||||
setActiveNoRewardKey(null)
|
||||
setDismissedNoRewardKey(null)
|
||||
setIsNoRewardFadingOut(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -245,6 +349,54 @@ export function MobileAnimalOverlay({
|
||||
}
|
||||
}, [revealPhase, shouldRenderReward])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
revealPhase !== 'result' ||
|
||||
rewardType !== 'none' ||
|
||||
noRewardKey === null ||
|
||||
lastBetPeriodNo !== roundId ||
|
||||
recentSuccessfulSelections.length === 0 ||
|
||||
hasWinningSelection ||
|
||||
activeNoRewardKey === noRewardKey ||
|
||||
dismissedNoRewardKey === noRewardKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setActiveNoRewardKey(noRewardKey)
|
||||
setIsNoRewardFadingOut(false)
|
||||
}, [
|
||||
activeNoRewardKey,
|
||||
dismissedNoRewardKey,
|
||||
hasWinningSelection,
|
||||
lastBetPeriodNo,
|
||||
noRewardKey,
|
||||
recentSuccessfulSelections.length,
|
||||
revealPhase,
|
||||
rewardType,
|
||||
roundId,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeNoRewardKey) {
|
||||
return
|
||||
}
|
||||
|
||||
const fadeTimerId = window.setTimeout(() => {
|
||||
setIsNoRewardFadingOut(true)
|
||||
}, REWARD_OVERLAY_DURATION_MS)
|
||||
const clearTimerId = window.setTimeout(() => {
|
||||
setDismissedNoRewardKey(activeNoRewardKey)
|
||||
setActiveNoRewardKey(null)
|
||||
setIsNoRewardFadingOut(false)
|
||||
}, REWARD_OVERLAY_DURATION_MS + SETTLEMENT_OVERLAY_FADE_OUT_MS)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(fadeTimerId)
|
||||
window.clearTimeout(clearTimerId)
|
||||
}
|
||||
}, [activeNoRewardKey])
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldRenderReward) {
|
||||
return
|
||||
@@ -398,6 +550,29 @@ export function MobileAnimalOverlay({
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldRenderNoReward && revealWinningCellId !== null) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-40 flex items-center justify-center overflow-hidden bg-[rgba(2,8,14,0.66)] px-design-6 backdrop-blur-[2px] transition-opacity duration-300',
|
||||
isNoRewardFadingOut && '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.88)_62%,rgba(2,4,10,0.96)_100%)]"
|
||||
/>
|
||||
<NoRewardSettlement
|
||||
betItems={stopBetItems}
|
||||
message={t('gameDesktop.animal.noReward')}
|
||||
selectedLabel={t('gameDesktop.animal.selectedResult')}
|
||||
resultLabel={t('gameDesktop.animal.winningResult')}
|
||||
resultNumber={revealWinningCellId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (hostingFlag) {
|
||||
return (
|
||||
<div className="absolute inset-0 z-40 flex flex-col items-center justify-center gap-design-10 bg-[rgba(2,8,14,0.38)] px-design-10 backdrop-blur-[1px]">
|
||||
|
||||
Reference in New Issue
Block a user