194 lines
7.3 KiB
JavaScript
194 lines
7.3 KiB
JavaScript
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>
|
||
);
|