cutechicken's picture
Update game.js
ec8c42a verified
Raw History Blame Contribute Delete
182 kB
import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // ๊ฒŒ์ž„ ์ƒํƒœ ์ถ”์  ๋ณ€์ˆ˜ let gameCanStart = false; let gameStarted = false; // ๊ฒŒ์ž„ ์ƒ์ˆ˜ const GAME_CONSTANTS = { MISSION_DURATION: 180, MAP_SIZE: 15000, MAX_ALTITUDE: 15000, MIN_ALTITUDE: 0, MAX_SPEED: 800, STALL_SPEED: 300, // 300kt ์ดํ•˜์—์„œ ์Šคํ†จ ์œ„ํ—˜ GRAVITY: 9.8, MOUSE_SENSITIVITY: 0.001, MAX_G_FORCE: 12.0, ENEMY_COUNT: 4, MISSILE_COUNT: 6, AMMO_COUNT: 940, // 940๋ฐœ๋กœ ๋ณ€๊ฒฝ BULLET_DAMAGE: 50, // ๋ฐœ๋‹น 50 ๋ฐ๋ฏธ์ง€ MAX_HEALTH: 1000, // ์ฒด๋ ฅ 1000 AIM9_COUNT: 8, // AIM-9 ๋ฏธ์‚ฌ์ผ 8๋ฐœ AIM9_DAMAGE: 1000, // AIM-9 ๋ฏธ์‚ฌ์ผ ํ”ผํ•ด 1000! AIM9_SPEED: 514.4, // 1000kt๋ฅผ m/s๋กœ ๋ณ€ํ™˜ AIM9_LOCK_RANGE: 6000, // ๋ฝ์˜จ ๊ฑฐ๋ฆฌ 6000m AIM9_LOCK_REQUIRED: 3, // 3๋ฒˆ ๋ฝ์˜จ ํ•„์š” ENEMY_AIM9_COUNT: 4, // ์  AIM-9 ๋ฏธ์‚ฌ์ผ ์ˆ˜ ENEMY_LOCK_RANGE: 5000, // ์  ๋ฝ์˜จ ๊ฑฐ๋ฆฌ ENEMY_LOCK_ANGLE: Math.PI / 12 // ์  ๋ฝ์˜จ ๊ฐ๋„ (15๋„) }; // ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค class Fighter { constructor() { this.mesh = null; this.isLoaded = false; // ๋ฌผ๋ฆฌ ์†์„ฑ this.position = new THREE.Vector3(0, 2000, 0); this.velocity = new THREE.Vector3(0, 0, 350); // ์ดˆ๊ธฐ ์†๋„ 350kt this.acceleration = new THREE.Vector3(0, 0, 0); this.rotation = new THREE.Euler(0, 0, 0, 'YXZ'); // ํ•ญ๊ณต๊ธฐ ํ‘œ์ค€ ์˜ค์ผ๋Ÿฌ ์ˆœ์„œ // ๋น„ํ–‰ ์ œ์–ด this.throttle = 0.7; // ์ดˆ๊ธฐ ์Šค๋กœํ‹€ 70% this.speed = 350; // ์ดˆ๊ธฐ ์†๋„ 350kt this.altitude = 2000; this.gForce = 1.0; this.health = GAME_CONSTANTS.MAX_HEALTH; // ์ฒด๋ ฅ 1000 // ์กฐ์ข… ์ž…๋ ฅ ์‹œ์Šคํ…œ 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.isMouseDown = false; // ๋งˆ์šฐ์Šค ๋ˆ„๋ฆ„ ์ƒํƒœ ์ถ”์  this.gunfireAudios = []; // ๊ธฐ๊ด€์ด ์†Œ๋ฆฌ ๋ฐฐ์—ด (์ตœ๋Œ€ 5๊ฐœ) // ํ”Œ๋ ˆ์–ด ์‹œ์Šคํ…œ this.flareCount = 3; // ํ”Œ๋ ˆ์–ด 3ํšŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ this.deployedFlares = []; // ๋ฐฐ์น˜๋œ ํ”Œ๋ ˆ์–ด ๋ฐฐ์—ด this.lastFlareTime = 0; // ๋งˆ์ง€๋ง‰ ํ”Œ๋ ˆ์–ด ์‚ฌ์šฉ ์‹œ๊ฐ„ // AIM-9 ๋ฏธ์‚ฌ์ผ ์‹œ์Šคํ…œ this.aim9Missiles = GAME_CONSTANTS.AIM9_COUNT; this.firedMissiles = []; // ๋ฐœ์‚ฌ๋œ ๋ฏธ์‚ฌ์ผ ๋ฐฐ์—ด this.currentWeapon = 'MG'; // 'MG' ๋˜๋Š” 'AIM9' this.lockTarget = null; // ํ˜„์žฌ ๋ฝ์˜จ ์ค‘์ธ ํƒ€๊ฒŸ this.lockProgress = 0; // ๋ฝ์˜จ ์ง„ํ–‰๋„ (0~3) this.lastLockTime = 0; // ๋งˆ์ง€๋ง‰ ๋ฝ์˜จ ์‹œ๋„ ์‹œ๊ฐ„ this.lockAudios = { locking: null, locked: null }; this.initializeLockAudios(); // ์Šคํ†จ ํƒˆ์ถœ์„ ์œ„ํ•œ Fํ‚ค ์ƒํƒœ this.escapeKeyPressed = false; this.stallEscapeProgress = 0; // ์Šคํ†จ ํƒˆ์ถœ ์ง„ํ–‰๋„ (0~2์ดˆ) // ์นด๋ฉ”๋ผ ์„ค์ • this.cameraDistance = 250; this.cameraHeight = 30; this.cameraLag = 0.06; // ๊ฒฝ๊ณ  ์‹œ์Šคํ…œ this.altitudeWarning = false; this.stallWarning = false; this.warningBlinkTimer = 0; this.warningBlinkState = false; // Over-G ์‹œ์Šคํ…œ this.overG = false; this.maxGForce = 9.0; this.overGTimer = 0; // Over-G ์ง€์† ์‹œ๊ฐ„ // ๊ฒฝ๊ณ ์Œ ์‹œ์Šคํ…œ this.warningAudios = { altitude: null, pullup: null, overg: null, stall: null, normal: null, // ์—”์ง„ ์†Œ๋ฆฌ missileLock: null, missileIncoming: null }; this.initializeWarningAudios(); // ๋ฏธ์‚ฌ์ผ ๊ฒฝ๊ณ  ์‹œ์Šคํ…œ this.incomingMissiles = []; // ํ”Œ๋ ˆ์ด์–ด๋ฅผ ํ–ฅํ•ด ๋‚ ์•„์˜ค๋Š” ๋ฏธ์‚ฌ์ผ this.missileWarningActive = false; this.beingLockedBy = []; // ๋ฝ์˜จ ์ค‘์ธ ์ ๋“ค } // ํ—ค๋”ฉ์„ 0~360๋„๋กœ ์ •๊ทœํ™”ํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ normalizeHeading(radians) { let degrees = radians * (180 / Math.PI); while (degrees < 0) degrees += 360; while (degrees >= 360) degrees -= 360; return degrees; } // ๋ผ๋””์•ˆ์„ -ฯ€ ~ ฯ€ ๋ฒ”์œ„๋กœ ์ •๊ทœํ™” normalizeAngle(angle) { while (angle > Math.PI) angle -= Math.PI * 2; while (angle < -Math.PI) angle += Math.PI * 2; return angle; } initializeWarningAudios() { try { this.warningAudios.altitude = new Audio('sounds/altitude.ogg'); this.warningAudios.altitude.volume = 0.75; this.warningAudios.pullup = new Audio('sounds/pullup.ogg'); this.warningAudios.pullup.volume = 0.9; this.warningAudios.overg = new Audio('sounds/overg.ogg'); this.warningAudios.overg.volume = 0.75; this.warningAudios.stall = new Audio('sounds/alert.ogg'); this.warningAudios.stall.volume = 0.75; // ์—”์ง„ ์†Œ๋ฆฌ ์„ค์ • this.warningAudios.normal = new Audio('sounds/normal.ogg'); this.warningAudios.normal.volume = 0.5; this.warningAudios.normal.loop = true; // ์—”์ง„ ์†Œ๋ฆฌ๋Š” ๊ณ„์† ๋ฐ˜๋ณต // ๋ฏธ์‚ฌ์ผ ๊ฒฝ๊ณ ์Œ ์ถ”๊ฐ€ this.warningAudios.missileLock = new Audio('sounds/missile2.ogg'); this.warningAudios.missileLock.volume = 0.8; this.warningAudios.missileLock.loop = true; this.warningAudios.missileIncoming = new Audio('sounds/missile3.ogg'); this.warningAudios.missileIncoming.volume = 1.0; this.warningAudios.missileIncoming.loop = true; // ๊ฒฝ๊ณ ์Œ์—๋งŒ ended ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€ (์—”์ง„ ์†Œ๋ฆฌ ์ œ์™ธ) Object.keys(this.warningAudios).forEach(key => { if (key !== 'normal' && key !== 'missileLock' && key !== 'missileIncoming' && this.warningAudios[key]) { this.warningAudios[key].addEventListener('ended', () => { this.updateWarningAudios(); }); } }); } catch (e) { console.log('Warning audio initialization failed:', e); } } initializeLockAudios() { try { this.lockAudios.locking = new Audio('sounds/lockon.ogg'); this.lockAudios.locking.volume = 0.5; this.lockAudios.locking.loop = true; this.lockAudios.locked = new Audio('sounds/lockondone.ogg'); this.lockAudios.locked.volume = 0.7; } catch (e) { console.log('Lock audio initialization failed:', e); } } startEngineSound() { // ์—”์ง„ ์†Œ๋ฆฌ ์‹œ์ž‘ if (this.warningAudios.normal) { this.warningAudios.normal.play().catch(e => { console.log('Engine sound failed to start:', e); }); } } updateWarningAudios() { let currentWarning = null; // ๋ฏธ์‚ฌ์ผ ๊ฒฝ๊ณ ๊ฐ€ ์ตœ์šฐ์„  if (this.incomingMissiles.length > 0) { // missileIncoming ์žฌ์ƒ if (this.warningAudios.missileIncoming && this.warningAudios.missileIncoming.paused) { this.warningAudios.missileIncoming.play().catch(e => {}); } } else { // ๋ฏธ์‚ฌ์ผ์ด ์—†์œผ๋ฉด missileIncoming ์ค‘์ง€ if (this.warningAudios.missileIncoming && !this.warningAudios.missileIncoming.paused) { this.warningAudios.missileIncoming.pause(); this.warningAudios.missileIncoming.currentTime = 0; } } // ๋ฝ์˜จ ๊ฒฝ๊ณ  if (this.beingLockedBy.length > 0 && this.incomingMissiles.length === 0) { // missileLock ์žฌ์ƒ if (this.warningAudios.missileLock && this.warningAudios.missileLock.paused) { this.warningAudios.missileLock.play().catch(e => {}); } } else { // ๋ฝ์˜จ์ด ์—†์œผ๋ฉด missileLock ์ค‘์ง€ if (this.warningAudios.missileLock && !this.warningAudios.missileLock.paused) { this.warningAudios.missileLock.pause(); this.warningAudios.missileLock.currentTime = 0; } } // ๊ธฐ์กด ๊ฒฝ๊ณ ์Œ ์ฒ˜๋ฆฌ if (this.altitude < 250) { currentWarning = 'pullup'; } else if (this.altitude < 500) { currentWarning = 'altitude'; } else if (this.overG) { currentWarning = 'overg'; } else if (this.stallWarning) { currentWarning = 'stall'; } // ๊ฒฝ๊ณ ์Œ๋งŒ ๊ด€๋ฆฌ (์—”์ง„ ์†Œ๋ฆฌ, ๋ฏธ์‚ฌ์ผ ๊ฒฝ๊ณ ์Œ ์ œ์™ธ) Object.keys(this.warningAudios).forEach(key => { if (key !== 'normal' && key !== 'missileLock' && key !== 'missileIncoming' && key !== currentWarning && this.warningAudios[key] && !this.warningAudios[key].paused) { this.warningAudios[key].pause(); this.warningAudios[key].currentTime = 0; } }); if (currentWarning && this.warningAudios[currentWarning]) { if (this.warningAudios[currentWarning].paused) { this.warningAudios[currentWarning].play().catch(e => {}); } } } stopAllWarningAudios() { // ๋ชจ๋“  ์˜ค๋””์˜ค ์ •์ง€ (์—”์ง„ ์†Œ๋ฆฌ ํฌํ•จ) Object.values(this.warningAudios).forEach(audio => { if (audio && !audio.paused) { audio.pause(); audio.currentTime = 0; } }); // ๋ฝ์˜จ ์†Œ๋ฆฌ๋„ ์ •์ง€ if (this.lockAudios.locking && !this.lockAudios.locking.paused) { this.lockAudios.locking.pause(); this.lockAudios.locking.currentTime = 0; } } 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); this.mesh.rotation.y = Math.PI / 4; 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(); 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; 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 ์ „ํˆฌ๊ธฐ ๋ชจ๋ธ ์ƒ์„ฑ ์™„๋ฃŒ'); } switchWeapon() { if (this.currentWeapon === 'MG') { this.currentWeapon = 'AIM9'; // ๋ฌด๊ธฐ ์ „ํ™˜ ์‹œ ๋ฝ์˜จ ์ดˆ๊ธฐํ™” this.lockTarget = null; this.lockProgress = 0; if (this.lockAudios.locking && !this.lockAudios.locking.paused) { this.lockAudios.locking.pause(); this.lockAudios.locking.currentTime = 0; } } else { this.currentWeapon = 'MG'; // MG๋กœ ์ „ํ™˜ ์‹œ ๋ฝ์˜จ ํ•ด์ œ this.lockTarget = null; this.lockProgress = 0; if (this.lockAudios.locking && !this.lockAudios.locking.paused) { this.lockAudios.locking.pause(); this.lockAudios.locking.currentTime = 0; } } } updateLockOn(enemies, deltaTime) { // AIM-9 ๋ชจ๋“œ๊ฐ€ ์•„๋‹ˆ๋ฉด ๋ฝ์˜จ ํ•˜์ง€ ์•Š์Œ if (this.currentWeapon !== 'AIM9') { this.lockTarget = null; this.lockProgress = 0; return; } const now = Date.now(); // 1์ดˆ์— ํ•œ ๋ฒˆ์”ฉ ๋ฝ์˜จ ์ฒ˜๋ฆฌ if (now - this.lastLockTime < 1000) { return; } // ํ˜„์žฌ ํฌ๋กœ์Šคํ—ค์–ด ๋‚ด์˜ ์  ์ฐพ๊ธฐ let closestEnemy = null; let closestDistance = GAME_CONSTANTS.AIM9_LOCK_RANGE; enemies.forEach(enemy => { if (!enemy.mesh || !enemy.isLoaded) return; const distance = this.position.distanceTo(enemy.position); if (distance > GAME_CONSTANTS.AIM9_LOCK_RANGE) return; // ์ ์ด ํฌ๋กœ์Šคํ—ค์–ด ๋‚ด์— ์žˆ๋Š”์ง€ ํ™•์ธ const toEnemy = enemy.position.clone().sub(this.position).normalize(); const forward = new THREE.Vector3(0, 0, 1); // ์ฟผํ„ฐ๋‹ˆ์–ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ „๋ฐฉ ๋ฒกํ„ฐ ๊ณ„์‚ฐ const quaternion = new THREE.Quaternion(); const pitchQuat = new THREE.Quaternion(); const yawQuat = new THREE.Quaternion(); const rollQuat = new THREE.Quaternion(); pitchQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.rotation.x); yawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y); rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), this.rotation.z); quaternion.multiply(rollQuat); quaternion.multiply(pitchQuat); quaternion.multiply(yawQuat); forward.applyQuaternion(quaternion); const dotProduct = forward.dot(toEnemy); const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct))); // ํฌ๋กœ์Šคํ—ค์–ด ๋ฒ”์œ„ ๋‚ด (์•ฝ 10๋„) if (angle < Math.PI / 18 && distance < closestDistance) { closestEnemy = enemy; closestDistance = distance; } }); // ๋ฝ์˜จ ๋Œ€์ƒ ์—…๋ฐ์ดํŠธ if (closestEnemy) { if (this.lockTarget === closestEnemy) { // ๊ฐ™์€ ํƒ€๊ฒŸ์— ๊ณ„์† ๋ฝ์˜จ ์ค‘ this.lockProgress++; this.lastLockTime = now; // ๋ฝ์˜จ ์†Œ๋ฆฌ ์žฌ์ƒ if (this.lockProgress < GAME_CONSTANTS.AIM9_LOCK_REQUIRED) { if (this.lockAudios.locking && this.lockAudios.locking.paused) { this.lockAudios.locking.play().catch(e => {}); } } else { // ๋ฝ์˜จ ์™„๋ฃŒ if (this.lockAudios.locking && !this.lockAudios.locking.paused) { this.lockAudios.locking.pause(); this.lockAudios.locking.currentTime = 0; } if (this.lockAudios.locked) { this.lockAudios.locked.play().catch(e => {}); } } } else { // ์ƒˆ๋กœ์šด ํƒ€๊ฒŸ this.lockTarget = closestEnemy; this.lockProgress = 1; this.lastLockTime = now; // ๋ฝ์˜จ ์†Œ๋ฆฌ ์‹œ์ž‘ if (this.lockAudios.locking && this.lockAudios.locking.paused) { this.lockAudios.locking.play().catch(e => {}); } } } else { // ํƒ€๊ฒŸ ์žƒ์Œ this.lockTarget = null; this.lockProgress = 0; if (this.lockAudios.locking && !this.lockAudios.locking.paused) { this.lockAudios.locking.pause(); this.lockAudios.locking.currentTime = 0; } } } updateMouseInput(deltaX, deltaY) { // Over-G ์ƒํƒœ์—์„œ ์Šคํ†จ์ด ํ•ด์ œ๋˜์ง€ ์•Š์•˜์œผ๋ฉด ํ”ผ์น˜ ์กฐ์ž‘ ๋ถˆ๊ฐ€ if (this.overG && this.overGTimer > 1.0 && this.stallWarning) { // ์š”(Yaw)๋งŒ ์ œํ•œ์ ์œผ๋กœ ํ—ˆ์šฉ const sensitivity = GAME_CONSTANTS.MOUSE_SENSITIVITY * 0.3; this.targetYaw += deltaX * sensitivity * 0.3; return; } const sensitivity = GAME_CONSTANTS.MOUSE_SENSITIVITY * 1.0; // ๋งˆ์šฐ์Šค ์ž…๋ ฅ์„ ๊ฐ์†๋„๋กœ ๋ณ€ํ™˜ const pitchRate = -deltaY * sensitivity; const yawRate = deltaX * sensitivity * 0.8; // ํ˜„์žฌ ์ƒํƒœ์—์„œ์˜ ํ”ผ์น˜ ์ œํ•œ this.targetPitch += pitchRate; this.targetYaw += yawRate; // ์š” ํšŒ์ „์— ๋”ฐ๋ฅธ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฑ…ํฌ (๋กค) // ์‹ค์ œ ํ•ญ๊ณต๊ธฐ์ฒ˜๋Ÿผ ์„ ํšŒ ์‹œ ์ž๋™์œผ๋กœ ๊ธฐ์šธ์–ด์ง const targetBankAngle = -yawRate * 15; // ์š” ์†๋„์— ๋น„๋ก€ํ•œ ๋ฑ…ํฌ ๊ฐ๋„ // ๋กค์„ ๋ชฉํ‘œ ๋ฑ…ํฌ ๊ฐ๋„๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ „ํ™˜ this.targetRoll = THREE.MathUtils.lerp(this.targetRoll, targetBankAngle, 0.1); // ๊ฐ๋„ ์ œํ•œ const maxPitchAngle = Math.PI / 3; // 60๋„ const maxRollAngle = Math.PI * 0.5; // 90๋„ this.targetPitch = Math.max(-maxPitchAngle, Math.min(maxPitchAngle, this.targetPitch)); this.targetRoll = Math.max(-maxRollAngle, Math.min(maxRollAngle, this.targetRoll)); } updateControls(keys, deltaTime) { // W/S: ์Šค๋กœํ‹€๋งŒ ์ œ์–ด (๊ฐ€์†/๊ฐ์†) if (keys.w) { this.throttle = Math.min(1.0, this.throttle + deltaTime * 0.5); } if (keys.s) { this.throttle = Math.max(0.1, this.throttle - deltaTime * 0.5); } // A/D: ๋ณด์กฐ ์š” ์ œ์–ด (๋Ÿฌ๋”) - ๋ฐ˜์‘์„ฑ ๊ฐœ์„  if (keys.a) { this.targetYaw -= deltaTime * 1.2; } if (keys.d) { this.targetYaw += deltaTime * 1.2; } } updatePhysics(deltaTime) { if (!this.mesh) return; // ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „ ๋ณด๊ฐ„ const rotationSpeed = deltaTime * 2.0; const yawRotationSpeed = deltaTime * 3.0; // ํ”ผ์น˜์™€ ๋กค์€ ์ง์ ‘ ๋ณด๊ฐ„ this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetPitch, rotationSpeed); this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRoll, rotationSpeed * 1.5); // Yaw ๋ณด๊ฐ„ (์ตœ๋‹จ ๊ฒฝ๋กœ) let yawDiff = this.normalizeAngle(this.targetYaw - this.rotation.y); this.rotation.y += yawDiff * yawRotationSpeed; // rotation.y๋ฅผ -ฯ€ ~ ฯ€ ๋ฒ”์œ„๋กœ ์œ ์ง€ this.rotation.y = this.normalizeAngle(this.rotation.y); this.targetYaw = this.normalizeAngle(this.targetYaw); // ๋กค ์ž๋™ ๋ณต๊ท€ ์‹œ์Šคํ…œ if (Math.abs(yawDiff) < 0.05) { // ์š” ํšŒ์ „์ด ๊ฑฐ์˜ ์—†์„ ๋•Œ ๋กค์„ 0์œผ๋กœ ๋ณต๊ท€ this.targetRoll *= 0.95; } // ์š” ํšŒ์ „์ด ์ฃผ๋„์ , ๋กค์€ ๋ณด์กฐ์  let bankTurnRate = 0; if (Math.abs(this.rotation.z) > 0.3) { // ๋กค์ด ์ถฉ๋ถ„ํžˆ ํด ๋•Œ๋งŒ const bankAngle = this.rotation.z; bankTurnRate = Math.sin(bankAngle) * deltaTime * 0.1; // ๋งค์šฐ ์ž‘์€ ์„ ํšŒ์œจ this.targetYaw += bankTurnRate; } // ํ˜„์‹ค์ ์ธ ์†๋„ ๊ณ„์‚ฐ - 1750kt๊นŒ์ง€ ์ฆ๊ฐ€ const minSpeed = 0; // ์ตœ์†Œ ์†๋„ 0kt (m/s) const maxSpeed = 900.6; // ์ตœ๋Œ€ ์†๋„ 1750kt๋ฅผ m/s๋กœ ๋ณ€ํ™˜ (1750 * 0.5144444) let targetSpeed = minSpeed + (maxSpeed - minSpeed) * this.throttle; // ํ”ผ์น˜ ๊ฐ๋„์— ๋”ฐ๋ฅธ ์†๋„ ๋ณ€ํ™” const pitchAngle = this.rotation.x; const pitchDegrees = Math.abs(pitchAngle) * (180 / Math.PI); // ๊ธฐ์ˆ˜๊ฐ€ ์œ„๋ฅผ ํ–ฅํ•˜๊ณ  ์žˆ์„ ๊ฒฝ์šฐ ๋น ๋ฅธ ์†๋„ ๊ฐ์†Œ if (pitchAngle < -0.1 && !this.stallWarning) { // ์Šคํ†จ์ด ์•„๋‹ ๋•Œ๋งŒ ์ƒ์Šน์œผ๋กœ ์ธํ•œ ๊ฐ์† const climbFactor = Math.abs(pitchAngle) / (Math.PI / 2); // 90๋„ ๊ธฐ์ค€ if (pitchDegrees > 30) { // 30๋„ ์ด์ƒ์ผ ๋•Œ ๊ธ‰๊ฒฉํ•œ ๊ฐ์† targetSpeed *= Math.max(0, 1 - climbFactor * 1.5); // ์ตœ๋Œ€ 150% ๊ฐ์† (0kt๊นŒ์ง€) } else { targetSpeed *= (1 - climbFactor * 0.3); // ์ •์ƒ์ ์ธ ๊ฐ์† } } else if (pitchAngle > 0.1) { // ๊ธฐ์ˆ˜๊ฐ€ ์•„๋ž˜๋กœ (ํ•˜๊ฐ•) - ์Šคํ†จ ์ƒํƒœ์—์„œ๋„ ์ ์šฉ const diveFactor = pitchAngle / (Math.PI / 3); targetSpeed *= (1 + diveFactor * 0.4); // ํ•˜๊ฐ• ์‹œ ๊ฐ€์† ์ฆ๊ฐ€ (0.2 -> 0.4) } // G-Force ๊ณ„์‚ฐ ๊ฐœ์„  const turnRate = Math.abs(bankTurnRate) * 100; const pitchRate = Math.abs(this.rotation.x - this.targetPitch) * 10; // ๊ณ ๋„์— ๋”ฐ๋ฅธ G-Force ์ฆ๊ฐ€ ๋ฐฐ์œจ ๊ณ„์‚ฐ const altitudeInKm = this.position.y / 1000; // ๋ฏธํ„ฐ๋ฅผ ํ‚ฌ๋กœ๋ฏธํ„ฐ๋กœ ๋ณ€ํ™˜ const altitudeMultiplier = 1 + (altitudeInKm * 0.2); // 1km๋‹น 20% ์ฆ๊ฐ€ // ์Šค๋กœํ‹€์— ๋”ฐ๋ฅธ G-Force ์ฆ๊ฐ€ ๋ฐฐ์œจ ๊ณ„์‚ฐ // THR 50% ์ดํ•˜: 0๋ฐฐ, THR 75%: 0.5๋ฐฐ, THR 100%: 1.0๋ฐฐ let throttleGMultiplier = 0; if (this.throttle > 0.5) { // 0.5 ~ 1.0 ๋ฒ”์œ„๋ฅผ 0 ~ 1.0์œผ๋กœ ๋งคํ•‘ throttleGMultiplier = (this.throttle - 0.5) * 2.0; } // ๋น„์ •์ƒ์ ์ธ ์ž์„ธ์— ์˜ํ•œ G-Force ์ถ”๊ฐ€ let abnormalG = 0; // ๋’ค์ง‘ํžŒ ์ƒํƒœ (๋กค์ด 90๋„ ์ด์ƒ) const isInverted = Math.abs(this.rotation.z) > Math.PI / 2; if (isInverted) { const baseG = 3.0 + Math.abs(Math.abs(this.rotation.z) - Math.PI / 2) * 2; abnormalG += baseG * altitudeMultiplier * (1 + throttleGMultiplier); // ์Šค๋กœํ‹€ ๋ฐฐ์œจ ์ถ”๊ฐ€ } // ๋กค์ด ยฑ30๋„ ์ด์ƒ์ผ ๋•Œ ์ถ”๊ฐ€ G-Force const rollDegrees = Math.abs(this.rotation.z) * (180 / Math.PI); if (rollDegrees > 30) { // 30๋„ ์ดˆ๊ณผ๋ถ„๋‹น 0.1G ์ถ”๊ฐ€ const extremeRollG = (rollDegrees - 30) * 0.1; abnormalG += extremeRollG * altitudeMultiplier * (1 + throttleGMultiplier); } // ํ”ผ์น˜ ๊ฐ๋„๊ฐ€ ยฑ40๋„ ์ด์ƒ์ผ ๋•Œ ์ถ”๊ฐ€ G-Force if (pitchDegrees >= 40) { // 40๋„ ์ด์ƒ์ผ ๋•Œ ๊ธ‰๊ฒฉํ•œ G-Force ์ฆ๊ฐ€ const extremePitchG = (pitchDegrees - 40) * 0.15; // 40๋„ ์ดˆ๊ณผ๋ถ„๋‹น 0.15G ์ถ”๊ฐ€ abnormalG += extremePitchG * altitudeMultiplier * (1 + throttleGMultiplier); } // ๊ธฐ์ˆ˜๊ฐ€ ๊ณ„์† ์œ„๋ฅผ ํ–ฅํ•˜๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ (ํ”ผ์น˜๊ฐ€ -30๋„ ์ดํ•˜) if (pitchAngle < -Math.PI / 6) { const baseG = 2.0 + Math.abs(pitchAngle + Math.PI / 6) * 3; abnormalG += baseG * altitudeMultiplier * (1 + throttleGMultiplier); // ์Šค๋กœํ‹€ ๋ฐฐ์œจ ์ถ”๊ฐ€ } // ๊ธฐ์ˆ˜๊ฐ€ ๊ณผ๋„ํ•˜๊ฒŒ ์•„๋ž˜๋ฅผ ํ–ฅํ•˜๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ (ํ”ผ์น˜๊ฐ€ 60๋„ ์ด์ƒ) if (pitchAngle > Math.PI / 3) { const baseG = 2.0 + Math.abs(pitchAngle - Math.PI / 3) * 3; abnormalG += baseG * altitudeMultiplier * (1 + throttleGMultiplier); // ์Šค๋กœํ‹€ ๋ฐฐ์œจ ์ถ”๊ฐ€ } // ๊ธ‰๊ฒฉํ•œ ๊ธฐ๋™์— ์˜ํ•œ G-Force const maneuverG = (turnRate + pitchRate + (Math.abs(this.rotation.z) * 3)) * (1 + throttleGMultiplier * 0.5); // ์ด G-Force ๊ณ„์‚ฐ this.gForce = 1.0 + maneuverG + abnormalG; // G-Force ํšŒ๋ณต ์กฐ๊ฑด ์ˆ˜์ • // 1. Over-G ์ƒํƒœ๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ ํšŒ๋ณต // 2. ํ”ผ์น˜๊ฐ€ ยฑ10๋„ ์ด๋‚ด์ผ ๋•Œ๋งŒ ํšŒ๋ณต // 3. ์Šคํ†จ ์ƒํƒœ๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ ํšŒ๋ณต const isPitchNeutral = Math.abs(pitchDegrees) <= 10; if (!this.overG && isPitchNeutral && !isInverted && !this.stallWarning) { // ์Šค๋กœํ‹€์ด ๋†’์„์ˆ˜๋ก G-Force๊ฐ€ ์ฒœ์ฒœํžˆ ๊ฐ์†Œ const recoveryRate = 2.0 - throttleGMultiplier * 1.5; // THR 100%์ผ ๋•Œ 0.5, THR 50%์ผ ๋•Œ 2.0 this.gForce = THREE.MathUtils.lerp(this.gForce, 1.0 + maneuverG, deltaTime * recoveryRate); } else if (this.overG) { // Over-G ์ƒํƒœ์—์„œ๋Š” ํ”ผ์น˜๊ฐ€ 0๋„ ๊ทผ์ฒ˜(ยฑ10๋„)๊ฐ€ ๋˜๊ณ  ์Šคํ†จ์ด ํšŒ๋ณต๋  ๋•Œ๊นŒ์ง€ ํšŒ๋ณตํ•˜์ง€ ์•Š์Œ if (!isPitchNeutral || this.stallWarning) { // ํ”ผ์น˜๊ฐ€ ์ค‘๋ฆฝ์ด ์•„๋‹ˆ๊ฑฐ๋‚˜ ์Šคํ†จ ์ƒํƒœ๋ฉด G-Force ์œ ์ง€ ๋˜๋Š” ์ฆ๊ฐ€๋งŒ ๊ฐ€๋Šฅ this.gForce = Math.max(this.gForce, 1.0 + maneuverG + abnormalG); } else { // ํ”ผ์น˜๊ฐ€ ์ค‘๋ฆฝ์ด๊ณ  ์Šคํ†จ์ด ์•„๋‹ ๋•Œ๋งŒ ๋งค์šฐ ์ฒœ์ฒœํžˆ ํšŒ๋ณต const overGRecoveryRate = 0.3 - throttleGMultiplier * 0.2; // Over-G ์ƒํƒœ์—์„œ๋Š” ๋” ๋А๋ฆฐ ํšŒ๋ณต this.gForce = THREE.MathUtils.lerp(this.gForce, 1.0 + maneuverG, deltaTime * overGRecoveryRate); } } // ์Šคํ†จ ์ƒํƒœ์—์„œ๋Š” Over-G๊ฐ€ ๊ฐ์†Œํ•˜์ง€ ์•Š๋„๋ก ์ถ”๊ฐ€ ์ฒ˜๋ฆฌ if (this.stallWarning && this.overG) { // ์Šคํ†จ ์ค‘์—๋Š” G-Force๋ฅผ 9.0 ์ด์ƒ์œผ๋กœ ์œ ์ง€ this.gForce = Math.max(this.gForce, 9.0); } this.overG = this.gForce > this.maxGForce; // Over-G ํƒ€์ด๋จธ ์—…๋ฐ์ดํŠธ if (this.overG) { this.overGTimer += deltaTime; // 1.5์ดˆ ์ด์ƒ Over-G ์ƒํƒœ์ผ ๊ฒฝ์šฐ ์‹œ์•ผ ํ๋ฆผ ์‹œ์ž‘ if (this.overGTimer > 1.5) { // ์†๋„ ๊ธ‰๊ฒฉํžˆ ๊ฐ์†Œ (2.5์ดˆ๋ถ€ํ„ฐ) if (this.overGTimer > 2.5) { targetSpeed *= Math.max(0.3, 1 - (this.overGTimer - 2.5) * 0.3); } // ์‹œ์•ผ ํ๋ฆผ ํšจ๊ณผ๋Š” UI์—์„œ ์ฒ˜๋ฆฌ (1.5์ดˆ๋ถ€ํ„ฐ) } } else { this.overGTimer = 0; // Over-G ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ฉด ํƒ€์ด๋จธ ๋ฆฌ์…‹ } // ์Šคํ†จ ๊ฒฝ๊ณ : 300kt ์ดํ•˜์—์„œ ์Šคํ†จ ์œ„ํ—˜ const speedKnots = this.speed * 1.94384; // m/s to knots const wasStalling = this.stallWarning; // ์Šคํ†จ ์ง„์ž… ์กฐ๊ฑด if (!this.stallWarning && speedKnots < GAME_CONSTANTS.STALL_SPEED) { this.stallWarning = true; this.stallEscapeProgress = 0; // ์Šคํ†จ ์ง„์ž… ์‹œ ์ง„ํ–‰๋„ ์ดˆ๊ธฐํ™” } // ์Šคํ†จ ํƒˆ์ถœ ์กฐ๊ฑด if (this.stallWarning) { if (this.escapeKeyPressed) { // Fํ‚ค๋ฅผ ๋ˆ„๋ฅด๊ณ  ์žˆ์œผ๋ฉด ์ง„ํ–‰๋„ ์ฆ๊ฐ€ this.stallEscapeProgress += deltaTime; // 2์ดˆ ์ด์ƒ ๋ˆ„๋ฅด๋ฉด ์Šคํ†จ์—์„œ ํƒˆ์ถœ if (this.stallEscapeProgress >= 2.0 && speedKnots >= 350) { this.stallWarning = false; this.stallEscapeProgress = 0; // ์Šคํ†จ์—์„œ ๋ฒ—์–ด๋‚˜๋ฉด ์ž์„ธ๋ฅผ ์•ฝ๊ฐ„ ์•ˆ์ •ํ™” this.targetPitch = Math.max(-0.2, Math.min(0.2, this.targetPitch)); } } else { // Fํ‚ค๋ฅผ ๋†“์œผ๋ฉด ์ง„ํ–‰๋„ ๊ฐ์†Œ this.stallEscapeProgress = Math.max(0, this.stallEscapeProgress - deltaTime * 2); } } // ์†๋„ ๋ณ€ํ™” ์ ์šฉ if (this.stallWarning) { // ์Šคํ†จ ์ƒํƒœ์—์„œ์˜ ์†๋„ ๋ณ€ํ™” if (pitchAngle > 0.1) { // ๊ธฐ์ˆ˜๊ฐ€ ์•„๋ž˜๋ฅผ ํ–ฅํ•  ๋•Œ // ๋‹ค์ด๋น™์œผ๋กœ ์ธํ•œ ์†๋„ ์ฆ๊ฐ€ - 1750kt ์ตœ๋Œ€ ์†๋„์— ๋งž์ถฐ ์กฐ์ • const diveSpeedGain = Math.min(pitchAngle * 500, 400); // ์ตœ๋Œ€ 400m/s ์ฆ๊ฐ€ this.speed = Math.min(maxSpeed, this.speed + diveSpeedGain * deltaTime); } else { // ๊ธฐ์ˆ˜๊ฐ€ ์œ„๋ฅผ ํ–ฅํ•˜๊ฑฐ๋‚˜ ์ˆ˜ํ‰์ผ ๋•Œ๋Š” ์†๋„ ๊ฐ์†Œ this.speed = Math.max(0, this.speed - deltaTime * 100); } } else { // ์ •์ƒ ๋น„ํ–‰ ์‹œ ์†๋„ ๋ณ€ํ™” this.speed = THREE.MathUtils.lerp(this.speed, targetSpeed, deltaTime * 0.5); } // ์Šคํ†จ ์ƒํƒœ์—์„œ์˜ ๋ฌผ๋ฆฌ ํšจ๊ณผ if (this.stallWarning) { // ๋ฐ”๋‹ฅ์œผ๋กœ ์ถ”๋ฝํ•˜๋ฉฐ ๊ฐ€์†๋„๊ฐ€ ๋น ๋ฅด๊ฒŒ ๋ถ™์Œ this.targetPitch = Math.min(Math.PI / 2.5, this.targetPitch + deltaTime * 2.5); // ๊ธฐ์ˆ˜๊ฐ€ ๋” ๊ทน๋‹จ์ ์œผ๋กœ ์•„๋ž˜๋กœ (72๋„๊นŒ์ง€) // ์กฐ์ข… ๋ถˆ๋Šฅ ์ƒํƒœ - ๋” ์‹ฌํ•œ ํ”๋“ค๋ฆผ this.rotation.x += (Math.random() - 0.5) * deltaTime * 0.8; this.rotation.z += (Math.random() - 0.5) * deltaTime * 0.8; // ์ค‘๋ ฅ์— ์˜ํ•œ ๊ฐ€์† const gravityAcceleration = GAME_CONSTANTS.GRAVITY * deltaTime * 3.0; // 3๋ฐฐ ์ค‘๋ ฅ this.velocity.y -= gravityAcceleration; } // ์†๋„ ๋ฒกํ„ฐ ๊ณ„์‚ฐ - ์ฟผํ„ฐ๋‹ˆ์–ธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ง๋ฒŒ ๋ฝ ์™„์ „ ํšŒํ”ผ const noseDirection = new THREE.Vector3(0, 0, 1); // ์ฟผํ„ฐ๋‹ˆ์–ธ ๊ธฐ๋ฐ˜ ํšŒ์ „ ์‹œ์Šคํ…œ const quaternion = new THREE.Quaternion(); // ๊ฐ ์ถ•์˜ ํšŒ์ „์„ ๊ฐœ๋ณ„ ์ฟผํ„ฐ๋‹ˆ์–ธ์œผ๋กœ ์ƒ์„ฑ const pitchQuat = new THREE.Quaternion(); const yawQuat = new THREE.Quaternion(); const rollQuat = new THREE.Quaternion(); // ์ถ• ๋ฒกํ„ฐ ์ •์˜ const xAxis = new THREE.Vector3(1, 0, 0); const yAxis = new THREE.Vector3(0, 1, 0); const zAxis = new THREE.Vector3(0, 0, 1); // ๊ฐ ์ถ•์— ๋Œ€ํ•œ ํšŒ์ „ ์„ค์ • pitchQuat.setFromAxisAngle(xAxis, this.rotation.x); yawQuat.setFromAxisAngle(yAxis, this.rotation.y); rollQuat.setFromAxisAngle(zAxis, this.rotation.z); // ์ฟผํ„ฐ๋‹ˆ์–ธ ํ•ฉ์„ฑ (์˜ฌ๋ฐ”๋ฅธ ํ•ญ๊ณต๊ธฐ ์ˆœ์„œ: Roll -> Pitch -> Yaw) // ํ•ญ๊ณต๊ธฐ๋Š” ๋จผ์ € ๋กค, ๊ทธ ๋‹ค์Œ ํ”ผ์น˜, ๋งˆ์ง€๋ง‰์œผ๋กœ ์š” ์ˆœ์„œ๋กœ ํšŒ์ „ quaternion.multiply(rollQuat); quaternion.multiply(pitchQuat); quaternion.multiply(yawQuat); // ๋ฐฉํ–ฅ ๋ฒกํ„ฐ์— ํšŒ์ „ ์ ์šฉ noseDirection.applyQuaternion(quaternion); if (!this.stallWarning) { // ์ •์ƒ ๋น„ํ–‰ ์‹œ this.velocity = noseDirection.multiplyScalar(this.speed); } else { // ์Šคํ†จ ์‹œ์—๋Š” ์ค‘๋ ฅ์ด ์ฃผ๋„์ ์ด์ง€๋งŒ ๋‹ค์ด๋น™ ์†๋„๋„ ๋ฐ˜์˜ this.velocity.x = noseDirection.x * this.speed * 0.5; // ์ „๋ฐฉ ์†๋„ ์ฆ๊ฐ€ this.velocity.z = noseDirection.z * this.speed * 0.5; // y ์†๋„๋Š” ์œ„์—์„œ ์ค‘๋ ฅ์œผ๋กœ ์ฒ˜๋ฆฌ๋จ } // ์ •์ƒ ๋น„ํ–‰ ์‹œ ์ค‘๋ ฅ ํšจ๊ณผ if (!this.stallWarning) { const gravityEffect = GAME_CONSTANTS.GRAVITY * deltaTime * 0.15; this.velocity.y -= gravityEffect; // ์–‘๋ ฅ ํšจ๊ณผ (์†๋„์— ๋น„๋ก€) - ์ตœ๋Œ€ ์†๋„ ์ฆ๊ฐ€์— ๋งž์ถฐ ์กฐ์ • const liftFactor = Math.min((this.speed / 500), 1.0) * 0.8; // 500m/s ์ด์ƒ์—์„œ ์ตœ๋Œ€ ์–‘๋ ฅ const lift = gravityEffect * liftFactor; this.velocity.y += lift; } // ์œ„์น˜ ์—…๋ฐ์ดํŠธ this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); // ์ง€๋ฉด ์ถฉ๋Œ if (this.position.y <= GAME_CONSTANTS.MIN_ALTITUDE) { this.position.y = GAME_CONSTANTS.MIN_ALTITUDE; this.health = 0; // ์ง€๋ฉด ์ถฉ๋Œ ์‹œ ํญ๋ฐœ ํšจ๊ณผ ์ถ”๊ฐ€ if (window.gameInstance) { window.gameInstance.createExplosionEffect(this.position); } return; } // ์ตœ๋Œ€ ๊ณ ๋„ ์ œํ•œ if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) { this.position.y = GAME_CONSTANTS.MAX_ALTITUDE; this.altitudeWarning = true; if (this.velocity.y > 0) this.velocity.y = 0; } else { this.altitudeWarning = false; } // ๋งต ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ 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); // ๋ฉ”์‹œ ํšŒ์ „์„ ์ฟผํ„ฐ๋‹ˆ์–ธ์œผ๋กœ ์„ค์ • const meshQuaternion = new THREE.Quaternion(); // Y์ถ• ํšŒ์ „์— 3ฯ€/2 ์ถ”๊ฐ€ (๋ชจ๋ธ ์˜คํ”„์…‹) const meshPitchQuat = new THREE.Quaternion(); const meshYawQuat = new THREE.Quaternion(); const meshRollQuat = new THREE.Quaternion(); meshPitchQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.rotation.x); meshYawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y + 3 * Math.PI / 2); meshRollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), this.rotation.z); // ์ฟผํ„ฐ๋‹ˆ์–ธ ํ•ฉ์„ฑ (์˜ฌ๋ฐ”๋ฅธ ์ˆœ์„œ) meshQuaternion.multiply(meshRollQuat); meshQuaternion.multiply(meshPitchQuat); meshQuaternion.multiply(meshYawQuat); this.mesh.quaternion.copy(meshQuaternion); // ๊ฒฝ๊ณ  ๊นœ๋นก์ž„ ํƒ€์ด๋จธ this.warningBlinkTimer += deltaTime; if (this.warningBlinkTimer >= 1.0) { this.warningBlinkTimer = 0; this.warningBlinkState = !this.warningBlinkState; } // ๊ณ ๋„ ๊ณ„์‚ฐ this.altitude = this.position.y; // ๊ฒฝ๊ณ ์Œ ์—…๋ฐ์ดํŠธ (์—”์ง„ ์†Œ๋ฆฌ๋Š” ๊ณ„์† ์œ ์ง€) this.updateWarningAudios(); // ์—”์ง„ ์†Œ๋ฆฌ ๋ณผ๋ฅจ์„ ์Šค๋กœํ‹€์— ์—ฐ๋™ if (this.warningAudios.normal && !this.warningAudios.normal.paused) { this.warningAudios.normal.volume = 0.3 + this.throttle * 0.4; // 0.3~0.7 } // ํ”Œ๋ ˆ์–ด ์—…๋ฐ์ดํŠธ for (let i = this.deployedFlares.length - 1; i >= 0; i--) { const flare = this.deployedFlares[i]; if (!flare.update(deltaTime)) { this.deployedFlares.splice(i, 1); } } } shoot(scene) { if (this.currentWeapon === 'MG') { // ๊ธฐ์กด MG ๋ฐœ์‚ฌ ๋กœ์ง // ํƒ„์•ฝ์ด ์—†์œผ๋ฉด ๋ฐœ์‚ฌํ•˜์ง€ ์•Š์Œ if (this.ammo <= 0) return; this.ammo--; // ์ง์„  ๋ชจ์–‘์˜ ํƒ„ํ™˜ (100% ๋” ํฌ๊ฒŒ) const bulletGeometry = new THREE.CylinderGeometry(1.0, 1.0, 16, 8); // ๋ฐ˜์ง€๋ฆ„ 0.75โ†’1.0, ๊ธธ์ด 12โ†’16 const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); // ๊ธฐ์ˆ˜ ๋์—์„œ ๋ฐœ์‚ฌ (์ฟผํ„ฐ๋‹ˆ์–ธ ์‚ฌ์šฉ) const muzzleOffset = new THREE.Vector3(0, 0, 10); // ์ „ํˆฌ๊ธฐ์™€ ๋™์ผํ•œ ์ฟผํ„ฐ๋‹ˆ์–ธ ์ƒ์„ฑ const quaternion = new THREE.Quaternion(); const pitchQuat = new THREE.Quaternion(); const yawQuat = new THREE.Quaternion(); const rollQuat = new THREE.Quaternion(); pitchQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.rotation.x); yawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y); rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), this.rotation.z); quaternion.multiply(rollQuat); quaternion.multiply(pitchQuat); quaternion.multiply(yawQuat); muzzleOffset.applyQuaternion(quaternion); bullet.position.copy(this.position).add(muzzleOffset); // ํƒ„ํ™˜์„ ๋ฐœ์‚ฌ ๋ฐฉํ–ฅ์œผ๋กœ ํšŒ์ „ bullet.quaternion.copy(quaternion); // ์‹ค๋ฆฐ๋”๊ฐ€ Z์ถ• ๋ฐฉํ–ฅ์„ ํ–ฅํ•˜๋„๋ก ์ถ”๊ฐ€ ํšŒ์ „ const cylinderRotation = new THREE.Quaternion(); cylinderRotation.setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2); bullet.quaternion.multiply(cylinderRotation); // ํƒ„ํ™˜ ์ดˆ๊ธฐ ์œ„์น˜ ์ €์žฅ bullet.startPosition = bullet.position.clone(); const bulletSpeed = 1500; // 1000์—์„œ 1500์œผ๋กœ ์ฆ๊ฐ€ (50% ๋น ๋ฅด๊ฒŒ) const direction = new THREE.Vector3(0, 0, 1); direction.applyQuaternion(quaternion); bullet.velocity = direction.multiplyScalar(bulletSpeed); scene.add(bullet); this.bullets.push(bullet); // m134.ogg ์†Œ๋ฆฌ ์žฌ์ƒ - ์ค‘์ฒฉ ์ œํ•œ ํ•ด์ œ, ๋žœ๋ค ํ”ผ์น˜ try { const audio = new Audio('sounds/m134.ogg'); audio.volume = 0.15; // 0.3์—์„œ 0.15๋กœ ๊ฐ์†Œ (50% ์ค„์ž„) // -2 ~ +2 ์‚ฌ์ด์˜ ๋žœ๋ค ํ”ผ์น˜ (playbackRate๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜) const randomPitch = 0.8 + Math.random() * 0.4; // 0.8 ~ 1.2 ๋ฒ”์œ„ audio.playbackRate = randomPitch; audio.play().catch(e => console.log('Gunfire sound failed to play')); // ์žฌ์ƒ ์™„๋ฃŒ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ๋ฅผ ์œ„ํ•ด ์ฐธ์กฐ ์ œ๊ฑฐ audio.addEventListener('ended', () => { audio.remove(); }); } catch (e) { console.log('Audio error:', e); } } else if (this.currentWeapon === 'AIM9') { // AIM-9 ๋ฏธ์‚ฌ์ผ ๋ฐœ์‚ฌ if (this.aim9Missiles <= 0) return; if (this.lockProgress < GAME_CONSTANTS.AIM9_LOCK_REQUIRED) return; if (!this.lockTarget) return; this.aim9Missiles--; // ๋‚ ๊ฐœ ๋ฐœ์‚ฌ ์œ„์น˜ ๊ฒฐ์ • - ์ขŒ์šฐ ๋‚ ๊ฐœ ์ค‘ ๋ฌด์ž‘์œ„ ์„ ํƒ const isLeftWing = Math.random() < 0.5; const wingOffset = new THREE.Vector3(isLeftWing ? -8 : 8, -1, 2); // ๋‚ ๊ฐœ ์œ„์น˜ // ์ „ํˆฌ๊ธฐ์˜ ํšŒ์ „์„ ์ ์šฉ const quaternion = new THREE.Quaternion(); const pitchQuat = new THREE.Quaternion(); const yawQuat = new THREE.Quaternion(); const rollQuat = new THREE.Quaternion(); pitchQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.rotation.x); yawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y); rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), this.rotation.z); quaternion.multiply(rollQuat); quaternion.multiply(pitchQuat); quaternion.multiply(yawQuat); wingOffset.applyQuaternion(quaternion); const missileStartPos = this.position.clone().add(wingOffset); // ๋ฏธ์‚ฌ์ผ ์ƒ์„ฑ const missile = new AIM9Missile(scene, missileStartPos, this.lockTarget, this.rotation.clone()); this.firedMissiles.push(missile); // ๋ฝ์˜จ ์ดˆ๊ธฐํ™” this.lockTarget = null; this.lockProgress = 0; if (this.lockAudios.locking && !this.lockAudios.locking.paused) { this.lockAudios.locking.pause(); this.lockAudios.locking.currentTime = 0; } // ๋ฐœ์‚ฌ์Œ try { const missileSound = new Audio('sounds/missile.ogg'); missileSound.volume = 0.7; missileSound.play().catch(e => {}); } catch (e) { console.log('Missile sound failed:', e); } } } deployFlares() { if (this.flareCount <= 0) return; const now = Date.now(); if (now - this.lastFlareTime < 2000) return; // 2์ดˆ ์ฟจ๋‹ค์šด this.flareCount--; this.lastFlareTime = now; // ๊ผฌ๋ฆฌ ๋ถ€๋ถ„ ์œ„์น˜ ๊ณ„์‚ฐ const tailOffset = new THREE.Vector3(0, 0, -15); const quaternion = new THREE.Quaternion(); const pitchQuat = new THREE.Quaternion(); const yawQuat = new THREE.Quaternion(); const rollQuat = new THREE.Quaternion(); pitchQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.rotation.x); yawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y); rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), this.rotation.z); quaternion.multiply(rollQuat); quaternion.multiply(pitchQuat); quaternion.multiply(yawQuat); tailOffset.applyQuaternion(quaternion); const flareStartPos = this.position.clone().add(tailOffset); // ํ›„๋ฐฉ ๋ฐฉํ–ฅ const backwardDirection = new THREE.Vector3(0, 0, -1); backwardDirection.applyQuaternion(quaternion); // ์–‘์ชฝ์œผ๋กœ 2๊ฐœ์”ฉ, ์ด 4๊ฐœ ํ”Œ๋ ˆ์–ด ๋ฐœ์‚ฌ for (let i = 0; i < 4; i++) { const isLeftSide = i < 2; const flare = new Flare(window.gameInstance.scene, flareStartPos, backwardDirection, isLeftSide); this.deployedFlares.push(flare); // ๋ชจ๋“  ๋ฏธ์‚ฌ์ผ์—๊ฒŒ ํ”Œ๋ ˆ์–ด ์•Œ๋ฆผ if (window.gameInstance) { window.gameInstance.notifyMissilesOfFlare(flare); } } // ํ”Œ๋ ˆ์–ด ๋ฐœ์‚ฌ์Œ try { const flareSound = new Audio('sounds/flare.ogg'); flareSound.volume = 0.6; flareSound.play().catch(e => { console.log('Flare sound not found, using alternative'); // ๋Œ€์ฒด ์†Œ๋ฆฌ (๊ธฐ์กด ์‚ฌ์šด๋“œ ํ™œ์šฉ) const altSound = new Audio('sounds/missile.ogg'); altSound.volume = 0.3; altSound.playbackRate = 2.0; // ๋น ๋ฅด๊ฒŒ ์žฌ์ƒ altSound.play().catch(e => {}); }); } catch (e) {} console.log(`Flares deployed! Remaining: ${this.flareCount}`); } updateBullets(scene, deltaTime, gameInstance) { for (let i = this.bullets.length - 1; i >= 0; i--) { const bullet = this.bullets[i]; bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime)); // ํƒ„ํ™˜๋„ ๊ฐ™์€ ๋ฐฉํ–ฅ์„ ์œ ์ง€ํ•˜๋„๋ก ํšŒ์ „ ์—…๋ฐ์ดํŠธ const direction = bullet.velocity.clone().normalize(); const angle = Math.atan2(direction.x, direction.z); bullet.rotation.y = angle; // ์ง€๋ฉด ์ถฉ๋Œ ์ฒดํฌ ์ถ”๊ฐ€ if (bullet.position.y <= 0) { // ํฌ๊ณ  ํ™”๋ คํ•œ ์ง€๋ฉด ์ถฉ๋Œ ํšจ๊ณผ gameInstance.createGroundImpactEffect(bullet.position); scene.remove(bullet); this.bullets.splice(i, 1); continue; } // 6000m ์ด์ƒ ๋‚ ์•„๊ฐ€๊ฑฐ๋‚˜ ๋†’์ด ์ œํ•œ ์ดˆ๊ณผ ์‹œ ์ œ๊ฑฐ if (bullet.position.distanceTo(bullet.startPosition) > 6000 || bullet.position.y > GAME_CONSTANTS.MAX_ALTITUDE + 500) { scene.remove(bullet); this.bullets.splice(i, 1); } } // ๋ฏธ์‚ฌ์ผ ์—…๋ฐ์ดํŠธ for (let i = this.firedMissiles.length - 1; i >= 0; i--) { const missile = this.firedMissiles[i]; const result = missile.update(deltaTime, this.position); if (result === 'hit' || result === 'expired') { this.firedMissiles.splice(i, 1); } } } takeDamage(damage) { this.health -= damage; return this.health <= 0; } getCameraPosition() { const backward = new THREE.Vector3(0, 0, -1); const up = new THREE.Vector3(0, 1, 0); // ์นด๋ฉ”๋ผ ์œ„์น˜ ๊ณ„์‚ฐ์—๋„ ์ฟผํ„ฐ๋‹ˆ์–ธ ์‚ฌ์šฉ const quaternion = new THREE.Quaternion(); const pitchQuat = new THREE.Quaternion(); const yawQuat = new THREE.Quaternion(); const rollQuat = new THREE.Quaternion(); pitchQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.rotation.x); yawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y); rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), this.rotation.z); quaternion.multiply(rollQuat); quaternion.multiply(pitchQuat); quaternion.multiply(yawQuat); backward.applyQuaternion(quaternion); up.applyQuaternion(quaternion); const cameraPosition = this.position.clone() .add(backward.multiplyScalar(this.cameraDistance)) .add(up.multiplyScalar(this.cameraHeight)); return cameraPosition; } getCameraTarget() { return this.position.clone(); } // HUD ํ‘œ์‹œ์šฉ ํ—ค๋”ฉ getter (0~360๋„) getHeadingDegrees() { return this.normalizeHeading(this.rotation.y); } } // ํ”Œ๋ ˆ์–ด ํด๋ž˜์Šค class Flare { constructor(scene, position, direction, isLeftSide) { this.scene = scene; this.position = position.clone(); this.velocity = direction.clone().multiplyScalar(200); // 200m/s ์†๋„ // ์ขŒ์šฐ๋กœ ๋ถ„์‚ฐ const sideOffset = isLeftSide ? -1 : 1; const perpendicular = new THREE.Vector3(sideOffset * 50, -20, (Math.random() - 0.5) * 30); this.velocity.add(perpendicular); this.lifeTime = 5.0; // 5์ดˆ ์ง€์† this.mesh = null; this.createFlare(); // ์—ด ์ถ”์  ํŠน์„ฑ this.heatSignature = 1.0; // ์ดˆ๊ธฐ ์—ด ์‹ ํ˜ธ } createFlare() { // ํ”Œ๋ ˆ์–ด ๋ฉ”์ธ ๋ชธ์ฒด const group = new THREE.Group(); // ๋ฐ์€ ์ค‘์‹ฌ๋ถ€ const coreGeometry = new THREE.SphereGeometry(0.5, 6, 6); const coreMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 1.0 }); const core = new THREE.Mesh(coreGeometry, coreMaterial); group.add(core); // ์—ด ๊ด‘์› ํšจ๊ณผ const glowGeometry = new THREE.SphereGeometry(2, 8, 8); const glowMaterial = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.6 }); const glow = new THREE.Mesh(glowGeometry, glowMaterial); group.add(glow); // ์™ธ๋ถ€ ๊ด‘์› const outerGlowGeometry = new THREE.SphereGeometry(4, 8, 8); const outerGlowMaterial = new THREE.MeshBasicMaterial({ color: 0xff3300, transparent: true, opacity: 0.3 }); const outerGlow = new THREE.Mesh(outerGlowGeometry, outerGlowMaterial); group.add(outerGlow); this.mesh = group; this.mesh.position.copy(this.position); this.scene.add(this.mesh); // ์—ฐ๊ธฐ ํŠธ๋ ˆ์ผ this.smokeTrail = []; this.smokeEmitTime = 0; } update(deltaTime) { this.lifeTime -= deltaTime; if (this.lifeTime <= 0) { this.destroy(); return false; } // ์—ด ์‹ ํ˜ธ ๊ฐ์†Œ this.heatSignature = Math.max(0, this.heatSignature - deltaTime * 0.15); // ์ค‘๋ ฅ ํšจ๊ณผ this.velocity.y -= 50 * deltaTime; // ์ค‘๋ ฅ ๊ฐ€์†๋„ // ์œ„์น˜ ์—…๋ฐ์ดํŠธ this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); this.mesh.position.copy(this.position); // ๋ฐ๊ธฐ ๊ฐ์†Œ const fadeRatio = this.lifeTime / 5.0; this.mesh.children[0].material.opacity = fadeRatio; this.mesh.children[1].material.opacity = fadeRatio * 0.6; this.mesh.children[2].material.opacity = fadeRatio * 0.3; // ์—ฐ๊ธฐ ์ƒ์„ฑ this.smokeEmitTime += deltaTime; if (this.smokeEmitTime >= 0.05) { this.smokeEmitTime = 0; this.createSmoke(); } // ์—ฐ๊ธฐ ์—…๋ฐ์ดํŠธ for (let i = this.smokeTrail.length - 1; i >= 0; i--) { const smoke = this.smokeTrail[i]; smoke.life -= deltaTime; if (smoke.life <= 0) { this.scene.remove(smoke.mesh); this.smokeTrail.splice(i, 1); } else { smoke.mesh.scale.multiplyScalar(1.05); smoke.mesh.material.opacity = smoke.life / 2.0 * 0.5; } } // ์ง€๋ฉด ์ถฉ๋Œ if (this.position.y <= 0) { this.destroy(); return false; } return true; } createSmoke() { const smokeGeometry = new THREE.SphereGeometry(1 + Math.random() * 1, 6, 6); const smokeMaterial = new THREE.MeshBasicMaterial({ color: 0xcccccc, transparent: true, opacity: 0.5 }); const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial); smoke.position.copy(this.position); smoke.position.add(new THREE.Vector3( (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2 )); this.scene.add(smoke); this.smokeTrail.push({ mesh: smoke, life: 2.0 }); // ์—ฐ๊ธฐ ์ œํ•œ if (this.smokeTrail.length > 30) { const oldSmoke = this.smokeTrail.shift(); this.scene.remove(oldSmoke.mesh); } } destroy() { if (this.mesh) { this.scene.remove(this.mesh); } this.smokeTrail.forEach(smoke => { this.scene.remove(smoke.mesh); }); this.smokeTrail = []; } } // AIM-9 ๋ฏธ์‚ฌ์ผ ํด๋ž˜์Šค class AIM9Missile { constructor(scene, position, target, rotation) { this.scene = scene; this.position = position.clone(); this.target = target; this.rotation = rotation.clone(); this.speed = 1028.8; // 2000kt๋ฅผ m/s๋กœ ๋ณ€ํ™˜ this.mesh = null; this.isLoaded = false; this.lifeTime = 30; // 30์ดˆ๋กœ ์ฆ๊ฐ€ (๋” ์˜ค๋ž˜ ์ถ”์ ) this.turnRate = 6.0; // ํšŒ์ „์œจ 2๋ฐฐ ์ฆ๊ฐ€ (๋” ๋น ๋ฅธ ์ถ”์ ) this.startPosition = position.clone(); // ์ถ”๋ ฅ ์—ฐ๊ธฐ ํŒŒํ‹ฐํด this.smokeTrail = []; this.smokeEmitTime = 0; // ๋ฏธ์‚ฌ์ผ ๋ฐœ์‚ฌ ๋ฐฉํ–ฅ ์„ค์ • const quaternion = new THREE.Quaternion(); const pitchQuat = new THREE.Quaternion(); const yawQuat = new THREE.Quaternion(); const rollQuat = new THREE.Quaternion(); pitchQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.rotation.x); yawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y); rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), this.rotation.z); quaternion.multiply(rollQuat); quaternion.multiply(pitchQuat); quaternion.multiply(yawQuat); const initialDirection = new THREE.Vector3(0, 0, 1); initialDirection.applyQuaternion(quaternion); this.velocity = initialDirection.multiplyScalar(this.speed); // ๋ฏธ์‚ฌ์ผ ์†Œ๋ฆฌ this.swingAudio = null; this.swingAudioPlayed = false; // ํ•œ ๋ฒˆ๋งŒ ์žฌ์ƒํ•˜๊ธฐ ์œ„ํ•œ ํ”Œ๋ž˜๊ทธ this.initializeAudio(); this.createMissile(); } initializeAudio() { // missileswing.ogg๋Š” update ๋ฉ”์„œ๋“œ์—์„œ ํ•œ ๋ฒˆ๋งŒ ์žฌ์ƒ // ์—ฌ๊ธฐ์„œ๋Š” ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์Œ } createMissile() { // ์ฆ‰์‹œ ํด๋ฐฑ ๋ชจ๋ธ ์ƒ์„ฑ (๋น„๋™๊ธฐ ๋กœ๋”ฉ ๋Œ€์‹ ) this.createFallbackMissile(); this.mesh.position.copy(this.position); this.scene.add(this.mesh); // ๋‚˜์ค‘์— GLB ๋ชจ๋ธ ๋กœ๋“œ ์‹œ๋„ (์„ ํƒ์‚ฌํ•ญ) const loader = new GLTFLoader(); loader.load('models/aim-9.glb', (result) => { // ๋ชจ๋ธ ๋กœ๋“œ ์„ฑ๊ณต ์‹œ ๊ต์ฒด if (this.mesh && this.mesh.parent) { this.scene.remove(this.mesh); this.mesh = result.scene; this.mesh.scale.set(0.75, 0.75, 0.75); this.mesh.position.copy(this.position); this.scene.add(this.mesh); console.log('AIM-9 model loaded and replaced'); } }, undefined, (error) => { console.log('AIM-9 model not found, keeping fallback'); } ); } createFallbackMissile() { // ํด๋ฐฑ ๋ฏธ์‚ฌ์ผ ๋ชจ๋ธ const group = new THREE.Group(); // ๋ฏธ์‚ฌ์ผ ๋ณธ์ฒด const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.4, 4, 8); const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0x808080 }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.rotation.x = Math.PI / 2; group.add(body); // ํƒ„๋‘ const noseGeometry = new THREE.ConeGeometry(0.3, 1, 8); const noseMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 }); const nose = new THREE.Mesh(noseGeometry, noseMaterial); nose.position.z = 2.5; nose.rotation.x = -Math.PI / 2; group.add(nose); // ๋‚ ๊ฐœ const finGeometry = new THREE.BoxGeometry(2, 0.1, 0.5); const finMaterial = new THREE.MeshLambertMaterial({ color: 0x606060 }); for (let i = 0; i < 4; i++) { const fin = new THREE.Mesh(finGeometry, finMaterial); fin.position.z = -1.5; fin.rotation.z = (Math.PI / 2) * i; group.add(fin); } // ๋ถˆ๊ฝƒ ํšจ๊ณผ const flameGeometry = new THREE.ConeGeometry(0.4, 1.5, 8); const flameMaterial = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.8 }); const flame = new THREE.Mesh(flameGeometry, flameMaterial); flame.position.z = -2.5; flame.rotation.x = Math.PI / 2; group.add(flame); this.mesh = group; this.mesh.scale.set(3.375, 3.375, 3.375); // 50% ๋” ํฌ๊ฒŒ (1.5 -> 2.25) this.isLoaded = true; } update(deltaTime, playerPosition) { if (!this.mesh || !this.target || !this.target.position) { this.destroy(); return 'expired'; } // *** ๊ฐ•ํ™”๋œ ํ”Œ๋ ˆ์–ด ํƒ์ง€ ๋กœ์ง *** // ํ”Œ๋ ˆ์–ด ํƒ์ง€ ๋ฐ ๋ชฉํ‘œ ๋ณ€๊ฒฝ - 100% ์„ฑ๊ณต๋ฅ  if (window.gameInstance && !this.isTargetingFlare) { const nearestFlare = window.gameInstance.findNearestFlare(this.position); if (nearestFlare) { const flareDistance = this.position.distanceTo(nearestFlare.position); // ํƒ์ง€ ๊ฑฐ๋ฆฌ๋ฅผ 1000m๋กœ ํ™•๋Œ€ํ•˜๊ณ  ์—ด ์‹ ํ˜ธ ์กฐ๊ฑด ์ œ๊ฑฐ if (flareDistance < 1000) { // ํ”Œ๋ ˆ์–ด๋กœ ๋ชฉํ‘œ ๋ณ€๊ฒฝ - 100% ์„ฑ๊ณต console.log('Missile redirected to flare! Distance:', flareDistance); this.target = nearestFlare; this.isTargetingFlare = true; // ํ”Œ๋ ˆ์–ด ๊ธฐ๋งŒ ์„ฑ๊ณต ์‚ฌ์šด๋“œ ์žฌ์ƒ try { const evadeSound = new Audio('sounds/missileswing3.ogg'); evadeSound.volume = 0.6; evadeSound.play().catch(e => {}); } catch (e) {} } } } // ํ”Œ๋ ˆ์–ด๋ฅผ ์ถ”์  ์ค‘์ด๊ณ  ํ”Œ๋ ˆ์–ด๊ฐ€ ์†Œ๋ฉธํ–ˆ๋‹ค๋ฉด if (this.isTargetingFlare && (!this.target.mesh || this.target.lifeTime <= 0)) { // ์ง์„ ์œผ๋กœ ๊ณ„์† ๋น„ํ–‰ this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); this.mesh.position.copy(this.position); this.lifeTime -= deltaTime; if (this.lifeTime <= 0 || this.position.y <= 0) { this.destroy(); return 'expired'; } // ์—ฐ๊ธฐ๋Š” ๊ณ„์† ์ƒ์„ฑ this.smokeEmitTime += deltaTime; if (this.smokeEmitTime >= 0.02) { this.smokeEmitTime = 0; this.createSmokeParticle(); } // ์—ฐ๊ธฐ ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ for (let i = this.smokeTrail.length - 1; i >= 0; i--) { const smoke = this.smokeTrail[i]; smoke.life -= deltaTime; if (smoke.life <= 0) { this.scene.remove(smoke.mesh); this.smokeTrail.splice(i, 1); } else { smoke.mesh.scale.multiplyScalar(1.02); smoke.mesh.material.opacity = smoke.life / 3.0; smoke.mesh.position.y += deltaTime * 2; } } return 'flying'; } // *** ํ”Œ๋ ˆ์–ด ํƒ์ง€ ๋กœ์ง ์ถ”๊ฐ€ ๋ *** this.lifeTime -= deltaTime; if (this.lifeTime <= 0) { this.destroy(); return 'expired'; } // missileswing.ogg๋ฅผ ํ•œ ๋ฒˆ๋งŒ ์žฌ์ƒ if (!this.swingAudioPlayed && this.lifeTime < 29.5) { try { this.swingAudio = new Audio('sounds/missileswing.ogg'); this.swingAudio.volume = 0.5; this.swingAudio.play().catch(e => {}); this.swingAudioPlayed = true; } catch (e) { console.log('Missile swing audio failed:', e); } } // ํƒ€๊ฒŸ ์ถ”์  - ์™„๋ฒฝํ•œ ์ถ”์  ์•Œ๊ณ ๋ฆฌ์ฆ˜ const toTarget = this.target.position.clone().sub(this.position); const distance = toTarget.length(); // ๋ช…์ค‘ ์ฒดํฌ - ๋ฒ”์œ„ ์ฆ๊ฐ€ if (distance < 50) { // ๋ช…์ค‘! console.log('AIM-9 missile hit! Distance:', distance); this.onHit(); return 'hit'; } // ์™„๋ฒฝํ•œ ์ถ”์  - ํƒ€๊ฒŸ์˜ ํ˜„์žฌ ์œ„์น˜๋ฅผ ์ •ํ™•ํžˆ ์ถ”์  toTarget.normalize(); // ์ฆ‰๊ฐ์ ์ธ ๋ฐฉํ–ฅ ์ „ํ™˜ - ํšŒ์ „์œจ ์ œํ•œ ์—†์Œ const newDirection = toTarget.clone(); // ์†๋„ ์ฆ๊ฐ€ - ๋” ๋น ๋ฅธ ์ถ”์  if (this.lifeTime > 25) { // ์ดˆ๊ธฐ 5์ดˆ๊ฐ„ ๊ฐ€์† this.speed = Math.min(this.speed + deltaTime * 300, 2057.6); // ์ตœ๋Œ€ 4000kt } else { // ์ดํ›„์—๋„ ๋†’์€ ์†๋„ ์œ ์ง€ this.speed = Math.min(this.speed + deltaTime * 100, 2057.6); } // ํƒ€๊ฒŸ๊ณผ์˜ ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ผ ์†๋„ ์ถ”๊ฐ€ ์ฆ๊ฐ€ if (distance < 1000) { // ๊ทผ๊ฑฐ๋ฆฌ์—์„œ ๋”์šฑ ๋น ๋ฅด๊ฒŒ this.speed = Math.min(this.speed * 1.2, 2572); // ์ตœ๋Œ€ 5000kt } this.velocity = newDirection.multiplyScalar(this.speed); // ์œ„์น˜ ์—…๋ฐ์ดํŠธ this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); this.mesh.position.copy(this.position); // ๋ฏธ์‚ฌ์ผ ํšŒ์ „ - ์ •ํ™•ํžˆ ํƒ€๊ฒŸ์„ ํ–ฅํ•จ this.mesh.lookAt(this.target.position); // ์ถ”๋ ฅ ์—ฐ๊ธฐ ์ƒ์„ฑ this.smokeEmitTime += deltaTime; if (this.smokeEmitTime >= 0.02) { this.smokeEmitTime = 0; this.createSmokeParticle(); } // ์—ฐ๊ธฐ ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ for (let i = this.smokeTrail.length - 1; i >= 0; i--) { const smoke = this.smokeTrail[i]; smoke.life -= deltaTime; if (smoke.life <= 0) { this.scene.remove(smoke.mesh); this.smokeTrail.splice(i, 1); } else { // ์—ฐ๊ธฐ ํ™•์‚ฐ ๋ฐ ํŽ˜์ด๋“œ smoke.mesh.scale.multiplyScalar(1.02); smoke.mesh.material.opacity = smoke.life / 3.0; // ์•ฝ๊ฐ„์˜ ์ƒ์Šน ํšจ๊ณผ smoke.mesh.position.y += deltaTime * 2; } } // ์ง€๋ฉด ์ถฉ๋Œ if (this.position.y <= 0) { this.destroy(); return 'expired'; } if (this.target.position.y <= 50 && distance < 200) { // ํƒ€๊ฒŸ์ด ์ง€๋ฉด ๊ทผ์ฒ˜์— ์žˆ๊ณ  ๊ฑฐ๋ฆฌ๊ฐ€ ๊ฐ€๊นŒ์šฐ๋ฉด ์ฆ‰์‹œ ๋ช…์ค‘ console.log('AIM-9 missile ground proximity hit!'); this.onHit(); return 'hit'; } return 'flying'; } createSmokeParticle() { // ๋ฏธ์‚ฌ์ผ ๋’ค์ชฝ ์œ„์น˜ ๊ณ„์‚ฐ const backward = this.velocity.clone().normalize().multiplyScalar(-3); const smokePos = this.position.clone().add(backward); // ํ•˜์–€ ์—ฐ๊ธฐ ํŒŒํ‹ฐํด ์ƒ์„ฑ const smokeGeometry = new THREE.SphereGeometry( 1 + Math.random() * 3.5, // ํฌ๊ธฐ ๋ณ€ํ™” 6, 6 ); const smokeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, // ํ•˜์–€์ƒ‰ transparent: true, opacity: 0.8 }); const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial); smoke.position.copy(smokePos); // ์•ฝ๊ฐ„์˜ ๋žœ๋ค ์˜คํ”„์…‹ smoke.position.x += (Math.random() - 0.5) * 1; smoke.position.y += (Math.random() - 0.5) * 1; smoke.position.z += (Math.random() - 0.5) * 1; this.scene.add(smoke); this.smokeTrail.push({ mesh: smoke, life: 3.0 // 3์ดˆ ๋™์•ˆ ์ง€์† }); // ์—ฐ๊ธฐ ํŒŒํ‹ฐํด ์ œํ•œ (์„ฑ๋Šฅ์„ ์œ„ํ•ด) if (this.smokeTrail.length > 150) { const oldSmoke = this.smokeTrail.shift(); this.scene.remove(oldSmoke.mesh); } } onHit() { // ๋ช…์ค‘ ํšจ๊ณผ if (window.gameInstance) { window.gameInstance.createExplosionEffect(this.position); } // ๋ช…์ค‘์Œ try { const hitSound = new Audio('sounds/missilehit.ogg'); hitSound.volume = 0.8; hitSound.play().catch(e => {}); } catch (e) { console.log('Missile hit sound failed:', e); } // ํƒ€๊ฒŸ์—๊ฒŒ ํ”ผํ•ด - ํ™•์‹คํ•˜๊ฒŒ ํ”ผํ•ด๋ฅผ ์ž…ํžˆ๊ณ  ํŒŒ๊ดด ์ฒ˜๋ฆฌ if (this.target && this.target.takeDamage) { console.log('AIM-9 hitting target, damage:', GAME_CONSTANTS.AIM9_DAMAGE); const isDead = this.target.takeDamage(GAME_CONSTANTS.AIM9_DAMAGE); console.log('Target destroyed:', isDead); // ํƒ€๊ฒŸ์ด ํŒŒ๊ดด๋˜์—ˆ๋‹ค๋ฉด ๊ฒŒ์ž„์—์„œ ์ œ๊ฑฐ if (isDead && window.gameInstance) { // ์  ๋ฐฐ์—ด์—์„œ ์ฐพ์•„์„œ ์ œ๊ฑฐ const enemyIndex = window.gameInstance.enemies.indexOf(this.target); if (enemyIndex !== -1) { console.log('Removing destroyed enemy from game'); this.target.destroy(); window.gameInstance.enemies.splice(enemyIndex, 1); window.gameInstance.score += 100; // ์  ์ฒ˜์น˜ ์Œ์„ฑ ์žฌ์ƒ (์ถ”๊ฐ€) try { const killVoice = new Audio('sounds/voice_kill_a.ogg'); killVoice.volume = 0.8; killVoice.play().catch(e => { console.log('Kill voice failed to play:', e); }); } catch (e) { console.log('Kill voice error:', e); } // ์ž๋ง‰ ํ‘œ์‹œ window.gameInstance.showSubtitle('Nice work, one enemy down. Keep going.', 4000); } } } this.destroy(); } destroy() { if (this.mesh) { this.scene.remove(this.mesh); } // ์—ฐ๊ธฐ ํŒŒํ‹ฐํด ์ •๋ฆฌ this.smokeTrail.forEach(smoke => { if (smoke.mesh) { this.scene.remove(smoke.mesh); } }); this.smokeTrail = []; if (this.swingAudio) { this.swingAudio.pause(); this.swingAudio = null; } } } // ์ ๊ธฐ์šฉ AIM-9 ๋ฏธ์‚ฌ์ผ ํด๋ž˜์Šค (์—ฌ๊ธฐ์— ์ถ”๊ฐ€!) class EnemyAIM9Missile { constructor(scene, position, target, rotation) { this.scene = scene; this.position = position.clone(); this.target = target; this.rotation = rotation.clone(); this.speed = 1028.8; // 2000kt๋ฅผ m/s๋กœ ๋ณ€ํ™˜ this.mesh = null; this.isLoaded = false; this.lifeTime = 20; // 20์ดˆ this.turnRate = 4.0; // ํšŒ์ „์œจ this.startPosition = position.clone(); // ์ถ”๋ ฅ ์—ฐ๊ธฐ ํŒŒํ‹ฐํด this.smokeTrail = []; this.smokeEmitTime = 0; // ๋ฏธ์‚ฌ์ผ ๋ฐœ์‚ฌ ๋ฐฉํ–ฅ ์„ค์ • const quaternion = new THREE.Quaternion(); const pitchQuat = new THREE.Quaternion(); const yawQuat = new THREE.Quaternion(); const rollQuat = new THREE.Quaternion(); pitchQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), this.rotation.x); yawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y); rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), this.rotation.z); quaternion.multiply(rollQuat); quaternion.multiply(pitchQuat); quaternion.multiply(yawQuat); const initialDirection = new THREE.Vector3(0, 0, 1); initialDirection.applyQuaternion(quaternion); this.velocity = initialDirection.multiplyScalar(this.speed); this.createMissile(); } createMissile() { // ํด๋ฐฑ ๋ฏธ์‚ฌ์ผ ๋ชจ๋ธ const group = new THREE.Group(); // ๋ฏธ์‚ฌ์ผ ๋ณธ์ฒด const bodyGeometry = new THREE.CylinderGeometry(0.3, 0.4, 4, 8); const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0x808080 }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.rotation.x = Math.PI / 2; group.add(body); // ํƒ„๋‘ const noseGeometry = new THREE.ConeGeometry(0.3, 1, 8); const noseMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 }); const nose = new THREE.Mesh(noseGeometry, noseMaterial); nose.position.z = 2.5; nose.rotation.x = -Math.PI / 2; group.add(nose); // ๋‚ ๊ฐœ const finGeometry = new THREE.BoxGeometry(2, 0.1, 0.5); const finMaterial = new THREE.MeshLambertMaterial({ color: 0x606060 }); for (let i = 0; i < 4; i++) { const fin = new THREE.Mesh(finGeometry, finMaterial); fin.position.z = -1.5; fin.rotation.z = (Math.PI / 2) * i; group.add(fin); } // ๋ถˆ๊ฝƒ ํšจ๊ณผ const flameGeometry = new THREE.ConeGeometry(0.4, 1.5, 8); const flameMaterial = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.8 }); const flame = new THREE.Mesh(flameGeometry, flameMaterial); flame.position.z = -2.5; flame.rotation.x = Math.PI / 2; group.add(flame); this.mesh = group; this.mesh.scale.set(3.375, 3.375, 3.375); this.mesh.position.copy(this.position); this.scene.add(this.mesh); this.isLoaded = true; } update(deltaTime, enemyPosition) { if (!this.mesh || !this.target || !this.target.position) { this.destroy(); return 'expired'; } // *** ๊ฐ•ํ™”๋œ ํ”Œ๋ ˆ์–ด ํƒ์ง€ ๋กœ์ง *** if (window.gameInstance && !this.isTargetingFlare) { const nearestFlare = window.gameInstance.findNearestFlare(this.position); if (nearestFlare) { const flareDistance = this.position.distanceTo(nearestFlare.position); if (flareDistance < 1000) { console.log('Enemy missile redirected to flare!'); this.target = nearestFlare; this.isTargetingFlare = true; } } } this.lifeTime -= deltaTime; if (this.lifeTime <= 0) { this.destroy(); return 'expired'; } // ํƒ€๊ฒŸ ์ถ”์  const toTarget = this.target.position.clone().sub(this.position); const distance = toTarget.length(); // ๋ช…์ค‘ ์ฒดํฌ if (distance < 50) { // ๋ช…์ค‘! console.log('Enemy AIM-9 missile hit player!'); this.onHit(); return 'hit'; } // ์ถ”์  ์•Œ๊ณ ๋ฆฌ์ฆ˜ toTarget.normalize(); // ํ˜„์žฌ ๋ฐฉํ–ฅ const currentDirection = this.velocity.clone().normalize(); // ์ƒˆ๋กœ์šด ๋ฐฉํ–ฅ์œผ๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ „ํ™˜ const turnSpeed = this.turnRate * deltaTime; const newDirection = currentDirection.lerp(toTarget, turnSpeed); newDirection.normalize(); // ์†๋„ ์—…๋ฐ์ดํŠธ this.velocity = newDirection.multiplyScalar(this.speed); // ์œ„์น˜ ์—…๋ฐ์ดํŠธ this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); this.mesh.position.copy(this.position); // ๋ฏธ์‚ฌ์ผ ํšŒ์ „ this.mesh.lookAt(this.target.position); // ์ถ”๋ ฅ ์—ฐ๊ธฐ ์ƒ์„ฑ this.smokeEmitTime += deltaTime; if (this.smokeEmitTime >= 0.02) { this.smokeEmitTime = 0; this.createSmokeParticle(); } // ์—ฐ๊ธฐ ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ for (let i = this.smokeTrail.length - 1; i >= 0; i--) { const smoke = this.smokeTrail[i]; smoke.life -= deltaTime; if (smoke.life <= 0) { this.scene.remove(smoke.mesh); this.smokeTrail.splice(i, 1); } else { // ์—ฐ๊ธฐ ํ™•์‚ฐ ๋ฐ ํŽ˜์ด๋“œ smoke.mesh.scale.multiplyScalar(1.02); smoke.mesh.material.opacity = smoke.life / 3.0; // ์•ฝ๊ฐ„์˜ ์ƒ์Šน ํšจ๊ณผ smoke.mesh.position.y += deltaTime * 2; } } // ์ง€๋ฉด ์ถฉ๋Œ if (this.position.y <= 0) { this.destroy(); return 'expired'; } return 'flying'; } createSmokeParticle() { // ๋ฏธ์‚ฌ์ผ ๋’ค์ชฝ ์œ„์น˜ ๊ณ„์‚ฐ const backward = this.velocity.clone().normalize().multiplyScalar(-3); const smokePos = this.position.clone().add(backward); // ํ•˜์–€ ์—ฐ๊ธฐ ํŒŒํ‹ฐํด ์ƒ์„ฑ const smokeGeometry = new THREE.SphereGeometry( 1 + Math.random() * 3.5, 6, 6 ); const smokeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 }); const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial); smoke.position.copy(smokePos); // ์•ฝ๊ฐ„์˜ ๋žœ๋ค ์˜คํ”„์…‹ smoke.position.x += (Math.random() - 0.5) * 1; smoke.position.y += (Math.random() - 0.5) * 1; smoke.position.z += (Math.random() - 0.5) * 1; this.scene.add(smoke); this.smokeTrail.push({ mesh: smoke, life: 3.0 }); // ์—ฐ๊ธฐ ํŒŒํ‹ฐํด ์ œํ•œ if (this.smokeTrail.length > 150) { const oldSmoke = this.smokeTrail.shift(); this.scene.remove(oldSmoke.mesh); } } onHit() { // ๋ช…์ค‘ ํšจ๊ณผ if (window.gameInstance) { window.gameInstance.createExplosionEffect(this.position); } // ํ”Œ๋ ˆ์ด์–ด์—๊ฒŒ ํ”ผํ•ด if (this.target && this.target.takeDamage) { this.target.takeDamage(GAME_CONSTANTS.AIM9_DAMAGE); } this.destroy(); } destroy() { if (this.mesh) { this.scene.remove(this.mesh); } // ์—ฐ๊ธฐ ํŒŒํ‹ฐํด ์ •๋ฆฌ this.smokeTrail.forEach(smoke => { if (smoke.mesh) { this.scene.remove(smoke.mesh); } }); this.smokeTrail = []; } } // ์  ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค - ์™„์ „ํžˆ ์žฌ์„ค๊ณ„ // ์  ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค - ์™„์ „ํžˆ ์žฌ์„ค๊ณ„ class EnemyFighter { constructor(scene, position) { this.mesh = null; this.isLoaded = false; this.scene = scene; this.position = position.clone(); this.rotation = new THREE.Euler(0, Math.random() * Math.PI * 2, 0); // ๋ฌผ๋ฆฌ ์†์„ฑ this.speed = 600; // 600kt๋กœ ์‹œ์ž‘ (500-750kt ๋ฒ”์œ„) this.velocity = new THREE.Vector3(0, 0, 0); this.health = GAME_CONSTANTS.MAX_HEALTH; this.minSpeed = 257; // 500kt in m/s this.maxSpeed = 386; // 750kt in m/s // AI ์ƒํƒœ this.aiState = 'patrol'; // patrol, combat, evade, retreat this.targetPosition = null; this.playerFighter = null; // ํšŒํ”ผ ์‹œ์Šคํ…œ this.temporaryEvadeMode = false; this.evadeTimer = 0; // ์ถฉ๋Œ ์˜ˆ์ธก this.predictedPosition = new THREE.Vector3(); // ์ „ํˆฌ ์‹œ์Šคํ…œ this.bullets = []; this.burstCounter = 0; // ํ˜„์žฌ ์—ฐ๋ฐœ ์นด์šดํ„ฐ this.lastShootTime = 0; this.canShoot = false; // ๋ฐœ์‚ฌ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ // ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „์„ ์œ„ํ•œ ๋ณ€์ˆ˜ this.targetRotation = this.rotation.clone(); this.turnSpeed = 1.5; // ์ดˆ๋‹น ํšŒ์ „ ์†๋„ (๋ผ๋””์•ˆ) // ์ถฉ๋Œ ํšŒํ”ผ this.nearbyEnemies = []; this.avoidanceVector = new THREE.Vector3(); // AIM-9 ๋ฏธ์‚ฌ์ผ ์‹œ์Šคํ…œ this.aim9Missiles = GAME_CONSTANTS.ENEMY_AIM9_COUNT; this.firedMissiles = []; this.lockTarget = null; this.lockProgress = 0; this.lastLockTime = 0; this.lastMissileFireTime = 0; this.isLocking = false; // ํ”Œ๋ ˆ์–ด ์‹œ์Šคํ…œ this.flareCount = 1; // ํ”Œ๋ ˆ์–ด 1ํšŒ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ this.deployedFlares = []; this.lastFlareTime = 0; this.hasUsedFlare = false; // ์ดˆ๊ธฐ ๋ชฉํ‘œ ์„ค์ • this.selectNewPatrolTarget(); } 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); this.mesh.rotation.y = Math.PI / 4; // ํ”Œ๋ ˆ์ด์–ด์™€ ๋™์ผํ•œ ์ดˆ๊ธฐ ํšŒ์ „๊ฐ’ 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; } update(playerPosition, deltaTime) { if (!this.mesh || !this.isLoaded) return; // ํ”Œ๋ ˆ์–ด ์—…๋ฐ์ดํŠธ ์ถ”๊ฐ€ for (let i = this.deployedFlares.length - 1; i >= 0; i--) { const flare = this.deployedFlares[i]; if (!flare.update(deltaTime)) { this.deployedFlares.splice(i, 1); } } // ๋ฏธ์‚ฌ์ผ ์œ„ํ˜‘ ๊ฐ์ง€ ๋ฐ ํ”Œ๋ ˆ์–ด ์‚ฌ์šฉ if (!this.hasUsedFlare && this.flareCount > 0 && window.gameInstance) { const incomingMissiles = window.gameInstance.findIncomingMissilesForEnemy(this); if (incomingMissiles.length > 0) { const nearestMissile = incomingMissiles[0]; const missileDistance = this.position.distanceTo(nearestMissile.position); if (missileDistance < 500) { this.deployFlares(); this.hasUsedFlare = true; } } } // ํšŒํ”ผ ํƒ€์ด๋จธ ์—…๋ฐ์ดํŠธ (๋ฏธ์‚ฌ์ผ ํšŒํ”ผ ์ œ์™ธ) if (this.temporaryEvadeMode && this.evadeTimer > 0) { this.evadeTimer -= deltaTime; if (this.evadeTimer <= 0) { this.temporaryEvadeMode = false; } } const distanceToPlayer = this.position.distanceTo(playerPosition); // ์ƒํƒœ ๊ฒฐ์ • (๋ฏธ์‚ฌ์ผ ํƒ์ง€ ์ œ๊ฑฐ) if (this.temporaryEvadeMode) { this.aiState = 'evade'; } else if (distanceToPlayer <= 3000) { this.aiState = 'combat'; } else { this.aiState = 'patrol'; } // ์ถฉ๋Œ ํšŒํ”ผ ๊ณ„์‚ฐ (๋‹ค๋ฅธ ์ ๊ธฐ์™€์˜ ์ถฉ๋Œ๋งŒ) this.calculateAvoidance(); this.checkCollisionPrediction(deltaTime); // AI ํ–‰๋™ ์‹คํ–‰ switch (this.aiState) { case 'patrol': this.executePatrol(deltaTime); break; case 'combat': this.executeCombat(playerPosition, deltaTime); break; case 'evade': this.executeEmergencyEvade(deltaTime); break; } // ๋ฌผ๋ฆฌ ์—…๋ฐ์ดํŠธ this.updatePhysics(deltaTime); // ํƒ„ํ™˜ ์—…๋ฐ์ดํŠธ this.updateBullets(deltaTime); // ๋ฏธ์‚ฌ์ผ ์—…๋ฐ์ดํŠธ this.updateMissiles(deltaTime); // ๋ฝ์˜จ ์‹œ์Šคํ…œ ์—…๋ฐ์ดํŠธ if (this.playerFighter) { this.updateLockOn(deltaTime); } } executePatrol(deltaTime) { // ๋ชฉํ‘œ ์ง€์ ๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ ํ™•์ธ if (!this.targetPosition || this.position.distanceTo(this.targetPosition) < 500) { this.selectNewPatrolTarget(); } // ๋ชฉํ‘œ๋ฅผ ํ–ฅํ•ด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ํšŒ์ „ this.smoothTurnToTarget(this.targetPosition, deltaTime); // ์†๋„ ์œ ์ง€ (500-750kt) - ํ•ญ์ƒ ์ตœ์†Œ ์†๋„ ์ด์ƒ ์œ ์ง€ if (this.speed < this.minSpeed) { this.speed = this.minSpeed; } this.speed = THREE.MathUtils.clamp(this.speed, this.minSpeed, this.maxSpeed); } executeCombat(playerPosition, deltaTime) { const distance = this.position.distanceTo(playerPosition); // ํ”Œ๋ ˆ์ด์–ด๋ฅผ ํ–ฅํ•ด ํšŒ์ „ this.smoothTurnToTarget(playerPosition, deltaTime); // ์กฐ์ค€ ์ •ํ™•๋„ ํ™•์ธ const aimAccuracy = this.calculateAimAccuracy(playerPosition); // ์ง์ง„ ์ƒํƒœ ํ™•์ธ (ํ”ผ์น˜์™€ ๋กค์ด ๊ฑฐ์˜ 0์— ๊ฐ€๊นŒ์šด์ง€) const isPitchLevel = Math.abs(this.rotation.x) < 0.1; // ์•ฝ 5.7๋„ ์ด๋‚ด const isRollLevel = Math.abs(this.rotation.z) < 0.1; // ์•ฝ 5.7๋„ ์ด๋‚ด const isStraightFlying = isPitchLevel && isRollLevel; // ์ง์ง„ ๋น„ํ–‰ ์ค‘์ด๊ณ  ์ •ํ™•ํ•œ ์กฐ์ค€ ์‹œ์—๋งŒ ๋ฐœ์‚ฌ ๊ฐ€๋Šฅ this.canShoot = isStraightFlying && aimAccuracy < 0.15; // ๋ฐœ์‚ฌ ์กฐ๊ฑด ์ถฉ์กฑ ์‹œ ๋ฐœ์‚ฌ if (this.canShoot) { this.fireWeapon(); } else { // ๋ฐœ์‚ฌํ•  ์ˆ˜ ์—†์œผ๋ฉด ์—ฐ๋ฐœ ์นด์šดํ„ฐ ๋ฆฌ์…‹ this.burstCounter = 0; } // ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ผ ๊ธฐ๋™ ๊ฒฐ์ • if (distance < 150) { // 150m ์ด๋‚ด๋ฉด ํšŒํ”ผ ๊ธฐ๋™ ์‹œ์ž‘ this.selectEvadeTarget(); // ์ผ์‹œ์  ํšŒํ”ผ ๋ชจ๋“œ ํ™œ์„ฑํ™” this.temporaryEvadeMode = true; this.evadeTimer = 1.0; } else if (distance < 500) { // 500m ์ด๋‚ด๋ฉด ์ธก๋ฉด ํšŒํ”ผ this.selectEvadeTarget(); } else if (distance > 2000) { // ๋ฉ€๋ฉด ์ ‘๊ทผ this.targetPosition = playerPosition.clone(); } } // ์ƒˆ๋กœ์šด ๋ฉ”์„œ๋“œ: ์ถฉ๋Œ ์˜ˆ์ธก checkCollisionPrediction(deltaTime) { if (!this.nearbyEnemies) return; // 2์ดˆ ํ›„ ์˜ˆ์ƒ ์œ„์น˜ ๊ณ„์‚ฐ const predictTime = 2.0; const forward = new THREE.Vector3(0, 0, 1).applyEuler(this.rotation); this.predictedPosition.copy(this.position).add(forward.multiplyScalar(this.speed * predictTime)); this.nearbyEnemies.forEach(enemy => { if (enemy === this || !enemy.position) return; // ๋‹ค๋ฅธ ์ ๊ธฐ์˜ ์˜ˆ์ƒ ์œ„์น˜ const enemyForward = new THREE.Vector3(0, 0, 1).applyEuler(enemy.rotation); const enemyPredicted = enemy.position.clone().add(enemyForward.multiplyScalar(enemy.speed * predictTime)); // ์˜ˆ์ƒ ๊ฑฐ๋ฆฌ const predictedDistance = this.predictedPosition.distanceTo(enemyPredicted); // 150m ์ด๋‚ด๋กœ ์ ‘๊ทผ ์˜ˆ์ƒ ์‹œ ์‚ฌ์ „ ํšŒํ”ผ if (predictedDistance < 150) { // ์˜ˆ๋ฐฉ์  ํšŒํ”ผ ๋ฐฉํ–ฅ ์„ค์ • const avoidDir = this.predictedPosition.clone().sub(enemyPredicted).normalize(); // ์ˆ˜์ง ๋ถ„๋ฆฌ ์ถ”๊ฐ€ if (this.position.y > enemy.position.y) { avoidDir.y += 0.3; } else { avoidDir.y -= 0.3; } this.avoidanceVector.add(avoidDir.multiplyScalar(1.5)); } }); } // ์ƒˆ๋กœ์šด ๋ฉ”์„œ๋“œ: ๊ธด๊ธ‰ ํšŒํ”ผ ์‹คํ–‰ executeEmergencyEvade(deltaTime) { // ํšŒํ”ผ ๋ฒกํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ๋ฐฉํ–ฅ์œผ๋กœ, ์—†์œผ๋ฉด ๊ธ‰์ƒ์Šน if (this.avoidanceVector.length() > 0) { const evadeDirection = this.avoidanceVector.clone().normalize(); // ๊ธ‰์„ ํšŒ const targetYaw = Math.atan2(evadeDirection.x, evadeDirection.z); const targetPitch = Math.asin(-evadeDirection.y) * 0.5; // ํ”ผ์น˜๋Š” ์ ˆ๋ฐ˜๋งŒ // ๋น ๋ฅธ ํšŒ์ „ ์†๋„ const emergencyTurnSpeed = this.turnSpeed * 2.0; const maxTurnRate = emergencyTurnSpeed * deltaTime; // Yaw ํšŒ์ „ const yawDiff = this.normalizeAngle(targetYaw - this.rotation.y); if (Math.abs(yawDiff) > maxTurnRate) { this.rotation.y += Math.sign(yawDiff) * maxTurnRate; } else { this.rotation.y = targetYaw; } // Pitch ํšŒ์ „ const pitchDiff = targetPitch - this.rotation.x; if (Math.abs(pitchDiff) > maxTurnRate * 0.7) { this.rotation.x += Math.sign(pitchDiff) * maxTurnRate * 0.7; } else { this.rotation.x = targetPitch; } // ๊ธ‰์„ ํšŒ ์‹œ ์•ฝ๊ฐ„์˜ ๋กค ์ถ”๊ฐ€ this.rotation.z = Math.sign(yawDiff) * Math.min(Math.abs(yawDiff), Math.PI / 6); } else { // ๊ธฐ๋ณธ ํšŒํ”ผ: ๊ธ‰์ƒ์Šน this.rotation.x = -Math.PI / 6; // 30๋„ ์ƒ์Šน } // ํšŒํ”ผ ์ค‘์—๋Š” ์ตœ๋Œ€ ์†๋„ this.speed = this.maxSpeed; } smoothTurnToTarget(targetPos, deltaTime, isEmergency = false) { // ๊ธฐ์กด ๋กœ์ง (ํ›„ํ‡ด๊ฐ€ ์•„๋‹ ๋•Œ) const direction = targetPos.clone().sub(this.position); direction.y *= 0.5; direction.normalize(); // ์ถฉ๋Œ ํšŒํ”ผ ๋ฒกํ„ฐ ์ ์šฉ (ํšŒํ”ผ๊ฐ€ ์šฐ์„ ) if (this.avoidanceVector.length() > 0) { const avoidanceStrength = this.temporaryEvadeMode ? 1.0 : 0.5; direction.add(this.avoidanceVector.multiplyScalar(avoidanceStrength)); direction.normalize(); } // ๋ชฉํ‘œ ํšŒ์ „ ๊ณ„์‚ฐ const targetYaw = Math.atan2(direction.x, direction.z); const targetPitch = Math.asin(-direction.y); // ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „ (์ตœ๋Œ€ ํšŒ์ „ ์†๋„ ์ œํ•œ) const turnSpeed = isEmergency ? this.turnSpeed * 2.0 : (this.temporaryEvadeMode ? this.turnSpeed * 1.5 : this.turnSpeed); const maxTurnRate = turnSpeed * deltaTime; // Yaw ํšŒ์ „ const yawDiff = this.normalizeAngle(targetYaw - this.rotation.y); if (Math.abs(yawDiff) > maxTurnRate) { this.rotation.y += Math.sign(yawDiff) * maxTurnRate; } else { this.rotation.y = targetYaw; } // Pitch ํšŒ์ „ (์ œํ•œ์ ) const maxPitchRate = maxTurnRate * 0.7; if (Math.abs(targetPitch - this.rotation.x) > maxPitchRate) { this.rotation.x += Math.sign(targetPitch - this.rotation.x) * maxPitchRate; } else { this.rotation.x = targetPitch; } // Pitch ์ œํ•œ (ยฑ40๋„) const maxPitchAngle = Math.PI * 40 / 180; this.rotation.x = THREE.MathUtils.clamp(this.rotation.x, -maxPitchAngle, maxPitchAngle); // ๋กค ์ž๋™ ๊ณ„์‚ฐ (์„ ํšŒ ์‹œ) if (!this.temporaryEvadeMode) { this.rotation.z = -yawDiff * 0.5; this.rotation.z = THREE.MathUtils.clamp(this.rotation.z, -Math.PI / 4, Math.PI / 4); } } calculateAvoidance() { this.avoidanceVector.set(0, 0, 0); if (!this.nearbyEnemies) return; let avoidCount = 0; let criticalAvoidance = false; this.nearbyEnemies.forEach(enemy => { if (enemy === this || !enemy.position) return; const distance = this.position.distanceTo(enemy.position); // 100m ๋ฏธ๋งŒ: ๊ธด๊ธ‰ ํšŒํ”ผ if (distance < 100 && distance > 0) { criticalAvoidance = true; // ๊ฐ•ํ•œ ๋ฐ˜๋ฐœ๋ ฅ const avoidDir = this.position.clone().sub(enemy.position).normalize(); const strength = 2.0; // ๋งค์šฐ ๊ฐ•ํ•œ ํšŒํ”ผ this.avoidanceVector.add(avoidDir.multiplyScalar(strength)); // ๊ณ ๋„ ์ฐจ์ด ์ถ”๊ฐ€ (์œ„/์•„๋ž˜๋กœ ๋ถ„์‚ฐ) if (this.position.y > enemy.position.y) { this.avoidanceVector.y += 0.5; // ์œ„๋กœ } else { this.avoidanceVector.y -= 0.5; // ์•„๋ž˜๋กœ } avoidCount++; } // 100-300m: ์˜ˆ๋ฐฉ์  ํšŒํ”ผ else if (distance < 300) { // ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์œผ๋กœ ํšŒํ”ผ const avoidDir = this.position.clone().sub(enemy.position).normalize(); const strength = (300 - distance) / 200; // 100-300m ๋ฒ”์œ„์—์„œ ๊ฐ•๋„ ๊ณ„์‚ฐ this.avoidanceVector.add(avoidDir.multiplyScalar(strength)); avoidCount++; } }); if (avoidCount > 0) { this.avoidanceVector.divideScalar(avoidCount); this.avoidanceVector.normalize(); // ๊ธด๊ธ‰ ํšŒํ”ผ ์‹œ ๋” ๊ฐ•ํ•œ ํšŒํ”ผ๋ ฅ ์ ์šฉ if (criticalAvoidance) { this.avoidanceVector.multiplyScalar(2.0); // ์ผ์‹œ์ ์œผ๋กœ ์ „ํˆฌ ์ƒํƒœ ํ•ด์ œ this.temporaryEvadeMode = true; this.evadeTimer = 2.0; // 2์ดˆ ๋™์•ˆ ํšŒํ”ผ ์šฐ์„  } } } updatePhysics(deltaTime) { if (!this.mesh) return; // ์†๋„ ๋ฒกํ„ฐ ๊ณ„์‚ฐ (ํ•ญ์ƒ ์ „์ง„) const forward = new THREE.Vector3(0, 0, 1); forward.applyEuler(this.rotation); // ์†๋„ ์œ ์ง€ (500-750kt, m/s๋กœ ๋ณ€ํ™˜) - ๊ฐ•์ œ๋กœ ์ตœ์†Œ ์†๋„ ์œ ์ง€ if (this.speed < this.minSpeed) { this.speed = this.minSpeed; } this.speed = THREE.MathUtils.clamp(this.speed, this.minSpeed, this.maxSpeed); // ์†๋„ ๋ฒกํ„ฐ ์ƒ์„ฑ - ํ•ญ์ƒ ์ „์ง„ this.velocity = forward.multiplyScalar(this.speed); // ์œ„์น˜ ์—…๋ฐ์ดํŠธ - ํ•ญ์ƒ ์ด๋™ this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); // ๊ณ ๋„ ์ œํ•œ if (this.position.y < 500) { this.position.y = 500; this.rotation.x = -0.2; // ์ƒ์Šน } else if (this.position.y > 8000) { this.position.y = 8000; this.rotation.x = 0.2; // ํ•˜๊ฐ• } // ๋งต ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2; const boundaryBuffer = mapLimit * 0.9; if (Math.abs(this.position.x) > boundaryBuffer || Math.abs(this.position.z) > boundaryBuffer) { // ๋งต ์ค‘์•™์„ ํ–ฅํ•ด ํšŒ์ „ const centerDirection = new THREE.Vector3(-this.position.x, 0, -this.position.z).normalize(); const targetYaw = Math.atan2(centerDirection.x, centerDirection.z); this.rotation.y = targetYaw; this.selectNewPatrolTarget(); // ์ƒˆ๋กœ์šด ๋ชฉํ‘œ ์„ ํƒ } // ํ•˜๋“œ ๋ฆฌ๋ฏธํŠธ this.position.x = THREE.MathUtils.clamp(this.position.x, -mapLimit, mapLimit); this.position.z = THREE.MathUtils.clamp(this.position.z, -mapLimit, 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; // ํ”Œ๋ ˆ์ด์–ด์™€ ๋™์ผํ•œ ํšŒ์ „ ์˜คํ”„์…‹ this.mesh.rotation.z = this.rotation.z; } fireWeapon() { const now = Date.now(); // 0.1์ดˆ์— 1๋ฐœ์”ฉ, 10๋ฐœ ์—ฐ๋ฐœ if (now - this.lastShootTime >= 100) { // ๋ฐœ์‚ฌ ์ง์ „์— ๋‹ค์‹œ ํ•œ๋ฒˆ ์ง์ง„ ์ƒํƒœ ํ™•์ธ const isPitchLevel = Math.abs(this.rotation.x) < 0.1; const isRollLevel = Math.abs(this.rotation.z) < 0.1; if (isPitchLevel && isRollLevel) { this.shoot(); this.lastShootTime = now; this.burstCounter++; // 10๋ฐœ ๋ฐœ์‚ฌ ์™„๋ฃŒ ์‹œ ์ž ์‹œ ํœด์‹ if (this.burstCounter >= 10) { this.burstCounter = 0; this.lastShootTime = now + 1000; // 1์ดˆ ๋Œ€๊ธฐ } } } } shoot() { // ํƒ„ํ™˜ ์ƒ์„ฑ (ํ”Œ๋ ˆ์ด์–ด์™€ ๋™์ผํ•œ ํฌ๊ธฐ) const bulletGeometry = new THREE.CylinderGeometry(1.0, 1.0, 16, 8); const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); // ๊ธฐ์ˆ˜ ๋์—์„œ๋งŒ ๋ฐœ์‚ฌ - ์ •ํ™•ํžˆ ์ „๋ฐฉ ์ค‘์•™์—์„œ const muzzleOffset = new THREE.Vector3(0, 0, 10); // X=0, Y=0์œผ๋กœ ์ค‘์•™ ๊ณ ์ • muzzleOffset.applyEuler(this.rotation); bullet.position.copy(this.position).add(muzzleOffset); // ํƒ„ํ™˜์„ ๋ฐœ์‚ฌ ๋ฐฉํ–ฅ์œผ๋กœ ํšŒ์ „ bullet.rotation.copy(this.rotation); bullet.rotateX(Math.PI / 2); // ์‹ค๋ฆฐ๋”๊ฐ€ Z์ถ• ๋ฐฉํ–ฅ์„ ํ–ฅํ•˜๋„๋ก // ํƒ„ํ™˜ ์ดˆ๊ธฐ ์œ„์น˜ ์ €์žฅ bullet.startPosition = bullet.position.clone(); // ํƒ„ํ™˜ ์†๋„ (ํ”Œ๋ ˆ์ด์–ด์™€ ๋™์ผํ•˜๊ฒŒ 1500) - ์ •ํ™•ํžˆ ์ „๋ฐฉ์œผ๋กœ const direction = new THREE.Vector3(0, 0, 1); direction.applyEuler(this.rotation); bullet.velocity = direction.multiplyScalar(1500); this.scene.add(bullet); this.bullets.push(bullet); // ์‚ฌ์šด๋“œ ์žฌ์ƒ if (this.playerFighter) { const distanceToPlayer = this.position.distanceTo(this.playerFighter.position); if (distanceToPlayer < 3000) { try { const audio = new Audio('sounds/MGLAUNCH.ogg'); const volumeMultiplier = 1 - (distanceToPlayer / 3000); audio.volume = 0.5 * volumeMultiplier; audio.play().catch(e => {}); } catch (e) {} } } } 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.y <= 0) { if (window.gameInstance) { window.gameInstance.createGroundImpactEffect(bullet.position); } this.scene.remove(bullet); this.bullets.splice(i, 1); continue; } // ๊ฑฐ๋ฆฌ ์ œํ•œ (ํ”Œ๋ ˆ์ด์–ด์™€ ๋™์ผํ•˜๊ฒŒ 6000m) if (bullet.position.distanceTo(bullet.startPosition) > 6000 || bullet.position.y > GAME_CONSTANTS.MAX_ALTITUDE + 500) { this.scene.remove(bullet); this.bullets.splice(i, 1); } } } updateMissiles(deltaTime) { for (let i = this.firedMissiles.length - 1; i >= 0; i--) { const missile = this.firedMissiles[i]; const result = missile.update(deltaTime, this.position); if (result === 'hit' || result === 'expired') { this.firedMissiles.splice(i, 1); } } } updateLockOn(deltaTime) { if (!this.playerFighter || this.aim9Missiles <= 0) { this.lockTarget = null; this.lockProgress = 0; this.isLocking = false; return; } const now = Date.now(); const distance = this.position.distanceTo(this.playerFighter.position); // ๋ฝ์˜จ ๋ฒ”์œ„ ๋‚ด์— ์žˆ๊ณ  ์ „ํˆฌ ์ค‘์ผ ๋•Œ๋งŒ if (distance <= GAME_CONSTANTS.ENEMY_LOCK_RANGE && this.aiState === 'combat') { // ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์‹œ์•ผ๊ฐ ๋‚ด์— ์žˆ๋Š”์ง€ ํ™•์ธ const toPlayer = this.playerFighter.position.clone().sub(this.position).normalize(); const forward = new THREE.Vector3(0, 0, 1).applyEuler(this.rotation); const dotProduct = forward.dot(toPlayer); const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct))); if (angle < GAME_CONSTANTS.ENEMY_LOCK_ANGLE) { // 1์ดˆ๋งˆ๋‹ค ๋ฝ์˜จ ์ง„ํ–‰ if (now - this.lastLockTime >= 1000) { if (this.lockTarget === this.playerFighter) { this.lockProgress++; this.lastLockTime = now; // ๋ฝ์˜จ ์‹œ์ž‘ ์•Œ๋ฆผ if (this.lockProgress === 1 && !this.isLocking) { this.isLocking = true; if (window.gameInstance) { window.gameInstance.onEnemyLockStart(this); } } // ๋ฝ์˜จ ์™„๋ฃŒ ๋ฐ ๋ฐœ์‚ฌ if (this.lockProgress >= GAME_CONSTANTS.AIM9_LOCK_REQUIRED) { // ๋ฏธ์‚ฌ์ผ ์ฟจ๋‹ค์šด ์ฒดํฌ (3์ดˆ) if (now - this.lastMissileFireTime >= 3000) { this.fireAIM9(); this.lastMissileFireTime = now; } // ๋ฝ์˜จ ๋ฆฌ์…‹ this.lockTarget = null; this.lockProgress = 0; this.isLocking = false; } } else { // ์ƒˆ๋กœ์šด ๋ฝ์˜จ ์‹œ์ž‘ this.lockTarget = this.playerFighter; this.lockProgress = 1; this.lastLockTime = now; this.isLocking = true; if (window.gameInstance) { window.gameInstance.onEnemyLockStart(this); } } } } else { // ์‹œ์•ผ๊ฐ์„ ๋ฒ—์–ด๋‚˜๋ฉด ๋ฝ์˜จ ํ•ด์ œ if (this.lockTarget) { this.lockTarget = null; this.lockProgress = 0; if (this.isLocking) { this.isLocking = false; if (window.gameInstance) { window.gameInstance.onEnemyLockLost(this); } } } } } else { // ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ๋ฝ์˜จ ํ•ด์ œ if (this.lockTarget) { this.lockTarget = null; this.lockProgress = 0; if (this.isLocking) { this.isLocking = false; if (window.gameInstance) { window.gameInstance.onEnemyLockLost(this); } } } } } fireAIM9() { if (this.aim9Missiles <= 0 || !this.lockTarget) return; this.aim9Missiles--; // ๋‚ ๊ฐœ ๋ฐœ์‚ฌ ์œ„์น˜ const isLeftWing = Math.random() < 0.5; const wingOffset = new THREE.Vector3(isLeftWing ? -6 : 6, -1, 2); wingOffset.applyEuler(this.rotation); const missileStartPos = this.position.clone().add(wingOffset); // ๋ฏธ์‚ฌ์ผ ์ƒ์„ฑ const missile = new EnemyAIM9Missile(this.scene, missileStartPos, this.lockTarget, this.rotation.clone()); this.firedMissiles.push(missile); // ๋ฐœ์‚ฌ ์•Œ๋ฆผ if (window.gameInstance) { window.gameInstance.onEnemyMissileLaunch(this, missile); } // ๋ฐœ์‚ฌ์Œ (๊ฑฐ๋ฆฌ์— ๋”ฐ๋ผ) if (this.playerFighter) { const distanceToPlayer = this.position.distanceTo(this.playerFighter.position); if (distanceToPlayer < 3000) { try { const missileSound = new Audio('sounds/missile.ogg'); const volumeMultiplier = 1 - (distanceToPlayer / 3000); missileSound.volume = 0.5 * volumeMultiplier; missileSound.play().catch(e => {}); } catch (e) {} } } } // ์ ๊ธฐ์šฉ ํ”Œ๋ ˆ์–ด ๋ฐฐ์น˜ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ deployFlares() { if (this.flareCount <= 0) return; this.flareCount--; // ๊ผฌ๋ฆฌ ๋ถ€๋ถ„ ์œ„์น˜ ๊ณ„์‚ฐ const tailOffset = new THREE.Vector3(0, 0, -10); tailOffset.applyEuler(this.rotation); const flareStartPos = this.position.clone().add(tailOffset); // ํ›„๋ฐฉ ๋ฐฉํ–ฅ const backwardDirection = new THREE.Vector3(0, 0, -1); backwardDirection.applyEuler(this.rotation); // 4๊ฐœ ํ”Œ๋ ˆ์–ด ๋ฐœ์‚ฌ for (let i = 0; i < 4; i++) { const isLeftSide = i < 2; const flare = new Flare(this.scene, flareStartPos, backwardDirection, isLeftSide); this.deployedFlares.push(flare); if (window.gameInstance) { window.gameInstance.notifyMissilesOfFlare(flare); } } console.log('Enemy deployed flares!'); // ์ ์ด ํ”Œ๋ ˆ์–ด๋ฅผ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ ์Œ์„ฑ ์žฌ์ƒ if (window.gameInstance) { try { const flareVoice = new Audio('sounds/voice_flare_a.ogg'); flareVoice.volume = 0.8; flareVoice.play().catch(e => { console.log('Flare voice failed to play:', e); }); } catch (e) { console.log('Flare voice error:', e); } // ์ž๋ง‰ ํ‘œ์‹œ window.gameInstance.showSubtitle('The enemy evaded the missile using flares!', 4000); } } calculateAimAccuracy(target) { const toTarget = target.clone().sub(this.position).normalize(); const forward = new THREE.Vector3(0, 0, 1).applyEuler(this.rotation); const dotProduct = forward.dot(toTarget); return Math.acos(Math.max(-1, Math.min(1, dotProduct))); } selectNewPatrolTarget() { const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2 * 0.7; // ๋” ๋‹ค์–‘ํ•œ ๊ณ ๋„ ์„ ํƒ const minAltitude = 1000; const maxAltitude = 6000; this.targetPosition = new THREE.Vector3( (Math.random() - 0.5) * 2 * mapLimit, minAltitude + Math.random() * (maxAltitude - minAltitude), (Math.random() - 0.5) * 2 * mapLimit ); } selectEvadeTarget() { // ํ˜„์žฌ ์œ„์น˜์—์„œ ๋žœ๋ค ๋ฐฉํ–ฅ์œผ๋กœ ํšŒํ”ผ const evadeDistance = 1000 + Math.random() * 1000; const evadeAngle = Math.random() * Math.PI * 2; this.targetPosition = new THREE.Vector3( this.position.x + Math.cos(evadeAngle) * evadeDistance, this.position.y + (Math.random() - 0.5) * 500, this.position.z + Math.sin(evadeAngle) * evadeDistance ); // ๋งต ๊ฒฝ๊ณ„ ํ™•์ธ const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2 * 0.8; this.targetPosition.x = THREE.MathUtils.clamp(this.targetPosition.x, -mapLimit, mapLimit); this.targetPosition.z = THREE.MathUtils.clamp(this.targetPosition.z, -mapLimit, mapLimit); this.targetPosition.y = THREE.MathUtils.clamp(this.targetPosition.y, 1000, 6000); } normalizeAngle(angle) { while (angle > Math.PI) angle -= Math.PI * 2; while (angle < -Math.PI) angle += Math.PI * 2; return angle; } takeDamage(damage) { console.log(`Enemy taking damage: ${damage}, Current health: ${this.health}`); this.health -= damage; console.log(`Enemy health after damage: ${this.health}`); const isDead = this.health <= 0; console.log(`Enemy is dead: ${isDead}`); return isDead; } destroy() { if (this.mesh) { this.scene.remove(this.mesh); this.bullets.forEach(bullet => this.scene.remove(bullet)); this.bullets = []; this.firedMissiles.forEach(missile => missile.destroy()); this.firedMissiles = []; this.deployedFlares.forEach(flare => flare.destroy()); this.deployedFlares = []; 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.lastShootTime = 0; this.bgm = null; this.bgmPlaying = false; this.keys = { w: false, a: false, s: false, d: false, f: false, g: false, r: false }; this.isStarted = false; // ์Œ์„ฑ ์žฌ์ƒ ํ”Œ๋ž˜๊ทธ ์ถ”๊ฐ€ this.firstMissileVoicePlayed = false; window.gameInstance = this; 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('๊ฒŒ์ž„ ๋ฆฌ์†Œ์Šค ๋กœ๋”ฉ ์™„๋ฃŒ'); await this.preloadBGM(); this.showStartScreen(); this.animate(); } catch (error) { console.error('๊ฒŒ์ž„ ์‚ฌ์ „ ๋กœ๋”ฉ ์‹คํŒจ:', error); document.getElementById('loading').innerHTML = '<div class="loading-text" style="color: red;">๋กœ๋”ฉ ์‹คํŒจ. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด์ฃผ์„ธ์š”.</div>'; } } showStartScreen() { if (this.isLoaded && this.isBGMReady) { const loadingElement = document.getElementById('loading'); if (loadingElement) { loadingElement.style.display = 'none'; } const startScreen = document.getElementById('startScreen'); if (startScreen) { startScreen.style.display = 'flex'; } window.dispatchEvent(new Event('gameReady')); 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.25; this.bgm.loop = true; 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(); 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); enemy.playerFighter = this.fighter; // ํ”Œ๋ ˆ์ด์–ด ์ฐธ์กฐ ์ „๋‹ฌ 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(5000, 8000, 5000); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 15000; directionalLight.shadow.camera.left = -8000; directionalLight.shadow.camera.right = 8000; directionalLight.shadow.camera.top = 8000; directionalLight.shadow.camera.bottom = -8000; this.scene.add(directionalLight); // ๋‹จ์ˆœํ•œ ํ‰ํ‰ํ•œ ๋ฐ”๋‹ฅ const groundGeometry = new THREE.PlaneGeometry(GAME_CONSTANTS.MAP_SIZE, GAME_CONSTANTS.MAP_SIZE); const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x8FBC8F }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; this.scene.add(ground); // ๊ฐœ์„ ๋œ ๊ตฌ๋ฆ„ ์ถ”๊ฐ€ this.addClouds(); } addClouds() { // ๋‹ค์ธต ๊ตฌ๋ฆ„ ์‹œ์Šคํ…œ const cloudLayers = [ { count: 50, minHeight: 800, maxHeight: 1500, opacity: 0.7, scale: 1.5 }, // ๋‚ฎ์€ ๊ตฌ๋ฆ„ { count: 80, minHeight: 2000, maxHeight: 4000, opacity: 0.5, scale: 2.0 }, // ์ค‘๊ฐ„ ๊ตฌ๋ฆ„ { count: 40, minHeight: 5000, maxHeight: 8000, opacity: 0.3, scale: 3.0 } // ๋†’์€ ๊ตฌ๋ฆ„ ]; cloudLayers.forEach(layer => { for (let i = 0; i < layer.count; i++) { // ๊ตฌ๋ฆ„ ๊ทธ๋ฃน ์ƒ์„ฑ (์—ฌ๋Ÿฌ ๊ตฌ์ฒด๋กœ ๊ตฌ์„ฑ) const cloudGroup = new THREE.Group(); // ๊ตฌ๋ฆ„์„ ๊ตฌ์„ฑํ•˜๋Š” ๊ตฌ์ฒด ๊ฐœ์ˆ˜ const sphereCount = 5 + Math.floor(Math.random() * 8); for (let j = 0; j < sphereCount; j++) { const radius = 50 + Math.random() * 100; const cloudGeometry = new THREE.SphereGeometry(radius, 8, 6); const cloudMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: layer.opacity * (0.7 + Math.random() * 0.3) }); const cloudPart = new THREE.Mesh(cloudGeometry, cloudMaterial); // ๊ตฌ๋ฆ„ ๋‚ด ๊ตฌ์ฒด๋“ค์˜ ์œ„์น˜๋ฅผ ๋žœ๋คํ•˜๊ฒŒ ๋ฐฐ์น˜ cloudPart.position.set( (Math.random() - 0.5) * 200, (Math.random() - 0.5) * 50, (Math.random() - 0.5) * 200 ); cloudGroup.add(cloudPart); } // ๊ตฌ๋ฆ„ ๊ทธ๋ฃน ์œ„์น˜ ์„ค์ • cloudGroup.position.set( (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE, layer.minHeight + Math.random() * (layer.maxHeight - layer.minHeight), (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE ); // ๊ตฌ๋ฆ„ ํฌ๊ธฐ ๋ณ€ํ™” const scale = layer.scale * (0.8 + Math.random() * 0.4); cloudGroup.scale.set(scale, scale * 0.5, scale); // ๊ตฌ๋ฆ„ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์œ„ํ•œ ์†์„ฑ ์ถ”๊ฐ€ cloudGroup.userData = { driftSpeed: (Math.random() - 0.5) * 0.1, floatSpeed: Math.random() * 0.5 + 0.5, initialY: cloudGroup.position.y, time: Math.random() * Math.PI * 2 }; this.scene.add(cloudGroup); // ๊ตฌ๋ฆ„ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ (์• ๋‹ˆ๋ฉ”์ด์…˜์šฉ) if (!this.clouds) this.clouds = []; this.clouds.push(cloudGroup); } }); // ์•ˆ๊ฐœ ํšจ๊ณผ๋ฅผ ์œ„ํ•œ ๋‚ฎ์€ ๊ตฌ๋ฆ„์ธต ์ถ”๊ฐ€ for (let i = 0; i < 30; i++) { const fogGeometry = new THREE.BoxGeometry(500, 100, 500); const fogMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.2 }); const fog = new THREE.Mesh(fogGeometry, fogMaterial); fog.position.set( (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE, 50 + Math.random() * 200, (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE ); this.scene.add(fog); } } setupEventListeners() { document.addEventListener('keydown', (event) => { if (this.isGameOver) return; if (!this.isStarted) return; switch(event.code) { case 'KeyW': this.keys.w = true; console.log('W key pressed - Accelerating'); break; case 'KeyA': this.keys.a = true; console.log('A key pressed - Turning left'); break; case 'KeyS': this.keys.s = true; console.log('S key pressed - Decelerating'); break; case 'KeyD': this.keys.d = true; console.log('D key pressed - Turning right'); break; case 'KeyF': this.keys.f = true; // Fํ‚ค ๋ˆ„๋ฅผ ๋•Œ ํ”Œ๋ ˆ์–ด ๋ฐœ์‚ฌ if (!event.repeat && this.fighter) { this.fighter.deployFlares(); } break; case 'KeyG': this.keys.g = true; console.log('G key pressed - Stall escape'); break; case 'KeyR': if (!event.repeat && this.fighter) { // this.fighter ์ฒดํฌ ์ถ”๊ฐ€ this.fighter.switchWeapon(); console.log('R key pressed - Switching weapon to', this.fighter.currentWeapon); } break; } }); document.addEventListener('keyup', (event) => { if (this.isGameOver) return; // gameStarted ๋Œ€์‹  this.isStarted ์‚ฌ์šฉ if (!this.isStarted) 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; case 'KeyF': this.keys.f = false; break; case 'KeyG': this.keys.g = false; break; case 'KeyR': this.keys.r = false; break; } }); document.addEventListener('mousemove', (event) => { // ์—ฌ๊ธฐ๋„ gameStarted๋ฅผ this.isStarted๋กœ ๋ณ€๊ฒฝ if (!document.pointerLockElement || this.isGameOver || !this.isStarted) return; const deltaX = event.movementX || 0; const deltaY = event.movementY || 0; this.fighter.updateMouseInput(deltaX, deltaY); }); document.addEventListener('mousedown', (event) => { // ์—ฌ๊ธฐ๋„ gameStarted๋ฅผ this.isStarted๋กœ ๋ณ€๊ฒฝ if (!document.pointerLockElement || this.isGameOver || !this.isStarted) return; if (event.button === 0) { this.fighter.isMouseDown = true; this.lastShootTime = 0; } }); document.addEventListener('mouseup', (event) => { if (event.button === 0) { this.fighter.isMouseDown = false; } }); 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(); // ์—”์ง„ ์†Œ๋ฆฌ ์‹œ์ž‘ this.fighter.startEngineSound(); // ๊ฒŒ์ž„ ์‹œ์ž‘ ์Œ์„ฑ ์žฌ์ƒ try { const startVoice = new Audio('sounds/voice_start_a.ogg'); startVoice.volume = 0.8; startVoice.play().catch(e => { console.log('Start voice failed to play:', e); }); } catch (e) { console.log('Start voice error:', e); } // ์ž๋ง‰ ํ‘œ์‹œ this.showSubtitle('Listen up! Find and shut down all enemy squad that invaded their airspace.', 5000); console.log('๊ฒŒ์ž„ ์‹œ์ž‘!'); } showSubtitle(text, duration) { // DOM์ด ์ค€๋น„๋  ๋•Œ๊นŒ์ง€ ์•ฝ๊ฐ„ ๋Œ€๊ธฐ setTimeout(() => { // ๊ธฐ์กด ์ž๋ง‰ ์ œ๊ฑฐ const existingSubtitle = document.getElementById('gameSubtitle'); if (existingSubtitle) { existingSubtitle.remove(); } // ์ƒˆ ์ž๋ง‰ ์ƒ์„ฑ const subtitle = document.createElement('div'); subtitle.id = 'gameSubtitle'; subtitle.style.cssText = ` position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%); color: #ffffff; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); background: rgba(0,0,0,0.7); padding: 15px 30px; border-radius: 5px; z-index: 2000; text-align: center; max-width: 80%; pointer-events: none; `; subtitle.textContent = text; document.body.appendChild(subtitle); console.log('Subtitle displayed:', text); // ์ง€์ •๋œ ์‹œ๊ฐ„ ํ›„ ์ œ๊ฑฐ setTimeout(() => { if (subtitle && subtitle.parentNode) { subtitle.style.transition = 'opacity 0.5s'; subtitle.style.opacity = '0'; setTimeout(() => { if (subtitle && subtitle.parentNode) { subtitle.remove(); } }, 500); } }, duration); }, 100); } 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) { // ์‹œ๊ฐ„์ด ๋๋‚ฌ์„ ๋•Œ ์ ์ด ๋‚จ์•„์žˆ์œผ๋ฉด ํŒจ๋ฐฐ, ๋ชจ๋‘ ์ œ๊ฑฐํ–ˆ์œผ๋ฉด ์Šน๋ฆฌ if (this.enemies.length > 0) { this.endGame(false, "TIME OVER - MISSION FAILED"); } else { this.endGame(true); } } } }, 1000); } // ์ ์ด ํ”Œ๋ ˆ์ด์–ด๋ฅผ ๋ฝ์˜จ ์‹œ์ž‘ํ•  ๋•Œ onEnemyLockStart(enemy) { console.log('Warning: Enemy locking on!'); // ๋ฝ์˜จ ์ค‘์ธ ์  ์ถ”๊ฐ€ if (!this.fighter.beingLockedBy.includes(enemy)) { this.fighter.beingLockedBy.push(enemy); } // ๊ฒฝ๊ณ ์Œ ์—…๋ฐ์ดํŠธ this.fighter.updateWarningAudios(); } // ์ ์ด ๋ฝ์˜จ์„ ์žƒ์—ˆ์„ ๋•Œ onEnemyLockLost(enemy) { console.log('Enemy lock lost'); // ๋ฝ์˜จ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ const index = this.fighter.beingLockedBy.indexOf(enemy); if (index !== -1) { this.fighter.beingLockedBy.splice(index, 1); } // ๊ฒฝ๊ณ ์Œ ์—…๋ฐ์ดํŠธ this.fighter.updateWarningAudios(); } // ์ ์ด ๋ฏธ์‚ฌ์ผ์„ ๋ฐœ์‚ฌํ–ˆ์„ ๋•Œ onEnemyMissileLaunch(enemy, missile) { console.log('Warning: Missile launched!'); // ํ”Œ๋ ˆ์ด์–ด์˜ incoming missiles ๋ชฉ๋ก์— ์ถ”๊ฐ€ this.fighter.incomingMissiles.push(missile); // ๋ฝ์˜จ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ (๋ฏธ์‚ฌ์ผ ๋ฐœ์‚ฌ ํ›„ ๋ฝ์˜จ ํ•ด์ œ) const index = this.fighter.beingLockedBy.indexOf(enemy); if (index !== -1) { this.fighter.beingLockedBy.splice(index, 1); } // ๊ฒฝ๊ณ ์Œ ์—…๋ฐ์ดํŠธ this.fighter.updateWarningAudios(); // ์ฒซ ๋ฒˆ์งธ ๋ฏธ์‚ฌ์ผ ๋ฐœ์‚ฌ ์‹œ์—๋งŒ ์Œ์„ฑ ์žฌ์ƒ if (!this.firstMissileVoicePlayed) { this.firstMissileVoicePlayed = true; try { const missileVoice = new Audio('sounds/voice_missile_a.ogg'); missileVoice.volume = 0.8; missileVoice.play().catch(e => { console.log('Missile voice failed to play:', e); }); } catch (e) { console.log('Missile voice error:', e); } // ์ž๋ง‰ ํ‘œ์‹œ this.showSubtitle('Watch out the enemy missile.', 4000); } } notifyMissilesOfFlare(flare) { // ๋ชจ๋“  ํ™œ์„ฑ ๋ฏธ์‚ฌ์ผ์—๊ฒŒ ํ”Œ๋ ˆ์–ด ์กด์žฌ ์•Œ๋ฆผ const allMissiles = []; // ํ”Œ๋ ˆ์ด์–ด ๋ฏธ์‚ฌ์ผ this.fighter.firedMissiles.forEach(missile => { allMissiles.push(missile); }); // ์  ๋ฏธ์‚ฌ์ผ this.enemies.forEach(enemy => { enemy.firedMissiles.forEach(missile => { allMissiles.push(missile); }); }); // ๊ฐ ๋ฏธ์‚ฌ์ผ์ด ํ”Œ๋ ˆ์–ด๋ฅผ ๊ฐ์ง€ํ•˜๋„๋ก allMissiles.forEach(missile => { if (missile && missile.position) { const distance = missile.position.distanceTo(flare.position); if (distance < 500) { // ๋ฏธ์‚ฌ์ผ์ด ํ”Œ๋ ˆ์–ด๋ฅผ ๊ฐ์ง€ missile.detectedFlare = flare; } } }); } findNearestFlare(position) { let nearestFlare = null; let nearestDistance = Infinity; // ํ”Œ๋ ˆ์ด์–ด ํ”Œ๋ ˆ์–ด this.fighter.deployedFlares.forEach(flare => { if (flare.heatSignature > 0.3) { const distance = position.distanceTo(flare.position); if (distance < nearestDistance) { nearestDistance = distance; nearestFlare = flare; } } }); // ์  ํ”Œ๋ ˆ์–ด this.enemies.forEach(enemy => { enemy.deployedFlares.forEach(flare => { if (flare.heatSignature > 0.3) { const distance = position.distanceTo(flare.position); if (distance < nearestDistance) { nearestDistance = distance; nearestFlare = flare; } } }); }); return nearestFlare; } findIncomingMissilesForEnemy(enemy) { const incomingMissiles = []; // ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ๋ฐœ์‚ฌํ•œ ๋ฏธ์‚ฌ์ผ ์ค‘ ์ด ์ ์„ ์ถ”์ ํ•˜๋Š” ๊ฒƒ this.fighter.firedMissiles.forEach(missile => { if (missile.target === enemy) { incomingMissiles.push(missile); } }); // ๊ฑฐ๋ฆฌ์ˆœ์œผ๋กœ ์ •๋ ฌ incomingMissiles.sort((a, b) => { const distA = enemy.position.distanceTo(a.position); const distB = enemy.position.distanceTo(b.position); return distA - distB; }); return incomingMissiles; } updateUI() { if (this.fighter.isLoaded) { const speedKnots = Math.round(this.fighter.speed * 1.94384); const altitudeFeet = Math.round(this.fighter.altitude * 3.28084); const altitudeMeters = Math.round(this.fighter.altitude); 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'); // ์ฒด๋ ฅ๋ฐ”์— ์ˆ˜์น˜ ํ‘œ์‹œ const healthBar = document.getElementById('healthBar'); if (healthBar) { healthBar.innerHTML = ` <div id="health" style="width: ${(this.fighter.health / GAME_CONSTANTS.MAX_HEALTH) * 100}%"></div> <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-weight: bold;"> ${this.fighter.health}/${GAME_CONSTANTS.MAX_HEALTH} </div> `; } if (scoreElement) scoreElement.textContent = `Score: ${this.score}`; if (timeElement) timeElement.textContent = `Time: ${this.gameTime}s`; // ๋ฌด๊ธฐ ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ if (ammoElement) { if (this.fighter.currentWeapon === 'MG') { ammoElement.innerHTML = `20MM MG: ${this.fighter.ammo}<br>FLARES: ${this.fighter.flareCount}`; ammoElement.style.color = '#0f0'; } else { ammoElement.innerHTML = `AIM-9: ${this.fighter.aim9Missiles}<br>FLARES: ${this.fighter.flareCount}`; ammoElement.style.color = '#ff0000'; // ๋ฝ์˜จ ์ƒํƒœ ํ‘œ์‹œ if (this.fighter.lockTarget && this.fighter.lockProgress > 0) { const lockStatus = this.fighter.lockProgress >= GAME_CONSTANTS.AIM9_LOCK_REQUIRED ? 'LOCKED' : `LOCKING ${this.fighter.lockProgress}/3`; ammoElement.innerHTML = `AIM-9: ${this.fighter.aim9Missiles}<br>FLARES: ${this.fighter.flareCount}<br>[${lockStatus}]`; } } } if (gameStatsElement) { gameStatsElement.innerHTML = ` <div>Score: ${this.score}</div> <div>Time: ${this.gameTime}s</div> <div>Speed: ${speedKnots} KT</div> <div>Alt: ${altitudeMeters}m (${altitudeFeet} FT)</div> <div>Throttle: ${Math.round(this.fighter.throttle * 100)}%</div> <div>G-Force: ${this.fighter.gForce.toFixed(1)}</div> <div>Targets: ${this.enemies.length}</div> `; } this.updateWarnings(); this.updateHUD(); } } updateHUD() { // HUD ํฌ๋กœ์Šคํ—ค์–ด ์—…๋ฐ์ดํŠธ const hudElement = document.getElementById('hudCrosshair'); if (!hudElement) return; // ๋กค ๊ฐ๋„์— ๋”ฐ๋ผ HUD ํšŒ์ „ const rollDegrees = this.fighter.rotation.z * (180 / Math.PI); hudElement.style.transform = `translate(-50%, -50%) rotate(${-rollDegrees}deg)`; // ํ”ผ์น˜ ๋ž˜๋” ์—…๋ฐ์ดํŠธ const pitchLadder = document.getElementById('pitchLadder'); if (pitchLadder) { const pitchDegrees = this.fighter.rotation.x * (180 / Math.PI); // ์Œ์ˆ˜๋ฅผ ๊ณฑํ•ด์„œ ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์œผ๋กœ, 10๋„๋‹น 20ํ”ฝ์…€ const pitchOffset = -pitchDegrees * 2; pitchLadder.style.transform = `translateY(${pitchOffset}px)`; } // ๋น„ํ–‰ ์ •๋ณด ์—…๋ฐ์ดํŠธ const speedKnots = Math.round(this.fighter.speed * 1.94384); const altitudeMeters = Math.round(this.fighter.altitude); const pitchDegrees = Math.round(this.fighter.rotation.x * (180 / Math.PI)); const rollDegreesRounded = Math.round(rollDegrees); const headingDegrees = Math.round(this.fighter.getHeadingDegrees()); // ์„ ํšŒ์œจ ๊ณ„์‚ฐ (๋„/์ดˆ) if (!this.lastHeading) this.lastHeading = headingDegrees; let turnRate = (headingDegrees - this.lastHeading); if (turnRate > 180) turnRate -= 360; if (turnRate < -180) turnRate += 360; this.lastHeading = headingDegrees; const turnRateDegPerSec = Math.round(turnRate / (1/60)); // 60 FPS ๊ธฐ์ค€ // HUD ์ •๋ณด ์—…๋ฐ์ดํŠธ const hudSpeed = document.getElementById('hudSpeed'); const hudAltitude = document.getElementById('hudAltitude'); const hudHeading = document.getElementById('hudHeading'); const hudPitch = document.getElementById('hudPitch'); const hudRoll = document.getElementById('hudRoll'); const hudTurnRate = document.getElementById('hudTurnRate'); if (hudSpeed) hudSpeed.textContent = `SPD: ${speedKnots} KT`; if (hudAltitude) hudAltitude.textContent = `ALT: ${altitudeMeters} M`; if (hudHeading) hudHeading.textContent = `HDG: ${String(headingDegrees).padStart(3, '0')}ยฐ`; if (hudPitch) hudPitch.textContent = `PITCH: ${pitchDegrees}ยฐ`; if (hudRoll) hudRoll.textContent = `ROLL: ${rollDegreesRounded}ยฐ`; if (hudTurnRate) hudTurnRate.textContent = `TURN: ${Math.abs(turnRateDegPerSec) > 1 ? turnRateDegPerSec : 0}ยฐ/s`; // ์  ํƒ€๊ฒŸ ๋งˆ์ปค ์—…๋ฐ์ดํŠธ const targetMarkers = document.getElementById('targetMarkers'); if (targetMarkers) { targetMarkers.innerHTML = ''; // ๋ชจ๋“  ์ ์— ๋Œ€ํ•ด ์ฒ˜๋ฆฌ this.enemies.forEach(enemy => { if (!enemy.mesh || !enemy.isLoaded) return; const distance = this.fighter.position.distanceTo(enemy.position); if (distance > 10000) return; // 10km ์ด์ƒ์€ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ // ์ ์˜ ํ™”๋ฉด ์ขŒํ‘œ ๊ณ„์‚ฐ const enemyScreenPos = this.getScreenPosition(enemy.position); if (!enemyScreenPos) return; // ํ™”๋ฉด ์ค‘์•™์œผ๋กœ๋ถ€ํ„ฐ์˜ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ const centerX = window.innerWidth / 2; const centerY = window.innerHeight / 2; const distFromCenter = Math.sqrt( Math.pow(enemyScreenPos.x - centerX, 2) + Math.pow(enemyScreenPos.y - centerY, 2) ); // ํฌ๋กœ์Šคํ—ค์–ด ๋ฐ˜๊ฒฝ (75px) const crosshairRadius = 75; const isInCrosshair = distFromCenter < crosshairRadius; // ํƒ€๊ฒŸ ๋งˆ์ปค ์ƒ์„ฑ const marker = document.createElement('div'); marker.className = 'target-marker'; if (isInCrosshair) { marker.classList.add('in-crosshair'); // AIM-9 ๋ชจ๋“œ์—์„œ ๋ฝ์˜จ ํ‘œ์‹œ if (this.fighter.currentWeapon === 'AIM9' && distance < GAME_CONSTANTS.AIM9_LOCK_RANGE) { if (this.fighter.lockTarget === enemy) { if (this.fighter.lockProgress >= GAME_CONSTANTS.AIM9_LOCK_REQUIRED) { marker.classList.add('locked'); marker.style.border = '2px solid #ff0000'; marker.style.boxShadow = '0 0 20px #ff0000'; } else { // ๋ฝ์˜จ ์ค‘ marker.style.border = '2px solid #ffff00'; marker.style.boxShadow = '0 0 10px #ffff00'; marker.style.animation = `target-pulse ${1.0 / this.fighter.lockProgress}s infinite`; } } } else if (distance < 2000) { marker.classList.add('locked'); } // ํƒ€๊ฒŸ ๋ฐ•์Šค ์ถ”๊ฐ€ const targetBox = document.createElement('div'); targetBox.className = 'target-box'; marker.appendChild(targetBox); // ๊ฑฐ๋ฆฌ ์ •๋ณด ์ถ”๊ฐ€ const targetInfo = document.createElement('div'); targetInfo.className = 'target-info'; targetInfo.textContent = `${Math.round(distance)}m`; // AIM-9 ๋ชจ๋“œ์—์„œ ๋ฝ์˜จ ์ •๋ณด ์ถ”๊ฐ€ if (this.fighter.currentWeapon === 'AIM9' && this.fighter.lockTarget === enemy) { if (this.fighter.lockProgress >= GAME_CONSTANTS.AIM9_LOCK_REQUIRED) { targetInfo.textContent += ' LOCKED'; targetInfo.style.color = '#ff0000'; } else { targetInfo.textContent += ` LOCK ${this.fighter.lockProgress}/3`; targetInfo.style.color = '#ffff00'; } } marker.appendChild(targetInfo); } marker.style.left = `${enemyScreenPos.x}px`; marker.style.top = `${enemyScreenPos.y}px`; targetMarkers.appendChild(marker); }); } } getScreenPosition(worldPosition) { // 3D ์ขŒํ‘œ๋ฅผ ํ™”๋ฉด ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ const vector = worldPosition.clone(); vector.project(this.camera); // ์นด๋ฉ”๋ผ ๋’ค์— ์žˆ๋Š” ๊ฐ์ฒด๋Š” ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ if (vector.z > 1) return null; const x = (vector.x * 0.5 + 0.5) * window.innerWidth; const y = (-vector.y * 0.5 + 0.5) * window.innerHeight; return { x, y }; } updateWarnings() { // ๊ธฐ์กด ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ const existingWarnings = document.querySelectorAll('.warning-message'); existingWarnings.forEach(w => w.remove()); // ์Šคํ†จ ํƒˆ์ถœ ๊ฒฝ๊ณ  ์ œ๊ฑฐ const existingStallWarnings = document.querySelectorAll('.stall-escape-warning'); existingStallWarnings.forEach(w => w.remove()); // ๋ฏธ์‚ฌ์ผ ๊ฒฝ๊ณ  ์ œ๊ฑฐ const existingMissileWarnings = document.querySelectorAll('.missile-warning'); existingMissileWarnings.forEach(w => w.remove()); // ๊ณ ๋„ ๊ฒฝ๊ณ  ์™ธ๊ณฝ ํšจ๊ณผ let altitudeEdgeEffect = document.getElementById('altitudeEdgeEffect'); // ๋ฏธ์‚ฌ์ผ ๊ฒฝ๊ณ ๊ฐ€ ์ตœ์šฐ์„  if (this.fighter.incomingMissiles.length > 0) { if (!altitudeEdgeEffect) { altitudeEdgeEffect = document.createElement('div'); altitudeEdgeEffect.id = 'altitudeEdgeEffect'; document.body.appendChild(altitudeEdgeEffect); } // ๊ฐ•๋ ฌํ•œ ๋ถ‰์€์ƒ‰ ํšจ๊ณผ const edgeIntensity = 0.8; altitudeEdgeEffect.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1300; background: radial-gradient(ellipse at center, transparent 30%, rgba(255, 0, 0, ${edgeIntensity * 0.4}) 50%, rgba(255, 0, 0, ${edgeIntensity}) 100%); box-shadow: inset 0 0 200px rgba(255, 0, 0, ${edgeIntensity}), inset 0 0 150px rgba(255, 0, 0, ${edgeIntensity * 0.8}); animation: missile-pulse 0.3s infinite; `; // ๋ฏธ์‚ฌ์ผ ๊ฒฝ๊ณ  ํ…์ŠคํŠธ const missileWarning = document.createElement('div'); missileWarning.className = 'missile-warning'; missileWarning.style.cssText = ` position: fixed; top: 20%; left: 50%; transform: translateX(-50%); color: #ff0000; font-size: 36px; font-weight: bold; text-shadow: 0 0 20px rgba(255,0,0,1), 0 0 40px rgba(255,0,0,0.8); z-index: 1600; text-align: center; animation: missile-blink 0.2s infinite; `; missileWarning.innerHTML = 'MISSILE INCOMING!<br>USE FLARE! PRESS F!'; document.body.appendChild(missileWarning); } else if (this.fighter.altitude < 500) { if (!altitudeEdgeEffect) { altitudeEdgeEffect = document.createElement('div'); altitudeEdgeEffect.id = 'altitudeEdgeEffect'; document.body.appendChild(altitudeEdgeEffect); } let edgeIntensity; if (this.fighter.altitude < 250) { // PULL UP ๊ฒฝ๊ณ  - ๊ฐ•ํ•œ ๋ถ‰์€ ํšจ๊ณผ edgeIntensity = 0.6; altitudeEdgeEffect.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1300; background: radial-gradient(ellipse at center, transparent 40%, rgba(255, 0, 0, ${edgeIntensity * 0.3}) 60%, rgba(255, 0, 0, ${edgeIntensity}) 100%); box-shadow: inset 0 0 150px rgba(255, 0, 0, ${edgeIntensity}), inset 0 0 100px rgba(255, 0, 0, ${edgeIntensity * 0.8}); animation: pulse-red 0.5s infinite; `; } else { // LOW ALTITUDE ๊ฒฝ๊ณ  - ์•ฝํ•œ ๋ถ‰์€ ํšจ๊ณผ edgeIntensity = 0.3; altitudeEdgeEffect.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1300; background: radial-gradient(ellipse at center, transparent 50%, rgba(255, 0, 0, ${edgeIntensity * 0.2}) 70%, rgba(255, 0, 0, ${edgeIntensity}) 100%); box-shadow: inset 0 0 100px rgba(255, 0, 0, ${edgeIntensity}), inset 0 0 50px rgba(255, 0, 0, ${edgeIntensity * 0.5}); `; } } else { // ๊ณ ๋„๊ฐ€ ์•ˆ์ „ํ•˜๋ฉด ํšจ๊ณผ ์ œ๊ฑฐ if (altitudeEdgeEffect) { altitudeEdgeEffect.remove(); } } if (this.fighter.warningBlinkState) { const warningContainer = document.createElement('div'); warningContainer.className = 'warning-message'; warningContainer.style.cssText = ` position: fixed; top: 30%; left: 50%; transform: translateX(-50%); color: #ff0000; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); z-index: 1500; text-align: center; `; let warningText = ''; if (this.fighter.altitude < 250) { warningText += 'PULL UP! PULL UP!\n'; } else if (this.fighter.altitude < 500) { warningText += 'LOW ALTITUDE WARNING\n'; } if (this.fighter.altitudeWarning) { warningText += 'ALTITUDE LIMIT\n'; } if (this.fighter.stallWarning) { warningText += 'STALL WARNING\n'; } if (this.fighter.overG) { warningText += 'OVER-G! OVER-G!\n'; } if (warningText) { warningContainer.innerHTML = warningText.replace(/\n/g, '<br>'); document.body.appendChild(warningContainer); } } // ์Šคํ†จ ์ƒํƒœ์ผ ๋•Œ๋งŒ "Press F to Escape" ๊ฒฝ๊ณ  ํ‘œ์‹œ if (this.fighter.stallWarning) { const stallEscapeWarning = document.createElement('div'); stallEscapeWarning.className = 'stall-escape-warning'; stallEscapeWarning.style.cssText = ` position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%); background: rgba(255, 0, 0, 0.8); color: #ffffff; font-size: 28px; font-weight: bold; padding: 15px 30px; border: 3px solid #ff0000; border-radius: 10px; z-index: 1600; text-align: center; animation: blink 0.5s infinite; `; stallEscapeWarning.innerHTML = 'PRESS G TO ESCAPE'; document.body.appendChild(stallEscapeWarning); // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ ์ถ”๊ฐ€ if (!document.getElementById('blinkAnimation')) { const style = document.createElement('style'); style.id = 'blinkAnimation'; style.innerHTML = ` @keyframes blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0.3; } } @keyframes pulse-green { 0%, 100% { opacity: 1; transform: translateX(-50%) scale(1); } 50% { opacity: 0.8; transform: translateX(-50%) scale(1.1); } } @keyframes box-pulse { 0%, 100% { background: rgba(255, 0, 0, 0.9); box-shadow: 0 0 20px rgba(255, 0, 0, 0.8), 0 0 40px rgba(255, 0, 0, 0.4); } 50% { background: rgba(255, 50, 50, 1); box-shadow: 0 0 30px rgba(255, 100, 100, 1), 0 0 60px rgba(255, 0, 0, 0.8); } } @keyframes pulse-red { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } @keyframes missile-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } @keyframes missile-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0.2; } } `; document.head.appendChild(style); } } // Over-G ์‹œ์•ผ ํšจ๊ณผ - ์ˆ˜์ •๋œ ๋ถ€๋ถ„ if (this.fighter.overG && this.fighter.overGTimer > 1.5) { let blurEffect = document.getElementById('overGBlurEffect'); if (!blurEffect) { blurEffect = document.createElement('div'); blurEffect.id = 'overGBlurEffect'; document.body.appendChild(blurEffect); } // Over-G ์ง€์† ์‹œ๊ฐ„์— ๋”ฐ๋ผ ์ ์ง„์ ์œผ๋กœ ์–ด๋‘์›Œ์ง // 1.5์ดˆ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์—ฌ ์„œ์„œํžˆ ์ง„ํ–‰ const adjustedTimer = Math.max(0, this.fighter.overGTimer - 1.5); // 1.5์ดˆ ์ดํ›„๋ถ€ํ„ฐ ์นด์šดํŠธ const darknessFactor = Math.min(adjustedTimer / 2.0, 0.7); // 2์ดˆ์— ๊ฑธ์ณ ์ตœ๋Œ€ 70%๊นŒ์ง€๋งŒ ์–ด๋‘์›Œ์ง // ์‹œ์•ผ ๊ฐ€์žฅ์ž๋ฆฌ๋ถ€ํ„ฐ ์–ด๋‘์›Œ์ง€๋Š” ํšจ๊ณผ // ์ค‘์•™์€ ์ƒ๋Œ€์ ์œผ๋กœ ๋Šฆ๊ฒŒ ์–ด๋‘์›Œ์ง const centerTransparency = Math.max(0.3, 1 - darknessFactor * 0.8); // ์ค‘์•™์€ ์ตœ์†Œ 30% ํˆฌ๋ช…๋„ ์œ ์ง€ const midTransparency = Math.max(0.2, 1 - darknessFactor * 0.6); // ์ค‘๊ฐ„ ํˆฌ๋ช…๋„ const edgeOpacity = Math.min(0.8, darknessFactor * 0.8); // ๊ฐ€์žฅ์ž๋ฆฌ ์ตœ๋Œ€ 80% ๋ถˆํˆฌ๋ช… // ๋ถ‰์€ ์ƒ‰์กฐ ์ œ๊ฑฐ, ๊ฒ€์€์ƒ‰๋งŒ ์‚ฌ์šฉ blurEffect.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, rgba(0, 0, 0, ${1 - centerTransparency}) 0%, rgba(0, 0, 0, ${1 - midTransparency}) 50%, rgba(0, 0, 0, ${edgeOpacity}) 80%, rgba(0, 0, 0, ${Math.min(0.85, edgeOpacity + 0.05)}) 100%); pointer-events: none; z-index: 1400; transition: background 0.3s ease-out; `; // ์‹ฌํ•œ Over-G ์ƒํƒœ์—์„œ ์•ฝ๊ฐ„์˜ ํ™”๋ฉด ํ”๋“ค๋ฆผ ํšจ๊ณผ if (darknessFactor > 0.5) { const shake = (1 - Math.random() * 2) * 1; blurEffect.style.transform = `translate(${shake}px, ${shake}px)`; } } else { // Over-G ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ฉด ํšจ๊ณผ ์ œ๊ฑฐ const blurEffect = document.getElementById('overGBlurEffect'); if (blurEffect) { // ๋ถ€๋“œ๋Ÿฌ์šด ์ œ๊ฑฐ๋ฅผ ์œ„ํ•œ ํŽ˜์ด๋“œ์•„์›ƒ blurEffect.style.transition = 'opacity 0.5s ease-out'; blurEffect.style.opacity = '0'; setTimeout(() => { if (blurEffect.parentNode) { blurEffect.remove(); } }, 500); } } } 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); } }); } // ์ƒˆ๋กœ์šด ๋ฉ”์„œ๋“œ: ํƒ„ํ™˜ ํฌ๊ธฐ์˜ ์ง€๋ฉด ์ถฉ๋Œ ํšจ๊ณผ createGroundImpactEffect(position) { // ์ž‘์€ ํ”Œ๋ž˜์‹œ const flashGeometry = new THREE.SphereGeometry(3, 8, 8); const flashMaterial = new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 0.8 }); const flash = new THREE.Mesh(flashGeometry, flashMaterial); flash.position.copy(position); flash.position.y = 0.5; this.scene.add(flash); // ์ž‘์€ ํŒŒํŽธ ํŒŒํ‹ฐํด const particleCount = 10; const particles = []; for (let i = 0; i < particleCount; i++) { // ๋ถˆ๊ฝƒ ํŒŒํ‹ฐํด const particleGeometry = new THREE.SphereGeometry(0.3 + Math.random() * 0.3, 4, 4); const particleMaterial = new THREE.MeshBasicMaterial({ color: Math.random() > 0.5 ? 0xffaa00 : 0xff6600, transparent: true, opacity: 1.0 }); const particle = new THREE.Mesh(particleGeometry, particleMaterial); particle.position.copy(position); particle.position.y = 0.2; // ์ ๋‹นํ•œ ์†๋„ const angle = Math.random() * Math.PI * 2; const speed = 10 + Math.random() * 30; const upSpeed = 15 + Math.random() * 25; particle.velocity = new THREE.Vector3( Math.cos(angle) * speed, upSpeed, Math.sin(angle) * speed ); particle.life = 0.5; // 0.5์ดˆ ์ง€์† this.scene.add(particle); particles.push(particle); } // ์ž‘์€ ๋จผ์ง€ ํšจ๊ณผ const dustCount = 3; const dusts = []; for (let i = 0; i < dustCount; i++) { const dustGeometry = new THREE.SphereGeometry(1 + Math.random() * 1, 6, 6); const dustMaterial = new THREE.MeshBasicMaterial({ color: 0x8b7355, transparent: true, opacity: 0.4 }); const dust = new THREE.Mesh(dustGeometry, dustMaterial); dust.position.copy(position); dust.position.y = 0.5; dust.position.x += (Math.random() - 0.5) * 2; dust.position.z += (Math.random() - 0.5) * 2; dust.velocity = new THREE.Vector3( (Math.random() - 0.5) * 5, 3 + Math.random() * 5, (Math.random() - 0.5) * 5 ); dust.life = 0.8; this.scene.add(dust); dusts.push(dust); } // ํƒ„์ฐฉ์  ๋งˆํฌ const impactGeometry = new THREE.RingGeometry(0.5, 2, 8); const impactMaterial = new THREE.MeshBasicMaterial({ color: 0x333333, side: THREE.DoubleSide, transparent: true, opacity: 0.5 }); const impact = new THREE.Mesh(impactGeometry, impactMaterial); impact.position.copy(position); impact.position.y = 0.05; impact.rotation.x = -Math.PI / 2; this.scene.add(impact); // ์• ๋‹ˆ๋ฉ”์ด์…˜ const animateImpact = () => { let allDead = true; // ํ”Œ๋ž˜์‹œ if (flash.material.opacity > 0) { flash.material.opacity -= 0.08; flash.scale.multiplyScalar(1.1); allDead = false; } else if (flash.parent) { this.scene.remove(flash); } // ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ particles.forEach(particle => { if (particle.life > 0) { allDead = false; particle.life -= 0.03; // ์œ„์น˜ ์—…๋ฐ์ดํŠธ particle.position.add(particle.velocity.clone().multiplyScalar(0.03)); // ์ค‘๋ ฅ particle.velocity.y -= 2; // ํŽ˜์ด๋“œ ์•„์›ƒ particle.material.opacity = particle.life * 2; // ํฌ๊ธฐ ๊ฐ์†Œ const scale = particle.life * 2; particle.scale.set(scale, scale, scale); } else if (particle.parent) { this.scene.remove(particle); } }); // ๋จผ์ง€ ์—…๋ฐ์ดํŠธ dusts.forEach(dust => { if (dust.life > 0) { allDead = false; dust.life -= 0.02; // ์œ„์น˜ ์—…๋ฐ์ดํŠธ dust.position.add(dust.velocity.clone().multiplyScalar(0.02)); // ์ƒ์Šน ๊ฐ์† dust.velocity.y *= 0.95; // ํ™•์‚ฐ dust.scale.multiplyScalar(1.03); // ํŽ˜์ด๋“œ ์•„์›ƒ dust.material.opacity = dust.life * 0.5; } else if (dust.parent) { this.scene.remove(dust); } }); // ํƒ„์ฐฉ์  ํŽ˜์ด๋“œ if (impact.material.opacity > 0) { impact.material.opacity -= 0.01; allDead = false; } else if (impact.parent) { this.scene.remove(impact); } if (!allDead) { requestAnimationFrame(animateImpact); } }; animateImpact(); // ์ž‘์€ ์ถฉ๋Œ์Œ try { const impactSound = new Audio('sounds/hit.ogg'); impactSound.volume = 0.2; impactSound.play().catch(e => { console.log('Impact sound not found or failed to play'); }); } catch (e) { console.log('Impact sound error:', e); } } checkCollisions() { // ํ”Œ๋ ˆ์ด์–ด์™€ ์ ๊ธฐ์˜ ์ง์ ‘ ์ถฉ๋Œ ์ฒดํฌ (์ตœ์šฐ์„ ) for (let i = this.enemies.length - 1; i >= 0; i--) { const enemy = this.enemies[i]; if (!enemy.mesh || !enemy.isLoaded) continue; const distance = this.fighter.position.distanceTo(enemy.position); // ์ง์ ‘ ์ถฉ๋Œ ํŒ์ • (80m ์ด๋‚ด) if (distance < 80) { console.log('Direct collision detected! Distance:', distance); // ์–‘์ชฝ ๋ชจ๋‘์˜ ์œ„์น˜ ์ €์žฅ (์ถฉ๋Œ ์ค‘๊ฐ„ ์ง€์ ) const collisionPoint = this.fighter.position.clone().add(enemy.position).divideScalar(2); const playerExplosionPos = this.fighter.position.clone(); const enemyExplosionPos = enemy.position.clone(); // ๋” ํฐ ํญ๋ฐœ์Œ ์žฌ์ƒ (์‹œ๊ฐ ํšจ๊ณผ๋ณด๋‹ค ๋จผ์ €) try { const collisionSound = new Audio('sounds/bang.ogg'); collisionSound.volume = 1.0; collisionSound.play().catch(e => {}); // ๋‘ ๋ฒˆ์งธ ํญ๋ฐœ์Œ (์•ฝ๊ฐ„์˜ ๋”œ๋ ˆ์ด) setTimeout(() => { const secondExplosion = new Audio('sounds/bang.ogg'); secondExplosion.volume = 0.8; secondExplosion.play().catch(e => {}); }, 100); } catch (e) {} // 1. ์ถฉ๋Œ ์ง€์ ์— ํฐ ํญ๋ฐœ ํšจ๊ณผ this.createExplosionEffect(collisionPoint); // 2. ์–‘์ชฝ ์œ„์น˜์—๋„ ํญ๋ฐœ ํšจ๊ณผ setTimeout(() => { this.createExplosionEffect(playerExplosionPos); this.createExplosionEffect(enemyExplosionPos); }, 50); // 3. ์ ๊ธฐ ์ œ๊ฑฐ enemy.destroy(); this.enemies.splice(i, 1); this.score += 200; // ์ถฉ๋Œ ํ‚ฌ์€ ๋ณด๋„ˆ์Šค ์ ์ˆ˜ // 4. ํ”Œ๋ ˆ์ด์–ด ํŒŒ๊ดด this.fighter.health = 0; // 5. ๊ฒŒ์ž„ ์ข…๋ฃŒ setTimeout(() => { this.endGame(false, "COLLISION WITH ENEMY"); }, 100); // ํญ๋ฐœ ํšจ๊ณผ๊ฐ€ ๋ณด์ด๋„๋ก ์•ฝ๊ฐ„์˜ ๋”œ๋ ˆ์ด return; // ์ถฉ๋Œ ์ฒ˜๋ฆฌ ํ›„ ์ฆ‰์‹œ ์ข…๋ฃŒ } } // ํ”Œ๋ ˆ์ด์–ด ํƒ„ํ™˜ vs ์ ๊ธฐ ์ถฉ๋Œ 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 < 90) { // ์ ๊ธฐ ์œ„์น˜๋ฅผ ๋ฏธ๋ฆฌ ์ €์žฅ (์ค‘์š”!) const explosionPosition = enemy.position.clone(); // ํžˆํŠธ ํ‘œ์‹œ ์ถ”๊ฐ€ this.showHitMarker(explosionPosition); // ํ”ผ๊ฒฉ ์ดํŽ™ํŠธ ์ถ”๊ฐ€ this.createHitEffect(explosionPosition); // ํƒ„ํ™˜ ์ œ๊ฑฐ this.scene.remove(bullet); this.fighter.bullets.splice(i, 1); if (enemy.takeDamage(GAME_CONSTANTS.BULLET_DAMAGE)) { // ๋””๋ฒ„๊น…์šฉ ๋กœ๊ทธ ์ถ”๊ฐ€ console.log('Enemy destroyed! Creating explosion at:', explosionPosition); // ์ ๊ธฐ ํŒŒ๊ดด ์‹œ ํญ๋ฐœ ํšจ๊ณผ ์ถ”๊ฐ€ - ์ €์žฅ๋œ ์œ„์น˜ ์‚ฌ์šฉ this.createExplosionEffect(explosionPosition); // ์ ๊ธฐ ์ œ๊ฑฐ - ์ฒด๋ ฅ์ด 0 ์ดํ•˜์ผ ๋•Œ๋งŒ ์ œ๊ฑฐ enemy.destroy(); this.enemies.splice(j, 1); this.score += 100; // ์ถ”๊ฐ€ ๋””๋ฒ„๊น… console.log('Enemies remaining:', this.enemies.length); // ์  ์ฒ˜์น˜ ์Œ์„ฑ ์žฌ์ƒ (์ถ”๊ฐ€) try { const killVoice = new Audio('sounds/voice_kill_a.ogg'); killVoice.volume = 0.8; killVoice.play().catch(e => { console.log('Kill voice failed to play:', e); }); } catch (e) { console.log('Kill voice error:', e); } // ์ž๋ง‰ ํ‘œ์‹œ this.showSubtitle('Nice work, one enemy down. Keep going.', 4000); } // else ๋ธ”๋ก ์ œ๊ฑฐ - ์ ์ด ํŒŒ๊ดด๋˜์ง€ ์•Š์•˜์œผ๋ฉด ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š์Œ break; // ํ•˜๋‚˜์˜ ํƒ„ํ™˜์€ ํ•˜๋‚˜์˜ ์ ๋งŒ ๋งž์ถœ ์ˆ˜ ์žˆ์Œ } } } // ์  ํƒ„ํ™˜ vs ํ”Œ๋ ˆ์ด์–ด ์ถฉ๋Œ this.enemies.forEach(enemy => { for (let index = enemy.bullets.length - 1; index >= 0; index--) { const bullet = enemy.bullets[index]; const distance = bullet.position.distanceTo(this.fighter.position); if (distance < 100) { // ํ”Œ๋ ˆ์ด์–ด ์œ„์น˜ ์ €์žฅ const playerPosition = this.fighter.position.clone(); // ํ”Œ๋ ˆ์ด์–ด ํ”ผ๊ฒฉ ์ดํŽ™ํŠธ this.createHitEffect(playerPosition); // ํƒ„ํ™˜ ์ œ๊ฑฐ this.scene.remove(bullet); enemy.bullets.splice(index, 1); if (this.fighter.takeDamage(GAME_CONSTANTS.BULLET_DAMAGE)) { console.log('Player destroyed! Creating explosion'); // ํ”Œ๋ ˆ์ด์–ด ํŒŒ๊ดด ์‹œ ํญ๋ฐœ ํšจ๊ณผ ์ถ”๊ฐ€ this.createExplosionEffect(playerPosition); this.endGame(false); } } } }); // incoming missiles ์—…๋ฐ์ดํŠธ (ํŒŒ๊ดด๋œ ๋ฏธ์‚ฌ์ผ ์ œ๊ฑฐ) this.fighter.incomingMissiles = this.fighter.incomingMissiles.filter(missile => { return missile && missile.mesh && missile.mesh.parent; }); } createHitEffect(position) { // ํ”ผ๊ฒฉ ํŒŒํ‹ฐํด ํšจ๊ณผ ์ƒ์„ฑ const particleCount = 15; const particles = []; for (let i = 0; i < particleCount; i++) { const particleGeometry = new THREE.SphereGeometry(0.5, 4, 4); const particleMaterial = new THREE.MeshBasicMaterial({ color: Math.random() > 0.5 ? 0xffaa00 : 0xff6600, transparent: true, opacity: 1.0 }); const particle = new THREE.Mesh(particleGeometry, particleMaterial); particle.position.copy(position); // ๋žœ๋ค ์†๋„ ์„ค์ • particle.velocity = new THREE.Vector3( (Math.random() - 0.5) * 100, (Math.random() - 0.5) * 100, (Math.random() - 0.5) * 100 ); particle.life = 1.0; // 1์ดˆ ๋™์•ˆ ์ง€์† this.scene.add(particle); particles.push(particle); } // ์ค‘์•™ ํญ๋ฐœ ํ”Œ๋ž˜์‹œ const flashGeometry = new THREE.SphereGeometry(5, 8, 8); const flashMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.8 }); const flash = new THREE.Mesh(flashGeometry, flashMaterial); flash.position.copy(position); this.scene.add(flash); // ํŒŒํ‹ฐํด ์• ๋‹ˆ๋ฉ”์ด์…˜ const animateParticles = () => { let allDead = true; particles.forEach(particle => { if (particle.life > 0) { allDead = false; particle.life -= 0.02; // ์œ„์น˜ ์—…๋ฐ์ดํŠธ particle.position.add(particle.velocity.clone().multiplyScalar(0.02)); // ์ค‘๋ ฅ ํšจ๊ณผ particle.velocity.y -= 2; // ํŽ˜์ด๋“œ ์•„์›ƒ particle.material.opacity = particle.life; // ํฌ๊ธฐ ๊ฐ์†Œ const scale = particle.life; particle.scale.set(scale, scale, scale); } else if (particle.parent) { this.scene.remove(particle); } }); // ํ”Œ๋ž˜์‹œ ํšจ๊ณผ if (flash.material.opacity > 0) { flash.material.opacity -= 0.05; flash.scale.multiplyScalar(1.1); } else if (flash.parent) { this.scene.remove(flash); } if (!allDead || flash.material.opacity > 0) { requestAnimationFrame(animateParticles); } }; animateParticles(); // ํ”ผ๊ฒฉ ์‚ฌ์šด๋“œ ์žฌ์ƒ try { const hitSound = new Audio('sounds/hit.ogg'); hitSound.volume = 0.3; hitSound.play().catch(e => { console.log('Hit sound not found or failed to play'); }); } catch (e) { console.log('Hit sound error:', e); } } // 2. createExplosionEffect ์ˆ˜์ • - emissive ์†์„ฑ ์ œ๊ฑฐ createExplosionEffect(position) { // ์œ„์น˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ if (!position || position.x === undefined) { console.error('Invalid position for explosion effect'); return; } console.log('Creating explosion effect at position:', position); // ํญ๋ฐœ์Œ์„ ๊ฐ€์žฅ ๋จผ์ € ์žฌ์ƒ (์‹œ๊ฐํšจ๊ณผ๋ณด๋‹ค ์šฐ์„ ) try { const explosionSound = new Audio('sounds/bang.ogg'); explosionSound.volume = 1.0; // ์ตœ๋Œ€ ์Œ๋Ÿ‰ // ์ฆ‰์‹œ ์žฌ์ƒ ์‹œ๋„ const playPromise = explosionSound.play(); if (playPromise !== undefined) { playPromise.then(() => { console.log('Explosion sound played successfully'); }).catch(e => { console.error('Explosion sound failed:', e); }); } } catch (e) { console.error('Explosion sound error:', e); } // ๋ฉ”์ธ ํญ๋ฐœ ํ”Œ๋ž˜์‹œ const explosionGeometry = new THREE.SphereGeometry(50, 16, 16); const explosionMaterial = new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 1.0 }); const explosion = new THREE.Mesh(explosionGeometry, explosionMaterial); explosion.position.copy(position); this.scene.add(explosion); // ํญ๋ฐœ ํŒŒํŽธ๋“ค const debrisCount = 30; const debris = []; for (let i = 0; i < debrisCount; i++) { const debrisGeometry = new THREE.BoxGeometry( 2 + Math.random() * 4, 2 + Math.random() * 4, 2 + Math.random() * 4 ); const debrisMaterial = new THREE.MeshBasicMaterial({ color: Math.random() > 0.5 ? 0xff6600 : 0x333333, transparent: true, opacity: 1.0 }); const debrisPiece = new THREE.Mesh(debrisGeometry, debrisMaterial); debrisPiece.position.copy(position); // ๋žœ๋ค ์†๋„์™€ ํšŒ์ „ debrisPiece.velocity = new THREE.Vector3( (Math.random() - 0.5) * 200, (Math.random() - 0.5) * 200, (Math.random() - 0.5) * 200 ); debrisPiece.rotationSpeed = new THREE.Vector3( Math.random() * 10, Math.random() * 10, Math.random() * 10 ); debrisPiece.life = 2.0; this.scene.add(debrisPiece); debris.push(debrisPiece); } // ์—ฐ๊ธฐ ํšจ๊ณผ const smokeCount = 10; const smoke = []; for (let i = 0; i < smokeCount; i++) { const smokeGeometry = new THREE.SphereGeometry(10 + Math.random() * 20, 8, 8); const smokeMaterial = new THREE.MeshBasicMaterial({ color: 0x222222, transparent: true, opacity: 0.8 }); const smokePuff = new THREE.Mesh(smokeGeometry, smokeMaterial); smokePuff.position.copy(position); smokePuff.position.add(new THREE.Vector3( (Math.random() - 0.5) * 20, (Math.random() - 0.5) * 20, (Math.random() - 0.5) * 20 )); smokePuff.velocity = new THREE.Vector3( (Math.random() - 0.5) * 50, Math.random() * 50 + 20, (Math.random() - 0.5) * 50 ); smokePuff.life = 3.0; this.scene.add(smokePuff); smoke.push(smokePuff); } // ์• ๋‹ˆ๋ฉ”์ด์…˜ const animateExplosion = () => { let allDead = true; // ๋ฉ”์ธ ํญ๋ฐœ ์• ๋‹ˆ๋ฉ”์ด์…˜ if (explosion.material.opacity > 0) { explosion.material.opacity -= 0.02; explosion.scale.multiplyScalar(1.08); allDead = false; } else if (explosion.parent) { this.scene.remove(explosion); } // ํŒŒํŽธ ์• ๋‹ˆ๋ฉ”์ด์…˜ debris.forEach(piece => { if (piece.life > 0) { allDead = false; piece.life -= 0.02; // ์œ„์น˜ ์—…๋ฐ์ดํŠธ piece.position.add(piece.velocity.clone().multiplyScalar(0.02)); // ์ค‘๋ ฅ piece.velocity.y -= 3; // ํšŒ์ „ piece.rotation.x += piece.rotationSpeed.x * 0.02; piece.rotation.y += piece.rotationSpeed.y * 0.02; piece.rotation.z += piece.rotationSpeed.z * 0.02; // ํŽ˜์ด๋“œ ์•„์›ƒ piece.material.opacity = piece.life / 2; } else if (piece.parent) { this.scene.remove(piece); } }); // ์—ฐ๊ธฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ smoke.forEach(puff => { if (puff.life > 0) { allDead = false; puff.life -= 0.02; // ์œ„์น˜ ์—…๋ฐ์ดํŠธ puff.position.add(puff.velocity.clone().multiplyScalar(0.02)); // ์ƒ์Šน ๊ฐ์† puff.velocity.y *= 0.98; puff.velocity.x *= 0.98; puff.velocity.z *= 0.98; // ํ™•์‚ฐ puff.scale.multiplyScalar(1.02); // ํŽ˜์ด๋“œ ์•„์›ƒ puff.material.opacity = (puff.life / 3) * 0.8; } else if (puff.parent) { this.scene.remove(puff); } }); if (!allDead) { requestAnimationFrame(animateExplosion); } }; animateExplosion(); } showHitMarker(position) { // ํžˆํŠธ ๋งˆ์ปค div ์ƒ์„ฑ const hitMarker = document.createElement('div'); hitMarker.style.cssText = ` position: fixed; color: #ff0000; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); z-index: 1500; pointer-events: none; animation: hitFade 0.5s ease-out forwards; `; hitMarker.textContent = 'HIT'; // 3D ์œ„์น˜๋ฅผ ํ™”๋ฉด ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜ const screenPos = this.getScreenPosition(position); if (screenPos) { hitMarker.style.left = `${screenPos.x}px`; hitMarker.style.top = `${screenPos.y}px`; hitMarker.style.transform = 'translate(-50%, -50%)'; document.body.appendChild(hitMarker); // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ ์ถ”๊ฐ€ if (!document.getElementById('hitAnimation')) { const style = document.createElement('style'); style.id = 'hitAnimation'; style.innerHTML = ` @keyframes hitFade { 0% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -100%) scale(1.5); } } `; document.head.appendChild(style); } // 0.5์ดˆ ํ›„ ์ œ๊ฑฐ setTimeout(() => { hitMarker.remove(); }, 500); } } 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.isStarted) { // ํ‚ค ์ƒํƒœ ๋””๋ฒ„๊น… if (this.keys.w || this.keys.s || this.keys.a || this.keys.d) { console.log('animate() - Keys state:', this.keys); } // Fํ‚ค ์ƒํƒœ๋ฅผ Fighter์— ์ „๋‹ฌ this.fighter.escapeKeyPressed = this.keys.g; // ์ปจํŠธ๋กค ์—…๋ฐ์ดํŠธ - ๋ฐ˜๋“œ์‹œ ๋ฌผ๋ฆฌ ์—…๋ฐ์ดํŠธ ์ „์— ํ˜ธ์ถœ this.fighter.updateControls(this.keys, deltaTime); // ๋ฝ์˜จ ์—…๋ฐ์ดํŠธ (AIM-9 ๋ชจ๋“œ์ผ ๋•Œ๋งŒ) this.fighter.updateLockOn(this.enemies, deltaTime); // ๋ฌผ๋ฆฌ ์—…๋ฐ์ดํŠธ this.fighter.updatePhysics(deltaTime); // ํƒ„ํ™˜ ์—…๋ฐ์ดํŠธ this.fighter.updateBullets(this.scene, deltaTime, this); // ๋งˆ์šฐ์Šค ๋ˆ„๋ฆ„ ์ƒํƒœ์ผ ๋•Œ ์—ฐ์† ๋ฐœ์‚ฌ if (this.fighter.isMouseDown) { const currentShootTime = Date.now(); if (!this.lastShootTime || currentShootTime - this.lastShootTime >= 100) { this.fighter.shoot(this.scene); this.lastShootTime = currentShootTime; } } // ์ ๊ธฐ ์—…๋ฐ์ดํŠธ this.enemies.forEach(enemy => { enemy.nearbyEnemies = this.enemies; }); this.enemies.forEach(enemy => { enemy.update(this.fighter.position, deltaTime); }); // ์ถฉ๋Œ ์ฒดํฌ this.checkCollisions(); // ๊ฒŒ์ž„ ์ข…๋ฃŒ ์กฐ๊ฑด ์ฒดํฌ if (this.fighter.health <= 0) { if (this.fighter.position.y <= 0) { this.endGame(false, "GROUND COLLISION"); } else { this.endGame(false); } return; } // UI ์—…๋ฐ์ดํŠธ this.updateUI(); this.updateRadar(); // ์ ์ด ๋ชจ๋‘ ์ œ๊ฑฐ๋˜์—ˆ๋Š”์ง€ ์ฒดํฌ if (this.enemies.length === 0) { this.endGame(true); } } else if (this.isLoaded && this.fighter.isLoaded) { // ๊ฒŒ์ž„์ด ์‹œ์ž‘๋˜์ง€ ์•Š์•˜์„ ๋•Œ๋„ ๋ฌผ๋ฆฌ๋Š” ์—…๋ฐ์ดํŠธ (์นด๋ฉ”๋ผ ์›€์ง์ž„์„ ์œ„ํ•ด) this.fighter.updatePhysics(deltaTime); } // ๊ตฌ๋ฆ„ ์• ๋‹ˆ๋ฉ”์ด์…˜ if (this.clouds) { this.clouds.forEach(cloud => { cloud.userData.time += deltaTime; cloud.position.x += cloud.userData.driftSpeed; cloud.position.y = cloud.userData.initialY + Math.sin(cloud.userData.time * cloud.userData.floatSpeed) * 20; const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2; if (cloud.position.x > mapLimit) cloud.position.x = -mapLimit; if (cloud.position.x < -mapLimit) cloud.position.x = mapLimit; }); } // ์นด๋ฉ”๋ผ ์—…๋ฐ์ดํŠธ if (this.fighter.isLoaded) { const targetCameraPos = this.fighter.getCameraPosition(); const targetCameraTarget = this.fighter.getCameraTarget(); this.camera.position.lerp(targetCameraPos, this.fighter.cameraLag); this.camera.lookAt(targetCameraTarget); } this.renderer.render(this.scene, this.camera); } endGame(victory = false, reason = "") { this.isGameOver = true; if (this.fighter && this.fighter.stopAllWarningAudios) { this.fighter.stopAllWarningAudios(); } if (this.bgm) { this.bgm.pause(); this.bgm = null; this.bgmPlaying = false; } if (this.gameTimer) { clearInterval(this.gameTimer); } document.exitPointerLock(); // ๋ชจ๋“  ๊ฒฝ๊ณ  ๋ฐ ํšจ๊ณผ ์ œ๊ฑฐ const existingWarnings = document.querySelectorAll('.warning-message, .stall-escape-warning, .missile-warning'); existingWarnings.forEach(w => w.remove()); const blurEffect = document.getElementById('overGBlurEffect'); if (blurEffect) { blurEffect.remove(); } const altitudeEffect = document.getElementById('altitudeEdgeEffect'); if (altitudeEffect) { altitudeEffect.remove(); } const gameOverDiv = document.createElement('div'); gameOverDiv.className = 'start-screen'; gameOverDiv.style.display = 'flex'; gameOverDiv.innerHTML = ` <h1 style="color: ${victory ? '#0f0' : '#f00'}; font-size: 48px;"> ${victory ? 'MISSION ACCOMPLISHED!' : 'SHOT DOWN!'} </h1> ${reason ? `<div style="color: #ff0000; font-size: 20px; margin: 10px 0;">${reason}</div>` : ''} <div style="color: #0f0; font-size: 24px; margin: 20px 0;"> Final Score: ${this.score}<br> Enemies Destroyed: ${GAME_CONSTANTS.ENEMY_COUNT - this.enemies.length}<br> Mission Time: ${GAME_CONSTANTS.MISSION_DURATION - this.gameTime}s </div> <button class="start-button" onclick="location.reload()"> New Mission </button> `; document.body.appendChild(gameOverDiv); } } // ์ „์—ญ ํ•จ์ˆ˜ ๋ฐ ์ด๋ฒคํŠธ window.gameInstance = null; window.startGame = function() { if (!window.gameInstance || !window.gameInstance.isLoaded || !window.gameInstance.isBGMReady) { console.log('๊ฒŒ์ž„์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค...'); return; } gameStarted = true; document.getElementById('startScreen').style.display = 'none'; document.body.requestPointerLock(); window.gameInstance.startBGM(); window.gameInstance.startGame(); } function showPointerLockNotification() { const existingNotification = document.getElementById('pointerLockNotification'); if (existingNotification) { existingNotification.remove(); } const notification = document.createElement('div'); notification.id = 'pointerLockNotification'; notification.innerHTML = 'Click to resume control'; notification.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.8); color: #00ff00; padding: 20px; border: 2px solid #00ff00; border-radius: 10px; font-size: 18px; z-index: 2001; text-align: center; pointer-events: none; `; document.body.appendChild(notification); const removeNotification = () => { if (document.pointerLockElement) { notification.remove(); document.removeEventListener('pointerlockchange', removeNotification); } }; document.addEventListener('pointerlockchange', removeNotification); } document.addEventListener('pointerlockchange', () => { if (document.pointerLockElement === document.body) { console.log('Pointer locked'); } else { console.log('Pointer unlocked'); if (gameStarted && window.gameInstance && !window.gameInstance.isGameOver) { console.log('๊ฒŒ์ž„ ์ค‘ ํฌ์ธํ„ฐ ๋ฝ ํ•ด์ œ๋จ - ํด๋ฆญํ•˜์—ฌ ๋‹ค์‹œ ์ž ๊ทธ์„ธ์š”'); showPointerLockNotification(); } } }); document.addEventListener('click', (event) => { if (!gameStarted && !event.target.classList.contains('start-button')) { event.preventDefault(); event.stopPropagation(); return false; } if (gameStarted && window.gameInstance && !window.gameInstance.isGameOver) { if (!document.pointerLockElement) { console.log('๊ฒŒ์ž„ ์ค‘ ํด๋ฆญ - ํฌ์ธํ„ฐ ๋ฝ ์žฌ์š”์ฒญ'); document.body.requestPointerLock(); } // ํด๋ฆญ์œผ๋กœ ๋‹จ๋ฐœ ์‚ฌ๊ฒฉ ์ œ๊ฑฐ (๋งˆ์šฐ์Šค ๋‹ค์šด/์—… ์ด๋ฒคํŠธ๋กœ ์ฒ˜๋ฆฌ) } }, true); window.addEventListener('gameReady', () => { gameCanStart = true; }); document.addEventListener('DOMContentLoaded', () => { console.log('์ „ํˆฌ๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘...'); const game = new Game(); // window.gameInstance = ์ œ๊ฑฐ });
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// ๊ฒŒ์ž„ ์ƒํƒœ ์ถ”์  ๋ณ€์ˆ˜
let gameCanStart = false;
let gameStarted = false;
// ๊ฒŒ์ž„ ์ƒ์ˆ˜
const GAME_CONSTANTS = {
MISSION_DURATION: 180,
MAP_SIZE: 15000,
MAX_ALTITUDE: 15000,
MIN_ALTITUDE: 0,
MAX_SPEED: 800,
STALL_SPEED: 300, // 300kt ์ดํ•˜์—์„œ ์Šคํ†จ ์œ„ํ—˜
GRAVITY: 9.8,
MOUSE_SENSITIVITY: 0.001,
MAX_G_FORCE: 12.0,
ENEMY_COUNT: 4,
MISSILE_COUNT: 6,
AMMO_COUNT: 940, // 940๋ฐœ๋กœ ๋ณ€๊ฒฝ
BULLET_DAMAGE: 50, // ๋ฐœ๋‹น 50 ๋ฐ๋ฏธ์ง€
MAX_HEALTH: 1000, // ์ฒด๋ ฅ 1000
AIM9_COUNT: 8, // AIM-9 ๋ฏธ์‚ฌ์ผ 8๋ฐœ
AIM9_DAMAGE: 1000, // AIM-9 ๋ฏธ์‚ฌ์ผ ํ”ผํ•ด 1000!
AIM9_SPEED: 514.4, // 1000kt๋ฅผ m/s๋กœ ๋ณ€ํ™˜
AIM9_LOCK_RANGE: 6000, // ๋ฝ์˜จ ๊ฑฐ๋ฆฌ 6000m
AIM9_LOCK_REQUIRED: 3, // 3๋ฒˆ ๋ฝ์˜จ ํ•„์š”
ENEMY_AIM9_COUNT: 4, // ์  AIM-9 ๋ฏธ์‚ฌ์ผ ์ˆ˜
ENEMY_LOCK_RANGE: 5000, // ์  ๋ฝ์˜จ ๊ฑฐ๋ฆฌ
ENEMY_LOCK_ANGLE: Math.PI / 12 // ์  ๋ฝ์˜จ ๊ฐ๋„ (15๋„)
};
// ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค
class Fighter {
constructor() {
this.mesh = null;
this.isLoaded = false;
// ๋ฌผ๋ฆฌ ์†์„ฑ
this.position = new THREE.Vector3(0, 2000, 0);
this.velocity = new THREE.Vector3(0, 0, 350); // ์ดˆ๊ธฐ ์†๋„ 350kt
this.acceleration = new THREE.Vector3(0, 0, 0);
this.rotation = new THREE.Euler(0, 0, 0, 'YXZ'); // ํ•ญ๊ณต๊ธฐ ํ‘œ์ค€ ์˜ค์ผ๋Ÿฌ ์ˆœ์„œ
// ๋น„ํ–‰ ์ œ์–ด
this.throttle = 0.7; // ์ดˆ๊ธฐ ์Šค๋กœํ‹€ 70%
this.speed = 350; // ์ดˆ๊ธฐ ์†๋„ 350kt
this.altitude = 2000;
this.gForce = 1.0;
this.health = GAME_CONSTANTS.MAX_HEALTH; // ์ฒด๋ ฅ 1000
// ์กฐ์ข… ์ž…๋ ฅ ์‹œ์Šคํ…œ
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.isMouseDown = false; // ๋งˆ์šฐ์Šค ๋ˆ„๋ฆ„ ์ƒํƒœ ์ถ”์ 
this.gunfireAudios = []; // ๊ธฐ๊ด€์ด ์†Œ๋ฆฌ ๋ฐฐ์—ด (์ตœ๋Œ€ 5๊ฐœ)
// ํ”Œ๋ ˆ์–ด ์‹œ์Šคํ…œ
this.flareCount = 3; // ํ”Œ๋ ˆ์–ด 3ํšŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
this.deployedFlares = []; // ๋ฐฐ์น˜๋œ ํ”Œ๋ ˆ์–ด ๋ฐฐ์—ด
this.lastFlareTime = 0; // ๋งˆ์ง€๋ง‰ ํ”Œ๋ ˆ์–ด ์‚ฌ์šฉ ์‹œ๊ฐ„
// AIM-9 ๋ฏธ์‚ฌ์ผ ์‹œ์Šคํ…œ
this.aim9Missiles = GAME_CONSTANTS.AIM9_COUNT;
this.firedMissiles = []; // ๋ฐœ์‚ฌ๋œ ๋ฏธ์‚ฌ์ผ ๋ฐฐ์—ด
this.currentWeapon = 'MG'; // 'MG' ๋˜๋Š” 'AIM9'
this.lockTarget = null; // ํ˜„์žฌ ๋ฝ์˜จ ์ค‘์ธ ํƒ€๊ฒŸ
this.lockProgress = 0; // ๋ฝ์˜จ ์ง„ํ–‰๋„ (0~3)
this.lastLockTime = 0; // ๋งˆ์ง€๋ง‰ ๋ฝ์˜จ ์‹œ๋„ ์‹œ๊ฐ„
this.lockAudios = {
locking: null,
locked: null
};
this.initializeLockAudios();
// ์Šคํ†จ ํƒˆ์ถœ์„ ์œ„ํ•œ Fํ‚ค ์ƒํƒœ
this.escapeKeyPressed = false;
this.stallEscapeProgress = 0; // ์Šคํ†จ ํƒˆ์ถœ ์ง„ํ–‰๋„ (0~2์ดˆ)
// ์นด๋ฉ”๋ผ ์„ค์ •
this.cameraDistance = 250;
this.cameraHeight = 30;
this.cameraLag = 0.06;