feat: add slot machine
103
frontend/casino/src/app/components/App/Slot/Reel.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { useState, useEffect, useRef, useLayoutEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import symbols2 from "../../../pages/SlotGamePage/symbols2";
|
||||
|
||||
function Reel({ isHorizontal, rng, rngReverse, cellCount }) {
|
||||
const carousel = useRef(null);
|
||||
const refs = useRef([]);
|
||||
const cells = refs.current.map((value) => value);
|
||||
const [selectedIndex, setSelectedIndex] = useState(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
let rotateFn = isHorizontal ? "rotateY" : "rotateX";
|
||||
let radius;
|
||||
let theta;
|
||||
let cellAngle;
|
||||
|
||||
function randomNumberInRange(min, max) {
|
||||
// 👇️ get number between min (inclusive) and max (inclusive)
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return cellCount < 7
|
||||
? setSelectedIndex(selectedIndex + randomNumberInRange(3, 6))
|
||||
: setSelectedIndex(selectedIndex + randomNumberInRange(6, 14));
|
||||
}, [rng]);
|
||||
useEffect(() => {
|
||||
return cellCount < 7
|
||||
? setSelectedIndex(selectedIndex + randomNumberInRange(-3, -6))
|
||||
: setSelectedIndex(selectedIndex + randomNumberInRange(-6, -14));
|
||||
}, [rngReverse]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setWidth(carousel.current.offsetWidth);
|
||||
setHeight(carousel.current.offsetHeight);
|
||||
setSelectedIndex(0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
theta = 360 / cellCount;
|
||||
rotateFn = isHorizontal ? "rotateY" : "rotateX";
|
||||
const angle = theta * selectedIndex * -1;
|
||||
carousel.current.style.transform = `translateZ(${-radius}px) ${rotateFn}(${angle}deg)`;
|
||||
}, [selectedIndex]);
|
||||
|
||||
function rotateCarousel() {
|
||||
const angles = theta * selectedIndex * -1;
|
||||
carousel.current.style.transform = `translateZ(${-radius}px) ${rotateFn}(${angles}deg)`;
|
||||
}
|
||||
|
||||
function changeCarousel() {
|
||||
theta = 360 / cellCount;
|
||||
const cellSize = isHorizontal ? width : height;
|
||||
radius = Math.round(cellSize / 2 / Math.tan(Math.PI / cellCount));
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const cell = cells[i];
|
||||
if (i < cellCount) {
|
||||
cell.style.opacity = 1;
|
||||
cellAngle = theta * i;
|
||||
cell.style.transform = `${rotateFn}(${cellAngle}deg) translateZ(${radius}px)`;
|
||||
rotateCarousel();
|
||||
} else {
|
||||
cell.style.opacity = 0;
|
||||
cell.style.transform = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onOrientationChange() {
|
||||
rotateFn = isHorizontal ? "rotateY" : "rotateX";
|
||||
changeCarousel();
|
||||
}
|
||||
// set initials
|
||||
onOrientationChange();
|
||||
|
||||
return (
|
||||
<div className="scene">
|
||||
<div className="carousel" ref={carousel}>
|
||||
{symbols2.map((c, index) => (
|
||||
<div
|
||||
key={c.id}
|
||||
className={`carousel__cell flex bg-white ${c.transform}`}
|
||||
ref={(element) => {
|
||||
refs.current[index] = element;
|
||||
}}
|
||||
>
|
||||
<img src={c.src} alt="..." className="h-24 w-24 m-auto" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reel.propTypes = {
|
||||
isHorizontal: PropTypes.bool.isRequired,
|
||||
cellCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
rng: PropTypes.bool.isRequired,
|
||||
rngReverse: PropTypes.bool.isRequired,
|
||||
};
|
||||
export default Reel;
|
|
@ -1,31 +1,67 @@
|
|||
import ContainerLayout from "../../utils/ContainerLayout"
|
||||
import { RPSPlayButton } from "../../components/web3/RPSPlayButton"
|
||||
import PropTypes from 'prop-types';
|
||||
import Reel from '../../components/App/Slot/Reel';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { RPSPlayButton } from '../../components/web3/RPSPlayButton';
|
||||
|
||||
export const SlotGamePage = () => {
|
||||
const [isHorizontal, setIsHorizontal] = useState(false)
|
||||
const [cellCount, setCellCount] = useState(7)
|
||||
const [rng, setRng] = useState(false)
|
||||
const [rngReverse, setRngReverse] = useState(false)
|
||||
|
||||
const mql = window.matchMedia('(orientation: portrait)')
|
||||
|
||||
mql.onchange = (e) => {
|
||||
if (e.matches) {
|
||||
setIsHorizontal(true)
|
||||
} else {
|
||||
setIsHorizontal(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRng = () => {
|
||||
setRng(!rng)
|
||||
}
|
||||
const handleRngReverse = () => {
|
||||
setRngReverse(!rngReverse)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (mql.matches) {
|
||||
setIsHorizontal(true)
|
||||
} else {
|
||||
setIsHorizontal(false)
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<ContainerLayout>
|
||||
<div className="flex flex-col items-center text-white">
|
||||
<p className="text-[46px] font-bold uppercase">
|
||||
брось кубики онлайн!
|
||||
</p>
|
||||
<div className="justify-center flex pointer-events-none">
|
||||
<div className="absolute w-[710px] h-[456px] bg-lime-800 rounded-[187px] blur-[300px]" />
|
||||
</div>
|
||||
<div className="flex flex-row min-w-full justify-center gap-10 relative mt-12">
|
||||
<img src="src/assets/img/dice-img.png" alt="Dice" />
|
||||
<img src="src/assets/img/dice-img.png" alt="Dice" />
|
||||
</div>
|
||||
{/* Кнопка для бека */}
|
||||
<RPSPlayButton />
|
||||
<div className="flex flex-row justify-around w-full font-semibold">
|
||||
<div className="bg-gray-500 rounded-[30px] py-5 px-10 text-2xl">
|
||||
Вы загадали: 5
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="max-h-[180px] bg-green-700 flex flex-col justify-between shadow text-white text-2xl font-bold py-4 px-6 rounded-xl">
|
||||
<div className="flex flex-col">
|
||||
<p>выигрыш:</p><span className="text-yellow-400">5000</span>
|
||||
</div>
|
||||
<div className="bg-gray-800 rounded-[30px] py-5 px-10 text-2xl">
|
||||
Выпало число: 5
|
||||
<div className="flex flex-col">
|
||||
<p>ставка:</p><span className="text-yellow-400">10 000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-[800px] w-full bg-fuchsia-600 flex items-center justify-center">
|
||||
<div className="flex flex-row portrait:flex-col justify-center items-center my-auto">
|
||||
<Reel rng={rng} rngReverse={rngReverse} cellCount={cellCount} isHorizontal={isHorizontal} />
|
||||
<Reel rng={rng} rngReverse={rngReverse} cellCount={cellCount} isHorizontal={isHorizontal} />
|
||||
<Reel rng={rng} rngReverse={rngReverse} cellCount={cellCount} isHorizontal={isHorizontal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-[100px]">
|
||||
<RPSPlayButton />
|
||||
</div>
|
||||
</div>
|
||||
</ContainerLayout>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
SlotGamePage.propTypes = {
|
||||
cellCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
|
||||
};
|
82
frontend/casino/src/app/pages/SlotGamePage/symbols2.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import bar from '../../../assets/img/slot/bar-512.png'
|
||||
import bell from '../../../assets/img/slot/bell-512.png'
|
||||
import cherries from '../../../assets/img/slot/cherries-512.png'
|
||||
import crown from '../../../assets/img/slot/crown-512.png'
|
||||
import diamond from '../../../assets/img/slot/diamond-512.png'
|
||||
import horseshoe from '../../../assets/img/slot/horseshoe-512.png'
|
||||
import seven from '../../../assets/img/slot/seven-512.png'
|
||||
import watermelon from '../../../assets/img/slot/watermelon-512.png'
|
||||
|
||||
const symbols2 = [
|
||||
{
|
||||
transform: 'rotateY(0deg) translateZ(288px)',
|
||||
src: seven,
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
transform: 'rotateY(40deg) translateZ(288px)',
|
||||
src: bar,
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
transform: 'rotateY(80deg) translateZ(288px)',
|
||||
src: bell,
|
||||
id: 3,
|
||||
},
|
||||
{
|
||||
transform: 'rotateY(120deg) translateZ(288px)',
|
||||
src: cherries,
|
||||
id: 4,
|
||||
},
|
||||
{
|
||||
transform: 'rotateY(160deg) translateZ(288px)',
|
||||
src: horseshoe,
|
||||
id: 5,
|
||||
},
|
||||
{
|
||||
transform: 'rotateY(200deg) translateZ(288px)',
|
||||
src: bar,
|
||||
id: 6,
|
||||
},
|
||||
{
|
||||
transform: 'rotateY(240deg) translateZ(288px)',
|
||||
src: crown,
|
||||
id: 7,
|
||||
},
|
||||
{
|
||||
transform: 'rotateY(280deg) translateZ(288px)',
|
||||
src: diamond,
|
||||
id: 8,
|
||||
},
|
||||
{
|
||||
transform: 'rotateY(320deg) translateZ(288px)',
|
||||
src: bar,
|
||||
id: 9,
|
||||
},
|
||||
{
|
||||
src: horseshoe,
|
||||
id: 10,
|
||||
},
|
||||
{
|
||||
src: bar,
|
||||
id: 11,
|
||||
},
|
||||
{
|
||||
src: seven,
|
||||
id: 12,
|
||||
},
|
||||
{
|
||||
src: bar,
|
||||
id: 13,
|
||||
},
|
||||
{
|
||||
src: watermelon,
|
||||
id: 14,
|
||||
},
|
||||
{
|
||||
src: cherries,
|
||||
id: 15,
|
||||
},
|
||||
]
|
||||
|
||||
export default symbols2
|
BIN
frontend/casino/src/assets/img/slot/apple-512.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
frontend/casino/src/assets/img/slot/bar-512.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
frontend/casino/src/assets/img/slot/bell-512.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
frontend/casino/src/assets/img/slot/cherries-512.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
frontend/casino/src/assets/img/slot/clover-512.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
frontend/casino/src/assets/img/slot/coin-512.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
frontend/casino/src/assets/img/slot/crown-512.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
frontend/casino/src/assets/img/slot/diamond-512.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
frontend/casino/src/assets/img/slot/grapes-512.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
frontend/casino/src/assets/img/slot/horseshoe-512.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
frontend/casino/src/assets/img/slot/orange-512.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
frontend/casino/src/assets/img/slot/seven-512.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
frontend/casino/src/assets/img/slot/star-512.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
frontend/casino/src/assets/img/slot/watermelon-512.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
frontend/casino/src/assets/img/slot/win-512.png
Normal file
After Width: | Height: | Size: 16 KiB |
|
@ -1,3 +1,39 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities;
|
||||
|
||||
.scene {
|
||||
border: 1px solid #ccc;
|
||||
position: relative;
|
||||
width: 210px;
|
||||
height: 140px;
|
||||
perspective: 1500px;
|
||||
}
|
||||
|
||||
.carousel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transform: translateZ(-288px);
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 1.4s;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.carousel__cell {
|
||||
position: absolute;
|
||||
width: 190px;
|
||||
height: 120px;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
border: 2px solid black;
|
||||
line-height: 116px;
|
||||
font-size: 80px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-align: center;
|
||||
transition:
|
||||
transform 1s,
|
||||
opacity 1s;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
|