cutechicken commited on
Commit
aeb079a
·
verified ·
1 Parent(s): 3d5a5a4

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +227 -536
game.js CHANGED
@@ -717,158 +717,52 @@ class Fighter {
717
  }
718
 
719
  // 적 전투기 클래스
 
720
  class EnemyFighter {
721
  constructor(scene, position) {
722
  this.mesh = null;
723
  this.isLoaded = false;
724
  this.scene = scene;
725
  this.position = position.clone();
726
- this.velocity = new THREE.Vector3(0, 0, 120);
727
- this.rotation = new THREE.Euler(0, 0, 0);
728
- this.health = GAME_CONSTANTS.MAX_HEALTH; // 체력 1000
729
- this.speed = 386; // 750kt in m/s (750 * 0.514444) - 1000kt에서 750kt 감소
 
 
 
 
 
 
 
 
 
730
  this.bullets = [];
 
 
731
  this.lastShootTime = 0;
 
 
732
 
733
- // 개선된 AI 상태
734
- this.aiState = 'patrol';
735
- this.targetPosition = this.generateRandomTarget();
736
- this.lastDirectionChange = Date.now();
737
- this.playerFighter = null; // 플레이어 참조 저장용
738
-
739
- // 부드러운 선회를 위한 변수
740
- this.targetRotation = new THREE.Euler(0, Math.random() * Math.PI * 2, 0);
741
- this.turnRadius = 1500; // 선회 반경
742
- this.isTurning = false;
743
- this.turnDirection = 1; // 1: 우회전, -1: 좌회전
744
 
745
- // 다른 적기들과의 충돌 회피
746
  this.nearbyEnemies = [];
747
- this.separationRadius = 300; // 최소 거리 300m
748
-
749
- // 전투 관련 변수
750
- this.isEngaged = false; // 전투 중 상태
751
- this.burstCount = 0; // 현재 연발 수
752
- this.lastBurstTime = 0; // 마지막 연발 시작 시간
753
- this.predictedTargetPos = new THREE.Vector3(); // 예측 사격 위치
754
-
755
- // 새로운 항공역학적 기동 변수
756
- this.maneuverState = 'straight'; // straight, turning, climbing, diving, combat_turn
757
- this.maneuverTimer = 0;
758
- this.nextManeuverTime = 3 + Math.random() * 4; // 3-7초마다 기동 변경
759
-
760
- // 전투 기동 패턴
761
- this.combatPattern = 'approach'; // approach, attack, evade, reposition
762
- this.combatTargetOffset = new THREE.Vector3();
763
- this.lastAttackPosition = new THREE.Vector3();
764
- this.evasionDirection = new THREE.Vector3();
765
-
766
- // 물리적 제한 추가
767
- this.maxBankAngle = Math.PI / 3; // 최대 60도 뱅크
768
- this.maxPitchRate = 1.0; // 초당 최대 피치 변화율
769
- this.maxRollRate = 2.0; // 초당 최대 롤 변화율
770
- this.currentG = 1.0; // 현재 G-Force
771
-
772
- // 에너지 관리
773
- this.throttle = 0.7; // 스로틀 제어
774
- this.optimalSpeed = 386; // 최적 전투 속도
775
- }
776
-
777
- generateRandomTarget() {
778
- // 맵 범위 내 랜덤 목표 지점 생성
779
- const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2 * 0.8;
780
- return new THREE.Vector3(
781
- (Math.random() - 0.5) * 2 * mapLimit,
782
- 1000 + Math.random() * 3000, // 1000m ~ 4000m 고도
783
- (Math.random() - 0.5) * 2 * mapLimit
784
- );
785
- }
786
-
787
- // 새로운 메서드: 자연스러운 기동 선택
788
- selectNewManeuver() {
789
- const maneuvers = ['straight', 'turning_left', 'turning_right', 'climbing', 'diving', 'barrel_roll'];
790
- const weights = [0.3, 0.2, 0.2, 0.15, 0.1, 0.05]; // 각 기동의 확률
791
-
792
- let random = Math.random();
793
- let accumulator = 0;
794
-
795
- for (let i = 0; i < maneuvers.length; i++) {
796
- accumulator += weights[i];
797
- if (random <= accumulator) {
798
- this.maneuverState = maneuvers[i];
799
- break;
800
- }
801
- }
802
 
803
- // 기동 지속 시간 설정
804
- switch (this.maneuverState) {
805
- case 'straight':
806
- this.maneuverTimer = 2 + Math.random() * 3;
807
- break;
808
- case 'turning_left':
809
- case 'turning_right':
810
- this.maneuverTimer = 3 + Math.random() * 4;
811
- this.turnDirection = this.maneuverState === 'turning_left' ? -1 : 1;
812
- break;
813
- case 'climbing':
814
- case 'diving':
815
- this.maneuverTimer = 2 + Math.random() * 2;
816
- break;
817
- case 'barrel_roll':
818
- this.maneuverTimer = 4;
819
- break;
820
- }
821
  }
822
 
823
- // 새로운 메서드: 전투 기동 패턴 선택
824
- selectCombatPattern(playerPosition, distance) {
825
- const angleToPlayer = this.getAngleToTarget(playerPosition);
826
-
827
- if (this.combatPattern === 'approach' && distance < 2000) {
828
- // 접근 완료, 공격 패턴으로 전환
829
- this.combatPattern = 'attack';
830
- this.lastAttackPosition = this.position.clone();
831
- } else if (this.combatPattern === 'attack' && distance < 500) {
832
- // 너무 가까움, 회피 기동
833
- this.combatPattern = 'evade';
834
- this.evasionDirection = new THREE.Vector3(
835
- Math.random() - 0.5,
836
- Math.random() * 0.5,
837
- Math.random() - 0.5
838
- ).normalize();
839
- } else if (this.combatPattern === 'evade' && distance > 1500) {
840
- // 안전거리 확보, 재위치 선정
841
- this.combatPattern = 'reposition';
842
- } else if (this.combatPattern === 'reposition' && distance > 1000 && distance < 3000) {
843
- // 재위치 완료, 다시 접근
844
- this.combatPattern = 'approach';
845
- }
846
-
847
- // 공격 위치 오프셋 계산 (플레이어 주변 300m 반경)
848
- if (this.combatPattern === 'reposition' || this.combatPattern === 'approach') {
849
- const offsetAngle = Math.random() * Math.PI * 2;
850
- const offsetDistance = 200 + Math.random() * 100; // 200-300m
851
- this.combatTargetOffset = new THREE.Vector3(
852
- Math.cos(offsetAngle) * offsetDistance,
853
- (Math.random() - 0.5) * 100,
854
- Math.sin(offsetAngle) * offsetDistance
855
- );
856
- }
857
- }
858
-
859
- // 새로운 메서드: 타겟까지의 각도 계산
860
- getAngleToTarget(targetPosition) {
861
- const toTarget = targetPosition.clone().sub(this.position).normalize();
862
- const forward = new THREE.Vector3(0, 0, 1).applyEuler(this.rotation);
863
- return Math.acos(Math.max(-1, Math.min(1, forward.dot(toTarget))));
864
- }
865
- async initialize(loader) {
866
  try {
867
  const result = await loader.loadAsync('models/mig-29.glb');
868
  this.mesh = result.scene;
869
  this.mesh.position.copy(this.position);
870
  this.mesh.scale.set(1.5, 1.5, 1.5);
871
- this.mesh.rotation.y = 3 * Math.PI / 2;
872
 
873
  this.mesh.traverse((child) => {
874
  if (child.isMesh) {
@@ -885,7 +779,7 @@ async initialize(loader) {
885
  this.createFallbackModel();
886
  }
887
  }
888
-
889
  createFallbackModel() {
890
  const group = new THREE.Group();
891
 
@@ -906,43 +800,35 @@ async initialize(loader) {
906
  this.mesh.scale.set(1.5, 1.5, 1.5);
907
  this.scene.add(this.mesh);
908
  this.isLoaded = true;
909
-
910
- console.log('Fallback 적기 모델 생성 완료');
911
  }
912
-
913
  update(playerPosition, deltaTime) {
914
- if (!this.mesh) return;
915
 
916
- // AI 상태 업데이트
917
  const distanceToPlayer = this.position.distanceTo(playerPosition);
918
 
919
- // 전투 거리 체크 (3000m 이내)
920
- if (distanceToPlayer < 3000) {
921
  this.aiState = 'combat';
922
- this.isEngaged = true;
923
- this.targetPosition = playerPosition.clone();
924
- } else if (distanceToPlayer > 5000) {
925
  this.aiState = 'patrol';
926
- this.isEngaged = false;
927
- this.combatPattern = 'approach'; // 전투 패턴 리셋
928
  }
929
 
930
- // 다른 적기와의 충돌 회피
931
- this.avoidOtherEnemies(deltaTime);
932
-
933
- // 기동 타이머 업데이트
934
- this.maneuverTimer -= deltaTime;
935
- if (this.maneuverTimer <= 0 && this.aiState === 'patrol') {
936
- this.selectNewManeuver();
937
- }
938
 
939
  // AI 행동 실행
940
  switch (this.aiState) {
941
  case 'patrol':
942
- this.patrol(deltaTime);
943
  break;
944
  case 'combat':
945
- this.combat(playerPosition, deltaTime);
 
 
 
946
  break;
947
  }
948
 
@@ -953,411 +839,185 @@ async initialize(loader) {
953
  this.updateBullets(deltaTime);
954
  }
955
 
956
- avoidOtherEnemies(deltaTime) {
957
- if (!this.nearbyEnemies) return;
958
-
959
- let closestDistance = Infinity;
960
- let avoidanceNeeded = false;
961
- let avoidDirection = new THREE.Vector3();
962
 
963
- this.nearbyEnemies.forEach(enemy => {
964
- if (enemy === this || !enemy.position) return;
965
-
966
- const distance = this.position.distanceTo(enemy.position);
967
- if (distance < this.separationRadius && distance > 0) {
968
- avoidanceNeeded = true;
969
- if (distance < closestDistance) {
970
- closestDistance = distance;
971
- // 반대 방향으로 회피
972
- avoidDirection = this.position.clone().sub(enemy.position).normalize();
973
- }
974
- }
975
- });
976
 
977
- if (avoidanceNeeded) {
978
- // 긴급 회피: 급선회
979
- const avoidAngle = Math.atan2(avoidDirection.x, avoidDirection.z);
980
- this.targetRotation.y = avoidAngle;
981
- this.isTurning = true;
982
-
983
- // 고도도 변경 (더 부드럽게)
984
- if (avoidDirection.y > 0) {
985
- this.targetRotation.x = -0.15;
986
- } else {
987
- this.targetRotation.x = 0.15;
988
- }
989
- }
990
- }
991
-
992
- patrol(deltaTime) {
993
- // 자연스러운 기동 실행
994
- switch (this.maneuverState) {
995
- case 'straight':
996
- // 직진 비행
997
- this.targetRotation.x = 0;
998
- this.targetRotation.z = 0;
999
-
1000
- // 목표 지점을 향해 천천히 방향 조정
1001
- const dirToTarget = this.targetPosition.clone().sub(this.position);
1002
- if (dirToTarget.length() > 0) {
1003
- dirToTarget.normalize();
1004
- const targetYaw = Math.atan2(dirToTarget.x, dirToTarget.z);
1005
- const yawDiff = this.normalizeAngle(targetYaw - this.rotation.y);
1006
- this.targetRotation.y = this.rotation.y + yawDiff * deltaTime * 0.8;
1007
- }
1008
- break;
1009
-
1010
- case 'turning_left':
1011
- case 'turning_right':
1012
- // 뱅크 턴 실행
1013
- const bankAngle = this.turnDirection * Math.PI / 4; // 45도 뱅크
1014
- this.targetRotation.z = bankAngle;
1015
- this.targetRotation.y += this.turnDirection * deltaTime * 0.8;
1016
-
1017
- // 턴 중 약간의 기수 하강
1018
- this.targetRotation.x = 0.1;
1019
- break;
1020
-
1021
- case 'climbing':
1022
- // 상승
1023
- this.targetRotation.x = -Math.PI / 6; // 30도 상승
1024
- this.targetRotation.z = 0;
1025
- this.throttle = Math.min(1.0, this.throttle + deltaTime * 0.3);
1026
- break;
1027
-
1028
- case 'diving':
1029
- // 하강
1030
- this.targetRotation.x = Math.PI / 8; // 22.5도 하강
1031
- this.targetRotation.z = 0;
1032
- this.throttle = Math.max(0.5, this.throttle - deltaTime * 0.2);
1033
- break;
1034
-
1035
- case 'barrel_roll':
1036
- // 배럴 롤
1037
- const rollProgress = (4 - this.maneuverTimer) / 4;
1038
- this.targetRotation.z = Math.sin(rollProgress * Math.PI * 4) * Math.PI / 2;
1039
- this.targetRotation.x = Math.sin(rollProgress * Math.PI * 2) * 0.2;
1040
- break;
1041
  }
1042
 
1043
- // 목표 지점에 도달했는지 확인
1044
- const distanceToTarget = this.position.distanceTo(this.targetPosition);
1045
- if (distanceToTarget < 500) {
1046
- // 새로운 목표 생성
1047
- this.targetPosition = this.generateRandomTarget();
1048
- this.selectNewManeuver();
 
 
 
 
 
 
 
 
 
 
 
 
 
1049
  }
1050
 
1051
- // 고도 조정
1052
- const altitudeDiff = this.targetPosition.y - this.position.y;
1053
- if (Math.abs(altitudeDiff) > 200 && this.maneuverState === 'straight') {
1054
- if (altitudeDiff > 0) {
1055
- this.targetRotation.x = Math.max(-0.3, Math.min(0, altitudeDiff * 0.0001));
 
 
1056
  } else {
1057
- this.targetRotation.x = Math.min(0.3, Math.max(0, altitudeDiff * 0.0001));
 
1058
  }
1059
  }
1060
 
1061
- // 속도 관리
1062
- this.updateSpeedControl(deltaTime);
1063
- }
1064
-
1065
- combat(playerPosition, deltaTime) {
1066
- const distance = this.position.distanceTo(playerPosition);
1067
-
1068
- // 전투 패턴 업데이트
1069
- this.selectCombatPattern(playerPosition, distance);
1070
-
1071
- // 전투 패턴별 기동
1072
- switch (this.combatPattern) {
1073
- case 'approach':
1074
- // 플레이어 근처 목표점으로 접근
1075
- const approachTarget = playerPosition.clone().add(this.combatTargetOffset);
1076
- this.executeInterceptCourse(approachTarget, deltaTime);
1077
-
1078
- // 에너지 관리 - 속도 유지
1079
- this.throttle = 0.8;
1080
- break;
1081
-
1082
- case 'attack':
1083
- // 예측 사격을 위한 리드 계산
1084
- if (this.playerFighter && this.playerFighter.velocity) {
1085
- const bulletTravelTime = distance / 1200;
1086
- this.predictedTargetPos = playerPosition.clone().add(
1087
- this.playerFighter.velocity.clone().multiplyScalar(bulletTravelTime * 0.7)
1088
- );
1089
- } else {
1090
- this.predictedTargetPos = playerPosition.clone();
1091
- }
1092
-
1093
- // 공격 자세 유지
1094
- this.executeAttackRun(this.predictedTargetPos, deltaTime);
1095
-
1096
- // 사격 판단
1097
- const aimAccuracy = this.calculateAimAccuracy(this.predictedTargetPos);
1098
- if (distance < 1500 && aimAccuracy < 0.15) {
1099
- this.fireWeapon();
1100
- }
1101
- break;
1102
-
1103
- case 'evade':
1104
- // 회피 기동 - 브레이크 턴 또는 배럴 롤
1105
- this.executeEvasiveManeuver(deltaTime);
1106
- break;
1107
-
1108
- case 'reposition':
1109
- // 재위치 - 고도와 속도 이점 확보
1110
- const repositionTarget = playerPosition.clone();
1111
- repositionTarget.y += 500 + Math.random() * 1000; // 위에서 공격
1112
- repositionTarget.add(this.combatTargetOffset);
1113
-
1114
- this.executeInterceptCourse(repositionTarget, deltaTime);
1115
-
1116
- // 속도 회복
1117
- this.throttle = 0.9;
1118
- break;
1119
- }
1120
 
1121
- // 속도 관리
1122
- this.updateSpeedControl(deltaTime);
1123
- }
1124
-
1125
- // 요격 코스 실행
1126
- executeInterceptCourse(target, deltaTime) {
1127
- const toTarget = target.clone().sub(this.position);
1128
- const distance = toTarget.length();
1129
-
1130
- if (distance > 0) {
1131
- toTarget.normalize();
1132
 
1133
- // 리드 - 목표의 미래 위치를 예측하여 선회
1134
- const targetYaw = Math.atan2(toTarget.x, toTarget.z);
1135
- const targetPitch = Math.asin(-toTarget.y);
1136
 
1137
- // 부드러운 선회를 위한 각도 차이 계산
1138
  const yawDiff = this.normalizeAngle(targetYaw - this.rotation.y);
1139
  const pitchDiff = targetPitch - this.rotation.x;
1140
 
1141
- // 뱅크 각도를 이용한 자연스러운 선회
1142
- if (Math.abs(yawDiff) > 0.1) {
1143
- // 회전에 따른 롤 적용
1144
- this.targetRotation.z = Math.max(-this.maxBankAngle,
1145
- Math.min(this.maxBankAngle, -yawDiff * 1.5));
1146
  } else {
1147
- this.targetRotation.z *= 0.9; // 롤 복귀
1148
  }
1149
 
1150
- // 목표 방향으로 회전
1151
- this.targetRotation.y = this.rotation.y + yawDiff * deltaTime * 1.0;
1152
- this.targetRotation.x = this.rotation.x + pitchDiff * deltaTime * 0.6;
 
 
 
 
1153
 
1154
- // 피치 제한
1155
- this.targetRotation.x = Math.max(-Math.PI / 4, Math.min(Math.PI / 4, this.targetRotation.x));
1156
  }
1157
- }
1158
-
1159
- // 공격 진입
1160
- executeAttackRun(target, deltaTime) {
1161
- const toTarget = target.clone().sub(this.position);
1162
- const distance = toTarget.length();
1163
 
1164
- if (distance > 0) {
1165
- toTarget.normalize();
1166
- const targetYaw = Math.atan2(toTarget.x, toTarget.z);
1167
- const targetPitch = Math.asin(-toTarget.y);
1168
 
1169
- // 정밀 조준을 위한 작은 수정
1170
- const yawDiff = this.normalizeAngle(targetYaw - this.rotation.y);
1171
- const pitchDiff = targetPitch - this.rotation.x;
1172
 
1173
- // 안정적인 사격 자세 유지
1174
- this.targetRotation.y = this.rotation.y + yawDiff * deltaTime * 1.5;
1175
- this.targetRotation.x = this.rotation.x + pitchDiff * deltaTime * 1.0;
1176
- this.targetRotation.z = -yawDiff * 0.3;
1177
 
1178
- // 속도 조절 - 목표와의 상대 속도 관리
1179
- if (distance < 800) {
1180
- this.throttle = 0.6; // 감속
1181
- } else {
1182
- this.throttle = 0.85;
1183
- }
1184
- }
1185
- }
1186
-
1187
- // 회피 기동
1188
- executeEvasiveManeuver(deltaTime) {
1189
- // 브레이크 턴 또는 스플릿-S
1190
- const evasiveType = Math.random() > 0.5 ? 'break' : 'split_s';
1191
-
1192
- if (evasiveType === 'break') {
1193
- // 급격한 브레이크 턴
1194
- this.targetRotation.z = this.evasionDirection.x * this.maxBankAngle * 1.2;
1195
- this.targetRotation.y += this.evasionDirection.x * deltaTime * 3.0;
1196
- this.targetRotation.x = 0.2; // 약간의 기수 하강
1197
- this.throttle = 0.4; // 급감속
1198
- } else {
1199
- // 스플릿-S (반전 하강)
1200
- this.targetRotation.z += deltaTime * 4.0; // 빠른 롤
1201
- if (Math.abs(this.rotation.z) > Math.PI * 0.8) {
1202
- this.targetRotation.x = Math.PI / 3; // 급하강
1203
  }
1204
- this.throttle = 0.3;
1205
- }
1206
 
1207
- // G-Force 시뮬레이션
1208
- this.currentG = 3.0 + Math.abs(this.rotation.z) * 2;
 
 
1209
  }
1210
 
1211
- // 조준 정확도 계산
1212
- calculateAimAccuracy(target) {
1213
- const toTarget = target.clone().sub(this.position).normalize();
1214
- const forward = new THREE.Vector3(0, 0, 1).applyEuler(this.rotation);
1215
 
1216
- const dotProduct = forward.dot(toTarget);
1217
- const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct)));
 
1218
 
1219
- return angle;
1220
- }
1221
-
1222
- // 무기 발사
1223
- fireWeapon() {
1224
- const now = Date.now();
1225
- if (now - this.lastBurstTime > 2000) {
1226
- this.burstCount = 0;
1227
- this.lastBurstTime = now;
1228
- }
1229
 
1230
- if (this.burstCount < 5 && now - this.lastShootTime > 200) {
1231
- this.shoot();
1232
- this.burstCount++;
 
 
 
 
 
 
 
1233
  }
1234
- }
1235
-
1236
- // 속도 제어
1237
- updateSpeedControl(deltaTime) {
1238
- // 목표 속도 계산
1239
- let targetSpeed = this.optimalSpeed * this.throttle;
1240
 
1241
- // 고도에 따른 속도 보정
1242
- const altitudeFactor = 1 + (this.position.y / 10000) * 0.2;
1243
- targetSpeed *= altitudeFactor;
1244
 
1245
- // 기동에 따른 속도 손실
1246
- if (this.currentG > 2.0) {
1247
- targetSpeed *= (1 - (this.currentG - 2.0) * 0.05);
 
 
 
1248
  }
1249
 
1250
- // 부드러운 속도 변화
1251
- this.speed = THREE.MathUtils.lerp(this.speed, targetSpeed, deltaTime * 0.5);
1252
- }
1253
-
1254
- // 각도 정규화 헬퍼 메서드
1255
- normalizeAngle(angle) {
1256
- while (angle > Math.PI) angle -= Math.PI * 2;
1257
- while (angle < -Math.PI) angle += Math.PI * 2;
1258
- return angle;
1259
- }
1260
-
1261
- updatePhysics(deltaTime) {
1262
- if (!this.mesh) return;
1263
-
1264
- // 부드러운 회전 적용 - 항공역학적 제한 적용
1265
- const rotationSpeed = deltaTime * 2.0;
1266
- const rollSpeed = deltaTime * this.maxRollRate;
1267
- const pitchSpeed = deltaTime * this.maxPitchRate;
1268
-
1269
- // 각 축별로 다른 속도로 회전
1270
- this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetRotation.x, pitchSpeed);
1271
- this.rotation.y = THREE.MathUtils.lerp(this.rotation.y, this.targetRotation.y, rotationSpeed);
1272
- this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRotation.z, rollSpeed);
1273
-
1274
- // G-Force 계산
1275
- const turnRate = Math.abs(this.targetRotation.y - this.rotation.y) * 10;
1276
- const pitchRate = Math.abs(this.targetRotation.x - this.rotation.x) * 10;
1277
- this.currentG = 1.0 + turnRate + pitchRate + Math.abs(this.rotation.z) * 2;
1278
-
1279
- // 속도 벡터 계산 - 실제 항공기처럼 기수 방향으로
1280
- const forward = new THREE.Vector3(0, 0, 1);
1281
- forward.applyEuler(this.rotation);
1282
-
1283
- // 전진 속도 적용
1284
- this.velocity = forward.multiplyScalar(this.speed);
1285
-
1286
- // 뱅크 각도에 따른 선회력
1287
- if (Math.abs(this.rotation.z) > 0.1) {
1288
- const lift = new THREE.Vector3(0, 1, 0);
1289
- lift.applyEuler(this.rotation);
1290
- const turnForce = Math.sin(this.rotation.z) * this.currentG * 30;
1291
- this.velocity.add(lift.multiplyScalar(turnForce * deltaTime));
1292
- }
1293
-
1294
- // 중력 효과
1295
- const gravity = GAME_CONSTANTS.GRAVITY * deltaTime;
1296
- this.velocity.y -= gravity;
1297
-
1298
- // 양력 계산 (속도에 비례)
1299
- const liftFactor = (this.speed / this.optimalSpeed) * 0.8;
1300
- const lift = gravity * liftFactor * Math.cos(this.rotation.x);
1301
- this.velocity.y += lift;
1302
-
1303
- // 항력 (드래그)
1304
- const dragFactor = 0.02 + Math.abs(this.rotation.x) * 0.01;
1305
- this.velocity.multiplyScalar(1 - dragFactor * deltaTime);
1306
-
1307
- // 위치 업데이트
1308
- this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
1309
-
1310
- // 고도 제한
1311
- if (this.position.y < 300) {
1312
- this.position.y = 300;
1313
- this.velocity.y = Math.max(0, this.velocity.y);
1314
- this.targetRotation.x = -0.3; // 강제 상승
1315
- } else if (this.position.y > 8000) {
1316
- this.position.y = 8000;
1317
- this.velocity.y = Math.min(0, this.velocity.y);
1318
- this.targetRotation.x = 0.2; // 하강
1319
  }
1320
 
1321
- // 맵 경계 처리 - 부드러운 회귀
1322
- const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
1323
- const boundaryBuffer = mapLimit * 0.9;
1324
-
1325
- if (Math.abs(this.position.x) > boundaryBuffer || Math.abs(this.position.z) > boundaryBuffer) {
1326
- // 맵 중앙을 향해 자연스럽게 회전
1327
- const centerDirection = new THREE.Vector3(-this.position.x, 0, -this.position.z).normalize();
1328
- const targetYaw = Math.atan2(centerDirection.x, centerDirection.z);
1329
- this.targetRotation.y = targetYaw;
1330
 
1331
- // 경계에서 뱅크
1332
- if (this.position.x > boundaryBuffer) {
1333
- this.targetRotation.z = -this.maxBankAngle;
1334
- } else if (this.position.x < -boundaryBuffer) {
1335
- this.targetRotation.z = this.maxBankAngle;
 
 
 
 
 
 
1336
  }
1337
  }
1338
 
1339
- // 하드 리미트
1340
- if (this.position.x > mapLimit) this.position.x = mapLimit;
1341
- if (this.position.x < -mapLimit) this.position.x = -mapLimit;
1342
- if (this.position.z > mapLimit) this.position.z = mapLimit;
1343
- if (this.position.z < -mapLimit) this.position.z = -mapLimit;
1344
-
1345
- // 메시 업데이트
1346
- this.mesh.position.copy(this.position);
1347
- this.mesh.rotation.x = this.rotation.x;
1348
- this.mesh.rotation.y = this.rotation.y + Math.PI;
1349
- this.mesh.rotation.z = this.rotation.z;
1350
-
1351
- // 자동 롤 복귀 (전투 중이 아닐 때)
1352
- if (!this.isEngaged && Math.abs(this.targetRotation.z) > 0.01) {
1353
- this.targetRotation.z *= 0.95;
1354
- }
1355
- }
1356
-
1357
  shoot() {
1358
- this.lastShootTime = Date.now();
1359
-
1360
- // 직선 모양의 탄환 (100% 더 크게)
1361
  const bulletGeometry = new THREE.CylinderGeometry(0.8, 0.8, 12, 8);
1362
  const bulletMaterial = new THREE.MeshBasicMaterial({
1363
  color: 0xff0000,
@@ -1371,10 +1031,11 @@ async initialize(loader) {
1371
  muzzleOffset.applyEuler(this.rotation);
1372
  bullet.position.copy(this.position).add(muzzleOffset);
1373
 
1374
- // 탄환을 발사 방향으로 회전
1375
  bullet.rotation.copy(this.rotation);
1376
  bullet.rotateX(Math.PI / 2);
1377
 
 
1378
  const direction = new THREE.Vector3(0, 0, 1);
1379
  direction.applyEuler(this.rotation);
1380
  bullet.velocity = direction.multiplyScalar(1200);
@@ -1382,38 +1043,27 @@ async initialize(loader) {
1382
  this.scene.add(bullet);
1383
  this.bullets.push(bullet);
1384
 
1385
- // MGLAUNCH.ogg 소리 재생 - 플레이어가 3000m 이내에 있을 때만
1386
  if (this.playerFighter) {
1387
  const distanceToPlayer = this.position.distanceTo(this.playerFighter.position);
1388
  if (distanceToPlayer < 3000) {
1389
  try {
1390
  const audio = new Audio('sounds/MGLAUNCH.ogg');
1391
- audio.volume = 0.5;
1392
-
1393
- // 거리에 따른 음량 조절
1394
  const volumeMultiplier = 1 - (distanceToPlayer / 3000);
1395
  audio.volume = 0.5 * volumeMultiplier;
1396
-
1397
- audio.play().catch(e => console.log('Enemy gunfire sound failed to play'));
1398
-
1399
- audio.addEventListener('ended', () => {
1400
- audio.remove();
1401
- });
1402
- } catch (e) {
1403
- console.log('Audio error:', e);
1404
- }
1405
  }
1406
  }
1407
  }
1408
-
1409
  updateBullets(deltaTime) {
1410
  for (let i = this.bullets.length - 1; i >= 0; i--) {
1411
  const bullet = this.bullets[i];
1412
  bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
1413
 
1414
- // 지면 충돌 체크
1415
  if (bullet.position.y <= 0) {
1416
- // 크고 화려한 지면 충돌 효과
1417
  if (window.gameInstance) {
1418
  window.gameInstance.createGroundImpactEffect(bullet.position);
1419
  }
@@ -1422,18 +1072,59 @@ async initialize(loader) {
1422
  continue;
1423
  }
1424
 
 
1425
  if (bullet.position.distanceTo(this.position) > 5000) {
1426
  this.scene.remove(bullet);
1427
  this.bullets.splice(i, 1);
1428
  }
1429
  }
1430
  }
1431
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1432
  takeDamage(damage) {
1433
  this.health -= damage;
1434
  return this.health <= 0;
1435
  }
1436
-
1437
  destroy() {
1438
  if (this.mesh) {
1439
  this.scene.remove(this.mesh);
 
717
  }
718
 
719
  // 적 전투기 클래스
720
+ // 적 전투기 클래스 - 완전히 재설계
721
  class EnemyFighter {
722
  constructor(scene, position) {
723
  this.mesh = null;
724
  this.isLoaded = false;
725
  this.scene = scene;
726
  this.position = position.clone();
727
+ this.rotation = new THREE.Euler(0, Math.random() * Math.PI * 2, 0);
728
+
729
+ // 물리 속성
730
+ this.speed = 600; // 600kt로 시작 (500-750kt 범위)
731
+ this.velocity = new THREE.Vector3(0, 0, 0);
732
+ this.health = GAME_CONSTANTS.MAX_HEALTH;
733
+
734
+ // AI 상태
735
+ this.aiState = 'patrol'; // patrol, combat, evade
736
+ this.targetPosition = null;
737
+ this.playerFighter = null;
738
+
739
+ // 전투 시스템
740
  this.bullets = [];
741
+ this.burstCounter = 0; // 현재 연발 카운터
742
+ this.attackCounter = 0; // 공격 횟수 카운터
743
  this.lastShootTime = 0;
744
+ this.evadeTimer = 0;
745
+ this.isEvading = false;
746
 
747
+ // 부드러운 회전을 위한 변수
748
+ this.targetRotation = this.rotation.clone();
749
+ this.turnSpeed = 1.5; // 초당 회전 속도 (라디안)
 
 
 
 
 
 
 
 
750
 
751
+ // 충돌 회피
752
  this.nearbyEnemies = [];
753
+ this.avoidanceVector = new THREE.Vector3();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
 
755
+ // 초기 목표 설정
756
+ this.selectNewPatrolTarget();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
  }
758
 
759
+ async initialize(loader) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  try {
761
  const result = await loader.loadAsync('models/mig-29.glb');
762
  this.mesh = result.scene;
763
  this.mesh.position.copy(this.position);
764
  this.mesh.scale.set(1.5, 1.5, 1.5);
765
+ this.mesh.rotation.y = this.rotation.y + Math.PI;
766
 
767
  this.mesh.traverse((child) => {
768
  if (child.isMesh) {
 
779
  this.createFallbackModel();
780
  }
781
  }
782
+
783
  createFallbackModel() {
784
  const group = new THREE.Group();
785
 
 
800
  this.mesh.scale.set(1.5, 1.5, 1.5);
801
  this.scene.add(this.mesh);
802
  this.isLoaded = true;
 
 
803
  }
804
+
805
  update(playerPosition, deltaTime) {
806
+ if (!this.mesh || !this.isLoaded) return;
807
 
 
808
  const distanceToPlayer = this.position.distanceTo(playerPosition);
809
 
810
+ // 상태 결정
811
+ if (distanceToPlayer <= 3000 && !this.isEvading) {
812
  this.aiState = 'combat';
813
+ } else if (this.isEvading) {
814
+ this.aiState = 'evade';
815
+ } else {
816
  this.aiState = 'patrol';
 
 
817
  }
818
 
819
+ // 충돌 회피 계산
820
+ this.calculateAvoidance();
 
 
 
 
 
 
821
 
822
  // AI 행동 실행
823
  switch (this.aiState) {
824
  case 'patrol':
825
+ this.executePatrol(deltaTime);
826
  break;
827
  case 'combat':
828
+ this.executeCombat(playerPosition, deltaTime);
829
+ break;
830
+ case 'evade':
831
+ this.executeEvade(deltaTime);
832
  break;
833
  }
834
 
 
839
  this.updateBullets(deltaTime);
840
  }
841
 
842
+ executePatrol(deltaTime) {
843
+ // 목표 지점까지의 거리 확인
844
+ if (!this.targetPosition || this.position.distanceTo(this.targetPosition) < 500) {
845
+ this.selectNewPatrolTarget();
846
+ }
 
847
 
848
+ // 목표를 향해 부드럽게 회전
849
+ this.smoothTurnToTarget(this.targetPosition, deltaTime);
 
 
 
 
 
 
 
 
 
 
 
850
 
851
+ // 속도 유지 (500-750kt)
852
+ this.speed = THREE.MathUtils.clamp(this.speed, 257, 386); // m/s 변환
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
853
  }
854
 
855
+ executeCombat(playerPosition, deltaTime) {
856
+ // 플레이어를 향해 회전
857
+ this.smoothTurnToTarget(playerPosition, deltaTime);
858
+
859
+ // 조준 정확도 확인
860
+ const aimAccuracy = this.calculateAimAccuracy(playerPosition);
861
+
862
+ // 정확한 조준 시 발사
863
+ if (aimAccuracy < 0.1 && this.attackCounter < 3) {
864
+ this.fireWeapon();
865
+ }
866
+
867
+ // 3번 공격 후 회피 모드로 전환
868
+ if (this.attackCounter >= 3) {
869
+ this.isEvading = true;
870
+ this.evadeTimer = 3.0; // 3초 회피
871
+ this.attackCounter = 0;
872
+ this.selectEvadeTarget();
873
+ }
874
  }
875
 
876
+ executeEvade(deltaTime) {
877
+ // 회피 타이머 업데이트
878
+ this.evadeTimer -= deltaTime;
879
+
880
+ if (this.evadeTimer <= 0) {
881
+ this.isEvading = false;
882
+ this.selectNewPatrolTarget();
883
  } else {
884
+ // 회피 목표를 향해 이동
885
+ this.smoothTurnToTarget(this.targetPosition, deltaTime);
886
  }
887
  }
888
 
889
+ smoothTurnToTarget(targetPos, deltaTime) {
890
+ // 타겟 방향 계산
891
+ const direction = targetPos.clone().sub(this.position);
892
+ direction.y *= 0.3; // 수직 이동을 제한
893
+ direction.normalize();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
894
 
895
+ // 충돌 회피 벡터 적용
896
+ if (this.avoidanceVector.length() > 0) {
897
+ direction.add(this.avoidanceVector.multiplyScalar(0.5));
898
+ direction.normalize();
899
+ }
 
 
 
 
 
 
900
 
901
+ // 목표 회전 계산
902
+ const targetYaw = Math.atan2(direction.x, direction.z);
903
+ const targetPitch = Math.asin(-direction.y);
904
 
905
+ // 부드러운 회전 (최대 회전 속도 제한)
906
  const yawDiff = this.normalizeAngle(targetYaw - this.rotation.y);
907
  const pitchDiff = targetPitch - this.rotation.x;
908
 
909
+ const maxTurnRate = this.turnSpeed * deltaTime;
910
+
911
+ // Yaw 회전
912
+ if (Math.abs(yawDiff) > maxTurnRate) {
913
+ this.rotation.y += Math.sign(yawDiff) * maxTurnRate;
914
  } else {
915
+ this.rotation.y = targetYaw;
916
  }
917
 
918
+ // Pitch 회전 (제한적)
919
+ const maxPitchRate = maxTurnRate * 0.5;
920
+ if (Math.abs(pitchDiff) > maxPitchRate) {
921
+ this.rotation.x += Math.sign(pitchDiff) * maxPitchRate;
922
+ } else {
923
+ this.rotation.x = targetPitch;
924
+ }
925
 
926
+ // Pitch 제한
927
+ this.rotation.x = THREE.MathUtils.clamp(this.rotation.x, -Math.PI / 6, Math.PI / 6);
928
  }
 
 
 
 
 
 
929
 
930
+ calculateAvoidance() {
931
+ this.avoidanceVector.set(0, 0, 0);
 
 
932
 
933
+ if (!this.nearbyEnemies) return;
 
 
934
 
935
+ let avoidCount = 0;
 
 
 
936
 
937
+ this.nearbyEnemies.forEach(enemy => {
938
+ if (enemy === this || !enemy.position) return;
939
+
940
+ const distance = this.position.distanceTo(enemy.position);
941
+ if (distance < 300 && distance > 0) {
942
+ // 반대 방향으로 회피
943
+ const avoidDir = this.position.clone().sub(enemy.position).normalize();
944
+ const strength = (300 - distance) / 300;
945
+ this.avoidanceVector.add(avoidDir.multiplyScalar(strength));
946
+ avoidCount++;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
947
  }
948
+ });
 
949
 
950
+ if (avoidCount > 0) {
951
+ this.avoidanceVector.divideScalar(avoidCount);
952
+ this.avoidanceVector.normalize();
953
+ }
954
  }
955
 
956
+ updatePhysics(deltaTime) {
957
+ if (!this.mesh) return;
 
 
958
 
959
+ // 속도 벡터 계산 (항상 전진)
960
+ const forward = new THREE.Vector3(0, 0, 1);
961
+ forward.applyEuler(this.rotation);
962
 
963
+ // 속도 유지 (500-750kt, m/s로 변환)
964
+ this.speed = THREE.MathUtils.clamp(this.speed, 257, 386);
965
+ this.velocity = forward.multiplyScalar(this.speed);
 
 
 
 
 
 
 
966
 
967
+ // 위치 업데이트
968
+ this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
969
+
970
+ // 고도 제한
971
+ if (this.position.y < 500) {
972
+ this.position.y = 500;
973
+ this.rotation.x = -0.1; // 약간 상승
974
+ } else if (this.position.y > 8000) {
975
+ this.position.y = 8000;
976
+ this.rotation.x = 0.1; // 약간 하강
977
  }
 
 
 
 
 
 
978
 
979
+ // 경계 처리
980
+ const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
981
+ const boundaryBuffer = mapLimit * 0.9;
982
 
983
+ if (Math.abs(this.position.x) > boundaryBuffer || Math.abs(this.position.z) > boundaryBuffer) {
984
+ // 중앙을 향해 회전
985
+ const centerDirection = new THREE.Vector3(-this.position.x, 0, -this.position.z).normalize();
986
+ const targetYaw = Math.atan2(centerDirection.x, centerDirection.z);
987
+ this.rotation.y = targetYaw;
988
+ this.selectNewPatrolTarget(); // 새로운 목표 선택
989
  }
990
 
991
+ // 하드 리미트
992
+ this.position.x = THREE.MathUtils.clamp(this.position.x, -mapLimit, mapLimit);
993
+ this.position.z = THREE.MathUtils.clamp(this.position.z, -mapLimit, mapLimit);
994
+
995
+ // 메시 업데이트
996
+ this.mesh.position.copy(this.position);
997
+ this.mesh.rotation.x = this.rotation.x;
998
+ this.mesh.rotation.y = this.rotation.y + Math.PI;
999
+ this.mesh.rotation.z = this.rotation.z;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1000
  }
1001
 
1002
+ fireWeapon() {
1003
+ const now = Date.now();
 
 
 
 
 
 
 
1004
 
1005
+ // 0.1초에 1발씩, 10발 연발
1006
+ if (now - this.lastShootTime >= 100 && this.burstCounter < 10) {
1007
+ this.shoot();
1008
+ this.lastShootTime = now;
1009
+ this.burstCounter++;
1010
+
1011
+ // 10발 발사 완료 시
1012
+ if (this.burstCounter >= 10) {
1013
+ this.burstCounter = 0;
1014
+ this.attackCounter++;
1015
+ }
1016
  }
1017
  }
1018
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1019
  shoot() {
1020
+ // 탄환 생성
 
 
1021
  const bulletGeometry = new THREE.CylinderGeometry(0.8, 0.8, 12, 8);
1022
  const bulletMaterial = new THREE.MeshBasicMaterial({
1023
  color: 0xff0000,
 
1031
  muzzleOffset.applyEuler(this.rotation);
1032
  bullet.position.copy(this.position).add(muzzleOffset);
1033
 
1034
+ // 탄환 회전
1035
  bullet.rotation.copy(this.rotation);
1036
  bullet.rotateX(Math.PI / 2);
1037
 
1038
+ // 탄환 속도
1039
  const direction = new THREE.Vector3(0, 0, 1);
1040
  direction.applyEuler(this.rotation);
1041
  bullet.velocity = direction.multiplyScalar(1200);
 
1043
  this.scene.add(bullet);
1044
  this.bullets.push(bullet);
1045
 
1046
+ // 사운드 재생
1047
  if (this.playerFighter) {
1048
  const distanceToPlayer = this.position.distanceTo(this.playerFighter.position);
1049
  if (distanceToPlayer < 3000) {
1050
  try {
1051
  const audio = new Audio('sounds/MGLAUNCH.ogg');
 
 
 
1052
  const volumeMultiplier = 1 - (distanceToPlayer / 3000);
1053
  audio.volume = 0.5 * volumeMultiplier;
1054
+ audio.play().catch(e => {});
1055
+ } catch (e) {}
 
 
 
 
 
 
 
1056
  }
1057
  }
1058
  }
1059
+
1060
  updateBullets(deltaTime) {
1061
  for (let i = this.bullets.length - 1; i >= 0; i--) {
1062
  const bullet = this.bullets[i];
1063
  bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
1064
 
1065
+ // 지면 충돌
1066
  if (bullet.position.y <= 0) {
 
1067
  if (window.gameInstance) {
1068
  window.gameInstance.createGroundImpactEffect(bullet.position);
1069
  }
 
1072
  continue;
1073
  }
1074
 
1075
+ // 거리 제한
1076
  if (bullet.position.distanceTo(this.position) > 5000) {
1077
  this.scene.remove(bullet);
1078
  this.bullets.splice(i, 1);
1079
  }
1080
  }
1081
  }
1082
+
1083
+ calculateAimAccuracy(target) {
1084
+ const toTarget = target.clone().sub(this.position).normalize();
1085
+ const forward = new THREE.Vector3(0, 0, 1).applyEuler(this.rotation);
1086
+ const dotProduct = forward.dot(toTarget);
1087
+ return Math.acos(Math.max(-1, Math.min(1, dotProduct)));
1088
+ }
1089
+
1090
+ selectNewPatrolTarget() {
1091
+ const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2 * 0.7;
1092
+ this.targetPosition = new THREE.Vector3(
1093
+ (Math.random() - 0.5) * 2 * mapLimit,
1094
+ 2000 + Math.random() * 2000, // 2000-4000m 고도
1095
+ (Math.random() - 0.5) * 2 * mapLimit
1096
+ );
1097
+ }
1098
+
1099
+ selectEvadeTarget() {
1100
+ // 현재 위치에서 랜덤 방향으로 회피
1101
+ const evadeDistance = 1000 + Math.random() * 1000;
1102
+ const evadeAngle = Math.random() * Math.PI * 2;
1103
+
1104
+ this.targetPosition = new THREE.Vector3(
1105
+ this.position.x + Math.cos(evadeAngle) * evadeDistance,
1106
+ this.position.y + (Math.random() - 0.5) * 500,
1107
+ this.position.z + Math.sin(evadeAngle) * evadeDistance
1108
+ );
1109
+
1110
+ // 맵 경계 확인
1111
+ const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2 * 0.8;
1112
+ this.targetPosition.x = THREE.MathUtils.clamp(this.targetPosition.x, -mapLimit, mapLimit);
1113
+ this.targetPosition.z = THREE.MathUtils.clamp(this.targetPosition.z, -mapLimit, mapLimit);
1114
+ this.targetPosition.y = THREE.MathUtils.clamp(this.targetPosition.y, 1000, 6000);
1115
+ }
1116
+
1117
+ normalizeAngle(angle) {
1118
+ while (angle > Math.PI) angle -= Math.PI * 2;
1119
+ while (angle < -Math.PI) angle += Math.PI * 2;
1120
+ return angle;
1121
+ }
1122
+
1123
  takeDamage(damage) {
1124
  this.health -= damage;
1125
  return this.health <= 0;
1126
  }
1127
+
1128
  destroy() {
1129
  if (this.mesh) {
1130
  this.scene.remove(this.mesh);