lukcy-draw/src/components/SlotMachine.jsx
2025-12-12 10:25:25 +07:00

194 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef, useState, useMemo } from 'react';
import { motion, useAnimation } from 'framer-motion';
const NUM_DUPLICATES = 5; // How many loops for the spin effect
// Fisher-Yates shuffle algorithm
const shuffleArray = (array) => {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
};
// Responsive slot heights based on screen size
const getSlotHeight = () => {
if (typeof window === 'undefined') return 320;
const width = window.innerWidth;
if (width < 640) return 96; // mobile (h-24)
if (width < 768) return 128; // sm (h-32)
if (width < 1024) return 176; // md (h-44)
if (width < 1280) return 224; // lg (h-56)
if (width < 1536) return 288; // xl (h-72)
return 320; // 2xl (h-80)
};
export function SlotMachine({ winner, isSpinning, onAnimationComplete, onDraw, poolLength }) {
const controls1 = useAnimation();
const controls2 = useAnimation();
const controls3 = useAnimation();
const [slotHeight, setSlotHeight] = useState(getSlotHeight());
// Generate randomized sequences for each reel (memoized so they stay consistent)
// Always start with 0 at position 0, then shuffle the rest
const randomSequences = useMemo(() => {
// First reel (hundreds): only 0 and 1
const createSequenceFirstReel = () => {
const remaining = [1];
const shuffled = shuffleArray(remaining);
return [0, ...shuffled]; // Always put 0 first, then 1
};
// Second and third reels: 0-9
const createSequence = () => {
const remaining = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const shuffled = shuffleArray(remaining);
return [0, ...shuffled]; // Always put 0 first
};
return [
createSequenceFirstReel(), // Reel 1: [0, 1]
createSequence(), // Reel 2: [0, 1-9 shuffled]
createSequence(), // Reel 3: [0, 1-9 shuffled]
];
}, []);
useEffect(() => {
const handleResize = () => setSlotHeight(getSlotHeight());
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
if (isSpinning && winner !== null) {
const digits = winner.toString().padStart(3, '0').split('').map(Number);
animateReels(digits);
}
}, [isSpinning, winner]);
const animateReels = async (digits) => {
// Reset positions instantly
await Promise.all([
controls1.set({ y: 0 }),
controls2.set({ y: 0 }),
controls3.set({ y: 0 })
]);
// Animate each reel with stagger
// For each digit, find its position in the randomized sequence
const spin = (control, digit, reelIndex, delay) => {
// Find where this digit appears in the last cycle of the randomized sequence
const lastCycleStart = NUM_DUPLICATES * 10;
const digitIndexInSequence = randomSequences[reelIndex].indexOf(digit);
const targetIndex = lastCycleStart + digitIndexInSequence;
const targetY = -(targetIndex * slotHeight);
return control.start({
y: targetY,
transition: {
duration: 20 + delay,
ease: [0.1, 0.9, 0.2, 1], // "Luxury" ease-out
delay: 0 // We can rely on duration difference for staggering or explicit delay
}
});
};
// Staggered finish times: 20s, 20.5s, 21s
await Promise.all([
spin(controls1, digits[0], 0, 0),
spin(controls2, digits[1], 1, 0.5),
spin(controls3, digits[2], 2, 1.0)
]);
if (onAnimationComplete) {
onAnimationComplete();
}
};
const Strip = ({ controls, reelIndex }) => {
// Generate the number strip with randomized sequence
const numbers = [];
const sequence = randomSequences[reelIndex];
// For the first reel (only 0-1), we need more repetitions to fill the strip
// For other reels (0-9), use normal repetitions
const repetitions = reelIndex === 0
? (NUM_DUPLICATES + 1) * 5 // First reel: repeat 30 times (2 numbers × 30 = 60 total)
: (NUM_DUPLICATES + 1); // Other reels: repeat 6 times (10 numbers × 6 = 60 total)
for (let i = 0; i < repetitions; i++) {
sequence.forEach(num => numbers.push(num));
}
return (
<motion.div
className="flex flex-col items-center w-full"
animate={controls}
>
{numbers.map((num, i) => (
<div
key={i}
className="flex items-center justify-center font-black relative"
style={{
height: slotHeight,
width: '100%',
fontSize: `${slotHeight * 0.78}px`,
fontWeight: 'bold',
fontFamily: 'system-ui, -apple-system, sans-serif',
background: 'linear-gradient(to bottom, #bf953f 0%, #fcf6ba 40%, #b38728 55%, #fbf5b7 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
filter: 'drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3))',
lineHeight: '1',
}}
>
{num}
</div>
))}
</motion.div>
);
};
const handleClick = () => {
if (!isSpinning && poolLength > 0 && onDraw) {
onDraw();
}
};
return (
<div
className={`relative flex items-center justify-center z-20 w-full ${
!isSpinning && poolLength > 0 ? 'cursor-pointer' : 'cursor-not-allowed'
}`}
onClick={handleClick}
>
<div className={`flex gap-2 sm:gap-3 md:gap-4 lg:gap-5 xl:gap-6 justify-center transition-transform duration-200 ${
!isSpinning && poolLength > 0 ? 'hover:scale-105 active:scale-95' : ''
}`}>
<SlotBox><Strip controls={controls1} reelIndex={0} /></SlotBox>
<SlotBox><Strip controls={controls2} reelIndex={1} /></SlotBox>
<SlotBox><Strip controls={controls3} reelIndex={2} /></SlotBox>
</div>
</div>
);
}
const SlotBox = ({ children }) => (
<div
className="relative w-32 h-40 sm:w-40 sm:h-48 md:w-48 md:h-60 lg:w-60 lg:h-[18rem] xl:w-72 xl:h-[22rem] 2xl:w-80 2xl:h-96 flex items-start justify-center overflow-hidden"
style={{
borderRadius: '1.5rem',
background: '#FFFFFF',
border: '3px solid #D4AF37',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3), 0 0 0 2px #D4AF37'
}}
>
<div className="relative w-full h-full">
{children}
</div>
</div>
);