gunship999 commited on
Commit
070a756
β€’
1 Parent(s): 5326e18

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +162 -349
game.js CHANGED
@@ -5,9 +5,9 @@ import { PointerLockControls } from 'three/addons/controls/PointerLockControls.j
5
  // κ²Œμž„ μƒμˆ˜
6
  const GAME_DURATION = 180;
7
  const MAP_SIZE = 2000;
8
- const HELICOPTER_HEIGHT = 100;
9
- const ENEMY_GROUND_HEIGHT = 5;
10
- const ENEMY_SCALE = 3;
11
  const MAX_HEALTH = 1000;
12
  const ENEMY_MOVE_SPEED = 0.1;
13
  const ENEMY_MODELS = [
@@ -33,10 +33,66 @@ let currentStage = 1;
33
  let isGameOver = false;
34
  let lastTime = performance.now();
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  // μ‚¬μš΄λ“œ μ‹œμŠ€ν…œ
37
  class SoundSystem {
38
  constructor() {
39
  this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
 
40
  this.buffers = new Map();
41
  this.pools = new Map();
42
  }
@@ -66,6 +122,10 @@ class SoundSystem {
66
  this.pools.set(name, pool);
67
  }
68
 
 
 
 
 
69
  play(name) {
70
  const pool = this.pools.get(name);
71
  const buffer = this.buffers.get(name);
@@ -105,7 +165,6 @@ const moveState = {
105
 
106
  async function initSounds() {
107
  try {
108
- await soundSystem.loadSound('gunshot', 'gun.wav');
109
  await soundSystem.loadSound('explosion', 'explosion.wav');
110
  console.log('All sounds loaded successfully');
111
  } catch (error) {
@@ -169,72 +228,21 @@ async function init() {
169
  }
170
  }
171
 
172
-
173
- async function testModelLoading() {
174
- const loader = new GLTFLoader();
175
- const testPath = ENEMY_MODELS[0];
 
176
 
177
- try {
178
- const gltf = await new Promise((resolve, reject) => {
179
- loader.load(testPath, resolve, undefined, reject);
180
- });
181
- console.log('Test model loaded successfully:', gltf);
182
- } catch (error) {
183
- console.error('Test model loading failed:', error);
184
- console.error('Test path was:', testPath);
185
- }
186
- }
187
-
188
- function createTerrain() {
189
- return new Promise((resolve) => {
190
- const geometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 200, 200);
191
- const material = new THREE.MeshStandardMaterial({
192
- color: 0xD2B48C,
193
- roughness: 0.8,
194
- metalness: 0.2
195
- });
196
-
197
- const vertices = geometry.attributes.position.array;
198
- for (let i = 0; i < vertices.length; i += 3) {
199
- vertices[i + 2] = Math.sin(vertices[i] * 0.01) * Math.cos(vertices[i + 1] * 0.01) * 20;
200
  }
201
-
202
- geometry.attributes.position.needsUpdate = true;
203
- geometry.computeVertexNormals();
204
-
205
- const terrain = new THREE.Mesh(geometry, material);
206
- terrain.rotation.x = -Math.PI / 2;
207
- terrain.receiveShadow = true;
208
- scene.add(terrain);
209
-
210
- addObstacles();
211
- resolve();
212
- });
213
- }
214
-
215
- function addObstacles() {
216
- const rockGeometry = new THREE.DodecahedronGeometry(10);
217
- const rockMaterial = new THREE.MeshStandardMaterial({
218
- color: 0x8B4513,
219
- roughness: 0.9
220
  });
221
-
222
- for (let i = 0; i < 100; i++) {
223
- const rock = new THREE.Mesh(rockGeometry, rockMaterial);
224
- rock.position.set(
225
- (Math.random() - 0.5) * MAP_SIZE * 0.9,
226
- Math.random() * 10,
227
- (Math.random() - 0.5) * MAP_SIZE * 0.9
228
- );
229
- rock.rotation.set(
230
- Math.random() * Math.PI,
231
- Math.random() * Math.PI,
232
- Math.random() * Math.PI
233
- );
234
- rock.castShadow = true;
235
- rock.receiveShadow = true;
236
- scene.add(rock);
237
- }
238
  }
239
 
240
  async function loadEnemies() {
@@ -259,7 +267,9 @@ async function loadEnemies() {
259
  const tempMaterial = new THREE.MeshPhongMaterial({
260
  color: 0xff0000,
261
  transparent: true,
262
- opacity: 0.8
 
 
263
  });
264
  const tempEnemy = new THREE.Mesh(tempGeometry, tempMaterial);
265
  tempEnemy.position.copy(position);
@@ -267,6 +277,11 @@ async function loadEnemies() {
267
  tempEnemy.receiveShadow = true;
268
  scene.add(tempEnemy);
269
 
 
 
 
 
 
270
  const enemy = {
271
  model: tempEnemy,
272
  health: 100,
@@ -293,9 +308,19 @@ async function loadEnemies() {
293
  node.receiveShadow = true;
294
  node.material.metalness = 0.2;
295
  node.material.roughness = 0.8;
 
 
296
  }
297
  });
298
 
 
 
 
 
 
 
 
 
299
  scene.remove(tempEnemy);
300
  scene.add(model);
301
  enemy.model = model;
@@ -321,6 +346,58 @@ async function loadEnemies() {
321
  }
322
  }
323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  function createExplosion(position) {
325
  const particleCount = 30;
326
  const particles = [];
@@ -331,7 +408,9 @@ function createExplosion(position) {
331
  new THREE.MeshBasicMaterial({
332
  color: 0xff4400,
333
  transparent: true,
334
- opacity: 1
 
 
335
  })
336
  );
337
 
@@ -344,6 +423,12 @@ function createExplosion(position) {
344
 
345
  particles.push(particle);
346
  scene.add(particle);
 
 
 
 
 
 
347
  }
348
 
349
  function animateExplosion() {
@@ -370,7 +455,6 @@ function createExplosion(position) {
370
  function onClick() {
371
  if (!controls.isLocked) {
372
  controls.lock();
373
- // μ‚¬μš΄λ“œ μ»¨ν…μŠ€νŠΈ μ‹œμž‘ (λ§Žμ€ λΈŒλΌμš°μ €μ—μ„œ ν•„μš”)
374
  soundSystem.audioContext.resume();
375
  } else if (ammo > 0) {
376
  shoot();
@@ -411,282 +495,11 @@ function shoot() {
411
  const bullet = createBullet();
412
  bullets.push(bullet);
413
 
414
- soundSystem.play('gunshot');
415
- }
416
-
417
- function createBullet() {
418
- const bulletGeometry = new THREE.SphereGeometry(0.5);
419
- const bulletMaterial = new THREE.MeshBasicMaterial({
420
- color: 0xffff00,
421
- emissive: 0xffff00,
422
- emissiveIntensity: 1
423
- });
424
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
425
-
426
- bullet.position.copy(camera.position);
427
- const direction = new THREE.Vector3();
428
- camera.getWorldDirection(direction);
429
- bullet.velocity = direction.multiplyScalar(3);
430
-
431
- scene.add(bullet);
432
- return bullet;
433
- }
434
-
435
- function createEnemyBullet(enemy) {
436
- const bulletGeometry = new THREE.SphereGeometry(0.5);
437
- const bulletMaterial = new THREE.MeshBasicMaterial({
438
- color: 0xff0000,
439
- emissive: 0xff0000,
440
- emissiveIntensity: 1
441
- });
442
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
443
-
444
- bullet.position.copy(enemy.model.position);
445
- bullet.position.y += 2; // 총ꡬ 높이 μ‘°μ •
446
-
447
- const direction = new THREE.Vector3();
448
- direction.subVectors(camera.position, enemy.model.position).normalize();
449
- bullet.velocity = direction.multiplyScalar(ENEMY_CONFIG.BULLET_SPEED);
450
-
451
- scene.add(bullet);
452
- return bullet;
453
- }
454
-
455
- function reload() {
456
- ammo = 30;
457
- updateAmmoDisplay();
458
- }
459
-
460
- function updateAmmoDisplay() {
461
- document.getElementById('ammo').textContent = `Ammo: ${ammo}/30`;
462
- }
463
-
464
- function updateHealthBar() {
465
- const healthElement = document.getElementById('health');
466
- const healthPercentage = (playerHealth / MAX_HEALTH) * 100;
467
- healthElement.style.width = `${healthPercentage}%`;
468
- }
469
-
470
- function updateHelicopterHUD() {
471
- const altitude = Math.round(camera.position.y);
472
- document.querySelector('#altitude-indicator span').textContent = altitude;
473
-
474
- const speed = Math.round(
475
- Math.sqrt(
476
- moveState.forward * moveState.forward +
477
- moveState.right * moveState.right
478
- ) * 100
479
- );
480
- document.querySelector('#speed-indicator span').textContent = speed;
481
-
482
- const heading = Math.round(
483
- (camera.rotation.y * (180 / Math.PI) + 360) % 360
484
- );
485
- document.querySelector('#compass span').textContent = heading;
486
-
487
- updateRadar();
488
- }
489
-
490
- function updateRadar() {
491
- const radarTargets = document.querySelector('.radar-targets');
492
- radarTargets.innerHTML = '';
493
-
494
- enemies.forEach(enemy => {
495
- if (!enemy || !enemy.model) return;
496
-
497
- const relativePos = enemy.model.position.clone().sub(camera.position);
498
- const distance = relativePos.length();
499
-
500
- if (distance < 500) {
501
- // ν”Œλ ˆμ΄μ–΄μ˜ λ°©ν–₯을 κΈ°μ€€μœΌλ‘œ μƒλŒ€μ μΈ 각도 계산
502
- const playerAngle = camera.rotation.y;
503
- const enemyAngle = Math.atan2(relativePos.x, relativePos.z);
504
- const relativeAngle = enemyAngle - playerAngle;
505
-
506
- const normalizedDistance = distance / 500;
507
-
508
- const dot = document.createElement('div');
509
- dot.className = 'radar-dot';
510
- dot.style.left = `${50 + Math.sin(relativeAngle) * normalizedDistance * 45}%`;
511
- dot.style.top = `${50 + Math.cos(relativeAngle) * normalizedDistance * 45}%`;
512
- radarTargets.appendChild(dot);
513
- }
514
- });
515
- }
516
-
517
- function updateMovement() {
518
- if (controls.isLocked) {
519
- const speed = 2.0;
520
- if (moveState.forward) controls.moveForward(speed);
521
- if (moveState.backward) controls.moveForward(-speed);
522
- if (moveState.left) controls.moveRight(-speed);
523
- if (moveState.right) controls.moveRight(speed);
524
-
525
- // μ΅œμ†Œ 고도 μœ μ§€
526
- if (camera.position.y < HELICOPTER_HEIGHT) {
527
- camera.position.y = HELICOPTER_HEIGHT;
528
- }
529
- }
530
- }
531
-
532
- function updateBullets() {
533
- for (let i = bullets.length - 1; i >= 0; i--) {
534
- if (!bullets[i]) continue;
535
-
536
- bullets[i].position.add(bullets[i].velocity);
537
-
538
- enemies.forEach(enemy => {
539
- if (!enemy || !enemy.model) return;
540
-
541
- if (bullets[i] && bullets[i].position.distanceTo(enemy.model.position) < 5) {
542
- scene.remove(bullets[i]);
543
- bullets.splice(i, 1);
544
- enemy.health -= 25;
545
-
546
- // 피격 효과
547
- createExplosion(enemy.model.position.clone());
548
-
549
- if (enemy.health <= 0) {
550
- // 파괴 효과
551
- createExplosion(enemy.model.position.clone());
552
- createExplosion(enemy.model.position.clone().add(new THREE.Vector3(2, 0, 2)));
553
- createExplosion(enemy.model.position.clone().add(new THREE.Vector3(-2, 0, -2)));
554
- scene.remove(enemy.model);
555
- enemies = enemies.filter(e => e !== enemy);
556
- console.log('Enemy destroyed, remaining enemies:', enemies.length);
557
- }
558
- }
559
- });
560
-
561
- if (bullets[i] && bullets[i].position.distanceTo(camera.position) > 1000) {
562
- scene.remove(bullets[i]);
563
- bullets.splice(i, 1);
564
- }
565
- }
566
- }
567
-
568
- function updateEnemyBullets() {
569
- for (let i = enemyBullets.length - 1; i >= 0; i--) {
570
- if (!enemyBullets[i]) continue;
571
-
572
- enemyBullets[i].position.add(enemyBullets[i].velocity);
573
-
574
- if (enemyBullets[i].position.distanceTo(camera.position) < 3) {
575
- playerHealth -= 10;
576
- updateHealthBar();
577
- scene.remove(enemyBullets[i]);
578
- enemyBullets.splice(i, 1);
579
-
580
- if (playerHealth <= 0) {
581
- gameOver(false);
582
- }
583
- continue;
584
- }
585
-
586
- if (enemyBullets[i].position.distanceTo(camera.position) > 1000) {
587
- scene.remove(enemyBullets[i]);
588
- enemyBullets.splice(i, 1);
589
- }
590
- }
591
- }
592
-
593
- function updateEnemies() {
594
- const currentTime = Date.now();
595
-
596
- enemies.forEach(enemy => {
597
- if (!enemy || !enemy.model) {
598
- console.warn('Invalid enemy detected');
599
- return;
600
- }
601
-
602
- // 적의 ν˜„μž¬ μœ„μΉ˜μ—μ„œ ν”Œλ ˆμ΄μ–΄ λ°©ν–₯으둜 ν–₯ν•˜λŠ” 벑터 계산
603
- const direction = new THREE.Vector3();
604
- direction.subVectors(camera.position, enemy.model.position);
605
- direction.y = 0; // yμΆ• 이동 μ œν•œ
606
- direction.normalize();
607
-
608
- // μƒˆλ‘œμš΄ μœ„μΉ˜ 계산
609
- const newPosition = enemy.model.position.clone()
610
- .add(direction.multiplyScalar(enemy.speed));
611
- newPosition.y = ENEMY_GROUND_HEIGHT;
612
- enemy.model.position.copy(newPosition);
613
-
614
- // 적이 ν”Œλ ˆμ΄μ–΄λ₯Ό 바라보도둝 μ„€μ •
615
- enemy.model.lookAt(new THREE.Vector3(
616
- camera.position.x,
617
- enemy.model.position.y,
618
- camera.position.z
619
- ));
620
-
621
- // 곡격 둜직
622
- const distanceToPlayer = enemy.model.position.distanceTo(camera.position);
623
- if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE &&
624
- currentTime - enemy.lastAttackTime > ENEMY_CONFIG.ATTACK_INTERVAL) {
625
-
626
- console.log('Enemy attacking!');
627
- enemyBullets.push(createEnemyBullet(enemy));
628
- enemy.lastAttackTime = currentTime;
629
- }
630
- });
631
- }
632
-
633
- function checkGameStatus() {
634
- if (enemies.length === 0 && currentStage < 5) {
635
- currentStage++;
636
- document.getElementById('stage').style.display = 'block';
637
- document.getElementById('stage').textContent = `Stage ${currentStage}`;
638
- setTimeout(() => {
639
- document.getElementById('stage').style.display = 'none';
640
- loadEnemies();
641
- }, 2000);
642
- }
643
- }
644
-
645
- function gameOver(won) {
646
- isGameOver = true;
647
- controls.unlock();
648
- soundSystem.audioContext.suspend();
649
- alert(won ? 'Mission Complete!' : 'Game Over!');
650
- location.reload();
651
- }
652
-
653
- function gameLoop() {
654
- requestAnimationFrame(gameLoop);
655
-
656
- const time = performance.now();
657
- const delta = (time - lastTime) / 1000;
658
- lastTime = time;
659
-
660
- if (controls.isLocked && !isGameOver) {
661
- updateMovement();
662
- updateBullets();
663
- updateEnemies();
664
- updateEnemyBullets();
665
- updateHelicopterHUD();
666
- checkGameStatus();
667
- }
668
-
669
- renderer.render(scene, camera);
670
- }
671
-
672
- // κ²Œμž„ μ‹œμž‘
673
- window.addEventListener('load', async () => {
674
- try {
675
- await init();
676
- console.log('Game started');
677
- console.log('Active enemies:', enemies.length);
678
- gameLoop();
679
- } catch (error) {
680
- console.error('Game initialization error:', error);
681
- alert('Error loading game. Please check console for details.');
682
- }
683
- });
684
-
685
- // 디버깅을 μœ„ν•œ μ „μ—­ μ ‘κ·Ό
686
- window.debugGame = {
687
- scene,
688
- camera,
689
- enemies,
690
- soundSystem,
691
- reloadEnemies: loadEnemies
692
- };
 
5
  // κ²Œμž„ μƒμˆ˜
6
  const GAME_DURATION = 180;
7
  const MAP_SIZE = 2000;
8
+ const HELICOPTER_HEIGHT = 30; // 30m둜 μ‘°μ •
9
+ const ENEMY_GROUND_HEIGHT = 0;
10
+ const ENEMY_SCALE = 10; // 크기 λŒ€ν­ 증가
11
  const MAX_HEALTH = 1000;
12
  const ENEMY_MOVE_SPEED = 0.1;
13
  const ENEMY_MODELS = [
 
33
  let isGameOver = false;
34
  let lastTime = performance.now();
35
 
36
+ // μ΄μ†Œλ¦¬ 생성기 클래슀
37
+ class GunSoundGenerator {
38
+ constructor(audioContext) {
39
+ this.audioContext = audioContext;
40
+ }
41
+
42
+ createGunshot() {
43
+ // 메인 μ˜€μ‹€λ ˆμ΄ν„° (μ΄μ†Œλ¦¬μ˜ μ£Όμš” 주파수)
44
+ const mainOsc = this.audioContext.createOscillator();
45
+ mainOsc.type = 'square';
46
+ mainOsc.frequency.setValueAtTime(100, this.audioContext.currentTime);
47
+ mainOsc.frequency.exponentialRampToValueAtTime(1, this.audioContext.currentTime + 0.1);
48
+
49
+ // λ…Έμ΄μ¦ˆ 생성
50
+ const noiseBuffer = this.createNoiseBuffer();
51
+ const noise = this.audioContext.createBufferSource();
52
+ noise.buffer = noiseBuffer;
53
+
54
+ // 게인 λ…Έλ“œλ“€
55
+ const mainGain = this.audioContext.createGain();
56
+ const noiseGain = this.audioContext.createGain();
57
+
58
+ // λ³Όλ₯¨ μ—”λ²¨λ‘œν”„ μ„€μ •
59
+ mainGain.gain.setValueAtTime(1, this.audioContext.currentTime);
60
+ mainGain.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.1);
61
+
62
+ noiseGain.gain.setValueAtTime(1, this.audioContext.currentTime);
63
+ noiseGain.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.05);
64
+
65
+ // μ—°κ²°
66
+ mainOsc.connect(mainGain);
67
+ noise.connect(noiseGain);
68
+ mainGain.connect(this.audioContext.destination);
69
+ noiseGain.connect(this.audioContext.destination);
70
+
71
+ // μž¬μƒ
72
+ mainOsc.start();
73
+ noise.start();
74
+ mainOsc.stop(this.audioContext.currentTime + 0.1);
75
+ noise.stop(this.audioContext.currentTime + 0.1);
76
+ }
77
+
78
+ createNoiseBuffer() {
79
+ const bufferSize = this.audioContext.sampleRate * 0.1;
80
+ const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
81
+ const output = buffer.getChannelData(0);
82
+
83
+ for (let i = 0; i < bufferSize; i++) {
84
+ output[i] = Math.random() * 2 - 1;
85
+ }
86
+
87
+ return buffer;
88
+ }
89
+ }
90
+
91
  // μ‚¬μš΄λ“œ μ‹œμŠ€ν…œ
92
  class SoundSystem {
93
  constructor() {
94
  this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
95
+ this.gunSoundGenerator = new GunSoundGenerator(this.audioContext);
96
  this.buffers = new Map();
97
  this.pools = new Map();
98
  }
 
122
  this.pools.set(name, pool);
123
  }
124
 
125
+ playGunshot() {
126
+ this.gunSoundGenerator.createGunshot();
127
+ }
128
+
129
  play(name) {
130
  const pool = this.pools.get(name);
131
  const buffer = this.buffers.get(name);
 
165
 
166
  async function initSounds() {
167
  try {
 
168
  await soundSystem.loadSound('explosion', 'explosion.wav');
169
  console.log('All sounds loaded successfully');
170
  } catch (error) {
 
228
  }
229
  }
230
 
231
+ function createEnemyOutline(model) {
232
+ const outlineMaterial = new THREE.MeshBasicMaterial({
233
+ color: 0xff0000,
234
+ side: THREE.BackSide
235
+ });
236
 
237
+ const outlineMesh = model.clone();
238
+ outlineMesh.traverse((node) => {
239
+ if (node.isMesh) {
240
+ node.material = outlineMaterial;
241
+ node.scale.multiplyScalar(1.05);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  });
244
+
245
+ model.add(outlineMesh);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  }
247
 
248
  async function loadEnemies() {
 
267
  const tempMaterial = new THREE.MeshPhongMaterial({
268
  color: 0xff0000,
269
  transparent: true,
270
+ opacity: 0.8,
271
+ emissive: 0xff0000,
272
+ emissiveIntensity: 0.5
273
  });
274
  const tempEnemy = new THREE.Mesh(tempGeometry, tempMaterial);
275
  tempEnemy.position.copy(position);
 
277
  tempEnemy.receiveShadow = true;
278
  scene.add(tempEnemy);
279
 
280
+ // μž„μ‹œ 적에 λΉ› μΆ”κ°€
281
+ const tempLight = new THREE.PointLight(0xff0000, 1, 20);
282
+ tempLight.position.set(0, 5, 0);
283
+ tempEnemy.add(tempLight);
284
+
285
  const enemy = {
286
  model: tempEnemy,
287
  health: 100,
 
308
  node.receiveShadow = true;
309
  node.material.metalness = 0.2;
310
  node.material.roughness = 0.8;
311
+ node.material.emissive = new THREE.Color(0x444444);
312
+ node.material.emissiveIntensity = 0.5;
313
  }
314
  });
315
 
316
+ // 아웃라인 효과 μΆ”κ°€
317
+ createEnemyOutline(model);
318
+
319
+ // 적 주변에 λΉ› μΆ”κ°€
320
+ const enemyLight = new THREE.PointLight(0xff0000, 1, 20);
321
+ enemyLight.position.set(0, 5, 0);
322
+ model.add(enemyLight);
323
+
324
  scene.remove(tempEnemy);
325
  scene.add(model);
326
  enemy.model = model;
 
346
  }
347
  }
348
 
349
+ function createTerrain() {
350
+ return new Promise((resolve) => {
351
+ const geometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 200, 200);
352
+ const material = new THREE.MeshStandardMaterial({
353
+ color: 0xD2B48C,
354
+ roughness: 0.8,
355
+ metalness: 0.2
356
+ });
357
+
358
+ const vertices = geometry.attributes.position.array;
359
+ for (let i = 0; i < vertices.length; i += 3) {
360
+ vertices[i + 2] = Math.sin(vertices[i] * 0.01) * Math.cos(vertices[i + 1] * 0.01) * 20;
361
+ }
362
+
363
+ geometry.attributes.position.needsUpdate = true;
364
+ geometry.computeVertexNormals();
365
+
366
+ const terrain = new THREE.Mesh(geometry, material);
367
+ terrain.rotation.x = -Math.PI / 2;
368
+ terrain.receiveShadow = true;
369
+ scene.add(terrain);
370
+
371
+ addObstacles();
372
+ resolve();
373
+ });
374
+ }
375
+
376
+ function addObstacles() {
377
+ const rockGeometry = new THREE.DodecahedronGeometry(10);
378
+ const rockMaterial = new THREE.MeshStandardMaterial({
379
+ color: 0x8B4513,
380
+ roughness: 0.9
381
+ });
382
+
383
+ for (let i = 0; i < 100; i++) {
384
+ const rock = new THREE.Mesh(rockGeometry, rockMaterial);
385
+ rock.position.set(
386
+ (Math.random() - 0.5) * MAP_SIZE * 0.9,
387
+ Math.random() * 10,
388
+ (Math.random() - 0.5) * MAP_SIZE * 0.9
389
+ );
390
+ rock.rotation.set(
391
+ Math.random() * Math.PI,
392
+ Math.random() * Math.PI,
393
+ Math.random() * Math.PI
394
+ );
395
+ rock.castShadow = true;
396
+ rock.receiveShadow = true;
397
+ scene.add(rock);
398
+ }
399
+ }
400
+
401
  function createExplosion(position) {
402
  const particleCount = 30;
403
  const particles = [];
 
408
  new THREE.MeshBasicMaterial({
409
  color: 0xff4400,
410
  transparent: true,
411
+ opacity: 1,
412
+ emissive: 0xff4400,
413
+ emissiveIntensity: 1
414
  })
415
  );
416
 
 
423
 
424
  particles.push(particle);
425
  scene.add(particle);
426
+
427
+ // 폭발 지점에 μΌμ‹œμ μΈ λΉ› μΆ”κ°€
428
+ const explosionLight = new THREE.PointLight(0xff4400, 2, 20);
429
+ explosionLight.position.copy(position);
430
+ scene.add(explosionLight);
431
+ setTimeout(() => scene.remove(explosionLight), 100);
432
  }
433
 
434
  function animateExplosion() {
 
455
  function onClick() {
456
  if (!controls.isLocked) {
457
  controls.lock();
 
458
  soundSystem.audioContext.resume();
459
  } else if (ammo > 0) {
460
  shoot();
 
495
  const bullet = createBullet();
496
  bullets.push(bullet);
497
 
498
+ soundSystem.playGunshot();
499
+
500
+ // 총ꡬ ν™”μ—Ό 효과
501
+ const muzzleFlash = new THREE.PointLight(0xffff00, 3, 10);
502
+ muzzleFlash.position.copy(camera.position);
503
+ scene.add(muzzleFlash);
504
+ setTimeout(() => scene.remove(muzzleFlash), 50);
505
+ }