zxciop commited on
Commit
be26a77
·
verified ·
1 Parent(s): fed4185

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +692 -891
templates/index.html CHANGED
@@ -211,990 +211,791 @@
211
  }
212
  }
213
  </script>
214
-
215
-
216
- <script type="module">
217
- import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
218
- import { EffectComposer } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js';
219
- import { RenderPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/RenderPass.js';
220
- import { UnrealBloomPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/UnrealBloomPass.js';
221
 
222
- // Game configuration
223
- const CONFIG = {
224
- GRID_SIZE: 20, // Number of units across/deep
225
- CELL_SIZE: 1, // Size of each grid cell/snake segment
226
- BASE_SPEED: 150, // Base milliseconds between updates
227
- DIFFICULTY_LEVELS: {
228
- 'EASY': { speedMultiplier: 1.3, obstacleMultiplier: 0.5 },
229
- 'NORMAL': { speedMultiplier: 1.0, obstacleMultiplier: 1.0 },
230
- 'HARD': { speedMultiplier: 0.7, obstacleMultiplier: 1.5 }
231
- },
232
- MAX_OBSTACLE_COUNT: 10, // Maximum number of obstacles
233
- FOOD_TYPES: [
234
- { type: 'regular', color: 0x00ff00, points: 1, speedEffect: 0 },
235
- { type: 'special', color: 0x00ffff, points: 5, speedEffect: -10 },
236
- { type: 'rare', color: 0xff00ff, points: 10, speedEffect: 10 }
237
- ],
238
- COMBO_TIMEOUT: 5000, // Milliseconds to get next food for combo
239
- HIGH_SCORES_COUNT: 5 // Number of high scores to save
240
- };
241
 
242
- // --- Particle System for Effects ---
243
- class ParticleSystem {
244
- constructor(scene) {
245
- this.scene = scene;
246
- this.particles = [];
247
-
248
- // Shared geometry for all particles
249
- this.geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
250
- }
251
-
252
- createFoodEffect(position, color) {
253
- const count = 20; // Number of particles
254
-
255
- for (let i = 0; i < count; i++) {
256
- // Create particle
257
- const material = new THREE.MeshBasicMaterial({
258
- color: color || 0x00ff00,
259
- transparent: true,
260
- opacity: 0.9
261
- });
262
-
263
- const particle = new THREE.Mesh(this.geometry, material);
264
-
265
- // Set initial position
266
- particle.position.copy(position);
267
-
268
- // Set random velocity
269
- const velocity = new THREE.Vector3(
270
- (Math.random() - 0.5) * 0.1,
271
- (Math.random()) * 0.1,
272
- (Math.random() - 0.5) * 0.1
273
- );
274
-
275
- // Add to scene
276
- this.scene.add(particle);
277
-
278
- // Store particle data
279
- this.particles.push({
280
- mesh: particle,
281
- velocity: velocity,
282
- life: 1.0, // Life from 1.0 to 0.0
283
- decay: 0.02 + Math.random() * 0.03 // Random decay rate
284
- });
285
- }
286
- }
287
-
288
- update() {
289
- // Update all particles
290
- for (let i = this.particles.length - 1; i >= 0; i--) {
291
- const particle = this.particles[i];
292
-
293
- // Update position
294
- particle.mesh.position.add(particle.velocity);
295
-
296
- // Simulate gravity
297
- particle.velocity.y -= 0.003;
298
-
299
- // Update life
300
- particle.life -= particle.decay;
301
-
302
- // Update opacity based on life
303
- particle.mesh.material.opacity = particle.life;
304
-
305
- // Remove dead particles
306
- if (particle.life <= 0) {
307
- this.scene.remove(particle.mesh);
308
- particle.mesh.material.dispose();
309
- this.particles.splice(i, 1);
310
- }
311
- }
312
  }
313
-
314
- clear() {
315
- // Remove all particles
316
- for (const particle of this.particles) {
 
 
 
 
 
 
 
317
  this.scene.remove(particle.mesh);
318
  particle.mesh.material.dispose();
319
- particle.mesh.geometry.dispose();
 
320
  }
321
- this.particles = [];
322
  }
323
  }
 
 
 
 
 
 
 
 
 
 
 
324
 
325
- // Game state management
326
- const GameState = {
327
- MENU: 'menu',
328
- PLAYING: 'playing',
329
- PAUSED: 'paused',
330
- GAME_OVER: 'gameOver',
331
- currentState: 'menu',
332
-
333
- changeState(newState) {
334
- this.currentState = newState;
335
-
336
- // Handle UI changes based on state
337
- switch(newState) {
338
- case this.MENU:
339
- document.getElementById('gameScreen').style.display = 'flex';
340
- document.getElementById('startScreen').style.display = 'block';
341
- document.getElementById('gameOverScreen').style.display = 'none';
342
- break;
343
- case this.PLAYING:
344
- document.getElementById('gameScreen').style.display = 'none';
345
- break;
346
- case this.PAUSED:
347
- document.getElementById('gameScreen').style.display = 'flex';
348
- document.getElementById('startScreen').style.display = 'none';
349
- document.getElementById('gameOverScreen').style.display = 'none';
350
- document.getElementById('pauseScreen').style.display = 'block';
351
- break;
352
- case this.GAME_OVER:
353
- document.getElementById('gameScreen').style.display = 'flex';
354
- document.getElementById('startScreen').style.display = 'none';
355
- document.getElementById('gameOverScreen').style.display = 'block';
356
- // The score will be updated by the game instance when it triggers game over
357
- document.getElementById('gameOverSound').play();
358
- break;
359
- }
360
  }
361
- };
362
-
363
- // --- Matrix Rain Background Effect ---
364
- class MatrixRain {
365
- constructor() {
366
- this.canvas = document.getElementById('matrixCanvas');
367
- this.ctx = this.canvas.getContext('2d');
368
- this.resize();
369
-
370
- this.fontSize = 14;
371
- this.columns = Math.floor(this.canvas.width / this.fontSize);
372
- this.drops = [];
373
- this.characters = '01アイウエオカキクケコサシスセソタチツテトナニヌネ<>{}[]()+-*/%=#@&?*:・゚✧ ≡ ░▒░▒░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█䷀ ▙⁞ ░▒▓█║│ ·▓▒░█▄▀■■▄▬▌▐ ⁞▏▄▀■■▄▬▌▐ ▄▀■■▄▬▌▐ . ▛ ⁞▏ ▏ ⁚⁝ .';
374
-
375
- this.resetDrops();
376
-
377
- this.animate = this.animate.bind(this);
378
- this.animate();
379
 
380
- window.addEventListener('resize', this.handleResize.bind(this));
381
- }
 
 
 
 
382
 
383
- handleResize() {
384
- this.resize();
385
- this.columns = Math.floor(this.canvas.width / this.fontSize);
386
- this.resetDrops();
387
- }
 
 
 
 
 
 
 
 
 
 
388
 
389
- resize() {
390
- this.canvas.width = window.innerWidth;
391
- this.canvas.height = window.innerHeight;
392
- }
393
-
394
- resetDrops() {
395
- this.drops = [];
396
- for(let i = 0; i < this.columns; i++) {
397
- // Start drops at random negative positions for staggered effect
398
- this.drops[i] = Math.floor(Math.random() * -100);
399
- }
400
  }
401
-
402
- animate() {
403
- // Check if the game is in menu, paused, OR game over state
404
- if (GameState.currentState === GameState.MENU ||
405
- GameState.currentState === GameState.PAUSED ||
406
- GameState.currentState === GameState.GAME_OVER ||
407
- GameState.currentState === GameState.PLAYING) { // Added PLAYING back, assuming you still want it there!
 
408
 
409
- // Semi-transparent background to create fade effect
410
- this.ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
411
- this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
412
-
413
- this.ctx.fillStyle = '#0f0'; // Or your matrix color
414
- this.ctx.font = this.fontSize + 'px monospace';
 
 
 
415
 
416
- for(let i = 0; i < this.drops.length; i++) {
417
- const text = this.characters.charAt(Math.floor(Math.random() * this.characters.length));
418
- this.ctx.fillText(text, i * this.fontSize, this.drops[i] * this.fontSize);
419
-
420
- if(this.drops[i] * this.fontSize > this.canvas.height && Math.random() > 0.975) {
421
- this.drops[i] = 0;
422
- }
423
- this.drops[i]++;
424
  }
 
425
  }
426
- requestAnimationFrame(this.animate);
427
  }
 
 
 
428
 
429
-
430
- // --- Object pooling for performance optimization ---
431
- class ObjectPool {
432
- constructor(createFunc, initialCount = 10) {
433
- this.pool = [];
434
- this.createFunc = createFunc;
435
-
436
- // Populate the pool initially
437
- for (let i = 0; i < initialCount; i++) {
438
- this.pool.push(this.createFunc());
439
- }
440
- }
441
 
442
- get() {
443
- if (this.pool.length > 0) {
444
- return this.pool.pop();
445
- }
446
- return this.createFunc();
447
  }
448
-
449
- release(object) {
450
- this.pool.push(object);
 
 
451
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
 
453
- clear() {
454
- this.pool = [];
455
- }
 
 
456
  }
 
 
 
 
457
 
458
- // --- Main Game Class ---
459
- class SnakeGame {
460
- constructor() {
461
- // Initialize properties
462
- this.scene = null;
463
- this.camera = null;
464
- this.renderer = null;
465
- this.snake = [];
466
- this.food = null;
467
- this.obstacles = [];
468
- this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
469
- this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
470
- this.score = 0;
471
- this.highScore = this.loadHighScores()[0]?.score || 0;
472
- this.gameSpeed = CONFIG.BASE_SPEED;
473
- this.lastUpdateTime = 0;
474
- this.isGameOver = false;
475
- this.isPaused = false;
476
- this.gameLoopId = null;
477
- this.bounds = Math.floor(CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE;
478
- this.obstacleCount = CONFIG.MAX_OBSTACLE_COUNT;
479
- this.comboCount = 0;
480
- this.lastFoodTime = 0;
481
- this.currentDifficulty = 'NORMAL';
482
- this.particleSystem = null;
483
- this.headLight = null;
484
 
485
- // Initialize materials
486
- this.materials = {
487
- snakeHead: new THREE.MeshStandardMaterial({
488
- color: 0x0000ff,
489
- emissive: 0x39FF14,
490
- roughness: 0.8,
491
- metalness: 0.22
492
- }),
493
- snakeBody: new THREE.MeshStandardMaterial({
494
- color: 0x00ff00,
495
- emissive: 0x005500,
496
- roughness: 0.3,
497
- metalness: 0.72
498
- }),
499
- food: new THREE.MeshBasicMaterial({
500
- color: 0x00ff00,
501
- wireframe: true
502
- }),
503
- obstacle: new THREE.MeshBasicMaterial({
504
- color: 0x008800,
505
- wireframe: true
506
- }),
507
- specialFood: new THREE.MeshBasicMaterial({
508
- color: 0x00ffff,
509
- wireframe: true
510
- }),
511
- rareFood: new THREE.MeshBasicMaterial({
512
- color: 0xff00ff,
513
- wireframe: true
514
- })
515
- };
516
 
517
- // Initialize geometries
518
- this.geometries = {
519
- segment: new THREE.BoxGeometry(
520
- CONFIG.CELL_SIZE,
521
- CONFIG.CELL_SIZE,
522
- CONFIG.CELL_SIZE
523
- ),
524
- foodBox: new THREE.BoxGeometry(
525
- CONFIG.CELL_SIZE * 0.8,
526
- CONFIG.CELL_SIZE * 0.8,
527
- CONFIG.CELL_SIZE * 0.8
528
- ),
529
- foodSphere: new THREE.SphereGeometry(
530
- CONFIG.CELL_SIZE * 0.5,
531
- 16,
532
- 12
533
- ),
534
- foodTetrahedron: new THREE.TetrahedronGeometry(
535
- CONFIG.CELL_SIZE * 0.6,
536
- 0
537
- ),
538
- obstacle: new THREE.BoxGeometry(
539
- CONFIG.CELL_SIZE,
540
- CONFIG.CELL_SIZE * 1.5,
541
- CONFIG.CELL_SIZE
542
- )
543
- };
544
 
545
- // Initialize object pools
546
- this.segmentPool = new ObjectPool(() => {
547
- return new THREE.Mesh(this.geometries.segment, this.materials.snakeBody.clone());
548
- }, 20);
549
 
550
- this.obstaclePool = new ObjectPool(() => {
551
- return new THREE.Mesh(this.geometries.obstacle, this.materials.obstacle);
552
- }, CONFIG.MAX_OBSTACLE_COUNT * 1.5);
553
 
554
- // Initialize the game
555
- this.setupEventListeners();
556
- this.init();
557
-
558
- // Create the matrix rain effect
559
- this.matrixRain = new MatrixRain();
560
-
561
- // Update high scores display
562
- this.updateHighScoresTable();
563
- }
 
 
 
564
 
565
- // Place food at random position
566
- // Place food at random position
567
- placeFood() {
568
- let foodPos;
569
- let validPosition = false;
570
- let attempts = 0;
571
- const maxAttempts = 100; // Prevent infinite loop
572
 
573
- // --- THIS IS THE UPDATED PART ---
574
- const numCells = CONFIG.GRID_SIZE;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  const xIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
576
  const zIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
577
 
578
- const potentialFoodPos = new THREE.Vector3(
579
  xIndex * CONFIG.CELL_SIZE,
580
- 0, // Assuming food is always on y=0 plane
581
  zIndex * CONFIG.CELL_SIZE
582
  );
583
- // --- END OF UPDATED PART ---
584
 
585
- while (!validPosition && attempts < maxAttempts) {
586
- // Check collision with snake
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  let collisionWithSnake = this.snake.some(segment =>
588
- segment.position.distanceTo(foodPos) < CONFIG.CELL_SIZE * 0.9
589
  );
590
 
591
- // Check collision with obstacles
592
- let collisionWithObstacle = this.obstacles.some(obstacle =>
593
- obstacle.position.distanceTo(foodPos) < CONFIG.CELL_SIZE * 0.9
594
  );
595
 
596
- validPosition = !collisionWithSnake && !collisionWithObstacle;
597
  attempts++;
598
  }
599
 
600
  if (validPosition) {
601
- this.food.position.copy(foodPos);
602
- } else {
603
- // Fallback in case we can't find a position after max attempts
604
- console.warn("Could not find valid position for food after max attempts");
605
- this.food.position.set(0, 5, 0); // Place above play area
606
  }
607
  }
 
 
 
 
 
 
 
 
608
 
609
- // Create obstacles
610
- createObstacles() {
611
- // Clear existing obstacles
612
- for (const obstacle of this.obstacles) {
613
- this.scene.remove(obstacle);
614
- }
615
- this.obstacles = [];
616
-
617
- // Create new obstacles
618
- for (let i = 0; i < this.obstacleCount; i++) {
619
- let obstaclePos;
620
- let validPosition = false;
621
- let attempts = 0;
622
- const maxAttempts = 50;
623
-
624
- while (!validPosition && attempts < maxAttempts) {
625
- obstaclePos = new THREE.Vector3(
626
- Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE,
627
- 0,
628
- Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE
629
- );
630
-
631
- // Check distance from snake start position
632
- let tooCloseToStart = obstaclePos.length() < CONFIG.CELL_SIZE * 3;
633
-
634
- // Check collision with snake and other obstacles
635
- let collisionWithSnake = this.snake.some(segment =>
636
- segment.position.distanceTo(obstaclePos) < CONFIG.CELL_SIZE * 2
637
- );
638
-
639
- let collisionWithObstacle = this.obstacles.some(obstacle =>
640
- obstacle.position.distanceTo(obstaclePos) < CONFIG.CELL_SIZE
641
- );
642
-
643
- validPosition = !tooCloseToStart && !collisionWithSnake && !collisionWithObstacle;
644
- attempts++;
645
- }
646
-
647
- if (validPosition) {
648
- const obstacle = new THREE.Mesh(this.geometries.segment, this.materials.obstacle);
649
- obstacle.position.copy(obstaclePos);
650
- this.obstacles.push(obstacle);
651
- this.scene.add(obstacle);
652
- }
653
- }
654
  }
 
655
 
656
- // Clear all game objects for a new game
657
- clearGameObjects() {
658
- // Clear snake
659
- for (const segment of this.snake) {
660
- this.scene.remove(segment);
661
- this.segmentPool.release(segment);
662
- }
663
- this.snake = [];
664
-
665
- // Clear food
666
- if (this.food) {
667
- this.scene.remove(this.food);
668
- this.food = null;
669
- }
670
-
671
- // Clear obstacles
672
- for (const obstacle of this.obstacles) {
673
- this.scene.remove(obstacle);
674
- }
675
- this.obstacles = [];
676
-
677
- // Clear particles
678
- this.particleSystem.clear();
679
  }
680
 
681
- // Update game logic
682
- update(time) {
683
- // Skip update if game is paused, over, or not playing
684
- if (this.isPaused || this.isGameOver || GameState.currentState !== GameState.PLAYING) {
685
- return;
686
- }
687
-
688
- // Control game speed
689
- if (time - this.lastUpdateTime < this.gameSpeed) {
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  return;
691
  }
692
- this.lastUpdateTime = time;
693
-
694
- // Update direction safely
695
- this.direction = this.nextDirection.clone();
696
-
697
- // Get current head position
698
- const head = this.snake[0];
699
- const newHeadPos = head.position.clone().add(
700
- this.direction.clone().multiplyScalar(CONFIG.CELL_SIZE)
701
- );
702
-
703
- // Check collision with walls
704
- const halfGrid = CONFIG.GRID_SIZE / 2;
705
- if (
706
- newHeadPos.x > halfGrid * CONFIG.CELL_SIZE ||
707
- newHeadPos.x < -halfGrid * CONFIG.CELL_SIZE ||
708
- newHeadPos.z > halfGrid * CONFIG.CELL_SIZE ||
709
- newHeadPos.z < -halfGrid * CONFIG.CELL_SIZE
710
- ) {
711
  this.triggerGameOver();
712
  return;
713
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
 
715
- // Check collision with self
716
- for (let i = 1; i < this.snake.length; i++) {
717
- if (newHeadPos.distanceTo(this.snake[i].position) < CONFIG.CELL_SIZE * 0.25) {
718
- this.triggerGameOver();
719
- return;
720
- }
721
- }
722
-
723
- // Check collision with obstacles
724
- for (const obstacle of this.obstacles) {
725
- if (newHeadPos.distanceTo(obstacle.position) < CONFIG.CELL_SIZE * 0.5) {
726
- this.triggerGameOver();
727
- return;
728
- }
729
- }
730
-
731
- // Create new head segment
732
- const newHead = this.segmentPool.get();
733
- newHead.position.copy(newHeadPos);
734
- this.snake.unshift(newHead);
735
- this.scene.add(newHead);
736
-
737
- // Check for food collision
738
- if (this.food && newHeadPos.distanceTo(this.food.position) < CONFIG.CELL_SIZE * 0.5) {
739
- // Get food properties
740
- const foodType = this.food.userData;
741
-
742
- // Increase score
743
- const basePoints = foodType.points || 1;
744
-
745
- // Handle combo system
746
- const currentTime = performance.now();
747
- if (currentTime - this.lastFoodTime < CONFIG.COMBO_TIMEOUT) {
748
- this.comboCount++;
749
- } else {
750
- this.comboCount = 1;
751
- }
752
- this.lastFoodTime = currentTime;
753
-
754
- // Calculate final score with combo multiplier
755
- const points = basePoints * this.comboCount;
756
- this.score += points;
757
-
758
- // Show combo
759
- if (this.comboCount > 1) {
760
- const comboElement = document.getElementById('combo');
761
- comboElement.textContent = `Combo x${this.comboCount}! +${points}`;
762
- comboElement.style.opacity = 1;
763
-
764
- // Hide combo text after a delay
765
- setTimeout(() => {
766
- comboElement.style.opacity = 0;
767
- }, 2000);
768
- }
769
-
770
- // Update score display
771
- document.getElementById('info').textContent = `Score: ${this.score} | High: ${Math.max(this.score, this.highScore)}`;
772
-
773
- // Apply speed effect from food type
774
- if (foodType.speedEffect) {
775
- this.gameSpeed = Math.max(50, this.gameSpeed - foodType.speedEffect);
776
- }
777
-
778
- // Play eat sound
779
- document.getElementById('eatSound').play();
780
-
781
- // Create particle effect at food position
782
- this.particleSystem.createFoodEffect(this.food.position.clone(), foodType.color);
783
-
784
- // Place new food
785
- this.chooseFoodType();
786
- this.placeFood();
787
  } else {
788
- // Remove tail if not eating
789
- const tail = this.snake.pop();
790
- this.scene.remove(tail);
791
- this.segmentPool.release(tail);
792
  }
 
793
 
794
- // Update particles
795
- this.particleSystem.update();
796
 
797
- // Animate snake segments (subtle wave effect)
798
- for (let i = 0; i < this.snake.length; i++) {
799
- const segment = this.snake[i];
800
- segment.rotation.y = Math.sin(time * 0.001 + i * 0.2) * 0.1;
801
- segment.position.y = Math.sin(time * 0.002 + i * 0.1) * 0.2;
802
  }
803
 
804
- // Animate food
805
- if (this.food) {
806
- this.food.rotation.y += 0.05;
807
- this.food.position.y = Math.sin(time * 0.002) * 0.3;
808
- }
809
- }
810
-
811
- // Choose a food type based on probability
812
- chooseFoodType() {
813
- // Food type probability
814
- const rand = Math.random();
815
- let foodType;
816
-
817
- if (rand < 0.05) { // 5% chance for rare food
818
- foodType = CONFIG.FOOD_TYPES[2];
819
- } else if (rand < 0.25) { // 20% chance for special food
820
- foodType = CONFIG.FOOD_TYPES[1];
821
- } else { // 75% chance for regular food
822
- foodType = CONFIG.FOOD_TYPES[0];
823
- }
824
 
825
- // Create food mesh with appropriate material
826
- let material;
827
- switch(foodType.type) {
828
- case 'special':
829
- material = this.materials.specialFood;
830
- break;
831
- case 'rare':
832
- material = this.materials.rareFood;
833
- break;
834
- default:
835
- material = this.materials.food;
836
  }
837
 
838
- // Create or update food mesh
839
- if (!this.food) {
840
- this.food = new THREE.Mesh(
841
- this.geometries.segment,
842
- material
843
- );
844
- this.scene.add(this.food);
845
- } else {
846
- this.food.material = material;
847
- }
848
 
849
- // Store food type data
850
- this.food.userData = foodType;
 
 
 
 
851
  }
852
 
853
- // Reset the game
854
- resetGame() {
855
- // Clear all game objects
856
- this.clearGameObjects();
857
-
858
- // Stop any playing audio
859
- document.getElementById('eatSound').pause();
860
- document.getElementById('gameOverSound').pause();
861
- document.getElementById('bgMusic').pause();
862
-
863
- // Reset game state
864
- this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
865
- this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
866
- this.score = 0;
867
- this.gameSpeed = CONFIG.BASE_SPEED * CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].speedMultiplier;
868
- this.isGameOver = false;
869
- this.isPaused = false;
870
- this.comboCount = 0;
871
- this.lastFoodTime = 0;
872
-
873
- // Update obstacle count based on difficulty
874
- this.obstacleCount = Math.floor(CONFIG.MAX_OBSTACLE_COUNT *
875
- CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].obstacleMultiplier);
876
-
877
- // Create initial snake
878
- const startSegment = this.segmentPool.get();
879
- startSegment.position.set(0, 0, 0);
880
- this.snake.push(startSegment);
881
- this.scene.add(startSegment);
882
-
883
- // Create food
884
- this.chooseFoodType();
885
- this.placeFood();
886
-
887
- // Create obstacles
888
- this.createObstacles();
889
-
890
- // Update score display
891
- document.getElementById('info').textContent = `Score: ${this.score} | High: ${this.highScore}`;
892
-
893
- // Start background music
894
- const music = document.getElementById('bgMusic');
895
- music.volume = 0.3;
896
- music.play();
897
- }
898
 
899
- // Start the game
900
- startGame() {
901
- this.resetGame();
902
- GameState.changeState(GameState.PLAYING);
903
- this.gameLoop();
904
  }
905
 
906
- // Game over
907
- triggerGameOver() {
908
- this.isGameOver = true;
909
-
910
- // Update final score display
911
- document.getElementById('finalScore').textContent = `Final Score: ${this.score}`;
912
-
913
- // Check for high score
914
- const highScores = this.loadHighScores();
915
- if (this.score > 0) {
916
- // Add current score to high scores
917
- highScores.push({
918
- score: this.score,
919
- difficulty: this.currentDifficulty,
920
- date: new Date().toLocaleDateString()
921
- });
922
-
923
- // Sort high scores
924
- highScores.sort((a, b) => b.score - a.score);
925
-
926
- // Keep only top scores
927
- const topScores = highScores.slice(0, CONFIG.HIGH_SCORES_COUNT);
928
-
929
- // Save high scores
930
- localStorage.setItem('snakeHighScores', JSON.stringify(topScores));
931
-
932
- // Update high score if needed
933
- this.highScore = Math.max(this.score, this.highScore);
934
- }
935
-
936
- // Update high scores table
937
- this.updateHighScoresTable();
938
-
939
- // Stop background music
940
- document.getElementById('bgMusic').pause();
941
-
942
- // Change game state to game over
943
- GameState.changeState(GameState.GAME_OVER);
944
  }
945
-
946
- // Game loop
947
- gameLoop(time) {
948
- // Update current time
949
- if (!time) time = 0;
950
-
951
- // Update game
952
- this.update(time);
953
-
954
- // Render scene
955
- this.render();
956
-
957
- // Continue game loop
958
- this.gameLoopId = requestAnimationFrame(this.gameLoop.bind(this));
 
 
959
  }
960
 
961
- // Render scene
962
- render() {
963
- this.renderer.render(this.scene, this.camera);
964
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965
 
966
- // Initialize game
967
- init() {
968
- // Create scene
969
- this.scene = new THREE.Scene();
970
- this.scene.background = null;
971
-
972
- // Add fog for depth
973
- this.scene.fog = new THREE.Fog(0x000500, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 2.5);
974
-
975
- // Create camera
976
- this.camera = new THREE.PerspectiveCamera(
977
- 65, window.innerWidth / window.innerHeight, 0.1, 1000
978
- );
979
- this.camera.position.set(0, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 0.9);
980
- this.camera.lookAt(0, -CONFIG.GRID_SIZE * 0.1, 0);
981
-
982
- // Create renderer
983
- this.renderer = new THREE.WebGLRenderer({
984
- canvas: document.getElementById('gameCanvas'),
985
- antialias: true,
986
- alpha: true
987
- });
988
- this.renderer.setSize(window.innerWidth, window.innerHeight);
989
- this.renderer.setPixelRatio(window.devicePixelRatio);
990
-
991
- // Create grid for visual reference
992
- const gridHelper = new THREE.GridHelper(
993
- CONFIG.GRID_SIZE * CONFIG.CELL_SIZE,
994
- CONFIG.GRID_SIZE,
995
- 0x005500,
996
- 0x003300
997
- );
998
- gridHelper.position.y = -CONFIG.CELL_SIZE / 2;
999
- this.scene.add(gridHelper);
1000
-
1001
- // Add ambient light
1002
- const ambientLight = new THREE.AmbientLight(0x404060);
1003
- this.scene.add(ambientLight);
1004
-
1005
- // Add directional light
1006
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
1007
- directionalLight.position.set(5, 10, 7);
1008
- this.scene.add(directionalLight);
1009
-
1010
- // Create head light
1011
- this.headLight = new THREE.PointLight(0x00ff00, 1, CONFIG.CELL_SIZE * 3);
1012
- this.scene.add(this.headLight);
1013
-
1014
- // Create particle system
1015
- this.particleSystem = new ParticleSystem(this.scene);
1016
-
1017
- // Handle window resize
1018
- window.addEventListener('resize', () => {
1019
- this.camera.aspect = window.innerWidth / window.innerHeight;
1020
- this.camera.updateProjectionMatrix();
1021
- this.renderer.setSize(window.innerWidth, window.innerHeight);
1022
- });
1023
- }
1024
 
1025
- // Set up event listeners
1026
- setupEventListeners() {
1027
- // Keyboard controls
1028
- document.addEventListener('keydown', this.handleKeyDown.bind(this));
1029
-
1030
- // Touch controls with prevention
1031
- const touchControls = document.getElementById('touchControls');
1032
- const preventDefault = (e) => {
1033
- e.preventDefault();
1034
- e.stopPropagation();
1035
- };
1036
-
1037
- document.getElementById('upBtn').addEventListener('touchstart', (e) => {
1038
- preventDefault(e);
1039
- this.handleDirectionChange(0, 0, -1);
1040
- });
1041
- document.getElementById('downBtn').addEventListener('touchstart', (e) => {
1042
- preventDefault(e);
1043
- this.handleDirectionChange(0, 0, 1);
1044
- });
1045
- document.getElementById('leftBtn').addEventListener('touchstart', (e) => {
1046
- preventDefault(e);
1047
- this.handleDirectionChange(-1, 0, 0);
1048
- });
1049
- document.getElementById('rightBtn').addEventListener('touchstart', (e) => {
1050
- preventDefault(e);
1051
- this.handleDirectionChange(1, 0, 0);
1052
- });
1053
-
1054
- // Prevent touch events on game canvas
1055
- document.getElementById('gameCanvas').addEventListener('touchstart', preventDefault, { passive: false });
1056
- document.getElementById('gameCanvas').addEventListener('touchmove', preventDefault, { passive: false });
1057
-
1058
- // Show touch controls on mobile devices
1059
- if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
1060
- touchControls.style.display = 'block';
1061
- }
1062
-
1063
- // UI buttons
1064
- document.getElementById('startBtn').addEventListener('click', () => {
1065
- this.startGame();
1066
- });
1067
-
1068
- document.getElementById('restartBtn').addEventListener('click', () => {
1069
- this.startGame();
1070
- });
1071
-
1072
- document.getElementById('menuBtn').addEventListener('click', () => {
1073
- GameState.changeState(GameState.MENU);
1074
- });
1075
-
1076
- document.getElementById('difficultyBtn').addEventListener('click', () => {
1077
- this.cycleDifficulty();
1078
- });
1079
-
1080
- // Pause screen buttons
1081
- document.getElementById('resumeBtn').addEventListener('click', () => {
1082
- this.togglePause();
1083
- });
1084
-
1085
- document.getElementById('quitBtn').addEventListener('click', () => {
1086
- GameState.changeState(GameState.MENU);
1087
- });
1088
- }
1089
 
1090
- // Cycle through difficulty levels
1091
- cycleDifficulty() {
1092
- const difficulties = Object.keys(CONFIG.DIFFICULTY_LEVELS);
1093
- const currentIndex = difficulties.indexOf(this.currentDifficulty);
1094
- const nextIndex = (currentIndex + 1) % difficulties.length;
1095
- this.currentDifficulty = difficulties[nextIndex];
1096
-
1097
- document.getElementById('difficultyBtn').textContent = `DIFFICULTY: ${this.currentDifficulty}`;
1098
- }
1099
 
1100
- // Handle keyboard input
1101
- handleKeyDown(event) {
1102
- if (GameState.currentState === GameState.PLAYING) {
1103
- switch(event.key) {
1104
- case 'ArrowUp':
1105
- this.handleDirectionChange(0, 0, -1);
1106
- event.preventDefault();
1107
- break;
1108
- case 'ArrowDown':
1109
- this.handleDirectionChange(0, 0, 1);
1110
- event.preventDefault();
1111
- break;
1112
- case 'ArrowLeft':
1113
- this.handleDirectionChange(-1, 0, 0);
1114
- event.preventDefault();
1115
- break;
1116
- case 'ArrowRight':
1117
- this.handleDirectionChange(1, 0, 0);
1118
- event.preventDefault();
1119
- break;
1120
- case 'p':
1121
- case 'P':
1122
- this.togglePause();
1123
- event.preventDefault();
1124
- break;
1125
- }
1126
- } else if (GameState.currentState === GameState.GAME_OVER ||
1127
- GameState.currentState === GameState.MENU) {
1128
- if (event.key === 'Enter') {
1129
- this.startGame();
1130
- event.preventDefault();
1131
- }
1132
- }
1133
- }
1134
 
1135
- // Handle direction change
1136
- handleDirectionChange(x, y, z) {
1137
- const newDirection = new THREE.Vector3(x, y, z).normalize().multiplyScalar(CONFIG.CELL_SIZE);
1138
-
1139
- // Prevent 180-degree turns (moving directly backwards)
1140
- if (this.direction.dot(newDirection) === -CONFIG.CELL_SIZE * CONFIG.CELL_SIZE) {
1141
- return;
1142
- }
1143
-
1144
- this.nextDirection = newDirection;
1145
- }
1146
 
1147
- // Toggle pause state
1148
- togglePause() {
1149
- this.isPaused = !this.isPaused;
1150
-
1151
- if (this.isPaused) {
1152
- // TODO: Show pause screen
1153
- document.getElementById('bgMusic').pause();
1154
- } else {
1155
- document.getElementById('bgMusic').play();
1156
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1157
  }
 
 
 
 
1158
 
1159
- // Load high scores from local storage
1160
- loadHighScores() {
1161
- const scores = localStorage.getItem('snakeHighScores');
1162
- return scores ? JSON.parse(scores) : [];
 
 
 
 
 
 
 
 
 
 
1163
  }
1164
 
1165
- // Update high scores table
1166
- updateHighScoresTable() {
1167
- const highScores = this.loadHighScores();
1168
- const table = document.getElementById('scoresTable');
1169
-
1170
- // Clear table except header
1171
- while (table.rows.length > 1) {
1172
- table.deleteRow(1);
1173
- }
1174
-
1175
- // Add high scores to table
1176
- for (let i = 0; i < highScores.length; i++) {
1177
- const row = table.insertRow(-1);
1178
-
1179
- const rankCell = row.insertCell(0);
1180
- rankCell.textContent = i + 1;
1181
-
1182
- const scoreCell = row.insertCell(1);
1183
- scoreCell.textContent = highScores[i].score;
1184
-
1185
- const difficultyCell = row.insertCell(2);
1186
- difficultyCell.textContent = highScores[i].difficulty;
1187
- }
 
 
 
 
 
 
 
 
 
 
 
1188
  }
1189
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1190
 
1191
- // Create and start the game
1192
- const game = new SnakeGame();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1193
 
1194
- // Start the game when the page loads
1195
- window.addEventListener('load', () => {
1196
- GameState.changeState(GameState.MENU);
1197
- });
1198
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1199
  </body>
1200
  </html>
 
211
  }
212
  }
213
  </script>
214
+ <script type="module">
215
+ import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
216
+ import { EffectComposer } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js';
217
+ import { RenderPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/RenderPass.js';
218
+ import { UnrealBloomPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/UnrealBloomPass.js';
 
 
219
 
220
+ // Game configuration
221
+ const CONFIG = {
222
+ GRID_SIZE: 25, // Number of units across/deep
223
+ CELL_SIZE: 1, // Size of each grid cell/snake segment
224
+ BASE_SPEED: 150, // Base milliseconds between updates
225
+ DIFFICULTY_LEVELS: {
226
+ 'EASY': { speedMultiplier: 1.3, obstacleMultiplier: 0.5 },
227
+ 'NORMAL': { speedMultiplier: 1.0, obstacleMultiplier: 1.0 },
228
+ 'HARD': { speedMultiplier: 0.7, obstacleMultiplier: 1.5 }
229
+ },
230
+ MAX_OBSTACLE_COUNT: 10, // Maximum number of obstacles
231
+ FOOD_TYPES: [
232
+ { type: 'regular', color: 0x00ff00, points: 1, speedEffect: 0 },
233
+ { type: 'special', color: 0x00ffff, points: 5, speedEffect: -10 },
234
+ { type: 'rare', color: 0xff00ff, points: 10, speedEffect: 10 }
235
+ ],
236
+ COMBO_TIMEOUT: 5000, // Milliseconds to get next food for combo
237
+ HIGH_SCORES_COUNT: 5 // Number of high scores to save
238
+ };
239
 
240
+ // --- Particle System for Effects ---
241
+ class ParticleSystem {
242
+ constructor(scene) {
243
+ this.scene = scene;
244
+ this.particles = [];
245
+ this.geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2); // Shared geometry
246
+ }
247
+
248
+ createFoodEffect(position, color) {
249
+ const count = 20;
250
+ for (let i = 0; i < count; i++) {
251
+ const material = new THREE.MeshBasicMaterial({
252
+ color: color || 0x00ff00,
253
+ transparent: true,
254
+ opacity: 0.9
255
+ });
256
+ const particle = new THREE.Mesh(this.geometry, material);
257
+ particle.position.copy(position);
258
+ const velocity = new THREE.Vector3(
259
+ (Math.random() - 0.5) * 0.1,
260
+ (Math.random()) * 0.1,
261
+ (Math.random() - 0.5) * 0.1
262
+ );
263
+ this.scene.add(particle);
264
+ this.particles.push({
265
+ mesh: particle,
266
+ velocity: velocity,
267
+ life: 1.0,
268
+ decay: 0.02 + Math.random() * 0.03
269
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
+ }
272
+
273
+ update() {
274
+ for (let i = this.particles.length - 1; i >= 0; i--) {
275
+ const particle = this.particles[i];
276
+ particle.mesh.position.add(particle.velocity);
277
+ particle.velocity.y -= 0.003; // Gravity
278
+ particle.life -= particle.decay;
279
+ particle.mesh.material.opacity = particle.life;
280
+
281
+ if (particle.life <= 0) {
282
  this.scene.remove(particle.mesh);
283
  particle.mesh.material.dispose();
284
+ // particle.mesh.geometry.dispose(); // Geometry is shared, don't dispose here
285
+ this.particles.splice(i, 1);
286
  }
 
287
  }
288
  }
289
+
290
+ clear() {
291
+ for (const particle of this.particles) {
292
+ this.scene.remove(particle.mesh);
293
+ particle.mesh.material.dispose();
294
+ }
295
+ this.particles = [];
296
+ // Note: Shared geometry (this.geometry) is not disposed here,
297
+ // as it might be needed again. Dispose it if ParticleSystem itself is destroyed.
298
+ }
299
+ }
300
 
301
+ // Game state management
302
+ const GameState = {
303
+ MENU: 'menu',
304
+ PLAYING: 'playing',
305
+ PAUSED: 'paused',
306
+ GAME_OVER: 'gameOver',
307
+ currentState: 'menu',
308
+
309
+ changeState(newState) {
310
+ this.currentState = newState;
311
+ switch(newState) {
312
+ case this.MENU:
313
+ document.getElementById('gameScreen').style.display = 'flex';
314
+ document.getElementById('startScreen').style.display = 'block';
315
+ document.getElementById('gameOverScreen').style.display = 'none';
316
+ document.getElementById('pauseScreen').style.display = 'none';
317
+ break;
318
+ case this.PLAYING:
319
+ document.getElementById('gameScreen').style.display = 'none';
320
+ break;
321
+ case this.PAUSED:
322
+ document.getElementById('gameScreen').style.display = 'flex';
323
+ document.getElementById('startScreen').style.display = 'none';
324
+ document.getElementById('gameOverScreen').style.display = 'none';
325
+ document.getElementById('pauseScreen').style.display = 'block';
326
+ break;
327
+ case this.GAME_OVER:
328
+ document.getElementById('gameScreen').style.display = 'flex';
329
+ document.getElementById('startScreen').style.display = 'none';
330
+ document.getElementById('gameOverScreen').style.display = 'block';
331
+ document.getElementById('pauseScreen').style.display = 'none';
332
+ document.getElementById('gameOverSound').play();
333
+ break;
 
 
334
  }
335
+ }
336
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
+ // --- Matrix Rain Background Effect ---
339
+ class MatrixRain {
340
+ constructor() {
341
+ this.canvas = document.getElementById('matrixCanvas');
342
+ this.ctx = this.canvas.getContext('2d');
343
+ this.resize();
344
 
345
+ this.fontSize = 14;
346
+ this.columns = Math.floor(this.canvas.width / this.fontSize);
347
+ this.drops = [];
348
+ this.characters = '01アイウエオカキクケコサシスセソタチツテトナニヌネ<>{}[]()+-*/%=#@&?*:・゚✧ ≡ ░▒░▒░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█䷀ ▙⁞ ░▒▓█║│ ·▓▒░█▄▀■■▄▬▌▐ ⁞▏▄▀■■▄▬▌▐ ▄▀■■▄▬▌▐ . ▛ ⁞▏ ▏ ⁚⁝ .';
349
+ this.resetDrops();
350
+ this.animate = this.animate.bind(this);
351
+ this.animate(); // Start animation
352
+ window.addEventListener('resize', this.handleResize.bind(this));
353
+ }
354
+
355
+ handleResize() {
356
+ this.resize();
357
+ this.columns = Math.floor(this.canvas.width / this.fontSize);
358
+ this.resetDrops();
359
+ }
360
 
361
+ resize() {
362
+ this.canvas.width = window.innerWidth;
363
+ this.canvas.height = window.innerHeight;
364
+ }
365
+
366
+ resetDrops() {
367
+ this.drops = [];
368
+ for(let i = 0; i < this.columns; i++) {
369
+ this.drops[i] = Math.floor(Math.random() * -100); // Start off-screen
 
 
370
  }
371
+ }
372
+
373
+ animate() {
374
+ // Show rain on all screens as per previous request, including PLAYING
375
+ if (GameState.currentState === GameState.MENU ||
376
+ GameState.currentState === GameState.PAUSED ||
377
+ GameState.currentState === GameState.GAME_OVER ||
378
+ GameState.currentState === GameState.PLAYING) {
379
 
380
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; // Fading effect
381
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
382
+
383
+ this.ctx.fillStyle = '#0f0'; // Matrix character color
384
+ this.ctx.font = this.fontSize + 'px monospace';
385
+
386
+ for(let i = 0; i < this.drops.length; i++) {
387
+ const text = this.characters.charAt(Math.floor(Math.random() * this.characters.length));
388
+ this.ctx.fillText(text, i * this.fontSize, this.drops[i] * this.fontSize);
389
 
390
+ if(this.drops[i] * this.fontSize > this.canvas.height && Math.random() > 0.975) {
391
+ this.drops[i] = 0; // Reset drop
 
 
 
 
 
 
392
  }
393
+ this.drops[i]++;
394
  }
 
395
  }
396
+ requestAnimationFrame(this.animate);
397
+ }
398
+ } // <-- Make sure this curly brace properly closes the MatrixRain class
399
 
400
+ // --- Object pooling for performance optimization ---
401
+ class ObjectPool {
402
+ constructor(createFunc, initialCount = 10) {
403
+ this.pool = [];
404
+ this.createFunc = createFunc;
 
 
 
 
 
 
 
405
 
406
+ for (let i = 0; i < initialCount; i++) {
407
+ this.pool.push(this.createFunc());
 
 
 
408
  }
409
+ }
410
+
411
+ get() {
412
+ if (this.pool.length > 0) {
413
+ return this.pool.pop();
414
  }
415
+ return this.createFunc();
416
+ }
417
+
418
+ release(object) {
419
+ this.pool.push(object);
420
+ }
421
+
422
+ clear() {
423
+ this.pool = [];
424
+ // Note: This doesn't dispose Three.js objects.
425
+ // Disposal should be handled by the code that uses the pool
426
+ // when objects are truly no longer needed by the scene.
427
+ }
428
+ }
429
+
430
+ // --- Main Game Class ---
431
+ class SnakeGame {
432
+ constructor() {
433
+ this.scene = null;
434
+ this.camera = null;
435
+ this.renderer = null;
436
+ this.composer = null; // For post-processing
437
+ this.snake = [];
438
+ this.food = null;
439
+ this.obstacles = [];
440
+ this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
441
+ this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
442
+ this.score = 0;
443
+ this.highScore = this.loadHighScores()[0]?.score || 0;
444
+ this.gameSpeed = CONFIG.BASE_SPEED;
445
+ this.lastUpdateTime = 0;
446
+ this.isGameOver = false;
447
+ this.isPaused = false;
448
+ this.gameLoopId = null;
449
+ this.bounds = Math.floor(CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE;
450
+ this.obstacleCount = CONFIG.MAX_OBSTACLE_COUNT;
451
+ this.comboCount = 0;
452
+ this.lastFoodTime = 0;
453
+ this.currentDifficulty = 'NORMAL';
454
+ this.particleSystem = null;
455
+ this.headLight = null;
456
+
457
+ this.materials = {
458
+ snakeHead: new THREE.MeshStandardMaterial({
459
+ color: 0x39FF14, emissive: 0x39FF14, roughness: 0.8, metalness: 0.22
460
+ }),
461
+ snakeBody: new THREE.MeshStandardMaterial({
462
+ color: 0x00ff00, emissive: 0x005500, roughness: 0.3, metalness: 0.72
463
+ }),
464
+ obstacle: new THREE.MeshStandardMaterial({ // Changed to Standard for lighting
465
+ color: 0xff0000, emissive: 0x550000, roughness: 0.5, metalness: 0.1
466
+ }),
467
+ // Food materials defined per type in chooseFoodType
468
+ };
469
+ this.geometries = {
470
+ segment: new THREE.BoxGeometry(CONFIG.CELL_SIZE, CONFIG.CELL_SIZE, CONFIG.CELL_SIZE),
471
+ foodBox: new THREE.BoxGeometry(CONFIG.CELL_SIZE * 0.8, CONFIG.CELL_SIZE * 0.8, CONFIG.CELL_SIZE * 0.8),
472
+ foodSphere: new THREE.SphereGeometry(CONFIG.CELL_SIZE * 0.5, 16, 12),
473
+ foodTetrahedron: new THREE.TetrahedronGeometry(CONFIG.CELL_SIZE * 0.6, 0),
474
+ obstacle: new THREE.BoxGeometry(CONFIG.CELL_SIZE, CONFIG.CELL_SIZE * 1.5, CONFIG.CELL_SIZE) // Taller obstacles
475
+ };
476
+
477
+ this.segmentPool = new ObjectPool(() => {
478
+ const segment = new THREE.Mesh(this.geometries.segment, this.materials.snakeBody.clone());
479
+ // segment.castShadow = true; // If using shadows
480
+ return segment;
481
+ }, 20);
482
+
483
+ this.obstaclePool = new ObjectPool(() => {
484
+ const obstacle = new THREE.Mesh(this.geometries.obstacle, this.materials.obstacle.clone());
485
+ // obstacle.castShadow = true; // If using shadows
486
+ return obstacle;
487
+ }, CONFIG.MAX_OBSTACLE_COUNT * 1.5);
488
 
489
+ this.init(); // Call init before matrixRain if matrixRain depends on game elements
490
+ this.matrixRain = new MatrixRain(); // Initialize after main game setup if it interacts
491
+ this.setupEventListeners();
492
+ this.updateHighScoresTable();
493
+ GameState.changeState(GameState.MENU); // Start in menu
494
  }
495
+
496
+ init() {
497
+ this.scene = new THREE.Scene();
498
+ this.scene.background = null; // For Matrix rain to show through
499
 
500
+ this.camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
501
+ this.camera.position.set(0, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 0.9);
502
+ this.camera.lookAt(0, -CONFIG.GRID_SIZE * 0.1, 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
 
504
+ this.renderer = new THREE.WebGLRenderer({
505
+ canvas: document.getElementById('gameCanvas'),
506
+ antialias: true,
507
+ alpha: true // Crucial for transparency
508
+ });
509
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
510
+ this.renderer.setPixelRatio(window.devicePixelRatio);
511
+ // this.renderer.shadowMap.enabled = true; // If you add shadows
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
 
513
+ // Post-processing for bloom
514
+ const renderPass = new RenderPass(this.scene, this.camera);
515
+ const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.2, 0.8, 0.75);
516
+ // bloomPass.threshold = 0;
517
+ // bloomPass.strength = 1.5; // Play with these
518
+ // bloomPass.radius = 0.5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
 
520
+ this.composer = new EffectComposer(this.renderer);
521
+ this.composer.addPass(renderPass);
522
+ this.composer.addPass(bloomPass);
 
523
 
524
+ const gridHelper = new THREE.GridHelper(CONFIG.GRID_SIZE * CONFIG.CELL_SIZE, CONFIG.GRID_SIZE, 0x008800, 0x004400);
525
+ gridHelper.position.y = -CONFIG.CELL_SIZE / 2; // Align with snake plane
526
+ this.scene.add(gridHelper);
527
 
528
+ const ambientLight = new THREE.AmbientLight(0x404060, 1); // Soft ambient
529
+ this.scene.add(ambientLight);
530
+
531
+ const directionalLight = new THREE.DirectionalLight(0xffffee, 1.5);
532
+ directionalLight.position.set(8, 15, 10);
533
+ // directionalLight.castShadow = true;
534
+ // directionalLight.shadow.mapSize.width = 1024;
535
+ // directionalLight.shadow.mapSize.height = 1024;
536
+ this.scene.add(directionalLight);
537
+
538
+ this.headLight = new THREE.PointLight(0x39FF14, 2, CONFIG.CELL_SIZE * 5); // Brighter, green light
539
+ this.headLight.castShadow = false; // Point lights can be expensive for shadows
540
+ this.scene.add(this.headLight);
541
 
542
+ this.particleSystem = new ParticleSystem(this.scene);
 
 
 
 
 
 
543
 
544
+ window.addEventListener('resize', () => {
545
+ this.camera.aspect = window.innerWidth / window.innerHeight;
546
+ this.camera.updateProjectionMatrix();
547
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
548
+ this.composer.setSize(window.innerWidth, window.innerHeight); // Resize composer too
549
+ }, false);
550
+ }
551
+
552
+ placeFood() {
553
+ let foodPos;
554
+ let validPosition = false;
555
+ let attempts = 0;
556
+ const maxAttempts = 100;
557
+
558
+ const numCells = CONFIG.GRID_SIZE;
559
+
560
+ while (!validPosition && attempts < maxAttempts) {
561
+ // Generate random indices within the grid
562
  const xIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
563
  const zIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
564
 
565
+ foodPos = new THREE.Vector3(
566
  xIndex * CONFIG.CELL_SIZE,
567
+ 0, // Food on the grid plane
568
  zIndex * CONFIG.CELL_SIZE
569
  );
 
570
 
571
+ let collisionWithSnake = this.snake.some(segment =>
572
+ segment.position.distanceToSquared(foodPos) < (CONFIG.CELL_SIZE * 0.9)**2 // Use distanceSquared
573
+ );
574
+
575
+ let collisionWithObstacle = this.obstacles.some(obstacle =>
576
+ obstacle.position.distanceToSquared(foodPos) < (CONFIG.CELL_SIZE * 0.9)**2
577
+ );
578
+
579
+ validPosition = !collisionWithSnake && !collisionWithObstacle;
580
+ attempts++;
581
+ }
582
+
583
+ if (validPosition) {
584
+ this.food.position.copy(foodPos);
585
+ } else {
586
+ console.warn("Could not find valid position for food. Placing at a default safe spot.");
587
+ // Fallback: try placing near origin, hoping it's clear
588
+ this.food.position.set(
589
+ (Math.floor(Math.random() * 3) - 1) * CONFIG.CELL_SIZE,
590
+ 0,
591
+ (Math.floor(Math.random() * 3) - 1) * CONFIG.CELL_SIZE
592
+ );
593
+ }
594
+ }
595
+
596
+ createObstacles() {
597
+ this.obstacles.forEach(obstacle => {
598
+ this.scene.remove(obstacle);
599
+ this.obstaclePool.release(obstacle);
600
+ });
601
+ this.obstacles = [];
602
+
603
+ const numCells = CONFIG.GRID_SIZE;
604
+
605
+ for (let i = 0; i < this.obstacleCount; i++) {
606
+ let obstaclePos;
607
+ let validPosition = false;
608
+ let attempts = 0;
609
+ const maxAttempts = 50;
610
+
611
+ while (!validPosition && attempts < maxAttempts) {
612
+ const xIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
613
+ const zIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
614
+ obstaclePos = new THREE.Vector3(
615
+ xIndex * CONFIG.CELL_SIZE,
616
+ 0, // Obstacles on the grid plane, but mesh is taller
617
+ zIndex * CONFIG.CELL_SIZE
618
+ );
619
+
620
+ let tooCloseToStart = obstaclePos.lengthSq() < (CONFIG.CELL_SIZE * 4)**2; // Check squared length
621
+
622
  let collisionWithSnake = this.snake.some(segment =>
623
+ segment.position.distanceToSquared(obstaclePos) < (CONFIG.CELL_SIZE * 2)**2
624
  );
625
 
626
+ let collisionWithOtherObstacle = this.obstacles.some(existingObstacle =>
627
+ existingObstacle.position.distanceToSquared(obstaclePos) < (CONFIG.CELL_SIZE * 1.5)**2 // Ensure spacing
 
628
  );
629
 
630
+ validPosition = !tooCloseToStart && !collisionWithSnake && !collisionWithOtherObstacle;
631
  attempts++;
632
  }
633
 
634
  if (validPosition) {
635
+ const obstacle = this.obstaclePool.get();
636
+ obstacle.position.copy(obstaclePos);
637
+ obstacle.position.y = (CONFIG.CELL_SIZE * 1.5 - CONFIG.CELL_SIZE) / 2 - CONFIG.CELL_SIZE / 2; // Adjust Y to sit on grid
638
+ this.obstacles.push(obstacle);
639
+ this.scene.add(obstacle);
640
  }
641
  }
642
+ }
643
+
644
+ clearGameObjects() {
645
+ this.snake.forEach(segment => {
646
+ this.scene.remove(segment);
647
+ this.segmentPool.release(segment);
648
+ });
649
+ this.snake = [];
650
 
651
+ if (this.food) {
652
+ this.scene.remove(this.food);
653
+ // No pool for food as it's a single, changing object
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
  }
655
+ this.food = null;
656
 
657
+ this.obstacles.forEach(obstacle => {
658
+ this.scene.remove(obstacle);
659
+ this.obstaclePool.release(obstacle);
660
+ });
661
+ this.obstacles = [];
662
+
663
+ this.particleSystem.clear();
664
+ }
665
+
666
+ update(time) {
667
+ if (this.isPaused || this.isGameOver || GameState.currentState !== GameState.PLAYING) {
668
+ return;
 
 
 
 
 
 
 
 
 
 
 
669
  }
670
 
671
+ if (time - this.lastUpdateTime < this.gameSpeed) {
672
+ return;
673
+ }
674
+ this.lastUpdateTime = time;
675
+
676
+ this.direction.copy(this.nextDirection); // More robust copy
677
+
678
+ const head = this.snake[0];
679
+ const newHeadPos = head.position.clone().add(this.direction); // CELL_SIZE is incorporated in direction
680
+
681
+ const halfGridWorld = (CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE;
682
+ if (
683
+ newHeadPos.x >= halfGridWorld || newHeadPos.x < -halfGridWorld ||
684
+ newHeadPos.z >= halfGridWorld || newHeadPos.z < -halfGridWorld
685
+ ) {
686
+ this.triggerGameOver();
687
+ return;
688
+ }
689
+
690
+ for (let i = 1; i < this.snake.length; i++) {
691
+ if (newHeadPos.distanceToSquared(this.snake[i].position) < (CONFIG.CELL_SIZE * 0.1)**2) { // Tighter collision
692
+ this.triggerGameOver();
693
  return;
694
  }
695
+ }
696
+
697
+ for (const obstacle of this.obstacles) {
698
+ if (newHeadPos.distanceToSquared(obstacle.position) < (CONFIG.CELL_SIZE * 0.75)**2) { // Check collision with obstacle
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  this.triggerGameOver();
700
  return;
701
  }
702
+ }
703
+
704
+ const newHead = this.segmentPool.get();
705
+ newHead.position.copy(newHeadPos);
706
+ newHead.material = this.materials.snakeHead; // Head material
707
+ if (this.snake.length > 0) {
708
+ this.snake[0].material = this.materials.snakeBody; // Old head becomes body
709
+ }
710
+ this.snake.unshift(newHead);
711
+ this.scene.add(newHead);
712
+ this.headLight.position.copy(newHeadPos); // Light follows head
713
+
714
+ if (this.food && newHeadPos.distanceToSquared(this.food.position) < (CONFIG.CELL_SIZE * 0.75)**2) {
715
+ const foodType = this.food.userData;
716
+ const basePoints = foodType.points || 1;
717
 
718
+ const currentTime = performance.now();
719
+ if (currentTime - this.lastFoodTime < CONFIG.COMBO_TIMEOUT) {
720
+ this.comboCount++;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
721
  } else {
722
+ this.comboCount = 1;
 
 
 
723
  }
724
+ this.lastFoodTime = currentTime;
725
 
726
+ const points = basePoints * this.comboCount;
727
+ this.score += points;
728
 
729
+ if (this.comboCount > 1) {
730
+ const comboElement = document.getElementById('combo');
731
+ comboElement.textContent = `Combo x${this.comboCount}! +${points}`;
732
+ comboElement.style.opacity = 1;
733
+ setTimeout(() => { comboElement.style.opacity = 0; }, 2000);
734
  }
735
 
736
+ document.getElementById('info').textContent = `Score: ${this.score} | High: ${Math.max(this.score, this.highScore)}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
 
738
+ if (foodType.speedEffect) {
739
+ this.gameSpeed = Math.max(50, this.gameSpeed + foodType.speedEffect); // Note: special food had -10, rare had +10
 
 
 
 
 
 
 
 
 
740
  }
741
 
742
+ document.getElementById('eatSound').currentTime = 0;
743
+ document.getElementById('eatSound').play();
744
+ this.particleSystem.createFoodEffect(this.food.position.clone(), new THREE.Color(foodType.color));
 
 
 
 
 
 
 
745
 
746
+ this.chooseFoodType(); // Gets new food type, applies material
747
+ this.placeFood(); // Places it
748
+ } else {
749
+ const tail = this.snake.pop();
750
+ this.scene.remove(tail);
751
+ this.segmentPool.release(tail);
752
  }
753
 
754
+ this.particleSystem.update();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
755
 
756
+ for (let i = 0; i < this.snake.length; i++) {
757
+ const segment = this.snake[i];
758
+ segment.rotation.y += Math.sin(time * 0.0001 + i * 0.1) * 0.01; // Slower, more subtle
759
+ segment.position.y = Math.sin(time * 0.002 + i * 0.2) * 0.15; // Subtle bob
 
760
  }
761
 
762
+ if (this.food) {
763
+ this.food.rotation.x += 0.01;
764
+ this.food.rotation.y += 0.02;
765
+ this.food.position.y = Math.sin(time * 0.0025) * 0.25 + 0.1; // Bobbing food
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
766
  }
767
+ }
768
+
769
+ chooseFoodType() {
770
+ const rand = Math.random();
771
+ let foodTypeData;
772
+ let geometry;
773
+
774
+ if (rand < 0.05) { // 5% rare
775
+ foodTypeData = CONFIG.FOOD_TYPES[2];
776
+ geometry = this.geometries.foodTetrahedron;
777
+ } else if (rand < 0.25) { // 20% special
778
+ foodTypeData = CONFIG.FOOD_TYPES[1];
779
+ geometry = this.geometries.foodSphere;
780
+ } else { // 75% regular
781
+ foodTypeData = CONFIG.FOOD_TYPES[0];
782
+ geometry = this.geometries.foodBox;
783
  }
784
 
785
+ const material = new THREE.MeshStandardMaterial({ // Use Standard for lighting
786
+ color: foodTypeData.color,
787
+ emissive: foodTypeData.color, // Make food glow
788
+ emissiveIntensity: 0.8,
789
+ roughness: 0.4,
790
+ metalness: 0.1
791
+ });
792
+
793
+ if (!this.food) {
794
+ this.food = new THREE.Mesh(geometry, material);
795
+ this.scene.add(this.food);
796
+ } else {
797
+ this.food.geometry.dispose(); // Dispose old geometry
798
+ this.food.geometry = geometry;
799
+ this.food.material.dispose(); // Dispose old material
800
+ this.food.material = material;
801
+ }
802
+ this.food.userData = foodTypeData;
803
+ }
804
+
805
+ resetGame() {
806
+ this.clearGameObjects();
807
 
808
+ // Stop sounds
809
+ ['eatSound', 'gameOverSound', 'bgMusic'].forEach(id => {
810
+ const sound = document.getElementById(id);
811
+ sound.pause();
812
+ sound.currentTime = 0;
813
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
814
 
815
+ this.direction.set(CONFIG.CELL_SIZE, 0, 0);
816
+ this.nextDirection.set(CONFIG.CELL_SIZE, 0, 0);
817
+ this.score = 0;
818
+ this.gameSpeed = CONFIG.BASE_SPEED * CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].speedMultiplier;
819
+ this.isGameOver = false;
820
+ this.isPaused = false;
821
+ GameState.currentState = GameState.PLAYING; // Set this before calling things that depend on it.
822
+ this.comboCount = 0;
823
+ this.lastFoodTime = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
 
825
+ this.obstacleCount = Math.floor(CONFIG.MAX_OBSTACLE_COUNT * CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].obstacleMultiplier);
 
 
 
 
 
 
 
 
826
 
827
+ const startSegment = this.segmentPool.get();
828
+ startSegment.position.set(0, 0, 0);
829
+ startSegment.material = this.materials.snakeHead; // Start with head material
830
+ this.snake.push(startSegment);
831
+ this.scene.add(startSegment);
832
+ this.headLight.position.copy(startSegment.position); // Initial light pos
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
833
 
834
+ this.chooseFoodType();
835
+ this.placeFood();
836
+ this.createObstacles();
 
 
 
 
 
 
 
 
837
 
838
+ document.getElementById('info').textContent = `Score: ${this.score} | High: ${this.highScore}`;
839
+
840
+ const music = document.getElementById('bgMusic');
841
+ music.volume = 0.2; // Quieter music
842
+ music.play().catch(e => console.warn("Music play failed:", e)); // Catch promise
843
+ }
844
+
845
+ startGame() {
846
+ this.resetGame(); // This sets GameState.PLAYING internally now
847
+ GameState.changeState(GameState.PLAYING); // Ensure UI updates
848
+ this.lastUpdateTime = performance.now(); // Initialize lastUpdateTime
849
+ if (this.gameLoopId) cancelAnimationFrame(this.gameLoopId); // Clear old loop
850
+ this.gameLoop();
851
+ }
852
+
853
+ triggerGameOver() {
854
+ if (this.isGameOver) return; // Prevent multiple triggers
855
+ this.isGameOver = true;
856
+ document.getElementById('finalScore').textContent = `Final Score: ${this.score}`;
857
+
858
+ const highScores = this.loadHighScores();
859
+ if (this.score > 0) {
860
+ highScores.push({ score: this.score, difficulty: this.currentDifficulty, date: new Date().toLocaleDateString() });
861
+ highScores.sort((a, b) => b.score - a.score);
862
+ const topScores = highScores.slice(0, CONFIG.HIGH_SCORES_COUNT);
863
+ localStorage.setItem('snakeHighScores', JSON.stringify(topScores));
864
+ this.highScore = Math.max(this.score, this.highScore); // Update current session high score
865
+ }
866
+ this.updateHighScoresTable();
867
+ document.getElementById('bgMusic').pause();
868
+ GameState.changeState(GameState.GAME_OVER);
869
+ }
870
+
871
+ gameLoop(time) { // time is passed by requestAnimationFrame
872
+ if (!this.isGameOver && GameState.currentState === GameState.PLAYING) {
873
+ this.update(time || performance.now()); // Use performance.now if time is undefined initially
874
+ }
875
+ this.render();
876
+ if (!this.isGameOver) { // Only loop if not game over
877
+ this.gameLoopId = requestAnimationFrame(this.gameLoop.bind(this));
878
+ } else {
879
+ if (this.gameLoopId) cancelAnimationFrame(this.gameLoopId); // Ensure loop stops
880
+ }
881
+ }
882
+
883
+ render() {
884
+ // this.renderer.render(this.scene, this.camera); // When using composer
885
+ if (this.composer) {
886
+ this.composer.render();
887
+ } else if (this.renderer && this.scene && this.camera) {
888
+ this.renderer.render(this.scene, this.camera);
889
  }
890
+ }
891
+
892
+ setupEventListeners() {
893
+ document.addEventListener('keydown', this.handleKeyDown.bind(this));
894
 
895
+ const touchControlsDiv = document.getElementById('touchControls');
896
+ const preventDefaultAndStopPropagation = (e) => { e.preventDefault(); e.stopPropagation(); };
897
+
898
+ document.getElementById('upBtn').addEventListener('touchstart', (e) => { preventDefaultAndStopPropagation(e); this.handleDirectionChange(0, 0, -CONFIG.CELL_SIZE); });
899
+ document.getElementById('downBtn').addEventListener('touchstart', (e) => { preventDefaultAndStopPropagation(e); this.handleDirectionChange(0, 0, CONFIG.CELL_SIZE); });
900
+ document.getElementById('leftBtn').addEventListener('touchstart', (e) => { preventDefaultAndStopPropagation(e); this.handleDirectionChange(-CONFIG.CELL_SIZE, 0, 0); });
901
+ document.getElementById('rightBtn').addEventListener('touchstart', (e) => { preventDefaultAndStopPropagation(e); this.handleDirectionChange(CONFIG.CELL_SIZE, 0, 0); });
902
+
903
+ const gameCanvas = document.getElementById('gameCanvas');
904
+ gameCanvas.addEventListener('touchstart', preventDefaultAndStopPropagation, { passive: false });
905
+ gameCanvas.addEventListener('touchmove', preventDefaultAndStopPropagation, { passive: false });
906
+
907
+ if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
908
+ touchControlsDiv.style.display = 'block';
909
  }
910
 
911
+ document.getElementById('startBtn').addEventListener('click', () => this.startGame());
912
+ document.getElementById('restartBtn').addEventListener('click', () => this.startGame());
913
+ document.getElementById('menuBtn').addEventListener('click', () => GameState.changeState(GameState.MENU));
914
+ document.getElementById('difficultyBtn').addEventListener('click', () => this.cycleDifficulty());
915
+ document.getElementById('resumeBtn').addEventListener('click', () => this.togglePause());
916
+ document.getElementById('quitBtn').addEventListener('click', () => {
917
+ this.isPaused = false; // Ensure unpaused
918
+ if (this.gameLoopId) cancelAnimationFrame(this.gameLoopId); // Stop game loop
919
+ document.getElementById('bgMusic').pause();
920
+ GameState.changeState(GameState.MENU);
921
+ });
922
+ }
923
+
924
+ cycleDifficulty() {
925
+ const difficulties = Object.keys(CONFIG.DIFFICULTY_LEVELS);
926
+ const currentIndex = difficulties.indexOf(this.currentDifficulty);
927
+ const nextIndex = (currentIndex + 1) % difficulties.length;
928
+ this.currentDifficulty = difficulties[nextIndex];
929
+ document.getElementById('difficultyBtn').textContent = `DIFFICULTY: ${this.currentDifficulty.toUpperCase()}`;
930
+ }
931
+
932
+ handleKeyDown(event) {
933
+ if (GameState.currentState === GameState.PLAYING && !this.isPaused) {
934
+ switch(event.key) {
935
+ case 'ArrowUp': case 'w': case 'W': this.handleDirectionChange(0, 0, -CONFIG.CELL_SIZE); event.preventDefault(); break;
936
+ case 'ArrowDown': case 's': case 'S': this.handleDirectionChange(0, 0, CONFIG.CELL_SIZE); event.preventDefault(); break;
937
+ case 'ArrowLeft': case 'a': case 'A': this.handleDirectionChange(-CONFIG.CELL_SIZE, 0, 0); event.preventDefault(); break;
938
+ case 'ArrowRight': case 'd': case 'D': this.handleDirectionChange(CONFIG.CELL_SIZE, 0, 0); event.preventDefault(); break;
939
+ case 'p': case 'P': this.togglePause(); event.preventDefault(); break;
940
+ }
941
+ } else if (GameState.currentState === GameState.PAUSED && (event.key === 'p' || event.key === 'P')) {
942
+ this.togglePause(); event.preventDefault();
943
+ } else if ((GameState.currentState === GameState.GAME_OVER || GameState.currentState === GameState.MENU) && event.key === 'Enter') {
944
+ this.startGame(); event.preventDefault();
945
  }
946
  }
947
+
948
+ handleDirectionChange(dx, dy, dz) {
949
+ const newDir = new THREE.Vector3(dx, dy, dz);
950
+ // Prevent 180-degree turns. Dot product will be negative.
951
+ // direction is already scaled by CELL_SIZE
952
+ if (this.direction.dot(newDir) < - (CONFIG.CELL_SIZE * CONFIG.CELL_SIZE * 0.5) ) { // Compare against negative magnitude squared
953
+ return;
954
+ }
955
+ this.nextDirection.copy(newDir);
956
+ }
957
+
958
+ togglePause() {
959
+ if (GameState.currentState !== GameState.PLAYING && GameState.currentState !== GameState.PAUSED) return;
960
 
961
+ this.isPaused = !this.isPaused;
962
+ if (this.isPaused) {
963
+ GameState.changeState(GameState.PAUSED);
964
+ document.getElementById('bgMusic').pause();
965
+ if (this.gameLoopId) cancelAnimationFrame(this.gameLoopId); // Pause game loop
966
+ } else {
967
+ GameState.changeState(GameState.PLAYING);
968
+ document.getElementById('bgMusic').play().catch(e => console.warn("Music play failed:", e));
969
+ this.lastUpdateTime = performance.now(); // Reset update timer to prevent jump
970
+ this.gameLoop(); // Resume game loop
971
+ }
972
+ }
973
+
974
+ loadHighScores() {
975
+ const scores = localStorage.getItem('snakeHighScores');
976
+ return scores ? JSON.parse(scores) : [];
977
+ }
978
 
979
+ updateHighScoresTable() {
980
+ const highScores = this.loadHighScores();
981
+ const table = document.getElementById('scoresTable');
982
+ while (table.rows.length > 1) { table.deleteRow(1); } // Clear existing
983
+
984
+ highScores.forEach((entry, index) => {
985
+ const row = table.insertRow(-1);
986
+ row.insertCell(0).textContent = index + 1;
987
+ row.insertCell(1).textContent = entry.score;
988
+ row.insertCell(2).textContent = entry.difficulty;
989
+ });
990
+ }
991
+ }
992
+
993
+ // Create and start the game
994
+ const game = new SnakeGame(); // GameState.MENU is set in constructor now.
995
+
996
+ // window.addEventListener('load', () => {
997
+ // // GameState.changeState(GameState.MENU); // Already handled in constructor
998
+ // });
999
+ </script>
1000
  </body>
1001
  </html>