Aleksmorshen commited on
Commit
5119566
·
verified ·
1 Parent(s): b22e569

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +379 -1952
index.html CHANGED
@@ -1,1974 +1,401 @@
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, maximum-scale=1.0, user-scalable=no">
6
- <title>ZOMBIE CITY - Survival Shooter</title>
7
- <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- font-family: 'Courier New', monospace;
13
- touch-action: none;
14
- -webkit-touch-callout: none;
15
- -webkit-user-select: none;
16
- user-select: none;
17
- }
18
-
19
- body {
20
- overflow: hidden;
21
- background: #000;
22
- color: #00ff00;
23
- height: 100vh;
24
- height: 100dvh;
25
- }
26
-
27
- #gameContainer {
28
- position: relative;
29
- width: 100%;
30
- height: 100vh;
31
- height: 100dvh;
32
- overflow: hidden;
33
- }
34
-
35
- #gameCanvas {
36
- position: absolute;
37
- top: 0;
38
- left: 0;
39
- width: 100%;
40
- height: 100%;
41
- z-index: 1;
42
- }
43
-
44
- #ui {
45
- position: absolute;
46
- top: 0;
47
- left: 0;
48
- width: 100%;
49
- height: 100%;
50
- z-index: 2;
51
- pointer-events: none;
52
- }
53
-
54
- #crosshair {
55
- position: absolute;
56
- top: 50%;
57
- left: 50%;
58
- transform: translate(-50%, -50%);
59
- width: 4px;
60
- height: 4px;
61
- background: #00ff00;
62
- border-radius: 50%;
63
- box-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00;
64
- }
65
-
66
- #crosshair::before, #crosshair::after {
67
- content: '';
68
- position: absolute;
69
- background: #00ff00;
70
- box-shadow: 0 0 5px #00ff00;
71
- }
72
-
73
- #crosshair::before {
74
- width: 2px;
75
- height: 15px;
76
- top: -20px;
77
- left: 50%;
78
- transform: translateX(-50%);
79
- }
80
-
81
- #crosshair::after {
82
- width: 15px;
83
- height: 2px;
84
- top: 50%;
85
- left: 15px;
86
- transform: translateY(-50%);
87
- }
88
-
89
- .crosshair-left {
90
- position: absolute;
91
- width: 15px;
92
- height: 2px;
93
- background: #00ff00;
94
- top: 50%;
95
- right: 15px;
96
- transform: translateY(-50%);
97
- box-shadow: 0 0 5px #00ff00;
98
- }
99
-
100
- .crosshair-bottom {
101
- position: absolute;
102
- width: 2px;
103
- height: 15px;
104
- background: #00ff00;
105
- bottom: -20px;
106
- left: 50%;
107
- transform: translateX(-50%);
108
- box-shadow: 0 0 5px #00ff00;
109
- }
110
-
111
- #healthBar {
112
- position: absolute;
113
- bottom: 30px;
114
- left: 30px;
115
- width: 250px;
116
- height: 25px;
117
- background: rgba(0, 0, 0, 0.8);
118
- border: 2px solid #00ff00;
119
- border-radius: 3px;
120
- overflow: hidden;
121
- box-shadow: 0 0 15px rgba(0, 255, 0, 0.5), inset 0 0 10px rgba(0, 0, 0, 0.5);
122
- }
123
-
124
- #healthFill {
125
- height: 100%;
126
- width: 100%;
127
- background: linear-gradient(180deg, #66ff66 0%, #00cc00 50%, #009900 100%);
128
- transition: width 0.3s ease-out;
129
- box-shadow: inset 0 2px 5px rgba(255, 255, 255, 0.3);
130
- }
131
-
132
- #healthText {
133
- position: absolute;
134
- bottom: 60px;
135
- left: 30px;
136
- font-size: 14px;
137
- color: #99ff99;
138
- text-shadow: 0 0 5px #00ff00;
139
- letter-spacing: 2px;
140
- }
141
-
142
- #armorBar {
143
- position: absolute;
144
- bottom: 70px;
145
- left: 30px;
146
- width: 200px;
147
- height: 15px;
148
- background: rgba(0, 0, 0, 0.8);
149
- border: 2px solid #3366ff;
150
- border-radius: 3px;
151
- overflow: hidden;
152
- box-shadow: 0 0 15px rgba(0, 100, 255, 0.5);
153
- }
154
-
155
- #armorFill {
156
- height: 100%;
157
- width: 50%;
158
- background: linear-gradient(180deg, #6699ff 0%, #0033cc 50%, #002299 100%);
159
- transition: width 0.3s ease-out;
160
- }
161
-
162
- #ammoCounter {
163
- position: absolute;
164
- bottom: 30px;
165
- right: 30px;
166
- font-size: 36px;
167
- color: #ffcc00;
168
- text-shadow: 0 0 10px #ff6600, 0 0 20px #ff3300;
169
- background: rgba(0, 0, 0, 0.8);
170
- padding: 15px 25px;
171
- border: 2px solid #ff6600;
172
- border-radius: 5px;
173
- box-shadow: 0 0 20px rgba(255, 100, 0, 0.5);
174
- }
175
-
176
- #ammoLabel {
177
- font-size: 12px;
178
- color: #ff9966;
179
- letter-spacing: 2px;
180
- }
181
-
182
- #score {
183
- position: absolute;
184
- top: 30px;
185
- left: 30px;
186
- font-size: 28px;
187
- color: #00ff00;
188
- text-shadow: 0 0 10px #00ff00;
189
- }
190
-
191
- #killCount {
192
- position: absolute;
193
- top: 70px;
194
- left: 30px;
195
- font-size: 18px;
196
- color: #99ff99;
197
- text-shadow: 0 0 5px #00ff00;
198
- }
199
-
200
- #waveInfo {
201
- position: absolute;
202
- top: 30px;
203
- right: 30px;
204
- font-size: 24px;
205
- color: #ff6666;
206
- text-shadow: 0 0 10px #ff0000;
207
- text-align: right;
208
- }
209
-
210
- #zombieCount {
211
- position: absolute;
212
- top: 60px;
213
- right: 30px;
214
- font-size: 16px;
215
- color: #ff9999;
216
- text-shadow: 0 0 5px #ff0000;
217
- }
218
-
219
- #startScreen {
220
- position: absolute;
221
- top: 0;
222
- left: 0;
223
- width: 100%;
224
- height: 100%;
225
- background: radial-gradient(ellipse at center, rgba(0, 30, 0, 0.9) 0%, rgba(0, 0, 0, 0.95) 100%);
226
- display: flex;
227
- flex-direction: column;
228
- justify-content: center;
229
- align-items: center;
230
- z-index: 10;
231
- text-align: center;
232
- }
233
-
234
- #title {
235
- font-size: 80px;
236
- margin-bottom: 20px;
237
- color: #00ff00;
238
- text-shadow: 0 0 20px #00ff00, 0 0 40px #00ff00, 0 0 60px #00ff00;
239
- letter-spacing: 10px;
240
- animation: pulse 2s infinite;
241
- }
242
-
243
- @keyframes pulse {
244
- 0%, 100% { text-shadow: 0 0 20px #00ff00, 0 0 40px #00ff00; }
245
- 50% { text-shadow: 0 0 30px #00ff00, 0 0 60px #00ff00, 0 0 80px #00ff00; }
246
- }
247
-
248
- #subtitle {
249
- font-size: 24px;
250
- margin-bottom: 60px;
251
- color: #99ff99;
252
- letter-spacing: 5px;
253
- }
254
-
255
- #startButton {
256
- background: linear-gradient(180deg, #33ff33 0%, #00cc00 100%);
257
- color: #000;
258
- border: none;
259
- padding: 20px 60px;
260
- font-size: 28px;
261
- cursor: pointer;
262
- border-radius: 5px;
263
- text-transform: uppercase;
264
- letter-spacing: 5px;
265
- font-weight: bold;
266
- box-shadow: 0 0 30px #00ff00, inset 0 2px 10px rgba(255, 255, 255, 0.3);
267
- transition: all 0.3s;
268
- pointer-events: auto;
269
- }
270
-
271
- #startButton:hover {
272
- background: linear-gradient(180deg, #55ff55 0%, #22ee22 100%);
273
- transform: scale(1.1);
274
- box-shadow: 0 0 50px #00ff00;
275
- }
276
-
277
- #gameOverScreen {
278
- position: absolute;
279
- top: 0;
280
- left: 0;
281
- width: 100%;
282
- height: 100%;
283
- background: rgba(0, 0, 0, 0.9);
284
- display: none;
285
- flex-direction: column;
286
- justify-content: center;
287
- align-items: center;
288
- z-index: 10;
289
- }
290
-
291
- #gameOverTitle {
292
- font-size: 70px;
293
- margin-bottom: 30px;
294
- color: #ff0000;
295
- text-shadow: 0 0 20px #ff0000;
296
- animation: flicker 0.5s infinite;
297
- }
298
-
299
- @keyframes flicker {
300
- 0%, 100% { opacity: 1; }
301
- 50% { opacity: 0.8; }
302
- }
303
-
304
- #finalScore {
305
- font-size: 40px;
306
- margin-bottom: 20px;
307
- color: #ffcc00;
308
- }
309
-
310
- #finalKills {
311
- font-size: 24px;
312
- margin-bottom: 40px;
313
- color: #99ff99;
314
- }
315
-
316
- #restartButton {
317
- background: linear-gradient(180deg, #33ff33 0%, #00cc00 100%);
318
- color: #000;
319
- border: none;
320
- padding: 20px 60px;
321
- font-size: 28px;
322
- cursor: pointer;
323
- border-radius: 5px;
324
- text-transform: uppercase;
325
- letter-spacing: 5px;
326
- font-weight: bold;
327
- box-shadow: 0 0 30px #00ff00;
328
- transition: all 0.3s;
329
- pointer-events: auto;
330
- }
331
-
332
- #restartButton:hover {
333
- background: linear-gradient(180deg, #55ff55 0%, #22ee22 100%);
334
- transform: scale(1.1);
335
- }
336
-
337
- #hitEffect {
338
- position: absolute;
339
- top: 0;
340
- left: 0;
341
- width: 100%;
342
- height: 100%;
343
- background: radial-gradient(ellipse at center, transparent 0%, rgba(255, 0, 0, 0.4) 100%);
344
- pointer-events: none;
345
- opacity: 0;
346
- z-index: 4;
347
- transition: opacity 0.1s;
348
- }
349
-
350
- #damageVignette {
351
- position: absolute;
352
- top: 0;
353
- left: 0;
354
- width: 100%;
355
- height: 100%;
356
- background: radial-gradient(ellipse at center, transparent 40%, rgba(100, 0, 0, 0.8) 100%);
357
- pointer-events: none;
358
- opacity: 0;
359
- z-index: 3;
360
- transition: opacity 0.5s;
361
- }
362
-
363
- #mobileControls {
364
- position: absolute;
365
- bottom: 0;
366
- width: 100%;
367
- height: 200px;
368
- display: none;
369
- justify-content: space-between;
370
- padding: 20px 30px;
371
- z-index: 10;
372
- pointer-events: none;
373
- }
374
-
375
- .joystickContainer {
376
- width: 150px;
377
- height: 150px;
378
- position: relative;
379
- pointer-events: auto;
380
- }
381
-
382
- .joystickBase {
383
- width: 150px;
384
- height: 150px;
385
- background: radial-gradient(circle, rgba(0, 255, 0, 0.3) 0%, rgba(0, 200, 0, 0.1) 100%);
386
- border: 3px solid rgba(0, 255, 0, 0.5);
387
- border-radius: 50%;
388
- position: absolute;
389
- top: 0;
390
- left: 0;
391
- }
392
-
393
- .joystickStick {
394
- width: 60px;
395
- height: 60px;
396
- background: radial-gradient(circle, rgba(100, 255, 100, 0.9) 0%, rgba(0, 200, 0, 0.8) 100%);
397
- border: 2px solid rgba(150, 255, 150, 0.8);
398
- border-radius: 50%;
399
- position: absolute;
400
- top: 50%;
401
- left: 50%;
402
- transform: translate(-50%, -50%);
403
- box-shadow: 0 0 20px rgba(0, 255, 0, 0.5);
404
- }
405
-
406
- #shootButton {
407
- width: 100px;
408
- height: 100px;
409
- background: radial-gradient(circle, rgba(255, 100, 0, 0.8) 0%, rgba(200, 50, 0, 0.6) 100%);
410
- border: 3px solid rgba(255, 150, 50, 0.8);
411
- border-radius: 50%;
412
- display: flex;
413
- justify-content: center;
414
- align-items: center;
415
- font-size: 16px;
416
- font-weight: bold;
417
- color: white;
418
- text-shadow: 0 0 10px #ff6600;
419
- pointer-events: auto;
420
- box-shadow: 0 0 30px rgba(255, 100, 0, 0.5);
421
- position: absolute;
422
- bottom: 50px;
423
- right: 180px;
424
- }
425
-
426
- #reloadButton {
427
- width: 70px;
428
- height: 70px;
429
- background: radial-gradient(circle, rgba(100, 100, 255, 0.8) 0%, rgba(50, 50, 200, 0.6) 100%);
430
- border: 3px solid rgba(150, 150, 255, 0.8);
431
- border-radius: 50%;
432
- display: flex;
433
- justify-content: center;
434
- align-items: center;
435
- font-size: 12px;
436
- font-weight: bold;
437
- color: white;
438
- pointer-events: auto;
439
- box-shadow: 0 0 20px rgba(100, 100, 255, 0.5);
440
- position: absolute;
441
- bottom: 160px;
442
- right: 200px;
443
- }
444
-
445
- #minimap {
446
- position: absolute;
447
- top: 100px;
448
- right: 30px;
449
- width: 150px;
450
- height: 150px;
451
- background: rgba(0, 0, 0, 0.7);
452
- border: 2px solid #00ff00;
453
- border-radius: 5px;
454
- overflow: hidden;
455
- }
456
-
457
- #minimapCanvas {
458
- width: 100%;
459
- height: 100%;
460
- }
461
-
462
- @media (max-width: 768px) {
463
- #mobileControls {
464
- display: flex;
465
- }
466
-
467
- #title {
468
- font-size: 48px;
469
- }
470
-
471
- #subtitle {
472
- font-size: 16px;
473
- padding: 0 20px;
474
- }
475
-
476
- #startButton, #restartButton {
477
- padding: 15px 40px;
478
- font-size: 20px;
479
- }
480
-
481
- #healthBar {
482
- width: 150px;
483
- height: 20px;
484
- bottom: 220px;
485
- left: 20px;
486
- }
487
-
488
- #armorBar {
489
- width: 120px;
490
- height: 12px;
491
- bottom: 250px;
492
- left: 20px;
493
- }
494
-
495
- #ammoCounter {
496
- bottom: 220px;
497
- right: 20px;
498
- font-size: 24px;
499
- padding: 10px 15px;
500
- }
501
-
502
- #score {
503
- top: 20px;
504
- left: 20px;
505
- font-size: 20px;
506
- }
507
-
508
- #killCount {
509
- top: 50px;
510
- font-size: 14px;
511
- }
512
-
513
- #waveInfo {
514
- top: 20px;
515
- font-size: 18px;
516
- }
517
-
518
- #minimap {
519
- display: none;
520
- }
521
- }
522
- </style>
523
  </head>
524
  <body>
525
- <div id="gameContainer">
526
- <div id="gameCanvas"></div>
527
-
528
- <div id="ui">
529
- <div id="crosshair">
530
- <div class="crosshair-left"></div>
531
- <div class="crosshair-bottom"></div>
532
- </div>
533
- <div id="healthText">HEALTH</div>
534
- <div id="healthBar">
535
- <div id="healthFill"></div>
536
- </div>
537
- <div id="armorBar">
538
- <div id="armorFill"></div>
539
- </div>
540
- <div id="ammoCounter">
541
- <div id="ammoLabel">AMMO</div>
542
- <span id="ammoValue">30</span> / <span id="ammoMax">30</span>
543
- </div>
544
- <div id="score">SCORE: <span id="scoreValue">0</span></div>
545
- <div id="killCount">KILLS: <span id="killValue">0</span></div>
546
- <div id="waveInfo">WAVE: <span id="waveValue">1</span></div>
547
- <div id="zombieCount">ZOMBIES: <span id="zombieValue">0</span></div>
548
- <div id="hitEffect"></div>
549
- <div id="damageVignette"></div>
550
- <div id="minimap">
551
- <canvas id="minimapCanvas"></canvas>
552
- </div>
553
- </div>
554
-
555
- <div id="mobileControls">
556
- <div class="joystickContainer" id="moveJoystick">
557
- <div class="joystickBase"></div>
558
- <div class="joystickStick" id="moveStick"></div>
559
- </div>
560
- <div class="joystickContainer" id="lookJoystick">
561
- <div class="joystickBase"></div>
562
- <div class="joystickStick" id="lookStick"></div>
563
- </div>
564
- <div id="shootButton">FIRE</div>
565
- <div id="reloadButton">R</div>
566
- </div>
567
-
568
- <div id="startScreen">
569
- <h1 id="title">ZOMBIE CITY</h1>
570
- <p id="subtitle">SURVIVE THE UNDEAD APOCALYPSE</p>
571
- <button id="startButton">START SURVIVAL</button>
572
- </div>
573
-
574
- <div id="gameOverScreen">
575
- <h1 id="gameOverTitle">YOU DIED</h1>
576
- <p id="finalScore">SCORE: <span id="finalScoreValue">0</span></p>
577
- <p id="finalKills">ZOMBIES KILLED: <span id="finalKillValue">0</span></p>
578
- <button id="restartButton">TRY AGAIN</button>
579
- </div>
580
- </div>
581
 
582
- <script type="importmap">
583
- {
584
- "imports": {
585
- "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
586
- "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
587
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  }
589
- </script>
590
-
591
- <script type="module">
592
- import * as THREE from 'three';
593
- import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
594
- import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
595
- import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
596
- import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
597
- import { SMAAPass } from 'three/addons/postprocessing/SMAAPass.js';
598
-
599
- let scene, camera, renderer, composer;
600
- let player, enemies = [], bullets = [], particles = [];
601
- let playerHealth = 100, playerArmor = 50, ammo = 30, maxAmmo = 30, reserveAmmo = 200;
602
- let score = 0, kills = 0, wave = 1;
603
- let gameActive = false, isReloading = false;
604
- let moveForward = false, moveBackward = false, moveLeft = false, moveRight = false;
605
- let canShoot = true, shotCooldown = 80;
606
- let clock = new THREE.Clock();
607
- let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
608
- let screenShake = { intensity: 0, decay: 0.9 };
609
- let cameraShakeOffset = new THREE.Vector3();
610
- let weaponBob = 0, weaponRecoil = 0;
611
- let moveJoystickData = { active: false, x: 0, y: 0 };
612
- let lookJoystickData = { active: false, x: 0, y: 0, lastX: 0, lastY: 0 };
613
- let mouseMovement = { x: 0, y: 0 };
614
- let isPointerLocked = false;
615
- let streetLights = [];
616
- let cars = [];
617
- let buildings = [];
618
- let collisionObjects = [];
619
- let minimapCtx;
620
- let spawnPoints = [];
621
-
622
- function init() {
623
- scene = new THREE.Scene();
624
- scene.background = new THREE.Color(0x1a2a3a);
625
- scene.fog = new THREE.FogExp2(0x1a2a3a, 0.008);
626
-
627
- camera = new THREE.PerspectiveCamera(85, window.innerWidth / window.innerHeight, 0.1, 1000);
628
- camera.position.set(0, 1.7, 0);
629
-
630
- renderer = new THREE.WebGLRenderer({
631
- antialias: true,
632
- powerPreference: "high-performance"
633
- });
634
- renderer.setSize(window.innerWidth, window.innerHeight);
635
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
636
- renderer.shadowMap.enabled = true;
637
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
638
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
639
- renderer.toneMappingExposure = 1.0;
640
- document.getElementById('gameCanvas').appendChild(renderer.domElement);
641
-
642
- setupPostProcessing();
643
- createCity();
644
- createLighting();
645
- setupControls();
646
- setupMinimap();
647
-
648
- player = new THREE.Object3D();
649
- player.position.set(0, 1.7, 0);
650
- player.add(camera);
651
- scene.add(player);
652
-
653
- document.getElementById('startButton').addEventListener('click', startGame);
654
- document.getElementById('startButton').addEventListener('touchend', startGame);
655
- document.getElementById('restartButton').addEventListener('click', restartGame);
656
- document.getElementById('restartButton').addEventListener('touchend', restartGame);
657
-
658
- window.addEventListener('resize', onWindowResize);
659
-
660
- animate();
661
- }
662
-
663
- function setupPostProcessing() {
664
- composer = new EffectComposer(renderer);
665
-
666
- const renderPass = new RenderPass(scene, camera);
667
- composer.addPass(renderPass);
668
-
669
- const bloomPass = new UnrealBloomPass(
670
- new THREE.Vector2(window.innerWidth, window.innerHeight),
671
- 0.3, 0.4, 0.85
672
- );
673
- composer.addPass(bloomPass);
674
-
675
- const smaaPass = new SMAAPass(window.innerWidth, window.innerHeight);
676
- composer.addPass(smaaPass);
677
- }
678
-
679
- function createCity() {
680
- const groundGeometry = new THREE.PlaneGeometry(500, 500, 100, 100);
681
- const groundMaterial = new THREE.MeshStandardMaterial({
682
- color: 0x2a2a2a,
683
- roughness: 0.9,
684
- metalness: 0.1
685
- });
686
- const ground = new THREE.Mesh(groundGeometry, groundMaterial);
687
- ground.rotation.x = -Math.PI / 2;
688
- ground.receiveShadow = true;
689
- scene.add(ground);
690
-
691
- createRoads();
692
- createBuildings();
693
- createCars();
694
- createStreetLights();
695
- createProps();
696
- createSpawnPoints();
697
- }
698
-
699
- function createRoads() {
700
- const roadMaterial = new THREE.MeshStandardMaterial({
701
- color: 0x333333,
702
- roughness: 0.8
703
- });
704
-
705
- const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
706
- const whiteLine = new THREE.MeshBasicMaterial({ color: 0xffffff });
707
-
708
- for (let i = -2; i <= 2; i++) {
709
- const roadH = new THREE.Mesh(
710
- new THREE.PlaneGeometry(500, 15),
711
- roadMaterial
712
- );
713
- roadH.rotation.x = -Math.PI / 2;
714
- roadH.position.set(0, 0.01, i * 80);
715
- scene.add(roadH);
716
-
717
- for (let j = -25; j <= 25; j++) {
718
- const line = new THREE.Mesh(
719
- new THREE.PlaneGeometry(4, 0.3),
720
- lineMaterial
721
- );
722
- line.rotation.x = -Math.PI / 2;
723
- line.position.set(j * 10, 0.02, i * 80);
724
- scene.add(line);
725
- }
726
-
727
- const roadV = new THREE.Mesh(
728
- new THREE.PlaneGeometry(15, 500),
729
- roadMaterial
730
- );
731
- roadV.rotation.x = -Math.PI / 2;
732
- roadV.position.set(i * 80, 0.01, 0);
733
- scene.add(roadV);
734
- }
735
-
736
- const sidewalkMaterial = new THREE.MeshStandardMaterial({
737
- color: 0x666666,
738
- roughness: 0.7
739
- });
740
-
741
- for (let i = -2; i <= 2; i++) {
742
- for (let side = -1; side <= 1; side += 2) {
743
- const sidewalkH = new THREE.Mesh(
744
- new THREE.BoxGeometry(500, 0.15, 3),
745
- sidewalkMaterial
746
- );
747
- sidewalkH.position.set(0, 0.075, i * 80 + side * 9);
748
- sidewalkH.receiveShadow = true;
749
- scene.add(sidewalkH);
750
- }
751
- }
752
- }
753
-
754
- function createBuildings() {
755
- const buildingColors = [0x4a5568, 0x5a6578, 0x3a4558, 0x6a7588, 0x2d3748];
756
-
757
- const gridPositions = [];
758
- for (let x = -3; x <= 3; x++) {
759
- for (let z = -3; z <= 3; z++) {
760
- if (Math.abs(x) <= 2 && Math.abs(z) <= 2) {
761
- if (x !== 0 && z !== 0) {
762
- gridPositions.push({ x: x * 80 - 35, z: z * 80 - 35 });
763
- gridPositions.push({ x: x * 80 + 35, z: z * 80 - 35 });
764
- gridPositions.push({ x: x * 80 - 35, z: z * 80 + 35 });
765
- gridPositions.push({ x: x * 80 + 35, z: z * 80 + 35 });
766
- }
767
- }
768
- }
769
- }
770
-
771
- gridPositions.forEach(pos => {
772
- const width = 20 + Math.random() * 25;
773
- const depth = 20 + Math.random() * 25;
774
- const height = 15 + Math.random() * 60;
775
-
776
- const buildingGeometry = new THREE.BoxGeometry(width, height, depth);
777
- const buildingMaterial = new THREE.MeshStandardMaterial({
778
- color: buildingColors[Math.floor(Math.random() * buildingColors.length)],
779
- roughness: 0.8,
780
- metalness: 0.2
781
- });
782
-
783
- const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
784
- building.position.set(pos.x, height / 2, pos.z);
785
- building.castShadow = true;
786
- building.receiveShadow = true;
787
- scene.add(building);
788
- buildings.push(building);
789
-
790
- collisionObjects.push({
791
- position: new THREE.Vector3(pos.x, height / 2, pos.z),
792
- width: width,
793
- depth: depth
794
- });
795
-
796
- const windowMaterial = new THREE.MeshBasicMaterial({
797
- color: Math.random() > 0.3 ? 0xffffaa : 0x333333
798
- });
799
-
800
- const windowRows = Math.floor(height / 4);
801
- const windowCols = Math.floor(width / 4);
802
-
803
- for (let row = 0; row < windowRows; row++) {
804
- for (let col = 0; col < windowCols; col++) {
805
- if (Math.random() > 0.2) {
806
- const windowGeom = new THREE.PlaneGeometry(1.5, 2);
807
- const windowMat = new THREE.MeshBasicMaterial({
808
- color: Math.random() > 0.4 ? 0xffffcc : 0x222222
809
- });
810
-
811
- const windowMesh = new THREE.Mesh(windowGeom, windowMat);
812
- windowMesh.position.set(
813
- pos.x - width/2 + 2 + col * 4,
814
- 2 + row * 4,
815
- pos.z + depth/2 + 0.1
816
- );
817
- scene.add(windowMesh);
818
-
819
- const windowMesh2 = windowMesh.clone();
820
- windowMesh2.position.z = pos.z - depth/2 - 0.1;
821
- windowMesh2.rotation.y = Math.PI;
822
- scene.add(windowMesh2);
823
- }
824
- }
825
- }
826
- });
827
- }
828
-
829
- function createCars() {
830
- const carColors = [0xff0000, 0x0000ff, 0x00ff00, 0xffff00, 0xff00ff, 0x00ffff, 0xffffff, 0x333333];
831
-
832
- const carPositions = [
833
- { x: 20, z: 80, rot: 0 },
834
- { x: -30, z: 80, rot: 0 },
835
- { x: 50, z: -80, rot: Math.PI },
836
- { x: -60, z: -80, rot: Math.PI },
837
- { x: 80, z: 30, rot: Math.PI / 2 },
838
- { x: 80, z: -40, rot: Math.PI / 2 },
839
- { x: -80, z: 20, rot: -Math.PI / 2 },
840
- { x: -80, z: -50, rot: -Math.PI / 2 },
841
- { x: 10, z: 0, rot: 0 },
842
- { x: -25, z: 0, rot: Math.PI },
843
- { x: 0, z: 25, rot: Math.PI / 2 },
844
- { x: 0, z: -30, rot: -Math.PI / 2 }
845
- ];
846
-
847
- carPositions.forEach(pos => {
848
- const carGroup = new THREE.Group();
849
-
850
- const bodyGeometry = new THREE.BoxGeometry(4, 1.2, 2);
851
- const bodyMaterial = new THREE.MeshStandardMaterial({
852
- color: carColors[Math.floor(Math.random() * carColors.length)],
853
- roughness: 0.3,
854
- metalness: 0.8
855
- });
856
- const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
857
- body.position.y = 0.8;
858
- body.castShadow = true;
859
- carGroup.add(body);
860
-
861
- const topGeometry = new THREE.BoxGeometry(2.5, 1, 1.8);
862
- const topMaterial = new THREE.MeshStandardMaterial({
863
- color: 0x88ccff,
864
- roughness: 0.1,
865
- metalness: 0.9,
866
- transparent: true,
867
- opacity: 0.7
868
- });
869
- const top = new THREE.Mesh(topGeometry, topMaterial);
870
- top.position.set(-0.3, 1.8, 0);
871
- carGroup.add(top);
872
-
873
- const wheelGeometry = new THREE.CylinderGeometry(0.4, 0.4, 0.3, 16);
874
- const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x111111 });
875
-
876
- const wheelPositions = [
877
- { x: 1.3, y: 0.4, z: 1 },
878
- { x: 1.3, y: 0.4, z: -1 },
879
- { x: -1.3, y: 0.4, z: 1 },
880
- { x: -1.3, y: 0.4, z: -1 }
881
- ];
882
-
883
- wheelPositions.forEach(wPos => {
884
- const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
885
- wheel.rotation.x = Math.PI / 2;
886
- wheel.position.set(wPos.x, wPos.y, wPos.z);
887
- carGroup.add(wheel);
888
- });
889
-
890
- const headlightGeom = new THREE.SphereGeometry(0.15, 8, 8);
891
- const headlightMat = new THREE.MeshBasicMaterial({ color: 0xffffcc });
892
-
893
- const hl1 = new THREE.Mesh(headlightGeom, headlightMat);
894
- hl1.position.set(2, 0.8, 0.6);
895
- carGroup.add(hl1);
896
-
897
- const hl2 = new THREE.Mesh(headlightGeom, headlightMat);
898
- hl2.position.set(2, 0.8, -0.6);
899
- carGroup.add(hl2);
900
-
901
- carGroup.position.set(pos.x, 0, pos.z);
902
- carGroup.rotation.y = pos.rot;
903
- scene.add(carGroup);
904
- cars.push(carGroup);
905
-
906
- collisionObjects.push({
907
- position: new THREE.Vector3(pos.x, 1, pos.z),
908
- width: 5,
909
- depth: 2.5,
910
- rotation: pos.rot
911
- });
912
- });
913
- }
914
-
915
- function createStreetLights() {
916
- const lightPositions = [];
917
-
918
- for (let x = -200; x <= 200; x += 40) {
919
- for (let z = -2; z <= 2; z++) {
920
- lightPositions.push({ x: x, z: z * 80 + 11 });
921
- lightPositions.push({ x: x, z: z * 80 - 11 });
922
- }
923
- }
924
-
925
- for (let z = -200; z <= 200; z += 40) {
926
- for (let x = -2; x <= 2; x++) {
927
- lightPositions.push({ x: x * 80 + 11, z: z });
928
- lightPositions.push({ x: x * 80 - 11, z: z });
929
- }
930
- }
931
-
932
- const uniquePositions = [];
933
- lightPositions.forEach(pos => {
934
- const exists = uniquePositions.some(p =>
935
- Math.abs(p.x - pos.x) < 5 && Math.abs(p.z - pos.z) < 5
936
- );
937
- if (!exists && Math.abs(pos.x) < 200 && Math.abs(pos.z) < 200) {
938
- uniquePositions.push(pos);
939
- }
940
- });
941
-
942
- uniquePositions.slice(0, 80).forEach(pos => {
943
- const poleGeometry = new THREE.CylinderGeometry(0.1, 0.15, 6, 8);
944
- const poleMaterial = new THREE.MeshStandardMaterial({ color: 0x444444 });
945
- const pole = new THREE.Mesh(poleGeometry, poleMaterial);
946
- pole.position.set(pos.x, 3, pos.z);
947
- pole.castShadow = true;
948
- scene.add(pole);
949
-
950
- const armGeometry = new THREE.BoxGeometry(2, 0.1, 0.1);
951
- const arm = new THREE.Mesh(armGeometry, poleMaterial);
952
- arm.position.set(pos.x + 1, 6, pos.z);
953
- scene.add(arm);
954
-
955
- const lampGeometry = new THREE.SphereGeometry(0.3, 8, 8);
956
- const lampMaterial = new THREE.MeshBasicMaterial({ color: 0xffffcc });
957
- const lamp = new THREE.Mesh(lampGeometry, lampMaterial);
958
- lamp.position.set(pos.x + 2, 5.8, pos.z);
959
- scene.add(lamp);
960
-
961
- const streetLight = new THREE.PointLight(0xffffcc, 1, 25);
962
- streetLight.position.set(pos.x + 2, 5.5, pos.z);
963
- streetLight.castShadow = true;
964
- streetLight.shadow.mapSize.width = 512;
965
- streetLight.shadow.mapSize.height = 512;
966
- scene.add(streetLight);
967
- streetLights.push(streetLight);
968
-
969
- collisionObjects.push({
970
- position: new THREE.Vector3(pos.x, 3, pos.z),
971
- radius: 0.3
972
- });
973
- });
974
- }
975
-
976
- function createProps() {
977
- const trashCanGeometry = new THREE.CylinderGeometry(0.4, 0.35, 1, 12);
978
- const trashCanMaterial = new THREE.MeshStandardMaterial({ color: 0x228822 });
979
-
980
- for (let i = 0; i < 30; i++) {
981
- const trashCan = new THREE.Mesh(trashCanGeometry, trashCanMaterial);
982
- const angle = Math.random() * Math.PI * 2;
983
- const dist = 20 + Math.random() * 150;
984
- trashCan.position.set(
985
- Math.cos(angle) * dist,
986
- 0.5,
987
- Math.sin(angle) * dist
988
- );
989
- trashCan.castShadow = true;
990
- scene.add(trashCan);
991
- }
992
-
993
- const benchGeometry = new THREE.BoxGeometry(2, 0.5, 0.5);
994
- const benchMaterial = new THREE.MeshStandardMaterial({ color: 0x8b4513 });
995
-
996
- for (let i = 0; i < 20; i++) {
997
- const bench = new THREE.Mesh(benchGeometry, benchMaterial);
998
- const angle = Math.random() * Math.PI * 2;
999
- const dist = 30 + Math.random() * 120;
1000
- bench.position.set(
1001
- Math.cos(angle) * dist,
1002
- 0.5,
1003
- Math.sin(angle) * dist
1004
- );
1005
- bench.rotation.y = Math.random() * Math.PI;
1006
- bench.castShadow = true;
1007
- scene.add(bench);
1008
- }
1009
-
1010
- const barrelGeometry = new THREE.CylinderGeometry(0.5, 0.5, 1.2, 12);
1011
- const barrelMaterial = new THREE.MeshStandardMaterial({ color: 0x884422 });
1012
-
1013
- for (let i = 0; i < 15; i++) {
1014
- const barrel = new THREE.Mesh(barrelGeometry, barrelMaterial);
1015
- const angle = Math.random() * Math.PI * 2;
1016
- const dist = 25 + Math.random() * 100;
1017
- barrel.position.set(
1018
- Math.cos(angle) * dist,
1019
- 0.6,
1020
- Math.sin(angle) * dist
1021
- );
1022
- if (Math.random() > 0.7) {
1023
- barrel.rotation.x = Math.PI / 2;
1024
- barrel.position.y = 0.5;
1025
- }
1026
- barrel.castShadow = true;
1027
- scene.add(barrel);
1028
- }
1029
- }
1030
-
1031
- function createSpawnPoints() {
1032
- for (let x = -2; x <= 2; x++) {
1033
- for (let z = -2; z <= 2; z++) {
1034
- if (x !== 0 || z !== 0) {
1035
- spawnPoints.push(new THREE.Vector3(x * 60, 0, z * 60));
1036
- }
1037
- }
1038
- }
1039
-
1040
- spawnPoints.push(new THREE.Vector3(100, 0, 0));
1041
- spawnPoints.push(new THREE.Vector3(-100, 0, 0));
1042
- spawnPoints.push(new THREE.Vector3(0, 0, 100));
1043
- spawnPoints.push(new THREE.Vector3(0, 0, -100));
1044
- spawnPoints.push(new THREE.Vector3(100, 0, 100));
1045
- spawnPoints.push(new THREE.Vector3(-100, 0, -100));
1046
- spawnPoints.push(new THREE.Vector3(100, 0, -100));
1047
- spawnPoints.push(new THREE.Vector3(-100, 0, 100));
1048
- }
1049
-
1050
- function createLighting() {
1051
- const ambientLight = new THREE.AmbientLight(0x404060, 0.4);
1052
- scene.add(ambientLight);
1053
-
1054
- const moonLight = new THREE.DirectionalLight(0x6688cc, 0.5);
1055
- moonLight.position.set(50, 100, 50);
1056
- moonLight.castShadow = true;
1057
- moonLight.shadow.mapSize.width = 4096;
1058
- moonLight.shadow.mapSize.height = 4096;
1059
- moonLight.shadow.camera.near = 0.5;
1060
- moonLight.shadow.camera.far = 300;
1061
- moonLight.shadow.camera.left = -150;
1062
- moonLight.shadow.camera.right = 150;
1063
- moonLight.shadow.camera.top = 150;
1064
- moonLight.shadow.camera.bottom = -150;
1065
- scene.add(moonLight);
1066
-
1067
- const hemisphereLight = new THREE.HemisphereLight(0x6688aa, 0x222233, 0.3);
1068
- scene.add(hemisphereLight);
1069
- }
1070
-
1071
- function setupMinimap() {
1072
- const canvas = document.getElementById('minimapCanvas');
1073
- canvas.width = 150;
1074
- canvas.height = 150;
1075
- minimapCtx = canvas.getContext('2d');
1076
- }
1077
-
1078
- function updateMinimap() {
1079
- if (!minimapCtx) return;
1080
-
1081
- minimapCtx.fillStyle = 'rgba(20, 30, 40, 0.9)';
1082
- minimapCtx.fillRect(0, 0, 150, 150);
1083
-
1084
- const scale = 0.4;
1085
- const offsetX = 75;
1086
- const offsetZ = 75;
1087
-
1088
- minimapCtx.strokeStyle = '#333';
1089
- minimapCtx.lineWidth = 2;
1090
- for (let i = -2; i <= 2; i++) {
1091
- minimapCtx.beginPath();
1092
- minimapCtx.moveTo(0, 75 + i * 80 * scale);
1093
- minimapCtx.lineTo(150, 75 + i * 80 * scale);
1094
- minimapCtx.stroke();
1095
-
1096
- minimapCtx.beginPath();
1097
- minimapCtx.moveTo(75 + i * 80 * scale, 0);
1098
- minimapCtx.lineTo(75 + i * 80 * scale, 150);
1099
- minimapCtx.stroke();
1100
- }
1101
-
1102
- minimapCtx.fillStyle = '#555';
1103
- buildings.forEach(b => {
1104
- const bx = b.position.x * scale + offsetX;
1105
- const bz = b.position.z * scale + offsetZ;
1106
- minimapCtx.fillRect(bx - 5, bz - 5, 10, 10);
1107
- });
1108
-
1109
- minimapCtx.fillStyle = '#00ff00';
1110
- const px = player.position.x * scale + offsetX;
1111
- const pz = player.position.z * scale + offsetZ;
1112
- minimapCtx.beginPath();
1113
- minimapCtx.arc(px, pz, 4, 0, Math.PI * 2);
1114
- minimapCtx.fill();
1115
-
1116
- minimapCtx.strokeStyle = '#00ff00';
1117
- minimapCtx.lineWidth = 2;
1118
- minimapCtx.beginPath();
1119
- minimapCtx.moveTo(px, pz);
1120
- const angle = player.rotation.y;
1121
- minimapCtx.lineTo(px - Math.sin(angle) * 10, pz - Math.cos(angle) * 10);
1122
- minimapCtx.stroke();
1123
-
1124
- enemies.forEach(enemy => {
1125
- const ex = enemy.mesh.position.x * scale + offsetX;
1126
- const ez = enemy.mesh.position.z * scale + offsetZ;
1127
-
1128
- if (ex >= 0 && ex <= 150 && ez >= 0 && ez <= 150) {
1129
- minimapCtx.fillStyle = '#ff0000';
1130
- minimapCtx.beginPath();
1131
- minimapCtx.arc(ex, ez, 2, 0, Math.PI * 2);
1132
- minimapCtx.fill();
1133
  }
1134
- });
1135
- }
1136
-
1137
- function setupControls() {
1138
- if (!isMobile) {
1139
- document.addEventListener('keydown', onKeyDown);
1140
- document.addEventListener('keyup', onKeyUp);
1141
- document.addEventListener('mousedown', onMouseDown);
1142
- document.addEventListener('mouseup', onMouseUp);
1143
- document.addEventListener('mousemove', onMouseMove);
1144
- document.addEventListener('click', () => {
1145
- if (gameActive && !isPointerLocked) {
1146
- renderer.domElement.requestPointerLock();
1147
- }
1148
- });
1149
- document.addEventListener('pointerlockchange', () => {
1150
- isPointerLocked = document.pointerLockElement === renderer.domElement;
1151
- });
1152
  } else {
1153
- setupMobileControls();
1154
- }
1155
- }
1156
-
1157
- function setupMobileControls() {
1158
- const moveJoystick = document.getElementById('moveJoystick');
1159
- const moveStick = document.getElementById('moveStick');
1160
- const lookJoystick = document.getElementById('lookJoystick');
1161
- const lookStick = document.getElementById('lookStick');
1162
- const shootButton = document.getElementById('shootButton');
1163
- const reloadButton = document.getElementById('reloadButton');
1164
-
1165
- let moveTouchId = null;
1166
- let lookTouchId = null;
1167
-
1168
- moveJoystick.addEventListener('touchstart', (e) => {
1169
- e.preventDefault();
1170
- const touch = e.changedTouches[0];
1171
- moveTouchId = touch.identifier;
1172
- moveJoystickData.active = true;
1173
- updateJoystick(touch, moveJoystick, moveStick, moveJoystickData);
1174
- });
1175
-
1176
- moveJoystick.addEventListener('touchmove', (e) => {
1177
- e.preventDefault();
1178
- for (let touch of e.changedTouches) {
1179
- if (touch.identifier === moveTouchId) {
1180
- updateJoystick(touch, moveJoystick, moveStick, moveJoystickData);
1181
- }
1182
- }
1183
- });
1184
-
1185
- moveJoystick.addEventListener('touchend', (e) => {
1186
- for (let touch of e.changedTouches) {
1187
- if (touch.identifier === moveTouchId) {
1188
- moveTouchId = null;
1189
- moveJoystickData.active = false;
1190
- moveJoystickData.x = 0;
1191
- moveJoystickData.y = 0;
1192
- moveStick.style.transform = 'translate(-50%, -50%)';
1193
- moveForward = moveBackward = moveLeft = moveRight = false;
1194
- }
1195
- }
1196
- });
1197
-
1198
- lookJoystick.addEventListener('touchstart', (e) => {
1199
- e.preventDefault();
1200
- const touch = e.changedTouches[0];
1201
- lookTouchId = touch.identifier;
1202
- lookJoystickData.active = true;
1203
- lookJoystickData.lastX = touch.clientX;
1204
- lookJoystickData.lastY = touch.clientY;
1205
- });
1206
-
1207
- lookJoystick.addEventListener('touchmove', (e) => {
1208
- e.preventDefault();
1209
- for (let touch of e.changedTouches) {
1210
- if (touch.identifier === lookTouchId) {
1211
- const deltaX = touch.clientX - lookJoystickData.lastX;
1212
- const deltaY = touch.clientY - lookJoystickData.lastY;
1213
- lookJoystickData.x = deltaX * 0.01;
1214
- lookJoystickData.y = deltaY * 0.01;
1215
- lookJoystickData.lastX = touch.clientX;
1216
- lookJoystickData.lastY = touch.clientY;
1217
-
1218
- const rect = lookJoystick.getBoundingClientRect();
1219
- const centerX = rect.left + rect.width / 2;
1220
- const centerY = rect.top + rect.height / 2;
1221
- let dx = touch.clientX - centerX;
1222
- let dy = touch.clientY - centerY;
1223
- const dist = Math.sqrt(dx * dx + dy * dy);
1224
- const maxDist = 45;
1225
- if (dist > maxDist) {
1226
- dx = dx * maxDist / dist;
1227
- dy = dy * maxDist / dist;
1228
- }
1229
- lookStick.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
1230
- }
1231
- }
1232
- });
1233
-
1234
- lookJoystick.addEventListener('touchend', (e) => {
1235
- for (let touch of e.changedTouches) {
1236
- if (touch.identifier === lookTouchId) {
1237
- lookTouchId = null;
1238
- lookJoystickData.active = false;
1239
- lookJoystickData.x = 0;
1240
- lookJoystickData.y = 0;
1241
- lookStick.style.transform = 'translate(-50%, -50%)';
1242
- }
1243
- }
1244
- });
1245
-
1246
- shootButton.addEventListener('touchstart', (e) => {
1247
- e.preventDefault();
1248
- if (gameActive) shoot();
1249
- });
1250
-
1251
- reloadButton.addEventListener('touchstart', (e) => {
1252
- e.preventDefault();
1253
- if (gameActive) reload();
1254
- });
1255
-
1256
- function updateJoystick(touch, container, stick, data) {
1257
- const rect = container.getBoundingClientRect();
1258
- const centerX = rect.left + rect.width / 2;
1259
- const centerY = rect.top + rect.height / 2;
1260
-
1261
- let dx = touch.clientX - centerX;
1262
- let dy = touch.clientY - centerY;
1263
-
1264
- const dist = Math.sqrt(dx * dx + dy * dy);
1265
- const maxDist = 45;
1266
-
1267
- if (dist > maxDist) {
1268
- dx = dx * maxDist / dist;
1269
- dy = dy * maxDist / dist;
1270
- }
1271
-
1272
- data.x = dx / maxDist;
1273
- data.y = dy / maxDist;
1274
-
1275
- stick.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
1276
-
1277
- const threshold = 0.3;
1278
- moveForward = data.y < -threshold;
1279
- moveBackward = data.y > threshold;
1280
- moveLeft = data.x < -threshold;
1281
- moveRight = data.x > threshold;
1282
- }
1283
- }
1284
-
1285
- function onKeyDown(e) {
1286
- if (!gameActive) return;
1287
- switch (e.code) {
1288
- case 'KeyW': case 'ArrowUp': moveForward = true; break;
1289
- case 'KeyS': case 'ArrowDown': moveBackward = true; break;
1290
- case 'KeyA': case 'ArrowLeft': moveLeft = true; break;
1291
- case 'KeyD': case 'ArrowRight': moveRight = true; break;
1292
- case 'KeyR': reload(); break;
1293
- }
1294
- }
1295
-
1296
- function onKeyUp(e) {
1297
- switch (e.code) {
1298
- case 'KeyW': case 'ArrowUp': moveForward = false; break;
1299
- case 'KeyS': case 'ArrowDown': moveBackward = false; break;
1300
- case 'KeyA': case 'ArrowLeft': moveLeft = false; break;
1301
- case 'KeyD': case 'ArrowRight': moveRight = false; break;
1302
- }
1303
- }
1304
-
1305
- function onMouseDown(e) {
1306
- if (e.button === 0 && gameActive && isPointerLocked) shoot();
1307
- }
1308
-
1309
- function onMouseUp(e) {}
1310
-
1311
- function onMouseMove(e) {
1312
- if (!gameActive || !isPointerLocked) return;
1313
- mouseMovement.x = e.movementX || 0;
1314
- mouseMovement.y = e.movementY || 0;
1315
- }
1316
-
1317
- function shoot() {
1318
- if (!canShoot || ammo <= 0 || isReloading) return;
1319
-
1320
- ammo--;
1321
- updateUI();
1322
- canShoot = false;
1323
- setTimeout(() => canShoot = true, shotCooldown);
1324
-
1325
- weaponRecoil = 0.2;
1326
- screenShake.intensity = 0.03;
1327
-
1328
- const bulletGeometry = new THREE.SphereGeometry(0.06, 8, 8);
1329
- const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
1330
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
1331
-
1332
- const bulletPos = new THREE.Vector3();
1333
- camera.getWorldPosition(bulletPos);
1334
- bullet.position.copy(bulletPos);
1335
-
1336
- const direction = new THREE.Vector3(0, 0, -1);
1337
- direction.applyQuaternion(camera.quaternion);
1338
-
1339
- const spread = 0.015;
1340
- direction.x += (Math.random() - 0.5) * spread;
1341
- direction.y += (Math.random() - 0.5) * spread;
1342
- direction.normalize();
1343
-
1344
- scene.add(bullet);
1345
-
1346
- const bulletLight = new THREE.PointLight(0xffff00, 0.5, 3);
1347
- bullet.add(bulletLight);
1348
-
1349
- bullets.push({
1350
- mesh: bullet,
1351
- direction: direction,
1352
- speed: 3,
1353
- distance: 0
1354
- });
1355
-
1356
- createMuzzleFlash();
1357
-
1358
- if (ammo === 0 && reserveAmmo > 0) {
1359
- setTimeout(reload, 300);
1360
- }
1361
- }
1362
-
1363
- function createMuzzleFlash() {
1364
- const flashGeometry = new THREE.SphereGeometry(0.2, 8, 8);
1365
- const flashMaterial = new THREE.MeshBasicMaterial({
1366
- color: 0xffaa00,
1367
- transparent: true,
1368
- opacity: 1
1369
- });
1370
- const flash = new THREE.Mesh(flashGeometry, flashMaterial);
1371
-
1372
- const pos = new THREE.Vector3(0.2, -0.1, -0.8);
1373
- pos.applyQuaternion(camera.quaternion);
1374
- camera.getWorldPosition(flash.position);
1375
- flash.position.add(pos);
1376
-
1377
- scene.add(flash);
1378
-
1379
- const flashLight = new THREE.PointLight(0xffaa00, 2, 8);
1380
- flash.add(flashLight);
1381
-
1382
- let opacity = 1;
1383
- const fadeFlash = () => {
1384
- opacity -= 0.25;
1385
- flashMaterial.opacity = opacity;
1386
- flashLight.intensity = opacity * 2;
1387
- if (opacity > 0) {
1388
- requestAnimationFrame(fadeFlash);
1389
  } else {
1390
- scene.remove(flash);
1391
  }
1392
- };
1393
- fadeFlash();
1394
- }
1395
-
1396
- function reload() {
1397
- if (isReloading || ammo === maxAmmo || reserveAmmo <= 0) return;
1398
-
1399
- isReloading = true;
1400
-
1401
- setTimeout(() => {
1402
- const needed = maxAmmo - ammo;
1403
- const toReload = Math.min(needed, reserveAmmo);
1404
- ammo += toReload;
1405
- reserveAmmo -= toReload;
1406
- isReloading = false;
1407
- updateUI();
1408
- }, 1200);
1409
- }
1410
-
1411
- function createZombie() {
1412
- const zombie = new THREE.Group();
1413
-
1414
- const skinColors = [0x5a7a5a, 0x4a6a4a, 0x6a8a6a, 0x3a5a3a, 0x7a9a7a];
1415
- const skinColor = skinColors[Math.floor(Math.random() * skinColors.length)];
1416
-
1417
- const torsoGeometry = new THREE.BoxGeometry(0.8, 1.2, 0.5);
1418
- const clothesMaterial = new THREE.MeshStandardMaterial({
1419
- color: Math.random() > 0.5 ? 0x333344 : 0x443333,
1420
- roughness: 0.9
1421
- });
1422
- const torso = new THREE.Mesh(torsoGeometry, clothesMaterial);
1423
- torso.position.y = 1.4;
1424
- torso.castShadow = true;
1425
- zombie.add(torso);
1426
-
1427
- const headGeometry = new THREE.SphereGeometry(0.3, 12, 12);
1428
- const skinMaterial = new THREE.MeshStandardMaterial({
1429
- color: skinColor,
1430
- roughness: 0.8
1431
- });
1432
- const head = new THREE.Mesh(headGeometry, skinMaterial);
1433
- head.position.y = 2.2;
1434
- head.castShadow = true;
1435
- zombie.add(head);
1436
-
1437
- const hairGeometry = new THREE.SphereGeometry(0.25, 8, 8, 0, Math.PI * 2, 0, Math.PI / 2);
1438
- const hairMaterial = new THREE.MeshStandardMaterial({ color: 0x222222 });
1439
- const hair = new THREE.Mesh(hairGeometry, hairMaterial);
1440
- hair.position.y = 2.35;
1441
- hair.rotation.x = Math.random() * 0.3;
1442
- zombie.add(hair);
1443
-
1444
- const eyeGeometry = new THREE.SphereGeometry(0.05, 8, 8);
1445
- const eyeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
1446
-
1447
- const eye1 = new THREE.Mesh(eyeGeometry, eyeMaterial);
1448
- eye1.position.set(-0.1, 2.25, 0.25);
1449
- zombie.add(eye1);
1450
-
1451
- const eye2 = new THREE.Mesh(eyeGeometry, eyeMaterial);
1452
- eye2.position.set(0.1, 2.25, 0.25);
1453
- zombie.add(eye2);
1454
-
1455
- const mouthGeometry = new THREE.BoxGeometry(0.15, 0.05, 0.05);
1456
- const mouthMaterial = new THREE.MeshBasicMaterial({ color: 0x440000 });
1457
- const mouth = new THREE.Mesh(mouthGeometry, mouthMaterial);
1458
- mouth.position.set(0, 2.1, 0.28);
1459
- zombie.add(mouth);
1460
-
1461
- const armGeometry = new THREE.BoxGeometry(0.2, 0.8, 0.2);
1462
-
1463
- const armL = new THREE.Mesh(armGeometry, skinMaterial);
1464
- armL.position.set(-0.6, 1.4, 0);
1465
- armL.rotation.x = -0.5 + Math.random() * 0.3;
1466
- armL.rotation.z = 0.2;
1467
- armL.castShadow = true;
1468
- zombie.add(armL);
1469
-
1470
- const armR = new THREE.Mesh(armGeometry, skinMaterial);
1471
- armR.position.set(0.6, 1.4, 0);
1472
- armR.rotation.x = -0.5 + Math.random() * 0.3;
1473
- armR.rotation.z = -0.2;
1474
- armR.castShadow = true;
1475
- zombie.add(armR);
1476
-
1477
- const legGeometry = new THREE.BoxGeometry(0.25, 0.9, 0.25);
1478
- const pantsMaterial = new THREE.MeshStandardMaterial({
1479
- color: 0x222233,
1480
- roughness: 0.9
1481
- });
1482
-
1483
- const legL = new THREE.Mesh(legGeometry, pantsMaterial);
1484
- legL.position.set(-0.2, 0.45, 0);
1485
- legL.castShadow = true;
1486
- zombie.add(legL);
1487
-
1488
- const legR = new THREE.Mesh(legGeometry, pantsMaterial);
1489
- legR.position.set(0.2, 0.45, 0);
1490
- legR.castShadow = true;
1491
- zombie.add(legR);
1492
-
1493
- if (Math.random() > 0.5) {
1494
- const woundGeometry = new THREE.SphereGeometry(0.08, 6, 6);
1495
- const woundMaterial = new THREE.MeshBasicMaterial({ color: 0x660000 });
1496
- for (let i = 0; i < 3; i++) {
1497
- const wound = new THREE.Mesh(woundGeometry, woundMaterial);
1498
- wound.position.set(
1499
- (Math.random() - 0.5) * 0.6,
1500
- 1.2 + Math.random() * 0.8,
1501
- 0.26
1502
- );
1503
- wound.scale.setScalar(0.5 + Math.random() * 0.5);
1504
- zombie.add(wound);
1505
- }
1506
- }
1507
-
1508
- return zombie;
1509
- }
1510
-
1511
- function spawnZombieSwarm(position, count) {
1512
- for (let i = 0; i < count; i++) {
1513
- const zombie = createZombie();
1514
-
1515
- const offsetX = (Math.random() - 0.5) * 15;
1516
- const offsetZ = (Math.random() - 0.5) * 15;
1517
-
1518
- zombie.position.set(
1519
- position.x + offsetX,
1520
- 0,
1521
- position.z + offsetZ
1522
- );
1523
-
1524
- scene.add(zombie);
1525
-
1526
- enemies.push({
1527
- mesh: zombie,
1528
- health: 30 + wave * 5,
1529
- maxHealth: 30 + wave * 5,
1530
- speed: 0.04 + Math.random() * 0.03 + wave * 0.003,
1531
- lastAttack: 0,
1532
- attackCooldown: 600 + Math.random() * 400,
1533
- damage: 5 + wave,
1534
- animTime: Math.random() * Math.PI * 2,
1535
- walkCycle: Math.random() * Math.PI * 2
1536
- });
1537
- }
1538
-
1539
- updateUI();
1540
- }
1541
-
1542
- function spawnRandomSwarm() {
1543
- const validSpawnPoints = spawnPoints.filter(sp => {
1544
- const dist = player.position.distanceTo(sp);
1545
- return dist > 30 && dist < 150;
1546
- });
1547
-
1548
- if (validSpawnPoints.length > 0) {
1549
- const spawnPoint = validSpawnPoints[Math.floor(Math.random() * validSpawnPoints.length)];
1550
- const swarmSize = 3 + Math.floor(Math.random() * 5) + Math.floor(wave / 2);
1551
- spawnZombieSwarm(spawnPoint, swarmSize);
1552
- }
1553
- }
1554
-
1555
- function createBloodParticles(position, count = 15) {
1556
- for (let i = 0; i < count; i++) {
1557
- const geometry = new THREE.SphereGeometry(0.04 + Math.random() * 0.06, 6, 6);
1558
- const material = new THREE.MeshBasicMaterial({
1559
- color: new THREE.Color(0.4 + Math.random() * 0.2, 0, 0),
1560
- transparent: true,
1561
- opacity: 1
1562
- });
1563
- const particle = new THREE.Mesh(geometry, material);
1564
- particle.position.copy(position);
1565
- particle.position.y += 1 + Math.random() * 0.5;
1566
-
1567
- scene.add(particle);
1568
-
1569
- const velocity = new THREE.Vector3(
1570
- (Math.random() - 0.5) * 0.15,
1571
- Math.random() * 0.1,
1572
- (Math.random() - 0.5) * 0.15
1573
- );
1574
-
1575
- particles.push({
1576
- mesh: particle,
1577
- velocity: velocity,
1578
- life: 1,
1579
- decay: 0.03 + Math.random() * 0.02
1580
- });
1581
- }
1582
- }
1583
-
1584
- function createDeathEffect(position) {
1585
- for (let i = 0; i < 20; i++) {
1586
- const geometry = new THREE.SphereGeometry(0.08 + Math.random() * 0.1, 6, 6);
1587
- const material = new THREE.MeshBasicMaterial({
1588
- color: new THREE.Color(0.5 + Math.random() * 0.3, 0.2, 0.1),
1589
- transparent: true,
1590
- opacity: 1
1591
- });
1592
- const particle = new THREE.Mesh(geometry, material);
1593
- particle.position.copy(position);
1594
- particle.position.y += 1;
1595
-
1596
- scene.add(particle);
1597
-
1598
- const velocity = new THREE.Vector3(
1599
- (Math.random() - 0.5) * 0.2,
1600
- Math.random() * 0.15,
1601
- (Math.random() - 0.5) * 0.2
1602
- );
1603
-
1604
- particles.push({
1605
- mesh: particle,
1606
- velocity: velocity,
1607
- life: 1,
1608
- decay: 0.025
1609
- });
1610
  }
 
1611
  }
1612
-
1613
- function startGame() {
1614
- document.getElementById('startScreen').style.display = 'none';
1615
- gameActive = true;
1616
-
1617
- if (!isMobile) {
1618
- renderer.domElement.requestPointerLock();
1619
- }
1620
-
1621
- for (let i = 0; i < 4; i++) {
1622
- setTimeout(() => {
1623
- const angle = (i / 4) * Math.PI * 2;
1624
- const spawnPos = new THREE.Vector3(
1625
- Math.cos(angle) * 40,
1626
- 0,
1627
- Math.sin(angle) * 40
1628
- );
1629
- spawnZombieSwarm(spawnPos, 5 + wave);
1630
- }, i * 800);
1631
- }
1632
- }
1633
-
1634
- function restartGame() {
1635
- document.getElementById('gameOverScreen').style.display = 'none';
1636
-
1637
- playerHealth = 100;
1638
- playerArmor = 50;
1639
- ammo = maxAmmo;
1640
- reserveAmmo = 200;
1641
- score = 0;
1642
- kills = 0;
1643
- wave = 1;
1644
-
1645
- enemies.forEach(e => scene.remove(e.mesh));
1646
- enemies = [];
1647
-
1648
- bullets.forEach(b => scene.remove(b.mesh));
1649
- bullets = [];
1650
-
1651
- particles.forEach(p => scene.remove(p.mesh));
1652
- particles = [];
1653
-
1654
- player.position.set(0, 1.7, 0);
1655
- player.rotation.set(0, 0, 0);
1656
- camera.rotation.set(0, 0, 0);
1657
-
1658
- updateUI();
1659
-
1660
- if (!isMobile) {
1661
- renderer.domElement.requestPointerLock();
1662
- }
1663
-
1664
- gameActive = true;
1665
-
1666
- for (let i = 0; i < 4; i++) {
1667
- setTimeout(() => {
1668
- const angle = (i / 4) * Math.PI * 2;
1669
- const spawnPos = new THREE.Vector3(
1670
- Math.cos(angle) * 40,
1671
- 0,
1672
- Math.sin(angle) * 40
1673
- );
1674
- spawnZombieSwarm(spawnPos, 5);
1675
- }, i * 600);
1676
- }
1677
- }
1678
-
1679
- function gameOver() {
1680
- gameActive = false;
1681
-
1682
- if (document.pointerLockElement) {
1683
- document.exitPointerLock();
1684
  }
1685
-
1686
- document.getElementById('finalScoreValue').textContent = score;
1687
- document.getElementById('finalKillValue').textContent = kills;
1688
- document.getElementById('gameOverScreen').style.display = 'flex';
1689
  }
1690
-
1691
- function updateUI() {
1692
- document.getElementById('healthFill').style.width = `${playerHealth}%`;
1693
- document.getElementById('armorFill').style.width = `${playerArmor}%`;
1694
- document.getElementById('ammoValue').textContent = ammo;
1695
- document.getElementById('ammoMax').textContent = reserveAmmo;
1696
- document.getElementById('scoreValue').textContent = score;
1697
- document.getElementById('killValue').textContent = kills;
1698
- document.getElementById('waveValue').textContent = wave;
1699
- document.getElementById('zombieValue').textContent = enemies.length;
1700
-
1701
- const damageVignette = document.getElementById('damageVignette');
1702
- damageVignette.style.opacity = Math.max(0, (50 - playerHealth) / 50) * 0.8;
1703
- }
1704
-
1705
- function checkCollision(newPos) {
1706
- for (const obj of collisionObjects) {
1707
- if (obj.radius) {
1708
- const dist = new THREE.Vector2(newPos.x - obj.position.x, newPos.z - obj.position.z).length();
1709
- if (dist < obj.radius + 0.5) return true;
1710
- } else if (obj.width && obj.depth) {
1711
- const hw = obj.width / 2 + 0.5;
1712
- const hd = obj.depth / 2 + 0.5;
1713
-
1714
- if (Math.abs(newPos.x - obj.position.x) < hw &&
1715
- Math.abs(newPos.z - obj.position.z) < hd) {
1716
- return true;
1717
  }
1718
  }
1719
  }
1720
-
1721
- if (Math.abs(newPos.x) > 240 || Math.abs(newPos.z) > 240) return true;
1722
-
1723
- return false;
1724
  }
1725
-
1726
- let lastSpawnTime = 0;
1727
- let spawnInterval = 8000;
1728
-
1729
- function animate() {
1730
- requestAnimationFrame(animate);
1731
-
1732
- const delta = clock.getDelta();
1733
- const time = clock.getElapsedTime();
1734
-
1735
- if (gameActive) {
1736
- if (!isMobile && isPointerLocked) {
1737
- player.rotation.y -= mouseMovement.x * 0.002;
1738
- camera.rotation.x -= mouseMovement.y * 0.002;
1739
- camera.rotation.x = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, camera.rotation.x));
1740
- mouseMovement.x = 0;
1741
- mouseMovement.y = 0;
1742
- }
1743
-
1744
- if (isMobile && lookJoystickData.active) {
1745
- player.rotation.y -= lookJoystickData.x * 2;
1746
- camera.rotation.x -= lookJoystickData.y * 2;
1747
- camera.rotation.x = Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, camera.rotation.x));
1748
- lookJoystickData.x *= 0.5;
1749
- lookJoystickData.y *= 0.5;
1750
- }
1751
-
1752
- const speed = 0.18;
1753
- const direction = new THREE.Vector3();
1754
-
1755
- if (moveForward) direction.z -= 1;
1756
- if (moveBackward) direction.z += 1;
1757
- if (moveLeft) direction.x -= 1;
1758
- if (moveRight) direction.x += 1;
1759
-
1760
- if (direction.length() > 0) {
1761
- direction.normalize();
1762
- direction.applyAxisAngle(new THREE.Vector3(0, 1, 0), player.rotation.y);
1763
-
1764
- const newPos = player.position.clone();
1765
- newPos.add(direction.multiplyScalar(speed));
1766
-
1767
- if (!checkCollision(newPos)) {
1768
- player.position.copy(newPos);
1769
- } else {
1770
- const newPosX = player.position.clone();
1771
- newPosX.x += direction.x * speed / direction.length();
1772
- if (!checkCollision(newPosX)) {
1773
- player.position.x = newPosX.x;
1774
- }
1775
-
1776
- const newPosZ = player.position.clone();
1777
- newPosZ.z += direction.z * speed / direction.length();
1778
- if (!checkCollision(newPosZ)) {
1779
- player.position.z = newPosZ.z;
1780
- }
1781
- }
1782
-
1783
- weaponBob += delta * 12;
1784
- }
1785
-
1786
- for (let i = bullets.length - 1; i >= 0; i--) {
1787
- const bullet = bullets[i];
1788
- bullet.mesh.position.add(bullet.direction.clone().multiplyScalar(bullet.speed));
1789
- bullet.distance += bullet.speed;
1790
-
1791
- if (bullet.distance > 150) {
1792
- scene.remove(bullet.mesh);
1793
- bullets.splice(i, 1);
1794
- continue;
1795
- }
1796
-
1797
- for (let j = enemies.length - 1; j >= 0; j--) {
1798
- const enemy = enemies[j];
1799
- const enemyCenter = enemy.mesh.position.clone();
1800
- enemyCenter.y += 1.2;
1801
- const dist = bullet.mesh.position.distanceTo(enemyCenter);
1802
-
1803
- if (dist < 1) {
1804
- const damage = 35 + Math.random() * 20;
1805
- enemy.health -= damage;
1806
-
1807
- scene.remove(bullet.mesh);
1808
- bullets.splice(i, 1);
1809
-
1810
- createBloodParticles(enemy.mesh.position);
1811
-
1812
- if (enemy.health <= 0) {
1813
- createDeathEffect(enemy.mesh.position);
1814
- scene.remove(enemy.mesh);
1815
- enemies.splice(j, 1);
1816
- score += 50 * wave;
1817
- kills++;
1818
- updateUI();
1819
-
1820
- if (Math.random() < 0.25) {
1821
- playerHealth = Math.min(100, playerHealth + 8);
1822
- }
1823
- if (Math.random() < 0.15) {
1824
- playerArmor = Math.min(100, playerArmor + 10);
1825
- }
1826
- if (Math.random() < 0.2) {
1827
- reserveAmmo = Math.min(999, reserveAmmo + 15);
1828
- }
1829
- updateUI();
1830
- }
1831
- break;
1832
- }
1833
- }
1834
- }
1835
-
1836
- enemies.forEach((enemy, index) => {
1837
- enemy.animTime += delta * 3;
1838
- enemy.walkCycle += delta * 8;
1839
-
1840
- const targetPos = player.position.clone();
1841
- const direction = new THREE.Vector3();
1842
- direction.subVectors(targetPos, enemy.mesh.position).normalize();
1843
-
1844
- const newEnemyPos = enemy.mesh.position.clone();
1845
- newEnemyPos.add(direction.multiplyScalar(enemy.speed));
1846
-
1847
- let canMove = true;
1848
- for (const other of enemies) {
1849
- if (other !== enemy) {
1850
- const dist = newEnemyPos.distanceTo(other.mesh.position);
1851
- if (dist < 1) {
1852
- canMove = false;
1853
- break;
1854
- }
1855
- }
1856
- }
1857
-
1858
- if (canMove) {
1859
- enemy.mesh.position.copy(newEnemyPos);
1860
- }
1861
-
1862
- enemy.mesh.lookAt(player.position.x, enemy.mesh.position.y, player.position.z);
1863
-
1864
- const bobAmount = Math.sin(enemy.walkCycle) * 0.05;
1865
- enemy.mesh.position.y = bobAmount;
1866
-
1867
- const leftLeg = enemy.mesh.children[5];
1868
- const rightLeg = enemy.mesh.children[6];
1869
- const leftArm = enemy.mesh.children[3];
1870
- const rightArm = enemy.mesh.children[4];
1871
-
1872
- if (leftLeg && rightLeg) {
1873
- leftLeg.rotation.x = Math.sin(enemy.walkCycle) * 0.4;
1874
- rightLeg.rotation.x = Math.sin(enemy.walkCycle + Math.PI) * 0.4;
1875
- }
1876
-
1877
- if (leftArm && rightArm) {
1878
- leftArm.rotation.x = -0.5 + Math.sin(enemy.walkCycle + Math.PI) * 0.2;
1879
- rightArm.rotation.x = -0.5 + Math.sin(enemy.walkCycle) * 0.2;
1880
- }
1881
-
1882
- const dist = player.position.distanceTo(enemy.mesh.position);
1883
-
1884
- if (dist < 2 && Date.now() - enemy.lastAttack > enemy.attackCooldown) {
1885
- enemy.lastAttack = Date.now();
1886
-
1887
- let damage = enemy.damage;
1888
- if (playerArmor > 0) {
1889
- const absorbed = Math.min(playerArmor, damage * 0.5);
1890
- playerArmor -= absorbed;
1891
- damage -= absorbed;
1892
- }
1893
- playerHealth -= damage;
1894
-
1895
- updateUI();
1896
-
1897
- screenShake.intensity = 0.08;
1898
-
1899
- const hitEffect = document.getElementById('hitEffect');
1900
- hitEffect.style.opacity = '1';
1901
- setTimeout(() => hitEffect.style.opacity = '0', 150);
1902
-
1903
- if (playerHealth <= 0) {
1904
- gameOver();
1905
- }
1906
- }
1907
- });
1908
-
1909
- for (let i = particles.length - 1; i >= 0; i--) {
1910
- const p = particles[i];
1911
- p.velocity.y -= 0.008;
1912
- p.mesh.position.add(p.velocity);
1913
- p.life -= p.decay;
1914
- p.mesh.material.opacity = p.life;
1915
-
1916
- if (p.life <= 0 || p.mesh.position.y < 0) {
1917
- scene.remove(p.mesh);
1918
- particles.splice(i, 1);
1919
- }
1920
- }
1921
-
1922
- const now = Date.now();
1923
- if (now - lastSpawnTime > spawnInterval && enemies.length < 50) {
1924
- spawnRandomSwarm();
1925
- lastSpawnTime = now;
1926
- }
1927
-
1928
- if (kills > 0 && kills % 15 === 0 && kills / 15 >= wave) {
1929
- wave++;
1930
- spawnInterval = Math.max(3000, 8000 - wave * 500);
1931
- updateUI();
1932
-
1933
- for (let i = 0; i < 3; i++) {
1934
- setTimeout(() => spawnRandomSwarm(), i * 500);
1935
- }
1936
- }
1937
-
1938
- if (screenShake.intensity > 0) {
1939
- cameraShakeOffset.set(
1940
- (Math.random() - 0.5) * screenShake.intensity,
1941
- (Math.random() - 0.5) * screenShake.intensity,
1942
- 0
1943
- );
1944
- camera.position.x = cameraShakeOffset.x;
1945
- camera.position.z = cameraShakeOffset.z;
1946
- screenShake.intensity *= screenShake.decay;
1947
- if (screenShake.intensity < 0.001) {
1948
- screenShake.intensity = 0;
1949
- camera.position.x = 0;
1950
- camera.position.z = 0;
1951
- }
1952
- }
1953
-
1954
- streetLights.forEach((light, i) => {
1955
- light.intensity = 0.8 + Math.sin(time * 2 + i * 0.5) * 0.2;
1956
- });
1957
-
1958
- updateMinimap();
1959
- }
1960
-
1961
- composer.render();
1962
  }
1963
-
1964
- function onWindowResize() {
1965
- camera.aspect = window.innerWidth / window.innerHeight;
1966
- camera.updateProjectionMatrix();
1967
- renderer.setSize(window.innerWidth, window.innerHeight);
1968
- composer.setSize(window.innerWidth, window.innerHeight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1969
  }
1970
-
1971
- init();
1972
- </script>
 
 
 
1973
  </body>
1974
  </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="ru">
3
  <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Воксельная 4K-сцена</title>
6
+ <style>
7
+ html, body {
8
+ margin: 0;
9
+ padding: 0;
10
+ overflow: hidden;
11
+ background: #000;
12
+ height: 100%;
13
+ font-family: sans-serif;
14
+ }
15
+ #info {
16
+ position: absolute;
17
+ top: 10px; left: 10px;
18
+ color: #fff;
19
+ background: rgba(0,0,0,0.4);
20
+ padding: 8px 12px;
21
+ border-radius: 4px;
22
+ font-size: 12px;
23
+ z-index: 10;
24
+ }
25
+ #info b {
26
+ color: #ffd66b;
27
+ }
28
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  </head>
30
  <body>
31
+ <div id="info">
32
+ Воксельная 3D-сцена (приблизительный стиль Minecraft в 4K)<br>
33
+ <b>Управление:</b> ЛКМ — вращение, ПКМ/колёсико — панорамирование, колесо — масштаб.<br>
34
+ Разрешение можно сменить в коде: <b>targetWidth / targetHeight</b>.
35
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ <!-- Three.js с CDN -->
38
+ <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.min.js"></script>
39
+ <script src="https://cdn.jsdelivr.net/npm/three@0.161.0/examples/js/controls/OrbitControls.js"></script>
40
+
41
+ <script>
42
+ /* === БАЗОВАЯ СЦЕНА === */
43
+ const scene = new THREE.Scene();
44
+ scene.background = new THREE.Color(0x87ceeb); // небо
45
+
46
+ // Камера (ориентир под 4K, позже подгоним размер канваса)
47
+ const targetWidth = 3840; // 4K ширина
48
+ const targetHeight = 2160; // 4K высота
49
+ const aspect = targetWidth / targetHeight;
50
+
51
+ const camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 2000);
52
+ camera.position.set(60, 50, 80);
53
+ camera.lookAt(0, 0, 0);
54
+
55
+ // Рендерер
56
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
57
+ renderer.setSize(targetWidth, targetHeight, false);
58
+ renderer.setPixelRatio(window.devicePixelRatio > 2 ? 2 : window.devicePixelRatio);
59
+ renderer.outputEncoding = THREE.sRGBEncoding;
60
+ document.body.appendChild(renderer.domElement);
61
+
62
+ // Подгонять канвас под окно, сохраняя соотношение
63
+ function onWindowResize() {
64
+ const w = window.innerWidth;
65
+ const h = window.innerHeight;
66
+ const windowAspect = w / h;
67
+
68
+ if (windowAspect > aspect) {
69
+ // окно шире — вписываем по высоте
70
+ const newWidth = h * aspect;
71
+ renderer.setSize(newWidth, h, false);
72
+ renderer.domElement.style.width = newWidth + "px";
73
+ renderer.domElement.style.height = h + "px";
74
+ } else {
75
+ // окно уже — вписываем по ширине
76
+ const newHeight = w / aspect;
77
+ renderer.setSize(w, newHeight, false);
78
+ renderer.domElement.style.width = w + "px";
79
+ renderer.domElement.style.height = newHeight + "px";
80
  }
81
+ camera.aspect = aspect;
82
+ camera.updateProjectionMatrix();
83
+ }
84
+ window.addEventListener('resize', onWindowResize);
85
+ onWindowResize();
86
+
87
+ // Управление камерой
88
+ const controls = new THREE.OrbitControls(camera, renderer.domElement);
89
+ controls.enableDamping = true;
90
+ controls.dampingFactor = 0.08;
91
+ controls.target.set(32, 10, 32);
92
+
93
+ /* === ОСВЕЩЕНИЕ === */
94
+ const hemiLight = new THREE.HemisphereLight(0xffffff, 0x404040, 0.7);
95
+ scene.add(hemiLight);
96
+
97
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
98
+ dirLight.position.set(100, 120, 80);
99
+ dirLight.castShadow = true;
100
+ dirLight.shadow.mapSize.set(2048, 2048);
101
+ dirLight.shadow.camera.near = 1;
102
+ dirLight.shadow.camera.far = 400;
103
+ dirLight.shadow.camera.left = -150;
104
+ dirLight.shadow.camera.right = 150;
105
+ dirLight.shadow.camera.top = 150;
106
+ dirLight.shadow.camera.bottom = -150;
107
+ scene.add(dirLight);
108
+
109
+ /* === МАТЕРИАЛЫ ВОКСЕЛЕЙ === */
110
+ const textureLoader = new THREE.TextureLoader();
111
+ function makeRepeatingTexture(colorHex, repeat) {
112
+ // генерация простой белой текстуры и окрашивание через color — дешевле
113
+ const mat = new THREE.MeshStandardMaterial({
114
+ color: colorHex,
115
+ roughness: 1.0,
116
+ metalness: 0.0
117
+ });
118
+ return mat;
119
+ }
120
+
121
+ // Примитивный "minecraft-подобный" набор
122
+ const matGrassTop = new THREE.MeshStandardMaterial({ color: 0x76c445, roughness: 0.9 });
123
+ const matDirt = new THREE.MeshStandardMaterial({ color: 0x8b5a2b, roughness: 1.0 });
124
+ const matStone = new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 1.0 });
125
+ const matWater = new THREE.MeshStandardMaterial({ color: 0x2f66cc, roughness: 0.3, metalness: 0.1, transparent: true, opacity: 0.7 });
126
+ const matWood = new THREE.MeshStandardMaterial({ color: 0x8b6b3f, roughness: 1.0 });
127
+ const matLeaves = new THREE.MeshStandardMaterial({ color: 0x2f8a34, roughness: 0.9 });
128
+ const matCloud = new THREE.MeshStandardMaterial({ color: 0xf9f9ff, roughness: 0.9, transparent: true, opacity: 0.9 });
129
+ const matSmoke = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.9, transparent: true, opacity: 0.6 });
130
+ const matSand = new THREE.MeshStandardMaterial({ color: 0xdacb8a, roughness: 0.9 });
131
+
132
+ /* === ВОКСЕЛЬНЫЙ ГЕОМ === */
133
+ const boxGeo = new THREE.BoxGeometry(1, 1, 1);
134
+
135
+ // Функция создания блока
136
+ function addBlock(x, y, z, material, parent) {
137
+ const m = new THREE.Mesh(boxGeo, material);
138
+ m.position.set(x + 0.5, y + 0.5, z + 0.5);
139
+ m.castShadow = true;
140
+ m.receiveShadow = true;
141
+ parent.add(m);
142
+ }
143
+
144
+ /* === ГЕНЕРАЦИЯ ТЕРРАИНА === */
145
+
146
+ // Простая "шумоподобная" функция
147
+ function pseudoNoise(x, z) {
148
+ // детерминированный псевдо-шум (не настоящий перлин, но сгодится)
149
+ const s = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
150
+ return (s - Math.floor(s));
151
+ }
152
+
153
+ // Размер мира
154
+ const worldSizeX = 64;
155
+ const worldSizeZ = 64;
156
+
157
+ const worldGroup = new THREE.Group();
158
+ scene.add(worldGroup);
159
+
160
+ // Поверхность: трава/земля, с "горами", камнем и небольшим озером
161
+ for (let x = 0; x < worldSizeX; x++) {
162
+ for (let z = 0; z < worldSizeZ; z++) {
163
+ const n = pseudoNoise(x * 0.15, z * 0.15);
164
+ let height = Math.floor(6 + n * 12); // от 6 до ~18
165
+
166
+ // Лёгкое сглаживание по второму шуму
167
+ const n2 = pseudoNoise(x * 0.05 + 50, z * 0.05 - 20);
168
+ height += Math.floor(n2 * 4 - 2);
169
+
170
+ if (height < 3) height = 3;
171
+
172
+ // Вода - низкая точка
173
+ const waterLevel = 7;
174
+
175
+ for (let y = 0; y <= height; y++) {
176
+ let mat;
177
+ if (y === height) {
178
+ // Верхний слой: трава/песок у воды/каменные вершины
179
+ if (height > 15) {
180
+ mat = matStone;
181
+ } else if (y < waterLevel + 1 && Math.random() < 0.6) {
182
+ mat = matSand;
183
+ } else {
184
+ mat = matGrassTop;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  } else {
187
+ // Под землёй
188
+ if (y < height - 3) {
189
+ mat = matStone;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  } else {
191
+ mat = matDirt;
192
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  }
194
+ addBlock(x, y, z, mat, worldGroup);
195
  }
196
+
197
+ // Вода заполняет низины
198
+ if (height < waterLevel) {
199
+ for (let y = height + 1; y <= waterLevel; y++) {
200
+ addBlock(x, y, z, matWater, worldGroup);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  }
 
 
 
 
202
  }
203
+ }
204
+ }
205
+
206
+ /* === ДЕРЕВЬЯ === */
207
+ function addTree(baseX, baseZ, baseY) {
208
+ const trunkHeight = 4 + Math.floor(Math.random() * 2);
209
+ // ствол
210
+ for (let y = 0; y < trunkHeight; y++) {
211
+ addBlock(baseX, baseY + y, baseZ, matWood, worldGroup);
212
+ }
213
+ const topY = baseY + trunkHeight;
214
+
215
+ // крона (простая сфера из кубиков)
216
+ const radius = 2 + Math.floor(Math.random() * 1.5);
217
+ for (let x = -radius; x <= radius; x++) {
218
+ for (let y = -radius; y <= radius; y++) {
219
+ for (let z = -radius; z <= radius; z++) {
220
+ if (x*x + y*y + z*z <= radius * radius + 1) {
221
+ if (!(x === 0 && y <= 0 && z === 0)) {
222
+ addBlock(baseX + x, topY + y, baseZ + z, matLeaves, worldGroup);
 
 
 
 
 
 
 
223
  }
224
  }
225
  }
 
 
 
 
226
  }
227
+ }
228
+ }
229
+
230
+ // Расставим деревья по миру (на траве и не в воде)
231
+ for (let i = 0; i < 45; i++) {
232
+ const x = Math.floor(Math.random() * worldSizeX);
233
+ const z = Math.floor(Math.random() * worldSizeZ);
234
+
235
+ // Найдём наибольшую высоту в этой колонке
236
+ let topY = -1;
237
+ worldGroup.children.forEach(c => {
238
+ const px = Math.floor(c.position.x);
239
+ const pz = Math.floor(c.position.z);
240
+ if (px === x && pz === z) {
241
+ const py = Math.floor(c.position.y);
242
+ if (py > topY) topY = py;
243
+ }
244
+ });
245
+
246
+ if (topY < 2) continue;
247
+
248
+ // Проверка: верхний блок — трава
249
+ let topMat = null;
250
+ worldGroup.children.forEach(c => {
251
+ const px = Math.floor(c.position.x);
252
+ const py = Math.floor(c.position.y);
253
+ const pz = Math.floor(c.position.z);
254
+ if (px === x && pz === z && py === topY) topMat = c.material;
255
+ });
256
+
257
+ if (topMat === matGrassTop && Math.random() < 0.5) {
258
+ addTree(x, z, topY + 1);
259
+ }
260
+ }
261
+
262
+ /* === ОБЛАКА === */
263
+ const cloudsGroup = new THREE.Group();
264
+ scene.add(cloudsGroup);
265
+
266
+ function makeCloud(cx, cy, cz, size = 6) {
267
+ const cloudCluster = new THREE.Group();
268
+ const blocks = 12 + Math.floor(Math.random() * 10);
269
+
270
+ for (let i = 0; i < blocks; i++) {
271
+ const ox = (Math.random() - 0.5) * size;
272
+ const oy = (Math.random() - 0.5) * (size * 0.4);
273
+ const oz = (Math.random() - 0.5) * size;
274
+ const cube = new THREE.Mesh(boxGeo, matCloud.clone());
275
+ cube.scale.setScalar(1.6 + Math.random() * 0.7);
276
+ cube.position.set(cx + ox, cy + oy, cz + oz);
277
+ cube.castShadow = false;
278
+ cube.receiveShadow = false;
279
+ cloudCluster.add(cube);
280
+ }
281
+ cloudsGroup.add(cloudCluster);
282
+ }
283
+
284
+ // Несколько кучек ��блаков
285
+ for (let i = 0; i < 12; i++) {
286
+ const cx = (Math.random() - 0.5) * 200;
287
+ const cz = (Math.random() - 0.5) * 200;
288
+ const cy = 60 + Math.random() * 20;
289
+ makeCloud(cx, cy, cz, 12 + Math.random() * 6);
290
+ }
291
+
292
+ /* === ДЫМ ИЗ ТРУБЫ / КОСТРА === */
293
+ const smokeGroup = new THREE.Group();
294
+ scene.add(smokeGroup);
295
+
296
+ // "Костёр" в центре
297
+ const fireX = worldSizeX / 2;
298
+ const fireZ = worldSizeZ / 2;
299
+
300
+ // найдём высоту в центре
301
+ let centerHeight = 0;
302
+ worldGroup.children.forEach(c => {
303
+ const px = Math.floor(c.position.x);
304
+ const pz = Math.floor(c.position.z);
305
+ const py = Math.floor(c.position.y);
306
+ if (px === fireX && pz === fireZ && py > centerHeight) {
307
+ centerHeight = py;
308
+ }
309
+ });
310
+
311
+ // подложим "брёвна"
312
+ for (let ix = -1; ix <= 1; ix++) {
313
+ for (let iz = -1; iz <= 1; iz++) {
314
+ if (Math.abs(ix) !== Math.abs(iz)) {
315
+ addBlock(fireX + ix, centerHeight + 1, fireZ + iz, matWood, worldGroup);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
+ }
318
+ }
319
+
320
+ // параметры дыма
321
+ const smokeParticles = [];
322
+ const maxSmoke = 120;
323
+
324
+ function spawnSmokeParticle() {
325
+ if (smokeParticles.length >= maxSmoke) return;
326
+ const s = new THREE.Mesh(boxGeo, matSmoke.clone());
327
+ s.scale.set(0.8, 0.8, 0.8);
328
+ s.position.set(
329
+ fireX + 0.5 + (Math.random() - 0.5) * 0.4,
330
+ centerHeight + 2 + Math.random() * 0.4,
331
+ fireZ + 0.5 + (Math.random() - 0.5) * 0.4
332
+ );
333
+ s.material.opacity = 0.0;
334
+ s.userData = {
335
+ life: 0,
336
+ maxLife: 5 + Math.random() * 4,
337
+ riseSpeed: 1.2 + Math.random() * 0.8,
338
+ driftX: (Math.random() - 0.5) * 0.25,
339
+ driftZ: (Math.random() - 0.5) * 0.25
340
+ };
341
+ smokeGroup.add(s);
342
+ smokeParticles.push(s);
343
+ }
344
+
345
+ /* === АНИМАЦИЯ === */
346
+ let lastTime = performance.now();
347
+
348
+ function animate() {
349
+ requestAnimationFrame(animate);
350
+
351
+ const now = performance.now();
352
+ const dt = (now - lastTime) / 1000;
353
+ lastTime = now;
354
+
355
+ controls.update();
356
+
357
+ // Движение облаков
358
+ cloudsGroup.children.forEach(cluster => {
359
+ cluster.position.x += 0.2 * dt * 20; // плавный сдвиг
360
+ if (cluster.position.x > 150) cluster.position.x = -150;
361
+ });
362
+
363
+ // Периодический "выброс" дыма
364
+ if (Math.random() < 0.2) {
365
+ spawnSmokeParticle();
366
+ }
367
+
368
+ // Обновление частиц дыма
369
+ for (let i = smokeParticles.length - 1; i >= 0; i--) {
370
+ const p = smokeParticles[i];
371
+ const data = p.userData;
372
+ data.life += dt;
373
+
374
+ const t = data.life / data.maxLife;
375
+ p.position.y += data.riseSpeed * dt;
376
+ p.position.x += data.driftX * dt;
377
+ p.position.z += data.driftZ * dt;
378
+
379
+ // рост и рассеивание
380
+ const scale = 0.8 + t * 3;
381
+ p.scale.set(scale, scale, scale);
382
+
383
+ // появление и исчезновение
384
+ if (t < 0.2) {
385
+ p.material.opacity = t * 3;
386
+ } else if (t > 0.7) {
387
+ p.material.opacity = Math.max(0, 1 - (t - 0.7) / 0.3);
388
+ }
389
+
390
+ if (data.life >= data.maxLife || p.material.opacity <= 0) {
391
+ smokeGroup.remove(p);
392
+ smokeParticles.splice(i, 1);
393
  }
394
+ }
395
+
396
+ renderer.render(scene, camera);
397
+ }
398
+ animate();
399
+ </script>
400
  </body>
401
  </html>