Skip to content

Instantly share code, notes, and snippets.

@lardratboy
Last active December 22, 2025 22:44
Show Gist options
  • Select an option

  • Save lardratboy/c6ece962c07c333cf4a0d6dced9352ad to your computer and use it in GitHub Desktop.

Select an option

Save lardratboy/c6ece962c07c333cf4a0d6dced9352ad to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>🎰 LATTICE SLOTS</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Exo+2:wght@300;600&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
/* Custom scrollbar styling */
#left-panel::-webkit-scrollbar,
#right-panel::-webkit-scrollbar {
width: 8px;
}
#left-panel::-webkit-scrollbar-track,
#right-panel::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
#left-panel::-webkit-scrollbar-thumb {
background: rgba(255, 215, 0, 0.4);
border-radius: 4px;
}
#left-panel::-webkit-scrollbar-thumb:hover {
background: rgba(255, 215, 0, 0.6);
}
#right-panel::-webkit-scrollbar-thumb {
background: rgba(0, 212, 255, 0.4);
border-radius: 4px;
}
#right-panel::-webkit-scrollbar-thumb:hover {
background: rgba(0, 212, 255, 0.6);
}
body {
margin: 0;
overflow: hidden;
background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 50%, #0a0a0f 100%);
font-family: 'Exo 2', sans-serif;
touch-action: none;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
color: #fff;
}
/* Left Panel */
#left-panel {
position: absolute;
left: 15px;
top: 15px;
width: 220px;
background: rgba(10, 10, 20, 0.95);
border: 2px solid #ffd700;
border-radius: 16px;
padding: 15px;
z-index: 100;
box-shadow: 0 0 30px rgba(255, 215, 0, 0.3), inset 0 0 20px rgba(0,0,0,0.5);
max-height: calc(100vh - 30px);
overflow-y: auto;
}
#game-title {
font-family: 'Orbitron', sans-serif;
font-size: 1.4rem;
font-weight: 900;
text-align: center;
margin-bottom: 12px;
background: linear-gradient(180deg, #ffd700 0%, #ff8c00 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.5));
}
#credits-display {
background: linear-gradient(135deg, #1a1a2e 0%, #0d0d1a 100%);
border: 2px solid #444;
border-radius: 10px;
padding: 10px;
text-align: center;
margin-bottom: 12px;
}
#credits-label {
font-size: 0.75rem;
color: #888;
text-transform: uppercase;
letter-spacing: 2px;
}
#credits-value {
font-family: 'Orbitron', sans-serif;
font-size: 2rem;
font-weight: 900;
color: #00ff88;
text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
}
.control-row {
margin-bottom: 10px;
}
.control-label {
font-size: 0.75rem;
color: #aaa;
margin-bottom: 4px;
}
#bet-select {
width: 100%;
padding: 10px;
background: rgba(30, 30, 50, 0.9);
border: 1px solid #555;
border-radius: 8px;
color: #fff;
font-family: 'Exo 2', sans-serif;
font-size: 0.9rem;
cursor: pointer;
}
#bet-select:hover {
border-color: #ffd700;
}
#spin-all-btn {
width: 100%;
padding: 14px;
font-family: 'Orbitron', sans-serif;
font-size: 1.1rem;
font-weight: 700;
background: linear-gradient(180deg, #ffd700 0%, #ff8c00 100%);
border: none;
border-radius: 10px;
color: #000;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 10px;
text-shadow: none;
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.4);
}
#spin-all-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 25px rgba(255, 215, 0, 0.6);
}
#spin-all-btn:disabled {
background: linear-gradient(180deg, #666 0%, #444 100%);
cursor: not-allowed;
box-shadow: none;
}
.toggle-btn {
width: 100%;
padding: 10px;
background: rgba(40, 40, 60, 0.9);
border: 1px solid #555;
border-radius: 8px;
color: #fff;
font-family: 'Exo 2', sans-serif;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 6px;
}
.toggle-btn:hover {
background: rgba(60, 60, 90, 0.9);
border-color: #888;
}
.toggle-btn.active {
border-color: #00ff88;
color: #00ff88;
}
/* Column Spin Section */
#column-spin-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #333;
}
#column-spin-section h3 {
font-size: 0.8rem;
color: #888;
margin-bottom: 8px;
}
.column-group {
margin-bottom: 8px;
}
.column-group-label {
font-size: 0.7rem;
color: #666;
margin-bottom: 4px;
}
.column-buttons {
display: flex;
gap: 4px;
}
.col-btn {
flex: 1;
padding: 8px 0;
background: rgba(40, 40, 60, 0.9);
border: 1px solid #444;
border-radius: 5px;
color: #aaa;
font-family: 'Orbitron', sans-serif;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.col-btn:hover:not(:disabled) {
background: rgba(60, 60, 100, 0.9);
border-color: #ffd700;
color: #ffd700;
}
.col-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.col-btn.spinning {
background: rgba(255, 100, 0, 0.3);
border-color: #ff6600;
color: #ff6600;
}
/* Info Box */
#info-box {
background: rgba(0, 100, 50, 0.2);
border: 1px solid #00aa55;
border-radius: 8px;
padding: 10px;
margin-top: 10px;
text-align: center;
}
#info-box.win {
background: rgba(0, 200, 100, 0.3);
border-color: #00ff88;
}
#match-rule {
font-size: 0.85rem;
color: #00ff88;
font-weight: 600;
}
#status-text {
font-size: 0.75rem;
color: #888;
margin-top: 4px;
}
/* Controls Info */
#controls-info {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #333;
}
#controls-info h4 {
font-size: 0.8rem;
color: #00d4ff;
margin-bottom: 6px;
}
.control-hint {
font-size: 0.7rem;
color: #666;
margin-bottom: 2px;
}
.control-hint code {
background: rgba(255,255,255,0.1);
padding: 1px 4px;
border-radius: 3px;
color: #aaa;
font-size: 0.65rem;
}
/* Right Panel - Paytable */
#right-panel {
position: absolute;
right: 15px;
top: 15px;
width: 220px;
background: rgba(10, 10, 20, 0.95);
border: 2px solid #00d4ff;
border-radius: 16px;
padding: 15px;
z-index: 100;
box-shadow: 0 0 30px rgba(0, 212, 255, 0.2);
max-height: calc(100vh - 30px);
overflow-y: auto;
}
#paytable-title {
font-family: 'Orbitron', sans-serif;
font-size: 1.1rem;
text-align: center;
color: #00d4ff;
margin-bottom: 12px;
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
}
.rarity-section {
margin-bottom: 12px;
}
.rarity-label {
font-size: 0.7rem;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 5px;
padding-left: 5px;
}
.rarity-label.common { color: #888; }
.rarity-label.uncommon { color: #00d4ff; }
.rarity-label.rare { color: #ffd700; }
.paytable-row {
display: flex;
align-items: center;
padding: 6px 8px;
background: rgba(30, 30, 50, 0.5);
border-radius: 6px;
margin-bottom: 4px;
}
.paytable-symbol {
width: 28px;
height: 28px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Orbitron', sans-serif;
font-weight: bold;
font-size: 0.9rem;
color: #fff;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
margin-right: 10px;
}
.paytable-letter {
flex: 1;
font-family: 'Orbitron', sans-serif;
font-size: 0.95rem;
}
.paytable-payout {
font-size: 0.75rem;
color: #00ff88;
text-align: right;
}
/* Debug View */
#debug-panel {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #333;
display: none;
}
#debug-panel.visible {
display: block;
}
#debug-title {
font-family: 'Orbitron', sans-serif;
font-size: 0.9rem;
color: #ff6600;
margin-bottom: 10px;
text-align: center;
}
.debug-face {
margin-bottom: 12px;
}
.debug-face-title {
font-size: 0.7rem;
color: #ff8800;
text-align: center;
margin-bottom: 3px;
}
.debug-face-subtitle {
font-size: 0.6rem;
color: #666;
text-align: center;
margin-bottom: 5px;
}
.debug-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 2px;
}
.debug-cell {
aspect-ratio: 1;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Orbitron', sans-serif;
font-size: 0.65rem;
font-weight: bold;
color: #fff;
text-shadow: 1px 1px 1px rgba(0,0,0,0.5);
position: relative;
transition: transform 0.2s, box-shadow 0.2s;
}
/* Win Overlay */
#win-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: none;
justify-content: center;
align-items: center;
z-index: 200;
pointer-events: none;
}
#win-overlay.visible {
display: flex;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
#win-message {
font-family: 'Orbitron', sans-serif;
font-size: 3rem;
font-weight: 900;
text-align: center;
animation: winPulse 0.5s ease infinite alternate;
}
@keyframes winPulse {
from { transform: scale(1); filter: drop-shadow(0 0 20px #ffd700); }
to { transform: scale(1.1); filter: drop-shadow(0 0 40px #ffd700); }
}
@keyframes jackpotPulse {
0% { transform: scale(1); filter: drop-shadow(0 0 30px #ff00ff) hue-rotate(0deg); }
50% { transform: scale(1.15); filter: drop-shadow(0 0 50px #00ffff) hue-rotate(180deg); }
100% { transform: scale(1); filter: drop-shadow(0 0 30px #ff00ff) hue-rotate(360deg); }
}
#win-message.jackpot {
animation: jackpotPulse 0.5s ease infinite;
}
.win-credits {
color: #00ff88;
}
.win-text {
background: linear-gradient(180deg, #ffd700 0%, #ff8c00 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Floating Text */
#float-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 150;
}
.float-text {
position: absolute;
font-family: 'Orbitron', sans-serif;
font-weight: 900;
font-size: 2rem;
white-space: nowrap;
animation: floatUp 2s ease-out forwards;
opacity: 0;
}
@keyframes floatUp {
0% { opacity: 0; transform: translateY(0) scale(0.5); }
15% { opacity: 1; transform: translateY(-20px) scale(1.1); }
30% { transform: translateY(-40px) scale(1); }
100% { opacity: 0; transform: translateY(-150px) scale(0.8); }
}
.float-text.match { color: #00ff88; text-shadow: 0 0 20px #00ff88; }
.float-text.big-win { color: #ffd700; text-shadow: 0 0 30px #ffd700; }
.float-text.jackpot {
color: #ff00ff;
text-shadow: 0 0 30px #ff00ff, 0 0 60px #00ffff;
font-size: 3rem;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
<!-- Left Panel -->
<div id="left-panel">
<div id="game-title">🎰 LATTICE SLOTS</div>
<div id="credits-display">
<div id="credits-label">Credits</div>
<div id="credits-value">1000</div>
</div>
<div class="control-row">
<div class="control-label">Bet per Spin:</div>
<select id="bet-select">
<option value="5">5 Credits</option>
<option value="10">10 Credits</option>
<option value="25" selected>25 Credits</option>
<option value="50">50 Credits</option>
<option value="100">100 Credits</option>
</select>
</div>
<button id="spin-all-btn">🎲 SPIN ALL</button>
<button id="sound-toggle" class="toggle-btn active">πŸ”Š Sound: ON</button>
<button id="debug-toggle" class="toggle-btn">πŸ”§ Debug: OFF</button>
<div id="column-spin-section">
<h3>Spin Columns (0-9):</h3>
<div class="column-group">
<div class="column-group-label">Left + Floor</div>
<div class="column-buttons">
<button class="col-btn" data-col="0">0</button>
<button class="col-btn" data-col="1">1</button>
<button class="col-btn" data-col="2">2</button>
<button class="col-btn" data-col="3">3</button>
<button class="col-btn" data-col="4">4</button>
</div>
</div>
<div class="column-group">
<div class="column-group-label">Right + Floor</div>
<div class="column-buttons">
<button class="col-btn" data-col="5">5</button>
<button class="col-btn" data-col="6">6</button>
<button class="col-btn" data-col="7">7</button>
<button class="col-btn" data-col="8">8</button>
<button class="col-btn" data-col="9">9</button>
</div>
</div>
<div style="font-size: 0.65rem; color: #666; margin-top: 6px;">
<div>0-4: 5 cubes | 5-9: 10 cubes</div>
<div style="color: #ff6600;">House edge enabled</div>
</div>
</div>
<div id="info-box">
<div id="match-rule">Match 4+ to win!</div>
<div id="status-text">Ready to spin!</div>
</div>
<div id="controls-info">
<h4>⌨️ Controls</h4>
<div class="control-hint"><code>Space</code> Spin All</div>
<div class="control-hint"><code>0-9</code> Spin Column</div>
<div class="control-hint"><code>M</code> Sound</div>
<div class="control-hint"><code>Drag</code> Rotate</div>
<div class="control-hint"><code>Scroll</code> Zoom</div>
</div>
</div>
<!-- Right Panel - Paytable & Debug -->
<div id="right-panel">
<div id="paytable-title">πŸ’° PAYTABLE</div>
<div class="rarity-section">
<div class="rarity-label common">● COMMON</div>
<div class="paytable-row">
<div class="paytable-symbol" style="background: #ff4444;">A</div>
<div class="paytable-letter">A</div>
<div class="paytable-payout">β€” +1/ea</div>
</div>
<div class="paytable-row">
<div class="paytable-symbol" style="background: #44cc44;">B</div>
<div class="paytable-letter">B</div>
<div class="paytable-payout">β€” +1/ea</div>
</div>
<div class="paytable-row">
<div class="paytable-symbol" style="background: #00aaff;">C</div>
<div class="paytable-letter">C</div>
<div class="paytable-payout">β€” +2/ea</div>
</div>
</div>
<div class="rarity-section">
<div class="rarity-label uncommon">β—† UNCOMMON</div>
<div class="paytable-row">
<div class="paytable-symbol" style="background: #ffdd00;">D</div>
<div class="paytable-letter">D</div>
<div class="paytable-payout">1Γ—bet +3/ea</div>
</div>
<div class="paytable-row">
<div class="paytable-symbol" style="background: #dd44dd;">E</div>
<div class="paytable-letter">E</div>
<div class="paytable-payout">1Γ—bet +4/ea</div>
</div>
<div class="paytable-row">
<div class="paytable-symbol" style="background: #44dddd;">F</div>
<div class="paytable-letter">F</div>
<div class="paytable-payout">1Γ—bet +5/ea</div>
</div>
</div>
<div class="rarity-section">
<div class="rarity-label rare">β˜… RARE</div>
<div class="paytable-row">
<div class="paytable-symbol" style="background: #ff8800;">G</div>
<div class="paytable-letter">G</div>
<div class="paytable-payout">2Γ—bet +8/ea</div>
</div>
<div class="paytable-row">
<div class="paytable-symbol" style="background: #888888;">H</div>
<div class="paytable-letter">H</div>
<div class="paytable-payout">3Γ—bet +10/ea</div>
</div>
</div>
<div id="debug-panel">
<div id="debug-title">πŸ”§ DEBUG VIEW</div>
<div class="debug-face">
<div class="debug-face-title">XY Face (Left Wall)</div>
<div class="debug-face-subtitle">Columns 0-4</div>
<div class="debug-grid" id="debug-left"></div>
</div>
<div class="debug-face">
<div class="debug-face-title">YZ Face (Right Wall)</div>
<div class="debug-face-subtitle">Columns 5-9</div>
<div class="debug-grid" id="debug-right"></div>
</div>
<div class="debug-face">
<div class="debug-face-title">XZ Face (Floor)</div>
<div class="debug-face-subtitle">Shared by both</div>
<div class="debug-grid" id="debug-floor"></div>
</div>
</div>
</div>
<!-- Win Overlay -->
<div id="win-overlay">
<div id="win-message"></div>
</div>
<!-- Float Container -->
<div id="float-container"></div>
<script>
// ============================================
// SOUND MANAGER
// ============================================
class SlotSoundManager {
constructor() {
this.audioContext = null;
this.masterGain = null;
this.enabled = true;
this.initAudio();
}
initAudio() {
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.audioContext.createGain();
this.masterGain.gain.value = 0.3;
this.masterGain.connect(this.audioContext.destination);
} catch (e) {
console.warn('Web Audio API not supported');
this.enabled = false;
}
}
resume() {
if (this.audioContext?.state === 'suspended') this.audioContext.resume();
}
setEnabled(v) { this.enabled = v; }
isEnabled() { return this.enabled; }
playTone(freq, dur, wave = 'square', vol = 0.1) {
if (!this.enabled || !this.audioContext || this.audioContext.state === 'suspended') return;
const osc = this.audioContext.createOscillator();
const gain = this.audioContext.createGain();
const now = this.audioContext.currentTime;
osc.type = wave;
osc.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(vol, now + 0.01);
gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(now + dur);
}
playSweep(startFreq, endFreq, dur, wave = 'sawtooth', vol = 0.1) {
if (!this.enabled || !this.audioContext || this.audioContext.state === 'suspended') return;
const osc = this.audioContext.createOscillator();
const gain = this.audioContext.createGain();
const now = this.audioContext.currentTime;
osc.type = wave;
osc.frequency.setValueAtTime(startFreq, now);
osc.frequency.exponentialRampToValueAtTime(endFreq, now + dur);
gain.gain.setValueAtTime(vol, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + dur);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(now + dur);
}
playChord(freqs, dur, wave = 'square', vol = 0.06) {
freqs.forEach((f, i) => setTimeout(() => this.playTone(f, dur, wave, vol), i * 20));
}
click() {
this.playTone(400, 0.08, 'sine', 0.1);
}
spinStart() {
this.playTone(200, 0.15, 'sawtooth', 0.08);
setTimeout(() => this.playTone(300, 0.1, 'sawtooth', 0.06), 50);
}
spinTick(pitch = 0) {
this.playTone(150 + pitch * 20, 0.05, 'square', 0.03);
}
spinStop(col) {
const freq = 300 + col * 30;
this.playTone(freq, 0.15, 'triangle', 0.1);
}
// Small win (4 matches - our minimum)
winSmall() {
const notes = [523, 659, 784]; // C E G
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.2, 'square', 0.1), i * 100);
});
}
// Medium win (5-6 matches)
winMedium() {
const notes = [523, 659, 784, 1047]; // C E G C
notes.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, 0.25, 'triangle', 0.12), i * 120);
});
// Add sparkle
setTimeout(() => this.playSweep(2000, 4000, 0.3, 'sine', 0.05), 400);
}
// Big win (7+ matches)
winBig() {
// Fanfare!
const fanfare = [523, 523, 523, 659, 784, 659, 784, 1047];
const durations = [0.1, 0.1, 0.2, 0.2, 0.15, 0.15, 0.2, 0.5];
let time = 0;
fanfare.forEach((freq, i) => {
setTimeout(() => this.playTone(freq, durations[i] + 0.1, 'square', 0.1), time);
time += durations[i] * 1000;
});
// Celebration sweep
setTimeout(() => this.playSweep(500, 2000, 0.5, 'sawtooth', 0.08), time);
}
// Jackpot (huge win - 10+ matches or high value)
jackpot() {
// Epic ascending arpeggio
const notes = [262, 330, 392, 523, 659, 784, 1047, 1319, 1568];
notes.forEach((freq, i) => {
setTimeout(() => {
this.playTone(freq, 0.4, 'square', 0.08);
this.playTone(freq * 1.5, 0.4, 'triangle', 0.05);
}, i * 100);
});
// Big finish
setTimeout(() => {
this.playChord([1047, 1319, 1568, 2093], 1.0, 'square', 0.06);
this.playSweep(500, 3000, 0.8, 'sawtooth', 0.1);
}, 900);
}
noWin() {
this.playTone(200, 0.2, 'sawtooth', 0.06);
setTimeout(() => this.playTone(150, 0.3, 'sawtooth', 0.05), 100);
}
}
const soundManager = new SlotSoundManager();
// ============================================
// THREE.JS SETUP
// ============================================
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(14, 12, 14);
camera.lookAt(2.5, 2, 2.5);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x000000, 0);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// ============================================
// LIGHTING SETUP WITH SHADOWS
// ============================================
// Reduced ambient light to make shadows more visible
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
// Main directional light (from camera direction - top-right-front)
const mainLight = new THREE.DirectionalLight(0xffffff, 0.7);
mainLight.position.set(15, 20, 15);
mainLight.castShadow = true;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
mainLight.shadow.camera.near = 1;
mainLight.shadow.camera.far = 50;
mainLight.shadow.camera.left = -15;
mainLight.shadow.camera.right = 15;
mainLight.shadow.camera.top = 15;
mainLight.shadow.camera.bottom = -15;
mainLight.shadow.bias = -0.001;
mainLight.shadow.radius = 2;
scene.add(mainLight);
// Fill light from opposite side (softer)
const fillLight = new THREE.DirectionalLight(0x4488ff, 0.3);
fillLight.position.set(-10, 5, -10);
scene.add(fillLight);
// Rim light from below for depth
const rimLight = new THREE.DirectionalLight(0xff88ff, 0.15);
rimLight.position.set(0, -10, 0);
scene.add(rimLight);
// ============================================
// Starfield
function createStarfield() {
const geometry = new THREE.BufferGeometry();
const vertices = [];
const colors = [];
for (let i = 0; i < 2000; i++) {
const r = 50 + Math.random() * 30;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
vertices.push(
r * Math.sin(phi) * Math.cos(theta),
r * Math.sin(phi) * Math.sin(theta),
r * Math.cos(phi)
);
const brightness = 0.3 + Math.random() * 0.7;
colors.push(brightness, brightness, brightness * (0.8 + Math.random() * 0.2));
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.8,
vertexColors: true,
transparent: true,
opacity: 0.8
});
return new THREE.Points(geometry, material);
}
const starfield = createStarfield();
scene.add(starfield);
// ============================================
// GAME STATE
// ============================================
const GRID_SIZE = 5;
const SPACING = 1.1;
const OFFSET = 0.5;
let credits = 1000;
let currentBet = 25;
let isSpinning = false;
let spinningColumns = new Set();
let debugMode = false;
// Attract mode state
let attractMode = false;
let attractInterval = null;
let lastMatches = [];
let currentAttractIndex = 0;
// Symbol configuration
const SYMBOLS = {
A: { color: 0xff4444, letter: 'A', rarity: 'common', baseValue: 1, betMult: 0, weight: 20 },
B: { color: 0x44cc44, letter: 'B', rarity: 'common', baseValue: 1, betMult: 0, weight: 20 },
C: { color: 0x0088ff, letter: 'C', rarity: 'common', baseValue: 2, betMult: 0, weight: 18 },
D: { color: 0xffdd00, letter: 'D', rarity: 'uncommon', baseValue: 3, betMult: 1, weight: 10 },
E: { color: 0xdd44dd, letter: 'E', rarity: 'uncommon', baseValue: 4, betMult: 1, weight: 10 },
F: { color: 0x44dddd, letter: 'F', rarity: 'uncommon', baseValue: 5, betMult: 1, weight: 8 },
G: { color: 0xff8800, letter: 'G', rarity: 'rare', baseValue: 8, betMult: 2, weight: 4 },
H: { color: 0x888888, letter: 'H', rarity: 'rare', baseValue: 10, betMult: 3, weight: 2 }
};
const SYMBOL_KEYS = Object.keys(SYMBOLS);
const MIN_MATCH = 4;
// Weighted random symbol selection
function getRandomSymbol() {
const totalWeight = SYMBOL_KEYS.reduce((sum, k) => sum + SYMBOLS[k].weight, 0);
let random = Math.random() * totalWeight;
for (const key of SYMBOL_KEYS) {
random -= SYMBOLS[key].weight;
if (random <= 0) return key;
}
return SYMBOL_KEYS[0];
}
// ============================================
// CUBE DATA STRUCTURE
// ============================================
class SlotCube {
constructor(gridX, gridY, gridZ, plane, symbol) {
this.gridX = gridX;
this.gridY = gridY;
this.gridZ = gridZ;
this.plane = plane;
this.symbol = symbol;
this.position = this.calculatePosition();
this.targetPosition = this.position.clone();
this.rotation = 0;
this.targetRotation = 0;
this.scale = 1; // Target scale
this.displayScale = 1; // Current display scale (lerps toward scale)
this.isSpinning = false;
this.spinPhase = 0;
}
calculatePosition() {
const x = this.gridX * SPACING + OFFSET;
const y = this.gridY * SPACING + OFFSET;
const z = this.gridZ * SPACING + OFFSET;
if (this.plane === 'left') return new THREE.Vector3(-0.6, y, z);
if (this.plane === 'right') return new THREE.Vector3(x, y, -0.6);
return new THREE.Vector3(x, -0.6, z);
}
updatePosition() {
this.targetPosition = this.calculatePosition();
}
}
let cubes = [];
// Columns structure:
// Columns 0-4: Left wall (each column is a vertical strip at different Z positions)
// Columns 5-9: Right wall (each column is a vertical strip at different X positions)
// Floor cubes are included in both sets where they intersect
function getColumnCubes(colIndex) {
if (colIndex < 5) {
// Left wall column (Z = colIndex) + floor strip (X = 0, Z varies)
const wallCubes = cubes.filter(c => c.plane === 'left' && c.gridZ === colIndex);
const floorCubes = cubes.filter(c => c.plane === 'floor' && c.gridZ === colIndex);
return [...wallCubes, ...floorCubes];
} else {
// Right wall column (X = colIndex - 5) + floor strip (Z = 0, X varies)
const adjustedCol = colIndex - 5;
const wallCubes = cubes.filter(c => c.plane === 'right' && c.gridX === adjustedCol);
const floorCubes = cubes.filter(c => c.plane === 'floor' && c.gridX === adjustedCol);
return [...wallCubes, ...floorCubes];
}
}
function initBoard() {
cubes = [];
// Left wall (XY plane at X = -0.6)
for (let y = 0; y < GRID_SIZE; y++) {
for (let z = 0; z < GRID_SIZE; z++) {
cubes.push(new SlotCube(0, y, z, 'left', getRandomSymbol()));
}
}
// Right wall (YZ plane at Z = -0.6)
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
cubes.push(new SlotCube(x, y, 0, 'right', getRandomSymbol()));
}
}
// Floor (XZ plane at Y = -0.6)
for (let x = 0; x < GRID_SIZE; x++) {
for (let z = 0; z < GRID_SIZE; z++) {
cubes.push(new SlotCube(x, 0, z, 'floor', getRandomSymbol()));
}
}
}
initBoard();
// ============================================
// INSTANCED RENDERER
// ============================================
class CubeRenderer {
constructor(scene) {
this.scene = scene;
this.meshes = {};
this.geometry = new THREE.BoxGeometry(1, 1, 1);
this.dummy = new THREE.Object3D();
this.maxInstances = 100;
this.createMeshes();
}
createTexture(symbol, color) {
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 128;
const ctx = canvas.getContext('2d');
// Background
ctx.fillStyle = '#' + color.toString(16).padStart(6, '0');
ctx.fillRect(0, 0, 128, 128);
// Border
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 12;
ctx.strokeRect(6, 6, 116, 116);
// Letter
ctx.font = 'bold 72px Orbitron, Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.strokeStyle = '#000';
ctx.lineWidth = 5;
ctx.strokeText(symbol, 64, 68);
ctx.fillStyle = '#fff';
ctx.fillText(symbol, 64, 68);
return new THREE.CanvasTexture(canvas);
}
createMeshes() {
for (const [key, data] of Object.entries(SYMBOLS)) {
const texture = this.createTexture(data.letter, data.color);
const material = new THREE.MeshPhongMaterial({ map: texture, shininess: 80 });
const mesh = new THREE.InstancedMesh(this.geometry, material, this.maxInstances);
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
mesh.castShadow = true;
mesh.receiveShadow = true;
this.scene.add(mesh);
this.meshes[key] = mesh;
}
}
render(cubes, time) {
// Reset counts
Object.values(this.meshes).forEach(mesh => mesh.count = 0);
cubes.forEach(cube => {
const mesh = this.meshes[cube.symbol];
if (!mesh) return;
const idx = mesh.count;
// Lerp position
cube.position.lerp(cube.targetPosition, 0.15);
// Lerp scale smoothly toward target
cube.displayScale += (cube.scale - cube.displayScale) * 0.12;
// Spin animation
if (cube.isSpinning) {
cube.spinPhase += 0.3;
cube.rotation = cube.spinPhase;
} else {
// Snap to nearest 90 degrees
const target = Math.round(cube.rotation / (Math.PI / 2)) * (Math.PI / 2);
cube.rotation += (target - cube.rotation) * 0.2;
}
// Attract mode highlight - pulsing scale
let scale = cube.displayScale;
if (cube.attractHighlight) {
const pulse = 1.1 + 0.15 * Math.sin(time * 0.006);
scale = pulse;
}
this.dummy.position.copy(cube.position);
this.dummy.rotation.set(cube.isSpinning ? cube.spinPhase * 0.5 : 0, cube.rotation, 0);
this.dummy.scale.setScalar(scale);
this.dummy.updateMatrix();
mesh.setMatrixAt(idx, this.dummy.matrix);
mesh.count++;
});
Object.values(this.meshes).forEach(mesh => {
mesh.instanceMatrix.needsUpdate = true;
});
}
}
const cubeRenderer = new CubeRenderer(scene);
// ============================================
// MATCH DETECTION
// ============================================
function areNeighbors(c1, c2) {
const p1 = c1.plane, p2 = c2.plane;
// Same plane
if (p1 === p2) {
if (p1 === 'left') {
return Math.abs(c1.gridY - c2.gridY) + Math.abs(c1.gridZ - c2.gridZ) === 1;
}
if (p1 === 'right') {
return Math.abs(c1.gridX - c2.gridX) + Math.abs(c1.gridY - c2.gridY) === 1;
}
// Floor
return Math.abs(c1.gridX - c2.gridX) + Math.abs(c1.gridZ - c2.gridZ) === 1;
}
// Cross-face adjacency
if ((p1 === 'left' && p2 === 'floor') || (p1 === 'floor' && p2 === 'left')) {
const L = p1 === 'left' ? c1 : c2;
const F = p1 === 'floor' ? c1 : c2;
return L.gridY === 0 && F.gridX === 0 && L.gridZ === F.gridZ;
}
if ((p1 === 'right' && p2 === 'floor') || (p1 === 'floor' && p2 === 'right')) {
const R = p1 === 'right' ? c1 : c2;
const F = p1 === 'floor' ? c1 : c2;
return R.gridY === 0 && F.gridZ === 0 && R.gridX === F.gridX;
}
if ((p1 === 'left' && p2 === 'right') || (p1 === 'right' && p2 === 'left')) {
const L = p1 === 'left' ? c1 : c2;
const R = p1 === 'right' ? c1 : c2;
return L.gridZ === 0 && R.gridX === 0 && L.gridY === R.gridY;
}
return false;
}
function findMatches() {
const visited = new Set();
const groups = [];
cubes.forEach(start => {
if (visited.has(start)) return;
const group = [start];
const queue = [start];
visited.add(start);
while (queue.length > 0) {
const current = queue.shift();
cubes.forEach(neighbor => {
if (!visited.has(neighbor) &&
neighbor.symbol === current.symbol &&
areNeighbors(current, neighbor)) {
visited.add(neighbor);
group.push(neighbor);
queue.push(neighbor);
}
});
}
if (group.length >= MIN_MATCH) {
groups.push(group);
}
});
return groups;
}
function calculateWinnings(matches) {
let totalWin = 0;
matches.forEach(group => {
const symbol = SYMBOLS[group[0].symbol];
const count = group.length;
// Base value per cube
const baseWin = count * symbol.baseValue;
// Bet multiplier bonus
const betBonus = symbol.betMult * currentBet;
totalWin += baseWin + betBonus;
});
return totalWin;
}
// ============================================
// ATTRACT MODE
// ============================================
function startAttractMode(matches) {
if (matches.length === 0) return;
lastMatches = matches;
currentAttractIndex = 0;
attractMode = true;
// Reset all cube scales first
cubes.forEach(cube => cube.scale = 1);
// Start cycling through matches
attractCycle();
attractInterval = setInterval(attractCycle, 1500);
}
function stopAttractMode() {
if (!attractMode) return;
attractMode = false;
if (attractInterval) {
clearInterval(attractInterval);
attractInterval = null;
}
// Reset all cube scales and glows
cubes.forEach(cube => {
cube.scale = 1;
cube.attractHighlight = false;
});
lastMatches = [];
currentAttractIndex = 0;
}
function attractCycle() {
if (!attractMode || lastMatches.length === 0) return;
// Reset previous highlights
cubes.forEach(cube => {
cube.scale = 1;
cube.attractHighlight = false;
});
// Highlight current match group
const currentGroup = lastMatches[currentAttractIndex];
if (currentGroup) {
currentGroup.forEach(cube => {
cube.attractHighlight = true;
});
}
// Move to next group
currentAttractIndex = (currentAttractIndex + 1) % lastMatches.length;
}
// ============================================
// SPIN MECHANICS
// ============================================
async function spinColumn(colIndex, duration = 1500) {
if (spinningColumns.has(colIndex)) return;
const columnCubes = getColumnCubes(colIndex);
if (columnCubes.length === 0) return;
spinningColumns.add(colIndex);
// Mark cubes as spinning
columnCubes.forEach(cube => {
cube.isSpinning = true;
cube.spinPhase = 0;
});
soundManager.spinStart();
// Update column button
const btn = document.querySelector(`.col-btn[data-col="${colIndex}"]`);
if (btn) btn.classList.add('spinning');
// Spin duration with randomness
const spinTime = duration + Math.random() * 500;
const startTime = performance.now();
// Symbol cycling during spin - faster at start, slowing down near end
const symbolCycleInterval = setInterval(() => {
const elapsed = performance.now() - startTime;
const progress = elapsed / spinTime;
// Slow down symbol changes as we approach the end
const cycleChance = progress < 0.7 ? 1 : (1 - progress) * 3;
columnCubes.forEach(cube => {
if (cube.isSpinning && Math.random() < cycleChance) {
cube.symbol = getRandomSymbol();
}
});
}, 50); // Cycle symbols every 50ms
// Tick sounds during spin
const tickInterval = setInterval(() => {
soundManager.spinTick(colIndex);
}, 100);
await new Promise(resolve => setTimeout(resolve, spinTime));
clearInterval(tickInterval);
clearInterval(symbolCycleInterval);
// Final symbol assignment
columnCubes.forEach(cube => {
cube.symbol = getRandomSymbol();
cube.isSpinning = false;
});
soundManager.spinStop(colIndex);
// Update button
if (btn) btn.classList.remove('spinning');
spinningColumns.delete(colIndex);
}
async function spinAll() {
if (isSpinning || credits < currentBet) return;
// Stop attract mode
stopAttractMode();
isSpinning = true;
soundManager.resume();
// Deduct bet
credits -= currentBet;
updateCreditsDisplay();
updateStatus('Spinning...');
document.getElementById('spin-all-btn').disabled = true;
// Spin all columns with staggered timing
const spinPromises = [];
for (let i = 0; i < 10; i++) {
spinPromises.push(
new Promise(resolve => {
setTimeout(() => {
spinColumn(i, 1000 + i * 200).then(resolve);
}, i * 100);
})
);
}
await Promise.all(spinPromises);
// Small delay before checking matches
await new Promise(resolve => setTimeout(resolve, 300));
// Check for wins
const matches = findMatches();
const winnings = calculateWinnings(matches);
if (winnings > 0) {
credits += winnings;
updateCreditsDisplay();
// Collect all matching cubes into a Set for quick lookup
const matchingCubes = new Set();
matches.forEach(group => {
group.forEach(cube => matchingCubes.add(cube));
});
// Reveal effect: shrink non-matching cubes, grow matching ones
cubes.forEach(cube => {
if (matchingCubes.has(cube)) {
cube.scale = 1.1; // Emphasize matches
} else {
cube.scale = 0.7; // Shrink non-matches
}
});
const totalMatched = matches.reduce((sum, g) => sum + g.length, 0);
const multiplier = winnings / currentBet;
// Tiered win sounds based on payout multiplier
if (multiplier >= 6) {
// Jackpot! 6x+ bet - requires large rare matches
soundManager.jackpot();
showWinOverlay(`🎰 JACKPOT! +${winnings}`, true);
showFloatingText(`JACKPOT +${winnings}`, 'jackpot');
} else if (multiplier >= 3) {
// Big win: 3x-6x bet - rare symbol matches
soundManager.winBig();
showWinOverlay(`BIG WIN! +${winnings}`);
showFloatingText(`+${winnings}`, 'big-win');
} else if (multiplier >= 1) {
// Medium win: 1x-3x bet - broke even or profit
soundManager.winMedium();
showFloatingText(`+${winnings}`, 'match');
} else {
// Small win: < 1x bet - common symbols
soundManager.winSmall();
showFloatingText(`+${winnings}`, 'match');
}
updateStatus(`Won ${winnings} credits! (${multiplier.toFixed(1)}x)`);
document.getElementById('info-box').classList.add('win');
setTimeout(() => document.getElementById('info-box').classList.remove('win'), 2000);
// Restore non-matching cubes after reveal, then start attract mode
setTimeout(() => {
cubes.forEach(cube => {
if (!matchingCubes.has(cube)) {
cube.scale = 1; // Restore non-matches
}
});
// Normalize matching cubes too
matchingCubes.forEach(cube => cube.scale = 1);
// Start attract mode
if (!isSpinning) {
startAttractMode(matches);
}
}, 2000);
} else {
soundManager.noWin();
updateStatus('No matches. Try again!');
}
document.getElementById('spin-all-btn').disabled = false;
isSpinning = false;
}
async function spinSingleColumn(colIndex) {
if (spinningColumns.has(colIndex)) return;
// Stop attract mode
stopAttractMode();
const singleBet = Math.ceil(currentBet / 10);
if (credits < singleBet) {
updateStatus('Not enough credits!');
return;
}
soundManager.resume();
credits -= singleBet;
updateCreditsDisplay();
await spinColumn(colIndex, 800);
// Check matches after single column spin
await new Promise(resolve => setTimeout(resolve, 200));
const matches = findMatches();
const winnings = calculateWinnings(matches);
if (winnings > 0) {
credits += winnings;
updateCreditsDisplay();
// Collect all matching cubes into a Set for quick lookup
const matchingCubes = new Set();
matches.forEach(group => {
group.forEach(cube => matchingCubes.add(cube));
});
// Reveal effect: shrink non-matching cubes, grow matching ones
cubes.forEach(cube => {
if (matchingCubes.has(cube)) {
cube.scale = 1.1; // Emphasize matches
} else {
cube.scale = 0.7; // Shrink non-matches
}
});
const totalMatched = matches.reduce((sum, g) => sum + g.length, 0);
const multiplier = winnings / currentBet;
// Tiered win sounds based on multiplier
if (multiplier >= 6) {
soundManager.jackpot();
showFloatingText(`JACKPOT +${winnings}`, 'jackpot');
} else if (multiplier >= 3) {
soundManager.winBig();
showFloatingText(`+${winnings}`, 'big-win');
} else if (multiplier >= 1) {
soundManager.winMedium();
showFloatingText(`+${winnings}`, 'match');
} else {
soundManager.winSmall();
showFloatingText(`+${winnings}`, 'match');
}
updateStatus(`Won ${winnings} credits! (${multiplier.toFixed(1)}x)`);
// Restore non-matching cubes after reveal, then start attract mode
setTimeout(() => {
cubes.forEach(cube => {
if (!matchingCubes.has(cube)) {
cube.scale = 1; // Restore non-matches
}
});
// Normalize matching cubes too
matchingCubes.forEach(cube => cube.scale = 1);
// Start attract mode
if (!isSpinning && spinningColumns.size === 0) {
startAttractMode(matches);
}
}, 1500);
} else {
updateStatus(`Column ${colIndex} spun`);
}
}
// ============================================
// UI UPDATES
// ============================================
function updateCreditsDisplay() {
document.getElementById('credits-value').textContent = credits.toLocaleString();
}
function updateStatus(text) {
document.getElementById('status-text').textContent = text;
}
function showWinOverlay(text, isJackpot = false) {
const overlay = document.getElementById('win-overlay');
const message = document.getElementById('win-message');
message.innerHTML = `<span class="win-text">${text}</span>`;
message.classList.toggle('jackpot', isJackpot);
overlay.classList.add('visible');
const duration = isJackpot ? 3500 : 2500;
setTimeout(() => {
overlay.classList.remove('visible');
message.classList.remove('jackpot');
}, duration);
}
function showFloatingText(text, type) {
const container = document.getElementById('float-container');
const el = document.createElement('div');
el.className = `float-text ${type}`;
el.textContent = text;
el.style.left = '50%';
el.style.top = '50%';
el.style.transform = 'translate(-50%, -50%)';
container.appendChild(el);
setTimeout(() => el.remove(), 2000);
}
function updateDebugView() {
if (!debugMode) return;
const symbolColors = {};
for (const [key, data] of Object.entries(SYMBOLS)) {
symbolColors[key] = '#' + data.color.toString(16).padStart(6, '0');
}
// Left wall (Y rows, Z columns - displayed top to bottom)
const leftGrid = document.getElementById('debug-left');
leftGrid.innerHTML = '';
for (let y = GRID_SIZE - 1; y >= 0; y--) {
for (let z = 0; z < GRID_SIZE; z++) {
const cube = cubes.find(c => c.plane === 'left' && c.gridY === y && c.gridZ === z);
const cell = document.createElement('div');
cell.className = 'debug-cell';
if (cube) {
cell.style.background = symbolColors[cube.symbol];
cell.textContent = cube.symbol;
if (cube.attractHighlight) {
cell.style.boxShadow = '0 0 8px 2px #fff';
cell.style.transform = 'scale(1.1)';
cell.style.zIndex = '1';
}
}
leftGrid.appendChild(cell);
}
}
// Right wall (Y rows, X columns - displayed top to bottom)
const rightGrid = document.getElementById('debug-right');
rightGrid.innerHTML = '';
for (let y = GRID_SIZE - 1; y >= 0; y--) {
for (let x = 0; x < GRID_SIZE; x++) {
const cube = cubes.find(c => c.plane === 'right' && c.gridY === y && c.gridX === x);
const cell = document.createElement('div');
cell.className = 'debug-cell';
if (cube) {
cell.style.background = symbolColors[cube.symbol];
cell.textContent = cube.symbol;
if (cube.attractHighlight) {
cell.style.boxShadow = '0 0 8px 2px #fff';
cell.style.transform = 'scale(1.1)';
cell.style.zIndex = '1';
}
}
rightGrid.appendChild(cell);
}
}
// Floor (Z rows, X columns)
const floorGrid = document.getElementById('debug-floor');
floorGrid.innerHTML = '';
for (let z = 0; z < GRID_SIZE; z++) {
for (let x = 0; x < GRID_SIZE; x++) {
const cube = cubes.find(c => c.plane === 'floor' && c.gridX === x && c.gridZ === z);
const cell = document.createElement('div');
cell.className = 'debug-cell';
if (cube) {
cell.style.background = symbolColors[cube.symbol];
cell.textContent = cube.symbol;
if (cube.attractHighlight) {
cell.style.boxShadow = '0 0 8px 2px #fff';
cell.style.transform = 'scale(1.1)';
cell.style.zIndex = '1';
}
}
floorGrid.appendChild(cell);
}
}
}
// ============================================
// EVENT LISTENERS
// ============================================
document.getElementById('spin-all-btn').addEventListener('click', () => {
spinAll();
});
document.getElementById('bet-select').addEventListener('change', (e) => {
currentBet = parseInt(e.target.value);
stopAttractMode();
soundManager.click();
});
document.getElementById('sound-toggle').addEventListener('click', () => {
const enabled = !soundManager.isEnabled();
soundManager.setEnabled(enabled);
const btn = document.getElementById('sound-toggle');
btn.textContent = enabled ? 'πŸ”Š Sound: ON' : 'πŸ”‡ Sound: OFF';
btn.classList.toggle('active', enabled);
if (enabled) soundManager.click();
});
document.getElementById('debug-toggle').addEventListener('click', () => {
debugMode = !debugMode;
const btn = document.getElementById('debug-toggle');
btn.textContent = debugMode ? 'πŸ”§ Debug: ON' : 'πŸ”§ Debug: OFF';
btn.classList.toggle('active', debugMode);
document.getElementById('debug-panel').classList.toggle('visible', debugMode);
soundManager.click();
if (debugMode) updateDebugView();
});
// Column buttons
document.querySelectorAll('.col-btn').forEach(btn => {
btn.addEventListener('click', () => {
const col = parseInt(btn.dataset.col);
spinSingleColumn(col);
});
});
// Keyboard controls
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
spinAll();
} else if (e.key >= '0' && e.key <= '9') {
spinSingleColumn(parseInt(e.key));
} else if (e.key.toLowerCase() === 'm') {
document.getElementById('sound-toggle').click();
}
});
// ============================================
// ANIMATION LOOP
// ============================================
let lastDebugUpdate = 0;
function animate(time) {
requestAnimationFrame(animate);
// Rotate starfield slowly
starfield.rotation.y += 0.0002;
starfield.rotation.x += 0.0001;
// Render cubes with time for attract mode pulse
cubeRenderer.render(cubes, time || 0);
// Update debug view continuously (throttled to ~20fps for performance)
const now = performance.now();
if (debugMode && now - lastDebugUpdate > 50) {
updateDebugView();
lastDebugUpdate = now;
}
renderer.render(scene, camera);
}
// Handle resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Initialize
updateDebugView();
animate();
console.log('🎰 Lattice Slots loaded! Press Space to spin or use the buttons.');
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment