zxciop commited on
Commit
f8a4443
·
verified ·
1 Parent(s): ad43c96

Create index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +1012 -0
templates/index.html ADDED
@@ -0,0 +1,1012 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ text-shadow: 1px 1px 3px #0f0;
20
+ font-family: 'Courier New', Courier, monospace;
21
+ }
22
+ canvas {
23
+ display: block;
24
+ }
25
+ .game-ui {
26
+ position: absolute;
27
+ padding: 10px;
28
+ background-color: rgba(0, 20, 0, 0.9);
29
+ border: 1px solid #0f0;
30
+ border-radius: 5px;
31
+ font-size: 1.2em;
32
+ pointer-events: none;
33
+ }
34
+ #info {
35
+ top: 10px;
36
+ left: 10px;
37
+ }
38
+ #combo {
39
+ top: 10px;
40
+ right: 10px;
41
+ color: #0ff;
42
+ opacity: 0;
43
+ transition: opacity 0.3s;
44
+ }
45
+ #gameScreen {
46
+ position: absolute;
47
+ top: 0;
48
+ left: 0;
49
+ width: 100%;
50
+ height: 100%;
51
+ display: flex;
52
+ flex-direction: column;
53
+ justify-content: center;
54
+ align-items: center;
55
+ background-color: rgba(0, 10, 0, 0.0);
56
+ z-index: 10;
57
+ }
58
+ #startScreen, #gameOverScreen {
59
+ padding: 30px;
60
+ background-color: rgba(0, 30, 0, 0.7);
61
+ border: 2px solid #00ff00;
62
+ color: #2eff00;
63
+ text-shadow: lime 3px 3px 3px;
64
+ border-radius: 10px;
65
+ text-align: center;
66
+ max-width: 500px;
67
+ text-emphasis-color: #91ff83;
68
+ }
69
+ #gameOverScreen {
70
+ border-color: #f00;
71
+ }
72
+ .title {
73
+ font-size: 2.5em;
74
+ margin-bottom: 20px;
75
+ text-shadow: 2px 2px 5px #11ff00, 1px 1px 3px #eeff00, -3px -3px 3px #11ff00, 0px 2px 5px #2eff00;
76
+ }
77
+ .subtitle {
78
+ font-size: 1.2em;
79
+ margin-bottom: 30px;
80
+ }
81
+ .button {
82
+ display: inline-block;
83
+ padding: 10px 20px;
84
+ margin: 10px;
85
+ background-color: rgba(0, 40, 0, 0.5);
86
+ border: 1px solid #aaff00;
87
+ border-radius: 5px;
88
+ color: #0f0;
89
+ cursor: pointer;
90
+ transition: all 0.2s;
91
+ pointer-events: auto;
92
+ }
93
+ .button:hover {
94
+ background-color: rgba(0, 120, 0, 0.9);
95
+ transform: translateY(-5px);
96
+ box-shadow: 0px 2px 10px #2eff00;
97
+ }
98
+ .controls {
99
+ margin-top: 20px;
100
+ font-size: 0.9em;
101
+ opacity: 0.8;
102
+ }
103
+ #highScores {
104
+ margin-top: 20px;
105
+ text-align: left;
106
+ width: 100%;
107
+ }
108
+ #highScores table {
109
+ width: 100%;
110
+ border-collapse: collapse;
111
+ }
112
+ #highScores th, #highScores td {
113
+ padding: 5px;
114
+ border-bottom: 1px solid rgba(0, 255, 0, 0.5);
115
+ }
116
+ #touchControls {
117
+ position: absolute;
118
+ bottom: 20px;
119
+ left: 50%;
120
+ transform: translateX(-50%);
121
+ display: none; /* Hidden by default, shown on mobile */
122
+ }
123
+ .touchBtn {
124
+ width: 60px;
125
+ height: 60px;
126
+ background-color: rgba(0, 120, 0, 0.7);
127
+ border: 1px solid #0f0;
128
+ border-radius: 50%;
129
+ margin: 5px;
130
+ display: inline-flex;
131
+ justify-content: center;
132
+ align-items: center;
133
+ font-size: 20px;
134
+ cursor: pointer;
135
+ pointer-events: auto;
136
+ }
137
+ /* Matrix animation background */
138
+ #matrixCanvas {
139
+ position: fixed;
140
+ top: 0;
141
+ left: 0;
142
+ z-index: -1;
143
+ }
144
+ </style>
145
+ </head>
146
+ <body>
147
+ <!-- Matrix background -->
148
+ <canvas id="matrixCanvas"></canvas>
149
+
150
+ <!-- Game canvas -->
151
+ <canvas id="gameCanvas"></canvas>
152
+
153
+ <!-- Game UI -->
154
+ <div id="info" class="game-ui">Score: 0 | High: 0</div>
155
+ <div id="combo" class="game-ui">Combo x1!</div>
156
+
157
+ <!-- Touch controls for mobile -->
158
+ <div id="touchControls">
159
+ <div class="touchBtn" id="upBtn">↑</div>
160
+ <div style="display: flex;">
161
+ <div class="touchBtn" id="leftBtn">←</div>
162
+ <div class="touchBtn" id="downBtn">↓</div>
163
+ <div class="touchBtn" id="rightBtn">→</div>
164
+ </div>
165
+ </div>
166
+
167
+ <!-- Game screens -->
168
+ <div id="gameScreen">
169
+ <div id="startScreen">
170
+ <div class="title">MATRIX SNAKE 3D</div>
171
+ <div class="subtitle">Navigate the digital realm. Collect data packets. Avoid system firewalls.</div>
172
+ <div class="button" id="startBtn">START GAME</div>
173
+ <div class="button" id="difficultyBtn">DIFFICULTY: NORMAL</div>
174
+ <div class="controls">
175
+ Use Arrow Keys to change direction<br>
176
+ Press P to pause the game
177
+ </div>
178
+ <div id="highScores">
179
+ <h3>HIGH SCORES</h3>
180
+ <table id="scoresTable">
181
+ <tr><th>RANK</th><th>SCORE</th><th>DIFFICULTY</th></tr>
182
+ </table>
183
+ </div>
184
+ </div>
185
+ <div id="gameOverScreen" style="display: none;">
186
+ <div class="title" style="color: #f00;">SYSTEM FAILURE</div>
187
+ <div id="finalScore" class="subtitle">Final Score: 0</div>
188
+ <div class="button" id="restartBtn">RESTART</div>
189
+ <div class="button" id="menuBtn">MAIN MENU</div>
190
+ </div>
191
+ <div id="pauseScreen" style="display: none;">
192
+ <div class="title">PAUSED</div>
193
+ <div class="subtitle">Press P to resume</div>
194
+ <div class="button" id="resumeBtn">RESUME</div>
195
+ <div class="button" id="quitBtn">QUIT</div>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- Audio elements -->
200
+ <audio id="eatSound" preload="auto">
201
+ <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAANIAqJWUEQAFO+gRc5TRJIkiRJEiL///////////8RERERERERVVVVVVVVVVVVVVJEREREREVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/jGMQJA/Aa1flBABBTpGX9hDGMYxw7/+MMYxd/4wxIiI9////jDEQ7/jdEiJERERBaIiIzMzMzIiIiP//MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM/+MYxB4AAANIAAAAADMzMzMzMzMzMzMzMzMzMzMzM" type="audio/mpeg">
202
+ </audio>
203
+ <audio id="gameOverSound" preload="auto">
204
+ <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAKMFqZVQEwAhGKzc+FSIiIiIiIiIj4+Pj4+Pj4+Pj4+JIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIv/jIMQNAAAP8AEAAAI+IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIv/jEMQQAAAP8AAAAAI+IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIg==" type="audio/mpeg">
205
+ </audio>
206
+ <audio id="bgMusic" loop preload="auto">
207
+ <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAJcAKRWQEQAFNfQRc5znOc5znP/////////uc5znOc5znOc5znEREREREREREREMYxjGMY/+MYxBEJkFahX4wwAjGMYxjGMYxjGMYxERERERERESIiIiL//////////////+MYxBQG4AqlX8MQAu/////////////////////jIMQVBVwCqVfwBAC/////////////////" type="audio/mpeg">
208
+ </audio>
209
+
210
+
211
+ <script type="importmap">
212
+ {
213
+ "imports": {
214
+ "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
215
+ "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
216
+ }
217
+ }
218
+ </script>
219
+
220
+ <script type="module">
221
+ import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
222
+ import { EffectComposer } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js';
223
+ import { RenderPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/RenderPass.js';
224
+ import { UnrealBloomPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/UnrealBloomPass.js';
225
+
226
+ // Game configuration
227
+ const CONFIG = {
228
+ GRID_SIZE: 25, // Number of units across/deep
229
+ CELL_SIZE: 1, // Size of each grid cell/snake segment
230
+ BASE_SPEED: 150, // Base milliseconds between updates
231
+ DIFFICULTY_LEVELS: {
232
+ 'EASY': { speedMultiplier: 1.3, obstacleMultiplier: 0.5 },
233
+ 'NORMAL': { speedMultiplier: 1.0, obstacleMultiplier: 1.0 },
234
+ 'HARD': { speedMultiplier: 0.7, obstacleMultiplier: 1.5 }
235
+ },
236
+ MAX_OBSTACLE_COUNT: 10, // Maximum number of obstacles
237
+ FOOD_TYPES: [
238
+ { type: 'regular', color: 0x00ff00, points: 1, speedEffect: 0 },
239
+ { type: 'special', color: 0x00ffff, points: 5, speedEffect: -10 },
240
+ { type: 'rare', color: 0xff00ff, points: 10, speedEffect: 10 }
241
+ ],
242
+ COMBO_TIMEOUT: 5000, // Milliseconds to get next food for combo
243
+ HIGH_SCORES_COUNT: 5 // Number of high scores to save
244
+ };
245
+
246
+ // --- Particle System for Effects ---
247
+ class ParticleSystem {
248
+ constructor(scene) {
249
+ this.scene = scene;
250
+ this.particles = [];
251
+ this.geometry = new THREE.BoxGeometry(0.2, 0.1, 0.2); // Shared geometry
252
+ }
253
+
254
+ createFoodEffect(position, color) {
255
+ const count = 20;
256
+ for (let i = 0; i < count; i++) {
257
+ const material = new THREE.MeshBasicMaterial({
258
+ color: color || 0x00ff00,
259
+ transparent: true,
260
+ opacity: 0.8
261
+ });
262
+ const particle = new THREE.Mesh(this.geometry, material);
263
+ particle.position.copy(position);
264
+ const velocity = new THREE.Vector3(
265
+ (Math.random() - 0.5) * 0.1,
266
+ (Math.random()) * 0.25,
267
+ (Math.random() - 0.5) * 0.1
268
+ );
269
+ this.scene.add(particle);
270
+ this.particles.push({
271
+ mesh: particle,
272
+ velocity: velocity,
273
+ life: 1.0,
274
+ decay: 0.02 + Math.random() * 0.03
275
+ });
276
+ }
277
+ }
278
+
279
+ update() {
280
+ for (let i = this.particles.length - 1; i >= 0; i--) {
281
+ const particle = this.particles[i];
282
+ particle.mesh.position.add(particle.velocity);
283
+ particle.velocity.y -= 0.005; // Gravity
284
+ particle.life -= particle.decay;
285
+ particle.mesh.material.opacity = particle.life;
286
+
287
+ if (particle.life <= 0) {
288
+ this.scene.remove(particle.mesh);
289
+ particle.mesh.material.dispose();
290
+ // particle.mesh.geometry.dispose(); // Geometry is shared, don't dispose here
291
+ this.particles.splice(i, 1);
292
+ }
293
+ }
294
+ }
295
+
296
+ clear() {
297
+ for (const particle of this.particles) {
298
+ this.scene.remove(particle.mesh);
299
+ particle.mesh.material.dispose();
300
+ }
301
+ this.particles = [];
302
+ // Note: Shared geometry (this.geometry) is not disposed here,
303
+ // as it might be needed again. Dispose it if ParticleSystem itself is destroyed.
304
+ }
305
+ }
306
+
307
+ // Game state management
308
+ const GameState = {
309
+ MENU: 'menu',
310
+ PLAYING: 'playing',
311
+ PAUSED: 'paused',
312
+ GAME_OVER: 'gameOver',
313
+ currentState: 'menu',
314
+
315
+ changeState(newState) {
316
+ this.currentState = newState;
317
+ switch(newState) {
318
+ case this.MENU:
319
+ document.getElementById('gameScreen').style.display = 'flex';
320
+ document.getElementById('startScreen').style.display = 'block';
321
+ document.getElementById('gameOverScreen').style.display = 'none';
322
+ document.getElementById('pauseScreen').style.display = 'none';
323
+ break;
324
+ case this.PLAYING:
325
+ document.getElementById('gameScreen').style.display = 'none';
326
+ break;
327
+ case this.PAUSED:
328
+ document.getElementById('gameScreen').style.display = 'flex';
329
+ document.getElementById('startScreen').style.display = 'none';
330
+ document.getElementById('gameOverScreen').style.display = 'none';
331
+ document.getElementById('pauseScreen').style.display = 'block';
332
+ break;
333
+ case this.GAME_OVER:
334
+ document.getElementById('gameScreen').style.display = 'flex';
335
+ document.getElementById('startScreen').style.display = 'none';
336
+ document.getElementById('gameOverScreen').style.display = 'block';
337
+ document.getElementById('pauseScreen').style.display = 'none';
338
+ document.getElementById('gameOverSound').play();
339
+ break;
340
+ }
341
+ }
342
+ };
343
+
344
+ // --- Matrix Rain Background Effect ---
345
+ class MatrixRain {
346
+ constructor() {
347
+ this.canvas = document.getElementById('matrixCanvas');
348
+ this.ctx = this.canvas.getContext('2d');
349
+ this.resize();
350
+
351
+ this.fontSize = 14;
352
+ this.columns = Math.floor(this.canvas.width / this.fontSize);
353
+ this.drops = [];
354
+ this.characters = '01アイウエオカキクケコサシスセソタチツテトナニヌネ<>{}[]()+-*/%=#@&?*:・゚✧≡░▒░▒░▒▓║│·⁞▏ ▏⁚⁝.';
355
+ this.katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン';
356
+ this.characters += this.katakana;
357
+ this.resetDrops();
358
+ this.animate = this.animate.bind(this);
359
+ this.animate(); // Start animation
360
+ window.addEventListener('resize', this.handleResize.bind(this));
361
+ }
362
+
363
+ handleResize() {
364
+ this.resize();
365
+ this.columns = Math.floor(this.canvas.width / this.fontSize);
366
+ this.resetDrops();
367
+ }
368
+
369
+ resize() {
370
+ this.canvas.width = window.innerWidth;
371
+ this.canvas.height = window.innerHeight;
372
+ }
373
+
374
+ resetDrops() {
375
+ this.drops = [];
376
+ for(let i = 0; i < this.columns; i++) {
377
+ this.drops[i] = Math.floor(Math.random() * -100); // Start off-screen
378
+ }
379
+ }
380
+
381
+ animate() {
382
+ // Show rain on all screens as per previous request, including PLAYING
383
+ if (GameState.currentState === GameState.MENU ||
384
+ GameState.currentState === GameState.PAUSED ||
385
+ GameState.currentState === GameState.GAME_OVER ||
386
+ GameState.currentState === GameState.PLAYING) {
387
+
388
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.03)'; // Fading effect
389
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
390
+
391
+ this.ctx.fillStyle = '#00ff00'; // Matrix character color
392
+ this.ctx.emissiveColor = '#76ff76';
393
+ this.ctx.emissiveIntensity = '6';
394
+ this.ctx.font = this.fontSize + 'px monospace';
395
+
396
+ for(let i = 0; i < this.drops.length; i++) {
397
+ const text = this.characters.charAt(Math.floor(Math.random() * this.characters.length));
398
+ this.ctx.fillText(text, i * this.fontSize, this.drops[i] * this.fontSize);
399
+
400
+ if(this.drops[i] * this.fontSize > this.canvas.height && Math.random() > 0.975) {
401
+ this.drops[i] = 0; // Reset drop
402
+ }
403
+ this.drops[i]++;
404
+ }
405
+ }
406
+ requestAnimationFrame(this.animate);
407
+ }
408
+ } // <-- Make sure this curly brace properly closes the MatrixRain class
409
+
410
+ // --- Object pooling for performance optimization ---
411
+ class ObjectPool {
412
+ constructor(createFunc, initialCount = 10) {
413
+ this.pool = [];
414
+ this.createFunc = createFunc;
415
+
416
+ for (let i = 0; i < initialCount; i++) {
417
+ this.pool.push(this.createFunc());
418
+ }
419
+ }
420
+
421
+ get() {
422
+ if (this.pool.length > 0) {
423
+ return this.pool.pop();
424
+ }
425
+ return this.createFunc();
426
+ }
427
+
428
+ release(object) {
429
+ this.pool.push(object);
430
+ }
431
+
432
+ clear() {
433
+ this.pool = [];
434
+ // Note: This doesn't dispose Three.js objects.
435
+ // Disposal should be handled by the code that uses the pool
436
+ // when objects are truly no longer needed by the scene.
437
+ }
438
+ }
439
+
440
+ // --- Main Game Class ---
441
+ class SnakeGame {
442
+ constructor() {
443
+ this.scene = null;
444
+ this.camera = null;
445
+ this.renderer = null;
446
+ this.composer = null; // For post-processing
447
+ this.snake = [];
448
+ this.food = null;
449
+ this.obstacles = [];
450
+ this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
451
+ this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0);
452
+ this.score = 0;
453
+ this.highScore = this.loadHighScores()[0]?.score || 0;
454
+ this.gameSpeed = CONFIG.BASE_SPEED;
455
+ this.lastUpdateTime = 0;
456
+ this.isGameOver = false;
457
+ this.isPaused = false;
458
+ this.gameLoopId = null;
459
+ this.bounds = Math.floor(CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE;
460
+ this.obstacleCount = CONFIG.MAX_OBSTACLE_COUNT;
461
+ this.comboCount = 0;
462
+ this.lastFoodTime = 0;
463
+ this.currentDifficulty = 'NORMAL';
464
+ this.particleSystem = null;
465
+ this.headLight = null;
466
+
467
+ this.materials = {
468
+ snakeHead: new THREE.MeshStandardMaterial({
469
+ color: 0x39FF14, emissive: 0x39FF14, roughness: 0.1, metalness: 0.25
470
+ }),
471
+ snakeBody: new THREE.MeshStandardMaterial({
472
+ color: 0x00ff00, emissive: 0x005500, roughness: 0.1, metalness: 1.5
473
+ }),
474
+ obstacle: new THREE.MeshStandardMaterial({ // Changed to Standard for lighting
475
+ color: 0xff0000, emissive: 0x550000, roughness: 0.3, metalness: 1, wireframe: false
476
+ }),
477
+ // Food materials defined per type in chooseFoodType
478
+ };
479
+ this.geometries = {
480
+ segment: new THREE.BoxGeometry(CONFIG.CELL_SIZE, CONFIG.CELL_SIZE, CONFIG.CELL_SIZE),
481
+ foodBox: new THREE.BoxGeometry(CONFIG.CELL_SIZE * 0.8, CONFIG.CELL_SIZE * 0.8, CONFIG.CELL_SIZE * 0.8),
482
+ foodSphere: new THREE.SphereGeometry(CONFIG.CELL_SIZE * 0.5, 16, 12),
483
+ foodTetrahedron: new THREE.TetrahedronGeometry(CONFIG.CELL_SIZE * 0.6, 0),
484
+ obstacle: new THREE.BoxGeometry(CONFIG.CELL_SIZE * 0.9, CONFIG.CELL_SIZE * 1.5, CONFIG.CELL_SIZE * 0.9) // Taller obstacles
485
+ };
486
+
487
+ this.segmentPool = new ObjectPool(() => {
488
+ const segment = new THREE.Mesh(this.geometries.segment, this.materials.snakeBody.clone());
489
+ segment.castShadow = true; // If using shadows
490
+ return segment;
491
+ }, 20);
492
+
493
+ this.obstaclePool = new ObjectPool(() => {
494
+ const obstacle = new THREE.Mesh(this.geometries.obstacle, this.materials.obstacle.clone());
495
+ obstacle.castShadow = true; // If using shadows
496
+ return obstacle;
497
+ }, CONFIG.MAX_OBSTACLE_COUNT * 1.5);
498
+
499
+ this.init(); // Call init before matrixRain if matrixRain depends on game elements
500
+ this.matrixRain = new MatrixRain(); // Initialize after main game setup if it interacts
501
+ this.setupEventListeners();
502
+ this.updateHighScoresTable();
503
+ GameState.changeState(GameState.MENU); // Start in menu
504
+ }
505
+
506
+ init() {
507
+ this.scene = new THREE.Scene();
508
+ this.scene.background = null; // For Matrix rain to show through
509
+ // Add fog for depth with brighter color
510
+ this.scene.fog = new THREE.Fog(0x001100, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 2.5);
511
+
512
+ this.camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
513
+ this.camera.position.set(0, CONFIG.GRID_SIZE * 0.4, CONFIG.GRID_SIZE * 0.9);
514
+ this.camera.lookAt(0, -CONFIG.GRID_SIZE * 0.1, 0);
515
+
516
+ this.renderer = new THREE.WebGLRenderer({
517
+ canvas: document.getElementById('gameCanvas'),
518
+ antialias: true,
519
+ alpha: true // Crucial for transparency
520
+ });
521
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
522
+ this.renderer.setPixelRatio(window.devicePixelRatio);
523
+ this.renderer.shadowMap.enabled = true; // If you add shadows
524
+
525
+ // Post-processing for bloom
526
+ const renderPass = new RenderPass(this.scene, this.camera);
527
+ const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.2, 0.8, 0.75);
528
+ bloomPass.threshold = 0.0;
529
+ bloomPass.strength = 1.3; // Play with these
530
+ bloomPass.radius = 0.3;
531
+
532
+ this.composer = new EffectComposer(this.renderer);
533
+ this.composer.addPass(renderPass);
534
+ this.composer.addPass(bloomPass);
535
+
536
+ const gridHelper = new THREE.GridHelper(CONFIG.GRID_SIZE * CONFIG.CELL_SIZE, CONFIG.GRID_SIZE, 0x008800, 0x00FF00);
537
+ gridHelper.position.y = -CONFIG.CELL_SIZE / 2; // Align with snake plane
538
+ this.scene.add(gridHelper);
539
+
540
+ const ambientLight = new THREE.AmbientLight(0x404060, 0.1); // Soft ambient
541
+ this.scene.add(ambientLight);
542
+
543
+ const directionalLight = new THREE.DirectionalLight(0xffffee, 1.6);
544
+ directionalLight.position.set(75, 13, 10);
545
+ directionalLight.castShadow = true;
546
+ directionalLight.shadow.mapSize.width = 1024;
547
+ directionalLight.shadow.mapSize.height = 1024;
548
+ this.scene.add(directionalLight);
549
+
550
+ this.headLight = new THREE.PointLight(0x39FF14, 2, CONFIG.CELL_SIZE * 5); // Brighter, green light
551
+ this.headLight.castShadow = false; // Point lights can be expensive for shadows
552
+ this.scene.add(this.headLight);
553
+
554
+ this.particleSystem = new ParticleSystem(this.scene);
555
+
556
+ window.addEventListener('resize', () => {
557
+ this.camera.aspect = window.innerWidth / window.innerHeight;
558
+ this.camera.updateProjectionMatrix();
559
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
560
+ this.composer.setSize(window.innerWidth, window.innerHeight); // Resize composer too
561
+ }, false);
562
+ }
563
+
564
+ placeFood() {
565
+ let foodPos;
566
+ let validPosition = false;
567
+ let attempts = 0;
568
+ const maxAttempts = 100;
569
+
570
+ const numCells = CONFIG.GRID_SIZE;
571
+
572
+ while (!validPosition && attempts < maxAttempts) {
573
+ // Generate random indices within the grid
574
+ const xIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
575
+ const zIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
576
+
577
+ foodPos = new THREE.Vector3(
578
+ xIndex * CONFIG.CELL_SIZE,
579
+ 0, // Food on the grid plane
580
+ zIndex * CONFIG.CELL_SIZE
581
+ );
582
+
583
+ let collisionWithSnake = this.snake.some(segment =>
584
+ segment.position.distanceToSquared(foodPos) < (CONFIG.CELL_SIZE * 0.9)**2 // Use distanceSquared
585
+ );
586
+
587
+ let collisionWithObstacle = this.obstacles.some(obstacle =>
588
+ obstacle.position.distanceToSquared(foodPos) < (CONFIG.CELL_SIZE * 0.9)**2
589
+ );
590
+
591
+ validPosition = !collisionWithSnake && !collisionWithObstacle;
592
+ attempts++;
593
+ }
594
+
595
+ if (validPosition) {
596
+ this.food.position.copy(foodPos);
597
+ } else {
598
+ console.warn("Could not find valid position for food. Placing at a default safe spot.");
599
+ // Fallback: try placing near origin, hoping it's clear
600
+ this.food.position.set(
601
+ (Math.floor(Math.random() * 3) - 1) * CONFIG.CELL_SIZE,
602
+ 0,
603
+ (Math.floor(Math.random() * 3) - 1) * CONFIG.CELL_SIZE
604
+ );
605
+ }
606
+ }
607
+
608
+ createObstacles() {
609
+ this.obstacles.forEach(obstacle => {
610
+ this.scene.remove(obstacle);
611
+ this.obstaclePool.release(obstacle);
612
+ });
613
+ this.obstacles = [];
614
+
615
+ const numCells = CONFIG.GRID_SIZE;
616
+
617
+ for (let i = 0; i < this.obstacleCount; i++) {
618
+ let obstaclePos;
619
+ let validPosition = false;
620
+ let attempts = 0;
621
+ const maxAttempts = 50;
622
+
623
+ while (!validPosition && attempts < maxAttempts) {
624
+ const xIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
625
+ const zIndex = Math.floor(Math.random() * numCells) - Math.floor((numCells - 1) / 2);
626
+ obstaclePos = new THREE.Vector3(
627
+ xIndex * CONFIG.CELL_SIZE,
628
+ 0, // Obstacles on the grid plane, but mesh is taller
629
+ zIndex * CONFIG.CELL_SIZE
630
+ );
631
+
632
+ let tooCloseToStart = obstaclePos.lengthSq() < (CONFIG.CELL_SIZE * 4)**2; // Check squared length
633
+
634
+ let collisionWithSnake = this.snake.some(segment =>
635
+ segment.position.distanceToSquared(obstaclePos) < (CONFIG.CELL_SIZE * 2)**2
636
+ );
637
+
638
+ let collisionWithOtherObstacle = this.obstacles.some(existingObstacle =>
639
+ existingObstacle.position.distanceToSquared(obstaclePos) < (CONFIG.CELL_SIZE * 1.5)**2 // Ensure spacing
640
+ );
641
+
642
+ validPosition = !tooCloseToStart && !collisionWithSnake && !collisionWithOtherObstacle;
643
+ attempts++;
644
+ }
645
+
646
+ if (validPosition) {
647
+ const obstacle = this.obstaclePool.get();
648
+ obstacle.position.copy(obstaclePos);
649
+ obstacle.position.y = (CONFIG.CELL_SIZE * 1.5 - CONFIG.CELL_SIZE) / 2 - CONFIG.CELL_SIZE / 2; // Adjust Y to sit on grid
650
+ this.obstacles.push(obstacle);
651
+ this.scene.add(obstacle);
652
+ }
653
+ }
654
+ }
655
+
656
+ clearGameObjects() {
657
+ this.snake.forEach(segment => {
658
+ this.scene.remove(segment);
659
+ this.segmentPool.release(segment);
660
+ });
661
+ this.snake = [];
662
+
663
+ if (this.food) {
664
+ this.scene.remove(this.food);
665
+ // No pool for food as it's a single, changing object
666
+ }
667
+ this.food = null;
668
+
669
+ this.obstacles.forEach(obstacle => {
670
+ this.scene.remove(obstacle);
671
+ this.obstaclePool.release(obstacle);
672
+ });
673
+ this.obstacles = [];
674
+
675
+ this.particleSystem.clear();
676
+ }
677
+
678
+ update(time) {
679
+ if (this.isPaused || this.isGameOver || GameState.currentState !== GameState.PLAYING) {
680
+ return;
681
+ }
682
+
683
+ if (time - this.lastUpdateTime < this.gameSpeed) {
684
+ return;
685
+ }
686
+ this.lastUpdateTime = time;
687
+
688
+ this.direction.copy(this.nextDirection); // More robust copy
689
+
690
+ const head = this.snake[0];
691
+ const newHeadPos = head.position.clone().add(this.direction); // CELL_SIZE is incorporated in direction
692
+
693
+ const halfGridWorld = (CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE;
694
+ if (
695
+ newHeadPos.x >= halfGridWorld || newHeadPos.x < -halfGridWorld ||
696
+ newHeadPos.z >= halfGridWorld || newHeadPos.z < -halfGridWorld
697
+ ) {
698
+ this.triggerGameOver();
699
+ return;
700
+ }
701
+
702
+ for (let i = 1; i < this.snake.length; i++) {
703
+ if (newHeadPos.distanceToSquared(this.snake[i].position) < (CONFIG.CELL_SIZE * 0.1)**2) { // Tighter collision
704
+ this.triggerGameOver();
705
+ return;
706
+ }
707
+ }
708
+
709
+ for (const obstacle of this.obstacles) {
710
+ if (newHeadPos.distanceToSquared(obstacle.position) < (CONFIG.CELL_SIZE * 0.75)**2) { // Check collision with obstacle
711
+ this.triggerGameOver();
712
+ return;
713
+ }
714
+ }
715
+
716
+ const newHead = this.segmentPool.get();
717
+ newHead.position.copy(newHeadPos);
718
+ newHead.material = this.materials.snakeHead; // Head material
719
+ if (this.snake.length > 0) {
720
+ this.snake[0].material = this.materials.snakeBody; // Old head becomes body
721
+ }
722
+ this.snake.unshift(newHead);
723
+ this.scene.add(newHead);
724
+ this.headLight.position.copy(newHeadPos); // Light follows head
725
+
726
+ if (this.food && newHeadPos.distanceToSquared(this.food.position) < (CONFIG.CELL_SIZE * 0.75)**2) {
727
+ const foodType = this.food.userData;
728
+ const basePoints = foodType.points || 1;
729
+
730
+ const currentTime = performance.now();
731
+ if (currentTime - this.lastFoodTime < CONFIG.COMBO_TIMEOUT) {
732
+ this.comboCount++;
733
+ } else {
734
+ this.comboCount = 1;
735
+ }
736
+ this.lastFoodTime = currentTime;
737
+
738
+ const points = basePoints * this.comboCount;
739
+ this.score += points;
740
+
741
+ if (this.comboCount > 1) {
742
+ const comboElement = document.getElementById('combo');
743
+ comboElement.textContent = `Combo x${this.comboCount}! +${points}`;
744
+ comboElement.style.opacity = 1;
745
+ setTimeout(() => { comboElement.style.opacity = 0; }, 2000);
746
+ }
747
+
748
+ document.getElementById('info').textContent = `Score: ${this.score} | High: ${Math.max(this.score, this.highScore)}`;
749
+
750
+ if (foodType.speedEffect) {
751
+ this.gameSpeed = Math.max(50, this.gameSpeed + foodType.speedEffect); // Note: special food had -10, rare had +10
752
+ }
753
+
754
+ document.getElementById('eatSound').currentTime = 0;
755
+ document.getElementById('eatSound').play();
756
+ this.particleSystem.createFoodEffect(this.food.position.clone(), new THREE.Color(foodType.color));
757
+
758
+ this.chooseFoodType(); // Gets new food type, applies material
759
+ this.placeFood(); // Places it
760
+ } else {
761
+ const tail = this.snake.pop();
762
+ this.scene.remove(tail);
763
+ this.segmentPool.release(tail);
764
+ }
765
+
766
+ this.particleSystem.update();
767
+
768
+ for (let i = 0; i < this.snake.length; i++) {
769
+ const segment = this.snake[i];
770
+ // segment.rotation.y += Math.sin(time * 0.1 + i * 0.1) * 0.01; // Slower, more subtle
771
+ // segment.position.y = Math.sin(time * 0.01 + i * 0.2) * 0.1; // Subtle bob
772
+ // segment.position.y = Math.sin(time * 0.01 + i * 0.2) * 1.5; // Jumping
773
+ }
774
+
775
+ if (this.food) {
776
+ this.food.rotation.x += 0.01;
777
+ this.food.rotation.y += 0.02;
778
+ this.food.position.y = Math.sin(time * 0.0055) * 0.25 + 0.1; // Bobbing food
779
+ }
780
+ }
781
+
782
+ chooseFoodType() {
783
+ const rand = Math.random();
784
+ let foodTypeData;
785
+ let geometry;
786
+
787
+ if (rand < 0.05) { // 5% rare
788
+ foodTypeData = CONFIG.FOOD_TYPES[2];
789
+ geometry = this.geometries.foodTetrahedron;
790
+ } else if (rand < 0.25) { // 20% special
791
+ foodTypeData = CONFIG.FOOD_TYPES[1];
792
+ geometry = this.geometries.foodSphere;
793
+ } else { // 75% regular
794
+ foodTypeData = CONFIG.FOOD_TYPES[0];
795
+ geometry = this.geometries.foodBox;
796
+ }
797
+
798
+ const material = new THREE.MeshStandardMaterial({ // Use Standard for lighting
799
+ color: foodTypeData.color,
800
+ emissive: foodTypeData.color, // Make food glow
801
+ emissiveIntensity: 0.7,
802
+ roughness: 0.1,
803
+ metalness: 0.7,
804
+ wireframe: true
805
+ });
806
+
807
+ if (!this.food) {
808
+ this.food = new THREE.Mesh(geometry, material);
809
+ this.scene.add(this.food);
810
+ } else {
811
+ this.food.geometry.dispose(); // Dispose old geometry
812
+ this.food.geometry = geometry;
813
+ this.food.material.dispose(); // Dispose old material
814
+ this.food.material = material;
815
+ }
816
+ this.food.userData = foodTypeData;
817
+ }
818
+
819
+ resetGame() {
820
+ this.clearGameObjects();
821
+
822
+ // Stop sounds
823
+ ['eatSound', 'gameOverSound', 'bgMusic'].forEach(id => {
824
+ const sound = document.getElementById(id);
825
+ sound.pause();
826
+ sound.currentTime = 0;
827
+ });
828
+
829
+ this.direction.set(CONFIG.CELL_SIZE, 0, 0);
830
+ this.nextDirection.set(CONFIG.CELL_SIZE, 0, 0);
831
+ this.score = 0;
832
+ this.gameSpeed = CONFIG.BASE_SPEED * CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].speedMultiplier;
833
+ this.isGameOver = false;
834
+ this.isPaused = false;
835
+ GameState.currentState = GameState.PLAYING; // Set this before calling things that depend on it.
836
+ this.comboCount = 0;
837
+ this.lastFoodTime = 0;
838
+
839
+ this.obstacleCount = Math.floor(CONFIG.MAX_OBSTACLE_COUNT * CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].obstacleMultiplier);
840
+
841
+ const startSegment = this.segmentPool.get();
842
+ startSegment.position.set(0, 0, 0);
843
+ startSegment.material = this.materials.snakeHead; // Start with head material
844
+ this.snake.push(startSegment);
845
+ this.scene.add(startSegment);
846
+ this.headLight.position.copy(startSegment.position); // Initial light pos
847
+
848
+ this.chooseFoodType();
849
+ this.placeFood();
850
+ this.createObstacles();
851
+
852
+ document.getElementById('info').textContent = `Score: ${this.score} | High: ${this.highScore}`;
853
+
854
+ const music = document.getElementById('bgMusic');
855
+ music.volume = 0.2; // Quieter music
856
+ music.play().catch(e => console.warn("Music play failed:", e)); // Catch promise
857
+ }
858
+
859
+ startGame() {
860
+ this.resetGame(); // This sets GameState.PLAYING internally now
861
+ GameState.changeState(GameState.PLAYING); // Ensure UI updates
862
+ this.lastUpdateTime = performance.now(); // Initialize lastUpdateTime
863
+ if (this.gameLoopId) cancelAnimationFrame(this.gameLoopId); // Clear old loop
864
+ this.gameLoop();
865
+ }
866
+
867
+ triggerGameOver() {
868
+ if (this.isGameOver) return; // Prevent multiple triggers
869
+ this.isGameOver = true;
870
+ document.getElementById('finalScore').textContent = `Final Score: ${this.score}`;
871
+
872
+ const highScores = this.loadHighScores();
873
+ if (this.score > 0) {
874
+ highScores.push({ score: this.score, difficulty: this.currentDifficulty, date: new Date().toLocaleDateString() });
875
+ highScores.sort((a, b) => b.score - a.score);
876
+ const topScores = highScores.slice(0, CONFIG.HIGH_SCORES_COUNT);
877
+ localStorage.setItem('snakeHighScores', JSON.stringify(topScores));
878
+ this.highScore = Math.max(this.score, this.highScore); // Update current session high score
879
+ }
880
+ this.updateHighScoresTable();
881
+ document.getElementById('bgMusic').pause();
882
+ GameState.changeState(GameState.GAME_OVER);
883
+ }
884
+
885
+ gameLoop(time) { // time is passed by requestAnimationFrame
886
+ if (!this.isGameOver && GameState.currentState === GameState.PLAYING) {
887
+ this.update(time || performance.now()); // Use performance.now if time is undefined initially
888
+ }
889
+ this.render();
890
+ if (!this.isGameOver) { // Only loop if not game over
891
+ this.gameLoopId = requestAnimationFrame(this.gameLoop.bind(this));
892
+ } else {
893
+ if (this.gameLoopId) cancelAnimationFrame(this.gameLoopId); // Ensure loop stops
894
+ }
895
+ }
896
+
897
+ render() {
898
+ // this.renderer.render(this.scene, this.camera); // When using composer
899
+ if (this.composer) {
900
+ this.composer.render(this.scene, this.camera);
901
+ } else if (this.renderer && this.scene && this.camera) {
902
+ this.renderer.render(this.scene, this.camera);
903
+ }
904
+ }
905
+
906
+ setupEventListeners() {
907
+ document.addEventListener('keydown', this.handleKeyDown.bind(this));
908
+
909
+ const touchControlsDiv = document.getElementById('touchControls');
910
+ const preventDefaultAndStopPropagation = (e) => { e.preventDefault(); e.stopPropagation(); };
911
+
912
+ document.getElementById('upBtn').addEventListener('touchstart', (e) => { preventDefaultAndStopPropagation(e); this.handleDirectionChange(0, 0, -CONFIG.CELL_SIZE); });
913
+ document.getElementById('downBtn').addEventListener('touchstart', (e) => { preventDefaultAndStopPropagation(e); this.handleDirectionChange(0, 0, CONFIG.CELL_SIZE); });
914
+ document.getElementById('leftBtn').addEventListener('touchstart', (e) => { preventDefaultAndStopPropagation(e); this.handleDirectionChange(-CONFIG.CELL_SIZE, 0, 0); });
915
+ document.getElementById('rightBtn').addEventListener('touchstart', (e) => { preventDefaultAndStopPropagation(e); this.handleDirectionChange(CONFIG.CELL_SIZE, 0, 0); });
916
+
917
+ const gameCanvas = document.getElementById('gameCanvas');
918
+ gameCanvas.addEventListener('touchstart', preventDefaultAndStopPropagation, { passive: false });
919
+ gameCanvas.addEventListener('touchmove', preventDefaultAndStopPropagation, { passive: false });
920
+
921
+ if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
922
+ touchControlsDiv.style.display = 'block';
923
+ }
924
+
925
+ document.getElementById('startBtn').addEventListener('click', () => this.startGame());
926
+ document.getElementById('restartBtn').addEventListener('click', () => this.startGame());
927
+ document.getElementById('menuBtn').addEventListener('click', () => GameState.changeState(GameState.MENU));
928
+ document.getElementById('difficultyBtn').addEventListener('click', () => this.cycleDifficulty());
929
+ document.getElementById('resumeBtn').addEventListener('click', () => this.togglePause());
930
+ document.getElementById('quitBtn').addEventListener('click', () => {
931
+ this.isPaused = false; // Ensure unpaused
932
+ if (this.gameLoopId) cancelAnimationFrame(this.gameLoopId); // Stop game loop
933
+ document.getElementById('bgMusic').pause();
934
+ GameState.changeState(GameState.MENU);
935
+ });
936
+ }
937
+
938
+ cycleDifficulty() {
939
+ const difficulties = Object.keys(CONFIG.DIFFICULTY_LEVELS);
940
+ const currentIndex = difficulties.indexOf(this.currentDifficulty);
941
+ const nextIndex = (currentIndex + 1) % difficulties.length;
942
+ this.currentDifficulty = difficulties[nextIndex];
943
+ document.getElementById('difficultyBtn').textContent = `DIFFICULTY: ${this.currentDifficulty.toUpperCase()}`;
944
+ }
945
+
946
+ handleKeyDown(event) {
947
+ if (GameState.currentState === GameState.PLAYING && !this.isPaused) {
948
+ switch(event.key) {
949
+ case 'ArrowUp': case 'w': case 'W': this.handleDirectionChange(0, 0, -CONFIG.CELL_SIZE); event.preventDefault(); break;
950
+ case 'ArrowDown': case 's': case 'S': this.handleDirectionChange(0, 0, CONFIG.CELL_SIZE); event.preventDefault(); break;
951
+ case 'ArrowLeft': case 'a': case 'A': this.handleDirectionChange(-CONFIG.CELL_SIZE, 0, 0); event.preventDefault(); break;
952
+ case 'ArrowRight': case 'd': case 'D': this.handleDirectionChange(CONFIG.CELL_SIZE, 0, 0); event.preventDefault(); break;
953
+ case 'p': case 'P': this.togglePause(); event.preventDefault(); break;
954
+ }
955
+ } else if (GameState.currentState === GameState.PAUSED && (event.key === 'p' || event.key === 'P')) {
956
+ this.togglePause(); event.preventDefault();
957
+ } else if ((GameState.currentState === GameState.GAME_OVER || GameState.currentState === GameState.MENU) && event.key === 'Enter') {
958
+ this.startGame(); event.preventDefault();
959
+ }
960
+ }
961
+
962
+ handleDirectionChange(dx, dy, dz) {
963
+ const newDir = new THREE.Vector3(dx, dy, dz);
964
+ // Prevent 180-degree turns. Dot product will be negative.
965
+ // direction is already scaled by CELL_SIZE
966
+ if (this.direction.dot(newDir) < - (CONFIG.CELL_SIZE * CONFIG.CELL_SIZE * 0.5) ) { // Compare against negative magnitude squared
967
+ return;
968
+ }
969
+ this.nextDirection.copy(newDir);
970
+ }
971
+
972
+ togglePause() {
973
+ if (GameState.currentState !== GameState.PLAYING && GameState.currentState !== GameState.PAUSED) return;
974
+
975
+ this.isPaused = !this.isPaused;
976
+ if (this.isPaused) {
977
+ GameState.changeState(GameState.PAUSED);
978
+ document.getElementById('bgMusic').pause();
979
+ if (this.gameLoopId) cancelAnimationFrame(this.gameLoopId); // Pause game loop
980
+ } else {
981
+ GameState.changeState(GameState.PLAYING);
982
+ document.getElementById('bgMusic').play().catch(e => console.warn("Music play failed:", e));
983
+ this.lastUpdateTime = performance.now(); // Reset update timer to prevent jump
984
+ this.gameLoop(); // Resume game loop
985
+ }
986
+ }
987
+
988
+ loadHighScores() {
989
+ const scores = localStorage.getItem('snakeHighScores');
990
+ return scores ? JSON.parse(scores) : [];
991
+ }
992
+
993
+ updateHighScoresTable() {
994
+ const highScores = this.loadHighScores();
995
+ const table = document.getElementById('scoresTable');
996
+ while (table.rows.length > 1) { table.deleteRow(1); } // Clear existing
997
+
998
+ highScores.forEach((entry, index) => {
999
+ const row = table.insertRow(-1);
1000
+ row.insertCell(0).textContent = index + 1;
1001
+ row.insertCell(1).textContent = entry.score;
1002
+ row.insertCell(2).textContent = entry.difficulty;
1003
+ });
1004
+ }
1005
+ }
1006
+
1007
+ // Create and start the game
1008
+ const game = new SnakeGame();
1009
+
1010
+ </script>
1011
+ </body>
1012
+ </html>