stroumphs commited on
Commit
927b60f
·
verified ·
1 Parent(s): 2fb72ee

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +1525 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Paddlebound
3
- emoji: 📉
4
- colorFrom: red
5
- colorTo: indigo
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: paddlebound
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: blue
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1525 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Paddlebound: Rogue Bounce</title>
7
+ <style>
8
+ /* Google Fonts Import */
9
+ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Ubuntu+Mono:wght@400;700&display=swap');
10
+
11
+ /* Base Styles */
12
+ :root {
13
+ --primary: #FF3E41;
14
+ --secondary: #39A0ED;
15
+ --accent: #4CC9F0;
16
+ --dark: #14213D;
17
+ --light: #F8F9FA;
18
+ --success: #4ADF86;
19
+ --warning: #F9C846;
20
+ --danger: #E63946;
21
+ --scoreboard: #333;
22
+ }
23
+
24
+ * {
25
+ margin: 0;
26
+ padding: 0;
27
+ box-sizing: border-box;
28
+ }
29
+
30
+ body {
31
+ font-family: 'Ubuntu Mono', monospace;
32
+ background-color: var(--dark);
33
+ color: var(--light);
34
+ overflow: hidden;
35
+ height: 100vh;
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ justify-content: center;
40
+ background-image:
41
+ radial-gradient(circle at 25% 25%, rgba(57, 160, 237, 0.2) 0%, transparent 50%),
42
+ radial-gradient(circle at 75% 75%, rgba(255, 62, 65, 0.2) 0%, transparent 50%);
43
+ background-position: center;
44
+ background-size: cover;
45
+ }
46
+
47
+ /* Game Main Container */
48
+ .game-container {
49
+ position: relative;
50
+ width: 800px;
51
+ height: 500px;
52
+ background-color: var(--dark);
53
+ border: 4px solid var(--light);
54
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
55
+ border-radius: 8px;
56
+ overflow: hidden;
57
+ }
58
+
59
+ /* Game Canvas */
60
+ #gameCanvas {
61
+ width: 100%;
62
+ height: 100%;
63
+ background-color: var(--dark);
64
+ }
65
+
66
+ /* Score Boards */
67
+ .score-board {
68
+ position: absolute;
69
+ top: 20px;
70
+ width: 100%;
71
+ display: flex;
72
+ justify-content: space-between;
73
+ padding: 0 30px;
74
+ z-index: 10;
75
+ font-family: 'Press Start 2P', cursive;
76
+ font-size: 20px;
77
+ color: var(--light);
78
+ text-shadow: 0 0 5px var(--primary);
79
+ }
80
+
81
+ .player-score, .opponent-score {
82
+ background-color: rgba(20, 33, 61, 0.8);
83
+ padding: 10px 20px;
84
+ border-radius: 5px;
85
+ border: 2px solid var(--light);
86
+ }
87
+
88
+ /* Power-up HUD */
89
+ .powerups-hud {
90
+ position: absolute;
91
+ bottom: 20px;
92
+ left: 50%;
93
+ transform: translateX(-50%);
94
+ display: flex;
95
+ gap: 10px;
96
+ z-index: 10;
97
+ }
98
+
99
+ .powerup-icon {
100
+ width: 30px;
101
+ height: 30px;
102
+ border-radius: 50%;
103
+ border: 2px solid var(--light);
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ font-size: 16px;
108
+ background-color: rgba(76, 201, 240, 0.2);
109
+ opacity: 0;
110
+ transition: opacity 0.3s;
111
+ }
112
+
113
+ .powerup-icon.active {
114
+ opacity: 1;
115
+ animation: pulse 1s infinite;
116
+ }
117
+
118
+ @keyframes pulse {
119
+ 0% { transform: scale(1); }
120
+ 50% { transform: scale(1.1); }
121
+ 100% { transform: scale(1); }
122
+ }
123
+
124
+ /* Game Overlay Screens */
125
+ .overlay {
126
+ position: absolute;
127
+ top: 0;
128
+ left: 0;
129
+ width: 100%;
130
+ height: 100%;
131
+ background-color: rgba(20, 33, 61, 0.9);
132
+ display: flex;
133
+ flex-direction: column;
134
+ align-items: center;
135
+ justify-content: center;
136
+ z-index: 20;
137
+ font-family: 'Press Start 2P', cursive;
138
+ text-align: center;
139
+ padding: 20px;
140
+ }
141
+
142
+ .title {
143
+ font-size: 3rem;
144
+ color: var(--primary);
145
+ margin-bottom: 20px;
146
+ text-shadow: 3px 3px 0 var(--light);
147
+ animation: titleGlow 2s infinite alternate;
148
+ }
149
+
150
+ @keyframes titleGlow {
151
+ from { text-shadow: 0 0 5px var(--light), 3px 3px 0 var(--light); }
152
+ to { text-shadow: 0 0 20px var(--accent), 3px 3px 0 var(--light); }
153
+ }
154
+
155
+ .subtitle {
156
+ font-size: 1.5rem;
157
+ color: var(--accent);
158
+ margin-bottom: 30px;
159
+ }
160
+
161
+ .btn {
162
+ background-color: var(--primary);
163
+ color: var(--light);
164
+ border: none;
165
+ padding: 15px 30px;
166
+ font-size: 1.2rem;
167
+ font-family: 'Press Start 2P', cursive;
168
+ cursor: pointer;
169
+ margin: 10px;
170
+ border-radius: 5px;
171
+ transition: all 0.3s;
172
+ }
173
+
174
+ .btn:hover {
175
+ background-color: var(--danger);
176
+ transform: translateY(-3px);
177
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
178
+ }
179
+
180
+ .btn:active {
181
+ transform: translateY(0);
182
+ }
183
+
184
+ .btn-secondary {
185
+ background-color: var(--secondary);
186
+ }
187
+
188
+ .btn-secondary:hover {
189
+ background-color: var(--accent);
190
+ }
191
+
192
+ /* Game Over Screen */
193
+ .game-over .title {
194
+ color: var(--danger);
195
+ }
196
+
197
+ /* Opponent Introduction */
198
+ .opponent-intro {
199
+ position: absolute;
200
+ top: 50%;
201
+ left: 50%;
202
+ transform: translate(-50%, -50%);
203
+ background-color: rgba(20, 33, 61, 0.9);
204
+ border: 3px solid var(--light);
205
+ padding: 20px;
206
+ border-radius: 10px;
207
+ z-index: 20;
208
+ text-align: center;
209
+ max-width: 500px;
210
+ }
211
+
212
+ .opponent-name {
213
+ font-size: 1.5rem;
214
+ color: var(--warning);
215
+ margin-bottom: 10px;
216
+ }
217
+
218
+ .opponent-description {
219
+ font-size: 1rem;
220
+ margin-bottom: 20px;
221
+ font-family: 'Ubuntu Mono', monospace;
222
+ }
223
+
224
+ .opponent-special {
225
+ color: var(--accent);
226
+ font-weight: bold;
227
+ }
228
+
229
+ /* Stage Modifier Indicator */
230
+ .modifier-indicator {
231
+ position: absolute;
232
+ top: 50%;
233
+ left: 50%;
234
+ transform: translate(-50%, -50%);
235
+ background-color: rgba(20, 33, 61, 0.9);
236
+ border: 3px solid var(--light);
237
+ padding: 15px;
238
+ border-radius: 10px;
239
+ z-index: 20;
240
+ text-align: center;
241
+ max-width: 500px;
242
+ animation: modifierFadeIn 0.5s;
243
+ }
244
+
245
+ @keyframes modifierFadeIn {
246
+ from { opacity: 0; transform: translate(-50%, -40%); }
247
+ to { opacity: 1; transform: translate(-50%, -50%); }
248
+ }
249
+
250
+ .modifier-title {
251
+ font-size: 1.3rem;
252
+ color: var(--success);
253
+ margin-bottom: 10px;
254
+ }
255
+
256
+ .modifier-effect {
257
+ font-size: 1rem;
258
+ font-style: italic;
259
+ }
260
+
261
+ /* Power-up Notification */
262
+ .powerup-notification {
263
+ position: absolute;
264
+ top: 50%;
265
+ left: 50%;
266
+ transform: translate(-50%, -50%);
267
+ background-color: rgba(76, 201, 240, 0.2);
268
+ border: 2px solid var(--light);
269
+ padding: 10px 20px;
270
+ border-radius: 5px;
271
+ z-index: 20;
272
+ text-align: center;
273
+ opacity: 0;
274
+ transition: all 0.3s;
275
+ }
276
+
277
+ .powerup-notification.show {
278
+ opacity: 1;
279
+ animation: powerUpFade 2s forwards;
280
+ }
281
+
282
+ @keyframes powerUpFade {
283
+ 0% { opacity: 1; transform: translate(-50%, -50%); }
284
+ 80% { opacity: 1; transform: translate(-50%, -60%); }
285
+ 100% { opacity: 0; transform: translate(-50%, -70%); }
286
+ }
287
+
288
+ /* Run Stats */
289
+ .run-stats {
290
+ display: flex;
291
+ flex-wrap: wrap;
292
+ justify-content: center;
293
+ gap: 20px;
294
+ margin: 20px 0;
295
+ }
296
+
297
+ .stat-item {
298
+ background-color: rgba(57, 160, 237, 0.2);
299
+ padding: 10px 20px;
300
+ border-radius: 5px;
301
+ border: 1px solid var(--accent);
302
+ min-width: 150px;
303
+ }
304
+
305
+ .stat-value {
306
+ font-size: 1.3rem;
307
+ color: var(--warning);
308
+ }
309
+
310
+ .stat-label {
311
+ font-size: 0.8rem;
312
+ color: var(--accent);
313
+ text-transform: uppercase;
314
+ }
315
+
316
+ /* Permanent Upgrades */
317
+ .upgrades-container {
318
+ display: flex;
319
+ flex-wrap: wrap;
320
+ justify-content: center;
321
+ gap: 15px;
322
+ margin-top: 20px;
323
+ max-width: 600px;
324
+ }
325
+
326
+ .upgrade-item {
327
+ background-color: rgba(20, 33, 61, 0.8);
328
+ border: 2px solid var(--accent);
329
+ border-radius: 5px;
330
+ padding: 15px;
331
+ width: 180px;
332
+ cursor: pointer;
333
+ transition: all 0.3s;
334
+ }
335
+
336
+ .upgrade-item:hover {
337
+ transform: translateY(-5px);
338
+ box-shadow: 0 5px 15px rgba(76, 201, 240, 0.3);
339
+ border-color: var(--success);
340
+ }
341
+
342
+ .upgrade-item.purchased {
343
+ border-color: var(--success);
344
+ background-color: rgba(74, 223, 134, 0.1);
345
+ }
346
+
347
+ .upgrade-item.disabled {
348
+ opacity: 0.6;
349
+ cursor: not-allowed;
350
+ }
351
+
352
+ .upgrade-name {
353
+ color: var(--warning);
354
+ margin-bottom: 5px;
355
+ font-size: 0.9rem;
356
+ }
357
+
358
+ .upgrade-description {
359
+ font-size: 0.8rem;
360
+ margin-bottom: 10px;
361
+ font-family: 'Ubuntu Mono', monospace;
362
+ }
363
+
364
+ .upgrade-cost {
365
+ color: var(--success);
366
+ font-size: 0.9rem;
367
+ }
368
+
369
+ .currency-display {
370
+ position: absolute;
371
+ top: 20px;
372
+ right: 20px;
373
+ background-color: rgba(20, 33, 61, 0.8);
374
+ padding: 8px 15px;
375
+ border-radius: 5px;
376
+ border: 2px solid var(--warning);
377
+ font-family: 'Press Start 2P', cursive;
378
+ font-size: 0.9rem;
379
+ color: var(--warning);
380
+ }
381
+
382
+ /* Responsive */
383
+ @media (max-width: 850px) {
384
+ .game-container {
385
+ width: 95%;
386
+ height: 400px;
387
+ }
388
+
389
+ .title {
390
+ font-size: 2rem;
391
+ }
392
+
393
+ .subtitle {
394
+ font-size: 1.2rem;
395
+ }
396
+
397
+ .btn {
398
+ padding: 10px 20px;
399
+ font-size: 1rem;
400
+ }
401
+
402
+ .opponent-intro, .modifier-indicator {
403
+ width: 90%;
404
+ }
405
+
406
+ .opponent-name {
407
+ font-size: 1.2rem;
408
+ }
409
+ }
410
+
411
+ @media (max-height: 700px) {
412
+ .game-container {
413
+ height: 350px;
414
+ }
415
+ }
416
+ </style>
417
+ </head>
418
+ <body>
419
+ <div class="game-container">
420
+ <canvas id="gameCanvas"></canvas>
421
+
422
+ <!-- Score UI -->
423
+ <div class="score-board">
424
+ <div class="player-score">YOU: 0</div>
425
+ <div class="opponent-score">CPU: 0</div>
426
+ </div>
427
+
428
+ <!-- Power-up HUD -->
429
+ <div class="powerups-hud">
430
+ <div class="powerup-icon" id="pup1">?</div>
431
+ <div class="powerup-icon" id="pup2">?</div>
432
+ <div class="powerup-icon" id="pup3">?</div>
433
+ </div>
434
+
435
+ <!-- Main Menu Overlay -->
436
+ <div class="overlay" id="mainMenu">
437
+ <h1 class="title">Paddlebound:<br>Rogue Bounce</h1>
438
+ <p class="subtitle">A Roguelike Ping Pong Adventure</p>
439
+ <button class="btn" id="startGame">New Run</button>
440
+ <button class="btn btn-secondary" id="upgradesBtn">Permanent Upgrades</button>
441
+ </div>
442
+
443
+ <!-- Game Over Overlay -->
444
+ <div class="overlay game-over" id="gameOver" style="display: none;">
445
+ <h1 class="title">Game Over</h1>
446
+ <p class="subtitle">Run Completed</p>
447
+
448
+ <div class="run-stats">
449
+ <div class="stat-item">
450
+ <div class="stat-value" id="stat-rounds">0</div>
451
+ <div class="stat-label">Opponents Defeated</div>
452
+ </div>
453
+ <div class="stat-item">
454
+ <div class="stat-value" id="stat-score">0</div>
455
+ <div class="stat-label">Total Points</div>
456
+ </div>
457
+ <div class="stat-item">
458
+ <div class="stat-value" id="stat-time">0:00</div>
459
+ <div class="stat-label">Time Played</div>
460
+ </div>
461
+ </div>
462
+
463
+ <p>Earned <span id="earned-currency" style="color: var(--warning);">0</span> upgrade points</p>
464
+
465
+ <button class="btn" id="restartGame">Try Again</button>
466
+ <button class="btn btn-secondary" id="backToMenu">Main Menu</button>
467
+ </div>
468
+
469
+ <!-- Upgrades Screen -->
470
+ <div class="overlay" id="upgradesScreen" style="display: none;">
471
+ <h1 class="title">Permanent Upgrades</h1>
472
+ <div class="currency-display">
473
+ Points: <span id="currency-amount">0</span>
474
+ </div>
475
+
476
+ <div class="upgrades-container">
477
+ <!-- Upgrade items will be added here dynamically -->
478
+ </div>
479
+
480
+ <button class="btn" id="backFromUpgrades">Back</button>
481
+ </div>
482
+ </div>
483
+
484
+ <script>
485
+ // Game Constants
486
+ const GAME_WIDTH = 800;
487
+ const GAME_HEIGHT = 500;
488
+ const PADDLE_WIDTH = 100;
489
+ const PADDLE_HEIGHT = 15;
490
+ const PADDLE_OFFSET = 20;
491
+ const BALL_SIZE = 10;
492
+ const BALL_INITIAL_SPEED = 5;
493
+ const WIN_SCORE = 11;
494
+ const POWERUP_SIZE = 15;
495
+ const POWERUP_SPAWN_CHANCE = 0.02; // 2% chance per frame
496
+ const POWERUP_DURATION = 10000; // 10 seconds
497
+
498
+ // Game Variables
499
+ let canvas, ctx;
500
+ let gameRunning = false;
501
+ let gameOver = false;
502
+ let animationFrameId;
503
+ let runStarted = false;
504
+ let winScore = WIN_SCORE;
505
+
506
+ // Game Objects
507
+ let player = {
508
+ x: GAME_WIDTH / 2 - PADDLE_WIDTH / 2,
509
+ y: GAME_HEIGHT - PADDLE_HEIGHT - PADDLE_OFFSET,
510
+ width: PADDLE_WIDTH,
511
+ height: PADDLE_HEIGHT,
512
+ color: '#4CC9F0',
513
+ speed: 8
514
+ };
515
+
516
+ let opponent = {
517
+ x: GAME_WIDTH / 2 - PADDLE_WIDTH / 2,
518
+ y: PADDLE_OFFSET,
519
+ width: PADDLE_WIDTH,
520
+ height: PADDLE_HEIGHT,
521
+ color: '#FF3E41',
522
+ speed: 5,
523
+ reactionTime: 0.5 // 0-1, where 1 is perfect reaction
524
+ };
525
+
526
+ let ball = {
527
+ x: GAME_WIDTH / 2,
528
+ y: GAME_HEIGHT / 2,
529
+ size: BALL_SIZE,
530
+ color: '#F8F9FA',
531
+ speedX: 0,
532
+ speedY: 0,
533
+ speed: BALL_INITIAL_SPEED,
534
+ active: false,
535
+ trail: []
536
+ };
537
+
538
+ // Power-ups
539
+ let powerups = [];
540
+ let activePowerups = [];
541
+ let powerupTypes = [
542
+ { id: 'paddleXL', name: 'Paddle XL', color: '#4ADF86', effect: 'Increase paddle size by 50%', duration: POWERUP_DURATION, icon: 'XL' },
543
+ { id: 'paddleXS', name: 'Paddle XS', color: '#E63946', effect: 'Decrease paddle size by 50%', duration: POWERUP_DURATION, icon: 'XS' },
544
+ { id: 'speedBoost', name: 'Speed Boost', color: '#F9C846', effect: 'Increase ball speed for opponent', duration: POWERUP_DURATION, icon: '⚡' },
545
+ { id: 'multiBall', name: 'Multi Ball', color: '#39A0ED', effect: 'Spawn 3 extra balls', duration: POWERUP_DURATION, icon: '③' },
546
+ { id: 'slowMotion', name: 'Slow Mo', color: '#9B59B6', effect: 'Slow down time', duration: POWERUP_DURATION, icon: '⌛' },
547
+ { id: 'magnet', name: 'Ball Magnet', color: '#FF7F50', effect: 'Ball gravitates to your paddle', duration: POWERUP_DURATION, icon: '🧲' }
548
+ ];
549
+
550
+ // Opponent Types
551
+ let opponents = [
552
+ {
553
+ name: "The Wall",
554
+ description: "A defensive opponent that covers a wide area.",
555
+ width: 150,
556
+ speed: 4,
557
+ reactionTime: 0.7,
558
+ color: "#2ECC71"
559
+ },
560
+ {
561
+ name: "Speed Demon",
562
+ description: "Moves extremely fast but has a smaller paddle.",
563
+ width: 70,
564
+ speed: 9,
565
+ reactionTime: 0.8,
566
+ color: "#E74C3C"
567
+ },
568
+ {
569
+ name: "The Trickster",
570
+ description: "Adjusts reaction time unpredictably.",
571
+ width: 100,
572
+ speed: 6,
573
+ reactionTime: 0.5,
574
+ color: "#9B59B6",
575
+ special: "Changes reaction time randomly during the match"
576
+ },
577
+ {
578
+ name: "The Turtle",
579
+ description: "Very slow but has perfect reaction time.",
580
+ width: 120,
581
+ speed: 3,
582
+ reactionTime: 1.0,
583
+ color: "#F39C12",
584
+ special: "Never misses the ball if it's within reach"
585
+ },
586
+ {
587
+ name: "The Juggernaut",
588
+ description: "Massive paddle but moves slowly.",
589
+ width: 200,
590
+ speed: 2.5,
591
+ reactionTime: 0.6,
592
+ color: "#16A085"
593
+ }
594
+ ];
595
+
596
+ // Stage Modifiers
597
+ let stageModifiers = [
598
+ {
599
+ name: "Wind Gusts",
600
+ description: "Random wind affects ball movement.",
601
+ effect: function() {
602
+ let windForce = (Math.random() - 0.5) * 0.5;
603
+ ball.speedX += windForce;
604
+ if (frameCount % 60 === 0) windForce = (Math.random() - 0.5) * 0.5;
605
+ },
606
+ color: "#A2D9CE"
607
+ },
608
+ {
609
+ name: "Gravity Well",
610
+ description: "Ball accelerates downward faster.",
611
+ effect: function() {
612
+ if (ball.speedY > 0) ball.speedY *= 1.02;
613
+ else ball.speedY /= 1.02;
614
+ },
615
+ color: "#95A5A6"
616
+ },
617
+ {
618
+ name: "Rebound Chaos",
619
+ description: "Ball speed increases unpredictably on bounces.",
620
+ effect: function() {
621
+ ball.trail.forEach((pos, i) => {
622
+ ctx.fillStyle = `rgba(76, 201, 240, ${0.2 + (i*0.1)})`;
623
+ ctx.fillRect(pos.x, pos.y, ball.size, ball.size);
624
+ });
625
+ },
626
+ color: "#D2B4DE"
627
+ },
628
+ {
629
+ name: "Multi-Ball Madness",
630
+ description: "Random chance for extra balls to spawn.",
631
+ effect: function() {
632
+ if (Math.random() < 0.005) spawnPowerup('multiBall');
633
+ },
634
+ color: "#3498DB",
635
+ special: "Watch out for unexpected extra balls!"
636
+ }
637
+ ];
638
+
639
+ // Game State
640
+ let gameState = {
641
+ playerScore: 0,
642
+ opponentScore: 0,
643
+ currentOpponent: 0,
644
+ currentModifier: null,
645
+ runStartTime: 0,
646
+ runStats: {
647
+ opponentsDefeated: 0,
648
+ totalPoints: 0,
649
+ powerupsCollected: 0,
650
+ roundsPlayed: 0
651
+ },
652
+ powerupsActive: {},
653
+ frameCount: 0
654
+ };
655
+
656
+ // Player progression
657
+ let playerMeta = {
658
+ currency: 0,
659
+ upgrades: {
660
+ paddleStartSize: 0, // 0-2 (0: default, 1: +20%, 2: +40%)
661
+ ballStartSpeed: 0, // 0-2 (0: default, 1: -10%, 2: -20%)
662
+ powerupDuration: 0, // 0-2 (0: default, 1: +25%, 2: +50%)
663
+ extraLife: false, // Start with an extra life
664
+ powerupSlots: 1 // Up to 3
665
+ }
666
+ };
667
+
668
+ // Upgrade definitions
669
+ let upgradeDefinitions = [
670
+ {
671
+ id: 'paddleSize1',
672
+ name: 'Paddle +20%',
673
+ description: 'Start with a 20% larger paddle',
674
+ cost: 100,
675
+ maxLevel: 2,
676
+ apply: function() {
677
+ playerMeta.upgrades.paddleStartSize = Math.min(playerMeta.upgrades.paddleStartSize + 1, this.maxLevel);
678
+ resetPaddleSizes();
679
+ },
680
+ getCurrentLevel: function() {
681
+ return playerMeta.upgrades.paddleStartSize;
682
+ }
683
+ },
684
+ {
685
+ id: 'ballSpeed1',
686
+ name: 'Slower Ball',
687
+ description: 'Start with 10% slower ball speed',
688
+ cost: 150,
689
+ maxLevel: 2,
690
+ apply: function() {
691
+ playerMeta.upgrades.ballStartSpeed = Math.min(playerMeta.upgrades.ballStartSpeed + 1, this.maxLevel);
692
+ },
693
+ getCurrentLevel: function() {
694
+ return playerMeta.upgrades.ballStartSpeed;
695
+ }
696
+ },
697
+ {
698
+ id: 'powerupDuration1',
699
+ name: 'Powerup +25%',
700
+ description: 'Powerups last 25% longer',
701
+ cost: 125,
702
+ maxLevel: 2,
703
+ apply: function() {
704
+ playerMeta.upgrades.powerupDuration = Math.min(playerMeta.upgrades.powerupDuration + 1, this.maxLevel);
705
+ },
706
+ getCurrentLevel: function() {
707
+ return playerMeta.upgrades.powerupDuration;
708
+ }
709
+ },
710
+ {
711
+ id: 'extraLife',
712
+ name: 'Extra Life',
713
+ description: 'Start with one additional chance per run',
714
+ cost: 250,
715
+ maxLevel: 1,
716
+ apply: function() {
717
+ playerMeta.upgrades.extraLife = true;
718
+ },
719
+ getCurrentLevel: function() {
720
+ return playerMeta.upgrades.extraLife ? 1 : 0;
721
+ }
722
+ },
723
+ {
724
+ id: 'powerupSlot2',
725
+ name: 'Powerup Slot #2',
726
+ description: 'Unlock an additional powerup slot',
727
+ cost: 200,
728
+ maxLevel: 1,
729
+ apply: function() {
730
+ playerMeta.upgrades.powerupSlots = Math.min(playerMeta.upgrades.powerupSlots + 1, 3);
731
+ updatePowerupHud();
732
+ },
733
+ getCurrentLevel: function() {
734
+ return playerMeta.upgrades.powerupSlots >= 2 ? 1 : 0;
735
+ }
736
+ },
737
+ {
738
+ id: 'powerupSlot3',
739
+ name: 'Powerup Slot #3',
740
+ description: 'Unlock a third powerup slot',
741
+ cost: 300,
742
+ maxLevel: 1,
743
+ prereq: 'powerupSlot2',
744
+ apply: function() {
745
+ playerMeta.upgrades.powerupSlots = Math.min(playerMeta.upgrades.powerupSlots + 1, 3);
746
+ updatePowerupHud();
747
+ },
748
+ getCurrentLevel: function() {
749
+ return playerMeta.upgrades.powerupSlots >= 3 ? 1 : 0;
750
+ }
751
+ }
752
+ ];
753
+
754
+ // DOM Elements
755
+ let mainMenu, gameOverScreen, upgradesScreen;
756
+ let startGameBtn, restartGameBtn, backToMenuBtn, upgradesBtn, backFromUpgradesBtn;
757
+
758
+ // Initialize the game
759
+ function init() {
760
+ canvas = document.getElementById('gameCanvas');
761
+ ctx = canvas.getContext('2d');
762
+ canvas.width = GAME_WIDTH;
763
+ canvas.height = GAME_HEIGHT;
764
+
765
+ // Get DOM elements
766
+ mainMenu = document.getElementById('mainMenu');
767
+ gameOverScreen = document.getElementById('gameOver');
768
+ upgradesScreen = document.getElementById('upgradesScreen');
769
+
770
+ startGameBtn = document.getElementById('startGame');
771
+ restartGameBtn = document.getElementById('restartGame');
772
+ backToMenuBtn = document.getElementById('backToMenu');
773
+ upgradesBtn = document.getElementById('upgradesBtn');
774
+ backFromUpgradesBtn = document.getElementById('backFromUpgrades');
775
+
776
+ // Event listeners
777
+ startGameBtn.addEventListener('click', startGame);
778
+ restartGameBtn.addEventListener('click', startGame);
779
+ backToMenuBtn.addEventListener('click', showMainMenu);
780
+ upgradesBtn.addEventListener('click', showUpgradesScreen);
781
+ backFromUpgradesBtn.addEventListener('click', showMainMenu);
782
+
783
+ // Load saved data
784
+ loadGame();
785
+
786
+ // Draw initial state
787
+ draw();
788
+
789
+ // Set up keyboard controls
790
+ document.addEventListener('keydown', keyDownHandler);
791
+ document.addEventListener('keyup', keyUpHandler);
792
+
793
+ // Initialize powerup HUD
794
+ updatePowerupHud();
795
+ }
796
+
797
+ // Load game state from localStorage
798
+ function loadGame() {
799
+ const savedGame = localStorage.getItem('paddleboundSave');
800
+ if (savedGame) {
801
+ try {
802
+ const savedData = JSON.parse(savedGame);
803
+ playerMeta.currency = savedData.currency || 0;
804
+ playerMeta.upgrades = savedData.upgrades || {
805
+ paddleStartSize: 0,
806
+ ballStartSpeed: 0,
807
+ powerupDuration: 0,
808
+ extraLife: false,
809
+ powerupSlots: 1
810
+ };
811
+
812
+ // Update currency display
813
+ updateCurrencyDisplay();
814
+ } catch (e) {
815
+ console.error("Failed to load save data:", e);
816
+ }
817
+ }
818
+ }
819
+
820
+ // Save game state to localStorage
821
+ function saveGame() {
822
+ const saveData = {
823
+ currency: playerMeta.currency,
824
+ upgrades: playerMeta.upgrades
825
+ };
826
+ localStorage.setItem('paddleboundSave', JSON.stringify(saveData));
827
+ }
828
+
829
+ // Reset game state for a new run
830
+ function resetGame() {
831
+ gameState = {
832
+ playerScore: 0,
833
+ opponentScore: 0,
834
+ currentOpponent: 0,
835
+ currentModifier: null,
836
+ runStartTime: Date.now(),
837
+ runStats: {
838
+ opponentsDefeated: 0,
839
+ totalPoints: 0,
840
+ powerupsCollected: 0,
841
+ roundsPlayed: 0
842
+ },
843
+ powerupsActive: {},
844
+ frameCount: 0
845
+ };
846
+
847
+ powerups = [];
848
+ activePowerups = [];
849
+ ball.trail = [];
850
+
851
+ // Reset player and opponent
852
+ resetPaddleSizes();
853
+
854
+ // Apply upgrades
855
+ if (playerMeta.upgrades.paddleStartSize > 0) {
856
+ player.width = PADDLE_WIDTH * (1 + 0.2 * playerMeta.upgrades.paddleStartSize);
857
+ opponent.width = PADDLE_WIDTH * (1 + 0.2 * playerMeta.upgrades.paddleStartSize);
858
+ }
859
+
860
+ // Position player and opponent
861
+ player.x = GAME_WIDTH / 2 - player.width / 2;
862
+ player.y = GAME_HEIGHT - PADDLE_HEIGHT - PADDLE_OFFSET;
863
+
864
+ opponent.x = GAME_WIDTH / 2 - opponent.width / 2;
865
+ opponent.y = PADDLE_OFFSET;
866
+
867
+ // Setup first opponent
868
+ setupOpponent(gameState.currentOpponent);
869
+
870
+ // Setup stage modifier
871
+ if (Math.random() > 0.3) { // 70% chance to have a modifier
872
+ gameState.currentModifier = stageModifiers[Math.floor(Math.random() * stageModifiers.length)];
873
+ showModifier(gameState.currentModifier);
874
+ }
875
+
876
+ // Reset ball
877
+ resetBall();
878
+
879
+ // Start with ball inactive (waits for player serve)
880
+ ball.active = false;
881
+ }
882
+
883
+ function resetPaddleSizes() {
884
+ player.width = PADDLE_WIDTH;
885
+ opponent.width = PADDLE_WIDTH;
886
+ }
887
+
888
+ // Set up a new opponent
889
+ function setupOpponent(index) {
890
+ const opponentType = opponents[index % opponents.length];
891
+
892
+ opponent = {
893
+ ...opponent,
894
+ width: opponentType.width,
895
+ speed: opponentType.speed,
896
+ reactionTime: opponentType.reactionTime,
897
+ color: opponentType.color,
898
+ name: opponentType.name,
899
+ description: opponentType.description,
900
+ special: opponentType.special
901
+ };
902
+
903
+ opponent.x = GAME_WIDTH / 2 - opponent.width / 2;
904
+ opponent.y = PADDLE_OFFSET;
905
+
906
+ // Show opponent introduction
907
+ showOpponentIntro(opponentType);
908
+ }
909
+
910
+ // Show opponent introduction
911
+ function showOpponentIntro(opponent) {
912
+ const introElement = document.createElement('div');
913
+ introElement.className = 'opponent-intro';
914
+ introElement.innerHTML = `
915
+ <div class="opponent-name">${opponent.name}</div>
916
+ <div class="opponent-description">${opponent.description}</div>
917
+ ${opponent.special ? `<div class="opponent-special">Special: ${opponent.special}</div>` : ''}
918
+ <button class="btn" id="startRound" style="margin-top: 15px;">Start Round</button>
919
+ `;
920
+
921
+ document.querySelector('.game-container').appendChild(introElement);
922
+
923
+ document.getElementById('startRound').addEventListener('click', () => {
924
+ introElement.remove();
925
+ ball.active = true;
926
+ });
927
+ }
928
+
929
+ // Show stage modifier
930
+ function showModifier(modifier) {
931
+ const modifierElement = document.createElement('div');
932
+ modifierElement.className = 'modifier-indicator';
933
+ modifierElement.innerHTML = `
934
+ <div class="modifier-title">Stage Modifier: ${modifier.name}</div>
935
+ <div class="modifier-effect">${modifier.description}</div>
936
+ ${modifier.special ? `<div class="opponent-special">Effect: ${modifier.special}</div>` : ''}
937
+ <button class="btn" id="continueGame" style="margin-top: 15px;">Continue</button>
938
+ `;
939
+
940
+ document.querySelector('.game-container').appendChild(modifierElement);
941
+
942
+ document.getElementById('continueGame').addEventListener('click', () => {
943
+ modifierElement.remove();
944
+ if (!ball.active) {
945
+ // If the ball was waiting to be served
946
+ ball.active = true;
947
+ }
948
+ });
949
+ }
950
+
951
+ // Start the game
952
+ function startGame() {
953
+ resetGame();
954
+
955
+ mainMenu.style.display = 'none';
956
+ gameOverScreen.style.display = 'none';
957
+
958
+ gameRunning = true;
959
+ gameOver = false;
960
+
961
+ // Reset scores
962
+ gameState.playerScore = 0;
963
+ gameState.opponentScore = 0;
964
+ gameState.currentOpponent = 0;
965
+
966
+ updateScores();
967
+
968
+ // Start game loop
969
+ if (animationFrameId) {
970
+ cancelAnimationFrame(animationFrameId);
971
+ }
972
+
973
+ gameLoop();
974
+ }
975
+
976
+ // Main game loop
977
+ function gameLoop() {
978
+ if (!gameRunning) return;
979
+
980
+ update();
981
+ draw();
982
+
983
+ animationFrameId = requestAnimationFrame(gameLoop);
984
+ }
985
+
986
+ // Update game state
987
+ function update() {
988
+ if (!gameRunning) return;
989
+
990
+ gameState.frameCount++;
991
+
992
+ // Apply stage modifier if active
993
+ if (gameState.currentModifier) {
994
+ gameState.currentModifier.effect();
995
+ }
996
+
997
+ // Update active power-ups and check for expiration
998
+ updatePowerups();
999
+
1000
+ // Ball movement
1001
+ if (ball.active) {
1002
+ // Store current position for trail
1003
+ ball.trail.push({x: ball.x, y: ball.y});
1004
+ if (ball.trail.length > 10) {
1005
+ ball.trail.shift();
1006
+ }
1007
+
1008
+ // Move ball
1009
+ ball.x += ball.speedX;
1010
+ ball.y += ball.speedY;
1011
+
1012
+ // Ball collisions with walls
1013
+ if (ball.x + ball.size > GAME_WIDTH || ball.x < 0) {
1014
+ ball.speedX = -ball.speedX;
1015
+ ball.x = Math.max(0, Math.min(GAME_WIDTH - ball.size, ball.x));
1016
+ }
1017
+
1018
+ // Ball collisions with player paddle
1019
+ if (
1020
+ ball.y + ball.size > player.y &&
1021
+ ball.y < player.y + player.height &&
1022
+ ball.x + ball.size > player.x &&
1023
+ ball.x < player.x + player.width
1024
+ ) {
1025
+ // Calculate angle based on where it hits the paddle
1026
+ const hitPosition = (ball.x - player.x) / player.width;
1027
+ const angle = hitPosition * Math.PI - Math.PI / 2; // -90 to 90 degrees
1028
+
1029
+ ball.speedX = Math.sin(angle) * ball.speed;
1030
+ ball.speedY = -Math.cos(angle) * ball.speed;
1031
+
1032
+ // Ball magnet effect
1033
+ if (gameState.powerupsActive.magnet) {
1034
+ ball.speedY *= 0.8; // Reduce vertical speed to make it easier to hit back
1035
+ }
1036
+
1037
+ // Powerup spawn chance
1038
+ if (Math.random() < POWERUP_SPAWN_CHANCE) {
1039
+ spawnPowerup();
1040
+ }
1041
+ }
1042
+
1043
+ // Ball collisions with opponent paddle
1044
+ if (
1045
+ ball.y < opponent.y + opponent.height &&
1046
+ ball.y + ball.size > opponent.y &&
1047
+ ball.x + ball.size > opponent.x &&
1048
+ ball.x < opponent.x + opponent.width
1049
+ ) {
1050
+ // Basic AI reaction calculation
1051
+ const paddleCenter = opponent.x + opponent.width / 2;
1052
+ const ballCenter = ball.x + ball.size / 2;
1053
+ const distanceFromCenter = ballCenter - paddleCenter;
1054
+
1055
+ // Calculate angle based on where it hits the paddle (simplified for AI)
1056
+ const angle = (distanceFromCenter / (opponent.width / 2)) * (Math.PI / 3); // -60 to 60 degrees
1057
+
1058
+ ball.speedX = Math.sin(angle) * ball.speed;
1059
+ ball.speedY = Math.cos(angle) * ball.speed;
1060
+ }
1061
+
1062
+ // Ball out of bounds - scoring
1063
+ if (ball.y < 0) {
1064
+ // Player scores
1065
+ gameState.playerScore++;
1066
+ gameState.runStats.totalPoints++;
1067
+ updateScores();
1068
+ resetBall();
1069
+
1070
+ // Check for win
1071
+ if (gameState.playerScore >= winScore && gameState.playerScore - gameState.opponentScore >= 2) {
1072
+ roundWon();
1073
+ }
1074
+ } else if (ball.y + ball.size > GAME_HEIGHT) {
1075
+ // Opponent scores
1076
+ gameState.opponentScore++;
1077
+ updateScores();
1078
+ resetBall();
1079
+
1080
+ // Check for loss
1081
+ if (gameState.opponentScore >= winScore && gameState.opponentScore - gameState.playerScore >= 2) {
1082
+ roundLost();
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ // Opponent AI - simple tracking of ball with some imperfection
1088
+ if (ball.active && Math.random() > 0.3) { // 70% chance to react to allow for "human-like" imperfection
1089
+ const paddleCenter = opponent.x + opponent.width / 2;
1090
+ const ballFutureX = ball.x + ball.speedX * 5; // Predict future position slightly
1091
+ const targetX = ballFutureX - opponent.width / 2;
1092
+
1093
+ // Apply reaction time
1094
+ const reactionSpeed = opponent.reactionTime * opponent.speed;
1095
+
1096
+ if (paddleCenter < ballFutureX - 10) {
1097
+ opponent.x += reactionSpeed;
1098
+ } else if (paddleCenter > ballFutureX + 10) {
1099
+ opponent.x -= reactionSpeed;
1100
+ }
1101
+
1102
+ // Keep opponent within bounds
1103
+ opponent.x = Math.max(0, Math.min(GAME_WIDTH - opponent.width, opponent.x));
1104
+ }
1105
+
1106
+ // Move powerups
1107
+ updatePowerupPositions();
1108
+ }
1109
+
1110
+ // Update active powerups
1111
+ function updatePowerups() {
1112
+ const now = Date.now();
1113
+ for (const powerupId in gameState.powerupsActive) {
1114
+ if (gameState.powerupsActive[powerupId].endTime <= now) {
1115
+ removePowerup(powerupId);
1116
+ }
1117
+ }
1118
+
1119
+ // Update powerup HUD
1120
+ updatePowerupHud();
1121
+ }
1122
+
1123
+ // Update powerup positions and check for collection
1124
+ function updatePowerupPositions() {
1125
+ for (let i = powerups.length - 1; i >= 0; i--) {
1126
+ const powerup = powerups[i];
1127
+
1128
+ // Move powerup down
1129
+ powerup.y += 1;
1130
+
1131
+ // Check if collected by player
1132
+ if (
1133
+ powerup.y + POWERUP_SIZE > player.y &&
1134
+ powerup.y < player.y + player.height &&
1135
+ powerup.x + POWERUP_SIZE > player.x &&
1136
+ powerup.x < player.x + player.width
1137
+ ) {
1138
+ // Apply powerup effect
1139
+ applyPowerup(powerup.type);
1140
+
1141
+ // Remove from array
1142
+ powerups.splice(i, 1);
1143
+ gameState.runStats.powerupsCollected++;
1144
+
1145
+ // Show collection notification
1146
+ showPowerupNotification(powerupTypes.find(p => p.id === powerup.type).name);
1147
+ }
1148
+
1149
+ // Remove if out of bounds
1150
+ if (powerup.y > GAME_HEIGHT) {
1151
+ powerups.splice(i, 1);
1152
+ }
1153
+ }
1154
+ }
1155
+
1156
+ // Spawn a new powerup at random position
1157
+ function spawnPowerup(specificType = null) {
1158
+ const type = specificType || powerupTypes[Math.floor(Math.random() * powerupTypes.length)].id;
1159
+
1160
+ powerups.push({
1161
+ x: Math.random() * (GAME_WIDTH - POWERUP_SIZE - 20) + 10,
1162
+ y: 10,
1163
+ type: type,
1164
+ size: POWERUP_SIZE
1165
+ });
1166
+ }
1167
+
1168
+ // Apply a powerup effect
1169
+ function applyPowerup(powerupId) {
1170
+ const now = Date.now();
1171
+ let duration = POWERUP_DURATION;
1172
+
1173
+ // Apply duration upgrade
1174
+ if (playerMeta.upgrades.powerupDuration > 0) {
1175
+ duration *= 1 + 0.25 * playerMeta.upgrades.powerupDuration;
1176
+ }
1177
+
1178
+ // Handle powerup effects
1179
+ switch (powerupId) {
1180
+ case 'paddleXL':
1181
+ gameState.powerupsActive.paddleXL = {
1182
+ originalWidth: player.width,
1183
+ endTime: now + duration
1184
+ };
1185
+ player.width *= 1.5;
1186
+ break;
1187
+
1188
+ case 'paddleXS':
1189
+ gameState.powerupsActive.paddleXS = {
1190
+ originalWidth: player.width,
1191
+ endTime: now + duration
1192
+ };
1193
+ player.width *= 0.5;
1194
+ break;
1195
+
1196
+ case 'speedBoost':
1197
+ gameState.powerupsActive.speedBoost = {
1198
+ originalSpeed: ball.speed,
1199
+ endTime: now + duration
1200
+ };
1201
+ ball.speed *= 1.5;
1202
+ break;
1203
+
1204
+ case 'multiBall':
1205
+ // Spawn 3 extra balls
1206
+ for (let i = 0; i < 3; i++) {
1207
+ const angle = Math.random() * Math.PI * 2;
1208
+ const speed = ball.speed * (0.8 + Math.random() * 0.4);
1209
+
1210
+ powerups.push({
1211
+ x: ball.x,
1212
+ y: ball.y,
1213
+ type: 'extraBall',
1214
+ size: BALL_SIZE,
1215
+ speedX: Math.cos(angle) * speed,
1216
+ speedY: Math.sin(angle) * speed,
1217
+ active: true,
1218
+ lifetime: 300 // frames
1219
+ });
1220
+ }
1221
+ break;
1222
+
1223
+ case 'slowMotion':
1224
+ gameState.powerupsActive.slowMotion = {
1225
+ endTime: now + duration
1226
+ };
1227
+ // Effect handled in the update loop
1228
+ break;
1229
+
1230
+ case 'magnet':
1231
+ gameState.powerupsActive.magnet = {
1232
+ endTime: now + duration
1233
+ };
1234
+ break;
1235
+ }
1236
+ }
1237
+
1238
+ // Remove a powerup effect
1239
+ function removePowerup(powerupId) {
1240
+ switch (powerupId) {
1241
+ case 'paddleXL':
1242
+ case 'paddleXS':
1243
+ player.width = gameState.powerupsActive[powerupId].originalWidth;
1244
+ break;
1245
+
1246
+ case 'speedBoost':
1247
+ ball.speed = gameState.powerupsActive[powerupId].originalSpeed;
1248
+ break;
1249
+ }
1250
+
1251
+ delete gameState.powerupsActive[powerupId];
1252
+ updatePowerupHud();
1253
+ }
1254
+
1255
+ // Show powerup notification
1256
+ function showPowerupNotification(powerupName) {
1257
+ const notification = document.querySelector('.powerup-notification');
1258
+
1259
+ if (!notification) {
1260
+ const notifElement = document.createElement('div');
1261
+ notifElement.className = 'powerup-notification';
1262
+ notifElement.textContent = `Powerup Collected: ${powerupName}`;
1263
+ document.querySelector('.game-container').appendChild(notifElement);
1264
+
1265
+ setTimeout(() => {
1266
+ notifElement.classList.add('show');
1267
+ }, 10);
1268
+
1269
+ setTimeout(() => {
1270
+ notifElement.remove();
1271
+ }, 2000);
1272
+ } else {
1273
+ notification.textContent = `Powerup Collected: ${powerupName}`;
1274
+ notification.classList.remove('show');
1275
+
1276
+ setTimeout(() => {
1277
+ notification.classList.add('show');
1278
+ }, 10);
1279
+ }
1280
+ }
1281
+
1282
+ // Reset ball to center
1283
+ function resetBall() {
1284
+ ball.x = GAME_WIDTH / 2 - ball.size / 2;
1285
+ ball.y = GAME_HEIGHT / 2 - ball.size / 2;
1286
+
1287
+ // Initial direction - slightly randomized
1288
+ const angle = Math.random() * Math.PI / 2 - Math.PI / 4; // -45 to 45 degrees
1289
+ const speed = BALL_INITIAL_SPEED;
1290
+
1291
+ if (playerMeta.upgrades.ballStartSpeed > 0) {
1292
+ speed *= 1 - 0.1 * playerMeta.upgrades.ballStartSpeed;
1293
+ }
1294
+
1295
+ ball.speed = speed;
1296
+ ball.speedX = Math.sin(angle) * ball.speed;
1297
+ ball.speedY = Math.cos(angle) * ball.speed;
1298
+
1299
+ // Randomly choose who serves (positive or negative Y direction)
1300
+ if (Math.random() > 0.5) {
1301
+ ball.speedY = -ball.speedY;
1302
+ }
1303
+
1304
+ ball.active = false; // Wait for player input (space) to serve
1305
+ ball.trail = [];
1306
+ }
1307
+
1308
+ // Player won the round
1309
+ function roundWon() {
1310
+ gameState.runStats.opponentsDefeated++;
1311
+ gameState.runStats.roundsPlayed++;
1312
+
1313
+ // Next opponent
1314
+ gameState.currentOpponent++;
1315
+
1316
+ // Reset scores
1317
+ gameState.playerScore = 0;
1318
+ gameState.opponentScore = 0;
1319
+
1320
+ // Setup next opponent
1321
+ setupOpponent(gameState.currentOpponent);
1322
+
1323
+ // Reset ball (but don't serve yet)
1324
+ resetBall();
1325
+ updateScores();
1326
+ }
1327
+
1328
+ // Player lost the round
1329
+ function roundLost() {
1330
+ gameState.runStats.roundsPlayed++;
1331
+ gameOver = true;
1332
+ gameRunning = false;
1333
+
1334
+ // Calculate earned currency (1 per point + 10 per opponent defeated)
1335
+ const earnedCurrency = Math.floor(gameState.runStats.totalPoints + gameState.runStats.opponentsDefeated * 10);
1336
+ playerMeta.currency += earnedCurrency;
1337
+ saveGame();
1338
+
1339
+ // Update game over screen
1340
+ document.getElementById('stat-rounds').textContent = gameState.runStats.opponentsDefeated;
1341
+ document.getElementById('stat-score').textContent = gameState.runStats.totalPoints;
1342
+
1343
+ // Calculate time played
1344
+ const timePlayed = Math.floor((Date.now() - gameState.runStartTime) / 1000);
1345
+ const minutes = Math.floor(timePlayed / 60);
1346
+ const seconds = timePlayed % 60;
1347
+ document.getElementById('stat-time').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
1348
+
1349
+ document.getElementById('earned-currency').textContent = earnedCurrency;
1350
+
1351
+ // Show game over screen
1352
+ gameOverScreen.style.display = 'flex';
1353
+ }
1354
+
1355
+ // Update score display
1356
+ function updateScores() {
1357
+ document.querySelector('.player-score').textContent = `YOU: ${gameState.playerScore}`;
1358
+ document.querySelector('.opponent-score').textContent = `${opponent.name.toUpperCase().substring(0, 5)}: ${gameState.opponentScore}`;
1359
+ }
1360
+
1361
+ // Update powerup HUD display
1362
+ function updatePowerupHud() {
1363
+ const activePowerups = Object.keys(gameState.powerupsActive);
1364
+ const powerupElements = document.querySelectorAll('.powerup-icon');
1365
+
1366
+ // Reset all icons
1367
+ powerupElements.forEach(el => {
1368
+ el.textContent = '?';
1369
+ el.style.backgroundColor = 'rgba(76, 201, 240, 0.2)';
1370
+ el.classList.remove('active');
1371
+ });
1372
+
1373
+ // Update active icons
1374
+ activePowerups.forEach((powerupId, index) => {
1375
+ if (index < playerMeta.upgrades.powerupSlots) {
1376
+ const powerup = powerupTypes.find(p => p.id === powerupId);
1377
+ if (powerup) {
1378
+ powerupElements[index].textContent = powerup.icon;
1379
+ powerupElements[index].style.backgroundColor = powerup.color;
1380
+ powerupElements[index].classList.add('active');
1381
+
1382
+ // Calculate remaining time percentage
1383
+ const remaining = (gameState.powerupsActive[powerupId].endTime - Date.now()) / POWERUP_DURATION;
1384
+ powerupElements[index].style.backgroundImage =
1385
+ `linear-gradient(to top, ${powerup.color} ${remaining * 100}%, rgba(76, 201, 240, 0.2) ${remaining * 100}%)`;
1386
+ }
1387
+ }
1388
+ });
1389
+ }
1390
+
1391
+ // Draw everything
1392
+ function draw() {
1393
+ // Clear canvas
1394
+ ctx.fillStyle = '#14213D';
1395
+ ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
1396
+
1397
+ // Draw center line
1398
+ ctx.setLineDash([5, 10]);
1399
+ ctx.beginPath();
1400
+ ctx.moveTo(GAME_WIDTH / 2, 0);
1401
+ ctx.lineTo(GAME_WIDTH / 2, GAME_HEIGHT);
1402
+ ctx.strokeStyle = 'rgba(248, 249, 250, 0.5)';
1403
+ ctx.lineWidth = 2;
1404
+ ctx.stroke();
1405
+ ctx.setLineDash([]);
1406
+
1407
+ // Draw ball trail
1408
+ if (ball.active) {
1409
+ ball.trail.forEach((pos, i) => {
1410
+ const alpha = 0.1 + (i / ball.trail.length) * 0.4;
1411
+ ctx.fillStyle = `rgba(248, 249, 250, ${alpha})`;
1412
+ ctx.fillRect(pos.x, pos.y, ball.size, ball.size);
1413
+ });
1414
+ }
1415
+
1416
+ // Draw powerups
1417
+ powerups.forEach(powerup => {
1418
+ const powerupType = powerupTypes.find(p => p.id === powerup.type);
1419
+ if (powerupType) {
1420
+ ctx.fillStyle = powerupType.color;
1421
+ ctx.fillRect(powerup.x, powerup.y, powerup.size, powerup.size);
1422
+
1423
+ // Draw icon/text
1424
+ ctx.fillStyle = '#FFFFFF';
1425
+ ctx.font = '10px Arial';
1426
+ ctx.textAlign = 'center';
1427
+ ctx.textBaseline = 'middle';
1428
+ ctx.fillText(powerupType.icon, powerup.x + powerup.size / 2, powerup.y + powerup.size / 2);
1429
+ } else if (powerup.type === 'extraBall') {
1430
+ // Draw extra balls (from multi-ball)
1431
+ ctx.fillStyle = '#FFFFFF';
1432
+ ctx.fillRect(powerup.x, powerup.y, powerup.size, powerup.size);
1433
+ }
1434
+ });
1435
+
1436
+ // Draw ball
1437
+ ctx.fillStyle = ball.color;
1438
+ ctx.fillRect(ball.x, ball.y, ball.size, ball.size);
1439
+
1440
+ // Draw player paddle
1441
+ ctx.fillStyle = player.color;
1442
+ ctx.fillRect(player.x, player.y, player.width, player.height);
1443
+
1444
+ // Draw opponent paddle
1445
+ ctx.fillStyle = opponent.color;
1446
+ ctx.fillRect(opponent.x, opponent.y, opponent.width, opponent.height);
1447
+
1448
+ // Draw ball direction indicator when waiting to serve
1449
+ if (!ball.active) {
1450
+ ctx.setLineDash([3, 3]);
1451
+ ctx.beginPath();
1452
+ ctx.moveTo(ball.x + ball.size / 2, ball.y + ball.size / 2);
1453
+ ctx.lineTo(
1454
+ ball.x + ball.size / 2 + ball.speedX * 30,
1455
+ ball.y + ball.size / 2 + ball.speedY * 30
1456
+ );
1457
+ ctx.strokeStyle = 'rgba(248, 249, 250, 0.7)';
1458
+ ctx.lineWidth = 2;
1459
+ ctx.stroke();
1460
+ ctx.setLineDash([]);
1461
+
1462
+ // Draw serve prompt
1463
+ ctx.fillStyle = 'rgba(248, 249, 250, 0.8)';
1464
+ ctx.font = '16px "Press Start 2P"';
1465
+ ctx.textAlign = 'center';
1466
+ ctx.fillText('PRESS SPACE TO SERVE', GAME_WIDTH / 2, 50);
1467
+ }
1468
+
1469
+ // If game is over but not showing game over screen (transition)
1470
+ if (gameOver && gameOverScreen.style.display === 'none') {
1471
+ ctx.fillStyle = 'rgba(230, 57, 70, 0.7)';
1472
+ ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
1473
+
1474
+ ctx.fillStyle = '#FFFFFF';
1475
+ ctx.font = '30px "Press Start 2P"';
1476
+ ctx.textAlign = 'center';
1477
+ ctx.fillText('GAME OVER', GAME_WIDTH / 2, GAME_HEIGHT / 2);
1478
+ }
1479
+ }
1480
+
1481
+ // Keyboard controls
1482
+ let rightPressed = false;
1483
+ let leftPressed = false;
1484
+
1485
+ function keyDownHandler(e) {
1486
+ if (e.key === 'Right' || e.key === 'ArrowRight') {
1487
+ rightPressed = true;
1488
+ } else if (e.key === 'Left' || e.key === 'ArrowLeft') {
1489
+ leftPressed = true;
1490
+ } else if (e.key === ' ' && !ball.active) {
1491
+ // Serve ball on space
1492
+ if (gameRunning && !gameOver) {
1493
+ ball.active = true;
1494
+ }
1495
+ } else if (e.key === 'Escape') {
1496
+ if (gameRunning) {
1497
+ // Pause game
1498
+ gameRunning = false;
1499
+ } else if (gameOverScreen.style.display === 'flex') {
1500
+ showMainMenu();
1501
+ } else if (upgradesScreen.style.display === 'flex') {
1502
+ showMainMenu();
1503
+ } else {
1504
+ // Resume game from pause
1505
+ gameRunning = true;
1506
+ gameLoop();
1507
+ }
1508
+ }
1509
+ }
1510
+
1511
+ function keyUpHandler(e) {
1512
+ if (e.key === 'Right' || e.key === 'ArrowRight') {
1513
+ rightPressed = false;
1514
+ } else if (e.key === 'Left' || e.key === 'ArrowLeft') {
1515
+ leftPressed = false;
1516
+ }
1517
+ }
1518
+
1519
+ // Show main menu
1520
+ function showMainMenu(){
1521
+ console.log('show main menu');
1522
+ }
1523
+ </script>
1524
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">This website has been generated by <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body>
1525
+ </html>