cutechicken commited on
Commit
dd5534d
ยท
verified ยท
1 Parent(s): 12284be

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +718 -719
game.js CHANGED
@@ -718,730 +718,729 @@ 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 = 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
  // ๋ฉ”์ธ ๊ฒŒ์ž„ ํด๋ž˜์Šค
 
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
  // ๋ฉ”์ธ ๊ฒŒ์ž„ ํด๋ž˜์Šค