samirerty commited on
Commit
0170441
·
verified ·
1 Parent(s): c8d5708

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +546 -897
index.html CHANGED
@@ -3,1035 +3,684 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>Neon Velocity - Cyberpunk Racer</title>
 
 
 
 
7
 
8
- <!-- Google Fonts for Typography -->
9
- <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@500;700&display=swap" rel="stylesheet">
12
-
13
  <style>
14
  :root {
15
- --bg-dark: #0F0C29;
16
- --bg-gradient: linear-gradient(to bottom, #0F0C29, #302B63, #24243e);
17
- --road-color: #1A1A2E;
18
- --road-line: #00FFE0;
19
- --neon-pink: #FF005C;
20
- --neon-blue: #00D4FF;
21
- --neon-gold: #FFD700;
22
- --neon-purple: #9D00FF;
23
- --text-color: #ffffff;
24
- --ui-bg: rgba(15, 12, 41, 0.9);
25
  }
26
 
27
  * {
28
  box-sizing: border-box;
29
  user-select: none;
30
- -webkit-user-select: none;
31
- touch-action: none;
32
  }
33
 
34
  body {
35
  margin: 0;
36
  padding: 0;
 
 
 
 
37
  width: 100vw;
38
  height: 100vh;
39
- background: var(--bg-gradient);
40
- font-family: 'Rajdhani', sans-serif;
41
- overflow: hidden;
42
  display: flex;
43
- justify-content: center;
44
  align-items: center;
45
- color: var(--text-color);
46
  }
47
 
48
- /* --- Header / Branding --- */
49
  header {
50
- position: absolute;
51
- top: 0;
52
- left: 0;
53
  width: 100%;
54
  padding: 10px 20px;
55
  display: flex;
56
  justify-content: space-between;
57
  align-items: center;
58
- z-index: 100;
59
- pointer-events: none; /* Let clicks pass through to game if needed */
 
 
60
  }
61
 
62
- .brand-link {
63
- font-family: 'Orbitron', sans-serif;
64
- font-size: 0.8rem;
65
- color: rgba(255, 255, 255, 0.5);
66
- text-decoration: none;
67
- pointer-events: auto;
68
- transition: color 0.3s;
69
  }
70
 
71
- .brand-link:hover {
72
- color: var(--neon-blue);
73
- text-shadow: 0 0 10px var(--neon-blue);
 
 
 
74
  }
 
75
 
76
- /* --- Game Container --- */
77
  #game-container {
78
  position: relative;
79
  width: 100%;
80
  height: 100%;
81
- max-width: 600px; /* Mobile-first vertical aspect ratio */
82
- background-color: var(--road-color);
83
- box-shadow: 0 0 50px rgba(0, 0, 0, 0.8);
84
- overflow: hidden;
85
  }
86
 
87
  canvas {
88
  display: block;
89
- width: 100%;
90
- height: 100%;
91
  }
92
 
93
- /* --- UI Overlay Screens --- */
94
- .screen {
95
  position: absolute;
96
  top: 0;
97
  left: 0;
98
  width: 100%;
99
  height: 100%;
 
100
  display: flex;
101
  flex-direction: column;
102
  justify-content: center;
103
  align-items: center;
104
- background: var(--ui-bg);
105
- backdrop-filter: blur(5px);
106
- z-index: 10;
107
- transition: opacity 0.5s ease;
108
  }
109
 
110
  .hidden {
111
  opacity: 0;
112
  pointer-events: none;
113
- z-index: -1;
114
  }
115
 
116
- h1 {
117
- font-family: 'Orbitron', sans-serif;
118
  font-size: 3rem;
119
- margin: 0 0 20px 0;
120
- background: linear-gradient(90deg, var(--neon-blue), var(--neon-pink));
121
- -webkit-background-clip: text;
122
- -webkit-text-fill-color: transparent;
123
- text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
124
  text-align: center;
125
- line-height: 1.1;
 
 
 
 
 
126
  }
127
 
128
  .btn {
129
- background: transparent;
130
- border: 2px solid var(--neon-blue);
131
- color: var(--neon-blue);
132
  padding: 15px 40px;
133
- font-family: 'Orbitron', sans-serif;
134
  font-size: 1.2rem;
 
 
135
  cursor: pointer;
136
  margin: 10px;
137
- transition: all 0.2s ease;
138
- text-transform: uppercase;
139
- box-shadow: 0 0 10px rgba(0, 212, 255, 0.2);
140
- position: relative;
141
- overflow: hidden;
142
  }
143
 
144
  .btn:hover {
145
- background: var(--neon-blue);
146
- color: #000;
147
- box-shadow: 0 0 30px rgba(0, 212, 255, 0.8);
148
  transform: scale(1.05);
 
149
  }
150
 
151
- .btn-secondary {
152
- border-color: var(--neon-pink);
153
- color: var(--neon-pink);
154
- box-shadow: 0 0 10px rgba(255, 0, 92, 0.2);
155
- }
156
-
157
- .btn-secondary:hover {
158
- background: var(--neon-pink);
159
- color: #fff;
160
- box-shadow: 0 0 30px rgba(255, 0, 92, 0.8);
161
  }
162
 
163
- .stats-display {
164
  display: flex;
165
- gap: 20px;
166
- margin: 20px 0;
167
- font-size: 1.5rem;
168
  }
169
 
170
- .stat-box {
171
- background: rgba(255, 255, 255, 0.1);
 
 
172
  padding: 10px 20px;
173
- border-radius: 5px;
174
- border-left: 3px solid var(--neon-gold);
175
- }
176
-
177
- .instructions {
178
- margin-top: 30px;
179
- text-align: center;
180
- color: rgba(255, 255, 255, 0.7);
181
- font-size: 0.9rem;
182
- line-height: 1.6;
183
  }
184
-
185
- .key-badge {
186
- display: inline-block;
187
- background: rgba(255, 255, 255, 0.2);
188
- padding: 2px 6px;
189
- border-radius: 4px;
190
- font-family: monospace;
191
- border: 1px solid rgba(255, 255, 255, 0.4);
192
  }
193
 
194
- /* --- HUD (Heads Up Display) --- */
195
  #hud {
196
  position: absolute;
197
- top: 0;
198
  left: 0;
199
  width: 100%;
200
- height: 100%;
 
 
201
  pointer-events: none;
202
  z-index: 5;
203
- display: none; /* Hidden on start screen */
204
  }
205
 
206
- .hud-panel {
207
- position: absolute;
208
- padding: 10px;
209
  }
210
 
211
- .hud-top-left {
212
- top: 10px;
213
- left: 10px;
214
  }
215
 
216
- .hud-top-right {
217
- top: 10px;
218
- right: 10px;
219
- text-align: right;
220
  }
221
 
222
- .hud-bottom {
223
- bottom: 20px;
 
224
  left: 50%;
225
  transform: translateX(-50%);
226
- width: 80%;
227
- }
228
-
229
- .label {
230
- font-size: 0.7rem;
231
- color: rgba(255, 255, 255, 0.6);
232
- text-transform: uppercase;
233
- letter-spacing: 1px;
234
- display: block;
235
- }
236
-
237
- .value {
238
- font-family: 'Orbitron', sans-serif;
239
- font-size: 1.5rem;
240
- color: #fff;
241
- text-shadow: 0 0 5px var(--neon-blue);
242
  }
243
 
244
- /* Speedometer */
245
- .speedometer {
246
- background: rgba(0, 0, 0, 0.5);
247
- border: 2px solid var(--neon-pink);
248
- border-radius: 50%;
249
- width: 80px;
250
- height: 80px;
251
  display: flex;
252
- flex-direction: column;
253
- justify-content: center;
254
- align-items: center;
255
- box-shadow: 0 0 15px rgba(255, 0, 92, 0.3);
256
- }
257
-
258
- .speed-val {
259
- font-size: 1.2rem;
260
- font-weight: bold;
261
- }
262
-
263
- .speed-unit {
264
- font-size: 0.6rem;
265
  }
266
-
267
- /* Bars */
268
- .bar-container {
269
- width: 200px;
270
- height: 10px;
271
- background: rgba(255, 255, 255, 0.2);
272
- margin-top: 5px;
273
  border-radius: 5px;
274
- overflow: hidden;
275
- position: relative;
276
  }
277
 
278
- .bar-fill {
279
- height: 100%;
280
- width: 100%;
281
- background: var(--neon-blue);
282
- transition: width 0.1s linear, background 0.3s;
283
- }
284
-
285
- .nitro-fill {
286
- background: var(--neon-gold);
287
- width: 0%;
288
- box-shadow: 0 0 10px var(--neon-gold);
289
- }
290
-
291
- /* --- Mobile Controls --- */
292
- .mobile-controls {
293
  position: absolute;
294
- bottom: 20px;
 
295
  width: 100%;
296
- display: none; /* Shown via JS on touch devices */
297
- justify-content: space-between;
298
- padding: 0 20px;
299
- pointer-events: none; /* Let clicks pass through empty space */
300
- }
301
-
302
- .control-btn {
303
- width: 70px;
304
- height: 70px;
305
- background: rgba(255, 255, 255, 0.1);
306
- border: 2px solid rgba(255, 255, 255, 0.3);
307
- border-radius: 50%;
308
- pointer-events: auto;
309
- display: flex;
310
- justify-content: center;
311
- align-items: center;
312
- font-size: 1.5rem;
313
- color: white;
314
- backdrop-filter: blur(2px);
315
- transition: background 0.1s;
316
- }
317
-
318
- .control-btn:active {
319
- background: rgba(0, 255, 224, 0.3);
320
- border-color: var(--neon-blue);
321
- }
322
-
323
- .control-group {
324
  display: flex;
325
- gap: 15px;
326
  }
327
-
328
- .nitro-btn {
329
- border-color: var(--neon-gold);
330
- }
331
- .nitro-btn:active {
332
- background: rgba(255, 215, 0, 0.3);
333
  }
334
 
335
- @media (max-width: 768px) {
336
- h1 { font-size: 2rem; }
337
- #game-container { max-width: 100%; border-radius: 0; }
338
- .mobile-controls { display: flex; }
 
339
  }
340
  </style>
341
  </head>
342
  <body>
343
 
344
  <header>
345
- <div class="brand-link">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: inherit; text-decoration: underline;">anycoder</a></div>
 
346
  </header>
347
 
348
- <main id="game-container">
349
  <canvas id="gameCanvas"></canvas>
350
 
351
- <!-- HUD Layer -->
352
- <div id="hud">
353
- <div class="hud-panel hud-top-left">
354
- <span class="label">Score</span>
355
- <span class="value" id="scoreVal">0</span>
356
- <div style="margin-top: 10px;">
357
- <span class="label">Distance</span>
358
- <span class="value" id="distVal">0m</span>
359
- </div>
360
  </div>
361
-
362
- <div class="hud-panel hud-top-right">
363
- <div class="speedometer">
364
- <span class="speed-val" id="speedVal">0</span>
365
- <span class="speed-unit">KM/H</span>
366
- </div>
367
  </div>
368
-
369
- <div class="hud-panel hud-bottom">
370
- <div style="display: flex; justify-content: space-between;">
371
- <span class="label">NITRO (SPACE)</span>
372
- <span class="label">FUEL</span>
373
- </div>
374
- <div class="bar-container">
375
- <div class="bar-fill nitro-fill" id="nitroBar"></div>
376
- </div>
377
- <div class="bar-container" style="height: 5px; margin-top: 2px;">
378
- <div class="bar-fill" id="fuelBar" style="background: var(--neon-pink);"></div>
379
- </div>
380
  </div>
 
381
 
382
- <!-- Touch Controls (Visible on Mobile) -->
383
- <div class="mobile-controls">
384
- <div class="control-group">
385
- <div class="control-btn" id="btnLeft">◄</div>
386
- <div class="control-btn" id="btnRight"></div>
387
- </div>
388
- <div class="control-btn nitro-btn" id="btnNitro">⚡</div>
389
- </div>
390
  </div>
391
 
392
  <!-- Start Screen -->
393
- <div id="start-screen" class="screen">
394
- <h1>NEON<br>VELOCITY</h1>
395
- <div class="stats-display">
396
- <div class="stat-box">
397
- <div class="label">High Score</div>
398
- <div class="value" id="highScoreStart">0</div>
399
- </div>
400
- </div>
401
 
402
- <button class="btn" onclick="game.start()">Start Race</button>
403
- <button class="btn btn-secondary" onclick="game.toggleSound()">Sound: ON</button>
404
-
405
- <div class="instructions">
406
- <p>Use <span class="key-badge">←</span> <span class="key-badge">→</span> to Steer</p>
407
- <p>Use <span class="key-badge">↑</span> <span class="key-badge">↓</span> to Control Speed</p>
408
- <p>Hold <span class="key-badge">SPACE</span> for Nitro Boost</p>
409
- <p>Avoid traffic & collect energy orbs!</p>
 
 
 
410
  </div>
411
  </div>
412
 
413
  <!-- Game Over Screen -->
414
- <div id="game-over-screen" class="screen hidden">
415
- <h1>CRASHED</h1>
416
- <div class="stats-display">
417
- <div class="stat-box">
418
- <div class="label">Final Score</div>
419
- <div class="value" id="finalScore">0</div>
420
- </div>
421
- <div class="stat-box">
422
- <div class="label">Distance</div>
423
- <div class="value" id="finalDist">0m</div>
424
- </div>
425
- </div>
426
-
427
- <button class="btn" onclick="game.restart()">Try Again</button>
428
- <button class="btn btn-secondary" onclick="game.goToMenu()">Main Menu</button>
429
  </div>
430
- </main>
431
-
432
- <script>
433
- /**
434
- * Game Constants & Configuration
435
- */
436
- const CONFIG = {
437
- laneCount: 4,
438
- roadWidthPercent: 0.9,
439
- baseSpeed: 10,
440
- maxSpeed: 25,
441
- nitroSpeed: 40,
442
- acceleration: 0.1,
443
- friction: 0.05,
444
- turnSpeed: 5,
445
- colors: {
446
- road: '#1A1A2E',
447
- line: '#00FFE0',
448
- grass: '#0F0C29',
449
- player: '#FF005C',
450
- enemy: ['#00D4FF', '#FFD700', '#9D00FF'],
451
- fuel: '#00FF00'
452
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  };
454
 
455
- /**
456
- * Sound System (Web Audio API)
457
- * Synthesizes sounds to avoid external assets.
458
- */
459
- class SoundManager {
460
- constructor() {
461
- this.enabled = true;
462
- this.ctx = new (window.AudioContext || window.webkitAudioContext)();
463
- this.engineOsc = null;
464
- this.engineGain = null;
465
- }
466
-
467
- initEngine() {
468
- if (!this.enabled) return;
469
- // Low frequency engine hum
470
- this.engineOsc = this.ctx.createOscillator();
471
- this.engineGain = this.ctx.createGain();
472
- this.engineOsc.type = 'sawtooth';
473
- this.engineOsc.frequency.value = 50;
474
- this.engineGain.gain.value = 0.05;
475
-
476
- this.engineOsc.connect(this.engineGain);
477
- this.engineGain.connect(this.ctx.destination);
478
- this.engineOsc.start();
479
- }
480
-
481
- updateEngine(speedRatio) {
482
- if (!this.enabled || !this.engineOsc) return;
483
- // Pitch shifts with speed
484
- const baseFreq = 50;
485
- const maxFreq = 150;
486
- this.engineOsc.frequency.setTargetAtTime(baseFreq + (speedRatio * maxFreq), this.ctx.currentTime, 0.1);
487
- }
488
-
489
- stopEngine() {
490
- if (this.engineOsc) {
491
- this.engineOsc.stop();
492
- this.engineOsc = null;
493
- }
494
- }
495
-
496
- playCrash() {
497
- if (!this.enabled) return;
498
- const osc = this.ctx.createOscillator();
499
- const gain = this.ctx.createGain();
500
- osc.type = 'square';
501
- osc.frequency.setValueAtTime(100, this.ctx.currentTime);
502
- osc.frequency.exponentialRampToValueAtTime(10, this.ctx.currentTime + 0.5);
503
- gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
504
- gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.5);
505
- osc.connect(gain);
506
- gain.connect(this.ctx.destination);
507
- osc.start();
508
- osc.stop(this.ctx.currentTime + 0.5);
509
- }
510
-
511
- playCollect() {
512
- if (!this.enabled) return;
513
- const osc = this.ctx.createOscillator();
514
- const gain = this.ctx.createGain();
515
- osc.type = 'sine';
516
- osc.frequency.setValueAtTime(600, this.ctx.currentTime);
517
- osc.frequency.linearRampToValueAtTime(1200, this.ctx.currentTime + 0.1);
518
- gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
519
- gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.1);
520
- osc.connect(gain);
521
- gain.connect(this.ctx.destination);
522
- osc.start();
523
- osc.stop(this.ctx.currentTime + 0.1);
524
- }
525
-
526
- toggle() {
527
- this.enabled = !this.enabled;
528
- return this.enabled;
529
- }
530
- }
531
-
532
- /**
533
- * Input Handling
534
- */
535
- class InputHandler {
536
- constructor() {
537
- this.keys = {
538
- ArrowLeft: false,
539
- ArrowRight: false,
540
- ArrowUp: false,
541
- ArrowDown: false,
542
- Space: false
543
- };
544
-
545
- window.addEventListener('keydown', (e) => {
546
- if(e.code === 'Space') this.keys.Space = true;
547
- if(e.key === 'ArrowLeft') this.keys.ArrowLeft = true;
548
- if(e.key === 'ArrowRight') this.keys.ArrowRight = true;
549
- if(e.key === 'ArrowUp') this.keys.ArrowUp = true;
550
- if(e.key === 'ArrowDown') this.keys.ArrowDown = true;
551
- });
552
-
553
- window.addEventListener('keyup', (e) => {
554
- if(e.code === 'Space') this.keys.Space = false;
555
- if(e.key === 'ArrowLeft') this.keys.ArrowLeft = false;
556
- if(e.key === 'ArrowRight') this.keys.ArrowRight = false;
557
- if(e.key === 'ArrowUp') this.keys.ArrowUp = false;
558
- if(e.key === 'ArrowDown') this.keys.ArrowDown = false;
559
- });
560
-
561
- // Touch Controls
562
- this.setupTouch('btnLeft', 'ArrowLeft');
563
- this.setupTouch('btnRight', 'ArrowRight');
564
- this.setupTouch('btnNitro', 'Space');
565
- }
566
-
567
- setupTouch(id, keyName) {
568
- const btn = document.getElementById(id);
569
- btn.addEventListener('touchstart', (e) => { e.preventDefault(); this.keys[keyName] = true; });
570
- btn.addEventListener('touchend', (e) => { e.preventDefault(); this.keys[keyName] = false; });
571
- }
572
- }
573
-
574
- /**
575
- * Game Entities
576
- */
577
- class Car {
578
- constructor(x, y, isPlayer = false) {
579
- this.x = x;
580
- this.y = y;
581
- this.width = 40;
582
- this.height = 70;
583
- this.isPlayer = isPlayer;
584
- this.speed = 0;
585
- this.color = isPlayer ? CONFIG.colors.player : CONFIG.colors.enemy[Math.floor(Math.random() * CONFIG.colors.enemy.length)];
586
- this.nitro = 100;
587
- this.fuel = 100;
588
- }
589
-
590
- draw(ctx) {
591
- ctx.save();
592
-
593
- // Glow effect
594
- ctx.shadowBlur = 15;
595
- ctx.shadowColor = this.color;
596
-
597
- // Body
598
- ctx.fillStyle = this.color;
599
- // Simple car shape
600
- ctx.beginPath();
601
- ctx.roundRect(this.x, this.y, this.width, this.height, 5);
602
- ctx.fill();
603
-
604
- // Roof / Windshield
605
- ctx.fillStyle = '#000';
606
- ctx.fillRect(this.x + 5, this.y + 15, this.width - 10, 25);
607
-
608
- // Tail lights
609
- ctx.fillStyle = '#ff0000';
610
- ctx.fillRect(this.x + 2, this.y + this.height - 5, 8, 5);
611
- ctx.fillRect(this.x + this.width - 10, this.y + this.height - 5, 8, 5);
612
-
613
- // Headlights
614
- ctx.fillStyle = '#fff';
615
- ctx.shadowColor = '#fff';
616
- ctx.fillRect(this.x + 2, this.y, 8, 5);
617
- ctx.fillRect(this.x + this.width - 10, this.y, 8, 5);
618
-
619
- ctx.restore();
620
- }
621
-
622
- getBounds() {
623
- // Slightly smaller hitbox for fairness
624
- return {
625
- x: this.x + 5,
626
- y: this.y + 5,
627
- width: this.width - 10,
628
- height: this.height - 10
629
- };
630
- }
631
- }
632
-
633
- class Particle {
634
- constructor(x, y, color, speed) {
635
- this.x = x;
636
- this.y = y;
637
- this.color = color;
638
- this.size = Math.random() * 3 + 1;
639
- this.speedY = speed + Math.random() * 5;
640
- this.life = 1.0;
641
- }
642
- update() {
643
- this.y += this.speedY;
644
- this.life -= 0.05;
645
- }
646
- draw(ctx) {
647
- ctx.globalAlpha = this.life;
648
- ctx.fillStyle = this.color;
649
- ctx.fillRect(this.x, this.y, this.size, this.size);
650
- ctx.globalAlpha = 1.0;
651
- }
652
- }
653
-
654
- /**
655
- * Main Game Engine
656
- */
657
- class Game {
658
- constructor() {
659
- this.canvas = document.getElementById('gameCanvas');
660
- this.ctx = this.canvas.getContext('2d');
661
- this.input = new InputHandler();
662
- this.sound = new SoundManager();
663
-
664
- this.state = 'MENU'; // MENU, PLAYING, GAMEOVER
665
- this.width = 0;
666
- this.height = 0;
667
- this.roadLeft = 0;
668
- this.roadWidth = 0;
669
-
670
- this.player = null;
671
- this.enemies = [];
672
- this.particles = [];
673
- this.powerups = [];
674
-
675
- this.score = 0;
676
- this.distance = 0;
677
- this.gameSpeed = 0;
678
- this.roadOffset = 0;
679
-
680
- this.highScore = localStorage.getItem('neonRacerHighScore') || 0;
681
-
682
- this.resize();
683
- window.addEventListener('resize', () => this.resize());
684
-
685
- document.getElementById('highScoreStart').innerText = Math.floor(this.highScore);
686
-
687
- this.loop = this.loop.bind(this);
688
- requestAnimationFrame(this.loop);
689
- }
690
-
691
- resize() {
692
- const container = document.getElementById('game-container');
693
- this.width = container.clientWidth;
694
- this.height = container.clientHeight;
695
- this.canvas.width = this.width;
696
- this.canvas.height = this.height;
697
-
698
- this.roadWidth = this.width * CONFIG.roadWidthPercent;
699
- this.roadLeft = (this.width - this.roadWidth) / 2;
700
-
701
- // Reposition player if exists
702
- if (this.player) {
703
- this.player.y = this.height - 150;
704
- }
705
- }
706
-
707
- start() {
708
- this.state = 'PLAYING';
709
- document.getElementById('start-screen').classList.add('hidden');
710
- document.getElementById('game-over-screen').classList.add('hidden');
711
- document.getElementById('hud').style.display = 'block';
712
-
713
- this.resetGame();
714
- this.sound.initEngine();
715
- }
716
-
717
- resetGame() {
718
- this.player = new Car(this.width / 2 - 20, this.height - 150, true);
719
- this.enemies = [];
720
- this.particles = [];
721
- this.powerups = [];
722
- this.score = 0;
723
- this.distance = 0;
724
- this.gameSpeed = CONFIG.baseSpeed;
725
- this.roadOffset = 0;
726
- }
727
-
728
- goToMenu() {
729
- this.state = 'MENU';
730
- this.sound.stopEngine();
731
- document.getElementById('game-over-screen').classList.add('hidden');
732
- document.getElementById('hud').style.display = 'none';
733
- document.getElementById('start-screen').classList.remove('hidden');
734
- document.getElementById('highScoreStart').innerText = Math.floor(this.highScore);
735
- }
736
-
737
- restart() {
738
- this.start();
739
- }
740
-
741
- toggleSound() {
742
- const isOn = this.sound.toggle();
743
- const btn = document.querySelector('.btn-secondary');
744
- btn.innerText = `Sound: ${isOn ? 'ON' : 'OFF'}`;
745
- }
746
-
747
- gameOver() {
748
- this.state = 'GAMEOVER';
749
- this.sound.playCrash();
750
- this.sound.stopEngine();
751
-
752
- if (this.score > this.highScore) {
753
- this.highScore = this.score;
754
- localStorage.setItem('neonRacerHighScore', this.highScore);
755
- }
756
-
757
- document.getElementById('finalScore').innerText = Math.floor(this.score);
758
- document.getElementById('finalDist').innerText = Math.floor(this.distance) + 'm';
759
- document.getElementById('game-over-screen').classList.remove('hidden');
760
- document.getElementById('hud').style.display = 'none';
761
- }
762
-
763
- spawnEnemy() {
764
- const laneWidth = this.roadWidth / CONFIG.laneCount;
765
- const lane = Math.floor(Math.random() * CONFIG.laneCount);
766
- const x = this.roadLeft + (lane * laneWidth) + (laneWidth - 40) / 2;
767
-
768
- // Ensure no overlap at spawn
769
- const tooClose = this.enemies.some(e => Math.abs(e.y - (-100)) < 150 && Math.abs(e.x - x) < 10);
770
-
771
- if (!tooClose) {
772
- const enemy = new Car(x, -100);
773
- // Enemies move slower than max speed
774
- enemy.speed = CONFIG.baseSpeed + Math.random() * 5;
775
- this.enemies.push(enemy);
776
- }
777
- }
778
-
779
- spawnPowerup() {
780
- const laneWidth = this.roadWidth / CONFIG.laneCount;
781
- const lane = Math.floor(Math.random() * CONFIG.laneCount);
782
- const x = this.roadLeft + (lane * laneWidth) + (laneWidth - 20) / 2;
783
-
784
- this.powerups.push({
785
- x: x,
786
- y: -100,
787
- width: 20,
788
- height: 20,
789
- type: Math.random() > 0.5 ? 'fuel' : 'nitro'
790
- });
791
- }
792
-
793
- update() {
794
- if (this.state !== 'PLAYING') return;
795
-
796
- // 1. Player Movement (Horizontal)
797
- if (this.input.keys.ArrowLeft) {
798
- this.player.x -= CONFIG.turnSpeed;
799
- }
800
- if (this.input.keys.ArrowRight) {
801
- this.player.x += CONFIG.turnSpeed;
802
- }
803
-
804
- // Road Boundaries
805
- if (this.player.x < this.roadLeft) this.player.x = this.roadLeft;
806
- if (this.player.x + this.player.width > this.roadLeft + this.roadWidth) {
807
- this.player.x = this.roadLeft + this.roadWidth - this.player.width;
808
- }
809
-
810
- // 2. Speed Control
811
- let targetSpeed = CONFIG.baseSpeed;
812
- this.player.fuel -= 0.02; // Constant fuel consumption
813
-
814
- if (this.input.keys.Space && this.player.nitro > 0) {
815
- targetSpeed = CONFIG.nitroSpeed;
816
- this.player.nitro -= 0.5;
817
- this.player.fuel -= 0.05;
818
- // Add particles
819
- this.particles.push(new Particle(
820
- this.player.x + this.player.width/2,
821
- this.player.y + this.player.height,
822
- '#00FFE0',
823
- this.gameSpeed
824
- ));
825
- } else if (this.input.keys.ArrowUp) {
826
- targetSpeed = CONFIG.maxSpeed;
827
- this.player.fuel -= 0.03;
828
- } else if (this.input.keys.ArrowDown) {
829
- targetSpeed = CONFIG.baseSpeed * 0.5;
830
- }
831
-
832
- // Nitro Regen
833
- if (!this.input.keys.Space && this.player.nitro < 100) {
834
- this.player.nitro += 0.1;
835
- }
836
-
837
- // Smooth Speed Transition
838
- this.gameSpeed += (targetSpeed - this.gameSpeed) * CONFIG.acceleration;
839
-
840
- // Check Fuel
841
- if (this.player.fuel <= 0) {
842
- this.gameSpeed = 0; // Car stops
843
- // Game over if stopped for too long? For now, just stop.
844
- }
845
-
846
- // 3. World Scrolling Logic
847
- this.roadOffset += this.gameSpeed;
848
- if (this.roadOffset >= 100) this.roadOffset = 0;
849
-
850
- this.distance += this.gameSpeed * 0.01;
851
- this.score += this.gameSpeed * 0.01;
852
-
853
- // 4. Enemy Logic
854
- if (Math.random() < 0.02 + (this.distance * 0.00001)) { // Spawn rate increases with distance
855
- this.spawnEnemy();
856
- }
857
-
858
- for (let i = this.enemies.length - 1; i >= 0; i--) {
859
- let e = this.enemies[i];
860
- // Move enemy down relative to player speed
861
- // If player is faster than enemy, enemy moves down. If player slower, enemy moves up (away).
862
- // Simplified: Everything moves down based on player speed, enemy has its own forward speed
863
- e.y += (this.gameSpeed - e.speed);
864
-
865
- if (e.y > this.height) {
866
- this.enemies.splice(i, 1);
867
- this.score += 50; // Overtake bonus
868
- } else if (e.y < -200) {
869
- this.enemies.splice(i, 1); // Remove if went off top
870
- } else {
871
- // Collision Check
872
- if (this.checkCollision(this.player, e)) {
873
- this.gameOver();
874
- }
875
- }
876
- }
877
-
878
- // 5. Powerups
879
- if (Math.random() < 0.005) this.spawnPowerup();
880
-
881
- for (let i = this.powerups.length - 1; i >= 0; i--) {
882
- let p = this.powerups[i];
883
- p.y += this.gameSpeed;
884
-
885
- if (p.y > this.height) {
886
- this.powerups.splice(i, 1);
887
- } else {
888
- // Simple box collision
889
- if (this.player.x < p.x + p.width &&
890
- this.player.x + this.player.width > p.x &&
891
- this.player.y < p.y + p.height &&
892
- this.player.y + this.player.height > p.y) {
893
-
894
- this.sound.playCollect();
895
- if (p.type === 'fuel') {
896
- this.player.fuel = Math.min(100, this.player.fuel + 20);
897
- this.score += 100;
898
- } else {
899
- this.player.nitro = Math.min(100, this.player.nitro + 30);
900
- this.score += 50;
901
- }
902
- this.powerups.splice(i, 1);
903
- }
904
- }
905
- }
906
-
907
- // 6. Particles
908
- for (let i = this.particles.length - 1; i >= 0; i--) {
909
- this.particles[i].update();
910
- if (this.particles[i].life <= 0) this.particles.splice(i, 1);
911
- }
912
-
913
- // 7. Update Audio
914
- this.sound.updateEngine(this.gameSpeed / CONFIG.nitroSpeed);
915
-
916
- // 8. Update HUD
917
- this.updateHUD();
918
- }
919
-
920
- checkCollision(rect1, rect2) {
921
- const b1 = rect1.getBounds();
922
- const b2 = rect2.getBounds();
923
- return (b1.x < b2.x + b2.width &&
924
- b1.x + b1.width > b2.x &&
925
- b1.y < b2.y + b2.height &&
926
- b1.y + b1.height > b2.y);
927
- }
928
-
929
- updateHUD() {
930
- document.getElementById('scoreVal').innerText = Math.floor(this.score);
931
- document.getElementById('distVal').innerText = Math.floor(this.distance) + 'm';
932
- document.getElementById('speedVal').innerText = Math.floor(this.gameSpeed * 10); // Fake KM/H
933
- document.getElementById('nitroBar').style.width = this.player.nitro + '%';
934
- document.getElementById('fuelBar').style.width = this.player.fuel + '%';
935
- }
936
-
937
- draw() {
938
- // Clear background
939
- this.ctx.fillStyle = CONFIG.colors.grass;
940
- this.ctx.fillRect(0, 0, this.width, this.height);
941
-
942
- // Draw Road
943
- this.ctx.fillStyle = CONFIG.colors.road;
944
- this.ctx.fillRect(this.roadLeft, 0, this.roadWidth, this.height);
945
-
946
- // Draw Road Borders (Neon)
947
- this.ctx.strokeStyle = CONFIG.colors.line;
948
- this.ctx.lineWidth = 4;
949
- this.ctx.shadowBlur = 10;
950
- this.ctx.shadowColor = CONFIG.colors.line;
951
-
952
- this.ctx.beginPath();
953
- this.ctx.moveTo(this.roadLeft, 0);
954
- this.ctx.lineTo(this.roadLeft, this.height);
955
- this.ctx.stroke();
956
-
957
- this.ctx.beginPath();
958
- this.ctx.moveTo(this.roadLeft + this.roadWidth, 0);
959
- this.ctx.lineTo(this.roadLeft + this.roadWidth, this.height);
960
- this.ctx.stroke();
961
-
962
- this.ctx.shadowBlur = 0; // Reset shadow
963
-
964
- // Draw Lane Markers (Moving)
965
- this.ctx.strokeStyle = 'rgba(0, 255, 224, 0.3)';
966
- this.ctx.lineWidth = 2;
967
- this.ctx.setLineDash([40, 60]);
968
- this.ctx.lineDashOffset = -this.roadOffset;
969
-
970
- const laneWidth = this.roadWidth / CONFIG.laneCount;
971
- for (let i = 1; i < CONFIG.laneCount; i++) {
972
- const x = this.roadLeft + i * laneWidth;
973
- this.ctx.beginPath();
974
- this.ctx.moveTo(x, 0);
975
- this.ctx.lineTo(x, this.height);
976
- this.ctx.stroke();
977
- }
978
- this.ctx.setLineDash([]); // Reset
979
-
980
- // Draw Powerups
981
- this.powerups.forEach(p => {
982
- this.ctx.fillStyle = p.type === 'fuel' ? '#00FF00' : '#FFD700';
983
- this.ctx.shadowBlur = 10;
984
- this.ctx.shadowColor = this.ctx.fillStyle;
985
- this.ctx.beginPath();
986
- this.ctx.arc(p.x + p.width/2, p.y + p.height/2, p.width/2, 0, Math.PI*2);
987
- this.ctx.fill();
988
- this.ctx.shadowBlur = 0;
989
-
990
- // Icon text
991
- this.ctx.fillStyle = '#000';
992
- this.ctx.font = '12px Arial';
993
- this.ctx.textAlign = 'center';
994
- this.ctx.textBaseline = 'middle';
995
- this.ctx.fillText(p.type === 'fuel' ? 'F' : 'N', p.x + p.width/2, p.y + p.height/2);
996
- });
997
-
998
- // Draw Enemies
999
- this.enemies.forEach(e => e.draw(this.ctx));
1000
-
1001
- // Draw Player
1002
- if (this.player && this.state !== 'GAMEOVER') {
1003
- this.player.draw(this.ctx);
1004
- }
1005
-
1006
- // Draw Particles
1007
- this.particles.forEach(p => p.draw(this.ctx));
1008
-
1009
- // Speed Lines Effect (when fast)
1010
- if (this.gameSpeed > 20) {
1011
- this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
1012
- this.ctx.lineWidth = 1;
1013
- for(let i=0; i<10; i++) {
1014
- const lx = Math.random() * this.width;
1015
- const ly = Math.random() * this.height;
1016
- const len = Math.random() * 50 + 20;
1017
- this.ctx.beginPath();
1018
- this.ctx.moveTo(lx, ly);
1019
- this.ctx.lineTo(lx, ly + len);
1020
- this.ctx.stroke();
1021
- }
1022
- }
1023
- }
1024
-
1025
- loop() {
1026
- this.update();
1027
- this.draw();
1028
- requestAnimationFrame(this.loop);
1029
- }
1030
- }
1031
-
1032
- // Initialize Game
1033
- const game = new Game();
1034
-
1035
- </script>
1036
- </body>
1037
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Axeman: Timber Battle</title>
7
+ <!-- Importing a nice font for the minimal look -->
8
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700;900&display=swap" rel="stylesheet">
9
+ <!-- FontAwesome for Icons -->
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
 
 
 
 
 
 
12
  <style>
13
  :root {
14
+ --bg-color: #1A1A2E;
15
+ --tree-color: #8B7355;
16
+ --cut-line: #FF2E63;
17
+ --player-color: #4ECDC4;
18
+ --axe-color: #C7C7C7;
19
+ --text-color: #FFFFFF;
20
+ --score-color: #FFD166;
21
+ --ui-overlay: rgba(26, 26, 46, 0.9);
22
+ --accent: #E94560;
 
23
  }
24
 
25
  * {
26
  box-sizing: border-box;
27
  user-select: none;
28
+ -webkit-tap-highlight-color: transparent;
 
29
  }
30
 
31
  body {
32
  margin: 0;
33
  padding: 0;
34
+ background-color: var(--bg-color);
35
+ color: var(--text-color);
36
+ font-family: 'Montserrat', sans-serif;
37
+ overflow: hidden;
38
  width: 100vw;
39
  height: 100vh;
 
 
 
40
  display: flex;
41
+ flex-direction: column;
42
  align-items: center;
 
43
  }
44
 
45
+ /* Header */
46
  header {
 
 
 
47
  width: 100%;
48
  padding: 10px 20px;
49
  display: flex;
50
  justify-content: space-between;
51
  align-items: center;
52
+ background: rgba(0,0,0,0.2);
53
+ position: absolute;
54
+ top: 0;
55
+ z-index: 10;
56
  }
57
 
58
+ h1 {
59
+ font-size: 1.2rem;
60
+ margin: 0;
61
+ font-weight: 900;
62
+ letter-spacing: 1px;
63
+ color: var(--player-color);
 
64
  }
65
 
66
+ .anycoder-link {
67
+ color: var(--text-color);
68
+ text-decoration: none;
69
+ font-size: 0.8rem;
70
+ opacity: 0.7;
71
+ transition: opacity 0.3s;
72
  }
73
+ .anycoder-link:hover { opacity: 1; text-decoration: underline; }
74
 
75
+ /* Game Container */
76
  #game-container {
77
  position: relative;
78
  width: 100%;
79
  height: 100%;
80
+ display: flex;
81
+ justify-content: center;
82
+ align-items: center;
 
83
  }
84
 
85
  canvas {
86
  display: block;
87
+ box-shadow: 0 0 50px rgba(0,0,0,0.5);
 
88
  }
89
 
90
+ /* UI Overlays */
91
+ .overlay {
92
  position: absolute;
93
  top: 0;
94
  left: 0;
95
  width: 100%;
96
  height: 100%;
97
+ background: var(--ui-overlay);
98
  display: flex;
99
  flex-direction: column;
100
  justify-content: center;
101
  align-items: center;
102
+ z-index: 20;
103
+ transition: opacity 0.3s ease;
 
 
104
  }
105
 
106
  .hidden {
107
  opacity: 0;
108
  pointer-events: none;
 
109
  }
110
 
111
+ .menu-title {
 
112
  font-size: 3rem;
113
+ font-weight: 900;
114
+ color: var(--cut-line);
115
+ text-transform: uppercase;
116
+ margin-bottom: 10px;
117
+ text-shadow: 2px 2px 0px var(--player-color);
118
  text-align: center;
119
+ }
120
+
121
+ .menu-subtitle {
122
+ font-size: 1rem;
123
+ color: var(--score-color);
124
+ margin-bottom: 30px;
125
  }
126
 
127
  .btn {
128
+ background: var(--accent);
129
+ color: white;
130
+ border: none;
131
  padding: 15px 40px;
 
132
  font-size: 1.2rem;
133
+ font-weight: bold;
134
+ border-radius: 50px;
135
  cursor: pointer;
136
  margin: 10px;
137
+ transition: transform 0.1s, background 0.2s;
138
+ box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4);
139
+ font-family: inherit;
140
+ min-width: 200px;
 
141
  }
142
 
143
  .btn:hover {
 
 
 
144
  transform: scale(1.05);
145
+ background: #ff5e78;
146
  }
147
 
148
+ .btn:active {
149
+ transform: scale(0.95);
 
 
 
 
 
 
 
 
150
  }
151
 
152
+ .mode-select {
153
  display: flex;
154
+ flex-direction: column;
155
+ gap: 10px;
156
+ margin-bottom: 20px;
157
  }
158
 
159
+ .mode-btn {
160
+ background: transparent;
161
+ border: 2px solid var(--player-color);
162
+ color: var(--player-color);
163
  padding: 10px 20px;
 
 
 
 
 
 
 
 
 
 
164
  }
165
+
166
+ .mode-btn.active {
167
+ background: var(--player-color);
168
+ color: var(--bg-color);
 
 
 
 
169
  }
170
 
171
+ /* HUD */
172
  #hud {
173
  position: absolute;
174
+ top: 60px;
175
  left: 0;
176
  width: 100%;
177
+ padding: 0 20px;
178
+ display: flex;
179
+ justify-content: space-between;
180
  pointer-events: none;
181
  z-index: 5;
 
182
  }
183
 
184
+ .hud-item {
185
+ text-align: center;
 
186
  }
187
 
188
+ .score-label {
189
+ font-size: 0.8rem;
190
+ opacity: 0.8;
191
  }
192
 
193
+ .score-value {
194
+ font-size: 2rem;
195
+ font-weight: 900;
196
+ color: var(--score-color);
197
  }
198
 
199
+ #combo-display {
200
+ position: absolute;
201
+ top: 20%;
202
  left: 50%;
203
  transform: translateX(-50%);
204
+ font-size: 2.5rem;
205
+ font-weight: 900;
206
+ color: var(--cut-line);
207
+ opacity: 0;
208
+ transition: opacity 0.2s;
209
+ text-shadow: 0 0 10px var(--cut-line);
210
+ pointer-events: none;
 
 
 
 
 
 
 
 
 
211
  }
212
 
213
+ .controls-hint {
214
+ margin-top: 20px;
 
 
 
 
 
215
  display: flex;
216
+ gap: 20px;
217
+ font-size: 0.9rem;
218
+ opacity: 0.7;
 
 
 
 
 
 
 
 
 
 
219
  }
220
+ .key {
221
+ border: 1px solid rgba(255,255,255,0.3);
222
+ padding: 5px 10px;
 
 
 
 
223
  border-radius: 5px;
 
 
224
  }
225
 
226
+ /* Touch Controls for Mobile */
227
+ #touch-zones {
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  position: absolute;
229
+ top: 0;
230
+ left: 0;
231
  width: 100%;
232
+ height: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  display: flex;
234
+ z-index: 2;
235
  }
236
+ .touch-zone {
237
+ flex: 1;
238
+ /* background: rgba(255,0,0,0.1); Debugging */
 
 
 
239
  }
240
 
241
+ /* Responsive Adjustments */
242
+ @media (max-width: 600px) {
243
+ .menu-title { font-size: 2rem; }
244
+ .btn { padding: 12px 30px; min-width: 160px; font-size: 1rem; }
245
+ .controls-hint { display: none; } /* Hide keyboard hints on mobile */
246
  }
247
  </style>
248
  </head>
249
  <body>
250
 
251
  <header>
252
+ <h1><i class="fas fa-tree"></i> AXEMAN</h1>
253
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a>
254
  </header>
255
 
256
+ <div id="game-container">
257
  <canvas id="gameCanvas"></canvas>
258
 
259
+ <!-- HUD -->
260
+ <div id="hud" class="hidden">
261
+ <div class="hud-item">
262
+ <div class="score-label">SCORE</div>
263
+ <div id="score-val" class="score-value">0</div>
 
 
 
 
264
  </div>
265
+ <div class="hud-item">
266
+ <div class="score-label">TIME</div>
267
+ <div id="time-val" class="score-value">--</div>
 
 
 
268
  </div>
269
+ <div class="hud-item">
270
+ <div class="score-label">BEST</div>
271
+ <div id="best-val" class="score-value">0</div>
 
 
 
 
 
 
 
 
 
272
  </div>
273
+ </div>
274
 
275
+ <div id="combo-display">COMBO x2</div>
276
+
277
+ <!-- Touch Zones (Invisible) -->
278
+ <div id="touch-zones" class="hidden">
279
+ <div class="touch-zone" id="touch-left"></div>
280
+ <div class="touch-zone" id="touch-right"></div>
 
 
281
  </div>
282
 
283
  <!-- Start Screen -->
284
+ <div id="start-screen" class="overlay">
285
+ <div class="menu-title">AXEMAN</div>
286
+ <div class="menu-subtitle">CUT FAST. DON'T MISTAKE.</div>
 
 
 
 
 
287
 
288
+ <div class="mode-select">
289
+ <button class="btn mode-btn active" data-mode="classic">CLASSIC</button>
290
+ <button class="btn mode-btn" data-mode="timeattack">TIME ATTACK (90s)</button>
291
+ <button class="btn mode-btn" data-mode="survival">SURVIVAL</button>
292
+ </div>
293
+
294
+ <button id="start-btn" class="btn">PLAY GAME</button>
295
+
296
+ <div class="controls-hint">
297
+ <span><span class="key">A</span> / <span class="key">←</span> Left</span>
298
+ <span><span class="key">D</span> / <span class="key">→</span> Right</span>
299
  </div>
300
  </div>
301
 
302
  <!-- Game Over Screen -->
303
+ <div id="game-over-screen" class="overlay hidden">
304
+ <h1 style="color: var(--cut-line); font-size: 3rem;">GAME OVER</h1>
305
+ <div style="font-size: 1.5rem; margin: 10px;">Score: <span id="final-score" style="color:var(--score-color)">0</span></div>
306
+ <div style="font-size: 1rem; margin-bottom: 30px;">Trees Cut: <span id="final-trees">0</span></div>
307
+ <button id="restart-btn" class="btn">TRY AGAIN</button>
308
+ <button id="menu-btn" class="btn" style="background:transparent; border:1px solid white; margin-top:10px;">MAIN MENU</button>
 
 
 
 
 
 
 
 
 
309
  </div>
310
+ </div>
311
+
312
+ <script>
313
+ /**
314
+ * AXEMAN GAME ENGINE
315
+ * Built with Vanilla JS, HTML5 Canvas, and Web Audio API
316
+ */
317
+
318
+ // --- Configuration ---
319
+ const CONFIG = {
320
+ colors: {
321
+ bg: '#1A1A2E',
322
+ tree: '#8B7355',
323
+ treeLight: '#A68B6B',
324
+ cutLine: '#FF2E63',
325
+ player: '#4ECDC4',
326
+ axe: '#C7C7C7',
327
+ gold: '#FFD166',
328
+ bird: '#FFFFFF',
329
+ ui: '#FFFFFF'
330
+ },
331
+ gravity: 0.5,
332
+ chopForce: 15,
333
+ treeBaseHeight: 400, // Starting height
334
+ segmentHeight: 40,
335
+ maxComboTime: 2000 // ms to keep combo
336
+ };
337
+
338
+ // --- Audio System (Web Audio API) ---
339
+ const AudioSys = {
340
+ ctx: null,
341
+ init: function() {
342
+ window.AudioContext = window.AudioContext || window.webkitAudioContext;
343
+ this.ctx = new AudioContext();
344
+ },
345
+ playTone: function(freq, type, duration, vol = 0.1) {
346
+ if (!this.ctx) return;
347
+ const osc = this.ctx.createOscillator();
348
+ const gain = this.ctx.createGain();
349
+ osc.type = type;
350
+ osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
351
+ gain.gain.setValueAtTime(vol, this.ctx.currentTime);
352
+ gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
353
+ osc.connect(gain);
354
+ gain.connect(this.ctx.destination);
355
+ osc.start();
356
+ osc.stop(this.ctx.currentTime + duration);
357
+ },
358
+ playNoise: function(duration, vol = 0.2) {
359
+ if (!this.ctx) return;
360
+ const bufferSize = this.ctx.sampleRate * duration;
361
+ const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
362
+ const data = buffer.getChannelData(0);
363
+ for (let i = 0; i < bufferSize; i++) {
364
+ data[i] = Math.random() * 2 - 1;
365
+ }
366
+ const noise = this.ctx.createBufferSource();
367
+ noise.buffer = buffer;
368
+ const gain = this.ctx.createGain();
369
+
370
+ // Lowpass filter for "thud" sound
371
+ const filter = this.ctx.createBiquadFilter();
372
+ filter.type = 'lowpass';
373
+ filter.frequency.value = 1000;
374
+
375
+ gain.gain.setValueAtTime(vol, this.ctx.currentTime);
376
+ gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
377
+
378
+ noise.connect(filter);
379
+ filter.connect(gain);
380
+ gain.connect(this.ctx.destination);
381
+ noise.start();
382
+ },
383
+ sfx: {
384
+ chop: () => {
385
+ AudioSys.playNoise(0.1, 0.3);
386
+ AudioSys.playTone(150, 'square', 0.1, 0.1);
387
+ },
388
+ hitBranch: () => {
389
+ AudioSys.playTone(100, 'sawtooth', 0.3, 0.3);
390
+ },
391
+ score: () => {
392
+ AudioSys.playTone(600, 'sine', 0.1, 0.1);
393
+ setTimeout(() => AudioSys.playTone(900, 'sine', 0.2, 0.1), 100);
394
+ },
395
+ combo: () => {
396
+ AudioSys.playTone(400, 'triangle', 0.1, 0.1);
397
+ setTimeout(() => AudioSys.playTone(600, 'triangle', 0.1, 0.1), 50);
398
+ setTimeout(() => AudioSys.playTone(800, 'triangle', 0.2, 0.1), 100);
399
+ },
400
+ powerup: () => {
401
+ AudioSys.playTone(300, 'sine', 0.1, 0.2);
402
+ AudioSys.playTone(600, 'sine', 0.3, 0.2);
403
+ },
404
+ gameOver: () => {
405
+ AudioSys.playTone(200, 'sawtooth', 1.0, 0.5);
406
+ setTimeout(() => AudioSys.playTone(100, 'sawtooth', 1.0, 0.5), 200);
407
+ }
408
+ }
409
+ };
410
+
411
+ // --- Game State Management ---
412
+ const Game = {
413
+ canvas: document.getElementById('gameCanvas'),
414
+ ctx: document.getElementById('gameCanvas').getContext('2d'),
415
+ width: 0,
416
+ height: 0,
417
+ state: 'MENU', // MENU, PLAYING, GAMEOVER
418
+ mode: 'classic', // classic, timeattack, survival
419
+ lastTime: 0,
420
+ score: 0,
421
+ bestScore: parseInt(localStorage.getItem('axeman_best')) || 0,
422
+ treesCut: 0,
423
+
424
+ // Mechanics
425
+ playerSide: 1, // 1 = Right, -1 = Left
426
+ lastChopSide: 0, // 0 = none, 1 = right, -1 = left
427
+ treeHP: 10,
428
+ treeMaxHP: 10,
429
+ treeWidth: 120,
430
+ timeRemaining: 0,
431
+ combo: 0,
432
+ comboTimer: 0,
433
+
434
+ // Entities
435
+ particles: [],
436
+ powerUps: [],
437
+ activePowerUp: null, // {type: 'GOLDEN_AXE', endTime: timestamp}
438
+ birds: [], // {x, y, side}
439
+
440
+ // Visual Shake
441
+ shake: 0,
442
+
443
+ init: function() {
444
+ this.resize();
445
+ window.addEventListener('resize', () => this.resize());
446
+ this.setupInputs();
447
+ this.loop(0);
448
+
449
+ // UI Updates
450
+ document.getElementById('best-val').innerText = this.bestScore;
451
+ },
452
+
453
+ resize: function() {
454
+ this.width = window.innerWidth;
455
+ this.height = window.innerHeight;
456
+ this.canvas.width = this.width;
457
+ this.canvas.height = this.height;
458
+ },
459
+
460
+ start: function(mode) {
461
+ AudioSys.init();
462
+ this.mode = mode;
463
+ this.state = 'PLAYING';
464
+ this.score = 0;
465
+ this.treesCut = 0;
466
+ this.combo = 0;
467
+ this.comboTimer = 0;
468
+ this.activePowerUp = null;
469
+ this.birds = [];
470
+ this.powerUps = [];
471
+
472
+ // Tree Setup
473
+ this.treeHP = 10;
474
+ this.treeMaxHP = 10;
475
+ this.lastChopSide = 0; // Reset so first hit is always safe
476
+
477
+ // Mode Setup
478
+ if (mode === 'timeattack') {
479
+ this.timeRemaining = 90;
480
+ } else if (mode === 'classic') {
481
+ this.timeRemaining = 10; // Initial buffer
482
+ } else {
483
+ this.timeRemaining = 9999;
484
+ }
485
+
486
+ // UI
487
+ document.getElementById('start-screen').classList.add('hidden');
488
+ document.getElementById('game-over-screen').classList.add('hidden');
489
+ document.getElementById('hud').classList.remove('hidden');
490
+ document.getElementById('touch-zones').classList.remove('hidden');
491
+
492
+ this.updateHUD();
493
+ },
494
+
495
+ endGame: function() {
496
+ this.state = 'GAMEOVER';
497
+ AudioSys.sfx.gameOver();
498
+
499
+ if (this.score > this.bestScore) {
500
+ this.bestScore = this.score;
501
+ localStorage.setItem('axeman_best', this.bestScore);
502
+ }
503
+
504
+ document.getElementById('game-over-screen').classList.remove('hidden');
505
+ document.getElementById('hud').classList.add('hidden');
506
+ document.getElementById('touch-zones').classList.add('hidden');
507
+
508
+ document.getElementById('final-score').innerText = this.score;
509
+ document.getElementById('final-trees').innerText = this.treesCut;
510
+ },
511
+
512
+ setupInputs: function() {
513
+ // Keyboard
514
+ window.addEventListener('keydown', (e) => {
515
+ if (this.state !== 'PLAYING') return;
516
+ if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') this.chop(-1);
517
+ if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') this.chop(1);
518
+ });
519
+
520
+ // Touch
521
+ const leftZone = document.getElementById('touch-left');
522
+ const rightZone = document.getElementById('touch-right');
523
+
524
+ const handleTouch = (side) => (e) => {
525
+ e.preventDefault();
526
+ if (this.state === 'PLAYING') this.chop(side);
527
  };
528
 
529
+ leftZone.addEventListener('touchstart', handleTouch(-1));
530
+ rightZone.addEventListener('touchstart', handleTouch(1));
531
+
532
+ // Mode Selection
533
+ document.querySelectorAll('.mode-btn').forEach(btn => {
534
+ btn.addEventListener('click', () => {
535
+ document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
536
+ btn.classList.add('active');
537
+ this.mode = btn.dataset.mode;
538
+ });
539
+ });
540
+
541
+ // Menu Buttons
542
+ document.getElementById('start-btn').addEventListener('click', () => this.start(this.mode));
543
+ document.getElementById('restart-btn').addEventListener('click', () => this.start(this.mode));
544
+ document.getElementById('menu-btn').addEventListener('click', () => {
545
+ document.getElementById('game-over-screen').classList.add('hidden');
546
+ document.getElementById('start-screen').classList.remove('hidden');
547
+ this.state = 'MENU';
548
+ });
549
+ },
550
+
551
+ chop: function(side) {
552
+ // 1. Validate Rule: Don't hit same side twice
553
+ if (this.lastChopSide === side) {
554
+ this.triggerShake(20);
555
+ AudioSys.sfx.hitBranch();
556
+ this.endGame();
557
+ return;
558
+ }
559
+
560
+ this.lastChopSide = side;
561
+ this.playerSide = side;
562
+
563
+ // 2. Audio & Visuals
564
+ AudioSys.sfx.chop();
565
+ this.spawnParticles(side);
566
+ this.triggerShake(5);
567
+
568
+ // 3. Logic
569
+ let damage = 1;
570
+ if (this.activePowerUp && this.activePowerUp.type === 'GOLDEN_AXE') damage = 2;
571
+
572
+ // Check for obstacles on this segment (Bird)
573
+ // Simplified: Random chance bird spawns on current hit side
574
+ // If bird exists, damage is 0 or negative? Let's say bird needs 2 hits.
575
+ // For simplicity: Bird just adds HP to tree if present.
576
+ const birdIndex = this.birds.findIndex(b => b.side === side && Math.abs(b.y - (this.height - 150)) < 50);
577
+ if (birdIndex !== -1) {
578
+ AudioSys.playTone(800, 'square', 0.1); // Tweet
579
+ this.birds.splice(birdIndex, 1);
580
+ this.treeHP += 1; // Penalty: extra hit needed
581
+ this.shake = 10;
582
+ }
583
+
584
+ this.treeHP -= damage;
585
+
586
+ // 4. Combo System
587
+ this.combo++;
588
+ this.comboTimer = CONFIG.maxComboTime;
589
+
590
+ // 5. Score
591
+ let points = 10 + (this.combo * 2);
592
+ if (this.activePowerUp && this.activePowerUp.type === 'MAGNET') points *= 2;
593
+ this.score += points;
594
+
595
+ // 6. Tree Logic
596
+ if (this.treeHP <= 0) {
597
+ this.treeDown();
598
+ }
599
+
600
+ // 7. Time Management (Classic Mode)
601
+ if (this.mode === 'classic') {
602
+ this.timeRemaining = Math.min(this.timeRemaining + 1, 10);
603
+ }
604
+
605
+ this.updateHUD();
606
+ },
607
+
608
+ treeDown: function() {
609
+ AudioSys.sfx.score();
610
+ this.treesCut++;
611
+
612
+ // Increase difficulty
613
+ const speedBoost = Math.min(5, Math.floor(this.treesCut / 2));
614
+ this.treeMaxHP = Math.min(20, 10 + speedBoost);
615
+
616
+ this.treeHP = this.treeMaxHP;
617
+ this.lastChopSide = 0; // Reset side restriction for new tree
618
+
619
+ // Visual: Tree falls
620
+ this.triggerShake(10);
621
+
622
+ // Chance for PowerUp
623
+ if (Math.random() < 0.2) this.spawnPowerUp();
624
+
625
+ // Spawn new bird occasionally
626
+ if (Math.random() < 0.3) {
627
+ this.birds.push({
628
+ side: Math.random() > 0.5 ? 1 : -1,
629
+ y: this.height - 200 - Math.random() * 200
630
+ });
631
+ }
632
+ },
633
+
634
+ spawnPowerUp: function() {
635
+ const types = ['GOLDEN_AXE', 'SLOW_TIME', 'COMBO_LOCK', 'MAGNET'];
636
+ const type = types[Math.floor(Math.random() * types.length)];
637
+ this.powerUps.push({
638
+ x: this.width / 2 + (Math.random() * 100 - 50),
639
+ y: -50,
640
+ type: type,
641
+ active: true
642
+ });
643
+ },
644
+
645
+ activatePowerUp: function(type) {
646
+ AudioSys.sfx.powerup();
647
+ const duration = 10000; // 10s
648
+
649
+ if (type === 'SLOW_TIME') {
650
+ // Logic handled in update
651
+ }
652
+
653
+ this.activePowerUp = {
654
+ type: type,
655
+ endTime: Date.now() + duration
656
+ };
657
+ },
658
+
659
+ spawnParticles: function(side) {
660
+ const count = 10;
661
+ const originX = this.width / 2 + (side * (this.treeWidth / 2));
662
+ const originY = this.height - 180;
663
+
664
+ for (let i = 0; i < count; i++) {
665
+ this.particles.push({
666
+ x: originX,
667
+ y: originY,
668
+ vx: (side * Math.random() * 5) + (Math.random() - 0.5),
669
+ vy: -Math.random() * 10,
670
+ life: 1.0,
671
+ color: CONFIG.colors.treeLight
672
+ });
673
+ }
674
+ },
675
+
676
+ triggerShake: function(intensity) {
677
+ this.shake = intensity;
678
+ },
679
+
680
+ updateHUD: function() {
681
+ document.getElementById('score-val').innerText = this.score;
682
+
683
+ let timeDisplay = "";
684
+ if (this.mode === 'timeattack') {
685
+ timeDisplay = Math.ceil(this.timeRemaining) + "s";
686
+ } else if (this.mode