Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: slots minigame #480

Merged
merged 14 commits into from
Feb 5, 2025
3 changes: 2 additions & 1 deletion assets/css/components.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "components/avatar.css";
@import "components/field.css";
@import "components/dropdown.css";
@import "components/coinflip.css";
@import "components/coinflip.css";
@import "components/slots_reel.css"
4 changes: 4 additions & 0 deletions assets/css/components/slots_reel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.reel-slot {
width: 79px;
height: 237px;
}
4 changes: 3 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
import live_select from "live_select"
import { QrScanner, Wheel, Confetti, Countdown, Sorting, CoinFlip, Redirect, CredentialScene , Banner } from "./hooks";
import { QrScanner, Wheel, Confetti, Countdown, Sorting, CoinFlip, Redirect, CredentialScene , Banner, ReelAnimation, PaytableModal } from "./hooks";

let Hooks = {
QrScanner: QrScanner,
Expand All @@ -34,6 +34,8 @@ let Hooks = {
CoinFlip: CoinFlip,
Redirect: Redirect,
CredentialScene: CredentialScene,
ReelAnimation: ReelAnimation,
PaytableModal: PaytableModal,
...live_select
};

Expand Down
2 changes: 1 addition & 1 deletion assets/js/hooks/confetti.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const Confetti = {
const jsConfetti = new JSConfetti();
if(this.el.dataset.is_win != undefined) {
jsConfetti.addConfetti({
confettiColors: ["#ff800d", "#fc993f"],
confettiColors: ["#ffdb0d", "#fee243"],
confettiRadius: 6
});
}
Expand Down
2 changes: 2 additions & 0 deletions assets/js/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export { Countdown } from "./countdown.js";
export { CoinFlip } from "./coinflip.js";
export { Redirect } from "./redirect.js";
export { CredentialScene } from "./credential-scene.js";
export { ReelAnimation } from "./reel_animation.js";
export { PaytableModal } from "./paytable_modal.js";
16 changes: 16 additions & 0 deletions assets/js/hooks/paytable_modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const PaytableModal = {
mounted() {
// For every payline group in the modal, cycle through its items every second.
let groups = this.el.querySelectorAll(".payline-group");
groups.forEach((group) => {
let items = group.querySelectorAll(".payline-item");
if (items.length <= 1) return;
let idx = 0;
setInterval(() => {
items[idx].classList.add("hidden");
idx = (idx + 1) % items.length;
items[idx].classList.remove("hidden");
}, 1000);
});
}
};
100 changes: 100 additions & 0 deletions assets/js/hooks/reel_animation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const rotationSpeed = 100
const extraTime = 1000
const iconSize = 79

export const ReelAnimation = {
mounted() {
this.positions = Array(3).fill(0)
this.absolutePositions = Array(3).fill(0)
this.rotations = Array(3).fill(0)
this.isRolling = false

this.handleEvent("roll_reels", ({ multiplier, target }) => {
if (this.isRolling) {
console.warn("Roll already in progress, skipping")
return
}

this.isRolling = true
const reelsList = document.querySelectorAll(".slots-container > .reel-slot")
const promises = []

for (let i = 0; i < reelsList.length; i++) {
if (!reelsList[i]) {
console.error(`Reel ${i} not found`)
continue
}
promises.push(this.roll(reelsList[i], i, target[i]))
}

Promise.all(promises)
.then(() => {
this.pushEvent("roll_complete", { positions: this.positions })
})
.catch(err => {
console.error("Roll failed:", err)
this.pushEvent("roll_error", { error: err.message })
})
.finally(() => {
this.isRolling = false
})
})
},

roll(reel, reelIndex, target) {
return new Promise((resolve, reject) => {
try {
const style = window.getComputedStyle(reel)
const backgroundImage = style.backgroundImage
if (!backgroundImage) {
throw new Error(`No background image for reel ${reelIndex}`)
}

const numImages = backgroundImage.split(',').length
const minSpins = reelIndex + 2
const currentPos = this.absolutePositions[reelIndex]
const currentIcon = Math.floor((currentPos / iconSize) % numImages)

const reversedTarget = numImages - target
let distance = reversedTarget - currentIcon
if (distance <= 0) {
distance += numImages
}

const spinsInPixels = minSpins * numImages * iconSize
const targetPixels = distance * iconSize
const delta = spinsInPixels + targetPixels
const newPosition = currentPos + delta

// Clear previous transition
reel.style.transition = 'none'
reel.offsetHeight // Force reflow

setTimeout(() => {
const duration = (8 + delta/iconSize) * rotationSpeed
const transitions = Array(numImages).fill(`background-position-y ${duration}ms cubic-bezier(.41,-0.01,.63,1.09)`)
const positions = Array.from({length: numImages}, (_, index) => {
const initialOffset = index * iconSize
return `${newPosition + initialOffset}px`
})

reel.style.transition = transitions.join(', ')
reel.style.backgroundPositionY = positions.join(', ')
}, reelIndex * 150)


setTimeout(() => {
// Clear transition after animation
reel.style.transition = 'none'
this.absolutePositions[reelIndex] = newPosition
// Also adjust the final position calculation
this.positions[reelIndex] = numImages - Math.floor((newPosition / iconSize) % numImages)
this.rotations[reelIndex]++
resolve()
}, (8 + delta/iconSize) * rotationSpeed + reelIndex * 150 + extraTime)
} catch (err) {
reject(err)
}
})
}
}
Loading