Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Rhythm Chef: Beat Bites Deluxe</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<style> | |
@keyframes moveDown { | |
0% { transform: translateY(-100px); } | |
100% { transform: translateY(calc(100vh - 200px)); } | |
} | |
.cue { | |
animation: moveDown var(--duration) linear forwards; | |
transition: all 0.1s ease; | |
} | |
.hit-effect { | |
animation: scaleFade 0.5s ease-out forwards; | |
} | |
@keyframes scaleFade { | |
0% { transform: scale(0.8); opacity: 1; } | |
100% { transform: scale(1.5); opacity: 0; } | |
} | |
.feedback-text { | |
animation: fadeUp 1s ease-out forwards; | |
} | |
@keyframes fadeUp { | |
0% { transform: translateY(0) scale(1); opacity: 1; } | |
100% { transform: translateY(-50px) scale(1.2); opacity: 0; } | |
} | |
.combo-text { | |
animation: pulse 0.5s ease infinite alternate; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); } | |
100% { transform: scale(1.1); } | |
} | |
#gameArea { | |
touch-action: manipulation; | |
background: radial-gradient(circle at center, #1a202c 0%, #111827 100%); | |
overflow: hidden; | |
} | |
.kitchen-counter { | |
background: linear-gradient(to bottom, #8B5A2B 0%, #A67C52 100%); | |
box-shadow: 0 -10px 20px rgba(0,0,0,0.3); | |
} | |
.particle { | |
position: absolute; | |
pointer-events: none; | |
will-change: transform, opacity; | |
} | |
.glow { | |
filter: drop-shadow(0 0 8px currentColor); | |
} | |
.score-glow { | |
text-shadow: 0 0 10px rgba(255, 215, 0, 0.7); | |
} | |
.perfect-hit { | |
animation: perfectGlow 0.5s ease-out forwards; | |
} | |
@keyframes perfectGlow { | |
0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); } | |
50% { box-shadow: 0 0 30px 15px rgba(255, 255, 255, 0.7); } | |
100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); } | |
} | |
/* New effects */ | |
.ripple { | |
position: absolute; | |
border-radius: 50%; | |
background: rgba(255, 255, 255, 0.3); | |
transform: scale(0); | |
pointer-events: none; | |
animation: ripple 1s linear; | |
} | |
@keyframes ripple { | |
to { | |
transform: scale(4); | |
opacity: 0; | |
} | |
} | |
.floating-bg-element { | |
position: absolute; | |
opacity: 0.1; | |
animation: floatBg 30s linear infinite; | |
z-index: 0; | |
} | |
@keyframes floatBg { | |
0% { transform: translate(0, 0) rotate(0deg); } | |
25% { transform: translate(50px, 50px) rotate(5deg); } | |
50% { transform: translate(100px, 0) rotate(0deg); } | |
75% { transform: translate(50px, -50px) rotate(-5deg); } | |
100% { transform: translate(0, 0) rotate(0deg); } | |
} | |
.combo-explosion { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
pointer-events: none; | |
z-index: 5; | |
} | |
.streak-light { | |
position: absolute; | |
height: 2px; | |
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent); | |
transform-origin: left center; | |
pointer-events: none; | |
z-index: 4; | |
} | |
.ingredient-trail { | |
position: absolute; | |
width: 10px; | |
height: 10px; | |
border-radius: 50%; | |
pointer-events: none; | |
z-index: 3; | |
} | |
.energy-wave { | |
position: absolute; | |
width: 300px; | |
height: 300px; | |
border-radius: 50%; | |
background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, transparent 70%); | |
transform: scale(0); | |
opacity: 1; | |
pointer-events: none; | |
animation: energyWave 1.5s ease-out forwards; | |
} | |
@keyframes energyWave { | |
0% { transform: scale(0); opacity: 1; } | |
100% { transform: scale(3); opacity: 0; } | |
} | |
.floating-sparkle { | |
position: absolute; | |
width: 6px; | |
height: 6px; | |
border-radius: 50%; | |
background-color: white; | |
pointer-events: none; | |
animation: sparkleFloat 2s ease-out forwards; | |
} | |
@keyframes sparkleFloat { | |
0% { transform: translate(0, 0) scale(1); opacity: 1; } | |
100% { transform: translate(random(100) - 50px, -100px) scale(0); opacity: 0; } | |
} | |
.screen-flash { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: white; | |
opacity: 0; | |
pointer-events: none; | |
z-index: 10; | |
} | |
.combo-streak { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); | |
pointer-events: none; | |
z-index: 2; | |
} | |
.kitchen-light { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
background: radial-gradient(circle at 50% 30%, rgba(255,215,0,0.1) 0%, transparent 70%); | |
pointer-events: none; | |
z-index: 1; | |
animation: kitchenLightPulse 2s infinite alternate; | |
} | |
@keyframes kitchenLightPulse { | |
0% { opacity: 0.3; } | |
100% { opacity: 0.7; } | |
} | |
.perfect-ring { | |
position: absolute; | |
border-radius: 50%; | |
border: 3px solid rgba(255,255,255,0.8); | |
transform: scale(0); | |
pointer-events: none; | |
animation: perfectRing 0.8s ease-out forwards; | |
} | |
@keyframes perfectRing { | |
0% { transform: scale(0); opacity: 1; } | |
100% { transform: scale(3); opacity: 0; } | |
} | |
/* Settings panel */ | |
.settings-panel { | |
transition: all 0.3s ease; | |
transform: translateY(100%); | |
} | |
.settings-panel.open { | |
transform: translateY(0); | |
} | |
.settings-overlay { | |
background-color: rgba(0, 0, 0, 0.7); | |
} | |
/* Range slider styling */ | |
input[type="range"] { | |
-webkit-appearance: none; | |
height: 8px; | |
background: #4a5568; | |
border-radius: 4px; | |
} | |
input[type="range"]::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
appearance: none; | |
width: 20px; | |
height: 20px; | |
border-radius: 50%; | |
background: #f6ad55; | |
cursor: pointer; | |
} | |
input[type="range"]::-moz-range-thumb { | |
width: 20px; | |
height: 20px; | |
border-radius: 50%; | |
background: #f6ad55; | |
cursor: pointer; | |
} | |
/* Preset buttons */ | |
.preset-btn { | |
transition: all 0.2s ease; | |
} | |
.preset-btn:hover { | |
transform: translateY(-2px); | |
} | |
.preset-btn.active { | |
background-color: #f6ad55; | |
color: #1a202c; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
} | |
</style> | |
</head> | |
<body class="bg-gray-900 text-white font-sans overflow-hidden"> | |
<!-- Start Screen --> | |
<div id="startScreen" class="fixed inset-0 flex flex-col items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800 z-10 transition-opacity duration-500"> | |
<div id="startScreenBg" class="absolute inset-0 overflow-hidden"> | |
<!-- Floating background elements --> | |
<div class="floating-bg-element text-6xl" style="top:20%; left:10%;">🍳</div> | |
<div class="floating-bg-element text-6xl" style="top:30%; left:70%;">🥘</div> | |
<div class="floating-bg-element text-6xl" style="top:60%; left:20%;">🍕</div> | |
<div class="floating-bg-element text-6xl" style="top:70%; left:80%;">🍣</div> | |
</div> | |
<div class="text-center mb-12 relative z-10"> | |
<h1 class="text-7xl font-bold mb-4 text-yellow-400 glow">🍳 RHYTHM CHEF</h1> | |
<h2 class="text-4xl mb-6 text-yellow-300 glow">Beat Bites Deluxe</h2> | |
<div class="text-xl text-gray-300 max-w-md mx-auto"> | |
Tap the ingredients in rhythm to cook up perfect dishes! | |
</div> | |
</div> | |
<button id="startButton" class="bg-gradient-to-r from-yellow-500 to-yellow-600 hover:from-yellow-400 hover:to-yellow-500 text-4xl text-gray-900 font-bold py-5 px-16 rounded-full transition-all transform hover:scale-105 shadow-lg glow relative z-10"> | |
START COOKING | |
</button> | |
<button id="settingsButton" class="absolute top-4 right-4 bg-gray-800 hover:bg-gray-700 text-yellow-400 text-2xl p-3 rounded-full transition-all transform hover:scale-110 shadow-lg"> | |
⚙️ | |
</button> | |
<div class="absolute bottom-8 text-gray-400 text-sm z-10"> | |
Tap the beat when ingredients reach the counter | |
</div> | |
</div> | |
<!-- Settings Panel --> | |
<div id="settingsPanel" class="fixed inset-0 z-20 flex items-end settings-overlay hidden"> | |
<div class="settings-panel bg-gray-800 w-full rounded-t-3xl p-6 max-h-[80vh] overflow-y-auto"> | |
<div class="flex justify-between items-center mb-6"> | |
<h3 class="text-3xl font-bold text-yellow-400">Game Settings</h3> | |
<button id="closeSettings" class="text-2xl p-2 rounded-full hover:bg-gray-700"> | |
✕ | |
</button> | |
</div> | |
<div class="space-y-8"> | |
<!-- Presets Section --> | |
<div> | |
<h4 class="text-xl font-semibold mb-4 text-gray-300">Presets</h4> | |
<div class="grid grid-cols-3 gap-3"> | |
<button data-preset="easy" class="preset-btn bg-gray-700 hover:bg-gray-600 py-3 rounded-lg"> | |
Easy | |
</button> | |
<button data-preset="medium" class="preset-btn bg-gray-700 hover:bg-gray-600 py-3 rounded-lg active"> | |
Medium | |
</button> | |
<button data-preset="hard" class="preset-btn bg-gray-700 hover:bg-gray-600 py-3 rounded-lg"> | |
Hard | |
</button> | |
</div> | |
</div> | |
<!-- Speed Settings --> | |
<div> | |
<div class="flex justify-between items-center mb-2"> | |
<label for="speedRange" class="text-lg font-medium">Speed: <span id="speedValue">2.0</span>s</label> | |
</div> | |
<input type="range" id="speedRange" min="0.5" max="4" step="0.1" value="2" class="w-full"> | |
</div> | |
<!-- BPM Settings --> | |
<div> | |
<div class="flex justify-between items-center mb-2"> | |
<label for="bpmRange" class="text-lg font-medium">Tempo: <span id="bpmValue">128</span> BPM</label> | |
</div> | |
<input type="range" id="bpmRange" min="60" max="200" step="1" value="128" class="w-full"> | |
</div> | |
<!-- Perfect Window --> | |
<div> | |
<div class="flex justify-between items-center mb-2"> | |
<label for="perfectRange" class="text-lg font-medium">Perfect Window: <span id="perfectValue">80</span>ms</label> | |
</div> | |
<input type="range" id="perfectRange" min="20" max="150" step="5" value="80" class="w-full"> | |
</div> | |
<!-- Good Window --> | |
<div> | |
<div class="flex justify-between items-center mb-2"> | |
<label for="goodRange" class="text-lg font-medium">Good Window: <span id="goodValue">160</span>ms</label> | |
</div> | |
<input type="range" id="goodRange" min="50" max="300" step="5" value="160" class="w-full"> | |
</div> | |
<!-- Number of Ingredients --> | |
<div> | |
<div class="flex justify-between items-center mb-2"> | |
<label for="ingredientsRange" class="text-lg font-medium">Ingredients: <span id="ingredientsValue">10</span></label> | |
</div> | |
<input type="range" id="ingredientsRange" min="4" max="20" step="1" value="10" class="w-full"> | |
</div> | |
<!-- Sequence Length --> | |
<div> | |
<div class="flex justify-between items-center mb-2"> | |
<label for="sequenceRange" class="text-lg font-medium">Sequence Length: <span id="sequenceValue">16</span> beats</label> | |
</div> | |
<input type="range" id="sequenceRange" min="8" max="32" step="4" value="16" class="w-full"> | |
</div> | |
<!-- Save/Load Section --> | |
<div class="pt-4 border-t border-gray-700"> | |
<h4 class="text-xl font-semibold mb-4 text-gray-300">Save/Load</h4> | |
<div class="grid grid-cols-2 gap-4"> | |
<div> | |
<label for="presetName" class="block text-sm font-medium mb-1">Preset Name</label> | |
<input type="text" id="presetName" placeholder="e.g. Level1" class="w-full bg-gray-700 rounded px-3 py-2 text-white"> | |
</div> | |
<div class="flex items-end"> | |
<button id="savePreset" class="bg-yellow-600 hover:bg-yellow-500 text-white px-4 py-2 rounded w-full"> | |
Save | |
</button> | |
</div> | |
</div> | |
<div class="mt-4"> | |
<label for="loadPreset" class="block text-sm font-medium mb-1">Load Preset</label> | |
<select id="loadPreset" class="w-full bg-gray-700 rounded px-3 py-2 text-white"> | |
<option value="">Select a preset</option> | |
</select> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Game Screen --> | |
<div id="gameScreen" class="fixed inset-0 hidden flex flex-col"> | |
<!-- Kitchen ambient light --> | |
<div class="kitchen-light"></div> | |
<!-- Score Display --> | |
<div class="flex justify-between px-8 pt-6 relative z-10"> | |
<div class="text-4xl font-bold score-glow"> | |
🏆 <span id="score">0</span> | |
</div> | |
<div id="comboContainer" class="text-4xl font-bold combo-text hidden"> | |
🔥 <span id="combo">0</span>x | |
</div> | |
<button id="gameSettingsButton" class="bg-gray-800 hover:bg-gray-700 text-yellow-400 text-xl p-2 rounded-full transition-all transform hover:scale-110 shadow-lg"> | |
⚙️ | |
</button> | |
</div> | |
<!-- Game Area --> | |
<div id="gameArea" class="flex-1 relative overflow-hidden"> | |
<!-- Floating particles background --> | |
<div id="particles"></div> | |
<!-- Combo streak effect --> | |
<div id="comboStreak" class="combo-streak hidden"></div> | |
<!-- Target Zone (Kitchen Counter) --> | |
<div id="targetZone" class="kitchen-counter absolute left-0 right-0 h-24 rounded-t-3xl flex items-center justify-center" style="bottom: 20%;"> | |
<div class="w-full h-1 bg-yellow-300 bg-opacity-50 rounded-full mx-8"></div> | |
</div> | |
<!-- Feedback Text --> | |
<div id="feedbackText" class="absolute left-1/2 transform -translate-x-1/2 text-6xl font-bold text-center opacity-0" style="bottom: 30%;"></div> | |
<!-- Combo explosion container --> | |
<div id="comboExplosion" class="combo-explosion"></div> | |
<!-- Energy waves container --> | |
<div id="energyWaves"></div> | |
</div> | |
</div> | |
<!-- End Screen --> | |
<div id="endScreen" class="fixed inset-0 hidden flex flex-col items-center justify-center bg-gray-900 bg-opacity-95 z-20"> | |
<div class="text-center relative"> | |
<h2 class="text-6xl font-bold mb-8 text-yellow-400 glow">DISH COMPLETE! 🎉</h2> | |
<div class="text-5xl mb-8">Final Score: <span id="finalScore" class="text-yellow-300">0</span></div> | |
<div id="finalDish" class="text-9xl mb-12">🍲</div> | |
<div class="text-2xl mb-8">Max Combo: <span id="maxCombo" class="text-yellow-300">0</span>x</div> | |
<button id="restartButton" class="bg-gradient-to-r from-yellow-500 to-yellow-600 hover:from-yellow-400 hover:to-yellow-500 text-4xl text-gray-900 font-bold py-5 px-16 rounded-full transition-all transform hover:scale-105 shadow-lg glow"> | |
COOK AGAIN | |
</button> | |
</div> | |
</div> | |
<!-- Screen flash effect --> | |
<div id="screenFlash" class="screen-flash"></div> | |
<script> | |
// Game state | |
const gameState = { | |
score: 0, | |
combo: 0, | |
maxCombo: 0, | |
isPlaying: false, | |
bpm: 128, | |
cueSpeed: 2, // seconds to reach target | |
perfectWindow: 80, // ms | |
goodWindow: 160, // ms, | |
ingredients: ['🍅', '🧀', '🍄', '🥩', '🥬', '🍞', '🥚', '🦐', '🌽', '🧅'], | |
dishes: ['🍕', '🍔', '🍣', '🥘', '🍛', '🍜', '🌮', '🥗', '🍲', '🍝'], | |
sequence: [ | |
{ beat: 1, type: 'tap' }, | |
{ beat: 2, type: 'tap' }, | |
{ beat: 3, type: 'tap' }, | |
{ beat: 4, type: 'tap' }, | |
{ beat: 5.5, type: 'tap' }, | |
{ beat: 6.5, type: 'tap' }, | |
{ beat: 7, type: 'tap' }, | |
{ beat: 8, type: 'tap' }, | |
{ beat: 9, type: 'tap' }, | |
{ beat: 10, type: 'tap' }, | |
{ beat: 11.5, type: 'tap' }, | |
{ beat: 12.5, type: 'tap' }, | |
{ beat: 13, type: 'tap' }, | |
{ beat: 14, type: 'tap' }, | |
{ beat: 15, type: 'tap' }, | |
{ beat: 16, type: 'tap' } | |
], | |
activeCues: [], | |
sequenceStartTime: 0, | |
currentSequenceIndex: 0, | |
audioContext: null, | |
metronomeInterval: null, | |
backgroundParticles: [], | |
lastTapPosition: { x: 0, y: 0 } | |
}; | |
// DOM elements | |
const startScreen = document.getElementById('startScreen'); | |
const gameScreen = document.getElementById('gameScreen'); | |
const endScreen = document.getElementById('endScreen'); | |
const startButton = document.getElementById('startButton'); | |
const restartButton = document.getElementById('restartButton'); | |
const gameArea = document.getElementById('gameArea'); | |
const targetZone = document.getElementById('targetZone'); | |
const feedbackText = document.getElementById('feedbackText'); | |
const scoreDisplay = document.getElementById('score'); | |
const comboDisplay = document.getElementById('combo'); | |
const comboContainer = document.getElementById('comboContainer'); | |
const finalScoreDisplay = document.getElementById('finalScore'); | |
const maxComboDisplay = document.getElementById('maxCombo'); | |
const finalDishDisplay = document.getElementById('finalDish'); | |
const particlesContainer = document.getElementById('particles'); | |
const comboExplosion = document.getElementById('comboExplosion'); | |
const comboStreak = document.getElementById('comboStreak'); | |
const energyWaves = document.getElementById('energyWaves'); | |
const screenFlash = document.getElementById('screenFlash'); | |
const startScreenBg = document.getElementById('startScreenBg'); | |
const settingsButton = document.getElementById('settingsButton'); | |
const gameSettingsButton = document.getElementById('gameSettingsButton'); | |
const settingsPanel = document.getElementById('settingsPanel'); | |
const closeSettings = document.getElementById('closeSettings'); | |
// Settings elements | |
const speedRange = document.getElementById('speedRange'); | |
const speedValue = document.getElementById('speedValue'); | |
const bpmRange = document.getElementById('bpmRange'); | |
const bpmValue = document.getElementById('bpmValue'); | |
const perfectRange = document.getElementById('perfectRange'); | |
const perfectValue = document.getElementById('perfectValue'); | |
const goodRange = document.getElementById('goodRange'); | |
const goodValue = document.getElementById('goodValue'); | |
const ingredientsRange = document.getElementById('ingredientsRange'); | |
const ingredientsValue = document.getElementById('ingredientsValue'); | |
const sequenceRange = document.getElementById('sequenceRange'); | |
const sequenceValue = document.getElementById('sequenceValue'); | |
const presetName = document.getElementById('presetName'); | |
const savePreset = document.getElementById('savePreset'); | |
const loadPreset = document.getElementById('loadPreset'); | |
const presetButtons = document.querySelectorAll('[data-preset]'); | |
// Initialize audio context | |
function initAudio() { | |
try { | |
gameState.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
} catch (e) { | |
console.warn('Web Audio API not supported'); | |
} | |
} | |
// Play a sound | |
function playSound(frequency, duration, type = 'sine') { | |
if (!gameState.audioContext) return; | |
const oscillator = gameState.audioContext.createOscillator(); | |
const gainNode = gameState.audioContext.createGain(); | |
oscillator.connect(gainNode); | |
gainNode.connect(gameState.audioContext.destination); | |
oscillator.type = type; | |
oscillator.frequency.value = frequency; | |
gainNode.gain.value = 0.1; | |
oscillator.start(); | |
oscillator.stop(gameState.audioContext.currentTime + duration); | |
} | |
// Create background particles | |
function createParticles() { | |
// Clear existing particles | |
particlesContainer.innerHTML = ''; | |
gameState.backgroundParticles = []; | |
// Create new particles | |
for (let i = 0; i < 50; i++) { | |
createParticle(true); | |
} | |
} | |
function createParticle(isBackground = false) { | |
const particle = document.createElement('div'); | |
particle.className = 'particle'; | |
// Random properties | |
const size = Math.random() * 10 + 2; | |
const x = Math.random() * 100; | |
const y = Math.random() * 100; | |
const duration = Math.random() * 20 + 10; | |
const delay = Math.random() * 5; | |
const opacity = Math.random() * 0.5 + 0.1; | |
const color = `hsl(${Math.random() * 60 + 20}, 70%, 60%)`; | |
const shape = Math.random() > 0.7 ? 'text' : 'circle'; | |
if (shape === 'text') { | |
const emojis = ['✨', '🌟', '⭐', '⚡', '💫', '🔥', '🍳', '🥘', '🍲']; | |
particle.textContent = emojis[Math.floor(Math.random() * emojis.length)]; | |
particle.style.fontSize = `${size}px`; | |
particle.style.width = 'auto'; | |
particle.style.height = 'auto'; | |
} else { | |
particle.style.width = `${size}px`; | |
particle.style.height = `${size}px`; | |
particle.style.backgroundColor = color; | |
particle.style.borderRadius = '50%'; | |
} | |
particle.style.left = `${x}%`; | |
particle.style.top = `${y}%`; | |
particle.style.opacity = opacity; | |
particle.style.animation = `float ${duration}s linear ${delay}s infinite`; | |
if (isBackground) { | |
particle.style.position = 'absolute'; | |
particlesContainer.appendChild(particle); | |
gameState.backgroundParticles.push(particle); | |
} | |
return particle; | |
} | |
// Create floating animation | |
const style = document.createElement('style'); | |
style.innerHTML = ` | |
@keyframes float { | |
0% { transform: translate(0, 0) rotate(0deg); opacity: ${Math.random() * 0.3 + 0.1}; } | |
50% { transform: translate(${Math.random() * 100 - 50}px, ${Math.random() * 50 - 25}px) rotate(${Math.random() * 180 - 90}deg); opacity: ${Math.random() * 0.5 + 0.3}; } | |
100% { transform: translate(0, 0) rotate(0deg); opacity: ${Math.random() * 0.3 + 0.1}; } | |
} | |
`; | |
document.head.appendChild(style); | |
// Start metronome | |
function startMetronome() { | |
if (!gameState.audioContext) return; | |
const secondsPerBeat = 60 / gameState.bpm; | |
let nextBeatTime = gameState.audioContext.currentTime; | |
gameState.metronomeInterval = setInterval(() => { | |
playSound(880, 0.05, 'triangle'); | |
nextBeatTime += secondsPerBeat; | |
// Visual metronome effect | |
createEnergyWave(targetZone.getBoundingClientRect().left + targetZone.offsetWidth / 2, | |
targetZone.getBoundingClientRect().top + targetZone.offsetHeight / 2); | |
}, secondsPerBeat * 1000); | |
} | |
// Stop metronome | |
function stopMetronome() { | |
if (gameState.metronomeInterval) { | |
clearInterval(gameState.metronomeInterval); | |
gameState.metronomeInterval = null; | |
} | |
} | |
// Create a cue element | |
function createCue() { | |
const cue = document.createElement('div'); | |
const ingredient = gameState.ingredients[Math.floor(Math.random() * gameState.ingredients.length)]; | |
cue.className = 'cue absolute w-20 h-20 rounded-full flex items-center justify-center text-4xl glow'; | |
cue.style.setProperty('--duration', `${gameState.cueSpeed}s`); | |
cue.style.top = '-100px'; | |
cue.style.left = `${Math.random() * 60 + 20}%`; | |
cue.textContent = ingredient; | |
// Add trail effect | |
createIngredientTrail(cue); | |
// Add to DOM and active cues array | |
gameArea.appendChild(cue); | |
const cueObj = { | |
element: cue, | |
targetTime: Date.now() + gameState.cueSpeed * 1000, | |
hit: false, | |
ingredient: ingredient | |
}; | |
gameState.activeCues.push(cueObj); | |
// Remove cue after animation completes | |
setTimeout(() => { | |
if (!cueObj.hit) { | |
handleMiss(cueObj); | |
} | |
}, gameState.cueSpeed * 1000); | |
return cueObj; | |
} | |
// Create ingredient trail effect | |
function createIngredientTrail(cue) { | |
const trailInterval = setInterval(() => { | |
if (!cue.parentElement) { | |
clearInterval(trailInterval); | |
return; | |
} | |
const rect = cue.getBoundingClientRect(); | |
const trail = document.createElement('div'); | |
trail.className = 'ingredient-trail'; | |
trail.style.left = `${rect.left + rect.width / 2}px`; | |
trail.style.top = `${rect.top + rect.height / 2}px`; | |
trail.style.backgroundColor = `hsl(${Math.random() * 60 + 20}, 80%, 60%)`; | |
trail.style.opacity = Math.random() * 0.6 + 0.2; | |
trail.style.transform = `scale(${Math.random() * 0.5 + 0.5})`; | |
document.body.appendChild(trail); | |
setTimeout(() => { | |
trail.style.transition = 'all 0.5s ease-out'; | |
trail.style.opacity = '0'; | |
trail.style.transform = 'scale(0)'; | |
setTimeout(() => trail.remove(), 500); | |
}, 10); | |
}, 50); | |
// Clean up interval when cue is removed | |
cue.dataset.trailInterval = trailInterval; | |
} | |
// Show feedback | |
function showFeedback(text, isGood) { | |
feedbackText.textContent = text; | |
feedbackText.className = `absolute left-1/2 transform -translate-x-1/2 text-6xl font-bold text-center glow ${ | |
isGood ? 'text-green-400' : 'text-red-400' | |
}`; | |
feedbackText.style.opacity = '1'; | |
// Create hit effect | |
if (isGood) { | |
createHitEffect(feedbackText.getBoundingClientRect()); | |
// Add ripple effect | |
createRippleEffect(gameState.lastTapPosition.x, gameState.lastTapPosition.y); | |
// Add perfect ring for perfect hits | |
if (text.includes('PERFECT')) { | |
createPerfectRing(gameState.lastTapPosition.x, gameState.lastTapPosition.y); | |
} | |
} | |
// Hide feedback after delay | |
setTimeout(() => { | |
feedbackText.style.opacity = '0'; | |
}, 1000); | |
} | |
// Create hit effect | |
function createHitEffect(rect) { | |
const colors = ['#FFD700', '#FF6347', '#7FFFD4', '#FF69B4', '#9370DB']; | |
for (let i = 0; i < 25; i++) { | |
const particle = document.createElement('div'); | |
particle.className = 'particle'; | |
const size = Math.random() * 16 + 4; | |
const color = colors[Math.floor(Math.random() * colors.length)]; | |
const angle = Math.random() * Math.PI * 2; | |
const distance = Math.random() * 100 + 30; | |
const duration = Math.random() * 1 + 0.5; | |
particle.style.width = `${size}px`; | |
particle.style.height = `${size}px`; | |
particle.style.left = `${rect.left + rect.width / 2}px`; | |
particle.style.top = `${rect.top + rect.height / 2}px`; | |
particle.style.backgroundColor = color; | |
particle.style.borderRadius = '50%'; | |
particle.style.opacity = '0.8'; | |
particle.style.transform = `translate(${Math.cos(angle) * distance}px, ${Math.sin(angle) * distance}px)`; | |
particle.style.transition = `all ${duration}s ease-out`; | |
particle.style.boxShadow = `0 0 ${size/2}px ${color}`; | |
document.body.appendChild(article); | |
setTimeout(() => { | |
particle.style.opacity = '0'; | |
particle.style.transform += ` scale(0.5)`; | |
setTimeout(() => particle.remove(), duration * 1000); | |
}, 10); | |
} | |
// Add floating sparkles | |
for (let i = 0; i < 10; i++) { | |
createFloatingSparkle(rect.left + rect.width / 2, rect.top + rect.height / 2); | |
} | |
} | |
// Create ripple effect | |
function createRippleEffect(x, y) { | |
const ripple = document.createElement('div'); | |
ripple.className = 'ripple'; | |
ripple.style.left = `${x}px`; | |
ripple.style.top = `${y}px`; | |
ripple.style.width = '50px'; | |
ripple.style.height = '50px'; | |
ripple.style.backgroundColor = `rgba(255, 255, 255, ${Math.random() * 0.2 + 0.1})`; | |
document.body.appendChild(ripple); | |
setTimeout(() => { | |
ripple.remove(); | |
}, 1000); | |
} | |
// Create perfect ring effect | |
function createPerfectRing(x, y) { | |
const ring = document.createElement('div'); | |
ring.className = 'perfect-ring'; | |
ring.style.left = `${x}px`; | |
ring.style.top = `${y}px`; | |
ring.style.width = '50px'; | |
ring.style.height = '50px'; | |
document.body.appendChild(ring); | |
setTimeout(() => { | |
ring.remove(); | |
}, 800); | |
} | |
// Create floating sparkle | |
function createFloatingSparkle(x, y) { | |
const sparkle = document.createElement('div'); | |
sparkle.className = 'floating-sparkle'; | |
sparkle.style.left = `${x}px`; | |
sparkle.style.top = `${y}px`; | |
sparkle.style.backgroundColor = `hsl(${Math.random() * 60 + 20}, 100%, 80%)`; | |
document.body.appendChild(sparkle); | |
setTimeout(() => { | |
sparkle.remove(); | |
}, 2000); | |
} | |
// Create energy wave | |
function createEnergyWave(x, y) { | |
const wave = document.createElement('div'); | |
wave.className = 'energy-wave'; | |
wave.style.left = `${x - 150}px`; | |
wave.style.top = `${y - 150}px`; | |
energyWaves.appendChild(wave); | |
setTimeout(() => { | |
wave.remove(); | |
}, 1500); | |
} | |
// Create streak light effect | |
function createStreakLight(x, y, angle, length) { | |
const light = document.createElement('div'); | |
light.className = 'streak-light'; | |
light.style.left = `${x}px`; | |
light.style.top = `${y}px`; | |
light.style.width = `${length}px`; | |
light.style.transform = `rotate(${angle}rad)`; | |
document.body.appendChild(light); | |
setTimeout(() => { | |
light.style.opacity = '0'; | |
light.style.transition = 'opacity 0.5s ease-out'; | |
setTimeout(() => light.remove(), 500); | |
}, 10); | |
} | |
// Handle successful hit | |
function handleHit(cue, accuracy) { | |
if (cue.hit) return; | |
cue.hit = true; | |
// Visual feedback on the cue | |
cue.element.classList.add('perfect-hit'); | |
cue.element.style.transform = 'scale(1.5)'; | |
setTimeout(() => { | |
cue.element.remove(); | |
}, 200); | |
// Clear trail interval | |
if (cue.element.dataset.trailInterval) { | |
clearInterval(parseInt(cue.element.dataset.trailInterval)); | |
} | |
// Determine hit quality | |
let points = 0; | |
let feedback = ''; | |
let soundFreq = 0; | |
if (Math.abs(accuracy) <= gameState.perfectWindow) { | |
points = 100; | |
feedback = 'PERFECT! ✨'; | |
soundFreq = 1046.50; // C note | |
playSound(soundFreq, 0.2, 'sine'); | |
playSound(soundFreq/2, 0.3, 'sine'); | |
// Screen flash for perfect hits | |
screenFlash.style.opacity = '0.3'; | |
screenFlash.style.transition = 'opacity 0.3s ease-out'; | |
setTimeout(() => { | |
screenFlash.style.opacity = '0'; | |
}, 300); | |
} else if (Math.abs(accuracy) <= gameState.goodWindow) { | |
points = 60; | |
feedback = 'GREAT! 👍'; | |
soundFreq = 783.99; // G note | |
playSound(soundFreq, 0.15, 'square'); | |
} else { | |
points = 30; | |
feedback = 'GOOD 👌'; | |
soundFreq = 523.25; // C note lower | |
playSound(soundFreq, 0.1, 'sawtooth'); | |
} | |
// Update score and combo | |
gameState.score += points; | |
gameState.combo += 1; | |
if (gameState.combo > gameState.maxCombo) { | |
gameState.maxCombo = gameState.combo; | |
} | |
scoreDisplay.textContent = gameState.score; | |
comboDisplay.textContent = gameState.combo; | |
// Show combo if > 1 | |
if (gameState.combo > 1) { | |
comboContainer.classList.remove('hidden'); | |
// Combo streak effect | |
if (gameState.combo % 5 === 0) { | |
comboStreak.classList.remove('hidden'); | |
setTimeout(() => { | |
comboStreak.classList.add('hidden'); | |
}, 300); | |
// Combo explosion for every 5 hits | |
if (gameState.combo % 10 === 0) { | |
createComboExplosion(); | |
} | |
} | |
// Streak lights for high combos | |
if (gameState.combo > 3) { | |
for (let i = 0; i < 3; i++) { | |
const angle = Math.random() * Math.PI * 2; | |
const length = Math.random() * 200 + 100; | |
createStreakLight(gameState.lastTapPosition.x, gameState.lastTapPosition.y, angle, length); | |
} | |
} | |
} | |
// Show feedback | |
showFeedback(feedback, true); | |
// Create ingredient transformation effect | |
createIngredientEffect(cue); | |
} | |
// Create combo explosion effect | |
function createComboExplosion() { | |
comboExplosion.innerHTML = ''; | |
for (let i = 0; i < 30; i++) { | |
const particle = document.createElement('div'); | |
particle.className = 'particle'; | |
const size = Math.random() * 20 + 10; | |
const color = `hsl(${Math.random() * 60 + 20}, 100%, 70%)`; | |
const angle = Math.random() * Math.PI * 2; | |
const distance = Math.random() * 300 + 100; | |
const duration = Math.random() * 1 + 0.5; | |
particle.style.width = `${size}px`; | |
particle.style.height = `${size}px`; | |
particle.style.left = '50%'; | |
particle.style.top = '50%'; | |
particle.style.backgroundColor = color; | |
particle.style.borderRadius = '50%'; | |
particle.style.opacity = '0.8'; | |
particle.style.transform = `translate(-50%, -50%) translate(${Math.cos(angle) * distance}px, ${Math.sin(angle) * distance}px)`; | |
particle.style.transition = `all ${duration}s ease-out`; | |
particle.style.boxShadow = `0 0 ${size/2}px ${color}`; | |
comboExplosion.appendChild(particle); | |
setTimeout(() => { | |
particle.style.opacity = '0'; | |
particle.style.transform += ` scale(0.5)`; | |
setTimeout(() => particle.remove(), duration * 1000); | |
}, 10); | |
} | |
} | |
// Create ingredient transformation effect | |
function createIngredientEffect(cue) { | |
const rect = cue.element.getBoundingClientRect(); | |
const effect = document.createElement('div'); | |
effect.className = 'absolute text-4xl glow'; | |
effect.style.left = `${rect.left}px`; | |
effect.style.top = `${rect.top}px`; | |
effect.style.width = `${rect.width}px`; | |
effect.style.height = `${rect.height}px`; | |
effect.style.display = 'flex'; | |
effect.style.alignItems = 'center'; | |
effect.style.justifyContent = 'center'; | |
effect.textContent = cue.ingredient; | |
document.body.appendChild(effect); | |
// Animate transformation | |
setTimeout(() => { | |
effect.style.transition = 'all 0.5s ease-out'; | |
effect.style.transform = 'translateY(-50px) scale(2)'; | |
effect.style.opacity = '0'; | |
// Change to a cooked version | |
const cookedItems = { | |
'🍅': '🍅', // tomato stays tomato | |
'🧀': '🧀', // cheese stays cheese | |
'🍄': '🍄', // mushroom stays mushroom | |
'🥩': '🍖', // raw meat -> cooked meat | |
'🥬': '🥗', // lettuce -> salad | |
'🍞': '🍞', // bread stays bread | |
'🥚': '🍳', // egg -> fried egg | |
'🦐': '🍤', // shrimp -> fried shrimp | |
'🌽': '🍿', // corn -> popcorn | |
'🧅': '🧅' // onion stays onion | |
}; | |
setTimeout(() => { | |
effect.textContent = cookedItems[cue.ingredient] || '🍽️'; | |
}, 250); | |
setTimeout(() => { | |
effect.remove(); | |
}, 1000); | |
}, 10); | |
} | |
// Handle miss | |
function handleMiss(cue) { | |
if (cue.hit) return; | |
cue.hit = true; | |
if (cue.element) { | |
cue.element.classList.add('opacity-50'); | |
cue.element.style.transform = 'scale(0.8)'; | |
setTimeout(() => { | |
cue.element.remove(); | |
}, 500); | |
// Clear trail interval | |
if (cue.element.dataset.trailInterval) { | |
clearInterval(parseInt(cue.element.dataset.trailInterval)); | |
} | |
} | |
// Reset combo | |
gameState.combo = 0; | |
comboDisplay.textContent = '0'; | |
comboContainer.classList.add('hidden'); | |
// Show feedback | |
showFeedback('MISS! 😩', false); | |
playSound(110, 0.3, 'sawtooth'); // Low buzz for miss | |
// Create break effect | |
if (cue.element) { | |
const rect = cue.element.getBoundingClientRect(); | |
for (let i = 0; i < 8; i++) { | |
const piece = document.createElement('div'); | |
piece.className = 'particle absolute text-xl'; | |
piece.textContent = cue.ingredient; | |
piece.style.left = `${rect.left + rect.width/2}px`; | |
piece.style.top = `${rect.top + rect.height/2}px`; | |
piece.style.transform = `translate(${(Math.random() - 0.5) * 80}px, ${(Math.random() - 0.5) * 80}px) rotate(${Math.random() * 360}deg)`; | |
piece.style.opacity = '0.7'; | |
piece.style.transition = 'all 0.8s ease-out'; | |
document.body.appendChild(piece); | |
setTimeout(() => { | |
piece.style.opacity = '0'; | |
piece.style.transform += ` translateY(50px)`; | |
setTimeout(() => piece.remove(), 800); | |
}, 10); | |
} | |
} | |
} | |
// Start game sequence | |
function startSequence() { | |
gameState.score = 0; | |
gameState.combo = 0; | |
gameState.maxCombo = 0; | |
gameState.isPlaying = true; | |
gameState.activeCues = []; | |
gameState.currentSequenceIndex = 0; | |
gameState.sequenceStartTime = Date.now(); | |
scoreDisplay.textContent = '0'; | |
comboDisplay.textContent = '0'; | |
comboContainer.classList.add('hidden'); | |
comboStreak.classList.add('hidden'); | |
// Create background particles | |
createParticles(); | |
// Start metronome | |
if (gameState.audioContext) { | |
startMetronome(); | |
} | |
// Start spawning cues | |
spawnNextCue(); | |
} | |
// Spawn next cue in sequence | |
function spawnNextCue() { | |
if (gameState.currentSequenceIndex >= gameState.sequence.length) { | |
endSequence(); | |
return; | |
} | |
const currentBeat = gameState.sequence[gameState.currentSequenceIndex].beat; | |
const beatsPerSecond = gameState.bpm / 60; | |
const beatTime = currentBeat / beatsPerSecond * 1000; | |
// Calculate delay until this cue should spawn | |
const spawnDelay = beatTime - (Date.now() - gameState.sequenceStartTime) - (gameState.cueSpeed * 1000); | |
if (spawnDelay <= 0) { | |
// We're behind schedule, spawn immediately | |
createCue(); | |
gameState.currentSequenceIndex++; | |
spawnNextCue(); | |
} else { | |
// Schedule next cue | |
setTimeout(() => { | |
createCue(); | |
gameState.currentSequenceIndex++; | |
spawnNextCue(); | |
}, spawnDelay); | |
} | |
} | |
// End sequence | |
function endSequence() { | |
gameState.isPlaying = false; | |
stopMetronome(); | |
// Clear background particles | |
particlesContainer.innerHTML = ''; | |
// Determine final dish based on score | |
const dishIndex = Math.min( | |
Math.floor(gameState.score / 500), | |
gameState.dishes.length - 1 | |
); | |
finalDishDisplay.textContent = gameState.dishes[dishIndex]; | |
// Wait for last cues to finish | |
setTimeout(() => { | |
finalScoreDisplay.textContent = gameState.score; | |
maxComboDisplay.textContent = gameState.maxCombo; | |
endScreen.classList.remove('hidden'); | |
// Celebration particles | |
for (let i = 0; i < 100; i++) { | |
setTimeout(() => { | |
const particle = createParticle(); | |
particle.style.position = 'fixed'; | |
particle.style.left = `${Math.random() * 100}%`; | |
particle.style.top = `${Math.random() * 100}%`; | |
particle.style.fontSize = `${Math.random() * 30 + 20}px`; | |
particle.style.opacity = '0.8'; | |
particle.style.animation = `float ${Math.random() * 3 + 2}s ease-out forwards`; | |
const emojis = ['✨', '🌟', '⭐', '⚡', '💫', '🔥', '🎉', '🎊', '🥳']; | |
particle.textContent = emojis[Math.floor(Math.random() * emojis.length)]; | |
endScreen.appendChild(particle); | |
setTimeout(() => { | |
particle.remove(); | |
}, 3000); | |
}, i * 50); | |
} | |
}, gameState.cueSpeed * 1000); | |
} | |
// Handle tap input | |
function handleTap(e) { | |
if (!gameState.isPlaying) return; | |
// Store tap position for effects | |
const rect = gameArea.getBoundingClientRect(); | |
gameState.lastTapPosition = { | |
x: (e.clientX || e.touches[0].clientX) - rect.left, | |
y: (e.clientY || e.touches[0].clientY) - rect.top | |
}; | |
const now = Date.now(); | |
let closestCue = null; | |
let closestDiff = Infinity; | |
// Find the closest active cue to target time | |
for (const cue of gameState.activeCues) { | |
if (!cue.hit) { | |
const diff = now - cue.targetTime; | |
if (Math.abs(diff) < Math.abs(closestDiff)) { | |
closestDiff = diff; | |
closestCue = cue; | |
} | |
} | |
} | |
// Visual feedback on target zone | |
targetZone.classList.add('perfect-hit'); | |
setTimeout(() => { | |
targetZone.classList.remove('perfect-hit'); | |
}, 300); | |
// Create energy wave at tap position | |
createEnergyWave(gameState.lastTapPosition.x, gameState.lastTapPosition.y); | |
// Check if tap was close enough to any cue | |
if (closestCue && Math.abs(closestDiff) <= gameState.goodWindow * 2) { | |
handleHit(closestCue, closestDiff); | |
} else { | |
// Penalize random taps | |
gameState.combo = 0; | |
comboDisplay.textContent = '0'; | |
comboContainer.classList.add('hidden'); | |
showFeedback('TOO EARLY! 👎', false); | |
playSound(110, 0.3, 'square'); | |
} | |
} | |
// Load presets from localStorage | |
function loadPresets() { | |
const presets = JSON.parse(localStorage.getItem('rhythmChefPresets')) || {}; | |
const select = document.getElementById('loadPreset'); | |
// Clear existing options except the first one | |
while (select.options.length > 1) { | |
select.remove(1); | |
} | |
// Add presets to dropdown | |
for (const [name, preset] of Object.entries(presets)) { | |
const option = document.createElement('option'); | |
option.value = name; | |
option.textContent = name; | |
select.appendChild(option); | |
} | |
} | |
// Save preset to localStorage | |
function savePresetToStorage(name) { | |
const presets = JSON.parse(localStorage.getItem('rhythmChefPresets')) || {}; | |
presets[name] = { | |
bpm: gameState.bpm, | |
cueSpeed: gameState.cueSpeed, | |
perfectWindow: gameState.perfectWindow, | |
goodWindow: gameState.goodWindow, | |
ingredients: gameState.ingredients, | |
sequence: gameState.sequence | |
}; | |
localStorage.setItem('rhythmChefPresets', JSON.stringify(presets)); | |
loadPresets(); | |
} | |
// Load preset from localStorage | |
function loadPresetFromStorage(name) { | |
const presets = JSON.parse(localStorage.getItem('rhythmChefPresets')) || {}; | |
const preset = presets[name]; | |
if (preset) { | |
gameState.bpm = preset.bpm; | |
gameState.cueSpeed = preset.cueSpeed; | |
gameState.perfectWindow = preset.perfectWindow; | |
gameState.goodWindow = preset.goodWindow; | |
gameState.ingredients = preset.ingredients; | |
gameState.sequence = preset.sequence; | |
// Update UI to match | |
bpmRange.value = gameState.bpm; | |
bpmValue.textContent = gameState.bpm; | |
speedRange.value = gameState.cueSpeed; | |
speedValue.textContent = gameState.cueSpeed.toFixed(1); | |
perfectRange.value = gameState.perfectWindow; | |
perfectValue.textContent = gameState.perfectWindow; | |
goodRange.value = gameState.goodWindow; | |
goodValue.textContent = gameState.goodWindow; | |
ingredientsRange.value = gameState.ingredients.length; | |
ingredientsValue.textContent = gameState.ingredients.length; | |
sequenceRange.value = gameState.sequence.length; | |
sequenceValue.textContent = gameState.sequence.length; | |
// Update preset name field | |
presetName.value = name; | |
return true; | |
} | |
return false; | |
} | |
// Apply preset settings | |
function applyPreset(presetName) { | |
switch(presetName) { | |
case 'easy': | |
gameState.bpm = 100; | |
gameState.cueSpeed = 2.5; | |
gameState.perfectWindow = 100; | |
gameState.goodWindow = 200; | |
gameState.ingredients = ['🍅', '🧀', '🍄', '🥩', '🥬', '🍞']; | |
gameState.sequence = Array(16).fill().map((_, i) => ({ beat: i + 1, type: 'tap' })); | |
break; | |
case 'medium': | |
gameState.bpm = 128; | |
gameState.cueSpeed = 2; | |
gameState.perfectWindow = 80; | |
gameState.goodWindow = 160; | |
gameState.ingredients = ['🍅', '🧀', '🍄', '🥩', '🥬', '🍞', '🥚', '🦐', '🌽', '🧅']; | |
gameState.sequence = [ | |
{ beat: 1, type: 'tap' }, { beat: 2, type: 'tap' }, { beat: 3, type: 'tap' }, { beat: 4, type: 'tap' }, | |
{ beat: 5.5, type: 'tap' }, { beat: 6.5, type: 'tap' }, { beat: 7, type: 'tap' }, { beat: 8, type: 'tap' }, | |
{ beat: 9, type: 'tap' }, { beat: 10, type: 'tap' }, { beat: 11.5, type: 'tap' }, { beat: 12.5, type: 'tap' }, | |
{ beat: 13, type: 'tap' }, { beat: 14, type: 'tap' }, { beat: 15, type: 'tap' }, { beat: 16, type: 'tap' } | |
]; | |
break; | |
case 'hard': | |
gameState.bpm = 160; | |
gameState.cueSpeed = 1.5; | |
gameState.perfectWindow = 60; | |
gameState.goodWindow = 120; | |
gameState.ingredients = ['🍅', '🧀', '🍄', '🥩', '🥬', '🍞', '🥚', '🦐', '🌽', '🧅', '🥕', '🍠', '🥦', '🥒', '🍆']; | |
gameState.sequence = [ | |
{ beat: 1, type: 'tap' }, { beat: 1.5, type: 'tap' }, { beat: 2, type: 'tap' }, { beat: 2.5, type: 'tap' }, | |
{ beat: 3, type: 'tap' }, { beat: 3.5, type: 'tap' }, { beat: 4, type: 'tap' }, { beat: 4.5, type: 'tap' }, | |
{ beat: 5, type: 'tap' }, { beat: 5.5, type: 'tap' }, { beat: 6, type: 'tap' }, { beat: 6.5, type: 'tap' }, | |
{ beat: 7, type: 'tap' }, { beat: 7.5, type: 'tap' }, { beat: 8, type: 'tap' }, { beat: 8.5, type: 'tap' } | |
]; | |
break; | |
} | |
// Update UI to match | |
bpmRange.value = gameState.bpm; | |
bpmValue.textContent = gameState.bpm; | |
speedRange.value = gameState.cueSpeed; | |
speedValue.textContent = gameState.cueSpeed.toFixed(1); | |
perfectRange.value = gameState.perfectWindow; | |
perfectValue.textContent = gameState.perfectWindow; | |
goodRange.value = gameState.goodWindow; | |
goodValue.textContent = gameState.goodWindow; | |
ingredientsRange.value = gameState.ingredients.length; | |
ingredientsValue.textContent = gameState.ingredients.length; | |
sequenceRange.value = gameState.sequence.length; | |
sequenceValue.textContent = gameState.sequence.length; | |
// Update active preset button | |
presetButtons.forEach(btn => { | |
btn.classList.toggle('active', btn.dataset.preset === presetName); | |
}); | |
} | |
// Initialize game | |
function initGame() { | |
initAudio(); | |
// Add floating elements to start screen | |
for (let i = 0; i < 10; i++) { | |
const element = document.createElement('div'); | |
element.className = 'floating-bg-element text-4xl'; | |
element.style.left = `${Math.random() * 100}%`; | |
element.style.top = `${Math.random() * 100}%`; | |
element.style.animationDuration = `${Math.random() * 30 + 20}s`; | |
element.style.animationDelay = `${Math.random() * 10}s`; | |
const emojis = ['🍳', '🥘', '🍕', '🍔', '🍟', '🌭', '🍿', '🧂', '🥗', '🍣']; | |
element.textContent = emojis[Math.floor(Math.random() * emojis.length)]; | |
startScreenBg.appendChild(element); | |
} | |
// Load presets | |
loadPresets(); | |
// Apply medium preset by default | |
applyPreset('medium'); | |
// Start button | |
startButton.addEventListener('click', () => { | |
startScreen.classList.add('opacity-0'); | |
setTimeout(() => { | |
startScreen.classList.add('hidden'); | |
gameScreen.classList.remove('hidden'); | |
startSequence(); | |
}, 500); | |
}); | |
// Restart button | |
restartButton.addEventListener('click', () => { | |
endScreen.classList.add('hidden'); | |
startSequence(); | |
}); | |
// Settings button | |
settingsButton.addEventListener('click', () => { | |
settingsPanel.classList.remove('hidden'); | |
setTimeout(() => { | |
settingsPanel.querySelector('.settings-panel').classList.add('open'); | |
}, 10); | |
}); | |
// Game settings button | |
gameSettingsButton.addEventListener('click', () => { | |
settingsPanel.classList.remove('hidden'); | |
setTimeout(() => { | |
settingsPanel.querySelector('.settings-panel').classList.add('open'); | |
}, 10); | |
}); | |
// Close settings | |
closeSettings.addEventListener('click', () => { | |
settingsPanel.querySelector('.settings-panel').classList.remove('open'); | |
setTimeout(() => { | |
settingsPanel.classList.add('hidden'); | |
}, 300); | |
}); | |
// Preset buttons | |
presetButtons.forEach(btn => { | |
btn.addEventListener('click', () => { | |
applyPreset(btn.dataset.preset); | |
}); | |
}); | |
// Range inputs | |
speedRange.addEventListener('input', () => { | |
gameState.cueSpeed = parseFloat(speedRange.value); | |
speedValue.textContent = gameState.cueSpeed.toFixed(1); | |
}); | |
bpmRange.addEventListener('input', () => { | |
gameState.bpm = parseInt(bpmRange.value); | |
bpmValue.textContent = gameState.bpm; | |
}); | |
perfectRange.addEventListener('input', () => { | |
gameState.perfectWindow = parseInt(perfectRange.value); | |
perfectValue.textContent = gameState.perfectWindow; | |
}); | |
goodRange.addEventListener('input', () => { | |
gameState.goodWindow = parseInt(goodRange.value); | |
goodValue.textContent = gameState.goodWindow; | |
}); | |
ingredientsRange.addEventListener('input', () => { | |
const count = parseInt(ingredientsRange.value); | |
ingredientsValue.textContent = count; | |
// Basic ingredients that are always available | |
const baseIngredients = ['🍅', '🧀', '🍄', '🥩', '🥬', '🍞', '🥚']; | |
// Additional ingredients that can be added | |
const extraIngredients = ['🦐', '🌽', '🧅', '🥕', '🍠', '🥦', '🥒', '🍆', '🍍', '🥑']; | |
// Create the ingredient list based on count | |
gameState.ingredients = [...baseIngredients]; | |
if (count > baseIngredients.length) { | |
const needed = count - baseIngredients.length; | |
gameState.ingredients.push(...extraIngredients.slice(0, Math.min(needed, extraIngredients.length))); | |
} else { | |
gameState.ingredients = gameState.ingredients.slice(0, count); | |
} | |
}); | |
sequenceRange.addEventListener('input', () => { | |
const length = parseInt(sequenceRange.value); | |
sequenceValue.textContent = length; | |
// Create a simple sequence with the specified length | |
gameState.sequence = Array(length).fill().map((_, i) => { | |
// Alternate between whole beats and half beats for variety | |
const beat = i % 2 === 0 ? i + 1 : i + 0.5; | |
return { beat, type: 'tap' }; | |
}); | |
}); | |
// Save preset | |
savePreset.addEventListener('click', () => { | |
const name = presetName.value.trim(); | |
if (name) { | |
savePresetToStorage(name); | |
presetName.value = ''; | |
// Show confirmation | |
const feedback = document.createElement('div'); | |
feedback.textContent = 'Preset saved!'; | |
feedback.className = 'text-green-400 text-sm mt-2'; | |
savePreset.parentNode.appendChild(feedback); | |
setTimeout(() => { | |
feedback.remove(); | |
}, 2000); | |
} | |
}); | |
// Load preset | |
loadPreset.addEventListener('change', () => { | |
if (loadPreset.value) { | |
loadPresetFromStorage(loadPreset.value); | |
presetName.value = loadPreset.value; | |
} | |
}); | |
// Tap input | |
gameArea.addEventListener('click', handleTap); | |
// Touch support | |
gameArea.addEventListener('touchstart', (e) => { | |
e.preventDefault(); | |
handleTap(e); | |
}); | |
} | |
// Start the game when ready | |
window.addEventListener('DOMContentLoaded', initGame); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LukasBe/game-4-rhytm-chef" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |