cutechicken commited on
Commit
3d5a5a4
ยท
verified ยท
1 Parent(s): dd5534d

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +719 -718
game.js CHANGED
@@ -718,729 +718,730 @@ class Fighter {
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 = 350; // ์ดˆ๊ธฐ ์†๋„ 350kt์™€ ์œ ์‚ฌํ•˜๊ฒŒ
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.throttle = 0.7; // ์ดˆ๊ธฐ ์Šค๋กœํ‹€ 70%
741
- this.altitude = position.y;
742
- this.gForce = 1.0;
743
-
744
- // ์กฐ์ข… ์ž…๋ ฅ ์‹œ์Šคํ…œ (ํ”Œ๋ ˆ์ด์–ด์™€ ์œ ์‚ฌ)
745
- this.pitchInput = 0;
746
- this.rollInput = 0;
747
- this.yawInput = 0;
748
-
749
- // ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „์„ ์œ„ํ•œ ๋ชฉํ‘œ๊ฐ’
750
- this.targetPitch = 0;
751
- this.targetRoll = 0;
752
- this.targetYaw = Math.atan2(this.velocity.x, this.velocity.z);
753
-
754
- // ์Šคํ†จ ์‹œ์Šคํ…œ
755
- this.stallWarning = false;
756
-
757
- // ๋‹ค๋ฅธ ์ ๊ธฐ๋“ค๊ณผ์˜ ์ถฉ๋Œ ํšŒํ”ผ
758
- this.nearbyEnemies = [];
759
- this.separationRadius = 500; // ์ตœ์†Œ ๊ฑฐ๋ฆฌ 500m
760
-
761
- // ์ „ํˆฌ ๊ด€๋ จ ๋ณ€์ˆ˜
762
- this.isEngaged = false;
763
- this.burstCount = 0;
764
- this.lastBurstTime = 0;
765
- this.predictedTargetPos = new THREE.Vector3();
766
-
767
- // ๊ธฐ๋™ ํŒจํ„ด ๋ณ€์ˆ˜
768
- this.maneuverState = 'straight'; // straight, turning, climbing, diving, evasive
769
- this.maneuverTimer = 0;
770
- this.nextManeuverTime = 3 + Math.random() * 4;
771
-
772
- // ์ „ํˆฌ ํŒจํ„ด
773
- this.combatPattern = 'approach'; // approach, attack, evade, reposition
774
- this.combatTimer = 0;
775
- this.lastAttackAngle = 0;
776
-
777
- // ์ˆœ์ฐฐ ๊ฒฝ๋กœ์ 
778
- this.patrolIndex = 0;
779
- this.arrivalThreshold = 300; // ๋ชฉํ‘œ ๋„๋‹ฌ ํŒ์ • ๊ฑฐ๋ฆฌ
780
- }
781
-
782
- generateRandomTarget() {
783
- // ๋งต ๋ฒ”์œ„ ๋‚ด ๋žœ๋ค ๋ชฉํ‘œ ์ง€์  ์ƒ์„ฑ
784
- const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2 * 0.8;
785
- return new THREE.Vector3(
786
- (Math.random() - 0.5) * 2 * mapLimit,
787
- 1500 + Math.random() * 2500, // 1500m ~ 4000m ๊ณ ๋„
788
- (Math.random() - 0.5) * 2 * mapLimit
789
- );
790
- }
791
-
792
- // ์ž์—ฐ์Šค๋Ÿฌ์šด ๊ธฐ๋™ ์„ ํƒ
793
- selectNewManeuver() {
794
- if (this.aiState === 'patrol') {
795
- // ์ˆœ์ฐฐ ์ค‘ ๊ธฐ๋™
796
- const maneuvers = ['straight', 'gentle_turn', 'altitude_change'];
797
- const weights = [0.5, 0.3, 0.2];
798
-
799
- let random = Math.random();
800
- let accumulator = 0;
801
-
802
- for (let i = 0; i < maneuvers.length; i++) {
803
- accumulator += weights[i];
804
- if (random <= accumulator) {
805
- this.maneuverState = maneuvers[i];
806
- break;
807
- }
808
- }
809
-
810
- this.maneuverTimer = 2 + Math.random() * 3;
811
- } else {
812
- // ์ „ํˆฌ ์ค‘ ๊ธฐ๋™
813
- const combatManeuvers = ['aggressive_turn', 'vertical_loop', 'barrel_roll', 'split_s'];
814
- this.maneuverState = combatManeuvers[Math.floor(Math.random() * combatManeuvers.length)];
815
- this.maneuverTimer = 1 + Math.random() * 2;
816
- }
817
- }
818
-
819
- // ์ „ํˆฌ ํŒจํ„ด ์„ ํƒ
820
- selectCombatPattern(playerPosition, distance) {
821
- const angleToPlayer = this.getAngleToTarget(playerPosition);
822
- const speedDiff = Math.abs(this.speed - this.playerFighter.speed);
823
-
824
- // ์ƒํ™ฉ์— ๋”ฐ๋ฅธ ์ „ํˆฌ ํŒจํ„ด ๊ฒฐ์ •
825
- if (distance > 2500) {
826
- this.combatPattern = 'approach';
827
- } else if (distance < 800 && angleToPlayer < Math.PI / 6) {
828
- this.combatPattern = 'attack';
829
- } else if (distance < 400 || (this.playerFighter && this.isPlayerBehind())) {
830
- this.combatPattern = 'evade';
831
- } else {
832
- this.combatPattern = 'reposition';
833
- }
834
-
835
- this.combatTimer = 0;
836
- }
837
-
838
- // ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๋’ค์— ์žˆ๋Š”์ง€ ํ™•์ธ
839
- isPlayerBehind() {
840
- if (!this.playerFighter) return false;
841
-
842
- const toPlayer = this.playerFighter.position.clone().sub(this.position).normalize();
843
- const forward = new THREE.Vector3(0, 0, 1).applyEuler(this.rotation);
844
- const dot = forward.dot(toPlayer);
845
-
846
- return dot < -0.5; // 120๋„ ์ด์ƒ ๋’ค์— ์žˆ์œผ๋ฉด true
847
- }
848
-
849
- // ํƒ€๊ฒŸ๊นŒ์ง€์˜ ๊ฐ๋„ ๊ณ„์‚ฐ
850
- getAngleToTarget(targetPosition) {
851
- const toTarget = targetPosition.clone().sub(this.position).normalize();
852
- const forward = new THREE.Vector3(0, 0, 1).applyEuler(this.rotation);
853
- return Math.acos(Math.max(-1, Math.min(1, forward.dot(toTarget))));
854
- }
855
-
856
- async initialize(loader) {
857
- try {
858
- const result = await loader.loadAsync('models/mig-29.glb');
859
- this.mesh = result.scene;
860
- this.mesh.position.copy(this.position);
861
- this.mesh.scale.set(1.5, 1.5, 1.5);
862
- this.mesh.rotation.y = 3 * Math.PI / 2;
863
-
864
- this.mesh.traverse((child) => {
865
- if (child.isMesh) {
866
- child.castShadow = true;
867
- child.receiveShadow = true;
868
- }
869
- });
870
-
871
- this.scene.add(this.mesh);
872
- this.isLoaded = true;
873
- console.log('MiG-29 ์ ๊ธฐ ๋กœ๋”ฉ ์™„๋ฃŒ');
874
- } catch (error) {
875
- console.error('MiG-29 ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ:', error);
876
- this.createFallbackModel();
877
- }
878
- }
 
 
 
 
 
 
 
 
 
879
 
880
- createFallbackModel() {
881
- const group = new THREE.Group();
882
-
883
- const fuselageGeometry = new THREE.CylinderGeometry(0.6, 1.0, 8, 8);
884
- const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x800000 });
885
- const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial);
886
- fuselage.rotation.x = -Math.PI / 2;
887
- group.add(fuselage);
888
-
889
- const wingGeometry = new THREE.BoxGeometry(12, 0.3, 3);
890
- const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x600000 });
891
- const wings = new THREE.Mesh(wingGeometry, wingMaterial);
892
- wings.position.z = -0.5;
893
- group.add(wings);
894
-
895
- this.mesh = group;
896
- this.mesh.position.copy(this.position);
897
- this.mesh.scale.set(1.5, 1.5, 1.5);
898
- this.scene.add(this.mesh);
899
- this.isLoaded = true;
900
-
901
- console.log('Fallback ์ ๊ธฐ ๋ชจ๋ธ ์ƒ์„ฑ ์™„๋ฃŒ');
902
- }
903
 
904
- update(playerPosition, deltaTime) {
905
- if (!this.mesh) return;
906
-
907
- // AI ์ƒํƒœ ์—…๋ฐ์ดํŠธ
908
- const distanceToPlayer = this.position.distanceTo(playerPosition);
909
-
910
- // ์ „ํˆฌ ๊ฑฐ๋ฆฌ ์ฒดํฌ (3000m ์ด๋‚ด)
911
- if (distanceToPlayer < 3000) {
912
- if (this.aiState !== 'combat') {
913
- this.aiState = 'combat';
914
- this.isEngaged = true;
915
- console.log('Enemy entering combat mode');
916
- }
917
- } else if (distanceToPlayer > 5000) {
918
- if (this.aiState !== 'patrol') {
919
- this.aiState = 'patrol';
920
- this.isEngaged = false;
921
- this.combatPattern = 'approach';
922
- console.log('Enemy returning to patrol');
923
- }
924
- }
925
-
926
- // ๋‹ค๋ฅธ ์ ๊ธฐ์™€์˜ ์ถฉ๋Œ ํšŒํ”ผ
927
- this.avoidOtherEnemies(deltaTime);
928
-
929
- // ๊ธฐ๋™ ํƒ€์ด๋จธ ์—…๋ฐ์ดํŠธ
930
- this.maneuverTimer -= deltaTime;
931
- if (this.maneuverTimer <= 0) {
932
- this.selectNewManeuver();
933
- }
934
-
935
- // AI ํ–‰๋™ ์‹คํ–‰
936
- switch (this.aiState) {
937
- case 'patrol':
938
- this.patrol(deltaTime);
939
- break;
940
- case 'combat':
941
- this.combat(playerPosition, deltaTime);
942
- break;
943
- }
944
-
945
- // ๋ฌผ๋ฆฌ ์—…๋ฐ์ดํŠธ (ํ”Œ๋ ˆ์ด์–ด์™€ ์œ ์‚ฌ)
946
- this.updatePhysics(deltaTime);
947
-
948
- // ํƒ„ํ™˜ ์—…๋ฐ์ดํŠธ
949
- this.updateBullets(deltaTime);
950
- }
951
-
952
- avoidOtherEnemies(deltaTime) {
953
- if (!this.nearbyEnemies) return;
954
-
955
- let avoidanceForce = new THREE.Vector3();
956
- let tooClose = false;
957
-
958
- this.nearbyEnemies.forEach(enemy => {
959
- if (enemy === this || !enemy.position) return;
960
-
961
- const distance = this.position.distanceTo(enemy.position);
962
- if (distance < this.separationRadius && distance > 0) {
963
- // ํšŒํ”ผ ๋ฒกํ„ฐ ๊ณ„์‚ฐ
964
- const avoid = this.position.clone().sub(enemy.position).normalize();
965
- const strength = (this.separationRadius - distance) / this.separationRadius;
966
- avoidanceForce.add(avoid.multiplyScalar(strength));
967
- tooClose = true;
968
- }
969
- });
970
-
971
- if (tooClose) {
972
- // ํšŒํ”ผ ๊ธฐ๋™ ์‹คํ–‰
973
- avoidanceForce.normalize();
974
- const avoidAngle = Math.atan2(avoidanceForce.x, avoidanceForce.z);
975
- this.targetYaw = avoidAngle;
976
-
977
- // ๊ณ ๋„ ๋ถ„๋ฆฌ
978
- if (avoidanceForce.y > 0.5) {
979
- this.targetPitch = -0.2; // ์ƒ์Šน
980
- } else if (avoidanceForce.y < -0.5) {
981
- this.targetPitch = 0.2; // ํ•˜๊ฐ•
982
- }
983
- }
984
- }
985
-
986
- patrol(deltaTime) {
987
- // ๋ชฉํ‘œ ์ง€์ ๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ์™€ ๋ฐฉํ–ฅ ๊ณ„์‚ฐ
988
- const toTarget = this.targetPosition.clone().sub(this.position);
989
- const distance = toTarget.length();
990
-
991
- // ๋ชฉํ‘œ ๋„๋‹ฌ ํ™•์ธ
992
- if (distance < this.arrivalThreshold) {
993
- console.log('Enemy reached patrol point, generating new target');
994
- this.targetPosition = this.generateRandomTarget();
995
- this.selectNewManeuver();
996
- }
997
-
998
- // ๋ชฉํ‘œ๋ฅผ ํ–ฅํ•œ ๋ฐฉํ–ฅ ์กฐ์ •
999
- if (distance > 0) {
1000
- toTarget.normalize();
1001
-
1002
- // ๋ชฉํ‘œ ๊ฐ๋„ ๊ณ„์‚ฐ
1003
- const targetYaw = Math.atan2(toTarget.x, toTarget.z);
1004
- const targetPitch = Math.asin(-toTarget.y);
1005
-
1006
- // ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „
1007
- const yawDiff = this.normalizeAngle(targetYaw - this.rotation.y);
1008
- const pitchDiff = targetPitch - this.rotation.x;
1009
-
1010
- // ๊ธฐ๋™ ์ƒํƒœ์— ๋”ฐ๋ฅธ ์กฐ์ข…
1011
- switch (this.maneuverState) {
1012
- case 'straight':
1013
- // ๋ชฉํ‘œ๋ฅผ ํ–ฅํ•ด ์ง์ง„
1014
- this.targetYaw += yawDiff * deltaTime * 0.5;
1015
- this.targetPitch += pitchDiff * deltaTime * 0.3;
1016
- this.targetRoll *= 0.9; // ๋กค ๋ณต๊ท€
1017
- break;
1018
-
1019
- case 'gentle_turn':
1020
- // ๋ถ€๋“œ๋Ÿฌ์šด ์„ ํšŒ
1021
- const turnDirection = Math.sign(yawDiff);
1022
- this.targetYaw += turnDirection * deltaTime * 0.8;
1023
- this.targetRoll = turnDirection * Math.PI / 6; // 30๋„ ๋ฑ…ํฌ
1024
- break;
1025
-
1026
- case 'altitude_change':
1027
- // ๊ณ ๋„ ๋ณ€๊ฒฝ
1028
- if (this.position.y < this.targetPosition.y) {
1029
- this.targetPitch = -0.2; // ์ƒ์Šน
1030
- this.throttle = Math.min(0.9, this.throttle + deltaTime * 0.3);
1031
- } else {
1032
- this.targetPitch = 0.1; // ํ•˜๊ฐ•
1033
- this.throttle = Math.max(0.5, this.throttle - deltaTime * 0.2);
1034
- }
1035
- break;
1036
- }
1037
-
1038
- // ์†๋„ ๊ด€๋ฆฌ
1039
- if (this.speed < 300) {
1040
- this.throttle = Math.min(1.0, this.throttle + deltaTime * 0.5);
1041
- } else if (this.speed > 450) {
1042
- this.throttle = Math.max(0.3, this.throttle - deltaTime * 0.3);
1043
- }
1044
- }
1045
- }
1046
-
1047
- combat(playerPosition, deltaTime) {
1048
- if (!this.playerFighter) return;
1049
-
1050
- const distance = this.position.distanceTo(playerPosition);
1051
-
1052
- // ์ „ํˆฌ ํŒจํ„ด ์—…๏ฟฝ๏ฟฝ๏ฟฝ์ดํŠธ
1053
- this.selectCombatPattern(playerPosition, distance);
1054
- this.combatTimer += deltaTime;
1055
-
1056
- // ์˜ˆ์ธก ์‚ฌ๊ฒฉ์„ ์œ„ํ•œ ๋ฆฌ๋“œ ๊ณ„์‚ฐ
1057
- const bulletSpeed = 1200;
1058
- const timeToTarget = distance / bulletSpeed;
1059
- this.predictedTargetPos = playerPosition.clone().add(
1060
- this.playerFighter.velocity.clone().multiplyScalar(timeToTarget * 0.8)
1061
- );
1062
-
1063
- // ์ „ํˆฌ ํŒจํ„ด๋ณ„ ํ–‰๋™
1064
- switch (this.combatPattern) {
1065
- case 'approach':
1066
- this.executeApproach(this.predictedTargetPos, deltaTime);
1067
- break;
1068
-
1069
- case 'attack':
1070
- this.executeAttack(this.predictedTargetPos, deltaTime, distance);
1071
- break;
1072
-
1073
- case 'evade':
1074
- this.executeEvasive(deltaTime);
1075
- break;
1076
-
1077
- case 'reposition':
1078
- this.executeReposition(playerPosition, deltaTime);
1079
- break;
1080
- }
1081
-
1082
- // ์†๋„ ๊ด€๋ฆฌ
1083
- this.updateCombatSpeed(deltaTime);
1084
- }
1085
-
1086
- executeApproach(target, deltaTime) {
1087
- const toTarget = target.clone().sub(this.position);
1088
- const distance = toTarget.length();
1089
-
1090
- if (distance > 0) {
1091
- toTarget.normalize();
1092
-
1093
- // ๋ฆฌ๋“œ ํ„ด - ๋ชฉํ‘œ์˜ ๋ฏธ๋ž˜ ์œ„์น˜๋ฅผ ์˜ˆ์ธกํ•˜์—ฌ ์„ ํšŒ
1094
- const targetYaw = Math.atan2(toTarget.x, toTarget.z);
1095
- const targetPitch = Math.asin(-toTarget.y);
1096
-
1097
- const yawDiff = this.normalizeAngle(targetYaw - this.rotation.y);
1098
- const pitchDiff = targetPitch - this.rotation.x;
1099
-
1100
- // ๊ณต๊ฒฉ์ ์ธ ์ ‘๊ทผ
1101
- this.targetYaw += yawDiff * deltaTime * 1.2;
1102
- this.targetPitch += pitchDiff * deltaTime * 0.8;
1103
-
1104
- // ๋ฑ…ํฌ ํ„ด
1105
- if (Math.abs(yawDiff) > 0.1) {
1106
- this.targetRoll = Math.sign(yawDiff) * Math.PI / 4; // 45๋„ ๋ฑ…ํฌ
1107
- } else {
1108
- this.targetRoll *= 0.9;
1109
- }
1110
-
1111
- // ์ตœ๋Œ€ ์†๋„๋กœ ์ ‘๊ทผ
1112
- this.throttle = 0.9;
1113
- }
1114
- }
1115
-
1116
- executeAttack(target, deltaTime, distance) {
1117
- const toTarget = target.clone().sub(this.position);
1118
- const angle = this.getAngleToTarget(target);
1119
-
1120
- if (distance > 0) {
1121
- toTarget.normalize();
1122
-
1123
- // ์ •๋ฐ€ ์กฐ์ค€
1124
- const targetYaw = Math.atan2(toTarget.x, toTarget.z);
1125
- const targetPitch = Math.asin(-toTarget.y);
1126
-
1127
- const yawDiff = this.normalizeAngle(targetYaw - this.rotation.y);
1128
- const pitchDiff = targetPitch - this.rotation.x;
1129
-
1130
- // ์•ˆ์ •์ ์ธ ์‚ฌ๊ฒฉ ์ž์„ธ
1131
- this.targetYaw += yawDiff * deltaTime * 1.5;
1132
- this.targetPitch += pitchDiff * deltaTime * 1.0;
1133
- this.targetRoll = -yawDiff * 0.3;
1134
-
1135
- // ์‚ฌ๊ฒฉ ํŒ๋‹จ
1136
- if (angle < Math.PI / 12 && distance < 1500) { // 15๋„ ์ด๋‚ด, 1500m ์ด๋‚ด
1137
- this.fireWeapon();
1138
- }
1139
-
1140
- // ์†๋„ ์กฐ์ ˆ
1141
- if (distance < 600) {
1142
- this.throttle = 0.5; // ๋„ˆ๋ฌด ๊ฐ€๊นŒ์šฐ๋ฉด ๊ฐ์†
1143
- } else {
1144
- this.throttle = 0.8;
1145
- }
1146
- }
1147
- }
1148
-
1149
- executeEvasive(deltaTime) {
1150
- // ๋ฐฉ์–ด์  ๊ธฐ๋™
1151
- const evasiveType = this.combatTimer < 1.5 ? 'break_turn' : 'vertical';
1152
-
1153
- if (evasiveType === 'break_turn') {
1154
- // ๊ธ‰๊ฒฉํ•œ ๋ธŒ๋ ˆ์ดํฌ ํ„ด
1155
- const direction = Math.sign(this.rotation.z) || 1;
1156
- this.targetRoll = direction * Math.PI / 2; // 90๋„ ๋กค
1157
- this.targetYaw += direction * deltaTime * 2.5;
1158
- this.targetPitch = 0.1;
1159
- this.throttle = 0.3; // ๊ธ‰๊ฐ์†
1160
- } else {
1161
- // ์ˆ˜์ง ๊ธฐ๋™ (Immelmann turn)
1162
- this.targetPitch = -Math.PI / 3; // 60๋„ ์ƒ์Šน
1163
- this.targetRoll = Math.PI; // 180๋„ ๋กค
1164
- this.throttle = 1.0; // ์ตœ๋Œ€ ์ถœ๋ ฅ
1165
- }
1166
-
1167
- // 3์ดˆ ํ›„ ํŒจํ„ด ๋ณ€๊ฒฝ
1168
- if (this.combatTimer > 3.0) {
1169
- this.combatPattern = 'reposition';
1170
- this.combatTimer = 0;
1171
- }
1172
- }
1173
-
1174
- executeReposition(playerPosition, deltaTime) {
1175
- // ์œ ๋ฆฌํ•œ ์œ„์น˜ ํ™•๋ณด
1176
- const offset = new THREE.Vector3(
1177
- Math.sin(this.combatTimer) * 1000,
1178
- 500,
1179
- Math.cos(this.combatTimer) * 1000
1180
- );
1181
-
1182
- const repositionTarget = playerPosition.clone().add(offset);
1183
- this.executeApproach(repositionTarget, deltaTime);
1184
-
1185
- // ์—๋„ˆ์ง€ ํšŒ๋ณต
1186
- this.throttle = 0.8;
1187
-
1188
- // 5์ดˆ ํ›„ ์žฌ๊ณต๊ฒฉ
1189
- if (this.combatTimer > 5.0) {
1190
- this.combatPattern = 'approach';
1191
- this.combatTimer = 0;
1192
- }
1193
- }
1194
-
1195
- updateCombatSpeed(deltaTime) {
1196
- // ์ „ํˆฌ ์ค‘ ์†๋„ ๊ด€๋ฆฌ
1197
- const minCombatSpeed = 250;
1198
- const maxCombatSpeed = 500;
1199
-
1200
- if (this.speed < minCombatSpeed) {
1201
- this.throttle = Math.min(1.0, this.throttle + deltaTime * 0.5);
1202
- } else if (this.speed > maxCombatSpeed) {
1203
- this.throttle = Math.max(0.3, this.throttle - deltaTime * 0.3);
1204
- }
1205
-
1206
- // G-Force ์ œํ•œ
1207
- if (this.gForce > 7.0) {
1208
- // ๊ณผ๋„ํ•œ G-Force ์‹œ ๊ธฐ๋™ ์ œํ•œ
1209
- this.targetPitch *= 0.8;
1210
- this.targetRoll *= 0.8;
1211
- }
1212
- }
1213
-
1214
- fireWeapon() {
1215
- const now = Date.now();
1216
-
1217
- // ์—ฐ๋ฐœ ์ œ์–ด
1218
- if (now - this.lastBurstTime > 2000) {
1219
- this.burstCount = 0;
1220
- this.lastBurstTime = now;
1221
- }
1222
-
1223
- if (this.burstCount < 5 && now - this.lastShootTime > 150) {
1224
- this.shoot();
1225
- this.burstCount++;
1226
- }
1227
- }
1228
-
1229
- // ๊ฐ๋„ ์ •๊ทœํ™”
1230
- normalizeAngle(angle) {
1231
- while (angle > Math.PI) angle -= Math.PI * 2;
1232
- while (angle < -Math.PI) angle += Math.PI * 2;
1233
- return angle;
1234
- }
1235
-
1236
- updatePhysics(deltaTime) {
1237
- if (!this.mesh) return;
1238
-
1239
- // ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „ ๋ณด๊ฐ„ (ํ”Œ๋ ˆ์ด์–ด์™€ ์œ ์‚ฌ)
1240
- const rotationSpeed = deltaTime * 2.0;
1241
-
1242
- this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetPitch, rotationSpeed);
1243
- this.rotation.y = THREE.MathUtils.lerp(this.rotation.y, this.targetYaw, rotationSpeed);
1244
- this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRoll, rotationSpeed * 1.5);
1245
-
1246
- // ๋กค์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ์š” ํšŒ์ „ (๋ฑ…ํฌ ํ„ด)
1247
- if (Math.abs(this.rotation.z) > 0.1 && !this.stallWarning) {
1248
- const bankAngle = this.rotation.z;
1249
- const turnRate = Math.sin(bankAngle) * deltaTime * 1.5;
1250
- this.targetYaw += turnRate;
1251
- }
1252
-
1253
- // ์†๋„ ๊ณ„์‚ฐ (ํ”Œ๋ ˆ์ด์–ด์™€ ์œ ์‚ฌํ•œ ๋ฌผ๋ฆฌ)
1254
- const minSpeed = 200; // ์ตœ์†Œ ์†๋„ 200m/s
1255
- const maxSpeed = 550; // ์ตœ๋Œ€ ์†๋„ 550m/s
1256
- let targetSpeed = minSpeed + (maxSpeed - minSpeed) * this.throttle;
1257
-
1258
- // ํ”ผ์น˜ ๊ฐ๋„์— ๋”ฐ๋ฅธ ์†๋„ ๋ณ€ํ™”
1259
- const pitchAngle = this.rotation.x;
1260
- if (pitchAngle < -0.1) { // ์ƒ์Šน
1261
- const climbFactor = Math.abs(pitchAngle) / (Math.PI / 2);
1262
- targetSpeed *= (1 - climbFactor * 0.3);
1263
- } else if (pitchAngle > 0.1) { // ํ•˜๊ฐ•
1264
- const diveFactor = pitchAngle / (Math.PI / 3);
1265
- targetSpeed *= (1 + diveFactor * 0.4);
1266
- }
1267
-
1268
- // G-Force ๊ณ„์‚ฐ
1269
- const turnRate2 = Math.abs(this.targetYaw - this.rotation.y) * 10;
1270
- const pitchRate = Math.abs(this.targetPitch - this.rotation.x) * 10;
1271
- this.gForce = 1.0 + turnRate2 + pitchRate + Math.abs(this.rotation.z) * 3;
1272
-
1273
- // ์Šคํ†จ ์ฒดํฌ
1274
- const speedKnots = this.speed * 1.94384;
1275
- if (speedKnots < GAME_CONSTANTS.STALL_SPEED && !this.stallWarning) {
1276
- this.stallWarning = true;
1277
- } else if (speedKnots > GAME_CONSTANTS.STALL_SPEED + 50 && this.stallWarning) {
1278
- this.stallWarning = false;
1279
- }
1280
-
1281
- // ์Šคํ†จ ์‹œ ๋น„์ƒ ํ•˜๊ฐ•
1282
- if (this.stallWarning) {
1283
- this.targetPitch = Math.min(Math.PI / 4, this.targetPitch + deltaTime * 1.5);
1284
- this.rotation.x += (Math.random() - 0.5) * deltaTime * 0.3;
1285
- this.rotation.z += (Math.random() - 0.5) * deltaTime * 0.3;
1286
-
1287
- // ์ค‘๋ ฅ ๊ฐ€์†
1288
- this.velocity.y -= GAME_CONSTANTS.GRAVITY * deltaTime * 2.0;
1289
- }
1290
-
1291
- // ์†๋„ ๋ณ€ํ™” ์ ์šฉ
1292
- this.speed = THREE.MathUtils.lerp(this.speed, targetSpeed, deltaTime * 0.5);
1293
-
1294
- // ์†๋„ ๋ฒกํ„ฐ ๊ณ„์‚ฐ
1295
- const noseDirection = new THREE.Vector3(0, 0, 1);
1296
- noseDirection.applyEuler(this.rotation);
1297
-
1298
- if (!this.stallWarning) {
1299
- this.velocity = noseDirection.multiplyScalar(this.speed);
1300
- } else {
1301
- // ์Šคํ†จ ์‹œ ๋ถ€๋ถ„์ ์ธ ์ œ์–ด๋งŒ ๊ฐ€๋Šฅ
1302
- this.velocity.x = noseDirection.x * this.speed * 0.3;
1303
- this.velocity.z = noseDirection.z * this.speed * 0.3;
1304
- }
1305
-
1306
- // ์ค‘๋ ฅ๊ณผ ์–‘๋ ฅ
1307
- if (!this.stallWarning) {
1308
- const gravityEffect = GAME_CONSTANTS.GRAVITY * deltaTime * 0.15;
1309
- this.velocity.y -= gravityEffect;
1310
-
1311
- const liftFactor = (this.speed / maxSpeed) * 0.8;
1312
- const lift = gravityEffect * liftFactor;
1313
- this.velocity.y += lift;
1314
- }
1315
-
1316
- // ์œ„์น˜ ์—…๋ฐ์ดํŠธ
1317
- this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
1318
-
1319
- // ๊ณ ๋„ ์ œํ•œ
1320
- if (this.position.y < 200) {
1321
- this.position.y = 200;
1322
- this.velocity.y = Math.max(0, this.velocity.y);
1323
- this.targetPitch = -0.3; // ๊ฐ•์ œ ์ƒ์Šน
1324
- } else if (this.position.y > 10000) {
1325
- this.position.y = 10000;
1326
- this.velocity.y = Math.min(0, this.velocity.y);
1327
- }
1328
-
1329
- // ๋งต ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ
1330
- const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
1331
- const boundaryBuffer = mapLimit * 0.9;
1332
-
1333
- if (Math.abs(this.position.x) > boundaryBuffer || Math.abs(this.position.z) > boundaryBuffer) {
1334
- // ๋งต ์ค‘์•™์„ ํ–ฅํ•ด ํšŒ์ „
1335
- const centerDirection = new THREE.Vector3(-this.position.x, 0, -this.position.z).normalize();
1336
- this.targetYaw = Math.atan2(centerDirection.x, centerDirection.z);
1337
- this.targetRoll = Math.sign(this.position.x) * Math.PI / 4;
1338
- }
1339
-
1340
- // ํ•˜๋“œ ๋ฆฌ๋ฏธํŠธ
1341
- if (this.position.x > mapLimit) this.position.x = mapLimit;
1342
- if (this.position.x < -mapLimit) this.position.x = -mapLimit;
1343
- if (this.position.z > mapLimit) this.position.z = mapLimit;
1344
- if (this.position.z < -mapLimit) this.position.z = -mapLimit;
1345
-
1346
- // ๋ฉ”์‹œ ์—…๋ฐ์ดํŠธ
1347
- this.mesh.position.copy(this.position);
1348
- this.mesh.rotation.x = this.rotation.x;
1349
- this.mesh.rotation.y = this.rotation.y + Math.PI;
1350
- this.mesh.rotation.z = this.rotation.z;
1351
-
1352
- // ๊ณ ๋„ ์—…๋ฐ์ดํŠธ
1353
- this.altitude = this.position.y;
1354
- }
1355
-
1356
- shoot() {
1357
- this.lastShootTime = Date.now();
1358
-
1359
- // ์ง์„  ๋ชจ์–‘์˜ ํƒ„ํ™˜ (100% ๋” ํฌ๊ฒŒ)
1360
- const bulletGeometry = new THREE.CylinderGeometry(0.8, 0.8, 12, 8);
1361
- const bulletMaterial = new THREE.MeshBasicMaterial({
1362
- color: 0xff0000,
1363
- emissive: 0xff0000,
1364
- emissiveIntensity: 1.0
1365
- });
1366
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
1367
-
1368
- // ๊ธฐ์ˆ˜ ๋์—์„œ ๋ฐœ์‚ฌ
1369
- const muzzleOffset = new THREE.Vector3(0, 0, 12);
1370
- muzzleOffset.applyEuler(this.rotation);
1371
- bullet.position.copy(this.position).add(muzzleOffset);
1372
-
1373
- // ํƒ„ํ™˜์„ ๋ฐœ์‚ฌ ๋ฐฉํ–ฅ์œผ๋กœ ํšŒ์ „
1374
- bullet.rotation.copy(this.rotation);
1375
- bullet.rotateX(Math.PI / 2);
1376
-
1377
- const direction = new THREE.Vector3(0, 0, 1);
1378
- direction.applyEuler(this.rotation);
1379
- bullet.velocity = direction.multiplyScalar(1200);
1380
-
1381
- this.scene.add(bullet);
1382
- this.bullets.push(bullet);
1383
-
1384
- // MGLAUNCH.ogg ์†Œ๋ฆฌ ์žฌ์ƒ - ํ”Œ๋ ˆ์ด์–ด๊ฐ€ 3000m ์ด๋‚ด์— ์žˆ์„ ๋•Œ๋งŒ
1385
- if (this.playerFighter) {
1386
- const distanceToPlayer = this.position.distanceTo(this.playerFighter.position);
1387
- if (distanceToPlayer < 3000) {
1388
- try {
1389
- const audio = new Audio('sounds/MGLAUNCH.ogg');
1390
- audio.volume = 0.5;
1391
-
1392
- // ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ฅธ ์Œ๋Ÿ‰ ์กฐ์ ˆ
1393
- const volumeMultiplier = 1 - (distanceToPlayer / 3000);
1394
- audio.volume = 0.5 * volumeMultiplier;
1395
-
1396
- audio.play().catch(e => console.log('Enemy gunfire sound failed to play'));
1397
-
1398
- audio.addEventListener('ended', () => {
1399
- audio.remove();
1400
- });
1401
- } catch (e) {
1402
- console.log('Audio error:', e);
1403
- }
1404
- }
1405
- }
1406
- }
1407
 
1408
- updateBullets(deltaTime) {
1409
- for (let i = this.bullets.length - 1; i >= 0; i--) {
1410
- const bullet = this.bullets[i];
1411
- bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
1412
-
1413
- // ์ง€๋ฉด ์ถฉ๋Œ ์ฒดํฌ
1414
- if (bullet.position.y <= 0) {
1415
- // ํฌ๊ณ  ํ™”๋ คํ•œ ์ง€๋ฉด ์ถฉ๋Œ ํšจ๊ณผ
1416
- if (window.gameInstance) {
1417
- window.gameInstance.createGroundImpactEffect(bullet.position);
1418
- }
1419
- this.scene.remove(bullet);
1420
- this.bullets.splice(i, 1);
1421
- continue;
1422
- }
1423
-
1424
- if (bullet.position.distanceTo(this.position) > 5000) {
1425
- this.scene.remove(bullet);
1426
- this.bullets.splice(i, 1);
1427
- }
1428
- }
1429
- }
1430
 
1431
- takeDamage(damage) {
1432
- this.health -= damage;
1433
- return this.health <= 0;
1434
- }
1435
 
1436
- destroy() {
1437
- if (this.mesh) {
1438
- this.scene.remove(this.mesh);
1439
- this.bullets.forEach(bullet => this.scene.remove(bullet));
1440
- this.bullets = [];
1441
- this.isLoaded = false;
1442
- }
1443
- }
1444
  }
1445
 
1446
  // ๋ฉ”์ธ ๊ฒŒ์ž„ ํด๋ž˜์Šค
 
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) {
875
+ child.castShadow = true;
876
+ child.receiveShadow = true;
877
+ }
878
+ });
879
+
880
+ this.scene.add(this.mesh);
881
+ this.isLoaded = true;
882
+ console.log('MiG-29 ์ ๊ธฐ ๋กœ๋”ฉ ์™„๋ฃŒ');
883
+ } catch (error) {
884
+ console.error('MiG-29 ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ:', error);
885
+ this.createFallbackModel();
886
+ }
887
+ }
888
 
889
+ createFallbackModel() {
890
+ const group = new THREE.Group();
891
+
892
+ const fuselageGeometry = new THREE.CylinderGeometry(0.6, 1.0, 8, 8);
893
+ const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x800000 });
894
+ const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial);
895
+ fuselage.rotation.x = -Math.PI / 2;
896
+ group.add(fuselage);
897
+
898
+ const wingGeometry = new THREE.BoxGeometry(12, 0.3, 3);
899
+ const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x600000 });
900
+ const wings = new THREE.Mesh(wingGeometry, wingMaterial);
901
+ wings.position.z = -0.5;
902
+ group.add(wings);
903
+
904
+ this.mesh = group;
905
+ this.mesh.position.copy(this.position);
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
+
949
+ // ๋ฌผ๋ฆฌ ์—…๋ฐ์ดํŠธ
950
+ this.updatePhysics(deltaTime);
951
+
952
+ // ํƒ„ํ™˜ ์—…๋ฐ์ดํŠธ
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,
1364
+ emissive: 0xff0000,
1365
+ emissiveIntensity: 1.0
1366
+ });
1367
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
1368
+
1369
+ // ๊ธฐ์ˆ˜ ๋์—์„œ ๋ฐœ์‚ฌ
1370
+ const muzzleOffset = new THREE.Vector3(0, 0, 12);
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);
1381
+
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
+ }
1420
+ this.scene.remove(bullet);
1421
+ this.bullets.splice(i, 1);
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);
1440
+ this.bullets.forEach(bullet => this.scene.remove(bullet));
1441
+ this.bullets = [];
1442
+ this.isLoaded = false;
1443
+ }
1444
+ }
1445
  }
1446
 
1447
  // ๋ฉ”์ธ ๊ฒŒ์ž„ ํด๋ž˜์Šค