cutechicken commited on
Commit
790485c
β€’
1 Parent(s): 2dc54b2

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +243 -325
game.js CHANGED
@@ -572,115 +572,200 @@ class Enemy {
572
  constructor(scene, position, type = 'tank') {
573
  this.scene = scene;
574
  this.position = position;
 
575
  this.type = type;
576
  this.health = type === 'tank' ? 100 : 200;
577
  this.lastAttackTime = 0;
578
  this.bullets = [];
579
  this.isLoaded = false;
580
  this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
581
-
582
- // Add new properties for body and turret
583
- this.body = null;
584
- this.turret = null;
585
- this.turretGroup = new THREE.Group();
586
  }
587
 
588
  async initialize(loader) {
589
- try {
590
- const bodyResult = await loader.loadAsync('/models/t90Body.glb');
591
- this.body = bodyResult.scene;
592
-
593
- const turretResult = await loader.loadAsync('/models/t90Turret.glb');
594
- this.turret = turretResult.scene;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
 
596
- this.turretGroup.position.y = 0.2;
597
- this.turretGroup.add(this.turret);
598
- this.body.add(this.turretGroup);
599
 
600
- this.body.position.copy(this.position);
601
- this.body.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
 
602
 
603
- // 그림자 μ„€μ • μ΅œμ ν™”
604
- this.body.traverse((child) => {
605
- if (child.isMesh) {
606
- child.castShadow = true;
607
- child.receiveShadow = false; // 그림자 λ°›κΈ° λΉ„ν™œμ„±ν™”
608
- child.material.shadowSide = THREE.FrontSide; // 그림자 μ΅œμ ν™”
609
  }
610
- });
611
 
612
- this.turret.traverse((child) => {
613
- if (child.isMesh) {
614
- child.castShadow = true;
615
- child.receiveShadow = false; // 그림자 λ°›κΈ° λΉ„ν™œμ„±ν™”
616
- child.material.shadowSide = THREE.FrontSide; // 그림자 μ΅œμ ν™”
 
 
 
 
 
617
  }
618
- });
619
-
620
- this.scene.add(this.body);
621
- this.isLoaded = true;
622
- } catch (error) {
623
- console.error('Error loading enemy tank model:', error);
624
- this.isLoaded = false;
625
- }
626
- }
627
- update(playerPosition) {
628
- if (!this.body || !this.isLoaded) return;
629
-
630
- // ν”Œλ ˆμ΄μ–΄ λ°©ν–₯으둜 이동 벑터 계산
631
- const direction = new THREE.Vector3()
632
- .subVectors(playerPosition, this.body.position)
633
- .normalize();
634
 
635
- // ν”Œλ ˆμ΄μ–΄μ™€μ˜ 거리 계산
636
- const distanceToPlayer = this.body.position.distanceTo(playerPosition);
637
- const minDistance = 50; // μ΅œμ†Œ 거리 μœ μ§€
 
 
 
638
 
639
- // μ΅œμ†Œ 거리보닀 멀리 μžˆμ„ λ•Œλ§Œ 이동
640
- if (distanceToPlayer > minDistance) {
641
- const moveVector = direction.multiplyScalar(this.moveSpeed);
642
- const newPosition = this.body.position.clone().add(moveVector);
643
 
644
- // μ§€ν˜• 높이에 맞좰 μœ„μΉ˜ μ‘°μ •
645
- const heightAtNewPos = window.gameInstance.getHeightAtPosition(
646
- newPosition.x,
647
- newPosition.z
648
- );
649
- newPosition.y = heightAtNewPos + TANK_HEIGHT;
650
-
651
- // 차체 이동
652
- this.body.position.copy(newPosition);
653
 
654
- // 이동 λ°©ν–₯으둜 차체 νšŒμ „
655
- const targetRotation = Math.atan2(direction.x, direction.z);
656
- this.body.rotation.y = targetRotation;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  }
658
-
659
- // 포탑 νšŒμ „ (항상 ν”Œλ ˆμ΄μ–΄λ₯Ό ν–₯ν•˜λ„λ‘)
660
- const turretDirection = new THREE.Vector2(
661
- playerPosition.x - this.body.position.x,
662
- playerPosition.z - this.body.position.z
 
 
 
663
  );
664
- const turretAngle = Math.atan2(turretDirection.x, turretDirection.y);
665
- this.turretGroup.rotation.y = turretAngle - this.body.rotation.y;
666
-
667
- // μ΄μ•Œ μ—…λ°μ΄νŠΈ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
  for (let i = this.bullets.length - 1; i >= 0; i--) {
669
  const bullet = this.bullets[i];
670
  bullet.position.add(bullet.velocity);
671
 
672
- // 맡 경�� 체크
673
  if (Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
674
  Math.abs(bullet.position.z) > MAP_SIZE / 2) {
675
  this.scene.remove(bullet);
676
  this.bullets.splice(i, 1);
 
 
 
 
 
 
 
 
 
 
 
 
677
  }
678
  }
679
  }
 
 
680
 
681
  shoot(playerPosition) {
682
- if (!this.turret) return;
683
-
684
  const currentTime = Date.now();
685
  const attackInterval = this.type === 'tank' ?
686
  ENEMY_CONFIG.ATTACK_INTERVAL :
@@ -688,29 +773,16 @@ class Enemy {
688
 
689
  if (currentTime - this.lastAttackTime < attackInterval) return;
690
 
691
- // μ΄μ•Œ 생성
692
- const bulletGeometry = new THREE.SphereGeometry(0.3);
693
  const bulletMaterial = new THREE.MeshBasicMaterial({
694
  color: this.type === 'tank' ? 0xff0000 : 0xff6600
695
  });
696
  const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
697
 
698
- // 포ꡬ μœ„μΉ˜ 계산
699
- const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
700
- const muzzlePosition = new THREE.Vector3();
701
- const turretWorldQuaternion = new THREE.Quaternion();
702
-
703
- this.turret.getWorldPosition(muzzlePosition);
704
- this.turret.getWorldQuaternion(turretWorldQuaternion);
705
-
706
- muzzleOffset.applyQuaternion(turretWorldQuaternion);
707
- muzzlePosition.add(muzzleOffset);
708
-
709
- bullet.position.copy(muzzlePosition);
710
 
711
- // λ°œμ‚¬ λ°©ν–₯ 계산
712
  const direction = new THREE.Vector3()
713
- .subVectors(playerPosition, muzzlePosition)
714
  .normalize();
715
 
716
  const bulletSpeed = this.type === 'tank' ?
@@ -719,11 +791,6 @@ class Enemy {
719
 
720
  bullet.velocity = direction.multiplyScalar(bulletSpeed);
721
 
722
- // λ°œμ‚¬μŒ 효과
723
- const shootSound = new Audio('sounds/enemyfire.ogg');
724
- shootSound.volume = 0.3;
725
- shootSound.play();
726
-
727
  this.scene.add(bullet);
728
  this.bullets.push(bullet);
729
  this.lastAttackTime = currentTime;
@@ -735,161 +802,14 @@ class Enemy {
735
  }
736
 
737
  destroy() {
738
- if (this.body) {
739
- this.scene.remove(this.body);
740
  this.bullets.forEach(bullet => this.scene.remove(bullet));
741
  this.bullets = [];
742
  this.isLoaded = false;
743
  }
744
  }
745
  }
746
- // 적 νƒ±ν¬μ˜ λ°œμ‚¬ μ΄νŽ™νŠΈ 생성
747
- createMuzzleFlash() {
748
- if (!this.turret) return;
749
-
750
- const flashGroup = new THREE.Group();
751
-
752
- // ν™”μ—Ό 효과
753
- const flameGeometry = new THREE.SphereGeometry(0.8);
754
- const flameMaterial = new THREE.MeshBasicMaterial({
755
- color: 0xff4500,
756
- transparent: true,
757
- opacity: 0.8
758
- });
759
- const flame = new THREE.Mesh(flameGeometry, flameMaterial);
760
- flame.scale.set(1.5, 1.5, 2);
761
- flashGroup.add(flame);
762
-
763
- // μ—°κΈ° 효과
764
- const smokeGeometry = new THREE.SphereGeometry(0.6);
765
- const smokeMaterial = new THREE.MeshBasicMaterial({
766
- color: 0x666666,
767
- transparent: true,
768
- opacity: 0.4
769
- });
770
-
771
- for (let i = 0; i < 3; i++) {
772
- const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial);
773
- smoke.position.set(
774
- Math.random() * 0.5 - 0.25,
775
- Math.random() * 0.5 - 0.25,
776
- -0.5 - Math.random()
777
- );
778
- smoke.scale.set(1.2, 1.2, 1.2);
779
- flashGroup.add(smoke);
780
- }
781
-
782
- // 포ꡬ μœ„μΉ˜ 계산
783
- const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
784
- const muzzlePosition = new THREE.Vector3();
785
- const turretWorldQuaternion = new THREE.Quaternion();
786
-
787
- this.turret.getWorldPosition(muzzlePosition);
788
- this.turret.getWorldQuaternion(turretWorldQuaternion);
789
-
790
- muzzleOffset.applyQuaternion(turretWorldQuaternion);
791
- muzzlePosition.add(muzzleOffset);
792
-
793
- flashGroup.position.copy(muzzlePosition);
794
- flashGroup.quaternion.copy(turretWorldQuaternion);
795
-
796
- this.scene.add(flashGroup);
797
-
798
- // μ΄νŽ™νŠΈ 제거
799
- setTimeout(() => {
800
- this.scene.remove(flashGroup);
801
- }, 100);
802
- }
803
-
804
- // 피격 효과 생성
805
- createHitEffect(position) {
806
- const particleCount = 10;
807
- const particles = [];
808
-
809
- for (let i = 0; i < particleCount; i++) {
810
- const geometry = new THREE.SphereGeometry(0.1);
811
- const material = new THREE.MeshBasicMaterial({
812
- color: 0xff6600,
813
- transparent: true,
814
- opacity: 1
815
- });
816
- const particle = new THREE.Mesh(geometry, material);
817
-
818
- particle.position.copy(position);
819
-
820
- const speed = Math.random() * 0.2 + 0.1;
821
- const angle = Math.random() * Math.PI * 2;
822
- const elevation = Math.random() * Math.PI - Math.PI / 2;
823
-
824
- particle.velocity = new THREE.Vector3(
825
- Math.cos(angle) * Math.cos(elevation) * speed,
826
- Math.sin(elevation) * speed,
827
- Math.sin(angle) * Math.cos(elevation) * speed
828
- );
829
-
830
- particle.gravity = -0.01;
831
- particle.life = Math.random() * 20 + 20;
832
- particle.fadeRate = 1 / particle.life;
833
-
834
- this.scene.add(particle);
835
- particles.push(particle);
836
- }
837
-
838
- // νŒŒν‹°ν΄ μ—…λ°μ΄νŠΈ 및 제거
839
- const updateParticles = () => {
840
- for (let i = particles.length - 1; i >= 0; i--) {
841
- const particle = particles[i];
842
- particle.velocity.y += particle.gravity;
843
- particle.position.add(particle.velocity);
844
- particle.material.opacity -= particle.fadeRate;
845
-
846
- if (particle.material.opacity <= 0) {
847
- this.scene.remove(particle);
848
- particles.splice(i, 1);
849
- }
850
- }
851
-
852
- if (particles.length > 0) {
853
- requestAnimationFrame(updateParticles);
854
- }
855
- };
856
-
857
- updateParticles();
858
- }
859
-
860
- // 적 탱크 AI μ—…λ°μ΄νŠΈ
861
- updateAI(playerPosition, obstacles) {
862
- if (!this.isLoaded || !this.body) return;
863
-
864
- // μž₯μ• λ¬Ό νšŒν”Ό 둜직
865
- const avoidanceForce = new THREE.Vector3();
866
- const avoidanceRadius = 20;
867
-
868
- obstacles.forEach(obstacle => {
869
- if (obstacle.userData.isCollidable) {
870
- const toObstacle = new THREE.Vector3()
871
- .subVectors(obstacle.position, this.body.position);
872
- const distance = toObstacle.length();
873
-
874
- if (distance < avoidanceRadius) {
875
- avoidanceForce.add(
876
- toObstacle.normalize().multiplyScalar(-1 * (avoidanceRadius - distance))
877
- );
878
- }
879
- }
880
- });
881
-
882
- // μ΅œμ’… 이동 λ°©ν–₯ 계산
883
- const targetDirection = new THREE.Vector3()
884
- .subVectors(playerPosition, this.body.position)
885
- .normalize();
886
-
887
- targetDirection.add(avoidanceForce.multiplyScalar(0.5));
888
- targetDirection.normalize();
889
-
890
- return targetDirection;
891
- }
892
- }
893
 
894
  // Particle 클래슀
895
  class Particle {
@@ -1158,56 +1078,56 @@ class Game {
1158
 
1159
  // λ ˆμ΄λ” μ—…λ°μ΄νŠΈ λ©”μ„œλ“œ μΆ”κ°€
1160
  updateRadar() {
1161
- const currentTime = Date.now();
1162
- if (currentTime - this.lastRadarUpdate < this.radarUpdateInterval) return;
1163
-
1164
- const radar = document.getElementById('radar');
1165
- const radarRect = radar.getBoundingClientRect();
1166
- const radarCenter = {
1167
- x: radarRect.width / 2,
1168
- y: radarRect.height / 2
1169
- };
1170
 
1171
- // κΈ°μ‘΄ 적 λ„νŠΈ 제거
1172
- const oldDots = radar.getElementsByClassName('enemy-dot');
1173
- while (oldDots[0]) {
1174
- oldDots[0].remove();
1175
- }
1176
 
1177
- // 탱크 μœ„μΉ˜ κ°€μ Έμ˜€κΈ°
1178
- const tankPos = this.tank.getPosition();
1179
 
1180
- // λͺ¨λ“  적에 λŒ€ν•΄ λ ˆμ΄λ”μ— ν‘œμ‹œ
1181
- this.enemies.forEach(enemy => {
1182
- if (!enemy.isLoaded || !enemy.body) return;
1183
 
1184
- const enemyPos = enemy.body.position;
1185
- const distance = tankPos.distanceTo(enemyPos);
1186
 
1187
- // λ ˆμ΄λ” λ²”μœ„ 내에 μžˆλŠ” 경우만 ν‘œμ‹œ
1188
- if (distance <= this.radarRange) {
1189
- // 탱크 κΈ°μ€€ μƒλŒ€ 각도 계산
1190
- const angle = Math.atan2(
1191
- enemyPos.x - tankPos.x,
1192
- enemyPos.z - tankPos.z
1193
- );
1194
 
1195
- // μƒλŒ€ 거리λ₯Ό λ ˆμ΄λ” 크기에 맞게 μŠ€μΌ€μΌλ§
1196
- const relativeDistance = distance / this.radarRange;
1197
- const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance);
1198
- const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance);
1199
-
1200
- // 적 λ„νŠΈ 생성 및 μΆ”κ°€
1201
- const dot = document.createElement('div');
1202
- dot.className = 'enemy-dot';
1203
- dot.style.left = `${dotX}px`;
1204
- dot.style.top = `${dotY}px`;
1205
- radar.appendChild(dot);
1206
- }
1207
- });
1208
 
1209
- this.lastRadarUpdate = currentTime;
1210
- }
1211
 
1212
  async addDesertDecorations() {
1213
  if (!this.obstacles) {
@@ -1919,47 +1839,45 @@ this.enemies.forEach(enemy => {
1919
  }
1920
 
1921
  animate() {
1922
- if (this.isGameOver) {
1923
- if (this.animationFrameId) {
1924
- cancelAnimationFrame(this.animationFrameId);
1925
- }
1926
- return;
1927
- }
1928
 
1929
- this.animationFrameId = requestAnimationFrame(() => this.animate());
1930
- // κ²Œμž„μ΄ μ‹œμž‘λ˜μ§€ μ•Šμ•˜μœΌλ©΄ λ Œλ”λ§λ§Œ μˆ˜ν–‰
1931
- if (!this.isStarted) {
1932
- this.renderer.render(this.scene, this.camera);
1933
- return;
1934
- }
1935
 
1936
- const currentTime = performance.now();
1937
- const deltaTime = (currentTime - this.lastTime) / 1000;
1938
- this.lastTime = currentTime;
1939
 
1940
- if (!this.isLoading) {
1941
- this.handleMovement();
1942
- this.tank.update(this.mouse.x, this.mouse.y, this.scene);
1943
-
1944
- const tankPosition = this.tank.getPosition();
1945
- this.enemies.forEach(enemy => {
1946
- if (enemy && enemy.isLoaded && enemy.body) { // body 쑴재 μ—¬λΆ€ 확인
1947
- enemy.update(tankPosition);
1948
-
1949
- const distanceToPlayer = enemy.body.position.distanceTo(tankPosition);
1950
- if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE) {
1951
- enemy.shoot(tankPosition);
1952
- }
1953
- }
1954
- });
1955
 
1956
- this.updateParticles();
1957
- this.checkCollisions();
1958
- this.updateUI();
1959
- this.updateRadar();
1960
- }
1961
-
1962
- this.renderer.render(this.scene, this.camera);
 
1963
  }
1964
 
1965
  // Start game
 
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/t90.glb' : '/models/t90.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);
603
+ this.isLoaded = false;
604
+ }
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)) {
643
+ hasCollision = true;
644
+ break;
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' ?
 
791
 
792
  bullet.velocity = direction.multiplyScalar(bulletSpeed);
793
 
 
 
 
 
 
794
  this.scene.add(bullet);
795
  this.bullets.push(bullet);
796
  this.lastAttackTime = currentTime;
 
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;
810
  }
811
  }
812
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
 
814
  // Particle 클래슀
815
  class Particle {
 
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