Skip to content

Commit 5ab98fa

Browse files
Merge pull request #144 from kanishka1804/feat/rhythm-tap-garden
feat: Add Rhythm Tap Garden game
2 parents 78d6fec + 5cd2e1d commit 5ab98fa

File tree

5 files changed

+544
-0
lines changed

5 files changed

+544
-0
lines changed

data/projects.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,5 +241,13 @@
241241
"category": "contributor",
242242
"categoryKey": "contributor",
243243
"difficulty": "easy"
244+
},
245+
{
246+
"title": "Rhythm Tap Garden",
247+
"slug": "rhythm-tap-garden",
248+
"description": "Grow a virtual plant by tapping in rhythm with a beat.",
249+
"category": "Small Games",
250+
"categoryKey": "games",
251+
"difficulty": "easy"
244252
}
245253
]
1.59 MB
Binary file not shown.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Rhythm Tap Garden</title>
7+
<link rel="stylesheet" href="style.css">
8+
</head>
9+
<body>
10+
<div class="container">
11+
<h1>🌱 Rhythm Tap Garden</h1>
12+
13+
<div class="game-info">
14+
<div class="score">Score: <span id="score">0</span></div>
15+
<div class="combo">Combo: <span id="combo">0</span></div>
16+
<div class="streak">Best Streak: <span id="streak">0</span></div>
17+
</div>
18+
19+
<div class="plant-container">
20+
<div class="plant" id="plant">
21+
<div class="stem"></div>
22+
<div class="leaves"></div>
23+
</div>
24+
</div>
25+
26+
<div class="tap-zone">
27+
<div class="beat-indicator" id="beatIndicator"></div>
28+
<button class="tap-button" id="tapButton">TAP</button>
29+
</div>
30+
31+
<div class="feedback" id="feedback"></div>
32+
33+
<div class="controls">
34+
<button id="startButton" class="control-btn">Start</button>
35+
<button id="stopButton" class="control-btn" disabled>Stop</button>
36+
</div>
37+
38+
<div class="timing-window">
39+
<p>Tap timing:</p>
40+
<div class="timing-bar">
41+
<div class="perfect-zone">Perfect</div>
42+
<div class="good-zone">Good</div>
43+
<div class="miss-zone">Miss</div>
44+
</div>
45+
</div>
46+
</div>
47+
48+
<audio id="backgroundMusic" loop>
49+
<source src="beat.mp3" type="audio/mpeg">
50+
</audio>
51+
52+
<script src="main.js"></script>
53+
</body>
54+
</html>

projects/rhythm-tap-garden/main.js

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Game State
2+
let score = 0;
3+
let combo = 0;
4+
let bestStreak = 0;
5+
let isPlaying = false;
6+
let currentBeat = 0;
7+
let beatInterval = null;
8+
let bpm = 120; // Beats per minute
9+
let beatDuration = (60 / bpm) * 1000; // Convert to milliseconds
10+
let lastBeatTime = 0;
11+
let plantGrowth = 0;
12+
13+
// DOM Elements
14+
const tapButton = document.getElementById('tapButton');
15+
const startButton = document.getElementById('startButton');
16+
const stopButton = document.getElementById('stopButton');
17+
const scoreDisplay = document.getElementById('score');
18+
const comboDisplay = document.getElementById('combo');
19+
const streakDisplay = document.getElementById('streak');
20+
const feedbackDisplay = document.getElementById('feedback');
21+
const beatIndicator = document.getElementById('beatIndicator');
22+
const plant = document.getElementById('plant');
23+
const backgroundMusic = document.getElementById('backgroundMusic');
24+
25+
// Timing windows (in milliseconds)
26+
const PERFECT_WINDOW = 100;
27+
const GOOD_WINDOW = 200;
28+
29+
// Initialize
30+
function init() {
31+
tapButton.addEventListener('click', handleTap);
32+
startButton.addEventListener('click', startGame);
33+
stopButton.addEventListener('click', stopGame);
34+
35+
// Also allow spacebar for tapping
36+
document.addEventListener('keydown', (e) => {
37+
if (e.code === 'Space' && isPlaying) {
38+
e.preventDefault();
39+
handleTap();
40+
}
41+
});
42+
}
43+
44+
// Start the game
45+
function startGame() {
46+
if (isPlaying) return;
47+
48+
isPlaying = true;
49+
score = 0;
50+
combo = 0;
51+
plantGrowth = 0;
52+
currentBeat = 0;
53+
54+
updateDisplay();
55+
updatePlantAppearance();
56+
57+
startButton.disabled = true;
58+
stopButton.disabled = false;
59+
60+
// Start background music
61+
backgroundMusic.play().catch(e => console.log('Audio play failed:', e));
62+
63+
// Start beat loop
64+
lastBeatTime = Date.now();
65+
beatLoop();
66+
}
67+
68+
// Stop the game
69+
function stopGame() {
70+
if (!isPlaying) return;
71+
72+
isPlaying = false;
73+
74+
if (beatInterval) {
75+
clearTimeout(beatInterval);
76+
beatInterval = null;
77+
}
78+
79+
backgroundMusic.pause();
80+
backgroundMusic.currentTime = 0;
81+
82+
startButton.disabled = false;
83+
stopButton.disabled = true;
84+
85+
beatIndicator.classList.remove('pulse');
86+
}
87+
88+
// Beat loop - creates the rhythm
89+
function beatLoop() {
90+
if (!isPlaying) return;
91+
92+
// Visual beat indicator
93+
beatIndicator.classList.add('pulse');
94+
setTimeout(() => {
95+
beatIndicator.classList.remove('pulse');
96+
}, 100);
97+
98+
lastBeatTime = Date.now();
99+
currentBeat++;
100+
101+
// Schedule next beat
102+
beatInterval = setTimeout(beatLoop, beatDuration);
103+
}
104+
105+
// Handle tap input
106+
function handleTap() {
107+
if (!isPlaying) return;
108+
109+
const currentTime = Date.now();
110+
const timeSinceLastBeat = currentTime - lastBeatTime;
111+
112+
// Calculate timing - handle wrapping around beat
113+
let timing;
114+
if (timeSinceLastBeat <= beatDuration / 2) {
115+
timing = timeSinceLastBeat;
116+
} else {
117+
timing = beatDuration - timeSinceLastBeat;
118+
}
119+
120+
// Determine accuracy
121+
let feedback;
122+
let points;
123+
124+
if (timing <= PERFECT_WINDOW) {
125+
feedback = 'PERFECT! ⭐';
126+
points = 100;
127+
combo++;
128+
growPlant(3);
129+
showFeedback(feedback, 'perfect');
130+
} else if (timing <= GOOD_WINDOW) {
131+
feedback = 'Good! ✓';
132+
points = 50;
133+
combo++;
134+
growPlant(2);
135+
showFeedback(feedback, 'good');
136+
} else {
137+
feedback = 'Miss ✗';
138+
points = 0;
139+
resetCombo();
140+
showFeedback(feedback, 'miss');
141+
return;
142+
}
143+
144+
// Update score with combo multiplier
145+
const comboMultiplier = Math.min(Math.floor(combo / 5) + 1, 3);
146+
score += points * comboMultiplier;
147+
148+
// Update best streak
149+
if (combo > bestStreak) {
150+
bestStreak = combo;
151+
}
152+
153+
updateDisplay();
154+
}
155+
156+
// Show feedback to user
157+
function showFeedback(text, type) {
158+
feedbackDisplay.textContent = text;
159+
feedbackDisplay.className = `feedback ${type}`;
160+
161+
setTimeout(() => {
162+
feedbackDisplay.textContent = '';
163+
feedbackDisplay.className = 'feedback';
164+
}, 500);
165+
}
166+
167+
// Grow the plant
168+
function growPlant(amount) {
169+
plantGrowth += amount;
170+
updatePlantAppearance();
171+
}
172+
173+
// Update plant visual based on growth
174+
function updatePlantAppearance() {
175+
plant.className = 'plant';
176+
177+
if (plantGrowth >= 30) {
178+
plant.classList.add('full-bloom');
179+
} else if (plantGrowth >= 15) {
180+
plant.classList.add('blooming');
181+
} else if (plantGrowth >= 5) {
182+
plant.classList.add('growing');
183+
}
184+
}
185+
186+
// Reset combo
187+
function resetCombo() {
188+
combo = 0;
189+
}
190+
191+
// Update display
192+
function updateDisplay() {
193+
scoreDisplay.textContent = score;
194+
comboDisplay.textContent = combo;
195+
streakDisplay.textContent = bestStreak;
196+
}
197+
198+
// Initialize the game when page loads
199+
init();

0 commit comments

Comments
 (0)