cutechicken commited on
Commit
825ece5
Β·
verified Β·
1 Parent(s): fff7ab3

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +2402 -0
game.js CHANGED
@@ -627,6 +627,2408 @@ class Enemy {
627
  maxConsecutiveHits: 3
628
  };
629
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
 
631
  // μž₯μ• λ¬Ό 감지 μ‹œμŠ€ν…œ
632
  detectObstacles() {
 
627
  maxConsecutiveHits: 3
628
  };
629
  }
630
+ import * as THREE from 'three';
631
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
632
+ import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
633
+
634
+ // κ²Œμž„ μƒμˆ˜
635
+ const GAME_DURATION = 180;
636
+ const MAP_SIZE = 500;
637
+ const TANK_HEIGHT = 0.5;
638
+ const ENEMY_GROUND_HEIGHT = 0;
639
+ const ENEMY_SCALE = 1;
640
+ const MAX_HEALTH = 1000;
641
+ const ENEMY_MOVE_SPEED = 0.1;
642
+ const ENEMY_COUNT_MAX = 3;
643
+ const PARTICLE_COUNT = 15;
644
+ const BUILDING_COUNT = 50;
645
+ const ENEMY_CONFIG = {
646
+ ATTACK_RANGE: 100,
647
+ ATTACK_INTERVAL: 2000,
648
+ BULLET_SPEED: 2
649
+ };
650
+
651
+ // TankPlayer 클래슀
652
+ class TankPlayer {
653
+ constructor() {
654
+ this.body = null;
655
+ this.turret = null;
656
+ this.position = new THREE.Vector3(0, 0, 0);
657
+ this.rotation = new THREE.Euler(0, 0, 0);
658
+ this.turretRotation = 0;
659
+ this.moveSpeed = 0.5;
660
+ this.turnSpeed = 0.03;
661
+ this.turretGroup = new THREE.Group();
662
+ this.health = MAX_HEALTH;
663
+ this.isLoaded = false;
664
+ this.ammo = 1; // λ³€κ²½: 10 -> 1
665
+ this.maxAmmo = 1; // μΆ”κ°€: μ΅œλŒ€ 포탄 수
666
+ this.isReloading = false; // μΆ”κ°€: 재μž₯μ „ μƒνƒœ
667
+ this.reloadTime = 3000; // μΆ”κ°€: 3초 재μž₯μ „ μ‹œκ°„
668
+ this.lastShootTime = 0;
669
+ this.bullets = [];
670
+ this.obstacles = [];
671
+ this.scene = new THREE.Scene();
672
+ this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
673
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
674
+ }
675
+ // λ³„λ„μ˜ λ©”μ„œλ“œλ‘œ 뢄리
676
+ createExplosionEffect(scene, position) {
677
+ // 폭발 쀑심 ν”Œλž˜μ‹œ
678
+ const flashGeometry = new THREE.SphereGeometry(3);
679
+ const flashMaterial = new THREE.MeshBasicMaterial({
680
+ color: 0xffff00,
681
+ transparent: true,
682
+ opacity: 1
683
+ });
684
+ const flash = new THREE.Mesh(flashGeometry, flashMaterial);
685
+ flash.position.copy(position);
686
+ scene.add(flash);
687
+
688
+ // 폭발 νŒŒν‹°ν΄
689
+ for (let i = 0; i < 30; i++) {
690
+ const size = Math.random() * 0.5 + 0.3;
691
+ const geometry = new THREE.SphereGeometry(size);
692
+
693
+ // λ‹€μ–‘ν•œ μƒ‰μƒμ˜ νŒŒν‹°ν΄
694
+ const colors = [0xff4500, 0xff8c00, 0xff0000, 0xffd700];
695
+ const material = new THREE.MeshBasicMaterial({
696
+ color: colors[Math.floor(Math.random() * colors.length)],
697
+ transparent: true,
698
+ opacity: 1
699
+ });
700
+
701
+ const particle = new THREE.Mesh(geometry, material);
702
+ particle.position.copy(position);
703
+
704
+ // νŒŒν‹°ν΄ 속도와 λ°©ν–₯ μ„€μ •
705
+ const speed = Math.random() * 0.5 + 0.3;
706
+ const angle = Math.random() * Math.PI * 2;
707
+ const elevation = Math.random() * Math.PI - Math.PI / 2;
708
+
709
+ particle.velocity = new THREE.Vector3(
710
+ Math.cos(angle) * Math.cos(elevation) * speed,
711
+ Math.sin(elevation) * speed,
712
+ Math.sin(angle) * Math.cos(elevation) * speed
713
+ );
714
+
715
+ particle.gravity = -0.015;
716
+ particle.life = Math.random() * 30 + 30;
717
+ particle.fadeRate = 0.97;
718
+
719
+ scene.add(particle);
720
+ window.gameInstance.particles.push({
721
+ mesh: particle,
722
+ velocity: particle.velocity,
723
+ gravity: particle.gravity,
724
+ life: particle.life,
725
+ fadeRate: particle.fadeRate
726
+ });
727
+ }
728
+
729
+ // 폭발 링 μ΄νŽ™νŠΈ
730
+ const ringGeometry = new THREE.RingGeometry(0.1, 2, 32);
731
+ const ringMaterial = new THREE.MeshBasicMaterial({
732
+ color: 0xff8c00,
733
+ transparent: true,
734
+ opacity: 1,
735
+ side: THREE.DoubleSide
736
+ });
737
+ const ring = new THREE.Mesh(ringGeometry, ringMaterial);
738
+ ring.position.copy(position);
739
+ ring.lookAt(new THREE.Vector3(0, 1, 0));
740
+ scene.add(ring);
741
+
742
+ // 링 ν™•μž₯ μ• λ‹ˆλ©”μ΄μ…˜
743
+ const expandRing = () => {
744
+ ring.scale.x += 0.2;
745
+ ring.scale.y += 0.2;
746
+ ring.material.opacity *= 0.95;
747
+
748
+ if (ring.material.opacity > 0.01) {
749
+ requestAnimationFrame(expandRing);
750
+ } else {
751
+ scene.remove(ring);
752
+ }
753
+ };
754
+ expandRing();
755
+
756
+ // 폭발음 효과
757
+ const explosionSound = new Audio('sounds/explosion.ogg');
758
+ explosionSound.volume = 0.4;
759
+ explosionSound.play();
760
+
761
+ // 카메라 흔듀림 효과
762
+ if (window.gameInstance && window.gameInstance.camera) {
763
+ const camera = window.gameInstance.camera;
764
+ const originalPosition = camera.position.clone();
765
+ let shakeTime = 0;
766
+ const shakeIntensity = 0.3;
767
+ const shakeDuration = 500;
768
+
769
+ const shakeCamera = () => {
770
+ if (shakeTime < shakeDuration) {
771
+ camera.position.x = originalPosition.x + (Math.random() - 0.5) * shakeIntensity;
772
+ camera.position.y = originalPosition.y + (Math.random() - 0.5) * shakeIntensity;
773
+ camera.position.z = originalPosition.z + (Math.random() - 0.5) * shakeIntensity;
774
+
775
+ shakeTime += 16;
776
+ requestAnimationFrame(shakeCamera);
777
+ } else {
778
+ camera.position.copy(originalPosition);
779
+ }
780
+ };
781
+ shakeCamera();
782
+ }
783
+
784
+ // 쀑심 ν”Œλž˜μ‹œ 제거
785
+ setTimeout(() => {
786
+ scene.remove(flash);
787
+ }, 100);
788
+ }
789
+
790
+ async initialize(scene, loader) {
791
+ try {
792
+ const bodyResult = await loader.loadAsync('/models/abramsBody.glb');
793
+ this.body = bodyResult.scene;
794
+
795
+ const turretResult = await loader.loadAsync('/models/abramsTurret.glb');
796
+ this.turret = turretResult.scene;
797
+
798
+ this.turretGroup.position.y = 0.2;
799
+ this.turretGroup.add(this.turret);
800
+ this.body.add(this.turretGroup);
801
+
802
+ // 그림자 섀정은 κ·ΈλŒ€λ‘œ μœ μ§€
803
+ this.body.traverse((child) => {
804
+ if (child.isMesh) {
805
+ child.castShadow = true;
806
+ child.receiveShadow = true;
807
+ child.material.shadowSide = THREE.BackSide;
808
+ child.material.needsUpdate = true;
809
+ }
810
+ });
811
+
812
+ this.turret.traverse((child) => {
813
+ if (child.isMesh) {
814
+ child.castShadow = true;
815
+ child.receiveShadow = true;
816
+ child.material.shadowSide = THREE.BackSide;
817
+ child.material.needsUpdate = true;
818
+ }
819
+ });
820
+
821
+ // 그림자 평면
822
+ const shadowPlaneGeometry = new THREE.PlaneGeometry(8, 8);
823
+ const shadowPlaneMaterial = new THREE.ShadowMaterial({
824
+ opacity: 0.3
825
+ });
826
+ this.shadowPlane = new THREE.Mesh(shadowPlaneGeometry, shadowPlaneMaterial);
827
+ this.shadowPlane.receiveShadow = true;
828
+ this.shadowPlane.rotation.x = -Math.PI / 2;
829
+ this.shadowPlane.position.y = 0.1;
830
+ this.body.add(this.shadowPlane);
831
+
832
+ // κ°„λ‹¨ν•œ 슀폰 μœ„μΉ˜ μ„€μ •
833
+ const spawnPosition = new THREE.Vector3(
834
+ (Math.random() - 0.5) * (MAP_SIZE * 0.8), // MAP_SIZE의 80%만 μ‚¬μš©
835
+ TANK_HEIGHT, // κ³ μ •λœ 높이 μ‚¬μš©
836
+ (Math.random() - 0.5) * (MAP_SIZE * 0.8)
837
+ );
838
+
839
+ this.body.position.copy(spawnPosition);
840
+
841
+ // 폭발 μ΄νŽ™νŠΈ λ©”μ„œλ“œ μΆ”κ°€
842
+ this.createExplosionEffect = (scene, position) => {
843
+ // 폭발 νŒŒν‹°ν΄
844
+ for (let i = 0; i < 15; i++) {
845
+ const size = Math.random() * 0.2 + 0.1;
846
+ const geometry = new THREE.SphereGeometry(size);
847
+ const material = new THREE.MeshBasicMaterial({
848
+ color: Math.random() < 0.5 ? 0xff4500 : 0xff8c00
849
+ });
850
+ const particle = new THREE.Mesh(geometry, material);
851
+ particle.position.copy(position);
852
+
853
+ const speed = Math.random() * 0.3 + 0.2;
854
+ const angle = Math.random() * Math.PI * 2;
855
+ const elevation = Math.random() * Math.PI - Math.PI / 2;
856
+
857
+ particle.velocity = new THREE.Vector3(
858
+ Math.cos(angle) * Math.cos(elevation) * speed,
859
+ Math.sin(elevation) * speed,
860
+ Math.sin(angle) * Math.cos(elevation) * speed
861
+ );
862
+
863
+ particle.gravity = -0.01;
864
+ particle.life = Math.random() * 20 + 20;
865
+ particle.fadeRate = 1 / particle.life;
866
+
867
+ scene.add(particle);
868
+ window.gameInstance.particles.push({
869
+ mesh: particle,
870
+ velocity: particle.velocity,
871
+ gravity: particle.gravity,
872
+ life: particle.life,
873
+ fadeRate: particle.fadeRate
874
+ });
875
+ }
876
+
877
+ // 좩돌 μ‚¬μš΄λ“œ μž¬μƒ
878
+ const explosionSound = new Audio('sounds/explosion.ogg');
879
+ explosionSound.volume = 0.3;
880
+ explosionSound.play();
881
+ };
882
+
883
+ scene.add(this.body);
884
+ this.isLoaded = true;
885
+ this.updateAmmoDisplay();
886
+
887
+ } catch (error) {
888
+ console.error('Error loading tank models:', error);
889
+ this.isLoaded = false;
890
+ }
891
+ }
892
+
893
+ shoot(scene) {
894
+ // 재μž₯μ „ μ€‘μ΄κ±°λ‚˜ 탄약이 μ—†μœΌλ©΄ λ°œμ‚¬ν•˜μ§€ μ•ŠμŒ
895
+ if (this.isReloading || this.ammo <= 0) {
896
+ return null;
897
+ }
898
+
899
+ // λ°œμ‚¬ λ”œλ ˆμ΄ 체크 (연속 λ°œμ‚¬ λ°©μ§€)
900
+ const currentTime = Date.now();
901
+ if (currentTime - this.lastShootTime < 100) { // 100ms의 λ°œμ‚¬ λ”œλ ˆμ΄
902
+ return null;
903
+ }
904
+ this.lastShootTime = currentTime;
905
+
906
+ // λ°œμ‚¬μŒ 효과 (ν•œ 번만 μž¬μƒ)
907
+ const sounds = ['sounds/mbtfire1.ogg', 'sounds/mbtfire2.ogg', 'sounds/mbtfire3.ogg', 'sounds/mbtfire4.ogg'];
908
+ const randomSound = sounds[Math.floor(Math.random() * sounds.length)];
909
+ const audio = new Audio(randomSound);
910
+ audio.volume = 0.5;
911
+
912
+ // 이전 μ˜€λ””μ˜€ μΈμŠ€ν„΄μŠ€κ°€ μžˆλ‹€λ©΄ 쀑지
913
+ if (this.lastAudio) {
914
+ this.lastAudio.pause();
915
+ this.lastAudio.currentTime = 0;
916
+ }
917
+
918
+ this.lastAudio = audio;
919
+ audio.play();
920
+
921
+ // λ°œμ‚¬ μ΄νŽ™νŠΈ
922
+ this.createMuzzleFlash(scene);
923
+
924
+ // 포탄 생성
925
+ const bullet = this.createBullet(scene);
926
+ if (bullet) {
927
+ this.ammo--;
928
+ this.updateAmmoDisplay();
929
+
930
+ // 탄약을 λͺ¨λ‘ μ†Œμ§„ν–ˆμ„ λ•Œλ§Œ 재μž₯μ „ μ‹œμž‘
931
+ if (this.ammo <= 0) {
932
+ this.startReload();
933
+ }
934
+ }
935
+ return bullet;
936
+ }
937
+
938
+ startReload() {
939
+ if (this.isReloading) return; // 이미 재μž₯μ „ 쀑이면 λ¬΄μ‹œ
940
+
941
+ this.isReloading = true;
942
+ const reloadingText = document.getElementById('reloadingText');
943
+ reloadingText.style.display = 'block';
944
+
945
+ setTimeout(() => {
946
+ this.ammo = this.maxAmmo;
947
+ this.isReloading = false;
948
+ reloadingText.style.display = 'none';
949
+ this.updateAmmoDisplay();
950
+ }, this.reloadTime);
951
+ }
952
+
953
+ createMuzzleFlash(scene) {
954
+ if (!this.turret) return;
955
+ const flashGroup = new THREE.Group();
956
+
957
+ // ν™”μ—Ό 크기 증가
958
+ const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8);
959
+ const flameMaterial = new THREE.MeshBasicMaterial({
960
+ color: 0xffa500,
961
+ transparent: true,
962
+ opacity: 0.8
963
+ });
964
+ const flame = new THREE.Mesh(flameGeometry, flameMaterial);
965
+ flame.scale.set(2, 2, 3);
966
+ flashGroup.add(flame);
967
+
968
+ // μ—°κΈ° 효과 크기 증가
969
+ const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8);
970
+ const smokeMaterial = new THREE.MeshBasicMaterial({
971
+ color: 0x555555,
972
+ transparent: true,
973
+ opacity: 0.5
974
+ });
975
+
976
+ for (let i = 0; i < 5; i++) { // μ—°κΈ° νŒŒν‹°ν΄ 수 증가
977
+ const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial);
978
+ smoke.position.set(
979
+ Math.random() * 1 - 0.5,
980
+ Math.random() * 1 - 0.5,
981
+ -1 - Math.random()
982
+ );
983
+ smoke.scale.set(1.5, 1.5, 1.5);
984
+ flashGroup.add(smoke);
985
+ }
986
+
987
+ // 포ꡬ μœ„μΉ˜ 계산
988
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
989
+ const muzzlePosition = new THREE.Vector3();
990
+ const turretWorldQuaternion = new THREE.Quaternion();
991
+
992
+ this.turret.getWorldPosition(muzzlePosition);
993
+ this.turret.getWorldQuaternion(turretWorldQuaternion);
994
+
995
+ muzzleOffset.applyQuaternion(turretWorldQuaternion);
996
+ muzzlePosition.add(muzzleOffset);
997
+
998
+ flashGroup.position.copy(muzzlePosition);
999
+ flashGroup.quaternion.copy(turretWorldQuaternion);
1000
+
1001
+ scene.add(flashGroup);
1002
+
1003
+ // μ΄νŽ™νŠΈ 지속 μ‹œκ°„ 증가
1004
+ setTimeout(() => {
1005
+ scene.remove(flashGroup);
1006
+ }, 500);
1007
+ }
1008
+
1009
+
1010
+
1011
+ createBullet(scene) {
1012
+ if (!this.turret) return null; // 터렛이 μ—†μœΌλ©΄ null λ°˜ν™˜
1013
+ // 포탄 크기 증가
1014
+ const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8);
1015
+ const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffd700 });
1016
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
1017
+
1018
+ // ν¬νƒ‘μ˜ μ›”λ“œ μœ„μΉ˜μ™€ λ°©ν–₯ κ°€μ Έμ˜€κΈ°
1019
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4); // 포ꡬ μœ„μΉ˜ μ‘°μ • (μ•žμͺ½μœΌλ‘œ 더 이동)
1020
+ const muzzlePosition = new THREE.Vector3();
1021
+ const turretWorldQuaternion = new THREE.Quaternion();
1022
+
1023
+ // ν¬νƒ‘μ˜ μ›”λ“œ λ³€ν™˜ ν–‰λ ¬ κ°€μ Έμ˜€κΈ°
1024
+ this.turret.getWorldPosition(muzzlePosition);
1025
+ this.turret.getWorldQuaternion(turretWorldQuaternion);
1026
+
1027
+ // 포ꡬ μ˜€ν”„μ…‹ 적용
1028
+ muzzleOffset.applyQuaternion(turretWorldQuaternion);
1029
+ muzzlePosition.add(muzzleOffset);
1030
+
1031
+ // 포탄 μœ„μΉ˜μ™€ νšŒμ „ μ„€μ •
1032
+ bullet.position.copy(muzzlePosition);
1033
+ bullet.quaternion.copy(turretWorldQuaternion);
1034
+
1035
+ // λ°œμ‚¬ λ°©ν–₯ μ„€μ •
1036
+ const direction = new THREE.Vector3(0, 0, 1);
1037
+ direction.applyQuaternion(turretWorldQuaternion);
1038
+ bullet.velocity = direction.multiplyScalar(5);
1039
+
1040
+ scene.add(bullet);
1041
+ this.bullets.push(bullet);
1042
+
1043
+ return bullet;
1044
+ }
1045
+
1046
+
1047
+ update(mouseX, mouseY, scene) {
1048
+ if (!this.body || !this.turretGroup) return;
1049
+
1050
+ const absoluteTurretRotation = mouseX;
1051
+ this.turretGroup.rotation.y = absoluteTurretRotation - this.body.rotation.y;
1052
+ this.turretRotation = absoluteTurretRotation;
1053
+
1054
+ // μ΄μ•Œ μ—…λ°μ΄νŠΈ 및 좩돌 체크
1055
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
1056
+ const bullet = this.bullets[i];
1057
+ const oldPosition = bullet.position.clone();
1058
+ bullet.position.add(bullet.velocity);
1059
+
1060
+ // μ§€ν˜• 높이 체크
1061
+ const terrainHeight = window.gameInstance.getHeightAtPosition(
1062
+ bullet.position.x,
1063
+ bullet.position.z
1064
+ );
1065
+
1066
+ if (bullet.position.y < terrainHeight ||
1067
+ Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
1068
+ Math.abs(bullet.position.z) > MAP_SIZE / 2) {
1069
+
1070
+ // 폭발 μ΄νŽ™νŠΈ 생성
1071
+ this.createExplosionEffect(scene, bullet.position);
1072
+
1073
+ scene.remove(bullet);
1074
+ this.bullets.splice(i, 1);
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ move(direction) {
1080
+ if (!this.body) return;
1081
+
1082
+ const moveVector = new THREE.Vector3();
1083
+ moveVector.x = direction.x * this.moveSpeed;
1084
+ moveVector.z = direction.z * this.moveSpeed;
1085
+
1086
+ // μƒˆλ‘œμš΄ μœ„μΉ˜ 계산
1087
+ const newPosition = this.body.position.clone().add(moveVector);
1088
+
1089
+ // μƒˆλ‘œμš΄ μœ„μΉ˜μ˜ μ§€ν˜• 높이 κ°€μ Έμ˜€κΈ°
1090
+ const heightAtNewPos = window.gameInstance.getHeightAtPosition(newPosition.x, newPosition.z);
1091
+
1092
+ // 탱크 λ†’μ΄λ§ŒνΌ λ”ν•΄μ„œ μ§€ν˜• μœ„μ— μœ„μΉ˜μ‹œν‚΄
1093
+ newPosition.y = heightAtNewPos + TANK_HEIGHT;
1094
+
1095
+ // 경사가 λ„ˆλ¬΄ κ°€νŒŒλ₯Έμ§€ 체크
1096
+ const currentHeight = this.body.position.y;
1097
+ const heightDifference = Math.abs(newPosition.y - currentHeight);
1098
+ const maxClimbAngle = 0.5; // μ΅œλŒ€ λ“±λ°˜ 각도
1099
+
1100
+ if (heightDifference / this.moveSpeed < maxClimbAngle) {
1101
+ this.body.position.copy(newPosition);
1102
+
1103
+ // νƒ±ν¬μ˜ λ°©ν–₯ 벑터 계산
1104
+ const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.body.quaternion);
1105
+ const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.body.quaternion);
1106
+
1107
+ // 탱크 μ£Όλ³€μ˜ 높이 계산
1108
+ const frontHeight = window.gameInstance.getHeightAtPosition(
1109
+ newPosition.x + forwardVector.x,
1110
+ newPosition.z + forwardVector.z
1111
+ );
1112
+ const backHeight = window.gameInstance.getHeightAtPosition(
1113
+ newPosition.x - forwardVector.x,
1114
+ newPosition.z - forwardVector.z
1115
+ );
1116
+ const rightHeight = window.gameInstance.getHeightAtPosition(
1117
+ newPosition.x + rightVector.x,
1118
+ newPosition.z + rightVector.z
1119
+ );
1120
+ const leftHeight = window.gameInstance.getHeightAtPosition(
1121
+ newPosition.x - rightVector.x,
1122
+ newPosition.z - rightVector.z
1123
+ );
1124
+
1125
+ // νƒ±ν¬μ˜ 기울기 계산 및 적용
1126
+ const pitch = Math.atan2(frontHeight - backHeight, 2);
1127
+ const roll = Math.atan2(rightHeight - leftHeight, 2);
1128
+
1129
+ // κΈ°μ‘΄ yμΆ• νšŒμ „μ€ μœ μ§€ν•˜λ©΄μ„œ μƒˆλ‘œμš΄ 기울기 적용
1130
+ const currentYRotation = this.body.rotation.y;
1131
+ this.body.rotation.set(pitch, currentYRotation, roll);
1132
+ }
1133
+ }
1134
+
1135
+ rotate(angle) {
1136
+ if (!this.body) return;
1137
+
1138
+ // yμΆ• νšŒμ „ 적용
1139
+ this.body.rotation.y += angle * this.turnSpeed;
1140
+
1141
+ // ν˜„μž¬ μœ„μΉ˜μ—μ„œμ˜ μ§€ν˜•μ— 맞좰 탱크 기울기 μ‘°μ •
1142
+ const position = this.body.position;
1143
+ const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.body.quaternion);
1144
+ const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.body.quaternion);
1145
+
1146
+ // 탱크 μ£Όλ³€μ˜ 높이 계산
1147
+ const frontHeight = window.gameInstance.getHeightAtPosition(
1148
+ position.x + forwardVector.x,
1149
+ position.z + forwardVector.z
1150
+ );
1151
+ const backHeight = window.gameInstance.getHeightAtPosition(
1152
+ position.x - forwardVector.x,
1153
+ position.z - forwardVector.z
1154
+ );
1155
+ const rightHeight = window.gameInstance.getHeightAtPosition(
1156
+ position.x + rightVector.x,
1157
+ position.z + rightVector.z
1158
+ );
1159
+ const leftHeight = window.gameInstance.getHeightAtPosition(
1160
+ position.x - rightVector.x,
1161
+ position.z - rightVector.z
1162
+ );
1163
+
1164
+ // νƒ±ν¬μ˜ 기울기 계산 및 적용
1165
+ const pitch = Math.atan2(frontHeight - backHeight, 2);
1166
+ const roll = Math.atan2(rightHeight - leftHeight, 2);
1167
+
1168
+ // ν˜„μž¬ yμΆ• νšŒμ „μ€ μœ μ§€ν•˜λ©΄μ„œ μƒˆλ‘œμš΄ 기울기 적용
1169
+ const currentYRotation = this.body.rotation.y;
1170
+ this.body.rotation.set(pitch, currentYRotation, roll);
1171
+ }
1172
+
1173
+ getPosition() {
1174
+ return this.body ? this.body.position : new THREE.Vector3();
1175
+ }
1176
+
1177
+ takeDamage(damage) {
1178
+ this.health -= damage;
1179
+ return this.health <= 0;
1180
+ }
1181
+
1182
+ startReload() {
1183
+ this.isReloading = true;
1184
+ const reloadingText = document.getElementById('reloadingText');
1185
+ reloadingText.style.display = 'block';
1186
+ setTimeout(() => {
1187
+ this.ammo = this.maxAmmo;
1188
+ this.isReloading = false;
1189
+ reloadingText.style.display = 'none';
1190
+ this.updateAmmoDisplay();
1191
+ }, this.reloadTime);
1192
+ }
1193
+
1194
+ updateAmmoDisplay() {
1195
+ document.getElementById('ammoDisplay').textContent = `APFSDS: ${this.ammo}/${this.maxAmmo}`;
1196
+ }
1197
+ }
1198
+
1199
+ // Enemy 클래슀
1200
+ class Enemy {
1201
+ constructor(scene, position, type = 'tank') {
1202
+ // κΈ°λ³Έ 속성
1203
+ this.scene = scene;
1204
+ this.position = position;
1205
+ this.mesh = null;
1206
+ this.type = type;
1207
+ this.health = type === 'tank' ? 100 : 200;
1208
+ this.lastAttackTime = 0;
1209
+ this.bullets = [];
1210
+ this.isLoaded = false;
1211
+ this.alternativePath = null;
1212
+ this.pathFindingTimeout = 0;
1213
+ this.lastPathUpdateTime = 0;
1214
+ this.pathUpdateInterval = 1000; // 1μ΄ˆλ§ˆλ‹€ 경둜 μ—…λ°μ΄νŠΈ
1215
+ this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
1216
+ async initialize(loader) {
1217
+ try {
1218
+ // 적 탱크 λͺ¨λΈ λ‘œλ“œ
1219
+ let modelPath;
1220
+ if (this.type === 'tank') {
1221
+ modelPath = '/models/t90Body.glb'; // T-90 탱크 λͺ¨λΈ
1222
+ } else {
1223
+ modelPath = '/models/heavyTankBody.glb'; // 쀑전차 λͺ¨λΈ
1224
+ }
1225
+
1226
+ const result = await loader.loadAsync(modelPath);
1227
+ this.mesh = result.scene;
1228
+
1229
+ // μœ„μΉ˜ μ„€μ •
1230
+ this.mesh.position.copy(this.position);
1231
+
1232
+ // 그림자 μ„€μ •
1233
+ this.mesh.traverse((child) => {
1234
+ if (child.isMesh) {
1235
+ child.castShadow = true;
1236
+ child.receiveShadow = true;
1237
+ child.material.shadowSide = THREE.BackSide;
1238
+ }
1239
+ });
1240
+
1241
+ // 그림자 평면 μΆ”κ°€
1242
+ const shadowPlaneGeometry = new THREE.PlaneGeometry(8, 8);
1243
+ const shadowPlaneMaterial = new THREE.ShadowMaterial({
1244
+ opacity: 0.3
1245
+ });
1246
+ this.shadowPlane = new THREE.Mesh(shadowPlaneGeometry, shadowPlaneMaterial);
1247
+ this.shadowPlane.receiveShadow = true;
1248
+ this.shadowPlane.rotation.x = -Math.PI / 2;
1249
+ this.shadowPlane.position.y = 0.1;
1250
+ this.mesh.add(this.shadowPlane);
1251
+
1252
+ // 씬에 μΆ”κ°€
1253
+ this.scene.add(this.mesh);
1254
+ this.isLoaded = true;
1255
+
1256
+ } catch (error) {
1257
+ console.error('Error loading enemy tank model:', error);
1258
+ this.isLoaded = false;
1259
+ }
1260
+ }
1261
+
1262
+ // AI μƒνƒœ 관리
1263
+ this.aiState = {
1264
+ mode: 'pursue',
1265
+ lastStateChange: 0,
1266
+ stateChangeCooldown: 3000,
1267
+ lastVisibilityCheck: 0,
1268
+ visibilityCheckInterval: 500,
1269
+ canSeePlayer: false,
1270
+ lastKnownPlayerPosition: null,
1271
+ searchStartTime: null,
1272
+ targetRotation: 0,
1273
+ currentRotation: 0,
1274
+ isAiming: false,
1275
+ aimingTime: 0,
1276
+ requiredAimTime: 1000 // 쑰쀀에 ν•„μš”ν•œ μ‹œκ°„
1277
+ };
1278
+
1279
+ // 경둜 탐색 및 νšŒν”Ό μ‹œμŠ€ν…œ
1280
+ this.pathfinding = {
1281
+ currentPath: [],
1282
+ pathUpdateInterval: 1000,
1283
+ lastPathUpdate: 0,
1284
+ isAvoidingObstacle: false,
1285
+ avoidanceDirection: null,
1286
+ obstacleCheckDistance: 10,
1287
+ avoidanceTime: 0,
1288
+ maxAvoidanceTime: 3000, // μ΅œλŒ€ νšŒν”Ό μ‹œκ°„
1289
+ sensorAngles: [-45, 0, 45], // μ „λ°© 감지 각도
1290
+ sensorDistance: 15 // 감지 거리
1291
+ };
1292
+
1293
+ // μ „νˆ¬ μ‹œμŠ€ν…œ
1294
+ this.combat = {
1295
+ minEngagementRange: 30,
1296
+ maxEngagementRange: 150,
1297
+ optimalRange: 80,
1298
+ aimThreshold: 0.1, // μ‘°μ€€ 정확도 μž„κ³„κ°’
1299
+ lastShotAccuracy: 0,
1300
+ consecutiveHits: 0,
1301
+ maxConsecutiveHits: 3
1302
+ };
1303
+ }
1304
+
1305
+ // μž₯μ• λ¬Ό 감지 μ‹œμŠ€ν…œ
1306
+ detectObstacles() {
1307
+ const obstacles = [];
1308
+ const position = this.mesh.position.clone();
1309
+ position.y += 1; // μ„Όμ„œ 높이 μ‘°μ •
1310
+
1311
+ this.pathfinding.sensorAngles.forEach(angle => {
1312
+ const direction = new THREE.Vector3(0, 0, 1)
1313
+ .applyQuaternion(this.mesh.quaternion)
1314
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), angle * Math.PI / 180);
1315
+
1316
+ const raycaster = new THREE.Raycaster(position, direction, 0, this.pathfinding.sensorDistance);
1317
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
1318
+
1319
+ if (intersects.length > 0) {
1320
+ obstacles.push({
1321
+ angle: angle,
1322
+ distance: intersects[0].distance,
1323
+ point: intersects[0].point
1324
+ });
1325
+ }
1326
+ });
1327
+
1328
+ return obstacles;
1329
+ }
1330
+
1331
+ // νšŒν”Ό λ°©ν–₯ 계산
1332
+ calculateAvoidanceDirection(obstacles) {
1333
+ if (obstacles.length === 0) return null;
1334
+
1335
+ // λͺ¨λ“  μž₯μ• λ¬Όμ˜ λ°©ν–₯을 κ³ λ €ν•˜μ—¬ 졜적의 νšŒν”Ό λ°©ν–₯ 계산
1336
+ const avoidanceVector = new THREE.Vector3();
1337
+ obstacles.forEach(obstacle => {
1338
+ const avoidDir = new THREE.Vector3()
1339
+ .subVectors(this.mesh.position, obstacle.point)
1340
+ .normalize()
1341
+ .multiplyScalar(1 / obstacle.distance); // 거리에 λ°˜λΉ„λ‘€ν•˜λŠ” κ°€μ€‘μΉ˜
1342
+ avoidanceVector.add(avoidDir);
1343
+ });
1344
+
1345
+ return avoidanceVector.normalize();
1346
+ }
1347
+
1348
+ // μ‘°μ€€ μ‹œμŠ€ν…œ
1349
+ updateAiming(playerPosition) {
1350
+ const targetDirection = new THREE.Vector3()
1351
+ .subVectors(playerPosition, this.mesh.position)
1352
+ .normalize();
1353
+
1354
+ // λͺ©ν‘œ νšŒμ „κ° 계산
1355
+ this.aiState.targetRotation = Math.atan2(targetDirection.x, targetDirection.z);
1356
+
1357
+ // ν˜„μž¬ νšŒμ „κ° λΆ€λ“œλŸ½κ²Œ μ‘°μ • - μ„ νšŒ 속도λ₯Ό 느리게 ν•˜κΈ° μœ„ν•΄ μ‘°μ •
1358
+ const rotationDiff = this.aiState.targetRotation - this.aiState.currentRotation;
1359
+ let rotationStep = Math.sign(rotationDiff) * Math.min(Math.abs(rotationDiff), 0.05); // κΈ°μ‘΄ 0.02μ—μ„œ 0.05둜 μˆ˜μ •
1360
+ this.aiState.currentRotation += rotationStep;
1361
+
1362
+ // λ©”μ‹œ νšŒμ „ 적용
1363
+ this.mesh.rotation.y = this.aiState.currentRotation;
1364
+
1365
+ // μ‘°μ€€ 정확도 계산
1366
+ const aimAccuracy = 1 - Math.abs(rotationDiff) / Math.PI;
1367
+ return aimAccuracy > this.combat.aimThreshold;
1368
+ }
1369
+
1370
+ // μ „νˆ¬ 거리 관리
1371
+ maintainCombatDistance(playerPosition) {
1372
+ const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
1373
+ let moveDirection = new THREE.Vector3();
1374
+
1375
+ if (distanceToPlayer < this.combat.minEngagementRange) {
1376
+ // λ„ˆλ¬΄ κ°€κΉŒμš°λ©΄ ν›„μ§„
1377
+ moveDirection.subVectors(this.mesh.position, playerPosition).normalize();
1378
+ } else if (distanceToPlayer > this.combat.maxEngagementRange) {
1379
+ // λ„ˆλ¬΄ λ©€λ©΄ μ „μ§„
1380
+ moveDirection.subVectors(playerPosition, this.mesh.position).normalize();
1381
+ } else if (Math.abs(distanceToPlayer - this.combat.optimalRange) > 10) {
1382
+ // 졜적 거리둜 μ‘°μ •
1383
+ const targetDistance = this.combat.optimalRange;
1384
+ moveDirection.subVectors(playerPosition, this.mesh.position).normalize();
1385
+ if (distanceToPlayer > targetDistance) {
1386
+ moveDirection.multiplyScalar(1);
1387
+ } else {
1388
+ moveDirection.multiplyScalar(-1);
1389
+ }
1390
+ }
1391
+
1392
+ return moveDirection;
1393
+ }
1394
+
1395
+ // λ°œμ‚¬ 쑰건 확인
1396
+ canShoot(playerPosition) {
1397
+ const distance = this.mesh.position.distanceTo(playerPosition);
1398
+ const hasLineOfSight = this.checkLineOfSight(playerPosition);
1399
+ const isAimed = this.updateAiming(playerPosition);
1400
+
1401
+ return distance <= this.combat.maxEngagementRange &&
1402
+ distance >= this.combat.minEngagementRange &&
1403
+ hasLineOfSight &&
1404
+ isAimed;
1405
+ }
1406
+
1407
+ // 메인 μ—…λ°μ΄νŠΈ ν•¨μˆ˜
1408
+ update(playerPosition) {
1409
+ if (!this.mesh || !this.isLoaded) return;
1410
+
1411
+ // AI μƒνƒœ μ—…λ°μ΄νŠΈ
1412
+ this.updateAIState(playerPosition);
1413
+
1414
+ // μž₯μ• λ¬Ό 감지 및 μ‹œμ•Ό 체크
1415
+ const obstacles = this.detectObstacles();
1416
+ const currentTime = Date.now();
1417
+ const hasLineOfSight = this.checkLineOfSight(playerPosition);
1418
+ const distanceToPlayer = this.mesh.position.distanceTo(playerPosition);
1419
+
1420
+ // μž₯μ• λ¬Ό νšŒν”Ό 둜직
1421
+ if (this.pathfinding.isAvoidingObstacle) {
1422
+ this.pathfinding.avoidanceTime += 16;
1423
+ if (this.pathfinding.avoidanceTime >= this.pathfinding.maxAvoidanceTime) {
1424
+ this.pathfinding.isAvoidingObstacle = false;
1425
+ } else {
1426
+ const avoidMove = this.pathfinding.avoidanceDirection.multiplyScalar(this.moveSpeed);
1427
+ this.mesh.position.add(avoidMove);
1428
+ }
1429
+ } else if (!hasLineOfSight) {
1430
+ // μ‹œμ•Όκ°€ 없을 λ•Œμ˜ 이동 둜직 μΆ”κ°€
1431
+ if (!this.alternativePath || this.mesh.position.distanceTo(this.alternativePath) < 2) {
1432
+ this.alternativePath = this.findAlternativePath(playerPosition);
1433
+ }
1434
+
1435
+ if (this.alternativePath) {
1436
+ const pathDirection = new THREE.Vector3()
1437
+ .subVectors(this.alternativePath, this.mesh.position)
1438
+ .normalize();
1439
+
1440
+ // 이동 μ „ μž₯μ• λ¬Ό 체크
1441
+ const nextPosition = this.mesh.position.clone()
1442
+ .add(pathDirection.multiplyScalar(this.moveSpeed));
1443
+
1444
+ if (!this.checkCollisionAtPosition(nextPosition)) {
1445
+ this.mesh.position.copy(nextPosition);
1446
+ } else {
1447
+ // μž₯애물이 μžˆμ„ 경우 μƒˆλ‘œμš΄ 경둜 탐색
1448
+ this.alternativePath = null;
1449
+ this.lastAvoidanceDirection = null;
1450
+ }
1451
+
1452
+ // 이동 λ°©ν–₯으둜 λΆ€λ“œλŸ½κ²Œ νšŒμ „
1453
+ const targetRotation = Math.atan2(pathDirection.x, pathDirection.z);
1454
+ this.mesh.rotation.y = this.smoothRotation(
1455
+ this.mesh.rotation.y,
1456
+ targetRotation,
1457
+ 0.1
1458
+ );
1459
+ }
1460
+ } else {
1461
+ // μ‹œμ•Όκ°€ μžˆμ„ λ•Œμ˜ 이동
1462
+ this.alternativePath = null;
1463
+
1464
+ // AI μƒνƒœμ— λ”°λ₯Έ 이동
1465
+ switch (this.aiState.mode) {
1466
+ case 'pursue':
1467
+ if (distanceToPlayer > ENEMY_CONFIG.ATTACK_RANGE * 0.7) {
1468
+ const moveDirection = new THREE.Vector3()
1469
+ .subVectors(playerPosition, this.mesh.position)
1470
+ .normalize();
1471
+ this.mesh.position.add(moveDirection.multiplyScalar(this.moveSpeed));
1472
+ }
1473
+ break;
1474
+
1475
+ case 'flank':
1476
+ const flankPosition = this.calculateFlankPosition(playerPosition);
1477
+ this.findPathToTarget(flankPosition);
1478
+ this.moveAlongPath();
1479
+ break;
1480
+
1481
+ case 'retreat':
1482
+ if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE * 0.3) {
1483
+ const retreatDirection = new THREE.Vector3()
1484
+ .subVectors(this.mesh.position, playerPosition)
1485
+ .normalize();
1486
+ this.mesh.position.add(retreatDirection.multiplyScalar(this.moveSpeed));
1487
+ }
1488
+ break;
1489
+ }
1490
+
1491
+ // ν”Œλ ˆμ΄μ–΄ λ°©ν–₯으둜 νšŒμ „
1492
+ const directionToPlayer = new THREE.Vector3()
1493
+ .subVectors(playerPosition, this.mesh.position)
1494
+ .normalize();
1495
+ const targetRotation = Math.atan2(directionToPlayer.x, directionToPlayer.z);
1496
+ this.mesh.rotation.y = this.smoothRotation(this.mesh.rotation.y, targetRotation, 0.1);
1497
+ }
1498
+ // μ „νˆ¬ 거리 μ‘°μ •
1499
+ const combatMove = this.maintainCombatDistance(playerPosition);
1500
+ if (combatMove.length() > 0) {
1501
+ this.mesh.position.add(combatMove.multiplyScalar(this.moveSpeed));
1502
+ }
1503
+
1504
+ // 곡격 처리
1505
+ if (hasLineOfSight && distanceToPlayer <= ENEMY_CONFIG.ATTACK_RANGE && this.canShoot(playerPosition)) {
1506
+ this.shoot(playerPosition);
1507
+ }
1508
+
1509
+ // μ΄μ•Œ μ—…λ°μ΄νŠΈ
1510
+ this.updateBullets();
1511
+
1512
+ // 탱크 기울기 μ‘°μ •
1513
+ this.adjustTankTilt();
1514
+ }
1515
+
1516
+ // μ‹œμ•Ό 확인 λ©”μ„œλ“œ
1517
+ checkLineOfSight(targetPosition) {
1518
+ if (!this.mesh) return false;
1519
+
1520
+ const startPos = this.mesh.position.clone();
1521
+ startPos.y += 2; // 포탑 높이
1522
+ const direction = new THREE.Vector3()
1523
+ .subVectors(targetPosition, startPos)
1524
+ .normalize();
1525
+ const distance = startPos.distanceTo(targetPosition);
1526
+
1527
+ const raycaster = new THREE.Raycaster(startPos, direction, 0, distance);
1528
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
1529
+
1530
+ return intersects.length === 0;
1531
+ }
1532
+
1533
+ // μž₯μ• λ¬Ό 좩돌 체크
1534
+ checkCollisionAtPosition(position) {
1535
+ const testBox = new THREE.Box3();
1536
+ const tankSize = new THREE.Vector3(3, 3, 3); // 탱크 크기에 맞게 μ‘°μ •
1537
+
1538
+ testBox.setFromCenterAndSize(
1539
+ position,
1540
+ tankSize
1541
+ );
1542
+
1543
+ return window.gameInstance.obstacles.some(obstacle => {
1544
+ if (obstacle.userData.isCollidable) {
1545
+ const obstacleBox = new THREE.Box3().setFromObject(obstacle);
1546
+ return testBox.intersectsBox(obstacleBox);
1547
+ }
1548
+ return false;
1549
+ });
1550
+ }
1551
+
1552
+ // λŒ€μ²΄ 경둜 μ°ΎκΈ°
1553
+ findAlternativePath(playerPosition) {
1554
+ const currentPos = this.mesh.position.clone();
1555
+ const directionToPlayer = new THREE.Vector3()
1556
+ .subVectors(playerPosition, currentPos)
1557
+ .normalize();
1558
+
1559
+ // 이전에 μ„ νƒν•œ 우회 λ°©ν–₯이 μžˆλ‹€λ©΄ κ·Έ λ°©ν–₯을 μœ μ§€
1560
+ if (this.lastAvoidanceDirection) {
1561
+ const checkPoint = currentPos.clone().add(
1562
+ this.lastAvoidanceDirection.multiplyScalar(30)
1563
+ );
1564
+
1565
+ if (this.checkPathClear(currentPos, checkPoint)) {
1566
+ return checkPoint;
1567
+ }
1568
+ }
1569
+
1570
+ // 쒌우 90도 λ°©ν–₯ 계산
1571
+ const leftDirection = new THREE.Vector3()
1572
+ .copy(directionToPlayer)
1573
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
1574
+ const rightDirection = new THREE.Vector3()
1575
+ .copy(directionToPlayer)
1576
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2);
1577
+
1578
+ // 체크 거리λ₯Ό 30λ―Έν„°λ‘œ μ„€μ •
1579
+ const checkDistance = 30;
1580
+ const leftPoint = currentPos.clone().add(leftDirection.multiplyScalar(checkDistance));
1581
+ const rightPoint = currentPos.clone().add(rightDirection.multiplyScalar(checkDistance));
1582
+
1583
+ // 각 λ°©ν–₯의 μž₯μ• λ¬Ό 체크
1584
+ const leftClear = this.checkPathClear(currentPos, leftPoint);
1585
+ const rightClear = this.checkPathClear(currentPos, rightPoint);
1586
+
1587
+ // μƒˆλ‘œμš΄ λ°©ν–₯ κ²°μ • 둜직
1588
+ let targetPoint = null;
1589
+ if (leftClear && rightClear) {
1590
+ // ν”Œλ ˆμ΄μ–΄μ™€μ˜ 각도λ₯Ό κ³ λ €ν•˜μ—¬ 더 효율적인 λ°©ν–₯ 선택
1591
+ const leftAngle = Math.abs(this.calculateAngleToPlayer(leftPoint, playerPosition));
1592
+ const rightAngle = Math.abs(this.calculateAngleToPlayer(rightPoint, playerPosition));
1593
+
1594
+ if (leftAngle < rightAngle) {
1595
+ targetPoint = leftPoint;
1596
+ this.lastAvoidanceDirection = leftDirection;
1597
+ } else {
1598
+ targetPoint = rightPoint;
1599
+ this.lastAvoidanceDirection = rightDirection;
1600
+ }
1601
+ } else if (leftClear) {
1602
+ targetPoint = leftPoint;
1603
+ this.lastAvoidanceDirection = leftDirection;
1604
+ } else if (rightClear) {
1605
+ targetPoint = rightPoint;
1606
+ this.lastAvoidanceDirection = rightDirection;
1607
+ } else {
1608
+ // μ–‘μͺ½ λ‹€ λ§‰ν˜€μžˆμ„ 경우 ν›„μ§„
1609
+ const backDirection = directionToPlayer.multiplyScalar(-1);
1610
+ targetPoint = currentPos.clone().add(backDirection.multiplyScalar(checkDistance));
1611
+ this.lastAvoidanceDirection = backDirection;
1612
+ }
1613
+
1614
+ return targetPoint;
1615
+ }
1616
+ // 경둜 μœ νš¨μ„± 확인
1617
+ checkPathClear(start, end) {
1618
+ const direction = new THREE.Vector3().subVectors(end, start).normalize();
1619
+ const distance = start.distanceTo(end);
1620
+ const raycaster = new THREE.Raycaster(start, direction, 0, distance);
1621
+ const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true);
1622
+ return intersects.length === 0;
1623
+ }
1624
+
1625
+ // ν”Œλ ˆμ΄μ–΄μ™€μ˜ 각도 계산
1626
+ calculateAngleToPlayer(position, playerPosition) {
1627
+ const dirToPlayer = new THREE.Vector3()
1628
+ .subVectors(playerPosition, position)
1629
+ .normalize();
1630
+ const forward = new THREE.Vector3(0, 0, 1);
1631
+ return Math.atan2(dirToPlayer.x, dirToPlayer.z);
1632
+ }
1633
+
1634
+ // λΆ€λ“œλŸ¬μš΄ νšŒμ „ 처리
1635
+ smoothRotation(current, target, factor) {
1636
+ let delta = target - current;
1637
+
1638
+ // 각도 차이λ₯Ό -PIμ—μ„œ PI μ‚¬μ΄λ‘œ μ •κ·œν™”
1639
+ while (delta > Math.PI) delta -= Math.PI * 2;
1640
+ while (delta < -Math.PI) delta += Math.PI * 2;
1641
+
1642
+ return current + delta * factor;
1643
+ }
1644
+
1645
+ // 탱크 기울기 μ‘°μ •
1646
+ adjustTankTilt() {
1647
+ const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion);
1648
+ const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.mesh.quaternion);
1649
+
1650
+ const frontHeight = window.gameInstance.getHeightAtPosition(
1651
+ this.mesh.position.x + forwardVector.x,
1652
+ this.mesh.position.z + forwardVector.z
1653
+ );
1654
+ const backHeight = window.gameInstance.getHeightAtPosition(
1655
+ this.mesh.position.x - forwardVector.x,
1656
+ this.mesh.position.z - forwardVector.z
1657
+ );
1658
+ const rightHeight = window.gameInstance.getHeightAtPosition(
1659
+ this.mesh.position.x + rightVector.x,
1660
+ this.mesh.position.z + rightVector.z
1661
+ );
1662
+ const leftHeight = window.gameInstance.getHeightAtPosition(
1663
+ this.mesh.position.x - rightVector.x,
1664
+ this.mesh.position.z - rightVector.z
1665
+ );
1666
+
1667
+ const pitch = Math.atan2(frontHeight - backHeight, 2);
1668
+ const roll = Math.atan2(rightHeight - leftHeight, 2);
1669
+
1670
+ const currentRotation = this.mesh.rotation.y;
1671
+ this.mesh.rotation.set(pitch, currentRotation, roll);
1672
+ }
1673
+
1674
+ updateBullets() {
1675
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
1676
+ const bullet = this.bullets[i];
1677
+ bullet.position.add(bullet.velocity);
1678
+
1679
+ if (Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
1680
+ Math.abs(bullet.position.z) > MAP_SIZE / 2) {
1681
+ this.scene.remove(bullet);
1682
+ this.bullets.splice(i, 1);
1683
+ continue;
1684
+ }
1685
+
1686
+ const bulletBox = new THREE.Box3().setFromObject(bullet);
1687
+ for (const obstacle of window.gameInstance.obstacles) {
1688
+ const obstacleBox = new THREE.Box3().setFromObject(obstacle);
1689
+ if (bulletBox.intersectsBox(obstacleBox)) {
1690
+ this.scene.remove(bullet);
1691
+ this.bullets.splice(i, 1);
1692
+ break;
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1697
+
1698
+ createMuzzleFlash() {
1699
+ if (!this.mesh) return;
1700
+
1701
+ const flashGroup = new THREE.Group();
1702
+
1703
+ const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8);
1704
+ const flameMaterial = new THREE.MeshBasicMaterial({
1705
+ color: 0xffa500,
1706
+ transparent: true,
1707
+ opacity: 0.8
1708
+ });
1709
+ const flame = new THREE.Mesh(flameGeometry, flameMaterial);
1710
+ flame.scale.set(2, 2, 3);
1711
+ flashGroup.add(flame);
1712
+
1713
+ const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8);
1714
+ const smokeMaterial = new THREE.MeshBasicMaterial({
1715
+ color: 0x555555,
1716
+ transparent: true,
1717
+ opacity: 0.5
1718
+ });
1719
+
1720
+ for (let i = 0; i < 5; i++) {
1721
+ const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial);
1722
+ smoke.position.set(
1723
+ Math.random() * 1 - 0.5,
1724
+ Math.random() * 1 - 0.5,
1725
+ -1 - Math.random()
1726
+ );
1727
+ smoke.scale.set(1.5, 1.5, 1.5);
1728
+ flashGroup.add(smoke);
1729
+ }
1730
+
1731
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
1732
+ const muzzlePosition = new THREE.Vector3();
1733
+ const meshWorldQuaternion = new THREE.Quaternion();
1734
+
1735
+ this.mesh.getWorldPosition(muzzlePosition);
1736
+ this.mesh.getWorldQuaternion(meshWorldQuaternion);
1737
+
1738
+ muzzleOffset.applyQuaternion(meshWorldQuaternion);
1739
+ muzzlePosition.add(muzzleOffset);
1740
+
1741
+ flashGroup.position.copy(muzzlePosition);
1742
+ flashGroup.quaternion.copy(meshWorldQuaternion);
1743
+
1744
+ this.scene.add(flashGroup);
1745
+
1746
+ setTimeout(() => {
1747
+ this.scene.remove(flashGroup);
1748
+ }, 500);
1749
+ }
1750
+
1751
+ shoot(playerPosition) {
1752
+ const currentTime = Date.now();
1753
+ const attackInterval = this.type === 'tank' ?
1754
+ ENEMY_CONFIG.ATTACK_INTERVAL :
1755
+ ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
1756
+
1757
+ if (currentTime - this.lastAttackTime < attackInterval) return;
1758
+
1759
+ // ν”Œλ ˆμ΄μ–΄μ™€μ˜ λ°©ν–₯ 차이 계산
1760
+ const directionToPlayer = new THREE.Vector3()
1761
+ .subVectors(playerPosition, this.mesh.position)
1762
+ .normalize();
1763
+ const forwardDirection = new THREE.Vector3(0, 0, 1)
1764
+ .applyQuaternion(this.mesh.quaternion)
1765
+ .normalize();
1766
+
1767
+ const dotProduct = forwardDirection.dot(directionToPlayer);
1768
+ const angleToPlayer = Math.acos(dotProduct);
1769
+
1770
+ // 일정 각도 μ΄ν•˜μΌ κ²½μš°μ—λ§Œ 곡격
1771
+ const attackAngleThreshold = Math.PI / 8; // μ•½ 22.5도
1772
+ if (angleToPlayer > attackAngleThreshold) return;
1773
+
1774
+ this.createMuzzleFlash();
1775
+
1776
+ const enemyFireSound = new Audio('sounds/mbtfire5.ogg');
1777
+ enemyFireSound.volume = 0.3;
1778
+ enemyFireSound.play();
1779
+
1780
+ const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8);
1781
+ const bulletMaterial = new THREE.MeshBasicMaterial({
1782
+ color: 0xff0000,
1783
+ emissive: 0xff0000,
1784
+ emissiveIntensity: 0.5
1785
+ });
1786
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
1787
+
1788
+ const muzzleOffset = new THREE.Vector3(0, 0.5, 4);
1789
+ const muzzlePosition = new THREE.Vector3();
1790
+ this.mesh.getWorldPosition(muzzlePosition);
1791
+ muzzleOffset.applyQuaternion(this.mesh.quaternion);
1792
+ muzzlePosition.add(muzzleOffset);
1793
+
1794
+ bullet.position.copy(muzzlePosition);
1795
+ bullet.quaternion.copy(this.mesh.quaternion);
1796
+
1797
+ const direction = new THREE.Vector3()
1798
+ .subVectors(playerPosition, muzzlePosition)
1799
+ .normalize();
1800
+
1801
+ const bulletSpeed = this.type === 'tank' ?
1802
+ ENEMY_CONFIG.BULLET_SPEED :
1803
+ ENEMY_CONFIG.BULLET_SPEED * 0.8;
1804
+
1805
+ bullet.velocity = direction.multiplyScalar(bulletSpeed);
1806
+
1807
+ const trailGeometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8);
1808
+ const trailMaterial = new THREE.MeshBasicMaterial({
1809
+ color: 0xff4444,
1810
+ transparent: true,
1811
+ opacity: 0.5
1812
+ });
1813
+
1814
+ const trail = new THREE.Mesh(trailGeometry, trailMaterial);
1815
+ trail.position.z = -1;
1816
+ bullet.add(trail);
1817
+
1818
+ this.scene.add(bullet);
1819
+ this.bullets.push(bullet);
1820
+ this.lastAttackTime = currentTime;
1821
+ }
1822
+
1823
+ takeDamage(damage) {
1824
+ this.health -= damage;
1825
+ return this.health <= 0;
1826
+ }
1827
+
1828
+ destroy() {
1829
+ if (this.mesh) {
1830
+ this.scene.remove(this.mesh);
1831
+ this.bullets.forEach(bullet => this.scene.remove(bullet));
1832
+ this.bullets = [];
1833
+ this.isLoaded = false;
1834
+ }
1835
+ }
1836
+ }
1837
+
1838
+ // Particle 클래슀
1839
+ class Particle {
1840
+ constructor(scene, position) {
1841
+ const geometry = new THREE.SphereGeometry(0.1);
1842
+ const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
1843
+ this.mesh = new THREE.Mesh(geometry, material);
1844
+ this.mesh.position.copy(position);
1845
+
1846
+ this.velocity = new THREE.Vector3(
1847
+ (Math.random() - 0.5) * 0.3,
1848
+ Math.random() * 0.2,
1849
+ (Math.random() - 0.5) * 0.3
1850
+ );
1851
+
1852
+ this.gravity = -0.01;
1853
+ this.lifetime = 60;
1854
+ this.age = 0;
1855
+
1856
+ scene.add(this.mesh);
1857
+ }
1858
+
1859
+ update() {
1860
+ this.velocity.y += this.gravity;
1861
+ this.mesh.position.add(this.velocity);
1862
+ this.age++;
1863
+ return this.age < this.lifetime;
1864
+ }
1865
+
1866
+ destroy(scene) {
1867
+ scene.remove(this.mesh);
1868
+ }
1869
+ }
1870
+
1871
+ // Game 클래슀
1872
+ class Game {
1873
+ constructor() {
1874
+ // κ²Œμž„ μ‹œμž‘ μ—¬λΆ€λ₯Ό μΆ”μ ν•˜λŠ” ν”Œλž˜κ·Έ μΆ”κ°€
1875
+ this.isStarted = false;
1876
+ // μ˜€λ””μ˜€ κ΄€λ ¨ 속성 μΆ”κ°€
1877
+ this.bgmPlaying = false; // BGM μž¬μƒ μƒνƒœ 좔적
1878
+ this.bgm = null; // BGM μ˜€λ””μ˜€ 객체 μ €μž₯
1879
+ this.engineSound = null;
1880
+ this.engineStopSound = null;
1881
+ this.isEngineRunning = false;
1882
+ this.scene = new THREE.Scene();
1883
+ this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
1884
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
1885
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
1886
+ this.renderer.shadowMap.enabled = true;
1887
+ this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // λΆ€λ“œλŸ¬μš΄ 그림자
1888
+ this.renderer.outputColorSpace = THREE.SRGBColorSpace;
1889
+ this.enemyLabels = new Map(); // 적 라벨을 μΆ”μ ν•˜κΈ° μœ„ν•œ Map μΆ”κ°€
1890
+ this.raycaster = new THREE.Raycaster();
1891
+ this.crosshair = document.getElementById('crosshair');
1892
+ document.getElementById('gameContainer').appendChild(this.renderer.domElement);
1893
+
1894
+
1895
+ // λ ˆμ΄λ” κ΄€λ ¨ 속성 μΆ”κ°€
1896
+ this.radarUpdateInterval = 100; // 100msλ§ˆλ‹€ λ ˆμ΄λ” μ—…λ°μ΄νŠΈ
1897
+ this.lastRadarUpdate = 0;
1898
+ this.radarRange = 200; // λ ˆμ΄λ” 감지 λ²”μœ„
1899
+
1900
+ this.tank = new TankPlayer();
1901
+ this.enemies = [];
1902
+ this.particles = [];
1903
+ this.buildings = [];
1904
+ this.loader = new GLTFLoader();
1905
+ this.controls = null;
1906
+ this.gameTime = GAME_DURATION;
1907
+ this.score = 0;
1908
+ this.isGameOver = false;
1909
+ this.isLoading = true;
1910
+ this.previousTankPosition = new THREE.Vector3();
1911
+ this.lastTime = performance.now();
1912
+ this.gameTimer = null;
1913
+ this.animationFrameId = null;
1914
+ this.lastAudio = null; // λ§ˆμ§€λ§‰ λ°œμ‚¬μŒ 좔적을 μœ„ν•œ 속성 μΆ”κ°€
1915
+
1916
+ this.mouse = { x: 0, y: 0 };
1917
+ this.keys = {
1918
+ forward: false,
1919
+ backward: false,
1920
+ left: false,
1921
+ right: false
1922
+ };
1923
+
1924
+ this.setupEventListeners();
1925
+ this.initialize();
1926
+ this.obstacles = []; // obstacles λ°°μ—΄ μΆ”κ°€
1927
+ }
1928
+ setupScene() {
1929
+ // 씬 μ΄ˆκΈ°ν™”
1930
+ this.scene.background = new THREE.Color(0x87CEEB); // ν•˜λŠ˜μƒ‰ λ°°κ²½
1931
+
1932
+ // μ•ˆκ°œ μ„€μ •
1933
+ this.scene.fog = new THREE.FogExp2(0x87CEEB, 0.0008);
1934
+
1935
+ // λ Œλ”λŸ¬ μ„€μ •
1936
+ this.renderer.shadowMap.enabled = true;
1937
+ this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
1938
+ this.renderer.setPixelRatio(window.devicePixelRatio);
1939
+
1940
+ // 카메라 초기 μ„€μ •
1941
+ this.camera.position.set(0, 15, -30);
1942
+ this.camera.lookAt(0, 0, 0);
1943
+
1944
+ // 씬 경계 μ„€μ •
1945
+ const mapBoundary = MAP_SIZE / 2;
1946
+ this.sceneBounds = {
1947
+ minX: -mapBoundary,
1948
+ maxX: mapBoundary,
1949
+ minZ: -mapBoundary,
1950
+ maxZ: mapBoundary
1951
+ };
1952
+ }
1953
+
1954
+ async initialize() {
1955
+ try {
1956
+ // BGM이 아직 μž¬μƒλ˜μ§€ μ•Šμ€ κ²½μš°μ—λ§Œ μž¬μƒ
1957
+ if (!this.bgmPlaying && !this.bgm) {
1958
+ this.bgm = new Audio('sounds/BGM.ogg');
1959
+ this.bgm.volume = 0.5;
1960
+ this.bgm.loop = true;
1961
+ this.bgm.play();
1962
+ this.bgmPlaying = true;
1963
+ }
1964
+
1965
+ // μ‹œμž‘ μ‚¬μš΄λ“œ μž¬μƒ
1966
+ const startSounds = ['sounds/start1.ogg', 'sounds/start2.ogg', 'sounds/start3.ogg'];
1967
+ const randomStartSound = startSounds[Math.floor(Math.random() * startSounds.length)];
1968
+ const startAudio = new Audio(randomStartSound);
1969
+ startAudio.volume = 0.5;
1970
+ startAudio.play();
1971
+
1972
+ // λ Œλ”λŸ¬ μ„€μ •
1973
+ this.renderer.shadowMap.enabled = true;
1974
+ this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
1975
+ this.renderer.outputColorSpace = THREE.SRGBColorSpace;
1976
+
1977
+ // κΈ°λ³Έ 씬 μ„€μ •
1978
+ this.setupScene();
1979
+
1980
+ // μ•ˆκ°œ 효과
1981
+ this.scene.fog = new THREE.FogExp2(0x87CEEB, 0.0008);
1982
+ this.scene.background = new THREE.Color(0x87CEEB);
1983
+
1984
+ // μ‘°λͺ… μ„€μ •
1985
+ // μ£Όλ³€κ΄‘
1986
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
1987
+ this.scene.add(ambientLight);
1988
+
1989
+ // 메인 νƒœμ–‘κ΄‘
1990
+ const mainLight = new THREE.DirectionalLight(0xffffff, 1.0);
1991
+ mainLight.position.set(MAP_SIZE/2, MAP_SIZE/2, MAP_SIZE/2);
1992
+ mainLight.castShadow = true;
1993
+
1994
+ // 그림자 μ„€μ •
1995
+ mainLight.shadow.mapSize.width = 4096;
1996
+ mainLight.shadow.mapSize.height = 4096;
1997
+ mainLight.shadow.camera.near = 0.5;
1998
+ mainLight.shadow.camera.far = MAP_SIZE * 2;
1999
+ mainLight.shadow.camera.left = -MAP_SIZE;
2000
+ mainLight.shadow.camera.right = MAP_SIZE;
2001
+ mainLight.shadow.camera.top = MAP_SIZE;
2002
+ mainLight.shadow.camera.bottom = -MAP_SIZE;
2003
+ mainLight.shadow.bias = -0.001;
2004
+ mainLight.shadow.radius = 2;
2005
+ mainLight.shadow.normalBias = 0.02;
2006
+
2007
+ this.scene.add(mainLight);
2008
+
2009
+ // 보쑰 νƒœμ–‘κ΄‘
2010
+ const secondaryLight = new THREE.DirectionalLight(0xffffff, 0.3);
2011
+ secondaryLight.position.set(-50, 50, -50);
2012
+ this.scene.add(secondaryLight);
2013
+
2014
+ // ν™˜κ²½κ΄‘
2015
+ const hemisphereLight = new THREE.HemisphereLight(
2016
+ 0x87CEEB,
2017
+ 0xFFE87C,
2018
+ 0.3
2019
+ );
2020
+ this.scene.add(hemisphereLight);
2021
+ // μ§€ν˜• 생성
2022
+ const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100);
2023
+ const groundMaterial = new THREE.MeshStandardMaterial({
2024
+ color: 0xD2B48C,
2025
+ roughness: 0.8,
2026
+ metalness: 0.2,
2027
+ envMapIntensity: 1.0
2028
+ });
2029
+
2030
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
2031
+ ground.rotation.x = -Math.PI / 2;
2032
+ ground.receiveShadow = true;
2033
+
2034
+ // μ§€ν˜• 높이 μ„€μ •
2035
+ const vertices = ground.geometry.attributes.position.array;
2036
+ for (let i = 0; i < vertices.length; i += 3) {
2037
+ vertices[i + 2] = 0; // λͺ¨λ“  높이λ₯Ό 0으둜 μ„€μ •
2038
+ }
2039
+
2040
+ ground.geometry.attributes.position.needsUpdate = true;
2041
+ ground.geometry.computeVertexNormals();
2042
+ this.ground = ground;
2043
+ this.scene.add(ground);
2044
+
2045
+ // 격자 효과 μΆ”κ°€
2046
+ const gridHelper = new THREE.GridHelper(MAP_SIZE, 50, 0x000000, 0x000000);
2047
+ gridHelper.material.opacity = 0.1;
2048
+ gridHelper.material.transparent = true;
2049
+ gridHelper.position.y = 0.1;
2050
+ this.scene.add(gridHelper);
2051
+
2052
+ // 사막 μž₯식 μΆ”κ°€
2053
+ await this.addDesertDecorations();
2054
+
2055
+ // 탱크 μ΄ˆκΈ°ν™”
2056
+ await this.tank.initialize(this.scene, this.loader);
2057
+ if (!this.tank.isLoaded) {
2058
+ throw new Error('Tank loading failed');
2059
+ }
2060
+
2061
+ // 슀폰 μœ„μΉ˜ 검증
2062
+ const spawnPos = this.findValidSpawnPosition();
2063
+ const heightAtSpawn = this.getHeightAtPosition(spawnPos.x, spawnPos.z);
2064
+ const slopeCheckPoints = [
2065
+ { x: spawnPos.x + 2, z: spawnPos.z },
2066
+ { x: spawnPos.x - 2, z: spawnPos.z },
2067
+ { x: spawnPos.x, z: spawnPos.z + 2 },
2068
+ { x: spawnPos.x, z: spawnPos.z - 2 }
2069
+ ];
2070
+
2071
+ const slopes = slopeCheckPoints.map(point => {
2072
+ const pointHeight = this.getHeightAtPosition(point.x, point.z);
2073
+ return Math.abs(pointHeight - heightAtSpawn) / 2;
2074
+ });
2075
+
2076
+ const maxSlope = Math.max(...slopes);
2077
+ if (maxSlope > 0.3) {
2078
+ location.reload();
2079
+ return;
2080
+ }
2081
+
2082
+ // 카메라 초기 μ„€μ •
2083
+ const tankPosition = this.tank.getPosition();
2084
+ this.camera.position.set(
2085
+ tankPosition.x,
2086
+ tankPosition.y + 15,
2087
+ tankPosition.z - 30
2088
+ );
2089
+ this.camera.lookAt(tankPosition);
2090
+
2091
+ // λ‘œλ”© μ™„λ£Œ 처리
2092
+ this.isLoading = false;
2093
+ document.getElementById('loading').style.display = 'none';
2094
+
2095
+ // κ²Œμž„ μ‹œμž‘
2096
+ this.animate();
2097
+ this.spawnEnemies();
2098
+ this.startGameTimer();
2099
+
2100
+ } catch (error) {
2101
+ console.error('Game initialization error:', error);
2102
+ this.handleLoadingError();
2103
+ }
2104
+ }
2105
+
2106
+ // λ ˆμ΄λ” μ—…λ°μ΄νŠΈ λ©”μ„œλ“œ μΆ”κ°€
2107
+ updateRadar() {
2108
+ const currentTime = Date.now();
2109
+ if (currentTime - this.lastRadarUpdate < this.radarUpdateInterval) return;
2110
+
2111
+ const radar = document.getElementById('radar');
2112
+ const radarRect = radar.getBoundingClientRect();
2113
+ const radarCenter = {
2114
+ x: radarRect.width / 2,
2115
+ y: radarRect.height / 2
2116
+ };
2117
+
2118
+ // κΈ°μ‘΄ 적 λ„νŠΈ 제거
2119
+ const oldDots = radar.getElementsByClassName('enemy-dot');
2120
+ while (oldDots[0]) {
2121
+ oldDots[0].remove();
2122
+ }
2123
+
2124
+ // 탱크 μœ„μΉ˜ κ°€μ Έμ˜€κΈ°
2125
+ const tankPos = this.tank.getPosition();
2126
+
2127
+ // λͺ¨λ“  적에 λŒ€ν•΄ λ ˆμ΄λ”μ— ν‘œμ‹œ
2128
+ this.enemies.forEach(enemy => {
2129
+ if (!enemy.mesh || !enemy.isLoaded) return;
2130
+
2131
+ const enemyPos = enemy.mesh.position;
2132
+ const distance = tankPos.distanceTo(enemyPos);
2133
+
2134
+ // λ ˆμ΄λ” λ²”μœ„ 내에 μžˆλŠ” 경우만 ν‘œμ‹œ
2135
+ if (distance <= this.radarRange) {
2136
+ // 탱크 κΈ°μ€€ μƒλŒ€ 각도 계산
2137
+ const angle = Math.atan2(
2138
+ enemyPos.x - tankPos.x,
2139
+ enemyPos.z - tankPos.z
2140
+ );
2141
+
2142
+ // μƒλŒ€ 거리λ₯Ό λ ˆμ΄λ” 크기에 맞게 μŠ€μΌ€μΌλ§
2143
+ const relativeDistance = distance / this.radarRange;
2144
+ const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance);
2145
+ const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance);
2146
+
2147
+ // 적 λ„νŠΈ 생성 및 μΆ”κ°€
2148
+ const dot = document.createElement('div');
2149
+ dot.className = 'enemy-dot';
2150
+ dot.style.left = `${dotX}px`;
2151
+ dot.style.top = `${dotY}px`;
2152
+ radar.appendChild(dot);
2153
+ }
2154
+ });
2155
+
2156
+ this.lastRadarUpdate = currentTime;
2157
+ }
2158
+
2159
+ async addDesertDecorations() {
2160
+ if (!this.obstacles) {
2161
+ this.obstacles = [];
2162
+ }
2163
+
2164
+ const BUILDING_COUNT = 50;
2165
+ const buildingModels = [
2166
+ 'models/house1.glb',
2167
+ 'models/house2.glb',
2168
+ 'models/house3.glb',
2169
+ 'models/house4.glb'
2170
+ ];
2171
+
2172
+ // 좩돌 λ°•μŠ€ μ‹œκ°ν™”λ₯Ό μœ„ν•œ 재질
2173
+ const collisionBoxMaterial = new THREE.LineBasicMaterial({
2174
+ color: 0xff0000,
2175
+ linewidth: 2
2176
+ });
2177
+
2178
+ for (let i = 0; i < BUILDING_COUNT; i++) {
2179
+ try {
2180
+ const modelPath = buildingModels[Math.floor(Math.random() * buildingModels.length)];
2181
+ const result = await this.loader.loadAsync(modelPath);
2182
+ const building = result.scene;
2183
+
2184
+ let x, z;
2185
+ const edgeSpawn = Math.random() < 0.7;
2186
+
2187
+ if (edgeSpawn) {
2188
+ if (Math.random() < 0.5) {
2189
+ x = (Math.random() < 0.5 ? -1 : 1) * (MAP_SIZE * 0.4 + Math.random() * MAP_SIZE * 0.1);
2190
+ z = (Math.random() - 0.5) * MAP_SIZE * 0.9;
2191
+ } else {
2192
+ x = (Math.random() - 0.5) * MAP_SIZE * 0.9;
2193
+ z = (Math.random() < 0.5 ? -1 : 1) * (MAP_SIZE * 0.4 + Math.random() * MAP_SIZE * 0.1);
2194
+ }
2195
+ } else {
2196
+ x = (Math.random() - 0.5) * MAP_SIZE * 0.6;
2197
+ z = (Math.random() - 0.5) * MAP_SIZE * 0.6;
2198
+ }
2199
+
2200
+ building.position.set(x, 0, z);
2201
+ building.rotation.y = Math.random() * Math.PI * 2;
2202
+ building.scale.set(1, 1, 1);
2203
+
2204
+ // 그림자 μ„€μ •
2205
+ building.traverse((child) => {
2206
+ if (child.isMesh) {
2207
+ child.castShadow = true;
2208
+ child.receiveShadow = true;
2209
+ }
2210
+ });
2211
+
2212
+ // 좩돌 λ°•μŠ€ 생성 및 μ‹œκ°ν™”
2213
+ const boundingBox = new THREE.Box3().setFromObject(building);
2214
+ const boxSize = boundingBox.getSize(new THREE.Vector3());
2215
+
2216
+ // 좩돌 λ°•μŠ€μ˜ μ™€μ΄μ–΄ν”„λ ˆμž„ 생성
2217
+ const boxGeometry = new THREE.BufferGeometry();
2218
+ const positions = new Float32Array([
2219
+ // λ°”λ‹₯ μ‚¬κ°ν˜•
2220
+ -boxSize.x/2, 0.1, -boxSize.z/2,
2221
+ -boxSize.x/2, 0.1, boxSize.z/2,
2222
+ boxSize.x/2, 0.1, boxSize.z/2,
2223
+ boxSize.x/2, 0.1, -boxSize.z/2,
2224
+ -boxSize.x/2, 0.1, -boxSize.z/2
2225
+ ]);
2226
+
2227
+ boxGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
2228
+ const collisionBoxLine = new THREE.Line(boxGeometry, collisionBoxMaterial);
2229
+ collisionBoxLine.position.copy(building.position);
2230
+ collisionBoxLine.rotation.y = building.rotation.y;
2231
+
2232
+ // 좩돌 데이터 μ„€μ •
2233
+ building.userData.isCollidable = true;
2234
+ building.userData.type = 'building';
2235
+ building.userData.collisionBox = collisionBoxLine;
2236
+
2237
+ let tooClose = false;
2238
+ for (const obstacle of this.obstacles) {
2239
+ const distance = building.position.distanceTo(obstacle.position);
2240
+ if (distance < 20) {
2241
+ tooClose = true;
2242
+ break;
2243
+ }
2244
+ }
2245
+
2246
+ if (!tooClose) {
2247
+ this.obstacles.push(building);
2248
+ this.scene.add(building);
2249
+ this.scene.add(collisionBoxLine); // 좩돌 λ°•μŠ€ 라인 μΆ”κ°€
2250
+ }
2251
+
2252
+ } catch (error) {
2253
+ console.error('Error loading building model:', error);
2254
+ }
2255
+ }
2256
+
2257
+
2258
+ // 선인μž₯ μΆ”κ°€ (κΈ°μ‘΄ μ½”λ“œ μœ μ§€)
2259
+ const cactusGeometry = new THREE.CylinderGeometry(0.5, 0.7, 4, 8);
2260
+ const cactusMaterial = new THREE.MeshStandardMaterial({
2261
+ color: 0x2F4F2F,
2262
+ roughness: 0.8
2263
+ });
2264
+
2265
+ for (let i = 0; i < 50; i++) {
2266
+ const cactus = new THREE.Mesh(cactusGeometry, cactusMaterial);
2267
+ cactus.position.set(
2268
+ (Math.random() - 0.5) * MAP_SIZE * 0.8,
2269
+ 2,
2270
+ (Math.random() - 0.5) * MAP_SIZE * 0.8
2271
+ );
2272
+ cactus.castShadow = true;
2273
+ cactus.receiveShadow = true;
2274
+ cactus.userData.isCollidable = false;
2275
+ cactus.userData.type = 'cactus';
2276
+
2277
+ this.scene.add(cactus);
2278
+ }
2279
+ }
2280
+
2281
+
2282
+ getHeightAtPosition(x, z) {
2283
+ return 0; // 항상 높이 0 λ°˜ν™˜
2284
+ }
2285
+
2286
+ findValidSpawnPosition() {
2287
+ const margin = 50;
2288
+ let position;
2289
+ let attempts = 0;
2290
+ const maxAttempts = 50;
2291
+ const maxSlope = 0.3; // μ΅œλŒ€ ν—ˆμš© 경사
2292
+
2293
+ while (attempts < maxAttempts) {
2294
+ position = new THREE.Vector3(
2295
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2),
2296
+ 0,
2297
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2)
2298
+ );
2299
+
2300
+ // ν˜„μž¬ μœ„μΉ˜μ˜ 높이 κ°€μ Έμ˜€κΈ°
2301
+ const height = this.getHeightAtPosition(position.x, position.z);
2302
+ position.y = height + TANK_HEIGHT;
2303
+
2304
+ // μ£Όλ³€ μ§€ν˜•μ˜ 경사 체크
2305
+ const checkPoints = [
2306
+ { x: position.x + 2, z: position.z },
2307
+ { x: position.x - 2, z: position.z },
2308
+ { x: position.x, z: position.z + 2 },
2309
+ { x: position.x, z: position.z - 2 }
2310
+ ];
2311
+
2312
+ const slopes = checkPoints.map(point => {
2313
+ const pointHeight = this.getHeightAtPosition(point.x, point.z);
2314
+ return Math.abs(pointHeight - height) / 2;
2315
+ });
2316
+
2317
+ const maxCurrentSlope = Math.max(...slopes);
2318
+
2319
+ if (maxCurrentSlope <= maxSlope) {
2320
+ return position;
2321
+ }
2322
+
2323
+ attempts++;
2324
+ }
2325
+
2326
+ // μ‹€νŒ¨ μ‹œ κΈ°λ³Έ μœ„μΉ˜ λ°˜ν™˜
2327
+ return new THREE.Vector3(0, TANK_HEIGHT, 0);
2328
+ }
2329
+
2330
+ setupEventListeners() {
2331
+ document.addEventListener('keydown', (event) => {
2332
+ if (this.isLoading || this.isGameOver) return;
2333
+ switch(event.code) {
2334
+ case 'KeyW': this.keys.forward = true; break;
2335
+ case 'KeyS': this.keys.backward = true; break;
2336
+ case 'KeyA': this.keys.left = true; break;
2337
+ case 'KeyD': this.keys.right = true; break;
2338
+ }
2339
+ });
2340
+
2341
+ document.addEventListener('keyup', (event) => {
2342
+ if (this.isLoading || this.isGameOver) return;
2343
+ switch(event.code) {
2344
+ case 'KeyW': this.keys.forward = false; break;
2345
+ case 'KeyS': this.keys.backward = false; break;
2346
+ case 'KeyA': this.keys.left = false; break;
2347
+ case 'KeyD': this.keys.right = false; break;
2348
+ }
2349
+ });
2350
+
2351
+ document.addEventListener('mousemove', (event) => {
2352
+ if (this.isLoading || this.isGameOver || !document.pointerLockElement) return;
2353
+
2354
+ const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
2355
+ this.mouse.x += movementX * 0.002;
2356
+ this.mouse.y = 0;
2357
+
2358
+ while (this.mouse.x > Math.PI) this.mouse.x -= Math.PI * 2;
2359
+ while (this.mouse.x < -Math.PI) this.mouse.x += Math.PI * 2;
2360
+ });
2361
+
2362
+ document.addEventListener('click', () => {
2363
+ if (!document.pointerLockElement) {
2364
+ document.body.requestPointerLock();
2365
+ } else if (!this.isGameOver && this.tank && this.tank.isLoaded) {
2366
+ const bullet = this.tank.shoot(this.scene);
2367
+ if (bullet) {
2368
+ // Shooting effects...
2369
+ }
2370
+ }
2371
+ });
2372
+
2373
+ document.addEventListener('pointerlockchange', () => {
2374
+ if (!document.pointerLockElement) {
2375
+ this.mouse.x = 0;
2376
+ this.mouse.y = 0;
2377
+ }
2378
+ });
2379
+
2380
+ window.addEventListener('resize', () => {
2381
+ this.camera.aspect = window.innerWidth / window.innerHeight;
2382
+ this.camera.updateProjectionMatrix();
2383
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
2384
+ });
2385
+ }
2386
+ handleMovement() {
2387
+ if (!this.tank.isLoaded || this.isGameOver) return;
2388
+
2389
+ const direction = new THREE.Vector3();
2390
+ const isMoving = this.keys.forward || this.keys.backward;
2391
+ // 이동 μ‹œμž‘ν•  λ•Œ μ‚¬μš΄λ“œ 처리
2392
+ if (isMoving && !this.isEngineRunning) {
2393
+ this.isEngineRunning = true;
2394
+
2395
+ // 이전 μ‚¬μš΄λ“œ μ •μ§€
2396
+ if (this.engineStopSound) {
2397
+ this.engineStopSound.pause();
2398
+ }
2399
+ if (this.engineSound) {
2400
+ this.engineSound.pause();
2401
+ }
2402
+
2403
+ // μ—”μ§„ μ •μ§€ μ‚¬μš΄λ“œ μž¬μƒ
2404
+ this.engineStopSound = new Audio('sounds/engine.ogg');
2405
+ this.engineStopSound.play();
2406
+
2407
+ // μ—”μ§„ μ •μ§€ μ‚¬μš΄λ“œ μ’…λ£Œ ν›„ μ—”μ§„ μ‚¬μš΄λ“œ μ‹œμž‘
2408
+ this.engineStopSound.onended = () => {
2409
+ this.engineSound = new Audio('sounds/engine.ogg');
2410
+ this.engineSound.loop = true;
2411
+ this.engineSound.play();
2412
+ };
2413
+ }
2414
+ // 이동 멈좜 λ•Œ μ‚¬μš΄λ“œ 처리
2415
+ else if (!isMoving && this.isEngineRunning) {
2416
+ this.isEngineRunning = false;
2417
+ if (this.engineSound) {
2418
+ this.engineSound.pause();
2419
+ this.engineSound = null;
2420
+ }
2421
+ if (this.engineStopSound) {
2422
+ this.engineStopSound.pause();
2423
+ this.engineStopSound = null;
2424
+ }
2425
+ const stopSound = new Audio('sounds/enginestop.ogg');
2426
+ stopSound.play();
2427
+ }
2428
+
2429
+
2430
+ // 차체 이동과 νšŒμ „μ€ μœ μ§€
2431
+ if (this.keys.forward) direction.z += 1;
2432
+ if (this.keys.backward) direction.z -= 1;
2433
+ if (this.keys.left) this.tank.rotate(-1);
2434
+ if (this.keys.right) this.tank.rotate(1);
2435
+
2436
+ if (direction.length() > 0) {
2437
+ direction.normalize();
2438
+ direction.applyEuler(this.tank.body.rotation);
2439
+ this.tank.move(direction);
2440
+ }
2441
+
2442
+ // 탱크 μœ„μΉ˜ κ°€μ Έμ˜€κΈ°
2443
+ const tankPos = this.tank.getPosition();
2444
+
2445
+ // μΉ΄λ©”λΌλŠ” 마우슀 X νšŒμ „μ—λ§Œ 따라감
2446
+ const cameraDistance = 15;
2447
+ const cameraHeight = 8;
2448
+ const cameraAngle = this.mouse.x + Math.PI; // 항상 ν¬νƒ‘μ˜ λ’€μͺ½μ— μœ„μΉ˜
2449
+
2450
+ const cameraX = tankPos.x + Math.sin(cameraAngle) * cameraDistance;
2451
+ const cameraZ = tankPos.z + Math.cos(cameraAngle) * cameraDistance;
2452
+
2453
+ this.camera.position.set(
2454
+ cameraX,
2455
+ tankPos.y + cameraHeight,
2456
+ cameraZ
2457
+ );
2458
+
2459
+ // 카메라가 탱크λ₯Ό 바라보도둝 μ„€μ •
2460
+ const lookAtPoint = new THREE.Vector3(
2461
+ tankPos.x,
2462
+ tankPos.y + 1,
2463
+ tankPos.z
2464
+ );
2465
+ this.camera.lookAt(lookAtPoint);
2466
+ }
2467
+
2468
+ createBuildings() {
2469
+ const buildingTypes = [
2470
+ { width: 10, height: 30, depth: 10, color: 0x808080 },
2471
+ { width: 15, height: 40, depth: 15, color: 0x606060 },
2472
+ { width: 20, height: 50, depth: 20, color: 0x404040 }
2473
+ ];
2474
+
2475
+ for (let i = 0; i < BUILDING_COUNT; i++) {
2476
+ const type = buildingTypes[Math.floor(Math.random() * buildingTypes.length)];
2477
+ const building = this.createBuilding(type);
2478
+
2479
+ let position;
2480
+ let attempts = 0;
2481
+ do {
2482
+ position = new THREE.Vector3(
2483
+ (Math.random() - 0.5) * (MAP_SIZE - type.width),
2484
+ type.height / 2,
2485
+ (Math.random() - 0.5) * (MAP_SIZE - type.depth)
2486
+ );
2487
+ attempts++;
2488
+ } while (this.checkBuildingCollision(position, type) && attempts < 50);
2489
+
2490
+ if (attempts < 50) {
2491
+ building.position.copy(position);
2492
+ this.buildings.push(building);
2493
+ this.scene.add(building);
2494
+ }
2495
+ }
2496
+ return Promise.resolve();
2497
+ }
2498
+
2499
+ createBuilding(type) {
2500
+ const geometry = new THREE.BoxGeometry(type.width, type.height, type.depth);
2501
+ const material = new THREE.MeshPhongMaterial({
2502
+ color: type.color,
2503
+ emissive: 0x222222,
2504
+ specular: 0x111111,
2505
+ shininess: 30
2506
+ });
2507
+ const building = new THREE.Mesh(geometry, material);
2508
+ building.castShadow = true;
2509
+ building.receiveShadow = true;
2510
+ return building;
2511
+ }
2512
+ checkBuildingCollision(position, type) {
2513
+ const margin = 5;
2514
+ const bbox = new THREE.Box3(
2515
+ new THREE.Vector3(
2516
+ position.x - (type.width / 2 + margin),
2517
+ 0,
2518
+ position.z - (type.depth / 2 + margin)
2519
+ ),
2520
+ new THREE.Vector3(
2521
+ position.x + (type.width / 2 + margin),
2522
+ type.height,
2523
+ position.z + (type.depth / 2 + margin)
2524
+ )
2525
+ );
2526
+
2527
+ return this.buildings.some(building => {
2528
+ const buildingBox = new THREE.Box3().setFromObject(building);
2529
+ return bbox.intersectsBox(buildingBox);
2530
+ });
2531
+ }
2532
+
2533
+ handleLoadingError() {
2534
+ this.isLoading = false;
2535
+ const loadingElement = document.getElementById('loading');
2536
+ if (loadingElement) {
2537
+ loadingElement.innerHTML = `
2538
+ <div class="loading-text" style="color: red;">
2539
+ Loading failed. Please refresh the page.
2540
+ </div>
2541
+ `;
2542
+ }
2543
+ }
2544
+
2545
+ startGameTimer() {
2546
+ if (this.gameTimer) {
2547
+ clearInterval(this.gameTimer);
2548
+ }
2549
+
2550
+ this.gameTimer = setInterval(() => {
2551
+ if (this.isLoading || this.isGameOver) {
2552
+ clearInterval(this.gameTimer);
2553
+ return;
2554
+ }
2555
+
2556
+ this.gameTime--;
2557
+ document.getElementById('time').textContent = `Time: ${this.gameTime}s`;
2558
+
2559
+ if (this.gameTime <= 0) {
2560
+ clearInterval(this.gameTimer);
2561
+ this.endGame();
2562
+ }
2563
+ }, 1000);
2564
+ }
2565
+
2566
+ spawnEnemies() {
2567
+ const spawnEnemy = () => {
2568
+ if (this.enemies.length < 3 && !this.isGameOver) { // μ΅œλŒ€ 3λŒ€λ‘œ μ œν•œ
2569
+ const position = this.getValidEnemySpawnPosition();
2570
+ if (position) {
2571
+ const type = Math.random() < 0.7 ? 'tank' : 'heavy';
2572
+ const enemy = new Enemy(this.scene, position, type);
2573
+ enemy.initialize(this.loader);
2574
+ this.enemies.push(enemy);
2575
+ }
2576
+ }
2577
+ if (!this.isGameOver) {
2578
+ setTimeout(spawnEnemy, 10000); // 10μ΄ˆλ§ˆλ‹€ 슀폰
2579
+ }
2580
+ };
2581
+
2582
+ spawnEnemy();
2583
+ }
2584
+
2585
+ getValidEnemySpawnPosition() {
2586
+ const margin = 50;
2587
+ let position;
2588
+ let attempts = 0;
2589
+ const maxAttempts = 50;
2590
+ const maxSlope = 0.3;
2591
+
2592
+ do {
2593
+ position = new THREE.Vector3(
2594
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2),
2595
+ 0,
2596
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2)
2597
+ );
2598
+
2599
+ const height = this.getHeightAtPosition(position.x, position.z);
2600
+ position.y = height + TANK_HEIGHT;
2601
+
2602
+ // μ£Όλ³€ 경사 체크
2603
+ const checkPoints = [
2604
+ { x: position.x + 2, z: position.z },
2605
+ { x: position.x - 2, z: position.z },
2606
+ { x: position.x, z: position.z + 2 },
2607
+ { x: position.x, z: position.z - 2 }
2608
+ ];
2609
+
2610
+ const slopes = checkPoints.map(point => {
2611
+ const pointHeight = this.getHeightAtPosition(point.x, point.z);
2612
+ return Math.abs(pointHeight - height) / 2;
2613
+ });
2614
+
2615
+ const maxCurrentSlope = Math.max(...slopes);
2616
+
2617
+ // ν”Œλ ˆμ΄μ–΄μ™€μ˜ 거리 체크
2618
+ const distanceToPlayer = position.distanceTo(this.tank.getPosition());
2619
+ if (distanceToPlayer > 100 && maxCurrentSlope <= maxSlope) {
2620
+ return position;
2621
+ }
2622
+
2623
+ attempts++;
2624
+ } while (attempts < maxAttempts);
2625
+
2626
+ return null;
2627
+ }
2628
+ updateParticles() {
2629
+ for (let i = this.particles.length - 1; i >= 0; i--) {
2630
+ const particle = this.particles[i];
2631
+
2632
+ // 쀑λ ₯ 적용
2633
+ particle.velocity.y += particle.gravity;
2634
+ particle.mesh.position.add(particle.velocity);
2635
+
2636
+ // 투λͺ…도 κ°μ†Œ
2637
+ particle.mesh.material.opacity -= particle.fadeRate;
2638
+ particle.life--;
2639
+
2640
+ // νŒŒν‹°ν΄ 제거
2641
+ if (particle.life <= 0 || particle.mesh.material.opacity <= 0) {
2642
+ this.scene.remove(particle.mesh);
2643
+ this.particles.splice(i, 1);
2644
+ }
2645
+ }
2646
+ }
2647
+
2648
+ createExplosion(position) {
2649
+ for (let i = 0; i < PARTICLE_COUNT; i++) {
2650
+ this.particles.push(new Particle(this.scene, position));
2651
+ }
2652
+ }
2653
+
2654
+ checkCollisions() {
2655
+ if (this.isLoading || !this.tank.isLoaded) return;
2656
+
2657
+ // λͺ…쀑 μ‚¬μš΄λ“œ λ°°μ—΄ μ •μ˜
2658
+ const hitSounds = [
2659
+ 'sounds/hit1.ogg', 'sounds/hit2.ogg', 'sounds/hit3.ogg',
2660
+ 'sounds/hit4.ogg', 'sounds/hit5.ogg', 'sounds/hit6.ogg', 'sounds/hit7.ogg'
2661
+ ];
2662
+
2663
+ // 피격 μ‚¬μš΄λ“œ λ°°μ—΄ μ •μ˜
2664
+ const beatSounds = ['sounds/beat1.ogg', 'sounds/beat2.ogg', 'sounds/beat3.ogg'];
2665
+
2666
+ const tankPosition = this.tank.getPosition();
2667
+ const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body);
2668
+
2669
+ // 탱크와 μž₯μ• λ¬Ό 좩돌 체크 (κ°œμ„ )
2670
+ this.obstacles.forEach(obstacle => {
2671
+ if (obstacle.userData.isCollidable) { // 좩돌 κ°€λŠ₯ν•œ 객체만 검사
2672
+ const obstacleBoundingBox = new THREE.Box3().setFromObject(obstacle);
2673
+ if (tankBoundingBox.intersectsBox(obstacleBoundingBox)) {
2674
+ this.tank.body.position.copy(this.previousTankPosition);
2675
+ }
2676
+ }
2677
+ });
2678
+ // 적 탱크와 μž₯μ• λ¬Ό 좩돌 체크 (μΆ”κ°€)
2679
+ this.enemies.forEach(enemy => {
2680
+ if (!enemy.mesh || !enemy.isLoaded) return;
2681
+
2682
+ const enemyBoundingBox = new THREE.Box3().setFromObject(enemy.mesh);
2683
+ const enemyPreviousPosition = enemy.mesh.position.clone();
2684
+
2685
+ this.obstacles.forEach(obstacle => {
2686
+ const obstacleBoundingBox = new THREE.Box3().setFromObject(obstacle);
2687
+ if (enemyBoundingBox.intersectsBox(obstacleBoundingBox)) {
2688
+ enemy.mesh.position.copy(enemyPreviousPosition);
2689
+ }
2690
+ });
2691
+ });
2692
+
2693
+ // 적 μ΄μ•Œκ³Ό ν”Œλ ˆμ΄μ–΄ 탱크 좩돌 체크
2694
+ this.enemies.forEach(enemy => {
2695
+ if (!enemy.mesh || !enemy.isLoaded) return;
2696
+
2697
+ enemy.bullets.forEach((bullet, bulletIndex) => {
2698
+ // ν”Œλ ˆμ΄μ–΄ νƒ±ν¬μ˜ λ°”μš΄λ”© λ°•μŠ€ 생성
2699
+ const tankBox = new THREE.Box3().setFromObject(this.tank.body);
2700
+ // λ°”μš΄λ”© λ°•μŠ€ 크기 μ‘°μ • (1.5배둜 μˆ˜μ •)
2701
+ tankBox.min.x -= 0.75;
2702
+ tankBox.max.x += 0.75;
2703
+
2704
+ // μ΄μ•Œμ˜ λ°”μš΄λ”© λ°•μŠ€ 생성
2705
+ const bulletBox = new THREE.Box3().setFromObject(bullet);
2706
+
2707
+ if (bulletBox.intersectsBox(tankBox)) {
2708
+ const randomBeatSound = beatSounds[Math.floor(Math.random() * beatSounds.length)];
2709
+ const beatAudio = new Audio(randomBeatSound);
2710
+ beatAudio.play();
2711
+
2712
+ if (this.tank.takeDamage(250)) {
2713
+ this.endGame();
2714
+ }
2715
+
2716
+ this.tank.createExplosionEffect(this.scene, bullet.position);
2717
+ this.scene.remove(bullet);
2718
+ enemy.bullets.splice(bulletIndex, 1);
2719
+
2720
+ document.getElementById('health').style.width =
2721
+ `${(this.tank.health / MAX_HEALTH) * 100}%`;
2722
+ }
2723
+ });
2724
+ });
2725
+ // ν”Œλ ˆμ΄μ–΄ 포탄
2726
+ // 포탄과 μž₯μ• λ¬Ό 좩돌 체크
2727
+ for (let i = this.tank.bullets.length - 1; i >= 0; i--) {
2728
+ const bullet = this.tank.bullets[i];
2729
+ const bulletBox = new THREE.Box3().setFromObject(bullet);
2730
+
2731
+ for (const obstacle of this.obstacles) {
2732
+ if (obstacle.userData.isCollidable) { // 좩돌 κ°€λŠ₯ν•œ 객체만 검사
2733
+ const obstacleBox = new THREE.Box3().setFromObject(obstacle);
2734
+ if (bulletBox.intersectsBox(obstacleBox)) {
2735
+ // 폭발 μ΄νŽ™νŠΈ 생성
2736
+ this.tank.createExplosionEffect(this.scene, bullet.position);
2737
+
2738
+ // 포탄 제거
2739
+ this.scene.remove(bullet);
2740
+ this.tank.bullets.splice(i, 1);
2741
+ break;
2742
+ }
2743
+ }
2744
+ }
2745
+ }
2746
+
2747
+ // 적 탱크와 μž₯μ• λ¬Ό 좩돌 체크
2748
+ this.enemies.forEach(enemy => {
2749
+ if (!enemy.mesh || !enemy.isLoaded) return;
2750
+
2751
+ enemy.bullets.forEach((bullet, bulletIndex) => {
2752
+ const distance = bullet.position.distanceTo(tankPosition);
2753
+ if (distance < 1) {
2754
+ // 피격 μ‚¬μš΄λ“œ μž¬μƒ
2755
+ const randomBeatSound = beatSounds[Math.floor(Math.random() * beatSounds.length)];
2756
+ const beatAudio = new Audio(randomBeatSound);
2757
+ beatAudio.play();
2758
+
2759
+ if (this.tank.takeDamage(250)) {
2760
+ this.endGame();
2761
+ }
2762
+
2763
+ // 기쑴의 createExplosion λŒ€μ‹  createExplosionEffect μ‚¬μš©
2764
+ this.tank.createExplosionEffect(this.scene, bullet.position);
2765
+
2766
+ this.scene.remove(bullet);
2767
+ enemy.bullets.splice(bulletIndex, 1);
2768
+
2769
+ document.getElementById('health').style.width =
2770
+ `${(this.tank.health / MAX_HEALTH) * 100}%`;
2771
+ }
2772
+ });
2773
+ });
2774
+
2775
+
2776
+ // ν”Œλ ˆμ΄μ–΄ μ΄μ•Œκ³Ό 적 좩돌 체크
2777
+ for (let i = this.tank.bullets.length - 1; i >= 0; i--) {
2778
+ const bullet = this.tank.bullets[i];
2779
+ for (let j = this.enemies.length - 1; j >= 0; j--) {
2780
+ const enemy = this.enemies[j];
2781
+ if (!enemy.mesh || !enemy.isLoaded) continue;
2782
+
2783
+ // 적 μ „μ°¨μ˜ λ°”μš΄λ”© λ°•μŠ€ 생성
2784
+ const enemyBox = new THREE.Box3().setFromObject(enemy.mesh);
2785
+ // λ°”μš΄λ”© λ°•μŠ€ 크기 μ‘°μ • (폭을 2배둜)
2786
+ enemyBox.min.x -= 0.75; // 2μ—μ„œ 1.5둜 μˆ˜μ •
2787
+ enemyBox.max.x += 0.75;
2788
+
2789
+ // μ΄μ•Œμ˜ λ°”μš΄λ”© λ°•μŠ€ 생성
2790
+ const bulletBox = new THREE.Box3().setFromObject(bullet);
2791
+
2792
+ // λ°•μŠ€ 좩돌 검사
2793
+ if (bulletBox.intersectsBox(enemyBox)) {
2794
+ const randomHitSound = hitSounds[Math.floor(Math.random() * hitSounds.length)];
2795
+ const hitAudio = new Audio(randomHitSound);
2796
+ hitAudio.play();
2797
+
2798
+ if (enemy.takeDamage(50)) {
2799
+ enemy.destroy();
2800
+ this.enemies.splice(j, 1);
2801
+ this.score += 100;
2802
+ document.getElementById('score').textContent = `Score: ${this.score}`;
2803
+ }
2804
+
2805
+ this.tank.createExplosionEffect(this.scene, bullet.position);
2806
+ this.scene.remove(bullet);
2807
+ this.tank.bullets.splice(i, 1);
2808
+ break;
2809
+ }
2810
+ }
2811
+ }
2812
+
2813
+ // ν”Œλ ˆμ΄μ–΄ 탱크와 적 μ „μ°¨ 좩돌 체크
2814
+ this.enemies.forEach(enemy => {
2815
+ if (!enemy.mesh || !enemy.isLoaded) return;
2816
+
2817
+ const enemyBoundingBox = new THREE.Box3().setFromObject(enemy.mesh);
2818
+ if (tankBoundingBox.intersectsBox(enemyBoundingBox)) {
2819
+ this.tank.body.position.copy(this.previousTankPosition);
2820
+ }
2821
+ });
2822
+
2823
+ // 이전 μœ„μΉ˜ μ €μž₯
2824
+ this.previousTankPosition.copy(this.tank.body.position);
2825
+ }
2826
+ endGame() {
2827
+ if (this.isGameOver) return;
2828
+
2829
+ this.isGameOver = true;
2830
+ // BGM μ •μ§€
2831
+ if (this.bgm) {
2832
+ this.bgm.pause();
2833
+ this.bgm = null;
2834
+ this.bgmPlaying = false;
2835
+ }
2836
+
2837
+ // 사망 μ‚¬μš΄λ“œ μž¬μƒ
2838
+ const deathSounds = ['sounds/death1.ogg', 'sounds/death2.ogg'];
2839
+ const randomDeathSound = deathSounds[Math.floor(Math.random() * deathSounds.length)];
2840
+ const deathAudio = new Audio(randomDeathSound);
2841
+ deathAudio.play();
2842
+
2843
+ if (this.gameTimer) {
2844
+ clearInterval(this.gameTimer);
2845
+ }
2846
+
2847
+ if (this.animationFrameId) {
2848
+ cancelAnimationFrame(this.animationFrameId);
2849
+ }
2850
+
2851
+ document.exitPointerLock();
2852
+
2853
+ const gameOverDiv = document.createElement('div');
2854
+ gameOverDiv.style.position = 'absolute';
2855
+ gameOverDiv.style.top = '50%';
2856
+ gameOverDiv.style.left = '50%';
2857
+ gameOverDiv.style.transform = 'translate(-50%, -50%)';
2858
+ gameOverDiv.style.color = '#0f0';
2859
+ gameOverDiv.style.fontSize = '48px';
2860
+ gameOverDiv.style.backgroundColor = 'rgba(0, 20, 0, 0.7)';
2861
+ gameOverDiv.style.padding = '20px';
2862
+ gameOverDiv.style.borderRadius = '10px';
2863
+ gameOverDiv.style.textAlign = 'center';
2864
+ gameOverDiv.innerHTML = `
2865
+ Game Over<br>
2866
+ Score: ${this.score}<br>
2867
+ Time Survived: ${GAME_DURATION - this.gameTime}s<br>
2868
+ <button onclick="location.reload()"
2869
+ style="font-size: 24px; padding: 10px; margin-top: 20px;
2870
+ cursor: pointer; background: #0f0; border: none;
2871
+ color: black; border-radius: 5px;">
2872
+ Play Again
2873
+ </button>
2874
+ `;
2875
+ document.body.appendChild(gameOverDiv);
2876
+ }
2877
+
2878
+ updateUI() {
2879
+ if (!this.isGameOver) {
2880
+ const healthBar = document.getElementById('health');
2881
+ if (healthBar) {
2882
+ healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`;
2883
+ }
2884
+
2885
+ const timeElement = document.getElementById('time');
2886
+ if (timeElement) {
2887
+ timeElement.textContent = `Time: ${this.gameTime}s`;
2888
+ }
2889
+
2890
+ const scoreElement = document.getElementById('score');
2891
+ if (scoreElement) {
2892
+ scoreElement.textContent = `Score: ${this.score}`;
2893
+ }
2894
+ }
2895
+ }
2896
+ // ν¬λ‘œμŠ€ν—€μ–΄ μ—…λ°μ΄νŠΈ λ©”μ„œλ“œ μΆ”κ°€
2897
+ updateCrosshair() {
2898
+ // ν™”λ©΄ μ€‘μ•™μ—μ„œ μ•½κ°„μ˜ μ—¬μœ λ₯Ό 두고 λ ˆμ΄μΊμŠ€νŒ…
2899
+ const raycasterDirection = new THREE.Vector2();
2900
+ this.raycaster.setFromCamera(raycasterDirection, this.camera);
2901
+
2902
+ // 적 μ „μ°¨μ˜ λ°”μš΄λ”© λ°•μŠ€λ„ ν¬ν•¨ν•˜μ—¬ 검사
2903
+ const detectEnemy = this.enemies.some(enemy => {
2904
+ if (!enemy.mesh || !enemy.isLoaded) return false;
2905
+
2906
+ // 적 μ „μ°¨μ˜ λ°”μš΄λ”© λ°•μŠ€ 생성
2907
+ const boundingBox = new THREE.Box3().setFromObject(enemy.mesh);
2908
+ const intersects = this.raycaster.ray.intersectsBox(boundingBox);
2909
+
2910
+ // λ°”μš΄λ”© λ°•μŠ€μ™€μ˜ ꡐ차 μ—¬λΆ€λ‘œ νŒλ‹¨
2911
+ return intersects;
2912
+ });
2913
+
2914
+ if (detectEnemy) {
2915
+ this.crosshair.classList.add('target-detected');
2916
+ } else {
2917
+ this.crosshair.classList.remove('target-detected');
2918
+ }
2919
+ }
2920
+ updateEnemyLabels() {
2921
+ const labelsContainer = document.getElementById('enemyLabels');
2922
+
2923
+ // κΈ°μ‘΄ 라벨 λͺ¨λ‘ 제거
2924
+ labelsContainer.innerHTML = '';
2925
+
2926
+ this.enemies.forEach((enemy, index) => {
2927
+ if (!enemy.mesh || !enemy.isLoaded) return;
2928
+
2929
+ // 적 μœ„μΉ˜λ₯Ό ν™”λ©΄ μ’Œν‘œλ‘œ λ³€ν™˜
2930
+ const enemyPosition = enemy.mesh.position.clone();
2931
+ enemyPosition.y += 5; // 적 머리 μœ„μ— ν‘œμ‹œν•˜κΈ° μœ„ν•΄ 높이 μ‘°μ •
2932
+
2933
+ const screenPosition = enemyPosition.project(this.camera);
2934
+
2935
+ // 화면에 λ³΄μ΄λŠ”μ§€ 확인
2936
+ if (screenPosition.z > 1) return; // 카메라 뒀에 μžˆλŠ” 경우
2937
+
2938
+ // 적과 ν”Œλ ˆμ΄μ–΄ μ‚¬μ΄μ˜ 거리 계산
2939
+ const distance = enemy.mesh.position.distanceTo(this.tank.getPosition());
2940
+
2941
+ // λ ˆμ΄λ” λ²”μœ„ 내에 μžˆλŠ” κ²½μš°μ—λ§Œ ν‘œμ‹œ
2942
+ if (distance <= this.radarRange) {
2943
+ const x = (screenPosition.x + 1) / 2 * window.innerWidth;
2944
+ const y = (-screenPosition.y + 1) / 2 * window.innerHeight;
2945
+
2946
+ // 라벨 생성
2947
+ const label = document.createElement('div');
2948
+ label.className = 'enemy-label';
2949
+ label.textContent = 'T-90';
2950
+ label.style.left = `${x}px`;
2951
+ label.style.top = `${y}px`;
2952
+
2953
+ // 거리에 λ”°λ₯Έ 투λͺ…도 μ‘°μ •
2954
+ const opacity = Math.max(0.2, 1 - (distance / this.radarRange));
2955
+ label.style.opacity = opacity;
2956
+
2957
+ labelsContainer.appendChild(label);
2958
+ }
2959
+ });
2960
+ }
2961
+
2962
+ animate() {
2963
+ if (this.isGameOver) {
2964
+ if (this.animationFrameId) {
2965
+ cancelAnimationFrame(this.animationFrameId);
2966
+ }
2967
+ return;
2968
+ }
2969
+
2970
+ this.animationFrameId = requestAnimationFrame(() => this.animate());
2971
+ // κ²Œμž„μ΄ μ‹œμž‘λ˜μ§€ μ•Šμ•˜μœΌλ©΄ λ Œλ”λ§λ§Œ μˆ˜ν–‰
2972
+ if (!this.isStarted) {
2973
+ this.renderer.render(this.scene, this.camera);
2974
+ return;
2975
+ }
2976
+
2977
+ const currentTime = performance.now();
2978
+ const deltaTime = (currentTime - this.lastTime) / 1000;
2979
+ this.lastTime = currentTime;
2980
+
2981
+ if (!this.isLoading) {
2982
+ this.handleMovement();
2983
+ this.tank.update(this.mouse.x, this.mouse.y, this.scene);
2984
+
2985
+ const tankPosition = this.tank.getPosition();
2986
+ this.enemies.forEach(enemy => {
2987
+ enemy.update(tankPosition);
2988
+
2989
+ if (enemy.isLoaded && enemy.mesh.position.distanceTo(tankPosition) < ENEMY_CONFIG.ATTACK_RANGE) {
2990
+ enemy.shoot(tankPosition);
2991
+ }
2992
+ });
2993
+ this.updateEnemyLabels(); // 적 라벨 μ—…λ°μ΄νŠΈ μΆ”κ°€
2994
+ this.updateParticles();
2995
+ this.checkCollisions();
2996
+ this.updateUI();
2997
+ this.updateRadar(); // λ ˆμ΄λ” μ—…λ°μ΄νŠΈ μΆ”κ°€
2998
+ // ν¬λ‘œμŠ€ν—€μ–΄ μ—…λ°μ΄νŠΈ μΆ”κ°€
2999
+ this.updateCrosshair();
3000
+ }
3001
+
3002
+ this.renderer.render(this.scene, this.camera);
3003
+ }
3004
+ }
3005
+
3006
+ // Start game
3007
+ window.startGame = function() {
3008
+ document.getElementById('startScreen').style.display = 'none';
3009
+ document.body.requestPointerLock();
3010
+
3011
+ if (!window.gameInstance) {
3012
+ window.gameInstance = new Game();
3013
+ }
3014
+
3015
+ // κ²Œμž„ μ‹œμž‘ μ„€μ •
3016
+ window.gameInstance.isStarted = true;
3017
+ window.gameInstance.initialize();
3018
+ };
3019
+
3020
+ // Initialize game
3021
+ document.addEventListener('DOMContentLoaded', () => {
3022
+ // κ²Œμž„ μΈμŠ€ν„΄μŠ€λ§Œ μƒμ„±ν•˜κ³  μ΄ˆκΈ°ν™”λŠ” ν•˜μ§€ μ•ŠμŒ
3023
+ window.gameInstance = new Game();
3024
+
3025
+ // 기본적인 씬 μ„€μ •λ§Œ μˆ˜ν–‰
3026
+ window.gameInstance.setupScene();
3027
+ window.gameInstance.animate(); // λ Œλ”λ§ μ‹œμž‘
3028
+
3029
+ // μ‹œμž‘ ν™”λ©΄ ν‘œμ‹œ
3030
+ document.getElementById('startScreen').style.display = 'block';
3031
+ });
3032
 
3033
  // μž₯μ• λ¬Ό 감지 μ‹œμŠ€ν…œ
3034
  detectObstacles() {