zxciop commited on
Commit
27457d9
·
verified ·
1 Parent(s): 5cba0a0

Create mSnake.html

Browse files
Files changed (1) hide show
  1. mSnake.html +1185 -0
mSnake.html ADDED
@@ -0,0 +1,1185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Matrix Snake 3D - Enhanced</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ overflow: hidden;
11
+ background-color: #000;
12
+ color: #0f0;
13
+ font-family: 'Courier New', Courier, monospace;
14
+ }
15
+ canvas {
16
+ display: block;
17
+ }
18
+ .game-ui {
19
+ position: absolute;
20
+ padding: 10px;
21
+ background-color: rgba(0, 20, 0, 0.8);
22
+ border: 1px solid #0f0;
23
+ border-radius: 5px;
24
+ font-size: 1.2em;
25
+ pointer-events: none;
26
+ }
27
+ #info {
28
+ top: 10px;
29
+ left: 10px;
30
+ }
31
+ #combo {
32
+ top: 10px;
33
+ right: 10px;
34
+ color: #0ff;
35
+ opacity: 0;
36
+ transition: opacity 0.3s;
37
+ }
38
+ #gameScreen {
39
+ position: absolute;
40
+ top: 0;
41
+ left: 0;
42
+ width: 100%;
43
+ height: 100%;
44
+ display: flex;
45
+ flex-direction: column;
46
+ justify-content: center;
47
+ align-items: center;
48
+ background-color: rgba(0, 10, 0, 0.8);
49
+ z-index: 10;
50
+ }
51
+ #startScreen, #gameOverScreen {
52
+ padding: 30px;
53
+ background-color: rgba(0, 30, 0, 0.9);
54
+ border: 2px solid #0f0;
55
+ border-radius: 10px;
56
+ text-align: center;
57
+ max-width: 500px;
58
+ }
59
+ #gameOverScreen {
60
+ border-color: #f00;
61
+ }
62
+ .title {
63
+ font-size: 2.5em;
64
+ margin-bottom: 20px;
65
+ text-shadow: 0 0 10px #0f0;
66
+ }
67
+ .subtitle {
68
+ font-size: 1.2em;
69
+ margin-bottom: 30px;
70
+ }
71
+ .button {
72
+ display: inline-block;
73
+ padding: 10px 20px;
74
+ margin: 10px;
75
+ background-color: rgba(0, 80, 0, 0.8);
76
+ border: 1px solid #0f0;
77
+ border-radius: 5px;
78
+ color: #0f0;
79
+ cursor: pointer;
80
+ transition: all 0.2s;
81
+ pointer-events: auto;
82
+ }
83
+ .button:hover {
84
+ background-color: rgba(0, 120, 0, 0.9);
85
+ transform: scale(1.05);
86
+ }
87
+ .controls {
88
+ margin-top: 20px;
89
+ font-size: 0.9em;
90
+ opacity: 0.8;
91
+ }
92
+ #highScores {
93
+ margin-top: 20px;
94
+ text-align: left;
95
+ width: 100%;
96
+ }
97
+ #highScores table {
98
+ width: 100%;
99
+ border-collapse: collapse;
100
+ }
101
+ #highScores th, #highScores td {
102
+ padding: 5px;
103
+ border-bottom: 1px solid rgba(0, 255, 0, 0.5);
104
+ }
105
+ #touchControls {
106
+ position: absolute;
107
+ bottom: 20px;
108
+ left: 50%;
109
+ transform: translateX(-50%);
110
+ display: none; /* Hidden by default, shown on mobile */
111
+ }
112
+ .touchBtn {
113
+ width: 60px;
114
+ height: 60px;
115
+ background-color: rgba(0, 50, 0, 0.5);
116
+ border: 1px solid #0f0;
117
+ border-radius: 50%;
118
+ margin: 5px;
119
+ display: inline-flex;
120
+ justify-content: center;
121
+ align-items: center;
122
+ font-size: 20px;
123
+ cursor: pointer;
124
+ pointer-events: auto;
125
+ }
126
+ /* Matrix animation background */
127
+ #matrixCanvas {
128
+ position: fixed;
129
+ top: 0;
130
+ left: 0;
131
+ z-index: -1;
132
+ }
133
+ </style>
134
+ </head>
135
+ <body>
136
+ <!-- Matrix background -->
137
+ <canvas id="matrixCanvas"></canvas>
138
+
139
+ <!-- Game canvas -->
140
+ <canvas id="gameCanvas"></canvas>
141
+
142
+ <!-- Game UI -->
143
+ <div id="info" class="game-ui">Score: 0 | High: 0</div>
144
+ <div id="combo" class="game-ui">Combo x1!</div>
145
+
146
+ <!-- Touch controls for mobile -->
147
+ <div id="touchControls">
148
+ <div class="touchBtn" id="upBtn">↑</div>
149
+ <div style="display: flex;">
150
+ <div class="touchBtn" id="leftBtn">←</div>
151
+ <div class="touchBtn" id="downBtn">↓</div>
152
+ <div class="touchBtn" id="rightBtn">→</div>
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Game screens -->
157
+ <div id="gameScreen">
158
+ <div id="startScreen">
159
+ <div class="title">MATRIX SNAKE 3D</div>
160
+ <div class="subtitle">Navigate the digital realm. Collect data packets. Avoid system firewalls.</div>
161
+ <div class="button" id="startBtn">START GAME</div>
162
+ <div class="button" id="difficultyBtn">DIFFICULTY: NORMAL</div>
163
+ <div class="controls">
164
+ Use Arrow Keys to change direction<br>
165
+ Press P to pause the game
166
+ </div>
167
+ <div id="highScores">
168
+ <h3>HIGH SCORES</h3>
169
+ <table id="scoresTable">
170
+ <tr><th>RANK</th><th>SCORE</th><th>DIFFICULTY</th></tr>
171
+ </table>
172
+ </div>
173
+ </div>
174
+ <div id="gameOverScreen" style="display: none;">
175
+ <div class="title" style="color: #f00;">SYSTEM FAILURE</div>
176
+ <div id="finalScore" class="subtitle">Final Score: 0</div>
177
+ <div class="button" id="restartBtn">RESTART</div>
178
+ <div class="button" id="menuBtn">MAIN MENU</div>
179
+ </div>
180
+ <div id="pauseScreen" style="display: none;">
181
+ <div class="title">PAUSED</div>
182
+ <div class="subtitle">Press P to resume</div>
183
+ <div class="button" id="resumeBtn">RESUME</div>
184
+ <div class="button" id="quitBtn">QUIT</div>
185
+ </div>
186
+ </div>
187
+
188
+ <!-- Audio elements -->
189
+ <audio id="eatSound" preload="auto">
190
+ <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAANIAqJWUEQAFO+gRc5TRJIkiRJEiL///////////8RERERERERVVVVVVVVVVVVVVJEREREREVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/jGMQJA/Aa1flBABBTpGX9hDGMYxw7/+MMYxd/4wxIiI9////jDEQ7/jdEiJERERBaIiIzMzMzIiIiP//MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM/+MYxB4AAANIAAAAADMzMzMzMzMzMzMzMzMzMzMzM" type="audio/mpeg">
191
+ </audio>
192
+ <audio id="gameOverSound" preload="auto">
193
+ <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAKMFqZVQEwAhGKzc+FSIiIiIiIiIj4+Pj4+Pj4+Pj4+JIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIv/jIMQNAAAP8AEAAAI+IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIv/jEMQQAAAP8AAAAAI+IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIg==" type="audio/mpeg">
194
+ </audio>
195
+ <audio id="bgMusic" loop preload="auto">
196
+ <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAJcAKRWQEQAFNfQRc5znOc5znP/////////uc5znOc5znOc5znEREREREREREREMYxjGMY/+MYxBEJkFahX4wwAjGMYxjGMYxjGMYxERERERERESIiIiL//////////////+MYxBQG4AqlX8MQAu/////////////////////jIMQVBVwCqVfwBAC/////////////////" type="audio/mpeg">
197
+ </audio>
198
+
199
+
200
+ // <!-- <script type="importmap">
201
+ // {
202
+ // "imports": {
203
+ "three": "https://unpkg.com/three@0.163.0/build/three.module.js",
204
+ "three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
205
+ // }
206
+ // }
207
+ // </script> -->
208
+
209
+ <script type="module">
210
+ import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
211
+ import { EffectComposer } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js';
212
+ import { RenderPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/RenderPass.js';
213
+ import { UnrealBloomPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/UnrealBloomPass.js';
214
+
215
+ // Game configuration
216
+ const CONFIG = {
217
+ GRID_SIZE: 25, // Number of units across/deep
218
+ CELL_SIZE: 1, // Size of each grid cell/snake segment
219
+ BASE_SPEED: 150, // Base milliseconds between updates
220
+ DIFFICULTY_LEVELS: {
221
+ 'EASY': { speedMultiplier: 1.3, obstacleMultiplier: 0.5 },
222
+ 'NORMAL': { speedMultiplier: 1.0, obstacleMultiplier: 1.0 },
223
+ 'HARD': { speedMultiplier: 0.7, obstacleMultiplier: 1.5 }
224
+ },
225
+ MAX_OBSTACLE_COUNT: 10, // Maximum number of obstacles
226
+ FOOD_TYPES: [
227
+ { type: 'regular', color: 0x00ff00, points: 1, speedEffect: 0 },
228
+ { type: 'special', color: 0x00ffff, points: 5, speedEffect: -10 },
229
+ { type: 'rare', color: 0xff00ff, points: 10, speedEffect: 10 }
230
+ ],
231
+ COMBO_TIMEOUT: 5000, // Milliseconds to get next food for combo
232
+ HIGH_SCORES_COUNT: 5 // Number of high scores to save
233
+ };
234
+
235
+ // --- Particle System for Effects ---
236
+ class ParticleSystem {
237
+ constructor(scene) {
238
+ this.scene = scene;
239
+ this.particles = [];
240
+
241
+ // Shared geometry for all particles
242
+ this.geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
243
+ }
244
+
245
+ createFoodEffect(position, color) {
246
+ const count = 20; // Number of particles
247
+
248
+ for (let i = 0; i < count; i++) {
249
+ // Create particle
250
+ const material = new THREE.MeshBasicMaterial({
251
+ color: color || 0x00ff00,
252
+ transparent: true,
253
+ opacity: 0.9
254
+ });
255
+
256
+ const particle = new THREE.Mesh(this.geometry, material);
257
+
258
+ // Set initial position
259
+ particle.position.copy(position);
260
+
261
+ // Set random velocity
262
+ const velocity = new THREE.Vector3(
263
+ (Math.random() - 0.5) * 0.1,
264
+ (Math.random()) * 0.1,
265
+ (Math.random() - 0.5) * 0.1
266
+ );
267
+
268
+ // Add to scene
269
+ this.scene.add(particle);
270
+
271
+ // Store particle data
272
+ this.particles.push({
273
+ mesh: particle,
274
+ velocity: velocity,
275
+ life: 1.0, // Life from 1.0 to 0.0
276
+ decay: 0.02 + Math.random() * 0.03 // Random decay rate
277
+ });
278
+ }
279
+ }
280
+
281
+ update() {
282
+ // Update all particles
283
+ for (let i = this.particles.length - 1; i >= 0; i--) {
284
+ const particle = this.particles[i];
285
+
286
+ // Update position
287
+ particle.mesh.position.add(particle.velocity);
288
+
289
+ // Simulate gravity
290
+ particle.velocity.y -= 0.003;
291
+
292
+ // Update life
293
+ particle.life -= particle.decay;
294
+
295
+ // Update opacity based on life
296
+ particle.mesh.material.opacity = particle.life;
297
+
298
+ // Remove dead particles
299
+ if (particle.life <= 0) {
300
+ this.scene.remove(particle.mesh);
301
+ particle.mesh.material.dispose();
302
+ this.particles.splice(i, 1);
303
+ }
304
+ }
305
+ }
306
+
307
+ clear() {
308
+ // Remove all particles
309
+ for (const particle of this.particles) {
310
+ this.scene.remove(particle.mesh);
311
+ particle.mesh.material.dispose();
312
+ particle.mesh.geometry.dispose();
313
+ }
314
+ this.particles = [];
315
+ }
316
+ }
317
+
318
+ // Game state management
319
+ const GameState = {
320
+ MENU: 'menu',
321
+ PLAYING: 'playing',
322
+ PAUSED: 'paused',
323
+ GAME_OVER: 'gameOver',
324
+ currentState: 'menu',
325
+
326
+ changeState(newState) {
327
+ this.currentState = newState;
328
+
329
+ // Handle UI changes based on state
330
+ switch(newState) {
331
+ case this.MENU:
332
+ document.getElementById('gameScreen').style.display = 'flex';
333
+ document.getElementById('startScreen').style.display = 'block';
334
+ document.getElementById('gameOverScreen').style.display = 'none';
335
+ break;
336
+ case this.PLAYING:
337
+ document.getElementById('gameScreen').style.display = 'none';
338
+ break;
339
+ case this.PAUSED:
340
+ document.getElementById('gameScreen').style.display = 'flex';
341
+ document.getElementById('startScreen').style.display = 'none';
342
+ document.getElementById('gameOverScreen').style.display = 'none';
343
+ document.getElementById('pauseScreen').style.display = 'block';
344
+ break;
345
+ case this.GAME_OVER:
346
+ document.getElementById('gameScreen').style.display = 'flex';
347
+ document.getElementById('startScreen').style.display = 'none';
348
+ document.getElementById('gameOverScreen').style.display = 'block';
349
+ // The score will be updated by the game instance when it triggers game over
350
+ document.getElementById('gameOverSound').play();
351
+ break;
352
+ }
353
+ }
354
+ };
355
+
356
+ // --- Matrix Rain Background Effect ---
357
+ class MatrixRain {
358
+ constructor() {
359
+ this.canvas = document.getElementById('matrixCanvas');
360
+ this.ctx = this.canvas.getContext('2d');
361
+ this.resize();
362
+
363
+ this.fontSize = 14;
364
+ this.columns = Math.floor(this.canvas.width / this.fontSize);
365
+ this.drops = [];
366
+ this.characters = '01アイウエオカキクケコサシスセソタチツテトナニヌネ<>{}[]()+-*/%=#@&?*:・゚✧ ≡ ░▒░▒░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█䷀ ▙⁞ ░▒▓█║│ ·▓▒░█▄▀■■▄▬▌▐ ⁞▏▄▀■■▄▬▌▐ ▄▀■■▄▬▌▐ . ▛ ⁞▏ ▏ ⁚⁝ .';
367
+
368
+ this.resetDrops();
369
+
370
+ this.animate = this.animate.bind(this);
371
+ this.animate();
372
+
373
+ window.addEventListener('resize', this.handleResize.bind(this));
374
+ }
375
+
376
+ handleResize() {
377
+ this.resize();
378
+ this.columns = Math.floor(this.canvas.width / this.fontSize);
379
+ this.resetDrops();
380
+ }
381
+
382
+ resize() {
383
+ this.canvas.width = window.innerWidth;
384
+ this.canvas.height = window.innerHeight;
385
+ }
386
+
387
+ resetDrops() {
388
+ this.drops = [];
389
+ for(let i = 0; i < this.columns; i++) {
390
+ // Start drops at random negative positions for staggered effect
391
+ this.drops[i] = Math.floor(Math.random() * -100);
392
+ }
393
+ }
394
+
395
+ animate() {
396
+ if (GameState.currentState === GameState.PLAYING) {
397
+ // Semi-transparent background to create fade effect
398
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
399
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
400
+
401
+ this.ctx.fillStyle = '#0f0';
402
+ this.ctx.font = this.fontSize + 'px monospace';
403
+
404
+ for(let i = 0; i < this.drops.length; i++) {
405
+ // Choose a random character
406
+ const text = this.characters.charAt(Math.floor(Math.random() * this.characters.length));
407
+
408
+ // Draw the character
409
+ this.ctx.fillText(text, i * this.fontSize, this.drops[i] * this.fontSize);
410
+
411
+ // Move drops down and reset when off the screen
412
+ if(this.drops[i] * this.fontSize > this.canvas.height && Math.random() > 0.975) {
413
+ this.drops[i] = 0;
414
+ }
415
+ this.drops[i]++;
416
+ }
417
+ }
418
+ requestAnimationFrame(this.animate);
419
+ }
420
+ }
421
+
422
+ // --- Object pooling for performance optimization ---
423
+ class ObjectPool {
424
+ constructor(createFunc, initialCount = 10) {
425
+ this.pool = [];
426
+ this.createFunc = createFunc;
427
+
428
+ // Populate the pool initially
429
+ for (let i = 0; i < initialCount; i++) {
430
+ this.pool.push(this.createFunc());
431
+ }
432
+ }
433
+
434
+ get() {
435
+ if (this.pool.length > 0) {
436
+ return this.pool.pop();
437
+ }
438
+ return this.createFunc();
439
+ }
440
+
441
+ release(object) {
442
+ this.pool.push(object);
443
+ }
444
+
445
+ clear() {
446
+ this.pool = [];
447
+ }
448
+ }
449
+
450
+ // --- Main Game Class ---
451
+ class SnakeGame {
452
+ constructor() {
453
+ // Initialize properties
454
+ this.scene = null;
455
+ this.camera = null;
456
+ this.renderer = null;
457
+ this.snake = [];
458
+ this.food = null;
459
+ this.obstacles = [];
460
+ this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
461
+ this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
462
+ this.score = 0;
463
+ this.highScore = this.loadHighScores()[0]?.score || 0;
464
+ this.gameSpeed = CONFIG.BASE_SPEED;
465
+ this.lastUpdateTime = 0;
466
+ this.isGameOver = false;
467
+ this.isPaused = false;
468
+ this.gameLoopId = null;
469
+ this.bounds = Math.floor(CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE;
470
+ this.obstacleCount = CONFIG.MAX_OBSTACLE_COUNT;
471
+ this.comboCount = 0;
472
+ this.lastFoodTime = 0;
473
+ this.currentDifficulty = 'NORMAL';
474
+ this.particleSystem = null;
475
+ this.headLight = null;
476
+
477
+ // Initialize materials
478
+ this.materials = {
479
+ snakeHead: new THREE.MeshStandardMaterial({
480
+ color: 0x0000ff,
481
+ emissive: 0x39FF14,
482
+ roughness: 0.8,
483
+ metalness: 0.22
484
+ }),
485
+ snakeBody: new THREE.MeshStandardMaterial({
486
+ color: 0x00ff00,
487
+ emissive: 0x005500,
488
+ roughness: 0.3,
489
+ metalness: 0.72
490
+ }),
491
+ food: new THREE.MeshBasicMaterial({
492
+ color: 0x00ff00,
493
+ wireframe: true
494
+ }),
495
+ obstacle: new THREE.MeshBasicMaterial({
496
+ color: 0x008800,
497
+ wireframe: true
498
+ }),
499
+ specialFood: new THREE.MeshBasicMaterial({
500
+ color: 0x00ffff,
501
+ wireframe: true
502
+ }),
503
+ rareFood: new THREE.MeshBasicMaterial({
504
+ color: 0xff00ff,
505
+ wireframe: true
506
+ })
507
+ };
508
+
509
+ // Initialize geometries
510
+ this.geometries = {
511
+ segment: new THREE.BoxGeometry(
512
+ CONFIG.CELL_SIZE,
513
+ CONFIG.CELL_SIZE,
514
+ CONFIG.CELL_SIZE
515
+ ),
516
+ foodBox: new THREE.BoxGeometry(
517
+ CONFIG.CELL_SIZE * 0.8,
518
+ CONFIG.CELL_SIZE * 0.8,
519
+ CONFIG.CELL_SIZE * 0.8
520
+ ),
521
+ foodSphere: new THREE.SphereGeometry(
522
+ CONFIG.CELL_SIZE * 0.5,
523
+ 16,
524
+ 12
525
+ ),
526
+ foodTetrahedron: new THREE.TetrahedronGeometry(
527
+ CONFIG.CELL_SIZE * 0.6,
528
+ 0
529
+ ),
530
+ obstacle: new THREE.BoxGeometry(
531
+ CONFIG.CELL_SIZE,
532
+ CONFIG.CELL_SIZE * 1.5,
533
+ CONFIG.CELL_SIZE
534
+ )
535
+ };
536
+
537
+ // Initialize object pools
538
+ this.segmentPool = new ObjectPool(() => {
539
+ return new THREE.Mesh(this.geometries.segment, this.materials.snakeBody.clone());
540
+ }, 20);
541
+
542
+ this.obstaclePool = new ObjectPool(() => {
543
+ return new THREE.Mesh(this.geometries.obstacle, this.materials.obstacle);
544
+ }, CONFIG.MAX_OBSTACLE_COUNT * 1.5);
545
+
546
+ // Initialize the game
547
+ this.setupEventListeners();
548
+ this.init();
549
+
550
+ // Create the matrix rain effect
551
+ this.matrixRain = new MatrixRain();
552
+
553
+ // Update high scores display
554
+ this.updateHighScoresTable();
555
+ }
556
+
557
+ // Place food at random position
558
+ placeFood() {
559
+ let foodPos;
560
+ let validPosition = false;
561
+ let attempts = 0;
562
+ const maxAttempts = 100; // Prevent infinite loop
563
+
564
+ while (!validPosition && attempts < maxAttempts) {
565
+ foodPos = new THREE.Vector3(
566
+ Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE,
567
+ 0,
568
+ Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE
569
+ );
570
+
571
+ // Check collision with snake
572
+ let collisionWithSnake = this.snake.some(segment =>
573
+ segment.position.distanceTo(foodPos) < CONFIG.CELL_SIZE * 0.9
574
+ );
575
+
576
+ // Check collision with obstacles
577
+ let collisionWithObstacle = this.obstacles.some(obstacle =>
578
+ obstacle.position.distanceTo(foodPos) < CONFIG.CELL_SIZE * 0.9
579
+ );
580
+
581
+ validPosition = !collisionWithSnake && !collisionWithObstacle;
582
+ attempts++;
583
+ }
584
+
585
+ if (validPosition) {
586
+ this.food.position.copy(foodPos);
587
+ } else {
588
+ // Fallback in case we can't find a position after max attempts
589
+ console.warn("Could not find valid position for food after max attempts");
590
+ this.food.position.set(0, 5, 0); // Place above play area
591
+ }
592
+ }
593
+
594
+ // Create obstacles
595
+ createObstacles() {
596
+ // Clear existing obstacles
597
+ for (const obstacle of this.obstacles) {
598
+ this.scene.remove(obstacle);
599
+ }
600
+ this.obstacles = [];
601
+
602
+ // Create new obstacles
603
+ for (let i = 0; i < this.obstacleCount; i++) {
604
+ let obstaclePos;
605
+ let validPosition = false;
606
+ let attempts = 0;
607
+ const maxAttempts = 50;
608
+
609
+ while (!validPosition && attempts < maxAttempts) {
610
+ obstaclePos = new THREE.Vector3(
611
+ Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE,
612
+ 0,
613
+ Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE
614
+ );
615
+
616
+ // Check distance from snake start position
617
+ let tooCloseToStart = obstaclePos.length() < CONFIG.CELL_SIZE * 3;
618
+
619
+ // Check collision with snake and other obstacles
620
+ let collisionWithSnake = this.snake.some(segment =>
621
+ segment.position.distanceTo(obstaclePos) < CONFIG.CELL_SIZE * 2
622
+ );
623
+
624
+ let collisionWithObstacle = this.obstacles.some(obstacle =>
625
+ obstacle.position.distanceTo(obstaclePos) < CONFIG.CELL_SIZE
626
+ );
627
+
628
+ validPosition = !tooCloseToStart && !collisionWithSnake && !collisionWithObstacle;
629
+ attempts++;
630
+ }
631
+
632
+ if (validPosition) {
633
+ const obstacle = new THREE.Mesh(this.geometries.segment, this.materials.obstacle);
634
+ obstacle.position.copy(obstaclePos);
635
+ this.obstacles.push(obstacle);
636
+ this.scene.add(obstacle);
637
+ }
638
+ }
639
+ }
640
+
641
+ // Clear all game objects for a new game
642
+ clearGameObjects() {
643
+ // Clear snake
644
+ for (const segment of this.snake) {
645
+ this.scene.remove(segment);
646
+ this.segmentPool.release(segment);
647
+ }
648
+ this.snake = [];
649
+
650
+ // Clear food
651
+ if (this.food) {
652
+ this.scene.remove(this.food);
653
+ this.food = null;
654
+ }
655
+
656
+ // Clear obstacles
657
+ for (const obstacle of this.obstacles) {
658
+ this.scene.remove(obstacle);
659
+ }
660
+ this.obstacles = [];
661
+
662
+ // Clear particles
663
+ this.particleSystem.clear();
664
+ }
665
+
666
+ // Update game logic
667
+ update(time) {
668
+ // Skip update if game is paused, over, or not playing
669
+ if (this.isPaused || this.isGameOver || GameState.currentState !== GameState.PLAYING) {
670
+ return;
671
+ }
672
+
673
+ // Control game speed
674
+ if (time - this.lastUpdateTime < this.gameSpeed) {
675
+ return;
676
+ }
677
+ this.lastUpdateTime = time;
678
+
679
+ // Update direction safely
680
+ this.direction = this.nextDirection.clone();
681
+
682
+ // Get current head position
683
+ const head = this.snake[0];
684
+ const newHeadPos = head.position.clone().add(
685
+ this.direction.clone().multiplyScalar(CONFIG.CELL_SIZE)
686
+ );
687
+
688
+ // Check collision with walls
689
+ const halfGrid = CONFIG.GRID_SIZE / 2;
690
+ if (
691
+ newHeadPos.x > halfGrid * CONFIG.CELL_SIZE ||
692
+ newHeadPos.x < -halfGrid * CONFIG.CELL_SIZE ||
693
+ newHeadPos.z > halfGrid * CONFIG.CELL_SIZE ||
694
+ newHeadPos.z < -halfGrid * CONFIG.CELL_SIZE
695
+ ) {
696
+ this.triggerGameOver();
697
+ return;
698
+ }
699
+
700
+ // Check collision with self
701
+ for (let i = 1; i < this.snake.length; i++) {
702
+ if (newHeadPos.distanceTo(this.snake[i].position) < CONFIG.CELL_SIZE * 0.25) {
703
+ this.triggerGameOver();
704
+ return;
705
+ }
706
+ }
707
+
708
+ // Check collision with obstacles
709
+ for (const obstacle of this.obstacles) {
710
+ if (newHeadPos.distanceTo(obstacle.position) < CONFIG.CELL_SIZE * 0.5) {
711
+ this.triggerGameOver();
712
+ return;
713
+ }
714
+ }
715
+
716
+ // Create new head segment
717
+ const newHead = this.segmentPool.get();
718
+ newHead.position.copy(newHeadPos);
719
+ this.snake.unshift(newHead);
720
+ this.scene.add(newHead);
721
+
722
+ // Check for food collision
723
+ if (this.food && newHeadPos.distanceTo(this.food.position) < CONFIG.CELL_SIZE * 0.5) {
724
+ // Get food properties
725
+ const foodType = this.food.userData;
726
+
727
+ // Increase score
728
+ const basePoints = foodType.points || 1;
729
+
730
+ // Handle combo system
731
+ const currentTime = performance.now();
732
+ if (currentTime - this.lastFoodTime < CONFIG.COMBO_TIMEOUT) {
733
+ this.comboCount++;
734
+ } else {
735
+ this.comboCount = 1;
736
+ }
737
+ this.lastFoodTime = currentTime;
738
+
739
+ // Calculate final score with combo multiplier
740
+ const points = basePoints * this.comboCount;
741
+ this.score += points;
742
+
743
+ // Show combo
744
+ if (this.comboCount > 1) {
745
+ const comboElement = document.getElementById('combo');
746
+ comboElement.textContent = `Combo x${this.comboCount}! +${points}`;
747
+ comboElement.style.opacity = 1;
748
+
749
+ // Hide combo text after a delay
750
+ setTimeout(() => {
751
+ comboElement.style.opacity = 0;
752
+ }, 2000);
753
+ }
754
+
755
+ // Update score display
756
+ document.getElementById('info').textContent = `Score: ${this.score} | High: ${Math.max(this.score, this.highScore)}`;
757
+
758
+ // Apply speed effect from food type
759
+ if (foodType.speedEffect) {
760
+ this.gameSpeed = Math.max(50, this.gameSpeed - foodType.speedEffect);
761
+ }
762
+
763
+ // Play eat sound
764
+ document.getElementById('eatSound').play();
765
+
766
+ // Create particle effect at food position
767
+ this.particleSystem.createFoodEffect(this.food.position.clone(), foodType.color);
768
+
769
+ // Place new food
770
+ this.chooseFoodType();
771
+ this.placeFood();
772
+ } else {
773
+ // Remove tail if not eating
774
+ const tail = this.snake.pop();
775
+ this.scene.remove(tail);
776
+ this.segmentPool.release(tail);
777
+ }
778
+
779
+ // Update particles
780
+ this.particleSystem.update();
781
+
782
+ // Animate snake segments (subtle wave effect)
783
+ for (let i = 0; i < this.snake.length; i++) {
784
+ const segment = this.snake[i];
785
+ segment.rotation.y = Math.sin(time * 0.001 + i * 0.2) * 0.1;
786
+ segment.position.y = Math.sin(time * 0.002 + i * 0.1) * 0.2;
787
+ }
788
+
789
+ // Animate food
790
+ if (this.food) {
791
+ this.food.rotation.y += 0.05;
792
+ this.food.position.y = Math.sin(time * 0.002) * 0.3;
793
+ }
794
+ }
795
+
796
+ // Choose a food type based on probability
797
+ chooseFoodType() {
798
+ // Food type probability
799
+ const rand = Math.random();
800
+ let foodType;
801
+
802
+ if (rand < 0.05) { // 5% chance for rare food
803
+ foodType = CONFIG.FOOD_TYPES[2];
804
+ } else if (rand < 0.25) { // 20% chance for special food
805
+ foodType = CONFIG.FOOD_TYPES[1];
806
+ } else { // 75% chance for regular food
807
+ foodType = CONFIG.FOOD_TYPES[0];
808
+ }
809
+
810
+ // Create food mesh with appropriate material
811
+ let material;
812
+ switch(foodType.type) {
813
+ case 'special':
814
+ material = this.materials.specialFood;
815
+ break;
816
+ case 'rare':
817
+ material = this.materials.rareFood;
818
+ break;
819
+ default:
820
+ material = this.materials.food;
821
+ }
822
+
823
+ // Create or update food mesh
824
+ if (!this.food) {
825
+ this.food = new THREE.Mesh(
826
+ this.geometries.segment,
827
+ material
828
+ );
829
+ this.scene.add(this.food);
830
+ } else {
831
+ this.food.material = material;
832
+ }
833
+
834
+ // Store food type data
835
+ this.food.userData = foodType;
836
+ }
837
+
838
+ // Reset the game
839
+ resetGame() {
840
+ // Clear all game objects
841
+ this.clearGameObjects();
842
+
843
+ // Stop any playing audio
844
+ document.getElementById('eatSound').pause();
845
+ document.getElementById('gameOverSound').pause();
846
+ document.getElementById('bgMusic').pause();
847
+
848
+ // Reset game state
849
+ this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
850
+ this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
851
+ this.score = 0;
852
+ this.gameSpeed = CONFIG.BASE_SPEED * CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].speedMultiplier;
853
+ this.isGameOver = false;
854
+ this.isPaused = false;
855
+ this.comboCount = 0;
856
+ this.lastFoodTime = 0;
857
+
858
+ // Update obstacle count based on difficulty
859
+ this.obstacleCount = Math.floor(CONFIG.MAX_OBSTACLE_COUNT *
860
+ CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].obstacleMultiplier);
861
+
862
+ // Create initial snake
863
+ const startSegment = this.segmentPool.get();
864
+ startSegment.position.set(0, 0, 0);
865
+ this.snake.push(startSegment);
866
+ this.scene.add(startSegment);
867
+
868
+ // Create food
869
+ this.chooseFoodType();
870
+ this.placeFood();
871
+
872
+ // Create obstacles
873
+ this.createObstacles();
874
+
875
+ // Update score display
876
+ document.getElementById('info').textContent = `Score: ${this.score} | High: ${this.highScore}`;
877
+
878
+ // Start background music
879
+ const music = document.getElementById('bgMusic');
880
+ music.volume = 0.3;
881
+ music.play();
882
+ }
883
+
884
+ // Start the game
885
+ startGame() {
886
+ this.resetGame();
887
+ GameState.changeState(GameState.PLAYING);
888
+ this.gameLoop();
889
+ }
890
+
891
+ // Game over
892
+ triggerGameOver() {
893
+ this.isGameOver = true;
894
+
895
+ // Update final score display
896
+ document.getElementById('finalScore').textContent = `Final Score: ${this.score}`;
897
+
898
+ // Check for high score
899
+ const highScores = this.loadHighScores();
900
+ if (this.score > 0) {
901
+ // Add current score to high scores
902
+ highScores.push({
903
+ score: this.score,
904
+ difficulty: this.currentDifficulty,
905
+ date: new Date().toLocaleDateString()
906
+ });
907
+
908
+ // Sort high scores
909
+ highScores.sort((a, b) => b.score - a.score);
910
+
911
+ // Keep only top scores
912
+ const topScores = highScores.slice(0, CONFIG.HIGH_SCORES_COUNT);
913
+
914
+ // Save high scores
915
+ localStorage.setItem('snakeHighScores', JSON.stringify(topScores));
916
+
917
+ // Update high score if needed
918
+ this.highScore = Math.max(this.score, this.highScore);
919
+ }
920
+
921
+ // Update high scores table
922
+ this.updateHighScoresTable();
923
+
924
+ // Stop background music
925
+ document.getElementById('bgMusic').pause();
926
+
927
+ // Change game state to game over
928
+ GameState.changeState(GameState.GAME_OVER);
929
+ }
930
+
931
+ // Game loop
932
+ gameLoop(time) {
933
+ // Update current time
934
+ if (!time) time = 0;
935
+
936
+ // Update game
937
+ this.update(time);
938
+
939
+ // Render scene
940
+ this.render();
941
+
942
+ // Continue game loop
943
+ this.gameLoopId = requestAnimationFrame(this.gameLoop.bind(this));
944
+ }
945
+
946
+ // Render scene
947
+ render() {
948
+ this.renderer.render(this.scene, this.camera);
949
+ }
950
+
951
+ // Initialize game
952
+ init() {
953
+ // Create scene
954
+ this.scene = new THREE.Scene();
955
+ this.scene.background = new THREE.Color(0x000000);
956
+
957
+ // Add fog for depth
958
+ this.scene.fog = new THREE.Fog(0x000500, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 2.5);
959
+
960
+ // Create camera
961
+ this.camera = new THREE.PerspectiveCamera(
962
+ 65, window.innerWidth / window.innerHeight, 0.1, 1000
963
+ );
964
+ this.camera.position.set(0, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 0.9);
965
+ this.camera.lookAt(0, -CONFIG.GRID_SIZE * 0.1, 0);
966
+
967
+ // Create renderer
968
+ this.renderer = new THREE.WebGLRenderer({
969
+ canvas: document.getElementById('gameCanvas'),
970
+ antialias: true,
971
+ alpha: true
972
+ });
973
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
974
+ this.renderer.setPixelRatio(window.devicePixelRatio);
975
+
976
+ // Create grid for visual reference
977
+ const gridHelper = new THREE.GridHelper(
978
+ CONFIG.GRID_SIZE * CONFIG.CELL_SIZE,
979
+ CONFIG.GRID_SIZE,
980
+ 0x005500,
981
+ 0x003300
982
+ );
983
+ gridHelper.position.y = -CONFIG.CELL_SIZE / 2;
984
+ this.scene.add(gridHelper);
985
+
986
+ // Add ambient light
987
+ const ambientLight = new THREE.AmbientLight(0x404060);
988
+ this.scene.add(ambientLight);
989
+
990
+ // Add directional light
991
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
992
+ directionalLight.position.set(5, 10, 7);
993
+ this.scene.add(directionalLight);
994
+
995
+ // Create head light
996
+ this.headLight = new THREE.PointLight(0x00ff00, 1, CONFIG.CELL_SIZE * 3);
997
+ this.scene.add(this.headLight);
998
+
999
+ // Create particle system
1000
+ this.particleSystem = new ParticleSystem(this.scene);
1001
+
1002
+ // Handle window resize
1003
+ window.addEventListener('resize', () => {
1004
+ this.camera.aspect = window.innerWidth / window.innerHeight;
1005
+ this.camera.updateProjectionMatrix();
1006
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
1007
+ });
1008
+ }
1009
+
1010
+ // Set up event listeners
1011
+ setupEventListeners() {
1012
+ // Keyboard controls
1013
+ document.addEventListener('keydown', this.handleKeyDown.bind(this));
1014
+
1015
+ // Touch controls with prevention
1016
+ const touchControls = document.getElementById('touchControls');
1017
+ const preventDefault = (e) => {
1018
+ e.preventDefault();
1019
+ e.stopPropagation();
1020
+ };
1021
+
1022
+ document.getElementById('upBtn').addEventListener('touchstart', (e) => {
1023
+ preventDefault(e);
1024
+ this.handleDirectionChange(0, 0, -1);
1025
+ });
1026
+ document.getElementById('downBtn').addEventListener('touchstart', (e) => {
1027
+ preventDefault(e);
1028
+ this.handleDirectionChange(0, 0, 1);
1029
+ });
1030
+ document.getElementById('leftBtn').addEventListener('touchstart', (e) => {
1031
+ preventDefault(e);
1032
+ this.handleDirectionChange(-1, 0, 0);
1033
+ });
1034
+ document.getElementById('rightBtn').addEventListener('touchstart', (e) => {
1035
+ preventDefault(e);
1036
+ this.handleDirectionChange(1, 0, 0);
1037
+ });
1038
+
1039
+ // Prevent touch events on game canvas
1040
+ document.getElementById('gameCanvas').addEventListener('touchstart', preventDefault, { passive: false });
1041
+ document.getElementById('gameCanvas').addEventListener('touchmove', preventDefault, { passive: false });
1042
+
1043
+ // Show touch controls on mobile devices
1044
+ if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
1045
+ touchControls.style.display = 'block';
1046
+ }
1047
+
1048
+ // UI buttons
1049
+ document.getElementById('startBtn').addEventListener('click', () => {
1050
+ this.startGame();
1051
+ });
1052
+
1053
+ document.getElementById('restartBtn').addEventListener('click', () => {
1054
+ this.startGame();
1055
+ });
1056
+
1057
+ document.getElementById('menuBtn').addEventListener('click', () => {
1058
+ GameState.changeState(GameState.MENU);
1059
+ });
1060
+
1061
+ document.getElementById('difficultyBtn').addEventListener('click', () => {
1062
+ this.cycleDifficulty();
1063
+ });
1064
+
1065
+ // Pause screen buttons
1066
+ document.getElementById('resumeBtn').addEventListener('click', () => {
1067
+ this.togglePause();
1068
+ });
1069
+
1070
+ document.getElementById('quitBtn').addEventListener('click', () => {
1071
+ GameState.changeState(GameState.MENU);
1072
+ });
1073
+ }
1074
+
1075
+ // Cycle through difficulty levels
1076
+ cycleDifficulty() {
1077
+ const difficulties = Object.keys(CONFIG.DIFFICULTY_LEVELS);
1078
+ const currentIndex = difficulties.indexOf(this.currentDifficulty);
1079
+ const nextIndex = (currentIndex + 1) % difficulties.length;
1080
+ this.currentDifficulty = difficulties[nextIndex];
1081
+
1082
+ document.getElementById('difficultyBtn').textContent = `DIFFICULTY: ${this.currentDifficulty}`;
1083
+ }
1084
+
1085
+ // Handle keyboard input
1086
+ handleKeyDown(event) {
1087
+ if (GameState.currentState === GameState.PLAYING) {
1088
+ switch(event.key) {
1089
+ case 'ArrowUp':
1090
+ this.handleDirectionChange(0, 0, -1);
1091
+ event.preventDefault();
1092
+ break;
1093
+ case 'ArrowDown':
1094
+ this.handleDirectionChange(0, 0, 1);
1095
+ event.preventDefault();
1096
+ break;
1097
+ case 'ArrowLeft':
1098
+ this.handleDirectionChange(-1, 0, 0);
1099
+ event.preventDefault();
1100
+ break;
1101
+ case 'ArrowRight':
1102
+ this.handleDirectionChange(1, 0, 0);
1103
+ event.preventDefault();
1104
+ break;
1105
+ case 'p':
1106
+ case 'P':
1107
+ this.togglePause();
1108
+ event.preventDefault();
1109
+ break;
1110
+ }
1111
+ } else if (GameState.currentState === GameState.GAME_OVER ||
1112
+ GameState.currentState === GameState.MENU) {
1113
+ if (event.key === 'Enter') {
1114
+ this.startGame();
1115
+ event.preventDefault();
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ // Handle direction change
1121
+ handleDirectionChange(x, y, z) {
1122
+ const newDirection = new THREE.Vector3(x, y, z).normalize().multiplyScalar(CONFIG.CELL_SIZE);
1123
+
1124
+ // Prevent 180-degree turns (moving directly backwards)
1125
+ if (this.direction.dot(newDirection) === -CONFIG.CELL_SIZE * CONFIG.CELL_SIZE) {
1126
+ return;
1127
+ }
1128
+
1129
+ this.nextDirection = newDirection;
1130
+ }
1131
+
1132
+ // Toggle pause state
1133
+ togglePause() {
1134
+ this.isPaused = !this.isPaused;
1135
+
1136
+ if (this.isPaused) {
1137
+ // TODO: Show pause screen
1138
+ document.getElementById('bgMusic').pause();
1139
+ } else {
1140
+ document.getElementById('bgMusic').play();
1141
+ }
1142
+ }
1143
+
1144
+ // Load high scores from local storage
1145
+ loadHighScores() {
1146
+ const scores = localStorage.getItem('snakeHighScores');
1147
+ return scores ? JSON.parse(scores) : [];
1148
+ }
1149
+
1150
+ // Update high scores table
1151
+ updateHighScoresTable() {
1152
+ const highScores = this.loadHighScores();
1153
+ const table = document.getElementById('scoresTable');
1154
+
1155
+ // Clear table except header
1156
+ while (table.rows.length > 1) {
1157
+ table.deleteRow(1);
1158
+ }
1159
+
1160
+ // Add high scores to table
1161
+ for (let i = 0; i < highScores.length; i++) {
1162
+ const row = table.insertRow(-1);
1163
+
1164
+ const rankCell = row.insertCell(0);
1165
+ rankCell.textContent = i + 1;
1166
+
1167
+ const scoreCell = row.insertCell(1);
1168
+ scoreCell.textContent = highScores[i].score;
1169
+
1170
+ const difficultyCell = row.insertCell(2);
1171
+ difficultyCell.textContent = highScores[i].difficulty;
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ // Create and start the game
1177
+ const game = new SnakeGame();
1178
+
1179
+ // Start the game when the page loads
1180
+ window.addEventListener('load', () => {
1181
+ GameState.changeState(GameState.MENU);
1182
+ });
1183
+ </script>
1184
+ </body>
1185
+ </html>