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

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +178 -540
game.js CHANGED
@@ -582,9 +582,17 @@ class Enemy {
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,7 +607,7 @@ class Enemy {
599
  currentRotation: 0,
600
  isAiming: false,
601
  aimingTime: 0,
602
- requiredAimTime: 1000 // 쑰쀀에 ν•„μš”ν•œ μ‹œκ°„
603
  };
604
 
605
  // 경둜 탐색 및 νšŒν”Ό μ‹œμŠ€ν…œ
@@ -611,9 +619,9 @@ class Enemy {
611
  avoidanceDirection: null,
612
  obstacleCheckDistance: 10,
613
  avoidanceTime: 0,
614
- maxAvoidanceTime: 3000, // μ΅œλŒ€ νšŒν”Ό μ‹œκ°„
615
- sensorAngles: [-45, 0, 45], // μ „λ°© 감지 각도
616
- sensorDistance: 15 // 감지 거리
617
  };
618
 
619
  // μ „νˆ¬ μ‹œμŠ€ν…œ
@@ -621,18 +629,40 @@ class Enemy {
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,277 +683,165 @@ class Enemy {
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,178 +850,37 @@ checkPathClear(start, end) {
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,145 +905,6 @@ checkPathClear(start, end) {
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 {
 
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
  currentRotation: 0,
608
  isAiming: false,
609
  aimingTime: 0,
610
+ requiredAimTime: 1000
611
  };
612
 
613
  // 경둜 탐색 및 νšŒν”Ό μ‹œμŠ€ν…œ
 
619
  avoidanceDirection: null,
620
  obstacleCheckDistance: 10,
621
  avoidanceTime: 0,
622
+ maxAvoidanceTime: 3000,
623
+ sensorAngles: [-45, 0, 45],
624
+ sensorDistance: 15
625
  };
626
 
627
  // μ „νˆ¬ μ‹œμŠ€ν…œ
 
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
 
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
  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
  }
906
  }
907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
 
909
  // Particle 클래슀
910
  class Particle {