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