Spaces:
Running
Running
| 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; | |
| } | |
| } | |
| } | |
| 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!'); | |
| } | |
| 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, r: false }; | |
| this.isStarted = 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; | |
| // gameStarted ๋์ this.isStarted ์ฌ์ฉ | |
| 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 'KeyR': | |
| if (!event.repeat) { // ํค๋ฅผ ๋๋ฅด๊ณ ์์ ๋ ๋ฐ๋ณต ๋ฐฉ์ง | |
| 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 '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) { | |
| 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(); | |
| } | |
| 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 F 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); | |
| } | |
| // 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.f; | |
| // ์ปจํธ๋กค ์ ๋ฐ์ดํธ - ๋ฐ๋์ ๋ฌผ๋ฆฌ ์ ๋ฐ์ดํธ ์ ์ ํธ์ถ | |
| 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 = ์ ๊ฑฐ | |
| }); |