cutechicken commited on
Commit
f677bab
β€’
1 Parent(s): b2a9297

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +224 -245
game.js CHANGED
@@ -1,6 +1,6 @@
1
- const THREE = window.THREE;
2
- const GLTFLoader = window.THREE.GLTFLoader;
3
- const PointerLockControls = window.THREE.PointerLockControls;
4
 
5
  // κ²Œμž„ μƒμˆ˜
6
  const GAME_DURATION = 180;
@@ -572,67 +572,31 @@ class Enemy {
572
  constructor(scene, position, type = 'tank') {
573
  this.scene = scene;
574
  this.position = position;
575
- this.body = null;
576
- this.turret = null;
577
- this.turretGroup = new THREE.Group();
578
  this.type = type;
579
  this.health = type === 'tank' ? 100 : 200;
580
  this.lastAttackTime = 0;
581
  this.bullets = [];
582
  this.isLoaded = false;
583
  this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
584
- this.turnSpeed = 0.03;
585
- this.targetRotation = 0;
586
- this.turretRotation = 0;
587
- this.canShoot = false; // 포탑이 μ •λ ¬λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜λŠ” ν”Œλž˜κ·Έ
588
  }
589
 
590
  async initialize(loader) {
591
  try {
592
- // λͺΈμ²΄ λ‘œλ“œ
593
- const bodyResult = await loader.loadAsync('/models/t90Body.glb');
594
- this.body = bodyResult.scene;
595
-
596
- // 포탑 λ‘œλ“œ
597
- const turretResult = await loader.loadAsync('/models/t90Turret.glb');
598
- this.turret = turretResult.scene;
599
-
600
- // 포탑 κ·Έλ£Ή μ„€μ •
601
- this.turretGroup.position.y = 0.2; // 포탑 높이 μ‘°μ •
602
- this.turretGroup.add(this.turret);
603
- this.body.add(this.turretGroup);
604
-
605
- // μœ„μΉ˜μ™€ μŠ€μΌ€μΌ μ„€μ •
606
- this.body.position.copy(this.position);
607
- this.body.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
608
-
609
- // 그림자 μ„€μ •
610
- this.body.traverse((child) => {
611
- if (child.isMesh) {
612
- child.castShadow = true;
613
- child.receiveShadow = true;
614
- }
615
- });
616
 
617
- this.turret.traverse((child) => {
618
  if (child.isMesh) {
619
  child.castShadow = true;
620
  child.receiveShadow = true;
621
  }
622
  });
623
 
624
- // 그림자 평면 μΆ”κ°€
625
- const shadowPlaneGeometry = new THREE.PlaneGeometry(8, 8);
626
- const shadowPlaneMaterial = new THREE.ShadowMaterial({
627
- opacity: 0.3
628
- });
629
- this.shadowPlane = new THREE.Mesh(shadowPlaneGeometry, shadowPlaneMaterial);
630
- this.shadowPlane.receiveShadow = true;
631
- this.shadowPlane.rotation.x = -Math.PI / 2;
632
- this.shadowPlane.position.y = 0.1;
633
- this.body.add(this.shadowPlane);
634
-
635
- this.scene.add(this.body);
636
  this.isLoaded = true;
637
  } catch (error) {
638
  console.error('Error loading enemy model:', error);
@@ -641,76 +605,38 @@ class Enemy {
641
  }
642
 
643
  update(playerPosition) {
644
- if (!this.body || !this.isLoaded) return;
645
 
646
- const direction = new THREE.Vector3()
647
- .subVectors(playerPosition, this.body.position)
648
- .normalize();
649
-
650
- const distanceToPlayer = this.body.position.distanceTo(playerPosition);
651
- const minDistance = 50;
652
-
653
- // 이전 μœ„μΉ˜ μ €μž₯
654
- const previousPosition = this.body.position.clone();
655
-
656
- if (distanceToPlayer > minDistance) {
657
- // λͺ©ν‘œ νšŒμ „ 각도 계산
658
- this.targetRotation = Math.atan2(direction.x, direction.z);
659
-
660
- // λΆ€λ“œλŸ¬μš΄ νšŒμ „
661
- let rotationDiff = this.targetRotation - this.body.rotation.y;
662
- while (rotationDiff > Math.PI) rotationDiff -= Math.PI * 2;
663
- while (rotationDiff < -Math.PI) rotationDiff += Math.PI * 2;
664
-
665
- this.body.rotation.y += Math.sign(rotationDiff) * Math.min(Math.abs(rotationDiff), this.turnSpeed);
666
-
667
- // 전진 λ°©ν–₯으둜 이동
668
- const moveVector = new THREE.Vector3(0, 0, this.moveSpeed);
669
- moveVector.applyEuler(this.body.rotation);
670
- const newPosition = this.body.position.clone().add(moveVector);
671
-
672
- // μ§€ν˜• 높이 κ°€μ Έμ˜€κΈ°
673
- const heightAtNewPos = window.gameInstance.getHeightAtPosition(
674
- newPosition.x,
675
- newPosition.z
676
- );
677
- newPosition.y = heightAtNewPos + TANK_HEIGHT;
678
-
679
- // 좩돌 검사 및 이동 처리
680
- this.handleCollisions(newPosition, previousPosition);
681
- }
682
-
683
- // 포탑 νšŒμ „ - ν”Œλ ˆμ΄μ–΄λ₯Ό ν–₯ν•΄
684
- const turretTargetRotation = Math.atan2(
685
- playerPosition.x - this.body.position.x,
686
- playerPosition.z - this.body.position.z
687
  );
 
688
 
689
- // μ›”λ“œ μ’Œν‘œμ—μ„œμ˜ 포탑 νšŒμ „μ„ 둜컬 νšŒμ „μœΌλ‘œ λ³€ν™˜
690
- const localTurretRotation = turretTargetRotation - this.body.rotation.y;
691
-
692
- // 포탑 νšŒμ „ 보간
693
- let turretRotationDiff = localTurretRotation - this.turretGroup.rotation.y;
694
- while (turretRotationDiff > Math.PI) turretRotationDiff -= Math.PI * 2;
695
- while (turretRotationDiff < -Math.PI) turretRotationDiff += Math.PI * 2;
696
-
697
- this.turretGroup.rotation.y += Math.sign(turretRotationDiff) * Math.min(Math.abs(turretRotationDiff), this.turnSpeed);
698
-
699
- // 포탑이 ν”Œλ ˆμ΄μ–΄λ₯Ό ν–₯ν•˜κ³  μžˆλŠ”μ§€ 확인
700
- this.canShoot = Math.abs(turretRotationDiff) < 0.1;
701
-
702
- // μ΄μ•Œ μ—…λ°μ΄νŠΈ 둜직
703
- this.updateBullets();
704
- }
705
-
706
- handleCollisions(newPosition, previousPosition) {
707
  // μž„μ‹œλ‘œ μœ„μΉ˜ μ΄λ™ν•˜μ—¬ 좩돌 체크
708
- this.body.position.copy(newPosition);
 
709
 
710
- const enemyBox = new THREE.Box3().setFromObject(this.body);
 
711
  let hasCollision = false;
712
 
713
- // μž₯μ• λ¬Ό 좩돌 검사
714
  for (const obstacle of window.gameInstance.obstacles) {
715
  const obstacleBox = new THREE.Box3().setFromObject(obstacle);
716
  if (enemyBox.intersectsBox(obstacleBox)) {
@@ -719,34 +645,127 @@ class Enemy {
719
  }
720
  }
721
 
722
- // λ‹€λ₯Έ 적과의 좩돌 검사
723
- for (const otherEnemy of window.gameInstance.enemies) {
724
- if (otherEnemy !== this && otherEnemy.body) {
725
- const otherEnemyBox = new THREE.Box3().setFromObject(otherEnemy.body);
726
- if (enemyBox.intersectsBox(otherEnemyBox)) {
727
- hasCollision = true;
728
- break;
 
 
729
  }
730
  }
731
  }
732
 
733
- // 맡 경계 검사
734
  const mapBoundary = MAP_SIZE / 2;
735
  if (Math.abs(newPosition.x) > mapBoundary ||
736
  Math.abs(newPosition.z) > mapBoundary) {
737
  hasCollision = true;
738
  }
739
 
740
- // 좩돌이 있으면 이전 μœ„μΉ˜λ‘œ 볡귀
741
  if (hasCollision) {
742
- this.body.position.copy(previousPosition);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
  }
744
  }
 
745
 
746
- shoot(playerPosition) {
747
- // 포탑이 μ •λ ¬λ˜μ§€ μ•Šμ•˜μœΌλ©΄ λ°œμ‚¬ν•˜μ§€ μ•ŠμŒ
748
- if (!this.canShoot) return;
749
 
 
750
  const currentTime = Date.now();
751
  const attackInterval = this.type === 'tank' ?
752
  ENEMY_CONFIG.ATTACK_INTERVAL :
@@ -754,28 +773,16 @@ class Enemy {
754
 
755
  if (currentTime - this.lastAttackTime < attackInterval) return;
756
 
757
- // 포탄 생성
758
  const bulletGeometry = new THREE.SphereGeometry(this.type === 'tank' ? 0.2 : 0.3);
759
  const bulletMaterial = new THREE.MeshBasicMaterial({
760
  color: this.type === 'tank' ? 0xff0000 : 0xff6600
761
  });
762
  const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
763
 
764
- // 포탑 λμ—μ„œ 포탄이 λ°œμ‚¬λ˜λ„λ‘ μœ„μΉ˜ μ‘°μ •
765
- const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
766
- const muzzlePosition = new THREE.Vector3();
767
- const turretWorldQuaternion = new THREE.Quaternion();
768
-
769
- this.turret.getWorldPosition(muzzlePosition);
770
- this.turret.getWorldQuaternion(turretWorldQuaternion);
771
-
772
- muzzleOffset.applyQuaternion(turretWorldQuaternion);
773
- muzzlePosition.add(muzzleOffset);
774
-
775
- bullet.position.copy(muzzlePosition);
776
 
777
  const direction = new THREE.Vector3()
778
- .subVectors(playerPosition, muzzlePosition)
779
  .normalize();
780
 
781
  const bulletSpeed = this.type === 'tank' ?
@@ -789,40 +796,14 @@ class Enemy {
789
  this.lastAttackTime = currentTime;
790
  }
791
 
792
- updateBullets() {
793
- for (let i = this.bullets.length - 1; i >= 0; i--) {
794
- const bullet = this.bullets[i];
795
- bullet.position.add(bullet.velocity);
796
-
797
- // 맡 λ°–μœΌλ‘œ λ‚˜κ°€κ±°λ‚˜ μž₯μ• λ¬Όκ³Ό μΆ©λŒν•˜λ©΄ 제거
798
- if (Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
799
- Math.abs(bullet.position.z) > MAP_SIZE / 2) {
800
- this.scene.remove(bullet);
801
- this.bullets.splice(i, 1);
802
- continue;
803
- }
804
-
805
- // μž₯μ• λ¬Όκ³Ό 좩돌 체크
806
- const bulletBox = new THREE.Box3().setFromObject(bullet);
807
- for (const obstacle of window.gameInstance.obstacles) {
808
- const obstacleBox = new THREE.Box3().setFromObject(obstacle);
809
- if (bulletBox.intersectsBox(obstacleBox)) {
810
- this.scene.remove(bullet);
811
- this.bullets.splice(i, 1);
812
- break;
813
- }
814
- }
815
- }
816
- }
817
-
818
  takeDamage(damage) {
819
  this.health -= damage;
820
  return this.health <= 0;
821
  }
822
 
823
  destroy() {
824
- if (this.body) {
825
- this.scene.remove(this.body);
826
  this.bullets.forEach(bullet => this.scene.remove(bullet));
827
  this.bullets = [];
828
  this.isLoaded = false;
@@ -1097,56 +1078,56 @@ class Game {
1097
 
1098
  // λ ˆμ΄λ” μ—…λ°μ΄νŠΈ λ©”μ„œλ“œ μΆ”κ°€
1099
  updateRadar() {
1100
- const currentTime = Date.now();
1101
- if (currentTime - this.lastRadarUpdate < this.radarUpdateInterval) return;
1102
-
1103
- const radar = document.getElementById('radar');
1104
- const radarRect = radar.getBoundingClientRect();
1105
- const radarCenter = {
1106
- x: radarRect.width / 2,
1107
- y: radarRect.height / 2
1108
- };
1109
 
1110
- // κΈ°μ‘΄ 적 λ„νŠΈ 제거
1111
- const oldDots = radar.getElementsByClassName('enemy-dot');
1112
- while (oldDots[0]) {
1113
- oldDots[0].remove();
1114
- }
1115
 
1116
- // 탱크 μœ„μΉ˜ κ°€μ Έμ˜€κΈ°
1117
- const tankPos = this.tank.getPosition();
1118
 
1119
- // λͺ¨λ“  적에 λŒ€ν•΄ λ ˆμ΄λ”μ— ν‘œμ‹œ
1120
- this.enemies.forEach(enemy => {
1121
- if (!enemy.isLoaded || !enemy.body) return; // body 체크둜 λ³€κ²½
1122
 
1123
- const enemyPos = enemy.body.position; // mesh λŒ€μ‹  body μ‚¬μš©
1124
- const distance = tankPos.distanceTo(enemyPos);
1125
 
1126
- // λ ˆμ΄λ” λ²”μœ„ 내에 μžˆλŠ” 경우만 ν‘œμ‹œ
1127
- if (distance <= this.radarRange) {
1128
- // 탱크 κΈ°μ€€ μƒλŒ€ 각도 계산
1129
- const angle = Math.atan2(
1130
- enemyPos.x - tankPos.x,
1131
- enemyPos.z - tankPos.z
1132
- );
1133
 
1134
- // μƒλŒ€ 거리λ₯Ό λ ˆμ΄λ” 크기에 맞게 μŠ€μΌ€μΌλ§
1135
- const relativeDistance = distance / this.radarRange;
1136
- const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance);
1137
- const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance);
1138
-
1139
- // 적 λ„νŠΈ 생성 및 μΆ”κ°€
1140
- const dot = document.createElement('div');
1141
- dot.className = 'enemy-dot';
1142
- dot.style.left = `${dotX}px`;
1143
- dot.style.top = `${dotY}px`;
1144
- radar.appendChild(dot);
1145
- }
1146
- });
1147
 
1148
- this.lastRadarUpdate = currentTime;
1149
- }
1150
 
1151
  async addDesertDecorations() {
1152
  if (!this.obstacles) {
@@ -1858,47 +1839,45 @@ this.enemies.forEach(enemy => {
1858
  }
1859
 
1860
  animate() {
1861
- if (this.isGameOver) {
1862
- if (this.animationFrameId) {
1863
- cancelAnimationFrame(this.animationFrameId);
1864
- }
1865
- return;
1866
- }
1867
 
1868
- this.animationFrameId = requestAnimationFrame(() => this.animate());
1869
- // κ²Œμž„μ΄ μ‹œμž‘λ˜μ§€ μ•Šμ•˜μœΌλ©΄ λ Œλ”λ§λ§Œ μˆ˜ν–‰
1870
- if (!this.isStarted) {
1871
- this.renderer.render(this.scene, this.camera);
1872
- return;
1873
- }
1874
 
1875
- const currentTime = performance.now();
1876
- const deltaTime = (currentTime - this.lastTime) / 1000;
1877
- this.lastTime = currentTime;
1878
 
1879
- if (!this.isLoading) {
1880
- this.handleMovement();
1881
- this.tank.update(this.mouse.x, this.mouse.y, this.scene);
1882
-
1883
- const tankPosition = this.tank.getPosition();
1884
- this.enemies.forEach(enemy => {
1885
- if (enemy.isLoaded && enemy.body) { // body의 쑴재 μ—¬λΆ€ 체크
1886
- enemy.update(tankPosition);
1887
-
1888
- // mesh λŒ€μ‹  body μ‚¬μš©
1889
- if (enemy.body.position.distanceTo(tankPosition) < ENEMY_CONFIG.ATTACK_RANGE) {
1890
- enemy.shoot(tankPosition);
1891
- }
1892
- }
1893
- });
1894
 
1895
- this.updateParticles();
1896
- this.checkCollisions();
1897
- this.updateUI();
1898
- this.updateRadar();
1899
- }
1900
-
1901
- this.renderer.render(this.scene, this.camera);
 
1902
  }
1903
 
1904
  // Start game
 
1
+ import * as THREE from 'three';
2
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
3
+ import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
4
 
5
  // κ²Œμž„ μƒμˆ˜
6
  const GAME_DURATION = 180;
 
572
  constructor(scene, position, type = 'tank') {
573
  this.scene = scene;
574
  this.position = position;
575
+ this.mesh = null;
 
 
576
  this.type = type;
577
  this.health = type === 'tank' ? 100 : 200;
578
  this.lastAttackTime = 0;
579
  this.bullets = [];
580
  this.isLoaded = false;
581
  this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
 
 
 
 
582
  }
583
 
584
  async initialize(loader) {
585
  try {
586
+ const modelPath = this.type === 'tank' ? '/models/enemy1.glb' : '/models/enemy4.glb';
587
+ const result = await loader.loadAsync(modelPath);
588
+ this.mesh = result.scene;
589
+ this.mesh.position.copy(this.position);
590
+ this.mesh.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
 
592
+ this.mesh.traverse((child) => {
593
  if (child.isMesh) {
594
  child.castShadow = true;
595
  child.receiveShadow = true;
596
  }
597
  });
598
 
599
+ this.scene.add(this.mesh);
 
 
 
 
 
 
 
 
 
 
 
600
  this.isLoaded = true;
601
  } catch (error) {
602
  console.error('Error loading enemy model:', error);
 
605
  }
606
 
607
  update(playerPosition) {
608
+ if (!this.mesh || !this.isLoaded) return;
609
 
610
+ const direction = new THREE.Vector3()
611
+ .subVectors(playerPosition, this.mesh.position)
612
+ .normalize();
613
+
614
+ const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
615
+ const minDistance = 50;
616
+
617
+ // 이전 μœ„μΉ˜ μ €μž₯
618
+ const previousPosition = this.mesh.position.clone();
619
+
620
+ if (distanceToPlayer > minDistance) {
621
+ const moveVector = direction.multiplyScalar(this.moveSpeed);
622
+ const newPosition = this.mesh.position.clone().add(moveVector);
623
+
624
+ // μ§€ν˜• 높이 κ°€μ Έμ˜€κΈ°
625
+ const heightAtNewPos = window.gameInstance.getHeightAtPosition(
626
+ newPosition.x,
627
+ newPosition.z
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  );
629
+ newPosition.y = heightAtNewPos + TANK_HEIGHT;
630
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  // μž„μ‹œλ‘œ μœ„μΉ˜ μ΄λ™ν•˜μ—¬ 좩돌 체크
632
+ const originalPosition = this.mesh.position.clone();
633
+ this.mesh.position.copy(newPosition);
634
 
635
+ // μž₯μ• λ¬Όκ³Ό 좩돌 체크
636
+ const enemyBox = new THREE.Box3().setFromObject(this.mesh);
637
  let hasCollision = false;
638
 
639
+ // λͺ¨λ“  μž₯애물에 λŒ€ν•΄ 좩돌 검사
640
  for (const obstacle of window.gameInstance.obstacles) {
641
  const obstacleBox = new THREE.Box3().setFromObject(obstacle);
642
  if (enemyBox.intersectsBox(obstacleBox)) {
 
645
  }
646
  }
647
 
648
+ // λ‹€λ₯Έ 적 νƒ±ν¬μ™€μ˜ 좩돌 검사
649
+ if (!hasCollision) {
650
+ for (const otherEnemy of window.gameInstance.enemies) {
651
+ if (otherEnemy !== this && otherEnemy.mesh) {
652
+ const otherEnemyBox = new THREE.Box3().setFromObject(otherEnemy.mesh);
653
+ if (enemyBox.intersectsBox(otherEnemyBox)) {
654
+ hasCollision = true;
655
+ break;
656
+ }
657
  }
658
  }
659
  }
660
 
661
+ // 맡 경계 체크
662
  const mapBoundary = MAP_SIZE / 2;
663
  if (Math.abs(newPosition.x) > mapBoundary ||
664
  Math.abs(newPosition.z) > mapBoundary) {
665
  hasCollision = true;
666
  }
667
 
668
+ // 좩돌이 있으면 이전 μœ„μΉ˜λ‘œ 볡귀, μ—†μœΌλ©΄ μƒˆ μœ„μΉ˜ μœ μ§€
669
  if (hasCollision) {
670
+ this.mesh.position.copy(previousPosition);
671
+
672
+ // 좩돌 μ‹œ 우회 경둜 μ‹œλ„
673
+ const alternateDirections = [
674
+ new THREE.Vector3(-direction.z, 0, direction.x), // μ™Όμͺ½μœΌλ‘œ 90도
675
+ new THREE.Vector3(direction.z, 0, -direction.x), // 였λ₯Έμͺ½μœΌλ‘œ 90도
676
+ new THREE.Vector3(-direction.x, 0, -direction.z) // 180도 νšŒμ „
677
+ ];
678
+
679
+ for (const altDirection of alternateDirections) {
680
+ const altMoveVector = altDirection.multiplyScalar(this.moveSpeed);
681
+ const altNewPosition = previousPosition.clone().add(altMoveVector);
682
+
683
+ this.mesh.position.copy(altNewPosition);
684
+ const altEnemyBox = new THREE.Box3().setFromObject(this.mesh);
685
+
686
+ let altHasCollision = false;
687
+
688
+ // μƒˆλ‘œμš΄ λ°©ν–₯에 λŒ€ν•œ 좩돌 검사
689
+ for (const obstacle of window.gameInstance.obstacles) {
690
+ const obstacleBox = new THREE.Box3().setFromObject(obstacle);
691
+ if (altEnemyBox.intersectsBox(obstacleBox)) {
692
+ altHasCollision = true;
693
+ break;
694
+ }
695
+ }
696
+
697
+ if (!altHasCollision) {
698
+ // 우회 κ²½λ‘œκ°€ κ°€λŠ₯ν•˜λ©΄ κ·Έ λ°©ν–₯으둜 이동
699
+ break;
700
+ } else {
701
+ // μš°νšŒλ„ λΆˆκ°€λŠ₯ν•˜λ©΄ 이전 μœ„μΉ˜λ‘œ 볡귀
702
+ this.mesh.position.copy(previousPosition);
703
+ }
704
+ }
705
+ }
706
+
707
+ // μ§€ν˜•μ— λ”°λ₯Έ 기울기 μ‘°μ •
708
+ const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
709
+ const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.mesh.quaternion);
710
+
711
+ const frontHeight = window.gameInstance.getHeightAtPosition(
712
+ this.mesh.position.x + forwardVector.x,
713
+ this.mesh.position.z + forwardVector.z
714
+ );
715
+ const backHeight = window.gameInstance.getHeightAtPosition(
716
+ this.mesh.position.x - forwardVector.x,
717
+ this.mesh.position.z - forwardVector.z
718
+ );
719
+ const rightHeight = window.gameInstance.getHeightAtPosition(
720
+ this.mesh.position.x + rightVector.x,
721
+ this.mesh.position.z + rightVector.z
722
+ );
723
+ const leftHeight = window.gameInstance.getHeightAtPosition(
724
+ this.mesh.position.x - rightVector.x,
725
+ this.mesh.position.z - rightVector.z
726
+ );
727
+
728
+ const pitch = Math.atan2(frontHeight - backHeight, 2);
729
+ const roll = Math.atan2(rightHeight - leftHeight, 2);
730
+
731
+ // ν˜„μž¬ νšŒμ „ μœ μ§€ν•˜λ©΄μ„œ 기울기만 적용
732
+ const currentRotation = this.mesh.rotation.y;
733
+ this.mesh.rotation.set(pitch, currentRotation, roll);
734
+ }
735
+
736
+ // ν”Œλ ˆμ΄μ–΄λ₯Ό ν–₯ν•΄ 포탑 νšŒμ „
737
+ this.mesh.lookAt(playerPosition);
738
+
739
+ // μ΄μ•Œ μ—…λ°μ΄νŠΈ
740
+ if (this.bullets) {
741
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
742
+ const bullet = this.bullets[i];
743
+ bullet.position.add(bullet.velocity);
744
+
745
+ // μ΄μ•Œμ΄ 맡 λ°–μœΌλ‘œ λ‚˜κ°€κ±°λ‚˜ μž₯μ• λ¬Όκ³Ό μΆ©λŒν•˜λ©΄ 제거
746
+ if (Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
747
+ Math.abs(bullet.position.z) > MAP_SIZE / 2) {
748
+ this.scene.remove(bullet);
749
+ this.bullets.splice(i, 1);
750
+ continue;
751
+ }
752
+
753
+ // μ΄μ•Œκ³Ό μž₯μ• λ¬Ό 좩돌 체크
754
+ const bulletBox = new THREE.Box3().setFromObject(bullet);
755
+ for (const obstacle of window.gameInstance.obstacles) {
756
+ const obstacleBox = new THREE.Box3().setFromObject(obstacle);
757
+ if (bulletBox.intersectsBox(obstacleBox)) {
758
+ this.scene.remove(bullet);
759
+ this.bullets.splice(i, 1);
760
+ break;
761
+ }
762
+ }
763
  }
764
  }
765
+ }
766
 
 
 
 
767
 
768
+ shoot(playerPosition) {
769
  const currentTime = Date.now();
770
  const attackInterval = this.type === 'tank' ?
771
  ENEMY_CONFIG.ATTACK_INTERVAL :
 
773
 
774
  if (currentTime - this.lastAttackTime < attackInterval) return;
775
 
 
776
  const bulletGeometry = new THREE.SphereGeometry(this.type === 'tank' ? 0.2 : 0.3);
777
  const bulletMaterial = new THREE.MeshBasicMaterial({
778
  color: this.type === 'tank' ? 0xff0000 : 0xff6600
779
  });
780
  const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
781
 
782
+ bullet.position.copy(this.mesh.position);
 
 
 
 
 
 
 
 
 
 
 
783
 
784
  const direction = new THREE.Vector3()
785
+ .subVectors(playerPosition, this.mesh.position)
786
  .normalize();
787
 
788
  const bulletSpeed = this.type === 'tank' ?
 
796
  this.lastAttackTime = currentTime;
797
  }
798
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  takeDamage(damage) {
800
  this.health -= damage;
801
  return this.health <= 0;
802
  }
803
 
804
  destroy() {
805
+ if (this.mesh) {
806
+ this.scene.remove(this.mesh);
807
  this.bullets.forEach(bullet => this.scene.remove(bullet));
808
  this.bullets = [];
809
  this.isLoaded = false;
 
1078
 
1079
  // λ ˆμ΄λ” μ—…λ°μ΄νŠΈ λ©”μ„œλ“œ μΆ”κ°€
1080
  updateRadar() {
1081
+ const currentTime = Date.now();
1082
+ if (currentTime - this.lastRadarUpdate < this.radarUpdateInterval) return;
1083
+
1084
+ const radar = document.getElementById('radar');
1085
+ const radarRect = radar.getBoundingClientRect();
1086
+ const radarCenter = {
1087
+ x: radarRect.width / 2,
1088
+ y: radarRect.height / 2
1089
+ };
1090
 
1091
+ // κΈ°μ‘΄ 적 λ„νŠΈ 제거
1092
+ const oldDots = radar.getElementsByClassName('enemy-dot');
1093
+ while (oldDots[0]) {
1094
+ oldDots[0].remove();
1095
+ }
1096
 
1097
+ // 탱크 μœ„μΉ˜ κ°€μ Έμ˜€κΈ°
1098
+ const tankPos = this.tank.getPosition();
1099
 
1100
+ // λͺ¨λ“  적에 λŒ€ν•΄ λ ˆμ΄λ”μ— ν‘œμ‹œ
1101
+ this.enemies.forEach(enemy => {
1102
+ if (!enemy.mesh || !enemy.isLoaded) return;
1103
 
1104
+ const enemyPos = enemy.mesh.position;
1105
+ const distance = tankPos.distanceTo(enemyPos);
1106
 
1107
+ // λ ˆμ΄λ” λ²”μœ„ 내에 μžˆλŠ” 경우만 ν‘œμ‹œ
1108
+ if (distance <= this.radarRange) {
1109
+ // 탱크 κΈ°μ€€ μƒλŒ€ 각도 계산
1110
+ const angle = Math.atan2(
1111
+ enemyPos.x - tankPos.x,
1112
+ enemyPos.z - tankPos.z
1113
+ );
1114
 
1115
+ // μƒλŒ€ 거리λ₯Ό λ ˆμ΄λ” 크기에 맞게 μŠ€μΌ€μΌλ§
1116
+ const relativeDistance = distance / this.radarRange;
1117
+ const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance);
1118
+ const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance);
1119
+
1120
+ // 적 λ„νŠΈ 생성 및 μΆ”κ°€
1121
+ const dot = document.createElement('div');
1122
+ dot.className = 'enemy-dot';
1123
+ dot.style.left = `${dotX}px`;
1124
+ dot.style.top = `${dotY}px`;
1125
+ radar.appendChild(dot);
1126
+ }
1127
+ });
1128
 
1129
+ this.lastRadarUpdate = currentTime;
1130
+ }
1131
 
1132
  async addDesertDecorations() {
1133
  if (!this.obstacles) {
 
1839
  }
1840
 
1841
  animate() {
1842
+ if (this.isGameOver) {
1843
+ if (this.animationFrameId) {
1844
+ cancelAnimationFrame(this.animationFrameId);
1845
+ }
1846
+ return;
1847
+ }
1848
 
1849
+ this.animationFrameId = requestAnimationFrame(() => this.animate());
1850
+ // κ²Œμž„μ΄ μ‹œμž‘λ˜μ§€ μ•Šμ•˜μœΌλ©΄ λ Œλ”λ§λ§Œ μˆ˜ν–‰
1851
+ if (!this.isStarted) {
1852
+ this.renderer.render(this.scene, this.camera);
1853
+ return;
1854
+ }
1855
 
1856
+ const currentTime = performance.now();
1857
+ const deltaTime = (currentTime - this.lastTime) / 1000;
1858
+ this.lastTime = currentTime;
1859
 
1860
+ if (!this.isLoading) {
1861
+ this.handleMovement();
1862
+ this.tank.update(this.mouse.x, this.mouse.y, this.scene);
1863
+
1864
+ const tankPosition = this.tank.getPosition();
1865
+ this.enemies.forEach(enemy => {
1866
+ enemy.update(tankPosition);
1867
+
1868
+ if (enemy.isLoaded && enemy.mesh.position.distanceTo(tankPosition) < ENEMY_CONFIG.ATTACK_RANGE) {
1869
+ enemy.shoot(tankPosition);
1870
+ }
1871
+ });
 
 
 
1872
 
1873
+ this.updateParticles();
1874
+ this.checkCollisions();
1875
+ this.updateUI();
1876
+ this.updateRadar(); // λ ˆμ΄λ” μ—…λ°μ΄νŠΈ μΆ”κ°€
1877
+ }
1878
+
1879
+ this.renderer.render(this.scene, this.camera);
1880
+ }
1881
  }
1882
 
1883
  // Start game