zxciop commited on
Commit
a0b0e69
·
verified ·
1 Parent(s): fab7512

Update templates/index.html

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