cutechicken commited on
Commit
0e6c440
Β·
verified Β·
1 Parent(s): 253e4b2

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +105 -439
game.js CHANGED
@@ -12,7 +12,7 @@ const MAX_HEALTH = 1000;
12
  const ENEMY_MOVE_SPEED = 0.1;
13
  const ENEMY_COUNT_MAX = 5;
14
  const PARTICLE_COUNT = 15;
15
- const BUILDING_COUNT = 30; // 건물 수 μΆ”κ°€
16
  const ENEMY_CONFIG = {
17
  ATTACK_RANGE: 100,
18
  ATTACK_INTERVAL: 2000,
@@ -75,62 +75,56 @@ class TankPlayer {
75
  }
76
 
77
  shoot(scene) {
78
- const currentTime = Date.now();
79
- if (currentTime - this.lastShootTime < this.shootInterval || this.ammo <= 0) return null;
80
-
81
- // μ΄μ•Œ 생성
82
- const bulletGeometry = new THREE.SphereGeometry(0.2);
83
- const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
84
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
85
-
86
- // μ΄μ•Œ μ‹œμž‘ μœ„μΉ˜ (포탑 끝)
87
- const bulletOffset = new THREE.Vector3(0, 0.5, 2);
88
- // ν¬νƒ‘μ˜ νšŒμ „μ„ 적용
89
- bulletOffset.applyQuaternion(this.turretGroup.quaternion);
90
- bulletOffset.applyQuaternion(this.body.quaternion);
91
- bullet.position.copy(this.body.position).add(bulletOffset);
92
-
93
- // μ΄μ•Œ 속도 (포탑 λ°©ν–₯)
94
- const direction = new THREE.Vector3(0, 0, 1);
95
- direction.applyQuaternion(this.turretGroup.quaternion);
96
- direction.applyQuaternion(this.body.quaternion);
97
- bullet.velocity = direction.multiplyScalar(2);
98
-
99
- scene.add(bullet);
100
- this.bullets.push(bullet);
101
- this.ammo--;
102
- this.lastShootTime = currentTime;
103
-
104
- document.getElementById('ammo').textContent = `Ammo: ${this.ammo}/10`;
105
-
106
- return bullet;
107
- }
108
 
109
  update(mouseX, mouseY) {
110
- if (!this.body || !this.turretGroup) return;
111
-
112
- // μ΄μ•Œ μ—…λ°μ΄νŠΈλ§Œ μˆ˜ν–‰ν•˜κ³  포탑 νšŒμ „μ€ Game 클래슀의 handleMovementμ—μ„œ 처리
113
- for (let i = this.bullets.length - 1; i >= 0; i--) {
114
- const bullet = this.bullets[i];
115
- bullet.position.add(bullet.velocity);
116
-
117
- // μ΄μ•Œμ΄ 맡 λ°–μœΌλ‘œ λ‚˜κ°€λ©΄ 제거
118
- if (Math.abs(bullet.position.x) > MAP_SIZE/2 ||
119
- Math.abs(bullet.position.z) > MAP_SIZE/2) {
120
- scene.remove(bullet);
121
- this.bullets.splice(i, 1);
122
- }
123
- }
124
- }
125
 
126
  move(direction) {
127
- if (!this.body) return;
128
-
129
- const moveVector = new THREE.Vector3();
130
- moveVector.x = direction.x * this.moveSpeed;
131
- moveVector.z = direction.z * this.moveSpeed;
132
-
133
- this.body.position.add(moveVector);
134
  }
135
 
136
  rotate(angle) {
@@ -147,23 +141,22 @@ class TankPlayer {
147
  return this.health <= 0;
148
  }
149
  }
150
- // Enemy 클래슀 μˆ˜μ •
151
  class Enemy {
152
  constructor(scene, position, type = 'tank') {
153
  this.scene = scene;
154
  this.position = position;
155
  this.mesh = null;
156
- this.type = type; // 'tank' λ˜λŠ” 'heavy'
157
- this.health = type === 'tank' ? 100 : 200; // heavyλŠ” 체λ ₯이 더 λ†’μŒ
158
  this.lastAttackTime = 0;
159
  this.bullets = [];
160
  this.isLoaded = false;
161
- this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7; // heavyλŠ” 더 느림
162
  }
163
 
164
  async initialize(loader) {
165
  try {
166
- // νƒ€μž…μ— 따라 λ‹€λ₯Έ λͺ¨λΈ λ‘œλ“œ
167
  const modelPath = this.type === 'tank' ? '/models/enemy1.glb' : '/models/enemy4.glb';
168
  const result = await loader.loadAsync(modelPath);
169
  this.mesh = result.scene;
@@ -188,22 +181,17 @@ class Enemy {
188
  update(playerPosition) {
189
  if (!this.mesh || !this.isLoaded) return;
190
 
191
- // ν”Œλ ˆμ΄μ–΄ λ°©ν–₯으둜 νšŒμ „
192
  const direction = new THREE.Vector3()
193
  .subVectors(playerPosition, this.mesh.position)
194
  .normalize();
195
 
196
  this.mesh.lookAt(playerPosition);
197
-
198
- // ν”Œλ ˆμ΄μ–΄ λ°©ν–₯으둜 이동 (νƒ€μž…μ— 따라 λ‹€λ₯Έ 속도)
199
  this.mesh.position.add(direction.multiplyScalar(this.moveSpeed));
200
 
201
- // μ΄μ•Œ μ—…λ°μ΄νŠΈ
202
  for (let i = this.bullets.length - 1; i >= 0; i--) {
203
  const bullet = this.bullets[i];
204
  bullet.position.add(bullet.velocity);
205
 
206
- // μ΄μ•Œμ΄ 맡 λ°–μœΌλ‘œ λ‚˜κ°€λ©΄ 제거
207
  if (Math.abs(bullet.position.x) > MAP_SIZE ||
208
  Math.abs(bullet.position.z) > MAP_SIZE) {
209
  this.scene.remove(bullet);
@@ -216,7 +204,7 @@ class Enemy {
216
  const currentTime = Date.now();
217
  const attackInterval = this.type === 'tank' ?
218
  ENEMY_CONFIG.ATTACK_INTERVAL :
219
- ENEMY_CONFIG.ATTACK_INTERVAL * 1.5; // heavyλŠ” λ°œμ‚¬ 간격이 더 κΉ€
220
 
221
  if (currentTime - this.lastAttackTime < attackInterval) return;
222
 
@@ -258,7 +246,6 @@ class Enemy {
258
  }
259
  }
260
 
261
- // Particle ν΄λž˜μŠ€λŠ” κ·ΈλŒ€λ‘œ μœ μ§€
262
  class Particle {
263
  constructor(scene, position) {
264
  const geometry = new THREE.SphereGeometry(0.1);
@@ -290,10 +277,9 @@ class Particle {
290
  scene.remove(this.mesh);
291
  }
292
  }
293
- // Game 클래슀
294
  class Game {
295
  constructor() {
296
- // κΈ°λ³Έ Three.js μ„€μ •
297
  this.scene = new THREE.Scene();
298
  this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
299
  this.renderer = new THREE.WebGLRenderer({ antialias: true });
@@ -301,7 +287,6 @@ class Game {
301
  this.renderer.shadowMap.enabled = true;
302
  document.getElementById('gameContainer').appendChild(this.renderer.domElement);
303
 
304
- // κ²Œμž„ μš”μ†Œ μ΄ˆκΈ°ν™”
305
  this.tank = new TankPlayer();
306
  this.enemies = [];
307
  this.particles = [];
@@ -315,7 +300,6 @@ class Game {
315
  this.previousTankPosition = new THREE.Vector3();
316
  this.lastTime = performance.now();
317
 
318
- // 마우슀/ν‚€λ³΄λ“œ μƒνƒœ
319
  this.mouse = { x: 0, y: 0 };
320
  this.keys = {
321
  forward: false,
@@ -324,408 +308,90 @@ class Game {
324
  right: false
325
  };
326
 
327
- // 이벀트 λ¦¬μŠ€λ„ˆ μ„€μ •
328
  this.setupEventListeners();
329
  this.initialize();
330
- this.disposedObjects = new Set(); // 제거된 객체 좔적
331
  }
332
 
333
- // λ¦¬μ†ŒμŠ€ 정리λ₯Ό μœ„ν•œ λ©”μ„œλ“œ
334
- dispose(object) {
335
- if (this.disposedObjects.has(object)) return;
336
-
337
- if (object.geometry) object.geometry.dispose();
338
- if (object.material) {
339
- if (Array.isArray(object.material)) {
340
- object.material.forEach(material => material.dispose());
341
- } else {
342
- object.material.dispose();
343
- }
344
- }
345
- if (object.parent) object.parent.remove(object);
346
- this.disposedObjects.add(object);
347
- }
348
 
349
- // μ΄μ•Œ 정리
350
- cleanupBullets() {
351
- // ν”Œλ ˆμ΄μ–΄ μ΄μ•Œ 정리
352
- for (let i = this.tank.bullets.length - 1; i >= 0; i--) {
353
- const bullet = this.tank.bullets[i];
354
- if (Math.abs(bullet.position.x) > MAP_SIZE/2 ||
355
- Math.abs(bullet.position.z) > MAP_SIZE/2) {
356
- this.dispose(bullet);
357
- this.tank.bullets.splice(i, 1);
358
- }
359
  }
360
-
361
- // 적 μ΄μ•Œ 정리
362
- this.enemies.forEach(enemy => {
363
- for (let i = enemy.bullets.length - 1; i >= 0; i--) {
364
- const bullet = enemy.bullets[i];
365
- if (Math.abs(bullet.position.x) > MAP_SIZE/2 ||
366
- Math.abs(bullet.position.z) > MAP_SIZE/2) {
367
- this.dispose(bullet);
368
- enemy.bullets.splice(i, 1);
369
- }
370
- }
371
- });
372
  }
373
 
374
- async initialize() {
375
- try {
376
- // μ‘°λͺ… μ„€μ •
377
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
378
- this.scene.add(ambientLight);
379
-
380
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
381
- directionalLight.position.set(50, 50, 50);
382
- directionalLight.castShadow = true;
383
- directionalLight.shadow.mapSize.width = 2048;
384
- directionalLight.shadow.mapSize.height = 2048;
385
- this.scene.add(directionalLight);
386
-
387
- // μ§€ν˜• 생성
388
- const ground = new THREE.Mesh(
389
- new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE),
390
- new THREE.MeshStandardMaterial({
391
- color: 0x333333,
392
- roughness: 0.9,
393
- metalness: 0.1
394
- })
395
- );
396
- ground.rotation.x = -Math.PI / 2;
397
- ground.receiveShadow = true;
398
- this.scene.add(ground);
399
-
400
- // 건물 생성
401
- await this.createBuildings();
402
-
403
- // 탱크 μ΄ˆκΈ°ν™”
404
- await this.tank.initialize(this.scene, this.loader);
405
- if (!this.tank.isLoaded) {
406
- throw new Error('Tank loading failed');
407
  }
408
-
409
- // 카메라 초기 μ„€μ •
410
- const tankPosition = this.tank.getPosition();
411
- this.camera.position.set(
412
- tankPosition.x,
413
- tankPosition.y + 15,
414
- tankPosition.z - 30
415
- );
416
- this.camera.lookAt(new THREE.Vector3(
417
- tankPosition.x,
418
- tankPosition.y + 2,
419
- tankPosition.z
420
- ));
421
-
422
- // λ‘œλ”© μ™„λ£Œ
423
- this.isLoading = false;
424
- document.getElementById('loading').style.display = 'none';
425
-
426
- // κ²Œμž„ μ‹œμž‘
427
- this.animate();
428
- this.spawnEnemies();
429
- this.startGameTimer();
430
-
431
- } catch (error) {
432
- console.error('Game initialization error:', error);
433
- this.handleLoadingError();
434
  }
435
  }
436
 
437
- setupEventListeners() {
438
- document.addEventListener('keydown', (event) => {
439
- if (this.isLoading) return;
440
- switch(event.code) {
441
- case 'KeyW': this.keys.forward = true; break;
442
- case 'KeyS': this.keys.backward = true; break;
443
- case 'KeyA': this.keys.left = true; break;
444
- case 'KeyD': this.keys.right = true; break;
445
- }
446
- });
447
-
448
- document.addEventListener('keyup', (event) => {
449
- if (this.isLoading) return;
450
- switch(event.code) {
451
- case 'KeyW': this.keys.forward = false; break;
452
- case 'KeyS': this.keys.backward = false; break;
453
- case 'KeyA': this.keys.left = false; break;
454
- case 'KeyD': this.keys.right = false; break;
455
- }
456
- });
457
-
458
- document.addEventListener('mousemove', (event) => {
459
- if (this.isLoading || !document.pointerLockElement) return;
460
- this.mouse.x += event.movementX * 0.002;
461
- this.mouse.y += event.movementY * 0.002;
462
- });
463
-
464
- document.addEventListener('click', () => {
465
- if (!document.pointerLockElement) {
466
- document.body.requestPointerLock();
467
- } else {
468
- const bullet = this.tank.shoot(this.scene);
469
- if (bullet) {
470
- // λ°œμ‚¬ 효과 μΆ”κ°€ κ°€λŠ₯
471
- }
472
- }
473
- });
474
-
475
- document.addEventListener('pointerlockchange', () => {
476
- if (!document.pointerLockElement) {
477
- this.mouse.x = 0;
478
- this.mouse.y = 0;
479
- }
480
- });
481
-
482
- window.addEventListener('resize', () => {
483
- this.camera.aspect = window.innerWidth / window.innerHeight;
484
- this.camera.updateProjectionMatrix();
485
- this.renderer.setSize(window.innerWidth, window.innerHeight);
486
- });
487
- }
488
-
489
- handleMovement() {
490
- if (!this.tank.isLoaded) return;
491
-
492
- const direction = new THREE.Vector3();
493
-
494
- if (this.keys.forward) direction.z += 1;
495
- if (this.keys.backward) direction.z -= 1;
496
- if (this.keys.left) direction.x -= 1;
497
- if (this.keys.right) direction.x += 1;
498
-
499
- if (direction.length() > 0) {
500
- direction.normalize();
501
 
502
- if (this.keys.left) this.tank.rotate(-1);
503
- if (this.keys.right) this.tank.rotate(1);
 
504
 
505
- direction.applyEuler(this.tank.body.rotation);
506
- this.tank.move(direction);
507
- }
508
-
509
- const mouseVector = new THREE.Vector2(this.mouse.x, -this.mouse.y);
510
- const rotationAngle = Math.atan2(mouseVector.x, mouseVector.y);
511
-
512
- if (this.tank.turretGroup) {
513
- this.tank.turretGroup.rotation.y = rotationAngle;
514
  }
515
-
516
- const tankPos = this.tank.getPosition();
517
- const cameraDistance = 30;
518
- const cameraHeight = 15;
519
- const lookAtHeight = 5;
520
-
521
- const tankRotation = this.tank.body.rotation.y;
522
-
523
- this.camera.position.set(
524
- tankPos.x - Math.sin(tankRotation) * cameraDistance,
525
- tankPos.y + cameraHeight,
526
- tankPos.z - Math.cos(tankRotation) * cameraDistance
527
- );
528
-
529
- const lookAtPoint = new THREE.Vector3(
530
- tankPos.x + Math.sin(tankRotation) * 10,
531
- tankPos.y + lookAtHeight,
532
- tankPos.z + Math.cos(tankRotation) * 10
533
- );
534
-
535
- this.camera.lookAt(lookAtPoint);
536
  }
537
 
538
- animate() {
539
- if (this.isGameOver) return;
540
-
541
- requestAnimationFrame(() => this.animate());
542
-
543
- const currentTime = performance.now();
544
- const deltaTime = (currentTime - this.lastTime) / 1000;
545
- this.lastTime = currentTime;
546
-
547
- if (this.isLoading) {
548
- this.renderer.render(this.scene, this.camera);
549
- return;
550
- }
551
 
552
- if (deltaTime < 1/60) return;
 
 
 
553
 
554
- this.handleMovement();
555
- this.tank.update(this.mouse.x, this.mouse.y);
556
-
557
- const tankPosition = this.tank.getPosition();
558
-
559
- this.enemies.forEach(enemy => {
560
- if (!enemy.mesh || !enemy.isLoaded) return;
561
-
562
- const distance = enemy.mesh.position.distanceTo(tankPosition);
563
- if (distance > 200) return;
564
-
565
- enemy.update(tankPosition);
566
- if (distance < ENEMY_CONFIG.ATTACK_RANGE) {
567
- enemy.shoot(tankPosition);
568
- }
569
- });
570
-
571
- this.updateParticles();
572
- this.checkCollisions();
573
- this.cleanupBullets();
574
- this.updateUI();
575
- this.renderer.render(this.scene, this.camera);
576
  }
577
 
578
- checkCollisions() {
579
- if (this.isLoading || !this.tank.isLoaded) return;
580
-
581
- const tankPosition = this.tank.getPosition();
582
- const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body);
583
-
584
- for (const enemy of this.enemies) {
585
- if (!enemy.mesh || !enemy.isLoaded) continue;
586
-
587
- for (let i = enemy.bullets.length - 1; i >= 0; i--) {
588
- const bullet = enemy.bullets[i];
589
- const distance = bullet.position.distanceTo(tankPosition);
590
-
591
- if (distance > 50) continue;
592
-
593
- if (distance < 1) {
594
- if (this.tank.takeDamage(10)) {
595
- this.endGame();
596
- return;
597
- }
598
- this.dispose(bullet);
599
- enemy.bullets.splice(i, 1);
600
- this.createExplosion(bullet.position);
601
- document.getElementById('health').style.width =
602
- `${(this.tank.health / MAX_HEALTH) * 100}%`;
603
- }
604
  }
605
- }
606
 
607
- for (const building of this.buildings) {
608
- const buildingBox = new THREE.Box3().setFromObject(building);
609
- if (tankBoundingBox.intersectsBox(buildingBox)) {
610
- this.tank.body.position.copy(this.previousTankPosition);
611
- break;
612
  }
613
- }
614
-
615
- this.previousTankPosition = this.tank.body.position.clone();
616
- }
617
-
618
- updateUI() {
619
- const healthBar = document.getElementById('health');
620
- if (healthBar) {
621
- healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`;
622
- }
623
-
624
- const timeElement = document.getElementById('time');
625
- if (timeElement) {
626
- timeElement.textContent = `Time: ${this.gameTime}s`;
627
- }
628
-
629
- const scoreElement = document.getElementById('score');
630
- if (scoreElement) {
631
- scoreElement.textContent = `Score: ${this.score}`;
632
- }
633
  }
634
 
635
- endGame() {
636
- this.isGameOver = true;
637
- const gameOverDiv = document.createElement('div');
638
- gameOverDiv.style.position = 'absolute';
639
- gameOverDiv.style.top = '50%';
640
- gameOverDiv.style.left = '50%';
641
- gameOverDiv.style.transform = 'translate(-50%, -50%)';
642
- gameOverDiv.style.color = 'white';
643
- gameOverDiv.style.fontSize = '48px';
644
- gameOverDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
645
- gameOverDiv.style.padding = '20px';
646
- gameOverDiv.style.borderRadius = '10px';
647
- gameOverDiv.innerHTML = `
648
- Game Over<br>
649
- Score: ${this.score}<br>
650
- Time Survived: ${GAME_DURATION - this.gameTime}s<br>
651
- <button onclick="location.reload()"
652
- style="font-size: 24px; padding: 10px; margin-top: 20px;
653
- cursor: pointer; background: #4CAF50; border: none;
654
- color: white; border-radius: 5px;">
655
- Play Again
656
- </button>
657
- `;
658
- document.body.appendChild(gameOverDiv);
659
- }
660
-
661
-
662
- animate() {
663
- if (this.isGameOver) return;
664
-
665
- requestAnimationFrame(() => this.animate());
666
-
667
- const currentTime = performance.now();
668
- const deltaTime = (currentTime - this.lastTime) / 1000;
669
- this.lastTime = currentTime;
670
-
671
- if (this.isLoading) {
672
- this.renderer.render(this.scene, this.camera);
673
- return;
674
- }
675
-
676
- // ν”„λ ˆμž„ μ œν•œ (60fps)
677
- if (deltaTime < 1/60) return;
678
-
679
- this.handleMovement();
680
- this.tank.update(this.mouse.x, this.mouse.y);
681
-
682
- const tankPosition = this.tank.getPosition();
683
-
684
- // 화면에 λ³΄μ΄λŠ” 적만 μ—…λ°μ΄νŠΈ
685
- this.enemies.forEach(enemy => {
686
- if (!enemy.mesh || !enemy.isLoaded) return;
687
-
688
- const distance = enemy.mesh.position.distanceTo(tankPosition);
689
- if (distance > 200) return; // λ¨Ό 적은 μ—…λ°μ΄νŠΈ μŠ€ν‚΅
690
-
691
- enemy.update(tankPosition);
692
- if (distance < ENEMY_CONFIG.ATTACK_RANGE) {
693
- enemy.shoot(tankPosition);
694
- }
695
- });
696
-
697
- this.updateParticles();
698
- this.checkCollisions();
699
- this.cleanupBullets();
700
- this.updateUI();
701
- this.renderer.render(this.scene, this.camera);
702
- }
703
-
704
- updateUI() {
705
- const healthBar = document.getElementById('health');
706
- if (healthBar) {
707
- healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`;
708
- }
709
-
710
- const timeElement = document.getElementById('time');
711
- if (timeElement) {
712
- timeElement.textContent = `Time: ${this.gameTime}s`;
713
- }
714
-
715
- const scoreElement = document.getElementById('score');
716
- if (scoreElement) {
717
- scoreElement.textContent = `Score: ${this.score}`;
718
- }
719
  }
720
  }
721
 
722
-
723
-
724
- // HTML의 startGame ν•¨μˆ˜μ™€ μ—°κ²°
725
  window.startGame = function() {
726
  document.getElementById('startScreen').style.display = 'none';
727
  document.body.requestPointerLock();
728
  };
729
 
730
- // κ²Œμž„ μΈμŠ€ν„΄μŠ€ 생성
731
  const game = new Game();
 
12
  const ENEMY_MOVE_SPEED = 0.1;
13
  const ENEMY_COUNT_MAX = 5;
14
  const PARTICLE_COUNT = 15;
15
+ const BUILDING_COUNT = 30;
16
  const ENEMY_CONFIG = {
17
  ATTACK_RANGE: 100,
18
  ATTACK_INTERVAL: 2000,
 
75
  }
76
 
77
  shoot(scene) {
78
+ const currentTime = Date.now();
79
+ if (currentTime - this.lastShootTime < this.shootInterval || this.ammo <= 0) return null;
80
+
81
+ const bulletGeometry = new THREE.SphereGeometry(0.2);
82
+ const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
83
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
84
+
85
+ const bulletOffset = new THREE.Vector3(0, 0.5, 2);
86
+ bulletOffset.applyQuaternion(this.turretGroup.quaternion);
87
+ bulletOffset.applyQuaternion(this.body.quaternion);
88
+ bullet.position.copy(this.body.position).add(bulletOffset);
89
+
90
+ const direction = new THREE.Vector3(0, 0, 1);
91
+ direction.applyQuaternion(this.turretGroup.quaternion);
92
+ direction.applyQuaternion(this.body.quaternion);
93
+ bullet.velocity = direction.multiplyScalar(2);
94
+
95
+ scene.add(bullet);
96
+ this.bullets.push(bullet);
97
+ this.ammo--;
98
+ this.lastShootTime = currentTime;
99
+
100
+ document.getElementById('ammo').textContent = `Ammo: ${this.ammo}/10`;
101
+
102
+ return bullet;
103
+ }
 
 
 
 
104
 
105
  update(mouseX, mouseY) {
106
+ if (!this.body || !this.turretGroup) return;
107
+
108
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
109
+ const bullet = this.bullets[i];
110
+ bullet.position.add(bullet.velocity);
111
+
112
+ if (Math.abs(bullet.position.x) > MAP_SIZE/2 ||
113
+ Math.abs(bullet.position.z) > MAP_SIZE/2) {
114
+ scene.remove(bullet);
115
+ this.bullets.splice(i, 1);
116
+ }
117
+ }
118
+ }
 
 
119
 
120
  move(direction) {
121
+ if (!this.body) return;
122
+
123
+ const moveVector = new THREE.Vector3();
124
+ moveVector.x = direction.x * this.moveSpeed;
125
+ moveVector.z = direction.z * this.moveSpeed;
126
+
127
+ this.body.position.add(moveVector);
128
  }
129
 
130
  rotate(angle) {
 
141
  return this.health <= 0;
142
  }
143
  }
144
+
145
  class Enemy {
146
  constructor(scene, position, type = 'tank') {
147
  this.scene = scene;
148
  this.position = position;
149
  this.mesh = null;
150
+ this.type = type;
151
+ this.health = type === 'tank' ? 100 : 200;
152
  this.lastAttackTime = 0;
153
  this.bullets = [];
154
  this.isLoaded = false;
155
+ this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
156
  }
157
 
158
  async initialize(loader) {
159
  try {
 
160
  const modelPath = this.type === 'tank' ? '/models/enemy1.glb' : '/models/enemy4.glb';
161
  const result = await loader.loadAsync(modelPath);
162
  this.mesh = result.scene;
 
181
  update(playerPosition) {
182
  if (!this.mesh || !this.isLoaded) return;
183
 
 
184
  const direction = new THREE.Vector3()
185
  .subVectors(playerPosition, this.mesh.position)
186
  .normalize();
187
 
188
  this.mesh.lookAt(playerPosition);
 
 
189
  this.mesh.position.add(direction.multiplyScalar(this.moveSpeed));
190
 
 
191
  for (let i = this.bullets.length - 1; i >= 0; i--) {
192
  const bullet = this.bullets[i];
193
  bullet.position.add(bullet.velocity);
194
 
 
195
  if (Math.abs(bullet.position.x) > MAP_SIZE ||
196
  Math.abs(bullet.position.z) > MAP_SIZE) {
197
  this.scene.remove(bullet);
 
204
  const currentTime = Date.now();
205
  const attackInterval = this.type === 'tank' ?
206
  ENEMY_CONFIG.ATTACK_INTERVAL :
207
+ ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
208
 
209
  if (currentTime - this.lastAttackTime < attackInterval) return;
210
 
 
246
  }
247
  }
248
 
 
249
  class Particle {
250
  constructor(scene, position) {
251
  const geometry = new THREE.SphereGeometry(0.1);
 
277
  scene.remove(this.mesh);
278
  }
279
  }
280
+
281
  class Game {
282
  constructor() {
 
283
  this.scene = new THREE.Scene();
284
  this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
285
  this.renderer = new THREE.WebGLRenderer({ antialias: true });
 
287
  this.renderer.shadowMap.enabled = true;
288
  document.getElementById('gameContainer').appendChild(this.renderer.domElement);
289
 
 
290
  this.tank = new TankPlayer();
291
  this.enemies = [];
292
  this.particles = [];
 
300
  this.previousTankPosition = new THREE.Vector3();
301
  this.lastTime = performance.now();
302
 
 
303
  this.mouse = { x: 0, y: 0 };
304
  this.keys = {
305
  forward: false,
 
308
  right: false
309
  };
310
 
 
311
  this.setupEventListeners();
312
  this.initialize();
313
+ this.disposedObjects = new Set();
314
  }
315
 
316
+ // 여기에 Game 클래슀의 λ‚˜λ¨Έμ§€ λ©”μ„œλ“œλ“€μ΄ λ“€μ–΄κ°‘λ‹ˆλ‹€...
317
+ // (initialize, setupEventListeners, handleMovement, animate, checkCollisions, updateUI, endGame λ“±)
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
+ createExplosion(position) {
320
+ for (let i = 0; i < PARTICLE_COUNT; i++) {
321
+ this.particles.push(new Particle(this.scene, position));
 
 
 
 
 
 
 
322
  }
 
 
 
 
 
 
 
 
 
 
 
 
323
  }
324
 
325
+ updateParticles() {
326
+ for (let i = this.particles.length - 1; i >= 0; i--) {
327
+ const particle = this.particles[i];
328
+ if (!particle.update()) {
329
+ particle.destroy(this.scene);
330
+ this.particles.splice(i, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  }
333
  }
334
 
335
+ async createBuildings() {
336
+ for (let i = 0; i < BUILDING_COUNT; i++) {
337
+ const x = (Math.random() - 0.5) * MAP_SIZE;
338
+ const z = (Math.random() - 0.5) * MAP_SIZE;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
+ const geometry = new THREE.BoxGeometry(10, 20, 10);
341
+ const material = new THREE.MeshStandardMaterial({ color: 0x808080 });
342
+ const building = new THREE.Mesh(geometry, material);
343
 
344
+ building.position.set(x, 10, z);
345
+ building.castShadow = true;
346
+ building.receiveShadow = true;
347
+
348
+ this.scene.add(building);
349
+ this.buildings.push(building);
 
 
 
350
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  }
352
 
353
+ spawnEnemies() {
354
+ const spawnInterval = setInterval(() => {
355
+ if (this.isGameOver || this.enemies.length >= ENEMY_COUNT_MAX) return;
 
 
 
 
 
 
 
 
 
 
356
 
357
+ const angle = Math.random() * Math.PI * 2;
358
+ const distance = 100 + Math.random() * 50;
359
+ const x = Math.cos(angle) * distance;
360
+ const z = Math.sin(angle) * distance;
361
 
362
+ const enemyType = Math.random() > 0.7 ? 'heavy' : 'tank';
363
+ const enemy = new Enemy(this.scene, new THREE.Vector3(x, ENEMY_GROUND_HEIGHT, z), enemyType);
364
+ enemy.initialize(this.loader);
365
+ this.enemies.push(enemy);
366
+ }, 3000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  }
368
 
369
+ startGameTimer() {
370
+ const timerInterval = setInterval(() => {
371
+ if (this.isGameOver) {
372
+ clearInterval(timerInterval);
373
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  }
 
375
 
376
+ this.gameTime--;
377
+ if (this.gameTime <= 0) {
378
+ this.endGame();
379
+ clearInterval(timerInterval);
 
380
  }
381
+ }, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  }
383
 
384
+ handleLoadingError() {
385
+ this.isLoading = false;
386
+ document.getElementById('loading').style.display = 'none';
387
+ document.getElementById('error').style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
389
  }
390
 
391
+ // κ²Œμž„ μ‹œμž‘
 
 
392
  window.startGame = function() {
393
  document.getElementById('startScreen').style.display = 'none';
394
  document.body.requestPointerLock();
395
  };
396
 
 
397
  const game = new Game();