cutechicken commited on
Commit
876ef81
โ€ข
1 Parent(s): d825a32

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +298 -214
game.js CHANGED
@@ -579,65 +579,30 @@ class Enemy {
579
  this.bullets = [];
580
  this.isLoaded = false;
581
  this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
582
- }
583
- createMuzzleFlash() {
584
- if (!this.mesh) return;
585
-
586
- const flashGroup = new THREE.Group();
587
-
588
- // ํ™”์—ผ ํฌ๊ธฐ ์ฆ๊ฐ€ ๋ฐ ์ƒ‰์ƒ ๋ณ€๊ฒฝ
589
- const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8);
590
- const flameMaterial = new THREE.MeshBasicMaterial({
591
- color: 0xffa500, // ๋…ธ๋ž€์ƒ‰์œผ๋กœ ๋ณ€๊ฒฝ
592
- transparent: true,
593
- opacity: 0.8
594
- });
595
- const flame = new THREE.Mesh(flameGeometry, flameMaterial);
596
- flame.scale.set(2, 2, 3);
597
- flashGroup.add(flame);
598
 
599
- // ์—ฐ๊ธฐ ํšจ๊ณผ ํฌ๊ธฐ ์ฆ๊ฐ€
600
- const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8);
601
- const smokeMaterial = new THREE.MeshBasicMaterial({
602
- color: 0x555555,
603
- transparent: true,
604
- opacity: 0.5
605
- });
606
-
607
- for (let i = 0; i < 5; i++) {
608
- const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial);
609
- smoke.position.set(
610
- Math.random() * 1 - 0.5,
611
- Math.random() * 1 - 0.5,
612
- -1 - Math.random()
613
- );
614
- smoke.scale.set(1.5, 1.5, 1.5);
615
- flashGroup.add(smoke);
616
  }
617
 
618
- // ํฌ๊ตฌ ์œ„์น˜ ๊ณ„์‚ฐ
619
- const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
620
- const muzzlePosition = new THREE.Vector3();
621
- const meshWorldQuaternion = new THREE.Quaternion();
622
-
623
- this.mesh.getWorldPosition(muzzlePosition);
624
- this.mesh.getWorldQuaternion(meshWorldQuaternion);
625
-
626
- muzzleOffset.applyQuaternion(meshWorldQuaternion);
627
- muzzlePosition.add(muzzleOffset);
628
-
629
- flashGroup.position.copy(muzzlePosition);
630
- flashGroup.quaternion.copy(meshWorldQuaternion);
631
-
632
- this.scene.add(flashGroup);
633
-
634
- // ์ดํŽ™ํŠธ ์ง€์† ์‹œ๊ฐ„ ์ฆ๊ฐ€
635
- setTimeout(() => {
636
- this.scene.remove(flashGroup);
637
- }, 500);
638
- }
639
-
640
-
641
  async initialize(loader) {
642
  try {
643
  const modelPath = this.type === 'tank' ? '/models/t90.glb' : '/models/t90.glb';
@@ -661,107 +626,189 @@ class Enemy {
661
  }
662
  }
663
 
664
- update(playerPosition) {
665
- if (!this.mesh || !this.isLoaded) return;
666
 
667
- const direction = new THREE.Vector3()
668
- .subVectors(playerPosition, this.mesh.position)
669
- .normalize();
670
-
671
- const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
672
- const minDistance = 50;
673
-
674
- // ์ด์ „ ์œ„์น˜ ์ €์žฅ
675
- const previousPosition = this.mesh.position.clone();
676
-
677
- if (distanceToPlayer > minDistance) {
678
- const moveVector = direction.multiplyScalar(this.moveSpeed);
679
- const newPosition = this.mesh.position.clone().add(moveVector);
680
-
681
- // ์ง€ํ˜• ๋†’์ด ๊ฐ€์ ธ์˜ค๊ธฐ
682
- const heightAtNewPos = window.gameInstance.getHeightAtPosition(
683
- newPosition.x,
684
- newPosition.z
685
- );
686
- newPosition.y = heightAtNewPos + TANK_HEIGHT;
687
-
688
- // ์ž„์‹œ๋กœ ์œ„์น˜ ์ด๋™ํ•˜์—ฌ ์ถฉ๋Œ ์ฒดํฌ
689
- const originalPosition = this.mesh.position.clone();
690
- this.mesh.position.copy(newPosition);
691
-
692
- // ์žฅ์• ๋ฌผ๊ณผ ์ถฉ๋Œ ์ฒดํฌ
693
- const enemyBox = new THREE.Box3().setFromObject(this.mesh);
694
- let hasCollision = false;
695
-
696
- // ๋ชจ๋“  ์žฅ์• ๋ฌผ์— ๋Œ€ํ•ด ์ถฉ๋Œ ๊ฒ€์‚ฌ
697
- for (const obstacle of window.gameInstance.obstacles) {
698
- const obstacleBox = new THREE.Box3().setFromObject(obstacle);
699
- if (enemyBox.intersectsBox(obstacleBox)) {
700
- hasCollision = true;
701
- break;
702
  }
703
  }
704
-
705
- // ๋‹ค๋ฅธ ์  ํƒฑํฌ์™€์˜ ์ถฉ๋Œ ๊ฒ€์‚ฌ
706
- if (!hasCollision) {
707
- for (const otherEnemy of window.gameInstance.enemies) {
708
- if (otherEnemy !== this && otherEnemy.mesh) {
709
- const otherEnemyBox = new THREE.Box3().setFromObject(otherEnemy.mesh);
710
- if (enemyBox.intersectsBox(otherEnemyBox)) {
711
- hasCollision = true;
712
- break;
713
- }
714
- }
715
  }
 
716
  }
717
-
718
- // ๋งต ๊ฒฝ๊ณ„ ์ฒดํฌ
719
- const mapBoundary = MAP_SIZE / 2;
720
- if (Math.abs(newPosition.x) > mapBoundary ||
721
- Math.abs(newPosition.z) > mapBoundary) {
722
- hasCollision = true;
723
  }
 
 
 
 
724
 
725
- // ์ถฉ๋Œ์ด ์žˆ์œผ๋ฉด ์ด์ „ ์œ„์น˜๋กœ ๋ณต๊ท€, ์—†์œผ๋ฉด ์ƒˆ ์œ„์น˜ ์œ ์ง€
726
- if (hasCollision) {
727
- this.mesh.position.copy(previousPosition);
728
-
729
- // ์ถฉ๋Œ ์‹œ ์šฐํšŒ ๊ฒฝ๋กœ ์‹œ๋„
730
- const alternateDirections = [
731
- new THREE.Vector3(-direction.z, 0, direction.x), // ์™ผ์ชฝ์œผ๋กœ 90๋„
732
- new THREE.Vector3(direction.z, 0, -direction.x), // ์˜ค๋ฅธ์ชฝ์œผ๋กœ 90๋„
733
- new THREE.Vector3(-direction.x, 0, -direction.z) // 180๋„ ํšŒ์ „
734
- ];
735
-
736
- for (const altDirection of alternateDirections) {
737
- const altMoveVector = altDirection.multiplyScalar(this.moveSpeed);
738
- const altNewPosition = previousPosition.clone().add(altMoveVector);
739
-
740
- this.mesh.position.copy(altNewPosition);
741
- const altEnemyBox = new THREE.Box3().setFromObject(this.mesh);
742
-
743
- let altHasCollision = false;
744
-
745
- // ์ƒˆ๋กœ์šด ๋ฐฉํ–ฅ์— ๋Œ€ํ•œ ์ถฉ๋Œ ๊ฒ€์‚ฌ
746
- for (const obstacle of window.gameInstance.obstacles) {
747
- const obstacleBox = new THREE.Box3().setFromObject(obstacle);
748
- if (altEnemyBox.intersectsBox(obstacleBox)) {
749
- altHasCollision = true;
750
- break;
751
- }
752
- }
753
-
754
- if (!altHasCollision) {
755
- // ์šฐํšŒ ๊ฒฝ๋กœ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉด ๊ทธ ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™
756
- break;
757
- } else {
758
- // ์šฐํšŒ๋„ ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฉด ์ด์ „ ์œ„์น˜๋กœ ๋ณต๊ท€
759
- this.mesh.position.copy(previousPosition);
760
- }
761
  }
 
 
 
 
 
 
 
 
 
 
 
 
762
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
 
764
- // ์ง€ํ˜•์— ๋”ฐ๋ฅธ ๊ธฐ์šธ๊ธฐ ์กฐ์ •
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
  const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
766
  const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.mesh.quaternion);
767
 
@@ -785,21 +832,15 @@ class Enemy {
785
  const pitch = Math.atan2(frontHeight - backHeight, 2);
786
  const roll = Math.atan2(rightHeight - leftHeight, 2);
787
 
788
- // ํ˜„์žฌ ํšŒ์ „ ์œ ์ง€ํ•˜๋ฉด์„œ ๊ธฐ์šธ๊ธฐ๋งŒ ์ ์šฉ
789
  const currentRotation = this.mesh.rotation.y;
790
  this.mesh.rotation.set(pitch, currentRotation, roll);
791
  }
792
-
793
- // ํ”Œ๋ ˆ์ด์–ด๋ฅผ ํ–ฅํ•ด ํฌํƒ‘ ํšŒ์ „
794
- this.mesh.lookAt(playerPosition);
795
-
796
- // ์ด์•Œ ์—…๋ฐ์ดํŠธ
797
- if (this.bullets) {
798
  for (let i = this.bullets.length - 1; i >= 0; i--) {
799
  const bullet = this.bullets[i];
800
  bullet.position.add(bullet.velocity);
801
 
802
- // ์ด์•Œ์ด ๋งต ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๊ฑฐ๋‚˜ ์žฅ์• ๋ฌผ๊ณผ ์ถฉ๋Œํ•˜๋ฉด ์ œ๊ฑฐ
803
  if (Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
804
  Math.abs(bullet.position.z) > MAP_SIZE / 2) {
805
  this.scene.remove(bullet);
@@ -807,7 +848,6 @@ class Enemy {
807
  continue;
808
  }
809
 
810
- // ์ด์•Œ๊ณผ ์žฅ์• ๋ฌผ ์ถฉ๋Œ ์ฒดํฌ
811
  const bulletBox = new THREE.Box3().setFromObject(bullet);
812
  for (const obstacle of window.gameInstance.obstacles) {
813
  const obstacleBox = new THREE.Box3().setFromObject(obstacle);
@@ -819,72 +859,116 @@ class Enemy {
819
  }
820
  }
821
  }
822
- }
823
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
 
825
  shoot(playerPosition) {
826
- const currentTime = Date.now();
827
- const attackInterval = this.type === 'tank' ?
828
- ENEMY_CONFIG.ATTACK_INTERVAL :
829
- ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
830
 
831
- if (currentTime - this.lastAttackTime < attackInterval) return;
832
 
833
- // ๋ฐœ์‚ฌ ์ดํŽ™ํŠธ ์ƒ์„ฑ
834
- this.createMuzzleFlash();
835
 
836
- // ๋ฐœ์‚ฌ ์‚ฌ์šด๋“œ ์žฌ์ƒ
837
- const enemyFireSound = new Audio('sounds/mbtfire5.ogg');
838
- enemyFireSound.volume = 0.3;
839
- enemyFireSound.play();
840
 
841
- // ํฌํƒ„ ์ƒ์„ฑ (ํ”Œ๋ ˆ์ด์–ด์™€ ์œ ์‚ฌํ•˜๊ฒŒ ์ˆ˜์ •)
842
- const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8);
843
- const bulletMaterial = new THREE.MeshBasicMaterial({
844
- color: 0xff0000, // ๋นจ๊ฐ„์ƒ‰ ํฌํƒ„
845
- emissive: 0xff0000,
846
- emissiveIntensity: 0.5
847
- });
848
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
849
 
850
- // ํฌ๊ตฌ ์œ„์น˜์—์„œ ๋ฐœ์‚ฌ
851
- const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
852
- const muzzlePosition = new THREE.Vector3();
853
- this.mesh.getWorldPosition(muzzlePosition);
854
- muzzleOffset.applyQuaternion(this.mesh.quaternion);
855
- muzzlePosition.add(muzzleOffset);
856
-
857
- bullet.position.copy(muzzlePosition);
858
-
859
- // ํฌํƒ„ ํšŒ์ „ ์„ค์ •
860
- bullet.quaternion.copy(this.mesh.quaternion);
861
-
862
- const direction = new THREE.Vector3()
863
- .subVectors(playerPosition, muzzlePosition)
864
- .normalize();
865
-
866
- const bulletSpeed = this.type === 'tank' ?
867
- ENEMY_CONFIG.BULLET_SPEED :
868
- ENEMY_CONFIG.BULLET_SPEED * 0.8;
869
-
870
- bullet.velocity = direction.multiplyScalar(bulletSpeed);
871
-
872
- // ํฌํƒ„ ํŠธ๋ ˆ์ผ ํšจ๊ณผ ์ถ”๊ฐ€
873
- const trailGeometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8);
874
- const trailMaterial = new THREE.MeshBasicMaterial({
875
- color: 0xff4444,
876
- transparent: true,
877
- opacity: 0.5
878
- });
879
-
880
- const trail = new THREE.Mesh(trailGeometry, trailMaterial);
881
- trail.position.z = -1;
882
- bullet.add(trail);
883
-
884
- this.scene.add(bullet);
885
- this.bullets.push(bullet);
886
- this.lastAttackTime = currentTime;
887
- }
888
 
889
  takeDamage(damage) {
890
  this.health -= damage;
 
579
  this.bullets = [];
580
  this.isLoaded = false;
581
  this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
582
+
583
+ // AI ์ƒํƒœ ๊ด€๋ฆฌ
584
+ this.aiState = {
585
+ mode: 'pursue', // 'pursue', 'flank', 'retreat'
586
+ lastStateChange: 0,
587
+ stateChangeCooldown: 3000,
588
+ lastVisibilityCheck: 0,
589
+ visibilityCheckInterval: 500,
590
+ canSeePlayer: false,
591
+ lastKnownPlayerPosition: null,
592
+ searchStartTime: null
593
+ };
 
 
 
 
594
 
595
+ // ๊ฒฝ๋กœ ํƒ์ƒ‰ ์ƒํƒœ
596
+ this.pathfinding = {
597
+ currentPath: [],
598
+ pathUpdateInterval: 1000,
599
+ lastPathUpdate: 0,
600
+ isAvoidingObstacle: false,
601
+ avoidanceDirection: null,
602
+ obstacleCheckDistance: 10
603
+ };
 
 
 
 
 
 
 
 
604
  }
605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  async initialize(loader) {
607
  try {
608
  const modelPath = this.type === 'tank' ? '/models/t90.glb' : '/models/t90.glb';
 
626
  }
627
  }
628
 
629
+ checkLineOfSight(playerPosition) {
630
+ if (!this.mesh) return false;
631
 
632
+ const startPos = this.mesh.position.clone();
633
+ startPos.y += 2; // ํƒฑํฌ ํฌํƒ‘ ๋†’์ด
634
+ const direction = new THREE.Vector3().subVectors(playerPosition, startPos).normalize();
635
+ const distance = startPos.distanceTo(playerPosition);
636
+
637
+ const raycaster = new THREE.Raycaster(startPos, direction, 0, distance);
638
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
639
+
640
+ return intersects.length === 0;
641
+ }
642
+
643
+ updateAIState(playerPosition) {
644
+ const currentTime = Date.now();
645
+ const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
646
+
647
+ // ์‹œ์•ผ ์ฒดํฌ
648
+ if (currentTime - this.aiState.lastVisibilityCheck > this.aiState.visibilityCheckInterval) {
649
+ this.aiState.canSeePlayer = this.checkLineOfSight(playerPosition);
650
+ this.aiState.lastVisibilityCheck = currentTime;
651
+
652
+ if (this.aiState.canSeePlayer) {
653
+ this.aiState.lastKnownPlayerPosition = playerPosition.clone();
654
+ this.aiState.searchStartTime = null;
 
 
 
 
 
 
 
 
 
 
 
 
655
  }
656
  }
657
+
658
+ // AI ์ƒํƒœ ์—…๋ฐ์ดํŠธ
659
+ if (currentTime - this.aiState.lastStateChange > this.aiState.stateChangeCooldown) {
660
+ if (this.health < 30) {
661
+ this.aiState.mode = 'retreat';
662
+ } else if (distanceToPlayer < 30 && this.aiState.canSeePlayer) {
663
+ this.aiState.mode = 'flank';
664
+ } else {
665
+ this.aiState.mode = 'pursue';
 
 
666
  }
667
+ this.aiState.lastStateChange = currentTime;
668
  }
669
+ }
670
+
671
+ findPathToTarget(targetPosition) {
672
+ const currentTime = Date.now();
673
+ if (currentTime - this.pathfinding.lastPathUpdate < this.pathfinding.pathUpdateInterval) {
674
+ return;
675
  }
676
+
677
+ // ๊ฐ„๋‹จํ•œ A* ๊ฒฝ๋กœ ์ฐพ๊ธฐ ๊ตฌํ˜„
678
+ const start = this.mesh.position.clone();
679
+ const end = targetPosition.clone();
680
 
681
+ // ์žฅ์• ๋ฌผ์„ ๊ณ ๋ คํ•œ ๊ฒฝ๋กœ์  ์ƒ์„ฑ
682
+ this.pathfinding.currentPath = this.generatePathPoints(start, end);
683
+ this.pathfinding.lastPathUpdate = currentTime;
684
+ }
685
+
686
+ generatePathPoints(start, end) {
687
+ const points = [];
688
+ const direction = new THREE.Vector3().subVectors(end, start).normalize();
689
+ const distance = start.distanceTo(end);
690
+ const steps = Math.ceil(distance / 10);
691
+
692
+ for (let i = 0; i <= steps; i++) {
693
+ const point = start.clone().add(direction.multiplyScalar(i * 10));
694
+ points.push(point);
695
+ }
696
+
697
+ return points;
698
+ }
699
+
700
+ moveAlongPath() {
701
+ if (this.pathfinding.currentPath.length === 0) return;
702
+
703
+ const targetPoint = this.pathfinding.currentPath[0];
704
+ const direction = new THREE.Vector3()
705
+ .subVectors(targetPoint, this.mesh.position)
706
+ .normalize();
707
+
708
+ // ์žฅ์• ๋ฌผ ๊ฐ์ง€ ๋ฐ ํšŒํ”ผ
709
+ if (this.detectObstacle(direction)) {
710
+ if (!this.pathfinding.isAvoidingObstacle) {
711
+ this.pathfinding.isAvoidingObstacle = true;
712
+ this.pathfinding.avoidanceDirection = this.calculateAvoidanceDirection(direction);
 
 
 
 
713
  }
714
+ direction.copy(this.pathfinding.avoidanceDirection);
715
+ } else {
716
+ this.pathfinding.isAvoidingObstacle = false;
717
+ }
718
+
719
+ // ์ด๋™ ์ ์šฉ
720
+ const moveVector = direction.multiplyScalar(this.moveSpeed);
721
+ this.mesh.position.add(moveVector);
722
+
723
+ // ๊ฒฝ๋กœ์ ์— ๋„๋‹ฌํ–ˆ๋Š”์ง€ ํ™•์ธ
724
+ if (this.mesh.position.distanceTo(targetPoint) < 2) {
725
+ this.pathfinding.currentPath.shift();
726
  }
727
+ }
728
+
729
+ detectObstacle(direction) {
730
+ const raycaster = new THREE.Raycaster(
731
+ this.mesh.position,
732
+ direction,
733
+ 0,
734
+ this.pathfinding.obstacleCheckDistance
735
+ );
736
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
737
+ return intersects.length > 0;
738
+ }
739
+
740
+ calculateAvoidanceDirection(currentDirection) {
741
+ const left = new THREE.Vector3(-currentDirection.z, 0, currentDirection.x);
742
+ const right = new THREE.Vector3(currentDirection.z, 0, -currentDirection.x);
743
+
744
+ // ์™ผ์ชฝ๊ณผ ์˜ค๋ฅธ์ชฝ ๋ฐฉํ–ฅ ์ค‘ ์žฅ์• ๋ฌผ์ด ์—†๋Š” ๋ฐฉํ–ฅ ์„ ํƒ
745
+ if (!this.detectObstacle(left)) return left;
746
+ if (!this.detectObstacle(right)) return right;
747
+
748
+ // ๋‘˜ ๋‹ค ๋ง‰ํ˜€์žˆ์œผ๋ฉด ๋’ค๋กœ
749
+ return currentDirection.multiplyScalar(-1);
750
+ }
751
+
752
+ update(playerPosition) {
753
+ if (!this.mesh || !this.isLoaded) return;
754
+
755
+ this.updateAIState(playerPosition);
756
 
757
+ let targetPosition = playerPosition;
758
+ if (!this.aiState.canSeePlayer && this.aiState.lastKnownPlayerPosition) {
759
+ targetPosition = this.aiState.lastKnownPlayerPosition;
760
+ }
761
+
762
+ // AI ๋ชจ๋“œ์— ๋”ฐ๋ฅธ ํ–‰๋™
763
+ switch (this.aiState.mode) {
764
+ case 'pursue':
765
+ this.findPathToTarget(targetPosition);
766
+ this.moveAlongPath();
767
+ break;
768
+
769
+ case 'flank':
770
+ const flankPosition = this.calculateFlankPosition(playerPosition);
771
+ this.findPathToTarget(flankPosition);
772
+ this.moveAlongPath();
773
+ break;
774
+
775
+ case 'retreat':
776
+ const retreatPosition = this.calculateRetreatPosition(playerPosition);
777
+ this.findPathToTarget(retreatPosition);
778
+ this.moveAlongPath();
779
+ break;
780
+ }
781
+
782
+ // ์ด์•Œ ์—…๋ฐ์ดํŠธ
783
+ this.updateBullets();
784
+
785
+ // ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์‹œ์•ผ์— ์žˆ์œผ๋ฉด ๋ฐœ์‚ฌ
786
+ if (this.aiState.canSeePlayer) {
787
+ this.shoot(playerPosition);
788
+ }
789
+
790
+ // ์ง€ํ˜•์— ๋”ฐ๋ฅธ ํƒฑํฌ ๊ธฐ์šธ๊ธฐ ์กฐ์ •
791
+ this.adjustTankTilt();
792
+ }
793
+
794
+ calculateFlankPosition(playerPosition) {
795
+ const angle = Math.random() * Math.PI * 2;
796
+ const radius = 40;
797
+ return new THREE.Vector3(
798
+ playerPosition.x + Math.cos(angle) * radius,
799
+ playerPosition.y,
800
+ playerPosition.z + Math.sin(angle) * radius
801
+ );
802
+ }
803
+
804
+ calculateRetreatPosition(playerPosition) {
805
+ const direction = new THREE.Vector3()
806
+ .subVectors(this.mesh.position, playerPosition)
807
+ .normalize();
808
+ return this.mesh.position.clone().add(direction.multiplyScalar(50));
809
+ }
810
+
811
+ adjustTankTilt() {
812
  const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
813
  const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.mesh.quaternion);
814
 
 
832
  const pitch = Math.atan2(frontHeight - backHeight, 2);
833
  const roll = Math.atan2(rightHeight - leftHeight, 2);
834
 
 
835
  const currentRotation = this.mesh.rotation.y;
836
  this.mesh.rotation.set(pitch, currentRotation, roll);
837
  }
838
+
839
+ updateBullets() {
 
 
 
 
840
  for (let i = this.bullets.length - 1; i >= 0; i--) {
841
  const bullet = this.bullets[i];
842
  bullet.position.add(bullet.velocity);
843
 
 
844
  if (Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
845
  Math.abs(bullet.position.z) > MAP_SIZE / 2) {
846
  this.scene.remove(bullet);
 
848
  continue;
849
  }
850
 
 
851
  const bulletBox = new THREE.Box3().setFromObject(bullet);
852
  for (const obstacle of window.gameInstance.obstacles) {
853
  const obstacleBox = new THREE.Box3().setFromObject(obstacle);
 
859
  }
860
  }
861
  }
 
862
 
863
+ createMuzzleFlash() {
864
+ if (!this.mesh) return;
865
+
866
+ const flashGroup = new THREE.Group();
867
+
868
+ const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8);
869
+ const flameMaterial = new THREE.MeshBasicMaterial({
870
+ color: 0xffa500,
871
+ transparent: true,
872
+ opacity: 0.8
873
+ });
874
+ const flame = new THREE.Mesh(flameGeometry, flameMaterial);
875
+ flame.scale.set(2, 2, 3);
876
+ flashGroup.add(flame);
877
+
878
+ const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8);
879
+ const smokeMaterial = new THREE.MeshBasicMaterial({
880
+ color: 0x555555,
881
+ transparent: true,
882
+ opacity: 0.5
883
+ });
884
+
885
+ for (let i = 0; i < 5; i++) {
886
+ const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial);
887
+ smoke.position.set(
888
+ Math.random() * 1 - 0.5,
889
+ Math.random() * 1 - 0.5,
890
+ -1 - Math.random()
891
+ );
892
+ smoke.scale.set(1.5, 1.5, 1.5);
893
+ flashGroup.add(smoke);
894
+ }
895
+
896
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
897
+ const muzzlePosition = new THREE.Vector3();
898
+ const meshWorldQuaternion = new THREE.Quaternion();
899
+
900
+ this.mesh.getWorldPosition(muzzlePosition);
901
+ this.mesh.getWorldQuaternion(meshWorldQuaternion);
902
+
903
+ muzzleOffset.applyQuaternion(meshWorldQuaternion);
904
+ muzzlePosition.add(muzzleOffset);
905
+
906
+ flashGroup.position.copy(muzzlePosition);
907
+ flashGroup.quaternion.copy(meshWorldQuaternion);
908
+
909
+ this.scene.add(flashGroup);
910
+
911
+ setTimeout(() => {
912
+ this.scene.remove(flashGroup);
913
+ }, 500);
914
+ }
915
 
916
  shoot(playerPosition) {
917
+ const currentTime = Date.now();
918
+ const attackInterval = this.type === 'tank' ?
919
+ ENEMY_CONFIG.ATTACK_INTERVAL :
920
+ ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
921
 
922
+ if (currentTime - this.lastAttackTime < attackInterval) return;
923
 
924
+ this.createMuzzleFlash();
 
925
 
926
+ const enemyFireSound = new Audio('sounds/mbtfire5.ogg');
927
+ enemyFireSound.volume = 0.3;
928
+ enemyFireSound.play();
 
929
 
930
+ const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8);
931
+ const bulletMaterial = new THREE.MeshBasicMaterial({
932
+ color: 0xff0000,
933
+ emissive: 0xff0000,
934
+ emissiveIntensity: 0.5
935
+ });
936
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
 
937
 
938
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
939
+ const muzzlePosition = new THREE.Vector3();
940
+ this.mesh.getWorldPosition(muzzlePosition);
941
+ muzzleOffset.applyQuaternion(this.mesh.quaternion);
942
+ muzzlePosition.add(muzzleOffset);
943
+
944
+ bullet.position.copy(muzzlePosition);
945
+ bullet.quaternion.copy(this.mesh.quaternion);
946
+
947
+ const direction = new THREE.Vector3()
948
+ .subVectors(playerPosition, muzzlePosition)
949
+ .normalize();
950
+
951
+ const bulletSpeed = this.type === 'tank' ?
952
+ ENEMY_CONFIG.BULLET_SPEED :
953
+ ENEMY_CONFIG.BULLET_SPEED * 0.8;
954
+
955
+ bullet.velocity = direction.multiplyScalar(bulletSpeed);
956
+
957
+ const trailGeometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8);
958
+ const trailMaterial = new THREE.MeshBasicMaterial({
959
+ color: 0xff4444,
960
+ transparent: true,
961
+ opacity: 0.5
962
+ });
963
+
964
+ const trail = new THREE.Mesh(trailGeometry, trailMaterial);
965
+ trail.position.z = -1;
966
+ bullet.add(trail);
967
+
968
+ this.scene.add(bullet);
969
+ this.bullets.push(bullet);
970
+ this.lastAttackTime = currentTime;
971
+ }
 
 
 
 
972
 
973
  takeDamage(damage) {
974
  this.health -= damage;