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 = '