cutechicken commited on
Commit
cf9ce80
β€’
1 Parent(s): 9f7f3e1

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +540 -178
game.js CHANGED
@@ -582,17 +582,9 @@ class Enemy {
582
  this.alternativePath = null;
583
  this.pathFindingTimeout = 0;
584
  this.lastPathUpdateTime = 0;
585
- this.pathUpdateInterval = 1000;
586
  this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
587
 
588
- // 쒌우 이동 κ΄€λ ¨ 속성 μΆ”κ°€
589
- this.movement = {
590
- direction: null, // 'left' λ˜λŠ” 'right'
591
- lastDirectionChange: 0, // λ§ˆμ§€λ§‰ λ°©ν–₯ λ³€κ²½ μ‹œκ°„
592
- directionDuration: 3000, // λ°©ν–₯ μœ μ§€ μ‹œκ°„ (3초)
593
- strafeSpeed: 0.15 // 쒌우 이동 속도
594
- };
595
-
596
  // AI μƒνƒœ 관리
597
  this.aiState = {
598
  mode: 'pursue',
@@ -607,7 +599,7 @@ class Enemy {
607
  currentRotation: 0,
608
  isAiming: false,
609
  aimingTime: 0,
610
- requiredAimTime: 1000
611
  };
612
 
613
  // 경둜 탐색 및 νšŒν”Ό μ‹œμŠ€ν…œ
@@ -619,9 +611,9 @@ class Enemy {
619
  avoidanceDirection: null,
620
  obstacleCheckDistance: 10,
621
  avoidanceTime: 0,
622
- maxAvoidanceTime: 3000,
623
- sensorAngles: [-45, 0, 45],
624
- sensorDistance: 15
625
  };
626
 
627
  // μ „νˆ¬ μ‹œμŠ€ν…œ
@@ -629,40 +621,18 @@ class Enemy {
629
  minEngagementRange: 30,
630
  maxEngagementRange: 150,
631
  optimalRange: 80,
632
- aimThreshold: 0.1,
633
  lastShotAccuracy: 0,
634
  consecutiveHits: 0,
635
  maxConsecutiveHits: 3
636
  };
637
  }
638
 
639
- async initialize(loader) {
640
- try {
641
- const modelPath = this.type === 'tank' ? '/models/t90.glb' : '/models/t90.glb';
642
- const result = await loader.loadAsync(modelPath);
643
- this.mesh = result.scene;
644
- this.mesh.position.copy(this.position);
645
- this.mesh.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
646
-
647
- this.mesh.traverse((child) => {
648
- if (child.isMesh) {
649
- child.castShadow = true;
650
- child.receiveShadow = true;
651
- }
652
- });
653
-
654
- this.scene.add(this.mesh);
655
- this.isLoaded = true;
656
- } catch (error) {
657
- console.error('Error loading enemy model:', error);
658
- this.isLoaded = false;
659
- }
660
- }
661
-
662
  detectObstacles() {
663
  const obstacles = [];
664
  const position = this.mesh.position.clone();
665
- position.y += 1;
666
 
667
  this.pathfinding.sensorAngles.forEach(angle => {
668
  const direction = new THREE.Vector3(0, 0, 1)
@@ -683,165 +653,277 @@ class Enemy {
683
 
684
  return obstacles;
685
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  }
687
- // 메인 μ—…λ°μ΄νŠΈ λ©”μ„œλ“œ
688
- update(playerPosition) {
689
- if (!this.mesh || !this.isLoaded) return;
690
 
691
- const currentTime = Date.now();
 
692
  const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
693
- const hasLineOfSight = this.checkLineOfSight(playerPosition);
694
 
695
- // λ°©ν–₯ κ²°μ • 둜직
696
- if (!this.movement.direction ||
697
- currentTime - this.movement.lastDirectionChange > this.movement.directionDuration) {
698
- this.movement.direction = Math.random() < 0.5 ? 'left' : 'right';
699
- this.movement.lastDirectionChange = currentTime;
 
 
 
 
 
 
 
 
 
 
700
  }
701
 
702
- if (hasLineOfSight && distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE) {
703
- // ν”Œλ ˆμ΄μ–΄λ₯Ό ν–₯ν•œ λ°©ν–₯ 벑터
704
- const directionToPlayer = new THREE.Vector3()
705
- .subVectors(playerPosition, this.mesh.position)
706
- .normalize();
707
 
708
- // 쒌우 이동을 μœ„ν•œ 수직 벑터 계산
709
- const sideVector = new THREE.Vector3()
710
- .crossVectors(directionToPlayer, new THREE.Vector3(0, 1, 0))
711
- .normalize();
 
712
 
713
- // μ„ νƒλœ λ°©ν–₯으둜 이동
714
- if (this.movement.direction === 'left') {
715
- this.mesh.position.add(sideVector.multiplyScalar(this.movement.strafeSpeed));
716
- } else {
717
- this.mesh.position.add(sideVector.multiplyScalar(-this.movement.strafeSpeed));
718
- }
719
 
720
- // ν”Œλ ˆμ΄μ–΄λ₯Ό ν–₯ν•΄ νšŒμ „
721
- const targetRotation = Math.atan2(directionToPlayer.x, directionToPlayer.z);
722
- this.mesh.rotation.y = this.smoothRotation(this.mesh.rotation.y, targetRotation, 0.1);
723
- }
 
 
724
 
725
- // μ „νˆ¬ 거리 μ‘°μ •
726
- const combatMove = this.maintainCombatDistance(playerPosition);
727
- if (combatMove.length() > 0) {
728
- this.mesh.position.add(combatMove.multiplyScalar(this.moveSpeed));
 
 
 
 
 
 
729
  }
 
 
730
 
731
- // 곡격 처리
732
- if (hasLineOfSight && distanceToPlayer <= ENEMY_CONFIG.ATTACK_RANGE && this.canShoot(playerPosition)) {
733
- this.shoot(playerPosition);
 
 
 
 
 
 
 
 
 
 
 
 
 
734
  }
 
 
 
 
 
 
 
735
 
736
- // μ΄μ•Œ μ—…λ°μ΄νŠΈ
737
- this.updateBullets();
 
 
 
 
738
 
739
- // 탱크 기울기 μ‘°μ •
740
- this.adjustTankTilt();
741
- }
 
 
 
 
 
 
 
742
 
743
- shoot(playerPosition) {
744
- const currentTime = Date.now();
745
- const attackInterval = this.type === 'tank' ?
746
- ENEMY_CONFIG.ATTACK_INTERVAL :
747
- ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
748
 
749
- if (currentTime - this.lastAttackTime < attackInterval) return;
 
 
 
 
 
 
 
 
750
 
751
- // ν”Œλ ˆμ΄μ–΄μ™€μ˜ λ°©ν–₯ 차이 계산
752
  const directionToPlayer = new THREE.Vector3()
753
  .subVectors(playerPosition, this.mesh.position)
754
  .normalize();
755
- const forwardDirection = new THREE.Vector3(0, 0, 1)
756
- .applyQuaternion(this.mesh.quaternion)
757
- .normalize();
758
 
759
- const dotProduct = forwardDirection.dot(directionToPlayer);
760
- const angleToPlayer = Math.acos(dotProduct);
 
 
 
761
 
762
- // 일정 각도 μ΄ν•˜μΌ κ²½μš°μ—λ§Œ 곡격
763
- const attackAngleThreshold = Math.PI / 8;
764
- if (angleToPlayer > attackAngleThreshold) return;
 
765
 
766
- this.createMuzzleFlash();
767
-
768
- const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8);
769
- const bulletMaterial = new THREE.MeshBasicMaterial({
770
- color: 0xff0000,
771
- emissive: 0xff0000,
772
- emissiveIntensity: 0.5
773
- });
774
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
775
 
776
- const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
777
- const muzzlePosition = new THREE.Vector3();
778
- this.mesh.getWorldPosition(muzzlePosition);
779
- muzzleOffset.applyQuaternion(this.mesh.quaternion);
780
- muzzlePosition.add(muzzleOffset);
781
-
782
- bullet.position.copy(muzzlePosition);
783
- bullet.quaternion.copy(this.mesh.quaternion);
784
-
785
- const direction = new THREE.Vector3()
786
- .subVectors(playerPosition, muzzlePosition)
787
- .normalize();
788
-
789
- const bulletSpeed = this.type === 'tank' ?
790
- ENEMY_CONFIG.BULLET_SPEED :
791
- ENEMY_CONFIG.BULLET_SPEED * 0.8;
792
-
793
- bullet.velocity = direction.multiplyScalar(bulletSpeed);
794
-
795
- this.scene.add(bullet);
796
- this.bullets.push(bullet);
797
- this.lastAttackTime = currentTime;
798
 
799
- // λ°œμ‚¬μŒ 효과
800
- const enemyFireSound = new Audio('sounds/mbtfire5.ogg');
801
- enemyFireSound.volume = 0.3;
802
- enemyFireSound.play();
803
- }
804
 
805
- createMuzzleFlash() {
806
- if (!this.mesh) return;
807
-
808
- const flashGroup = new THREE.Group();
809
- const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8);
810
- const flameMaterial = new THREE.MeshBasicMaterial({
811
- color: 0xffa500,
812
- transparent: true,
813
- opacity: 0.8
814
- });
815
- const flame = new THREE.Mesh(flameGeometry, flameMaterial);
816
- flame.scale.set(2, 2, 3);
817
- flashGroup.add(flame);
818
 
819
- const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
820
- const muzzlePosition = new THREE.Vector3();
821
- const meshWorldQuaternion = new THREE.Quaternion();
822
-
823
- this.mesh.getWorldPosition(muzzlePosition);
824
- this.mesh.getWorldQuaternion(meshWorldQuaternion);
825
-
826
- muzzleOffset.applyQuaternion(meshWorldQuaternion);
827
- muzzlePosition.add(muzzleOffset);
828
-
829
- flashGroup.position.copy(muzzlePosition);
830
- flashGroup.quaternion.copy(meshWorldQuaternion);
831
 
832
- this.scene.add(flashGroup);
 
833
 
834
- setTimeout(() => {
835
- this.scene.remove(flashGroup);
836
- }, 500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  }
 
 
 
 
 
 
 
 
 
 
838
  }
839
- // λΆ€κ°€ κΈ°λŠ₯ λ©”μ„œλ“œλ“€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840
  checkLineOfSight(playerPosition) {
841
  if (!this.mesh) return false;
842
 
843
  const startPos = this.mesh.position.clone();
844
- startPos.y += 2;
845
  const direction = new THREE.Vector3()
846
  .subVectors(playerPosition, startPos)
847
  .normalize();
@@ -850,37 +932,178 @@ class Enemy {
850
  const raycaster = new THREE.Raycaster(startPos, direction, 0, distance);
851
  const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
852
 
 
853
  return intersects.length === 0;
854
  }
 
 
 
 
 
 
855
 
856
- maintainCombatDistance(playerPosition) {
857
- const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
858
- let moveDirection = new THREE.Vector3();
859
-
860
- if (distanceToPlayer < this.combat.minEngagementRange) {
861
- moveDirection.subVectors(this.mesh.position, playerPosition).normalize();
862
- } else if (distanceToPlayer > this.combat.maxEngagementRange) {
863
- moveDirection.subVectors(playerPosition, this.mesh.position).normalize();
864
- } else if (Math.abs(distanceToPlayer - this.combat.optimalRange) > 10) {
865
- const targetDistance = this.combat.optimalRange;
866
- moveDirection.subVectors(playerPosition, this.mesh.position).normalize();
867
- if (distanceToPlayer > targetDistance) {
868
- moveDirection.multiplyScalar(1);
869
- } else {
870
- moveDirection.multiplyScalar(-1);
871
- }
 
 
 
 
 
 
 
 
872
  }
873
 
874
- return moveDirection;
 
 
 
 
 
 
 
 
875
  }
876
 
 
877
  smoothRotation(current, target, factor) {
878
  let delta = target - current;
 
 
879
  while (delta > Math.PI) delta -= Math.PI * 2;
880
  while (delta < -Math.PI) delta += Math.PI * 2;
 
881
  return current + delta * factor;
882
  }
883
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
  updateBullets() {
885
  for (let i = this.bullets.length - 1; i >= 0; i--) {
886
  const bullet = this.bullets[i];
@@ -905,6 +1128,145 @@ class Enemy {
905
  }
906
  }
907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
 
909
  // Particle 클래슀
910
  class Particle {
 
582
  this.alternativePath = null;
583
  this.pathFindingTimeout = 0;
584
  this.lastPathUpdateTime = 0;
585
+ this.pathUpdateInterval = 1000; // 1μ΄ˆλ§ˆλ‹€ 경둜 μ—…λ°μ΄νŠΈ
586
  this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
587
 
 
 
 
 
 
 
 
 
588
  // AI μƒνƒœ 관리
589
  this.aiState = {
590
  mode: 'pursue',
 
599
  currentRotation: 0,
600
  isAiming: false,
601
  aimingTime: 0,
602
+ requiredAimTime: 1000 // 쑰쀀에 ν•„μš”ν•œ μ‹œκ°„
603
  };
604
 
605
  // 경둜 탐색 및 νšŒν”Ό μ‹œμŠ€ν…œ
 
611
  avoidanceDirection: null,
612
  obstacleCheckDistance: 10,
613
  avoidanceTime: 0,
614
+ maxAvoidanceTime: 3000, // μ΅œλŒ€ νšŒν”Ό μ‹œκ°„
615
+ sensorAngles: [-45, 0, 45], // μ „λ°© 감지 각도
616
+ sensorDistance: 15 // 감지 거리
617
  };
618
 
619
  // μ „νˆ¬ μ‹œμŠ€ν…œ
 
621
  minEngagementRange: 30,
622
  maxEngagementRange: 150,
623
  optimalRange: 80,
624
+ aimThreshold: 0.1, // μ‘°μ€€ 정확도 μž„κ³„κ°’
625
  lastShotAccuracy: 0,
626
  consecutiveHits: 0,
627
  maxConsecutiveHits: 3
628
  };
629
  }
630
 
631
+ // μž₯μ• λ¬Ό 감지 μ‹œμŠ€ν…œ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  detectObstacles() {
633
  const obstacles = [];
634
  const position = this.mesh.position.clone();
635
+ position.y += 1; // μ„Όμ„œ 높이 μ‘°μ •
636
 
637
  this.pathfinding.sensorAngles.forEach(angle => {
638
  const direction = new THREE.Vector3(0, 0, 1)
 
653
 
654
  return obstacles;
655
  }
656
+
657
+ // νšŒν”Ό λ°©ν–₯ 계산
658
+ calculateAvoidanceDirection(obstacles) {
659
+ if (obstacles.length === 0) return null;
660
+
661
+ // λͺ¨λ“  μž₯μ• λ¬Όμ˜ λ°©ν–₯을 κ³ λ €ν•˜μ—¬ 졜적의 νšŒν”Ό λ°©ν–₯ 계산
662
+ const avoidanceVector = new THREE.Vector3();
663
+ obstacles.forEach(obstacle => {
664
+ const avoidDir = new THREE.Vector3()
665
+ .subVectors(this.mesh.position, obstacle.point)
666
+ .normalize()
667
+ .multiplyScalar(1 / obstacle.distance); // 거리에 λ°˜λΉ„λ‘€ν•˜λŠ” κ°€μ€‘μΉ˜
668
+ avoidanceVector.add(avoidDir);
669
+ });
670
+
671
+ return avoidanceVector.normalize();
672
+ }
673
+
674
+ // μ‘°μ€€ μ‹œμŠ€ν…œ
675
+ updateAiming(playerPosition) {
676
+ const targetDirection = new THREE.Vector3()
677
+ .subVectors(playerPosition, this.mesh.position)
678
+ .normalize();
679
+
680
+ // λͺ©ν‘œ νšŒμ „κ° 계산
681
+ this.aiState.targetRotation = Math.atan2(targetDirection.x, targetDirection.z);
682
+
683
+ // ν˜„μž¬ νšŒμ „κ° λΆ€λ“œλŸ½κ²Œ μ‘°μ • - μ„ νšŒ 속도λ₯Ό 느리게 ν•˜κΈ° μœ„ν•΄ μ‘°μ •
684
+ const rotationDiff = this.aiState.targetRotation - this.aiState.currentRotation;
685
+ let rotationStep = Math.sign(rotationDiff) * Math.min(Math.abs(rotationDiff), 0.05); // κΈ°μ‘΄ 0.02μ—μ„œ 0.05둜 μˆ˜μ •
686
+ this.aiState.currentRotation += rotationStep;
687
+
688
+ // λ©”μ‹œ νšŒμ „ 적용
689
+ this.mesh.rotation.y = this.aiState.currentRotation;
690
+
691
+ // μ‘°μ€€ 정확도 계산
692
+ const aimAccuracy = 1 - Math.abs(rotationDiff) / Math.PI;
693
+ return aimAccuracy > this.combat.aimThreshold;
694
  }
 
 
 
695
 
696
+ // μ „νˆ¬ 거리 관리
697
+ maintainCombatDistance(playerPosition) {
698
  const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
699
+ let moveDirection = new THREE.Vector3();
700
 
701
+ if (distanceToPlayer < this.combat.minEngagementRange) {
702
+ // λ„ˆλ¬΄ κ°€κΉŒμš°λ©΄ 후진
703
+ moveDirection.subVectors(this.mesh.position, playerPosition).normalize();
704
+ } else if (distanceToPlayer > this.combat.maxEngagementRange) {
705
+ // λ„ˆλ¬΄ λ©€λ©΄ 전진
706
+ moveDirection.subVectors(playerPosition, this.mesh.position).normalize();
707
+ } else if (Math.abs(distanceToPlayer - this.combat.optimalRange) > 10) {
708
+ // 졜적 거리둜 μ‘°μ •
709
+ const targetDistance = this.combat.optimalRange;
710
+ moveDirection.subVectors(playerPosition, this.mesh.position).normalize();
711
+ if (distanceToPlayer > targetDistance) {
712
+ moveDirection.multiplyScalar(1);
713
+ } else {
714
+ moveDirection.multiplyScalar(-1);
715
+ }
716
  }
717
 
718
+ return moveDirection;
719
+ }
 
 
 
720
 
721
+ // λ°œμ‚¬ 쑰건 확인
722
+ canShoot(playerPosition) {
723
+ const distance = this.mesh.position.distanceTo(playerPosition);
724
+ const hasLineOfSight = this.checkLineOfSight(playerPosition);
725
+ const isAimed = this.updateAiming(playerPosition);
726
 
727
+ return distance <= this.combat.maxEngagementRange &&
728
+ distance >= this.combat.minEngagementRange &&
729
+ hasLineOfSight &&
730
+ isAimed;
731
+ }
 
732
 
733
+ // 메인 μ—…λ°μ΄νŠΈ ν•¨μˆ˜
734
+ update(playerPosition) {
735
+ if (!this.mesh || !this.isLoaded) return;
736
+
737
+ // AI μƒνƒœ μ—…λ°μ΄νŠΈ
738
+ this.updateAIState(playerPosition);
739
 
740
+ // μž₯μ• λ¬Ό 감지 및 μ‹œμ•Ό 체크
741
+ const obstacles = this.detectObstacles();
742
+ const currentTime = Date.now();
743
+ const hasLineOfSight = this.checkLineOfSight(playerPosition);
744
+ const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
745
+
746
+ // 경둜 μ—…λ°μ΄νŠΈ μ£ΌκΈ° 체크
747
+ if (currentTime - this.lastPathUpdateTime > this.pathUpdateInterval) {
748
+ if (!hasLineOfSight) {
749
+ this.alternativePath = this.findAlternativePath(playerPosition);
750
  }
751
+ this.lastPathUpdateTime = currentTime;
752
+ }
753
 
754
+ // μž₯μ• λ¬Ό νšŒν”Ό 둜직
755
+ if (obstacles.length > 0 && !this.pathfinding.isAvoidingObstacle) {
756
+ this.pathfinding.isAvoidingObstacle = true;
757
+ this.pathfinding.avoidanceDirection = this.calculateAvoidanceDirection(obstacles);
758
+ this.pathfinding.avoidanceTime = 0;
759
+ }
760
+
761
+ // 이동 둜직 μ‹€ν–‰
762
+ if (this.pathfinding.isAvoidingObstacle) {
763
+ // νšŒν”Ό λ™μž‘
764
+ this.pathfinding.avoidanceTime += 16;
765
+ if (this.pathfinding.avoidanceTime >= this.pathfinding.maxAvoidanceTime) {
766
+ this.pathfinding.isAvoidingObstacle = false;
767
+ } else {
768
+ const avoidMove = this.pathfinding.avoidanceDirection.multiplyScalar(this.moveSpeed);
769
+ this.mesh.position.add(avoidMove);
770
  }
771
+ } else if (!hasLineOfSight) {
772
+ // μ‹œμ•Όκ°€ 없을 λ•Œμ˜ 이동
773
+ if (this.alternativePath) {
774
+ const pathDirection = new THREE.Vector3()
775
+ .subVectors(this.alternativePath, this.mesh.position)
776
+ .normalize();
777
+ this.mesh.position.add(pathDirection.multiplyScalar(this.moveSpeed));
778
 
779
+ const targetRotation = Math.atan2(pathDirection.x, pathDirection.z);
780
+ this.mesh.rotation.y = this.smoothRotation(this.mesh.rotation.y, targetRotation, 0.1);
781
+ }
782
+ } else {
783
+ // μ‹œμ•Όκ°€ μžˆμ„ λ•Œμ˜ 이동
784
+ this.alternativePath = null;
785
 
786
+ // AI μƒνƒœμ— λ”°λ₯Έ 이동
787
+ switch (this.aiState.mode) {
788
+ case 'pursue':
789
+ if (distanceToPlayer > ENEMY_CONFIG.ATTACK_RANGE * 0.7) {
790
+ const moveDirection = new THREE.Vector3()
791
+ .subVectors(playerPosition, this.mesh.position)
792
+ .normalize();
793
+ this.mesh.position.add(moveDirection.multiplyScalar(this.moveSpeed));
794
+ }
795
+ break;
796
 
797
+ case 'flank':
798
+ const flankPosition = this.calculateFlankPosition(playerPosition);
799
+ this.findPathToTarget(flankPosition);
800
+ this.moveAlongPath();
801
+ break;
802
 
803
+ case 'retreat':
804
+ if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE * 0.3) {
805
+ const retreatDirection = new THREE.Vector3()
806
+ .subVectors(this.mesh.position, playerPosition)
807
+ .normalize();
808
+ this.mesh.position.add(retreatDirection.multiplyScalar(this.moveSpeed));
809
+ }
810
+ break;
811
+ }
812
 
813
+ // ν”Œλ ˆμ΄μ–΄ λ°©ν–₯으둜 νšŒμ „
814
  const directionToPlayer = new THREE.Vector3()
815
  .subVectors(playerPosition, this.mesh.position)
816
  .normalize();
817
+ const targetRotation = Math.atan2(directionToPlayer.x, directionToPlayer.z);
818
+ this.mesh.rotation.y = this.smoothRotation(this.mesh.rotation.y, targetRotation, 0.1);
819
+ }
820
 
821
+ // μ „νˆ¬ 거리 μ‘°μ •
822
+ const combatMove = this.maintainCombatDistance(playerPosition);
823
+ if (combatMove.length() > 0) {
824
+ this.mesh.position.add(combatMove.multiplyScalar(this.moveSpeed));
825
+ }
826
 
827
+ // 곡격 처리
828
+ if (hasLineOfSight && distanceToPlayer <= ENEMY_CONFIG.ATTACK_RANGE && this.canShoot(playerPosition)) {
829
+ this.shoot(playerPosition);
830
+ }
831
 
832
+ // μ΄μ•Œ μ—…λ°μ΄νŠΈ
833
+ this.updateBullets();
 
 
 
 
 
 
 
834
 
835
+ // 탱크 기울기 μ‘°μ •
836
+ this.adjustTankTilt();
837
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
838
 
839
+ checkLineOfSight(targetPosition) {
840
+ if (!this.mesh) return false;
 
 
 
841
 
842
+ const startPos = this.mesh.position.clone();
843
+ startPos.y += 2; // 포탑 높이
844
+ const direction = new THREE.Vector3()
845
+ .subVectors(targetPosition, startPos)
846
+ .normalize();
847
+ const distance = startPos.distanceTo(targetPosition);
 
 
 
 
 
 
 
848
 
849
+ const raycaster = new THREE.Raycaster(startPos, direction, 0, distance);
850
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
 
 
 
 
 
 
 
 
 
 
851
 
852
+ return intersects.length === 0;
853
+ }
854
 
855
+ findAlternativePath(playerPosition) {
856
+ const currentPos = this.mesh.position.clone();
857
+ const directionToPlayer = new THREE.Vector3()
858
+ .subVectors(playerPosition, currentPos)
859
+ .normalize();
860
+
861
+ // 쒌우 90도 λ°©ν–₯ 계산
862
+ const leftDirection = new THREE.Vector3()
863
+ .copy(directionToPlayer)
864
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
865
+ const rightDirection = new THREE.Vector3()
866
+ .copy(directionToPlayer)
867
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2);
868
+
869
+ // 쒌우 30λ―Έν„° 지점 확인
870
+ const checkDistance = 30;
871
+ const leftPoint = currentPos.clone().add(leftDirection.multiplyScalar(checkDistance));
872
+ const rightPoint = currentPos.clone().add(rightDirection.multiplyScalar(checkDistance));
873
+
874
+ // 각 λ°©ν–₯의 μž₯μ• λ¬Ό 체크
875
+ const leftClear = this.checkPathClear(currentPos, leftPoint);
876
+ const rightClear = this.checkPathClear(currentPos, rightPoint);
877
+
878
+ if (leftClear && rightClear) {
879
+ // λ‘˜ λ‹€ κ°€λŠ₯ν•˜λ©΄ 랜덀 선택
880
+ return Math.random() < 0.5 ? leftPoint : rightPoint;
881
+ } else if (leftClear) {
882
+ return leftPoint;
883
+ } else if (rightClear) {
884
+ return rightPoint;
885
  }
886
+
887
+ return null;
888
+ }
889
+
890
+ checkPathClear(start, end) {
891
+ const direction = new THREE.Vector3().subVectors(end, start).normalize();
892
+ const distance = start.distanceTo(end);
893
+ const raycaster = new THREE.Raycaster(start, direction, 0, distance);
894
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
895
+ return intersects.length === 0;
896
  }
897
+
898
+ async initialize(loader) {
899
+ try {
900
+ const modelPath = this.type === 'tank' ? '/models/t90.glb' : '/models/t90.glb';
901
+ const result = await loader.loadAsync(modelPath);
902
+ this.mesh = result.scene;
903
+ this.mesh.position.copy(this.position);
904
+ this.mesh.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
905
+
906
+ this.mesh.traverse((child) => {
907
+ if (child.isMesh) {
908
+ child.castShadow = true;
909
+ child.receiveShadow = true;
910
+ }
911
+ });
912
+
913
+ this.scene.add(this.mesh);
914
+ this.isLoaded = true;
915
+ } catch (error) {
916
+ console.error('Error loading enemy model:', error);
917
+ this.isLoaded = false;
918
+ }
919
+ }
920
+
921
+ // μ‹œμ•Ό 확인 λ©”μ„œλ“œ (κΈ°μ‘΄ μ½”λ“œ μˆ˜μ •)
922
  checkLineOfSight(playerPosition) {
923
  if (!this.mesh) return false;
924
 
925
  const startPos = this.mesh.position.clone();
926
+ startPos.y += 2; // 포탑 높이
927
  const direction = new THREE.Vector3()
928
  .subVectors(playerPosition, startPos)
929
  .normalize();
 
932
  const raycaster = new THREE.Raycaster(startPos, direction, 0, distance);
933
  const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
934
 
935
+ // μž₯μ• λ¬Όκ³Όμ˜ 좩돌이 μžˆλŠ”μ§€ 확인
936
  return intersects.length === 0;
937
  }
938
+ // λŒ€μ²΄ 경둜 μ°ΎκΈ° λ©”μ„œλ“œ
939
+ findAlternativePath(playerPosition) {
940
+ const currentPos = this.mesh.position.clone();
941
+ const directionToPlayer = new THREE.Vector3()
942
+ .subVectors(playerPosition, currentPos)
943
+ .normalize();
944
 
945
+ // 쒌우 90도 λ°©ν–₯ 계산
946
+ const leftDirection = new THREE.Vector3()
947
+ .copy(directionToPlayer)
948
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
949
+ const rightDirection = new THREE.Vector3()
950
+ .copy(directionToPlayer)
951
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2);
952
+
953
+ // 쒌우 30λ―Έν„° 지점 확인
954
+ const checkDistance = 30;
955
+ const leftPoint = currentPos.clone().add(leftDirection.multiplyScalar(checkDistance));
956
+ const rightPoint = currentPos.clone().add(rightDirection.multiplyScalar(checkDistance));
957
+
958
+ // 각 λ°©ν–₯의 μž₯μ• λ¬Ό 체크
959
+ const leftClear = this.checkPathClear(currentPos, leftPoint);
960
+ const rightClear = this.checkPathClear(currentPos, rightPoint);
961
+
962
+ if (leftClear && rightClear) {
963
+ // λ‘˜ λ‹€ κ°€λŠ₯ν•˜λ©΄ 랜덀 선택
964
+ return Math.random() < 0.5 ? leftPoint : rightPoint;
965
+ } else if (leftClear) {
966
+ return leftPoint;
967
+ } else if (rightClear) {
968
+ return rightPoint;
969
  }
970
 
971
+ return null;
972
+ }
973
+ // 경둜 μœ νš¨μ„± 확인
974
+ checkPathClear(start, end) {
975
+ const direction = new THREE.Vector3().subVectors(end, start).normalize();
976
+ const distance = start.distanceTo(end);
977
+ const raycaster = new THREE.Raycaster(start, direction, 0, distance);
978
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
979
+ return intersects.length === 0;
980
  }
981
 
982
+ // λΆ€λ“œλŸ¬μš΄ νšŒμ „ 처리
983
  smoothRotation(current, target, factor) {
984
  let delta = target - current;
985
+
986
+ // 각도 차이λ₯Ό -PIμ—μ„œ PI μ‚¬μ΄λ‘œ μ •κ·œν™”
987
  while (delta > Math.PI) delta -= Math.PI * 2;
988
  while (delta < -Math.PI) delta += Math.PI * 2;
989
+
990
  return current + delta * factor;
991
  }
992
 
993
+
994
+ updateAIState(playerPosition) {
995
+ const currentTime = Date.now();
996
+ const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
997
+
998
+ if (currentTime - this.aiState.lastVisibilityCheck > this.aiState.visibilityCheckInterval) {
999
+ this.aiState.canSeePlayer = this.checkLineOfSight(playerPosition);
1000
+ this.aiState.lastVisibilityCheck = currentTime;
1001
+
1002
+ if (this.aiState.canSeePlayer) {
1003
+ this.aiState.lastKnownPlayerPosition = playerPosition.clone();
1004
+ this.aiState.searchStartTime = null;
1005
+ }
1006
+ }
1007
+ // μƒνƒœ λ³€κ²½ μΏ¨λ‹€μš΄μ„ 2초둜 μ„€μ •
1008
+ const stateChangeCooldown = 2000;
1009
+
1010
+ if (currentTime - this.aiState.lastStateChange > this.aiState.stateChangeCooldown) {
1011
+ if (this.health < 30) {
1012
+ this.aiState.mode = 'retreat';
1013
+ } else if (distanceToPlayer < 30 && this.aiState.canSeePlayer) {
1014
+ this.aiState.mode = 'flank';
1015
+ } else {
1016
+ this.aiState.mode = 'pursue';
1017
+ }
1018
+ this.aiState.lastStateChange = currentTime;
1019
+ }
1020
+ }
1021
+
1022
+ findPathToTarget(targetPosition) {
1023
+ const currentTime = Date.now();
1024
+ if (currentTime - this.pathfinding.lastPathUpdate < this.pathfinding.pathUpdateInterval) {
1025
+ return;
1026
+ }
1027
+
1028
+ this.pathfinding.currentPath = this.generatePathPoints(this.mesh.position.clone(), targetPosition);
1029
+ this.pathfinding.lastPathUpdate = currentTime;
1030
+ }
1031
+
1032
+ generatePathPoints(start, end) {
1033
+ const points = [];
1034
+ const direction = new THREE.Vector3().subVectors(end, start).normalize();
1035
+ const distance = start.distanceTo(end);
1036
+ const steps = Math.ceil(distance / 10);
1037
+
1038
+ for (let i = 0; i <= steps; i++) {
1039
+ const point = start.clone().add(direction.multiplyScalar(i * 10));
1040
+ points.push(point);
1041
+ }
1042
+
1043
+ return points;
1044
+ }
1045
+
1046
+ moveAlongPath() {
1047
+ if (this.pathfinding.currentPath.length === 0) return;
1048
+
1049
+ const targetPoint = this.pathfinding.currentPath[0];
1050
+ const direction = new THREE.Vector3()
1051
+ .subVectors(targetPoint, this.mesh.position)
1052
+ .normalize();
1053
+
1054
+ const moveVector = direction.multiplyScalar(this.moveSpeed);
1055
+ this.mesh.position.add(moveVector);
1056
+
1057
+ if (this.mesh.position.distanceTo(targetPoint) < 2) {
1058
+ this.pathfinding.currentPath.shift();
1059
+ }
1060
+ }
1061
+
1062
+ calculateFlankPosition(playerPosition) {
1063
+ const angle = Math.random() * Math.PI * 2;
1064
+ const radius = 40;
1065
+ return new THREE.Vector3(
1066
+ playerPosition.x + Math.cos(angle) * radius,
1067
+ playerPosition.y,
1068
+ playerPosition.z + Math.sin(angle) * radius
1069
+ );
1070
+ }
1071
+
1072
+ calculateRetreatPosition(playerPosition) {
1073
+ const direction = new THREE.Vector3()
1074
+ .subVectors(this.mesh.position, playerPosition)
1075
+ .normalize();
1076
+ return this.mesh.position.clone().add(direction.multiplyScalar(50));
1077
+ }
1078
+
1079
+ adjustTankTilt() {
1080
+ const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
1081
+ const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.mesh.quaternion);
1082
+
1083
+ const frontHeight = window.gameInstance.getHeightAtPosition(
1084
+ this.mesh.position.x + forwardVector.x,
1085
+ this.mesh.position.z + forwardVector.z
1086
+ );
1087
+ const backHeight = window.gameInstance.getHeightAtPosition(
1088
+ this.mesh.position.x - forwardVector.x,
1089
+ this.mesh.position.z - forwardVector.z
1090
+ );
1091
+ const rightHeight = window.gameInstance.getHeightAtPosition(
1092
+ this.mesh.position.x + rightVector.x,
1093
+ this.mesh.position.z + rightVector.z
1094
+ );
1095
+ const leftHeight = window.gameInstance.getHeightAtPosition(
1096
+ this.mesh.position.x - rightVector.x,
1097
+ this.mesh.position.z - rightVector.z
1098
+ );
1099
+
1100
+ const pitch = Math.atan2(frontHeight - backHeight, 2);
1101
+ const roll = Math.atan2(rightHeight - leftHeight, 2);
1102
+
1103
+ const currentRotation = this.mesh.rotation.y;
1104
+ this.mesh.rotation.set(pitch, currentRotation, roll);
1105
+ }
1106
+
1107
  updateBullets() {
1108
  for (let i = this.bullets.length - 1; i >= 0; i--) {
1109
  const bullet = this.bullets[i];
 
1128
  }
1129
  }
1130
 
1131
+ createMuzzleFlash() {
1132
+ if (!this.mesh) return;
1133
+
1134
+ const flashGroup = new THREE.Group();
1135
+
1136
+ const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8);
1137
+ const flameMaterial = new THREE.MeshBasicMaterial({
1138
+ color: 0xffa500,
1139
+ transparent: true,
1140
+ opacity: 0.8
1141
+ });
1142
+ const flame = new THREE.Mesh(flameGeometry, flameMaterial);
1143
+ flame.scale.set(2, 2, 3);
1144
+ flashGroup.add(flame);
1145
+
1146
+ const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8);
1147
+ const smokeMaterial = new THREE.MeshBasicMaterial({
1148
+ color: 0x555555,
1149
+ transparent: true,
1150
+ opacity: 0.5
1151
+ });
1152
+
1153
+ for (let i = 0; i < 5; i++) {
1154
+ const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial);
1155
+ smoke.position.set(
1156
+ Math.random() * 1 - 0.5,
1157
+ Math.random() * 1 - 0.5,
1158
+ -1 - Math.random()
1159
+ );
1160
+ smoke.scale.set(1.5, 1.5, 1.5);
1161
+ flashGroup.add(smoke);
1162
+ }
1163
+
1164
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
1165
+ const muzzlePosition = new THREE.Vector3();
1166
+ const meshWorldQuaternion = new THREE.Quaternion();
1167
+
1168
+ this.mesh.getWorldPosition(muzzlePosition);
1169
+ this.mesh.getWorldQuaternion(meshWorldQuaternion);
1170
+
1171
+ muzzleOffset.applyQuaternion(meshWorldQuaternion);
1172
+ muzzlePosition.add(muzzleOffset);
1173
+
1174
+ flashGroup.position.copy(muzzlePosition);
1175
+ flashGroup.quaternion.copy(meshWorldQuaternion);
1176
+
1177
+ this.scene.add(flashGroup);
1178
+
1179
+ setTimeout(() => {
1180
+ this.scene.remove(flashGroup);
1181
+ }, 500);
1182
+ }
1183
+
1184
+ shoot(playerPosition) {
1185
+ const currentTime = Date.now();
1186
+ const attackInterval = this.type === 'tank' ?
1187
+ ENEMY_CONFIG.ATTACK_INTERVAL :
1188
+ ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
1189
+
1190
+ if (currentTime - this.lastAttackTime < attackInterval) return;
1191
+
1192
+ // ν”Œλ ˆμ΄μ–΄μ™€μ˜ λ°©ν–₯ 차이 계산
1193
+ const directionToPlayer = new THREE.Vector3()
1194
+ .subVectors(playerPosition, this.mesh.position)
1195
+ .normalize();
1196
+ const forwardDirection = new THREE.Vector3(0, 0, 1)
1197
+ .applyQuaternion(this.mesh.quaternion)
1198
+ .normalize();
1199
+
1200
+ const dotProduct = forwardDirection.dot(directionToPlayer);
1201
+ const angleToPlayer = Math.acos(dotProduct);
1202
+
1203
+ // 일정 각도 μ΄ν•˜μΌ κ²½μš°μ—λ§Œ 곡격
1204
+ const attackAngleThreshold = Math.PI / 8; // μ•½ 22.5도
1205
+ if (angleToPlayer > attackAngleThreshold) return;
1206
+
1207
+ this.createMuzzleFlash();
1208
+
1209
+ const enemyFireSound = new Audio('sounds/mbtfire5.ogg');
1210
+ enemyFireSound.volume = 0.3;
1211
+ enemyFireSound.play();
1212
+
1213
+ const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8);
1214
+ const bulletMaterial = new THREE.MeshBasicMaterial({
1215
+ color: 0xff0000,
1216
+ emissive: 0xff0000,
1217
+ emissiveIntensity: 0.5
1218
+ });
1219
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
1220
+
1221
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
1222
+ const muzzlePosition = new THREE.Vector3();
1223
+ this.mesh.getWorldPosition(muzzlePosition);
1224
+ muzzleOffset.applyQuaternion(this.mesh.quaternion);
1225
+ muzzlePosition.add(muzzleOffset);
1226
+
1227
+ bullet.position.copy(muzzlePosition);
1228
+ bullet.quaternion.copy(this.mesh.quaternion);
1229
+
1230
+ const direction = new THREE.Vector3()
1231
+ .subVectors(playerPosition, muzzlePosition)
1232
+ .normalize();
1233
+
1234
+ const bulletSpeed = this.type === 'tank' ?
1235
+ ENEMY_CONFIG.BULLET_SPEED :
1236
+ ENEMY_CONFIG.BULLET_SPEED * 0.8;
1237
+
1238
+ bullet.velocity = direction.multiplyScalar(bulletSpeed);
1239
+
1240
+ const trailGeometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8);
1241
+ const trailMaterial = new THREE.MeshBasicMaterial({
1242
+ color: 0xff4444,
1243
+ transparent: true,
1244
+ opacity: 0.5
1245
+ });
1246
+
1247
+ const trail = new THREE.Mesh(trailGeometry, trailMaterial);
1248
+ trail.position.z = -1;
1249
+ bullet.add(trail);
1250
+
1251
+ this.scene.add(bullet);
1252
+ this.bullets.push(bullet);
1253
+ this.lastAttackTime = currentTime;
1254
+ }
1255
+
1256
+ takeDamage(damage) {
1257
+ this.health -= damage;
1258
+ return this.health <= 0;
1259
+ }
1260
+
1261
+ destroy() {
1262
+ if (this.mesh) {
1263
+ this.scene.remove(this.mesh);
1264
+ this.bullets.forEach(bullet => this.scene.remove(bullet));
1265
+ this.bullets = [];
1266
+ this.isLoaded = false;
1267
+ }
1268
+ }
1269
+ }
1270
 
1271
  // Particle 클래슀
1272
  class Particle {