cutechicken commited on
Commit
b16b51c
β€’
1 Parent(s): 632d61f

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +407 -381
game.js CHANGED
@@ -570,7 +570,6 @@ startReload() {
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,25 +590,22 @@ class Enemy {
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,473 +613,503 @@ class Enemy {
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 클래슀
 
570
  // Enemy 클래슀
571
  class Enemy {
572
  constructor(scene, position, type = 'tank') {
 
573
  this.scene = scene;
574
  this.position = position;
575
  this.mesh = null;
 
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
  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 클래슀