cutechicken commited on
Commit
375a269
β€’
1 Parent(s): b16b51c

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +381 -407
game.js CHANGED
@@ -570,6 +570,7 @@ startReload() {
570
  // Enemy 클래슀
571
  class Enemy {
572
  constructor(scene, position, type = 'tank') {
 
573
  this.scene = scene;
574
  this.position = position;
575
  this.mesh = null;
@@ -590,22 +591,25 @@ class Enemy {
590
  canSeePlayer: false,
591
  lastKnownPlayerPosition: null,
592
  searchStartTime: null,
593
- pathfindingAttempts: 0,
594
- maxPathfindingAttempts: 5,
595
- alternativePath: null
 
 
596
  };
597
 
598
- // 경둜 탐색 μ‹œμŠ€ν…œ
599
  this.pathfinding = {
600
  currentPath: [],
601
  pathUpdateInterval: 1000,
602
  lastPathUpdate: 0,
603
  isAvoidingObstacle: false,
604
  avoidanceDirection: null,
605
- obstacleCheckDistance: 15,
606
- sensorAngles: [-45, -30, -15, 0, 15, 30, 45],
607
- sensorDistance: 20,
608
- alternativeRoutes: []
 
609
  };
610
 
611
  // μ „νˆ¬ μ‹œμŠ€ν…œ
@@ -613,503 +617,473 @@ class Enemy {
613
  minEngagementRange: 30,
614
  maxEngagementRange: 150,
615
  optimalRange: 80,
616
- aimThreshold: 0.1,
617
  lastShotAccuracy: 0,
618
- turretRotation: 0,
619
- targetTurretRotation: 0,
620
- turretRotationSpeed: 0.05
621
  };
622
  }
623
 
624
- checkLineOfSight(playerPosition) {
625
- if (!this.mesh) return false;
 
 
 
626
 
627
- const startPos = this.mesh.position.clone();
628
- startPos.y += 2; // 포탑 λ†’μ΄μ—μ„œ μ‹œμž‘
629
- const direction = new THREE.Vector3()
630
- .subVectors(playerPosition, startPos)
631
- .normalize();
632
- const distance = startPos.distanceTo(playerPosition);
633
 
634
- const raycaster = new THREE.Raycaster(startPos, direction, 0, distance);
635
- const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
636
 
637
- // μž₯μ• λ¬Όκ³Όμ˜ 거리 확인
638
- if (intersects.length > 0) {
639
- const obstacleDistance = intersects[0].distance;
640
- return obstacleDistance > distance; // μž₯애물이 ν”Œλ ˆμ΄μ–΄λ³΄λ‹€ 멀리 있으면 true
641
- }
 
 
 
642
 
643
- return true; // μž₯애물이 μ—†μœΌλ©΄ μ‹œμ•Όκ°€ ν™•λ³΄λœ 것
644
  }
645
 
646
- findAlternativePath(playerPosition) {
647
- if (!this.mesh) return null;
 
648
 
649
- const currentPos = this.mesh.position.clone();
650
- const possibleRoutes = [];
 
 
 
 
 
 
 
651
 
652
- // μ—¬λŸ¬ κ°λ„λ‘œ 경둜 탐색
653
- for (let angle = 0; angle < 360; angle += 45) {
654
- const radians = angle * Math.PI / 180;
655
- const checkDistance = 30; // 탐색 거리
656
 
657
- const testPoint = new THREE.Vector3(
658
- currentPos.x + Math.cos(radians) * checkDistance,
659
- currentPos.y,
660
- currentPos.z + Math.sin(radians) * checkDistance
661
- );
662
 
663
- // ν•΄λ‹Ή μ§€μ κΉŒμ§€μ˜ 경둜 μž₯μ• λ¬Ό 체크
664
- const direction = new THREE.Vector3()
665
- .subVectors(testPoint, currentPos)
666
- .normalize();
667
- const raycaster = new THREE.Raycaster(currentPos, direction, 0, checkDistance);
668
- const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
669
 
670
- if (intersects.length === 0) {
671
- // ν•΄λ‹Ή μ§€μ μ—μ„œ ν”Œλ ˆμ΄μ–΄κΉŒμ§€μ˜ μ‹œμ•Ό 체크
672
- const toPlayerDirection = new THREE.Vector3()
673
- .subVectors(playerPosition, testPoint)
674
- .normalize();
675
- const toPlayerRaycaster = new THREE.Raycaster(testPoint, toPlayerDirection);
676
- const playerIntersects = toPlayerRaycaster.intersectObjects(window.gameInstance.obstacles, true);
677
-
678
- if (playerIntersects.length === 0) {
679
- possibleRoutes.push({
680
- point: testPoint,
681
- distance: testPoint.distanceTo(playerPosition)
682
- });
683
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
  }
685
  }
686
 
687
- // κ°€μž₯ 효율적인 경둜 선택
688
- if (possibleRoutes.length > 0) {
689
- possibleRoutes.sort((a, b) => a.distance - b.distance);
690
- return possibleRoutes[0].point;
691
- }
692
 
693
- return null;
 
 
 
 
 
 
 
 
 
694
  }
695
 
 
696
  update(playerPosition) {
697
  if (!this.mesh || !this.isLoaded) return;
698
 
699
- const currentTime = Date.now();
700
- const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
701
-
702
- // μ‹œμ•Ό 체크
703
- if (currentTime - this.aiState.lastVisibilityCheck > this.aiState.visibilityCheckInterval) {
704
- this.aiState.canSeePlayer = this.checkLineOfSight(playerPosition);
705
- this.aiState.lastVisibilityCheck = currentTime;
706
 
707
- if (this.aiState.canSeePlayer) {
708
- this.aiState.lastKnownPlayerPosition = playerPosition.clone();
709
- this.aiState.pathfindingAttempts = 0;
710
- this.aiState.alternativePath = null;
711
- }
 
 
 
712
  }
713
 
714
- // AI μƒνƒœ μ—…λ°μ΄νŠΈ
715
- if (currentTime - this.aiState.lastStateChange > this.aiState.stateChangeCooldown) {
716
- if (!this.aiState.canSeePlayer) {
717
- if (!this.aiState.alternativePath) {
718
- this.aiState.alternativePath = this.findAlternativePath(playerPosition);
719
- this.aiState.pathfindingAttempts++;
720
- }
721
  } else {
722
- if (distanceToPlayer < this.combat.minEngagementRange) {
723
- this.aiState.mode = 'retreat';
724
- } else if (distanceToPlayer > this.combat.maxEngagementRange) {
725
- this.aiState.mode = 'pursue';
726
- } else {
727
- this.aiState.mode = 'engage';
728
- }
729
  }
730
- this.aiState.lastStateChange = currentTime;
731
- }
732
-
733
- // 이동 및 μ „νˆ¬ 둜직
734
- if (this.aiState.canSeePlayer) {
735
- // 정상적인 μ „νˆ¬ 행동
736
  switch (this.aiState.mode) {
737
  case 'pursue':
738
- this.moveTowards(playerPosition);
 
739
  break;
740
- case 'retreat':
741
- this.moveAway(playerPosition);
 
 
742
  break;
743
- case 'engage':
744
- this.engageTarget(playerPosition);
 
 
745
  break;
746
  }
747
- } else {
748
- // 우회 경둜둜 이동
749
- if (this.aiState.alternativePath) {
750
- this.moveTowards(this.aiState.alternativePath);
751
- if (this.mesh.position.distanceTo(this.aiState.alternativePath) < 5) {
752
- this.aiState.alternativePath = null;
753
- }
754
- } else if (this.aiState.pathfindingAttempts < this.aiState.maxPathfindingAttempts) {
755
- this.aiState.alternativePath = this.findAlternativePath(playerPosition);
756
- this.aiState.pathfindingAttempts++;
757
- }
758
  }
759
 
760
- // 포탑 νšŒμ „ μ—…λ°μ΄νŠΈ
761
- this.updateTurretRotation(playerPosition);
 
 
 
762
 
763
- // λ°œμ‚¬ κ°€λŠ₯ μ—¬λΆ€ 확인 및 λ°œμ‚¬
764
  if (this.canShoot(playerPosition)) {
765
  this.shoot(playerPosition);
766
  }
767
 
768
  // μ΄μ•Œ μ—…λ°μ΄νŠΈ
769
  this.updateBullets();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  }
771
 
772
- moveTowards(target) {
773
- if (!this.mesh) return;
774
 
775
- const direction = new THREE.Vector3()
776
- .subVectors(target, this.mesh.position)
777
- .normalize();
 
778
 
779
- // μž₯μ• λ¬Ό 감지
780
- const obstacles = this.detectObstacles();
781
- if (obstacles.length > 0) {
782
- // νšŒν”Ό λ°©ν–₯ 계산
783
- const avoidanceDirection = this.calculateAvoidanceDirection(obstacles);
784
- if (avoidanceDirection) {
785
- direction.add(avoidanceDirection).normalize();
 
 
 
 
 
 
 
 
 
 
786
  }
787
  }
788
 
789
- // 이동 적용
790
- this.mesh.position.add(direction.multiplyScalar(this.moveSpeed));
791
-
792
- // 이동 λ°©ν–₯으둜 차체 νšŒμ „
793
- const targetRotation = Math.atan2(direction.x, direction.z);
794
- this.mesh.rotation.y = this.smoothRotation(this.mesh.rotation.y, targetRotation, 0.1);
 
 
 
 
795
  }
796
 
797
- moveAway(target) {
798
- if (!this.mesh) return;
 
 
 
799
 
800
- const direction = new THREE.Vector3()
801
- .subVectors(this.mesh.position, target)
802
- .normalize();
803
- this.mesh.position.add(direction.multiplyScalar(this.moveSpeed));
804
  }
805
 
806
- engageTarget(playerPosition) {
807
- if (!this.mesh) return;
 
 
 
808
 
809
- // 졜적 μ „νˆ¬ 거리 μœ μ§€
810
- const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
811
- const optimalRange = this.combat.optimalRange;
812
-
813
- if (Math.abs(distanceToPlayer - optimalRange) > 5) {
814
- if (distanceToPlayer < optimalRange) {
815
- this.moveAway(playerPosition);
816
- } else {
817
- this.moveTowards(playerPosition);
818
- }
819
  }
 
 
820
  }
821
 
822
- updateTurretRotation(playerPosition) {
823
- if (!this.mesh) return;
824
 
 
825
  const direction = new THREE.Vector3()
826
- .subVectors(playerPosition, this.mesh.position);
827
- this.combat.targetTurretRotation = Math.atan2(direction.x, direction.z);
828
-
829
- // λΆ€λ“œλŸ¬μš΄ 포탑 νšŒμ „
830
- this.combat.turretRotation = this.smoothRotation(
831
- this.combat.turretRotation,
832
- this.combat.targetTurretRotation,
833
- this.combat.turretRotationSpeed
834
- );
835
 
836
- // 포탑 λ©”μ‹œκ°€ μžˆλ‹€λ©΄ νšŒμ „ 적용
837
- if (this.mesh.getObjectByName('turret')) {
838
- this.mesh.getObjectByName('turret').rotation.y = this.combat.turretRotation;
 
 
839
  }
840
  }
841
 
842
- smoothRotation(current, target, speed) {
843
- let difference = target - current;
844
-
845
- // 각도 차이λ₯Ό -PIμ—μ„œ PI μ‚¬μ΄λ‘œ μ •κ·œν™”
846
- while (difference > Math.PI) difference -= Math.PI * 2;
847
- while (difference < -Math.PI) difference += Math.PI * 2;
848
-
849
- return current + difference * speed;
850
  }
851
 
852
- canShoot(playerPosition) {
853
- if (!this.mesh) return false;
854
-
855
- const currentTime = Date.now();
856
- const timeSinceLastShot = currentTime - this.lastAttackTime;
857
- const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
858
 
859
- return (
860
- this.aiState.canSeePlayer &&
861
- timeSinceLastShot >= ENEMY_CONFIG.ATTACK_INTERVAL &&
862
- distanceToPlayer <= this.combat.maxEngagementRange &&
863
- distanceToPlayer >= this.combat.minEngagementRange &&
864
- Math.abs(this.combat.turretRotation - this.combat.targetTurretRotation) < this.combat.aimThreshold
 
 
 
 
 
 
 
 
 
865
  );
 
 
 
 
 
 
 
 
 
 
866
  }
867
 
868
- detectObstacles() {
869
- const obstacles = [];
870
- const position = this.mesh.position.clone();
871
- position.y += 1;
872
-
873
- this.pathfinding.sensorAngles.forEach(angle => {
874
- const direction = new THREE.Vector3(0, 0, 1)
875
- .applyAxisAngle(new THREE.Vector3(0, 1, 0), angle * Math.PI / 180)
876
- .applyQuaternion(this.mesh.quaternion);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
877
 
878
- const raycaster = new THREE.Raycaster(position, direction, 0, this.pathfinding.sensorDistance);
879
- const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
 
 
 
 
 
 
 
 
 
 
 
 
880
 
881
- if (intersects.length > 0) {
882
- obstacles.push({
883
- angle: angle,
884
- distance: intersects[0].distance,
885
- point: intersects[0].point
886
- });
887
- }
888
  });
 
 
 
 
 
 
 
 
 
 
 
889
 
890
- return obstacles;
891
- }
 
 
 
 
 
 
 
892
 
893
- calculateAvoidanceDirection(obstacles) {
894
- if (obstacles.length === 0) return null;
895
 
896
- const avoidanceVector = new THREE.Vector3();
897
- obstacles.forEach(obstacle => {
898
- const avoidDir = new THREE.Vector3()
899
- .subVectors(this.mesh.position, obstacle.point)
900
- .normalize()
901
- .multiplyScalar(1 / obstacle.distance);
902
- avoidanceVector.add(avoidDir);
903
- });
904
 
905
- return avoidanceVector.normalize();
 
 
906
  }
907
 
908
  shoot(playerPosition) {
909
- const currentTime = Date.now();
910
- const attackInterval = this.type === 'tank' ?
911
- ENEMY_CONFIG.ATTACK_INTERVAL :
912
- ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
913
-
914
- if (currentTime - this.lastAttackTime < attackInterval) return;
915
-
916
- this.createMuzzleFlash();
917
 
918
- const enemyFireSound = new Audio('sounds/mbtfire5.ogg');
919
- enemyFireSound.volume = 0.3;
920
- enemyFireSound.play();
921
 
922
- const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8);
923
- const bulletMaterial = new THREE.MeshBasicMaterial({
924
- color: 0xff0000,
925
- emissive: 0xff0000,
926
- emissiveIntensity: 0.5
927
- });
928
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
929
 
930
- const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
931
- const muzzlePosition = new THREE.Vector3();
932
- this.mesh.getWorldPosition(muzzlePosition);
933
- muzzleOffset.applyQuaternion(this.mesh.quaternion);
934
- muzzlePosition.add(muzzleOffset);
935
-
936
- bullet.position.copy(muzzlePosition);
937
- bullet.quaternion.copy(this.mesh.quaternion);
938
-
939
- const direction = new THREE.Vector3()
940
- .subVectors(playerPosition, muzzlePosition)
941
- .normalize();
942
-
943
- const bulletSpeed = this.type === 'tank' ?
944
- ENEMY_CONFIG.BULLET_SPEED :
945
- ENEMY_CONFIG.BULLET_SPEED * 0.8;
946
-
947
- bullet.velocity = direction.multiplyScalar(bulletSpeed);
948
-
949
- const trailGeometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8);
950
- const trailMaterial = new THREE.MeshBasicMaterial({
951
- color: 0xff4444,
952
- transparent: true,
953
- opacity: 0.5
954
- });
955
-
956
- const trail = new THREE.Mesh(trailGeometry, trailMaterial);
957
- trail.position.z = -1;
958
- bullet.add(trail);
959
-
960
- this.scene.add(bullet);
961
- this.bullets.push(bullet);
962
- this.lastAttackTime = currentTime;
963
- }
964
 
965
- takeDamage(damage) {
966
- this.health -= damage;
967
-
968
- // 피격 효과 생성
969
- if (this.mesh) {
970
- const flashMaterial = new THREE.MeshBasicMaterial({
971
  color: 0xff0000,
972
- transparent: true,
973
- opacity: 0.5
974
  });
 
 
 
 
 
 
 
975
 
976
- this.mesh.traverse((child) => {
977
- if (child.isMesh) {
978
- child.originalMaterial = child.material;
979
- child.material = flashMaterial;
980
- }
981
- });
982
 
983
- // 0.1초 ν›„ μ›λž˜ 재질둜 볡ꡬ
984
- setTimeout(() => {
985
- this.mesh.traverse((child) => {
986
- if (child.isMesh && child.originalMaterial) {
987
- child.material = child.originalMaterial;
988
- }
989
- });
990
- }, 100);
991
- }
992
-
993
- return this.health <= 0;
994
- }
995
-
996
- destroy() {
997
- if (this.mesh) {
998
- // 폭발 효과 생��
999
- const explosionGeometry = new THREE.SphereGeometry(2, 32, 32);
1000
- const explosionMaterial = new THREE.MeshBasicMaterial({
1001
- color: 0xff4500,
1002
  transparent: true,
1003
- opacity: 0.8
1004
  });
1005
- const explosion = new THREE.Mesh(explosionGeometry, explosionMaterial);
1006
- explosion.position.copy(this.mesh.position);
1007
- this.scene.add(explosion);
1008
-
1009
- // 폭발 νŒŒν‹°ν΄ 생성
1010
- for (let i = 0; i < 20; i++) {
1011
- const particleGeometry = new THREE.SphereGeometry(0.2, 8, 8);
1012
- const particleMaterial = new THREE.MeshBasicMaterial({
1013
- color: Math.random() < 0.5 ? 0xff4500 : 0xff8c00,
1014
- transparent: true,
1015
- opacity: 1
1016
- });
1017
- const particle = new THREE.Mesh(particleGeometry, particleMaterial);
1018
- particle.position.copy(this.mesh.position);
1019
-
1020
- // 랜덀 λ°©ν–₯으둜 νŠ€μ–΄λ‚˜κ°€λŠ” 효과
1021
- const angle = Math.random() * Math.PI * 2;
1022
- const speed = Math.random() * 0.5 + 0.5;
1023
- particle.velocity = new THREE.Vector3(
1024
- Math.cos(angle) * speed,
1025
- Math.random() * speed,
1026
- Math.sin(angle) * speed
1027
- );
1028
-
1029
- this.scene.add(particle);
1030
- window.gameInstance.particles.push({
1031
- mesh: particle,
1032
- velocity: particle.velocity,
1033
- life: Math.random() * 60 + 60
1034
- });
1035
- }
1036
-
1037
- // 폭발음 μž¬μƒ
1038
- const explosionSound = new Audio('sounds/explosion.ogg');
1039
- explosionSound.volume = 0.4;
1040
- explosionSound.play();
1041
 
1042
- // λ©”μ‹œμ™€ μ΄μ•Œ 제거
1043
- this.scene.remove(this.mesh);
1044
- this.bullets.forEach(bullet => this.scene.remove(bullet));
1045
- this.bullets = [];
1046
- this.isLoaded = false;
1047
 
1048
- // 0.5초 ν›„ 폭발 효과 제거
1049
- setTimeout(() => {
1050
- this.scene.remove(explosion);
1051
- }, 500);
1052
  }
1053
- }
1054
 
1055
- // μΆ”κ°€ μœ ν‹Έλ¦¬ν‹° λ©”μ„œλ“œλ“€
1056
- createMuzzleFlash() {
1057
- if (!this.mesh) return;
1058
-
1059
- const flashGroup = new THREE.Group();
1060
-
1061
- const flameGeometry = new THREE.SphereGeometry(0.5, 8, 8);
1062
- const flameMaterial = new THREE.MeshBasicMaterial({
1063
- color: 0xffa500,
1064
- transparent: true,
1065
- opacity: 0.8
1066
- });
1067
- const flame = new THREE.Mesh(flameGeometry, flameMaterial);
1068
- flashGroup.add(flame);
1069
-
1070
- const smokeGeometry = new THREE.SphereGeometry(0.3, 8, 8);
1071
- const smokeMaterial = new THREE.MeshBasicMaterial({
1072
- color: 0x555555,
1073
- transparent: true,
1074
- opacity: 0.5
1075
- });
1076
-
1077
- for (let i = 0; i < 3; i++) {
1078
- const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial);
1079
- smoke.position.z = -0.5 - Math.random();
1080
- flashGroup.add(smoke);
1081
  }
1082
-
1083
- const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
1084
- const muzzlePosition = new THREE.Vector3();
1085
- this.mesh.getWorldPosition(muzzlePosition);
1086
- muzzleOffset.applyQuaternion(this.mesh.quaternion);
1087
- muzzlePosition.add(muzzleOffset);
1088
-
1089
- flashGroup.position.copy(muzzlePosition);
1090
- flashGroup.quaternion.copy(this.mesh.quaternion);
1091
-
1092
- this.scene.add(flashGroup);
1093
-
1094
- setTimeout(() => {
1095
- this.scene.remove(flashGroup);
1096
- }, 100);
1097
- }
1098
 
1099
- checkLineOfSight(playerPosition) {
1100
- if (!this.mesh) return false;
1101
-
1102
- const startPos = this.mesh.position.clone();
1103
- startPos.y += 1;
1104
- const direction = new THREE.Vector3()
1105
- .subVectors(playerPosition, startPos)
1106
- .normalize();
1107
-
1108
- const raycaster = new THREE.Raycaster(startPos, direction);
1109
- const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
1110
-
1111
- return intersects.length === 0;
1112
- }
1113
  }
1114
 
1115
  // Particle 클래슀
 
570
  // Enemy 클래슀
571
  class Enemy {
572
  constructor(scene, position, type = 'tank') {
573
+ // κΈ°λ³Έ 속성
574
  this.scene = scene;
575
  this.position = position;
576
  this.mesh = null;
 
591
  canSeePlayer: false,
592
  lastKnownPlayerPosition: null,
593
  searchStartTime: null,
594
+ targetRotation: 0,
595
+ currentRotation: 0,
596
+ isAiming: false,
597
+ aimingTime: 0,
598
+ requiredAimTime: 1000 // 쑰쀀에 ν•„μš”ν•œ μ‹œκ°„
599
  };
600
 
601
+ // 경둜 탐색 및 νšŒν”Ό μ‹œμŠ€ν…œ
602
  this.pathfinding = {
603
  currentPath: [],
604
  pathUpdateInterval: 1000,
605
  lastPathUpdate: 0,
606
  isAvoidingObstacle: false,
607
  avoidanceDirection: null,
608
+ obstacleCheckDistance: 10,
609
+ avoidanceTime: 0,
610
+ maxAvoidanceTime: 3000, // μ΅œλŒ€ νšŒν”Ό μ‹œκ°„
611
+ sensorAngles: [-45, 0, 45], // μ „λ°© 감지 각도
612
+ sensorDistance: 15 // 감지 거리
613
  };
614
 
615
  // μ „νˆ¬ μ‹œμŠ€ν…œ
 
617
  minEngagementRange: 30,
618
  maxEngagementRange: 150,
619
  optimalRange: 80,
620
+ aimThreshold: 0.1, // μ‘°μ€€ 정확도 μž„κ³„κ°’
621
  lastShotAccuracy: 0,
622
+ consecutiveHits: 0,
623
+ maxConsecutiveHits: 3
 
624
  };
625
  }
626
 
627
+ // μž₯μ• λ¬Ό 감지 μ‹œμŠ€ν…œ
628
+ detectObstacles() {
629
+ const obstacles = [];
630
+ const position = this.mesh.position.clone();
631
+ position.y += 1; // μ„Όμ„œ 높이 μ‘°μ •
632
 
633
+ this.pathfinding.sensorAngles.forEach(angle => {
634
+ const direction = new THREE.Vector3(0, 0, 1)
635
+ .applyQuaternion(this.mesh.quaternion)
636
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), angle * Math.PI / 180);
 
 
637
 
638
+ const raycaster = new THREE.Raycaster(position, direction, 0, this.pathfinding.sensorDistance);
639
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
640
 
641
+ if (intersects.length > 0) {
642
+ obstacles.push({
643
+ angle: angle,
644
+ distance: intersects[0].distance,
645
+ point: intersects[0].point
646
+ });
647
+ }
648
+ });
649
 
650
+ return obstacles;
651
  }
652
 
653
+ // νšŒν”Ό λ°©ν–₯ 계산
654
+ calculateAvoidanceDirection(obstacles) {
655
+ if (obstacles.length === 0) return null;
656
 
657
+ // λͺ¨λ“  μž₯μ• λ¬Όμ˜ λ°©ν–₯을 κ³ λ €ν•˜μ—¬ 졜적의 νšŒν”Ό λ°©ν–₯ 계산
658
+ const avoidanceVector = new THREE.Vector3();
659
+ obstacles.forEach(obstacle => {
660
+ const avoidDir = new THREE.Vector3()
661
+ .subVectors(this.mesh.position, obstacle.point)
662
+ .normalize()
663
+ .multiplyScalar(1 / obstacle.distance); // 거리에 λ°˜λΉ„λ‘€ν•˜λŠ” κ°€μ€‘μΉ˜
664
+ avoidanceVector.add(avoidDir);
665
+ });
666
 
667
+ return avoidanceVector.normalize();
668
+ }
 
 
669
 
670
+ // μ‘°μ€€ μ‹œμŠ€ν…œ
671
+ updateAiming(playerPosition) {
672
+ const targetDirection = new THREE.Vector3()
673
+ .subVectors(playerPosition, this.mesh.position)
674
+ .normalize();
675
 
676
+ // λͺ©ν‘œ νšŒμ „κ° 계산
677
+ this.aiState.targetRotation = Math.atan2(targetDirection.x, targetDirection.z);
 
 
 
 
678
 
679
+ // ν˜„μž¬ νšŒμ „κ° λΆ€λ“œλŸ½κ²Œ μ‘°μ •
680
+ const rotationDiff = this.aiState.targetRotation - this.aiState.currentRotation;
681
+ let rotationStep = Math.sign(rotationDiff) * Math.min(Math.abs(rotationDiff), 0.05);
682
+ this.aiState.currentRotation += rotationStep;
683
+
684
+ // λ©”μ‹œ νšŒμ „ 적용
685
+ this.mesh.rotation.y = this.aiState.currentRotation;
686
+
687
+ // μ‘°μ€€ 정확도 계산
688
+ const aimAccuracy = 1 - Math.abs(rotationDiff) / Math.PI;
689
+ return aimAccuracy > this.combat.aimThreshold;
690
+ }
691
+
692
+ // μ „νˆ¬ 거리 관리
693
+ maintainCombatDistance(playerPosition) {
694
+ const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
695
+ let moveDirection = new THREE.Vector3();
696
+
697
+ if (distanceToPlayer < this.combat.minEngagementRange) {
698
+ // λ„ˆλ¬΄ κ°€κΉŒμš°λ©΄ 후진
699
+ moveDirection.subVectors(this.mesh.position, playerPosition).normalize();
700
+ } else if (distanceToPlayer > this.combat.maxEngagementRange) {
701
+ // λ„ˆλ¬΄ λ©€λ©΄ 전진
702
+ moveDirection.subVectors(playerPosition, this.mesh.position).normalize();
703
+ } else if (Math.abs(distanceToPlayer - this.combat.optimalRange) > 10) {
704
+ // 졜적 거리둜 μ‘°μ •
705
+ const targetDistance = this.combat.optimalRange;
706
+ moveDirection.subVectors(playerPosition, this.mesh.position).normalize();
707
+ if (distanceToPlayer > targetDistance) {
708
+ moveDirection.multiplyScalar(1);
709
+ } else {
710
+ moveDirection.multiplyScalar(-1);
711
  }
712
  }
713
 
714
+ return moveDirection;
715
+ }
 
 
 
716
 
717
+ // λ°œμ‚¬ 쑰건 확인
718
+ canShoot(playerPosition) {
719
+ const distance = this.mesh.position.distanceTo(playerPosition);
720
+ const hasLineOfSight = this.checkLineOfSight(playerPosition);
721
+ const isAimed = this.updateAiming(playerPosition);
722
+
723
+ return distance <= this.combat.maxEngagementRange &&
724
+ distance >= this.combat.minEngagementRange &&
725
+ hasLineOfSight &&
726
+ isAimed;
727
  }
728
 
729
+ // 메인 μ—…λ°μ΄νŠΈ ν•¨μˆ˜
730
  update(playerPosition) {
731
  if (!this.mesh || !this.isLoaded) return;
732
 
733
+ // AI μƒνƒœ μ—…λ°μ΄νŠΈ
734
+ this.updateAIState(playerPosition);
 
 
 
 
 
735
 
736
+ // μž₯μ• λ¬Ό 감지
737
+ const obstacles = this.detectObstacles();
738
+
739
+ // 이동 및 νšŒν”Ό 둜직
740
+ if (obstacles.length > 0 && !this.pathfinding.isAvoidingObstacle) {
741
+ this.pathfinding.isAvoidingObstacle = true;
742
+ this.pathfinding.avoidanceDirection = this.calculateAvoidanceDirection(obstacles);
743
+ this.pathfinding.avoidanceTime = 0;
744
  }
745
 
746
+ // νšŒν”Ό λ™μž‘ μˆ˜ν–‰
747
+ if (this.pathfinding.isAvoidingObstacle) {
748
+ this.pathfinding.avoidanceTime += 16; // μ•½ 16ms per frame
749
+ if (this.pathfinding.avoidanceTime >= this.pathfinding.maxAvoidanceTime) {
750
+ this.pathfinding.isAvoidingObstacle = false;
 
 
751
  } else {
752
+ const avoidMove = this.pathfinding.avoidanceDirection.multiplyScalar(this.moveSpeed);
753
+ this.mesh.position.add(avoidMove);
 
 
 
 
 
754
  }
755
+ } else {
756
+ // 일반 이동 둜직
 
 
 
 
757
  switch (this.aiState.mode) {
758
  case 'pursue':
759
+ this.findPathToTarget(playerPosition);
760
+ this.moveAlongPath();
761
  break;
762
+ case 'flank':
763
+ const flankPosition = this.calculateFlankPosition(playerPosition);
764
+ this.findPathToTarget(flankPosition);
765
+ this.moveAlongPath();
766
  break;
767
+ case 'retreat':
768
+ const retreatPosition = this.calculateRetreatPosition(playerPosition);
769
+ this.findPathToTarget(retreatPosition);
770
+ this.moveAlongPath();
771
  break;
772
  }
 
 
 
 
 
 
 
 
 
 
 
773
  }
774
 
775
+ // μ „νˆ¬ 거리 μ‘°μ •
776
+ const combatMove = this.maintainCombatDistance(playerPosition);
777
+ if (combatMove.length() > 0) {
778
+ this.mesh.position.add(combatMove.multiplyScalar(this.moveSpeed));
779
+ }
780
 
781
+ // λ°œμ‚¬ 처리
782
  if (this.canShoot(playerPosition)) {
783
  this.shoot(playerPosition);
784
  }
785
 
786
  // μ΄μ•Œ μ—…λ°μ΄νŠΈ
787
  this.updateBullets();
788
+
789
+ // 탱크 기울기 μ‘°μ •
790
+ this.adjustTankTilt();
791
+ }
792
+ async initialize(loader) {
793
+ try {
794
+ const modelPath = this.type === 'tank' ? '/models/t90.glb' : '/models/t90.glb';
795
+ const result = await loader.loadAsync(modelPath);
796
+ this.mesh = result.scene;
797
+ this.mesh.position.copy(this.position);
798
+ this.mesh.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
799
+
800
+ this.mesh.traverse((child) => {
801
+ if (child.isMesh) {
802
+ child.castShadow = true;
803
+ child.receiveShadow = true;
804
+ }
805
+ });
806
+
807
+ this.scene.add(this.mesh);
808
+ this.isLoaded = true;
809
+ } catch (error) {
810
+ console.error('Error loading enemy model:', error);
811
+ this.isLoaded = false;
812
+ }
813
  }
814
 
815
+ checkLineOfSight(playerPosition) {
816
+ if (!this.mesh) return false;
817
 
818
+ const startPos = this.mesh.position.clone();
819
+ startPos.y += 2;
820
+ const direction = new THREE.Vector3().subVectors(playerPosition, startPos).normalize();
821
+ const distance = startPos.distanceTo(playerPosition);
822
 
823
+ const raycaster = new THREE.Raycaster(startPos, direction, 0, distance);
824
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
825
+
826
+ return intersects.length === 0;
827
+ }
828
+
829
+ updateAIState(playerPosition) {
830
+ const currentTime = Date.now();
831
+ const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
832
+
833
+ if (currentTime - this.aiState.lastVisibilityCheck > this.aiState.visibilityCheckInterval) {
834
+ this.aiState.canSeePlayer = this.checkLineOfSight(playerPosition);
835
+ this.aiState.lastVisibilityCheck = currentTime;
836
+
837
+ if (this.aiState.canSeePlayer) {
838
+ this.aiState.lastKnownPlayerPosition = playerPosition.clone();
839
+ this.aiState.searchStartTime = null;
840
  }
841
  }
842
 
843
+ if (currentTime - this.aiState.lastStateChange > this.aiState.stateChangeCooldown) {
844
+ if (this.health < 30) {
845
+ this.aiState.mode = 'retreat';
846
+ } else if (distanceToPlayer < 30 && this.aiState.canSeePlayer) {
847
+ this.aiState.mode = 'flank';
848
+ } else {
849
+ this.aiState.mode = 'pursue';
850
+ }
851
+ this.aiState.lastStateChange = currentTime;
852
+ }
853
  }
854
 
855
+ findPathToTarget(targetPosition) {
856
+ const currentTime = Date.now();
857
+ if (currentTime - this.pathfinding.lastPathUpdate < this.pathfinding.pathUpdateInterval) {
858
+ return;
859
+ }
860
 
861
+ this.pathfinding.currentPath = this.generatePathPoints(this.mesh.position.clone(), targetPosition);
862
+ this.pathfinding.lastPathUpdate = currentTime;
 
 
863
  }
864
 
865
+ generatePathPoints(start, end) {
866
+ const points = [];
867
+ const direction = new THREE.Vector3().subVectors(end, start).normalize();
868
+ const distance = start.distanceTo(end);
869
+ const steps = Math.ceil(distance / 10);
870
 
871
+ for (let i = 0; i <= steps; i++) {
872
+ const point = start.clone().add(direction.multiplyScalar(i * 10));
873
+ points.push(point);
 
 
 
 
 
 
 
874
  }
875
+
876
+ return points;
877
  }
878
 
879
+ moveAlongPath() {
880
+ if (this.pathfinding.currentPath.length === 0) return;
881
 
882
+ const targetPoint = this.pathfinding.currentPath[0];
883
  const direction = new THREE.Vector3()
884
+ .subVectors(targetPoint, this.mesh.position)
885
+ .normalize();
 
 
 
 
 
 
 
886
 
887
+ const moveVector = direction.multiplyScalar(this.moveSpeed);
888
+ this.mesh.position.add(moveVector);
889
+
890
+ if (this.mesh.position.distanceTo(targetPoint) < 2) {
891
+ this.pathfinding.currentPath.shift();
892
  }
893
  }
894
 
895
+ calculateFlankPosition(playerPosition) {
896
+ const angle = Math.random() * Math.PI * 2;
897
+ const radius = 40;
898
+ return new THREE.Vector3(
899
+ playerPosition.x + Math.cos(angle) * radius,
900
+ playerPosition.y,
901
+ playerPosition.z + Math.sin(angle) * radius
902
+ );
903
  }
904
 
905
+ calculateRetreatPosition(playerPosition) {
906
+ const direction = new THREE.Vector3()
907
+ .subVectors(this.mesh.position, playerPosition)
908
+ .normalize();
909
+ return this.mesh.position.clone().add(direction.multiplyScalar(50));
910
+ }
911
 
912
+ adjustTankTilt() {
913
+ const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
914
+ const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.mesh.quaternion);
915
+
916
+ const frontHeight = window.gameInstance.getHeightAtPosition(
917
+ this.mesh.position.x + forwardVector.x,
918
+ this.mesh.position.z + forwardVector.z
919
+ );
920
+ const backHeight = window.gameInstance.getHeightAtPosition(
921
+ this.mesh.position.x - forwardVector.x,
922
+ this.mesh.position.z - forwardVector.z
923
+ );
924
+ const rightHeight = window.gameInstance.getHeightAtPosition(
925
+ this.mesh.position.x + rightVector.x,
926
+ this.mesh.position.z + rightVector.z
927
  );
928
+ const leftHeight = window.gameInstance.getHeightAtPosition(
929
+ this.mesh.position.x - rightVector.x,
930
+ this.mesh.position.z - rightVector.z
931
+ );
932
+
933
+ const pitch = Math.atan2(frontHeight - backHeight, 2);
934
+ const roll = Math.atan2(rightHeight - leftHeight, 2);
935
+
936
+ const currentRotation = this.mesh.rotation.y;
937
+ this.mesh.rotation.set(pitch, currentRotation, roll);
938
  }
939
 
940
+ updateBullets() {
941
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
942
+ const bullet = this.bullets[i];
943
+ bullet.position.add(bullet.velocity);
944
+
945
+ if (Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
946
+ Math.abs(bullet.position.z) > MAP_SIZE / 2) {
947
+ this.scene.remove(bullet);
948
+ this.bullets.splice(i, 1);
949
+ continue;
950
+ }
951
+
952
+ const bulletBox = new THREE.Box3().setFromObject(bullet);
953
+ for (const obstacle of window.gameInstance.obstacles) {
954
+ const obstacleBox = new THREE.Box3().setFromObject(obstacle);
955
+ if (bulletBox.intersectsBox(obstacleBox)) {
956
+ this.scene.remove(bullet);
957
+ this.bullets.splice(i, 1);
958
+ break;
959
+ }
960
+ }
961
+ }
962
+ }
963
 
964
+ createMuzzleFlash() {
965
+ if (!this.mesh) return;
966
+
967
+ const flashGroup = new THREE.Group();
968
+
969
+ const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8);
970
+ const flameMaterial = new THREE.MeshBasicMaterial({
971
+ color: 0xffa500,
972
+ transparent: true,
973
+ opacity: 0.8
974
+ });
975
+ const flame = new THREE.Mesh(flameGeometry, flameMaterial);
976
+ flame.scale.set(2, 2, 3);
977
+ flashGroup.add(flame);
978
 
979
+ const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8);
980
+ const smokeMaterial = new THREE.MeshBasicMaterial({
981
+ color: 0x555555,
982
+ transparent: true,
983
+ opacity: 0.5
 
 
984
  });
985
+
986
+ for (let i = 0; i < 5; i++) {
987
+ const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial);
988
+ smoke.position.set(
989
+ Math.random() * 1 - 0.5,
990
+ Math.random() * 1 - 0.5,
991
+ -1 - Math.random()
992
+ );
993
+ smoke.scale.set(1.5, 1.5, 1.5);
994
+ flashGroup.add(smoke);
995
+ }
996
 
997
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
998
+ const muzzlePosition = new THREE.Vector3();
999
+ const meshWorldQuaternion = new THREE.Quaternion();
1000
+
1001
+ this.mesh.getWorldPosition(muzzlePosition);
1002
+ this.mesh.getWorldQuaternion(meshWorldQuaternion);
1003
+
1004
+ muzzleOffset.applyQuaternion(meshWorldQuaternion);
1005
+ muzzlePosition.add(muzzleOffset);
1006
 
1007
+ flashGroup.position.copy(muzzlePosition);
1008
+ flashGroup.quaternion.copy(meshWorldQuaternion);
1009
 
1010
+ this.scene.add(flashGroup);
 
 
 
 
 
 
 
1011
 
1012
+ setTimeout(() => {
1013
+ this.scene.remove(flashGroup);
1014
+ }, 500);
1015
  }
1016
 
1017
  shoot(playerPosition) {
1018
+ const currentTime = Date.now();
1019
+ const attackInterval = this.type === 'tank' ?
1020
+ ENEMY_CONFIG.ATTACK_INTERVAL :
1021
+ ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
 
 
 
 
1022
 
1023
+ if (currentTime - this.lastAttackTime < attackInterval) return;
 
 
1024
 
1025
+ this.createMuzzleFlash();
 
 
 
 
 
 
1026
 
1027
+ const enemyFireSound = new Audio('sounds/mbtfire5.ogg');
1028
+ enemyFireSound.volume = 0.3;
1029
+ enemyFireSound.play();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1030
 
1031
+ const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8);
1032
+ const bulletMaterial = new THREE.MeshBasicMaterial({
 
 
 
 
1033
  color: 0xff0000,
1034
+ emissive: 0xff0000,
1035
+ emissiveIntensity: 0.5
1036
  });
1037
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
1038
+
1039
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
1040
+ const muzzlePosition = new THREE.Vector3();
1041
+ this.mesh.getWorldPosition(muzzlePosition);
1042
+ muzzleOffset.applyQuaternion(this.mesh.quaternion);
1043
+ muzzlePosition.add(muzzleOffset);
1044
 
1045
+ bullet.position.copy(muzzlePosition);
1046
+ bullet.quaternion.copy(this.mesh.quaternion);
 
 
 
 
1047
 
1048
+ const direction = new THREE.Vector3()
1049
+ .subVectors(playerPosition, muzzlePosition)
1050
+ .normalize();
1051
+
1052
+ const bulletSpeed = this.type === 'tank' ?
1053
+ ENEMY_CONFIG.BULLET_SPEED :
1054
+ ENEMY_CONFIG.BULLET_SPEED * 0.8;
1055
+
1056
+ bullet.velocity = direction.multiplyScalar(bulletSpeed);
1057
+
1058
+ const trailGeometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8);
1059
+ const trailMaterial = new THREE.MeshBasicMaterial({
1060
+ color: 0xff4444,
 
 
 
 
 
 
1061
  transparent: true,
1062
+ opacity: 0.5
1063
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1064
 
1065
+ const trail = new THREE.Mesh(trailGeometry, trailMaterial);
1066
+ trail.position.z = -1;
1067
+ bullet.add(trail);
 
 
1068
 
1069
+ this.scene.add(bullet);
1070
+ this.bullets.push(bullet);
1071
+ this.lastAttackTime = currentTime;
 
1072
  }
 
1073
 
1074
+ takeDamage(damage) {
1075
+ this.health -= damage;
1076
+ return this.health <= 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1077
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1078
 
1079
+ destroy() {
1080
+ if (this.mesh) {
1081
+ this.scene.remove(this.mesh);
1082
+ this.bullets.forEach(bullet => this.scene.remove(bullet));
1083
+ this.bullets = [];
1084
+ this.isLoaded = false;
1085
+ }
1086
+ }
 
 
 
 
 
 
1087
  }
1088
 
1089
  // Particle 클래슀