refactor: 重构中奖和推送大奖事件,中奖过度动画,和开奖动画
This commit is contained in:
@@ -1,22 +1,20 @@
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
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 { LottiePlayer } from '@/components/lottie-player'
|
||||
import refreshIcon from '@/assets/system/refresh.webp'
|
||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { SmartImage } from '@/components/smart-image'
|
||||
import { useAnimalVm } from '@/features/game/hooks/use-animal-vm'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useGameRoundStore } from '@/store/game'
|
||||
|
||||
const revealBorderPath = new URL(
|
||||
'../../../../assets/lottie/test.json',
|
||||
import.meta.url,
|
||||
).href
|
||||
import { useGameAutoHostingStore, useGameRoundStore } from '@/store/game'
|
||||
|
||||
const animalModules = import.meta.glob('../../../../assets/animal/*.webp', {
|
||||
eager: true,
|
||||
@@ -35,6 +33,11 @@ const animalImageList = Object.entries(animalModules)
|
||||
.filter((item) => item.id > 0)
|
||||
.sort((left, right) => left.id - right.id)
|
||||
|
||||
const SETTLEMENT_REVEAL_RANDOM_DURATION_MS = 4_000
|
||||
const SETTLEMENT_REVEAL_RESULT_HOLD_MS = 1_000
|
||||
const SETTLEMENT_REVEAL_MIN_STEP_MS = 90
|
||||
const SETTLEMENT_REVEAL_MAX_STEP_MS = 480
|
||||
|
||||
function getRandomAnimalId(ids: number[], currentId: number | null) {
|
||||
if (ids.length === 0) {
|
||||
return null
|
||||
@@ -53,6 +56,17 @@ function getRandomAnimalId(ids: number[], currentId: number | null) {
|
||||
return nextId
|
||||
}
|
||||
|
||||
function getSettlementRevealStepDelay(progress: number) {
|
||||
const clampedProgress = Math.min(Math.max(progress, 0), 1)
|
||||
const easedProgress = clampedProgress ** 1.65
|
||||
|
||||
return (
|
||||
SETTLEMENT_REVEAL_MIN_STEP_MS +
|
||||
(SETTLEMENT_REVEAL_MAX_STEP_MS - SETTLEMENT_REVEAL_MIN_STEP_MS) *
|
||||
easedProgress
|
||||
)
|
||||
}
|
||||
|
||||
interface DesktopAnimalProps {
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
@@ -67,6 +81,7 @@ export function DesktopAnimal({
|
||||
onSelect,
|
||||
}: DesktopAnimalProps) {
|
||||
const { i18n, t } = useTranslation()
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
const animalIds = useMemo(() => animalImageList.map((item) => item.id), [])
|
||||
const containerRef = useRef<HTMLElement | null>(null)
|
||||
const cellRefs = useRef(new Map<number, HTMLButtonElement>())
|
||||
@@ -77,6 +92,7 @@ export function DesktopAnimal({
|
||||
top: number
|
||||
width: number
|
||||
} | null>(null)
|
||||
const [isRevealHoldingResult, setIsRevealHoldingResult] = useState(false)
|
||||
const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase)
|
||||
const revealWinningCellId = useGameRoundStore(
|
||||
(state) => state.revealAnimation.winningCellId,
|
||||
@@ -86,6 +102,11 @@ export function DesktopAnimal({
|
||||
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,
|
||||
)
|
||||
@@ -99,8 +120,10 @@ export function DesktopAnimal({
|
||||
selectionByCell,
|
||||
showStandbyState,
|
||||
} = useAnimalVm(animalIds, onSelect)
|
||||
|
||||
const isRevealRunning =
|
||||
revealPhase === 'spinning' || revealPhase === 'stopping'
|
||||
revealPhase === 'spinning' ||
|
||||
(revealPhase === 'stopping' && !isRevealHoldingResult)
|
||||
const isRevealResult = revealPhase === 'result'
|
||||
const hasSubmittedCurrentRound =
|
||||
roundPhase === 'betting' && Boolean(roundId) && lastBetPeriodNo === roundId
|
||||
@@ -115,15 +138,18 @@ export function DesktopAnimal({
|
||||
useEffect(() => {
|
||||
if (revealPhase === 'idle') {
|
||||
setRevealCellId(null)
|
||||
setIsRevealHoldingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (revealPhase === 'result') {
|
||||
setRevealCellId(revealWinningCellId)
|
||||
setIsRevealHoldingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (revealPhase === 'spinning') {
|
||||
setIsRevealHoldingResult(false)
|
||||
setRevealCellId((currentId) => getRandomAnimalId(animalIds, currentId))
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
@@ -136,35 +162,40 @@ export function DesktopAnimal({
|
||||
}
|
||||
|
||||
if (revealWinningCellId === null) {
|
||||
setIsRevealHoldingResult(false)
|
||||
return
|
||||
}
|
||||
|
||||
let elapsedMs = 0
|
||||
const timeoutIds: number[] = []
|
||||
const startedAt = performance.now()
|
||||
let timeoutId = 0
|
||||
setIsRevealHoldingResult(false)
|
||||
|
||||
for (let index = 0; index < 14; index += 1) {
|
||||
elapsedMs += 42 + index * 13
|
||||
timeoutIds.push(
|
||||
window.setTimeout(() => {
|
||||
setRevealCellId((currentId) =>
|
||||
getRandomAnimalId(animalIds, currentId),
|
||||
)
|
||||
}, elapsedMs),
|
||||
)
|
||||
const step = () => {
|
||||
const elapsedMs = performance.now() - startedAt
|
||||
|
||||
if (elapsedMs >= SETTLEMENT_REVEAL_RANDOM_DURATION_MS) {
|
||||
setRevealCellId(revealWinningCellId)
|
||||
setIsRevealHoldingResult(true)
|
||||
timeoutId = window.setTimeout(() => {
|
||||
finishRevealAnimation()
|
||||
}, SETTLEMENT_REVEAL_RESULT_HOLD_MS)
|
||||
return
|
||||
}
|
||||
|
||||
setRevealCellId((currentId) => getRandomAnimalId(animalIds, currentId))
|
||||
|
||||
const progress = elapsedMs / SETTLEMENT_REVEAL_RANDOM_DURATION_MS
|
||||
const nextDelayMs = getSettlementRevealStepDelay(progress)
|
||||
const remainingMs = SETTLEMENT_REVEAL_RANDOM_DURATION_MS - elapsedMs
|
||||
|
||||
timeoutId = window.setTimeout(step, Math.min(nextDelayMs, remainingMs))
|
||||
}
|
||||
|
||||
elapsedMs += 160
|
||||
timeoutIds.push(
|
||||
window.setTimeout(() => {
|
||||
setRevealCellId(revealWinningCellId)
|
||||
finishRevealAnimation()
|
||||
}, elapsedMs),
|
||||
)
|
||||
setRevealCellId((currentId) => getRandomAnimalId(animalIds, currentId))
|
||||
timeoutId = window.setTimeout(step, SETTLEMENT_REVEAL_MIN_STEP_MS)
|
||||
|
||||
return () => {
|
||||
for (const timeoutId of timeoutIds) {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}, [animalIds, finishRevealAnimation, revealPhase, revealWinningCellId])
|
||||
|
||||
@@ -214,7 +245,9 @@ export function DesktopAnimal({
|
||||
const selectionMeta = selectionByCell[item.id]
|
||||
const hasPlacedSelection = Boolean(selectionMeta)
|
||||
const isMarqueeActive = showStandbyState && item.id === marqueeId
|
||||
const isRevealWinner = isRevealResult && revealWinningCellId === item.id
|
||||
const isRevealWinner =
|
||||
(isRevealResult || isRevealHoldingResult) &&
|
||||
revealWinningCellId === item.id
|
||||
const warningType =
|
||||
cellWarning?.cellId === item.id ? cellWarning.type : null
|
||||
const showCellWarning = warningType !== null
|
||||
@@ -394,18 +427,27 @@ export function DesktopAnimal({
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute z-40 transition-[height,transform,width] duration-75 ease-linear"
|
||||
style={{
|
||||
height: revealFrame.height + 16,
|
||||
transform: `translate(${revealFrame.left - 8}px, ${revealFrame.top - 8}px)`,
|
||||
width: revealFrame.width + 16,
|
||||
height: revealFrame.height,
|
||||
transform: `translate(${revealFrame.left}px, ${revealFrame.top}px)`,
|
||||
width: revealFrame.width,
|
||||
}}
|
||||
>
|
||||
<LottiePlayer
|
||||
path={revealBorderPath}
|
||||
renderer="svg"
|
||||
loop
|
||||
autoplay
|
||||
speed={1.8}
|
||||
className="h-full w-full scale-[1.18] [&>svg]:h-full [&>svg]:w-full"
|
||||
<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}
|
||||
@@ -426,6 +468,42 @@ export function DesktopAnimal({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{hostingFlag ? (
|
||||
<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]">
|
||||
<SmartBackground
|
||||
src={hostingBg}
|
||||
className="h-design-350 w-design-930 flex flex-col items-center justify-center"
|
||||
>
|
||||
<div className={'flex flex-col gap-design-40 items-center'}>
|
||||
<div className={'flex items-center gap-design-20'}>
|
||||
<SmartImage
|
||||
src={refreshIcon}
|
||||
alt="refreshIcon"
|
||||
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.28)]"
|
||||
/>
|
||||
<div className={'text-design-20 text-[#ffffff] font-bold'}>
|
||||
{t('game.autoSpin.runningRounds', {
|
||||
count: completedAutoHostingRounds,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<SmartBackground
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={stopHosting}
|
||||
src={hostingBtn}
|
||||
className="h-design-80 w-design-170 flex cursor-pointer flex-col items-center justify-center transition-transform hover:-translate-y-[1px] active:translate-y-0"
|
||||
>
|
||||
{t('game.actions.stopAuto')}
|
||||
</SmartBackground>
|
||||
</div>
|
||||
</SmartBackground>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showStandbyState ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -468,7 +468,7 @@ export function DesktopControl() {
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
'relative z-10 flex h-full w-design-260 shrink-0 items-center justify-center bg-center bg-no-repeat text-design-32 font-bold',
|
||||
'relative z-10 flex h-full w-design-260 shrink-0 items-center justify-center bg-center bg-no-repeat text-design-24 font-bold',
|
||||
isConfirmClickable ? 'cursor-pointer' : 'cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
Mail,
|
||||
Maximize,
|
||||
Minimize,
|
||||
UserKey,
|
||||
UserRoundPlus,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
} from 'lucide-react'
|
||||
@@ -223,7 +225,7 @@ export function DesktopHeader() {
|
||||
}
|
||||
onClick={onOpenLogin}
|
||||
>
|
||||
<CircleAlert color={'#57B8BF'} size={16} />
|
||||
<UserKey color={'#57B8BF'} size={16} />
|
||||
<div>{t('gameDesktop.header.login')}</div>
|
||||
</button>
|
||||
<button
|
||||
@@ -233,7 +235,7 @@ export function DesktopHeader() {
|
||||
}
|
||||
onClick={onOpenRegister}
|
||||
>
|
||||
<CircleAlert color={'#57B8BF'} size={16} />
|
||||
<UserRoundPlus color={'#57B8BF'} size={16} />
|
||||
<div>{t('gameDesktop.header.register')}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
244
src/features/game/components/desktop/desktop-reward-overlay.tsx
Normal file
244
src/features/game/components/desktop/desktop-reward-overlay.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
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
|
||||
@@ -6,7 +6,6 @@ import fire from '@/assets/system/fire.webp'
|
||||
import lock from '@/assets/system/lock.webp'
|
||||
import statusCenter from '@/assets/system/status-center.webp'
|
||||
import statusLine from '@/assets/system/status-line.webp'
|
||||
import streakBg from '@/assets/system/streak.webp'
|
||||
import { LottiePlayer } from '@/components/lottie-player.tsx'
|
||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
|
||||
@@ -1,18 +1,53 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import broadcast from '@/assets/system/broadcast.webp'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { useGameSessionStore } from '@/store/game'
|
||||
|
||||
export function DesktopTitle() {
|
||||
const { t } = useTranslation()
|
||||
const jackpotBroadcasts = useGameSessionStore(
|
||||
(state) => state.jackpotBroadcasts,
|
||||
)
|
||||
const titles =
|
||||
jackpotBroadcasts.length > 0
|
||||
? jackpotBroadcasts.map((broadcast) => ({
|
||||
id: broadcast.id,
|
||||
message: broadcast.message,
|
||||
}))
|
||||
: [{ id: 'empty', message: '' }]
|
||||
const marqueeTitles =
|
||||
jackpotBroadcasts.length > 0
|
||||
? [
|
||||
...titles.map((title) => ({ ...title, id: `${title.id}:first` })),
|
||||
...titles.map((title) => ({ ...title, id: `${title.id}:second` })),
|
||||
]
|
||||
: titles
|
||||
|
||||
return (
|
||||
<section className="common-neon-inset text-design-16 w-full flex h-design-65 items-center gap-design-10 !px-design-20 ">
|
||||
<section className="common-neon-inset text-design-16 w-full flex h-design-65 items-center gap-design-10 !px-design-20 overflow-hidden">
|
||||
<SmartImage
|
||||
className={'w-design-24 h-design-24'}
|
||||
alt={'broadcast'}
|
||||
src={broadcast}
|
||||
/>
|
||||
<div className={'!text-[#FF970F]'}>
|
||||
{t('gameDesktop.title.announcement')}
|
||||
<div className="shrink-0 !text-[#FF970F]">
|
||||
{t('gameDesktop.title.announcement')}:
|
||||
</div>
|
||||
<div className="relative h-design-28 min-w-0 flex-1 overflow-hidden">
|
||||
<div
|
||||
className={
|
||||
jackpotBroadcasts.length > 0 ? 'desktop-title-vertical-marquee' : ''
|
||||
}
|
||||
>
|
||||
{marqueeTitles.map((title) => (
|
||||
<div
|
||||
className="flex h-design-28 items-center whitespace-nowrap !text-[#FF970F]"
|
||||
key={title.id}
|
||||
>
|
||||
{title.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -654,7 +654,7 @@ function DesktopWithdraw() {
|
||||
>
|
||||
{withdrawSubmitMutation.isPending
|
||||
? t('commonUi.action.submitting')
|
||||
: `${t('gameDesktop.withdraw.confirm')} ${t('gameDesktop.withdraw.withdrawal')}`}
|
||||
: `${t('gameDesktop.withdraw.confirm')}${t('gameDesktop.withdraw.withdrawal')}`}
|
||||
</SmartBackground>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -162,8 +162,8 @@ export function EntryNoticeGateModal() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-center gap-design-28">
|
||||
<label className="inline-flex cursor-pointer items-center gap-design-12 text-design-20 text-[#C4F2F7]">
|
||||
<div className="flex shrink-0 flex-col items-center justify-center gap-design-20">
|
||||
<label className="inline-flex cursor-pointer items-center justify-center gap-design-12 text-design-20 text-[#C4F2F7]">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hasAgreed}
|
||||
|
||||
Reference in New Issue
Block a user