import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // 게임 상수 const GAME_CONSTANTS = { MISSION_DURATION: 180, MAP_SIZE: 15000, MAX_ALTITUDE: 12000, MIN_ALTITUDE: 100, MAX_SPEED: 800, STALL_SPEED: 120, GRAVITY: 9.8, MOUSE_SENSITIVITY: 0.001, MAX_G_FORCE: 12.0, ENEMY_COUNT: 4, MISSILE_COUNT: 6, AMMO_COUNT: 300 }; // 전투기 클래스 class Fighter { constructor() { this.mesh = null; this.isLoaded = false; // 물리 속성 this.position = new THREE.Vector3(0, 2000, 0); this.velocity = new THREE.Vector3(0, 0, 150); this.acceleration = new THREE.Vector3(0, 0, 0); this.rotation = new THREE.Euler(0, 0, 0); // 비행 제어 this.baseSpeed = 150; this.throttle = 0.5; this.speed = 150; this.altitude = 2000; this.gForce = 1.0; this.health = 100; // 조종 입력 시스템 this.pitchInput = 0; this.rollInput = 0; this.yawInput = 0; // 마우스 누적 입력 this.mousePitch = 0; this.mouseRoll = 0; // 부드러운 회전을 위한 목표값 this.targetPitch = 0; this.targetRoll = 0; this.targetYaw = 0; // 무기 this.missiles = GAME_CONSTANTS.MISSILE_COUNT; this.ammo = GAME_CONSTANTS.AMMO_COUNT; this.bullets = []; this.lastShootTime = 0; // 카메라 설정 this.cameraDistance = 260; this.cameraHeight = 45; this.cameraLag = 0.1; } async initialize(scene, loader) { try { const result = await loader.loadAsync('models/f-15.glb'); this.mesh = result.scene; this.mesh.position.copy(this.position); this.mesh.scale.set(2, 2, 2); // 모델 방향 조정 (45도 회전) this.mesh.rotation.y = Math.PI / 4; // 45도 회전 // 그림자 설정 this.mesh.traverse((child) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }); scene.add(this.mesh); this.isLoaded = true; console.log('F-15 전투기 로딩 완료'); } catch (error) { console.error('F-15 모델 로딩 실패:', error); this.createFallbackModel(scene); } } createFallbackModel(scene) { const group = new THREE.Group(); // 동체 - Z축 방향으로 생성하여 앞을 향하게 함 const fuselageGeometry = new THREE.CylinderGeometry(0.8, 1.5, 12, 8); const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x606060 }); const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial); fuselage.rotation.x = Math.PI / 2; // X축으로 90도 회전하여 머리가 앞을 향하게 함 group.add(fuselage); // 주익 const wingGeometry = new THREE.BoxGeometry(16, 0.3, 4); const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 }); const wings = new THREE.Mesh(wingGeometry, wingMaterial); wings.position.z = -1; group.add(wings); // 수직 안정판 const tailGeometry = new THREE.BoxGeometry(0.3, 4, 3); const tailMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 }); const tail = new THREE.Mesh(tailGeometry, tailMaterial); tail.position.z = -5; tail.position.y = 1.5; group.add(tail); // 수평 안정판 const horizontalTailGeometry = new THREE.BoxGeometry(6, 0.2, 2); const horizontalTail = new THREE.Mesh(horizontalTailGeometry, tailMaterial); horizontalTail.position.z = -5; horizontalTail.position.y = 0.5; group.add(horizontalTail); this.mesh = group; this.mesh.position.copy(this.position); this.mesh.scale.set(2, 2, 2); scene.add(this.mesh); this.isLoaded = true; console.log('Fallback 전투기 모델 생성 완료'); } updateMouseInput(deltaX, deltaY) { this.mouseRoll += deltaX * GAME_CONSTANTS.MOUSE_SENSITIVITY; this.mousePitch += deltaY * GAME_CONSTANTS.MOUSE_SENSITIVITY; this.mouseRoll = Math.max(-1, Math.min(1, this.mouseRoll)); this.mousePitch = Math.max(-1, Math.min(1, this.mousePitch)); } updateControls(keys, deltaTime) { if (keys.w) { this.throttle = Math.min(1.0, this.throttle + deltaTime * 0.8); } if (keys.s) { this.throttle = Math.max(0.1, this.throttle - deltaTime * 0.8); } this.yawInput = 0; if (keys.a) this.yawInput = -0.5; if (keys.d) this.yawInput = 0.5; this.pitchInput = this.mousePitch; this.rollInput = this.mouseRoll; const maxPitchAngle = Math.PI / 6; const maxRollAngle = Math.PI / 4; this.targetPitch = this.pitchInput * maxPitchAngle; this.targetRoll = this.rollInput * maxRollAngle; this.targetYaw += this.yawInput * deltaTime * 1.5; this.mouseRoll *= 0.95; this.mousePitch *= 0.95; } updatePhysics(deltaTime) { if (!this.mesh) return; const rotationSpeed = deltaTime * 3.0; this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetPitch, rotationSpeed); this.rotation.y = THREE.MathUtils.lerp(this.rotation.y, this.targetYaw, rotationSpeed); this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRoll, rotationSpeed); const minSpeed = 100; const maxSpeed = 300; const targetSpeed = minSpeed + (maxSpeed - minSpeed) * this.throttle; this.speed = THREE.MathUtils.lerp(this.speed, targetSpeed, deltaTime * 2); const forward = new THREE.Vector3(0, 0, 1); forward.applyEuler(this.rotation); this.velocity = forward.multiplyScalar(this.speed); this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); if (this.position.y < GAME_CONSTANTS.MIN_ALTITUDE) { this.position.y = GAME_CONSTANTS.MIN_ALTITUDE; if (this.velocity.y < 0) this.velocity.y = 0; } if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) { this.position.y = GAME_CONSTANTS.MAX_ALTITUDE; if (this.velocity.y > 0) this.velocity.y = 0; } const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2; if (this.position.x > mapLimit) this.position.x = -mapLimit; if (this.position.x < -mapLimit) this.position.x = mapLimit; if (this.position.z > mapLimit) this.position.z = -mapLimit; if (this.position.z < -mapLimit) this.position.z = mapLimit; // 메시 업데이트 시 물리 회전과 모델 회전을 조합 this.mesh.position.copy(this.position); // 모델의 기본 회전(270도)에 물리 회전을 더함 this.mesh.rotation.x = this.rotation.x; this.mesh.rotation.y = this.rotation.y + 3 * Math.PI / 2; // 기본 270도 회전 유지 this.mesh.rotation.z = this.rotation.z; this.altitude = this.position.y; const rotationIntensity = Math.abs(this.targetPitch) + Math.abs(this.targetRoll); this.gForce = 1.0 + rotationIntensity * 3; } shoot(scene) { const currentTime = Date.now(); if (currentTime - this.lastShootTime < 100 || this.ammo <= 0) return; this.lastShootTime = currentTime; this.ammo--; const bulletGeometry = new THREE.SphereGeometry(0.2); const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.7 }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); // 총구 위치 계산 (전투기 앞쪽) const muzzleOffset = new THREE.Vector3(0, 0, 8); muzzleOffset.applyEuler(this.rotation); bullet.position.copy(this.position).add(muzzleOffset); const bulletSpeed = 1000; const direction = new THREE.Vector3(0, 0, 1); direction.applyEuler(this.rotation); bullet.velocity = direction.multiplyScalar(bulletSpeed).add(this.velocity); scene.add(bullet); this.bullets.push(bullet); // 발사음 (에러 방지) try { const audio = new Audio('sounds/gunfire.ogg'); if (audio) { audio.volume = 0.3; audio.play().catch(e => console.log('Gunfire sound failed to play')); } } catch (e) { // 조용히 무시 } } updateBullets(scene, deltaTime) { for (let i = this.bullets.length - 1; i >= 0; i--) { const bullet = this.bullets[i]; bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime)); if (bullet.position.distanceTo(this.position) > 8000 || bullet.position.y < 0 || bullet.position.y > GAME_CONSTANTS.MAX_ALTITUDE + 500) { scene.remove(bullet); this.bullets.splice(i, 1); } } } takeDamage(damage) { this.health -= damage; return this.health <= 0; } // 수정된 카메라 위치 계산 (워썬더 스타일) getCameraPosition() { // 전투기의 현재 회전을 고려한 뒤쪽 방향 계산 const backward = new THREE.Vector3(0, 0, -1); backward.applyEuler(this.rotation); // 월드 좌표계의 위쪽 방향 (전투기 회전에 영향받지 않음) const worldUp = new THREE.Vector3(0, 1, 0); // 카메라 위치 = 전투기 위치 + (뒤쪽 방향 * 거리) + (월드 위쪽 * 높이) const cameraPos = this.position.clone() .add(backward.multiplyScalar(this.cameraDistance)) .add(worldUp.multiplyScalar(this.cameraHeight)); return cameraPos; } getCameraTarget() { // 전투기가 향하는 방향의 앞쪽을 바라보도록 설정 const forward = new THREE.Vector3(0, 0, 1); forward.applyEuler(this.rotation); // 전투기 위치에서 앞쪽 방향으로 일정 거리만큼 떨어진 지점을 타겟으로 설정 return this.position.clone().add(forward.multiplyScalar(100)); } } // 적 전투기 클래스 class EnemyFighter { constructor(scene, position) { this.mesh = null; this.isLoaded = false; this.scene = scene; this.position = position.clone(); this.velocity = new THREE.Vector3(0, 0, 120); this.rotation = new THREE.Euler(0, 0, 0); this.health = 100; this.speed = 120; this.bullets = []; this.lastShootTime = 0; this.aiState = 'patrol'; this.targetPosition = position.clone(); this.patrolCenter = position.clone(); this.patrolRadius = 2000; this.lastStateChange = 0; } async initialize(loader) { try { const result = await loader.loadAsync('models/mig-29.glb'); this.mesh = result.scene; this.mesh.position.copy(this.position); this.mesh.scale.set(1.5, 1.5, 1.5); // 적 전투기도 270도 회전 this.mesh.rotation.y = 3 * Math.PI / 2; this.mesh.traverse((child) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }); this.scene.add(this.mesh); this.isLoaded = true; console.log('MiG-29 적기 로딩 완료'); } catch (error) { console.error('MiG-29 모델 로딩 실패:', error); this.createFallbackModel(); } } createFallbackModel() { const group = new THREE.Group(); const fuselageGeometry = new THREE.CylinderGeometry(0.6, 1.0, 8, 8); const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x800000 }); const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial); fuselage.rotation.x = -Math.PI / 2; // 동일한 방향 조정 group.add(fuselage); const wingGeometry = new THREE.BoxGeometry(12, 0.3, 3); const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x600000 }); const wings = new THREE.Mesh(wingGeometry, wingMaterial); wings.position.z = -0.5; group.add(wings); this.mesh = group; this.mesh.position.copy(this.position); this.mesh.scale.set(1.5, 1.5, 1.5); this.scene.add(this.mesh); this.isLoaded = true; console.log('Fallback 적기 모델 생성 완료'); } update(playerPosition, deltaTime) { if (!this.mesh || !this.isLoaded) return; const currentTime = Date.now(); const distanceToPlayer = this.position.distanceTo(playerPosition); if (distanceToPlayer < 5000) { const direction = new THREE.Vector3() .subVectors(playerPosition, this.position) .normalize(); this.velocity = direction.multiplyScalar(this.speed); this.rotation.y = Math.atan2(direction.x, direction.z); if (distanceToPlayer < 2000 && currentTime - this.lastShootTime > 1500) { this.shoot(); } } else { if (this.position.distanceTo(this.targetPosition) < 300) { const angle = Math.random() * Math.PI * 2; this.targetPosition = this.patrolCenter.clone().add( new THREE.Vector3( Math.cos(angle) * this.patrolRadius, (Math.random() - 0.5) * 1000, Math.sin(angle) * this.patrolRadius ) ); } const direction = new THREE.Vector3() .subVectors(this.targetPosition, this.position) .normalize(); this.velocity = direction.multiplyScalar(this.speed * 0.7); this.rotation.y = Math.atan2(direction.x, direction.z); } this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); if (this.position.y < GAME_CONSTANTS.MIN_ALTITUDE) { this.position.y = GAME_CONSTANTS.MIN_ALTITUDE; } if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) { this.position.y = GAME_CONSTANTS.MAX_ALTITUDE; } const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2; if (this.position.x > mapLimit) this.position.x = -mapLimit; if (this.position.x < -mapLimit) this.position.x = mapLimit; if (this.position.z > mapLimit) this.position.z = -mapLimit; if (this.position.z < -mapLimit) this.position.z = mapLimit; // 적 전투기 메시 업데이트 시에도 방향 조정 this.mesh.position.copy(this.position); this.mesh.rotation.x = this.rotation.x; this.mesh.rotation.y = this.rotation.y + 3 * Math.PI / 2; // 기본 270도 회전 유지 this.mesh.rotation.z = this.rotation.z; this.updateBullets(deltaTime); } shoot() { this.lastShootTime = Date.now(); const bulletGeometry = new THREE.SphereGeometry(0.15); const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 0.5 }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); const muzzleOffset = new THREE.Vector3(0, 0, 6); muzzleOffset.applyEuler(this.rotation); bullet.position.copy(this.position).add(muzzleOffset); const direction = new THREE.Vector3(0, 0, 1); direction.applyEuler(this.rotation); bullet.velocity = direction.multiplyScalar(800); this.scene.add(bullet); this.bullets.push(bullet); } updateBullets(deltaTime) { for (let i = this.bullets.length - 1; i >= 0; i--) { const bullet = this.bullets[i]; bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime)); if (bullet.position.distanceTo(this.position) > 5000 || bullet.position.y < 0) { this.scene.remove(bullet); this.bullets.splice(i, 1); } } } takeDamage(damage) { this.health -= damage; return this.health <= 0; } destroy() { if (this.mesh) { this.scene.remove(this.mesh); this.bullets.forEach(bullet => this.scene.remove(bullet)); this.bullets = []; this.isLoaded = false; } } } // 메인 게임 클래스 class Game { constructor() { this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000); this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; this.renderer.setClearColor(0x87CEEB); this.renderer.setPixelRatio(window.devicePixelRatio); document.getElementById('gameContainer').appendChild(this.renderer.domElement); this.loader = new GLTFLoader(); this.fighter = new Fighter(); this.enemies = []; this.isLoaded = false; this.isBGMReady = false; this.isGameOver = false; this.gameTime = GAME_CONSTANTS.MISSION_DURATION; this.score = 0; this.lastTime = performance.now(); this.gameTimer = null; this.animationFrameId = null; this.bgm = null; this.bgmPlaying = false; this.keys = { w: false, a: false, s: false, d: false }; this.isStarted = false; this.setupScene(); this.setupEventListeners(); this.preloadGame(); } async preloadGame() { try { console.log('게임 리소스 사전 로딩 중...'); await this.fighter.initialize(this.scene, this.loader); if (!this.fighter.isLoaded) { throw new Error('전투기 로딩 실패'); } await this.preloadEnemies(); this.isLoaded = true; console.log('게임 리소스 로딩 완료'); // BGM 사전 로딩 await this.preloadBGM(); // 모든 리소스 로딩 완료 후 UI 업데이트 this.showStartScreen(); this.animate(); } catch (error) { console.error('게임 사전 로딩 실패:', error); document.getElementById('loading').innerHTML = '
로딩 실패. 페이지를 새로고침해주세요.
'; } } showStartScreen() { if (this.isLoaded && this.isBGMReady) { // 로딩 화면 숨기기 const loadingElement = document.getElementById('loading'); if (loadingElement) { loadingElement.style.display = 'none'; } // Start Game 화면 표시 const startScreen = document.getElementById('startScreen'); if (startScreen) { startScreen.style.display = 'flex'; } console.log('모든 리소스 준비 완료 - Start Game 버튼 표시'); } } async preloadBGM() { console.log('BGM 사전 로딩...'); return new Promise((resolve) => { try { this.bgm = new Audio('sounds/main.ogg'); this.bgm.volume = 0.5; this.bgm.loop = true; // BGM 로딩 완료 이벤트 this.bgm.addEventListener('canplaythrough', () => { console.log('BGM 재생 준비 완료'); this.isBGMReady = true; resolve(); }); this.bgm.addEventListener('error', (e) => { console.log('BGM 에러:', e); this.isBGMReady = true; // 에러가 나더라도 게임은 시작할 수 있게 resolve(); }); // 파일 사전 로딩 this.bgm.load(); // 타임아웃 설정 (3초 후에도 로딩이 안되면 강제로 완료) setTimeout(() => { if (!this.isBGMReady) { console.log('BGM 로딩 타임아웃 - 게임 진행'); this.isBGMReady = true; resolve(); } }, 3000); } catch (error) { console.log('BGM 사전 로딩 실패:', error); this.isBGMReady = true; // 에러가 나더라도 게임은 시작할 수 있게 resolve(); } }); } async preloadEnemies() { for (let i = 0; i < GAME_CONSTANTS.ENEMY_COUNT; i++) { const angle = (i / GAME_CONSTANTS.ENEMY_COUNT) * Math.PI * 2; const distance = 6000 + Math.random() * 3000; const position = new THREE.Vector3( Math.cos(angle) * distance, 2500 + Math.random() * 2000, Math.sin(angle) * distance ); const enemy = new EnemyFighter(this.scene, position); await enemy.initialize(this.loader); this.enemies.push(enemy); } } setupScene() { this.scene.background = new THREE.Color(0x87CEEB); this.scene.fog = new THREE.Fog(0x87CEEB, 1000, 30000); const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); this.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); directionalLight.position.set(8000, 10000, 8000); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 20000; directionalLight.shadow.camera.left = -10000; directionalLight.shadow.camera.right = 10000; directionalLight.shadow.camera.top = 10000; directionalLight.shadow.camera.bottom = -10000; this.scene.add(directionalLight); const groundGeometry = new THREE.PlaneGeometry(GAME_CONSTANTS.MAP_SIZE, GAME_CONSTANTS.MAP_SIZE); const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x8FBC8F, transparent: true, opacity: 0.8 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; this.scene.add(ground); this.addClouds(); } addClouds() { const cloudGeometry = new THREE.SphereGeometry(100, 8, 6); const cloudMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.5 }); for (let i = 0; i < 100; i++) { const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial); cloud.position.set( (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE, Math.random() * 4000 + 1000, (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE ); cloud.scale.set( Math.random() * 3 + 1, Math.random() * 2 + 0.5, Math.random() * 3 + 1 ); this.scene.add(cloud); } } setupEventListeners() { document.addEventListener('keydown', (event) => { if (this.isGameOver) return; switch(event.code) { case 'KeyW': this.keys.w = true; break; case 'KeyA': this.keys.a = true; break; case 'KeyS': this.keys.s = true; break; case 'KeyD': this.keys.d = true; break; } }); document.addEventListener('keyup', (event) => { if (this.isGameOver) return; switch(event.code) { case 'KeyW': this.keys.w = false; break; case 'KeyA': this.keys.a = false; break; case 'KeyS': this.keys.s = false; break; case 'KeyD': this.keys.d = false; break; } }); document.addEventListener('mousemove', (event) => { if (!document.pointerLockElement || this.isGameOver) return; const deltaX = event.movementX || 0; const deltaY = event.movementY || 0; this.fighter.updateMouseInput(deltaX, deltaY); }); document.addEventListener('click', () => { if (!document.pointerLockElement) { document.body.requestPointerLock(); } else if (!this.isGameOver && this.fighter.isLoaded && this.isStarted) { this.fighter.shoot(this.scene); } }); window.addEventListener('resize', () => { this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(window.innerWidth, window.innerHeight); }); } startGame() { if (!this.isLoaded) { console.log('게임이 아직 로딩 중입니다...'); return; } this.isStarted = true; this.startGameTimer(); console.log('게임 시작!'); } startBGM() { if (this.bgmPlaying || !this.bgm) return; console.log('BGM 재생 시도...'); // 즉시 재생 시도 const playPromise = this.bgm.play(); if (playPromise !== undefined) { playPromise.then(() => { this.bgmPlaying = true; console.log('BGM 재생 시작 성공!'); }).catch(error => { console.log('자동 재생이 차단됨:', error); console.log('클릭 후 재생 시도 대기 중...'); // 사용자 상호작용 후 재생 재시도 const tryPlayOnInteraction = () => { if (!this.bgmPlaying && this.bgm) { console.log('사용자 상호작용으로 BGM 재생 시도...'); this.bgm.play().then(() => { this.bgmPlaying = true; console.log('BGM 재생 시작 성공 (클릭 후)!'); // 이벤트 리스너 제거 document.removeEventListener('click', tryPlayOnInteraction); document.removeEventListener('keydown', tryPlayOnInteraction); }).catch(e => console.log('BGM 재생 실패:', e)); } }; // 클릭이나 키보드 입력 시 재생 시도 document.addEventListener('click', tryPlayOnInteraction); document.addEventListener('keydown', tryPlayOnInteraction); }); } } startGameTimer() { this.gameTimer = setInterval(() => { if (!this.isGameOver) { this.gameTime--; if (this.gameTime <= 0) { this.endGame(true); } } }, 1000); } updateUI() { if (this.fighter.isLoaded) { const speedKnots = Math.round(this.fighter.speed * 1.94384); const altitudeFeet = Math.round(this.fighter.altitude * 3.28084); // 안전한 DOM 요소 업데이트 const scoreElement = document.getElementById('score'); const timeElement = document.getElementById('time'); const healthElement = document.getElementById('health'); const ammoElement = document.getElementById('ammoDisplay'); const gameStatsElement = document.getElementById('gameStats'); if (scoreElement) scoreElement.textContent = `Score: ${this.score}`; if (timeElement) timeElement.textContent = `Time: ${this.gameTime}s`; if (healthElement) healthElement.style.width = `${this.fighter.health}%`; if (ammoElement) ammoElement.textContent = `AMMO: ${this.fighter.ammo}`; if (gameStatsElement) { gameStatsElement.innerHTML = `
Score: ${this.score}
Time: ${this.gameTime}s
Speed: ${speedKnots} KT
Alt: ${altitudeFeet} FT
Throttle: ${Math.round(this.fighter.throttle * 100)}%
G-Force: ${this.fighter.gForce.toFixed(1)}
Targets: ${this.enemies.length}
`; } } } updateRadar() { const radar = document.getElementById('radar'); if (!radar) return; const oldDots = radar.getElementsByClassName('enemy-dot'); while (oldDots[0]) { oldDots[0].remove(); } const radarCenter = { x: 100, y: 100 }; const radarRange = 10000; this.enemies.forEach(enemy => { if (!enemy.mesh || !enemy.isLoaded) return; const distance = this.fighter.position.distanceTo(enemy.position); if (distance <= radarRange) { const relativePos = enemy.position.clone().sub(this.fighter.position); const angle = Math.atan2(relativePos.x, relativePos.z); const relativeDistance = distance / radarRange; const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance); const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance); const dot = document.createElement('div'); dot.className = 'enemy-dot'; dot.style.left = `${dotX}px`; dot.style.top = `${dotY}px`; radar.appendChild(dot); } }); } checkCollisions() { for (let i = this.fighter.bullets.length - 1; i >= 0; i--) { const bullet = this.fighter.bullets[i]; for (let j = this.enemies.length - 1; j >= 0; j--) { const enemy = this.enemies[j]; if (!enemy.mesh || !enemy.isLoaded) continue; const distance = bullet.position.distanceTo(enemy.position); if (distance < 25) { this.scene.remove(bullet); this.fighter.bullets.splice(i, 1); if (enemy.takeDamage(30)) { enemy.destroy(); this.enemies.splice(j, 1); this.score += 100; } break; } } } this.enemies.forEach(enemy => { enemy.bullets.forEach((bullet, index) => { const distance = bullet.position.distanceTo(this.fighter.position); if (distance < 30) { this.scene.remove(bullet); enemy.bullets.splice(index, 1); if (this.fighter.takeDamage(20)) { this.endGame(false); } } }); }); } animate() { if (this.isGameOver) return; this.animationFrameId = requestAnimationFrame(() => this.animate()); const currentTime = performance.now(); const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.1); this.lastTime = currentTime; if (this.isLoaded && this.fighter.isLoaded) { this.fighter.updateControls(this.keys, deltaTime); this.fighter.updatePhysics(deltaTime); this.fighter.updateBullets(this.scene, deltaTime); if (this.isStarted) { this.enemies.forEach(enemy => { enemy.update(this.fighter.position, deltaTime); }); this.checkCollisions(); this.updateUI(); this.updateRadar(); if (this.enemies.length === 0) { this.endGame(true); } } // 카메라 업데이트 const targetCameraPos = this.fighter.getCameraPosition(); const targetCameraTarget = this.fighter.getCameraTarget(); this.camera.position.lerp(targetCameraPos, this.fighter.cameraLag); this.camera.lookAt(targetCameraTarget); } else { if (this.fighter.isLoaded) { const initialCameraPos = this.fighter.getCameraPosition(); const initialTarget = this.fighter.getCameraTarget(); this.camera.position.copy(initialCameraPos); this.camera.lookAt(initialTarget); } } this.renderer.render(this.scene, this.camera); } endGame(victory = false) { this.isGameOver = true; if (this.bgm) { this.bgm.pause(); this.bgm = null; this.bgmPlaying = false; } if (this.gameTimer) { clearInterval(this.gameTimer); } document.exitPointerLock(); const gameOverDiv = document.createElement('div'); gameOverDiv.className = 'start-screen'; gameOverDiv.innerHTML = `

${victory ? 'MISSION ACCOMPLISHED!' : 'SHOT DOWN!'}

Final Score: ${this.score}
Enemies Destroyed: ${GAME_CONSTANTS.ENEMY_COUNT - this.enemies.length}
Mission Time: ${GAME_CONSTANTS.MISSION_DURATION - this.gameTime}s
`; document.body.appendChild(gameOverDiv); } } // 게임 인스턴스 let gameInstance = null; // 전역 함수 window.startGame = function() { // 게임과 BGM이 모두 준비되었는지 확인 if (!gameInstance || !gameInstance.isLoaded || !gameInstance.isBGMReady) { console.log('게임이 아직 준비되지 않았습니다...'); console.log('게임 로딩:', gameInstance?.isLoaded ? '완료' : '진행중'); console.log('BGM 준비:', gameInstance?.isBGMReady ? '완료' : '진행중'); return; } document.getElementById('startScreen').style.display = 'none'; document.body.requestPointerLock(); // 즉시 BGM 재생 시도 (사용자 클릭 시점) gameInstance.startBGM(); gameInstance.startGame(); }; // 게임 초기화 document.addEventListener('DOMContentLoaded', () => { console.log('전투기 시뮬레이터 초기화 중...'); gameInstance = new Game(); });