2025-12-12 02:15:19 +00:00
|
|
|
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(() => {
|
2025-12-12 03:22:12 +00:00
|
|
|
// 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
|
2025-12-12 02:15:19 +00:00
|
|
|
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 [
|
2025-12-12 03:22:12 +00:00
|
|
|
createSequenceFirstReel(), // Reel 1: [0, 1]
|
|
|
|
|
createSequence(), // Reel 2: [0, 1-9 shuffled]
|
|
|
|
|
createSequence(), // Reel 3: [0, 1-9 shuffled]
|
2025-12-12 02:15:19 +00:00
|
|
|
];
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
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];
|
|
|
|
|
|
|
|
|
|
// Repeat the randomized sequence NUM_DUPLICATES times, then one final cycle
|
|
|
|
|
for (let i = 0; i < NUM_DUPLICATES + 1; 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>
|
|
|
|
|
);
|