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: 300 | |
}; | |
// ์ ํฌ๊ธฐ ํด๋์ค | |
class Fighter { | |
constructor() { | |
this.mesh = null; | |
this.isLoaded = false; | |
// ๋ฌผ๋ฆฌ ์์ฑ | |
this.position = new THREE.Vector3(0, 2000, 0); | |
this.velocity = new THREE.Vector3(0, 0, 350); // ์ด๊ธฐ ์๋ 350kt | |
this.acceleration = new THREE.Vector3(0, 0, 0); | |
this.rotation = new THREE.Euler(0, 0, 0); | |
// ๋นํ ์ ์ด | |
this.throttle = 0.6; // ์ด๊ธฐ ์ค๋กํ 60% | |
this.speed = 350; // ์ด๊ธฐ ์๋ 350kt | |
this.altitude = 2000; | |
this.gForce = 1.0; | |
this.health = 100; | |
// ์กฐ์ข ์ ๋ ฅ ์์คํ | |
this.pitchInput = 0; | |
this.rollInput = 0; | |
this.yawInput = 0; | |
// ๋ง์ฐ์ค ๋์ ์ ๋ ฅ | |
this.mousePitch = 0; | |
this.mouseRoll = 0; | |
// ๋ถ๋๋ฌ์ด ํ์ ์ ์ํ ๋ชฉํ๊ฐ | |
this.targetPitch = 0; | |
this.targetRoll = 0; | |
this.targetYaw = 0; | |
// ๋ฌด๊ธฐ | |
this.missiles = GAME_CONSTANTS.MISSILE_COUNT; | |
this.ammo = GAME_CONSTANTS.AMMO_COUNT; | |
this.bullets = []; | |
this.lastShootTime = 0; | |
// ์คํจ ํ์ถ์ ์ํ Fํค ์ํ | |
this.escapeKeyPressed = false; | |
// ์นด๋ฉ๋ผ ์ค์ | |
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 // ์์ง ์๋ฆฌ ์ถ๊ฐ | |
}; | |
this.initializeWarningAudios(); | |
} | |
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; // ์์ง ์๋ฆฌ๋ ๊ณ์ ๋ฐ๋ณต | |
// ๊ฒฝ๊ณ ์์๋ง ended ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ถ๊ฐ (์์ง ์๋ฆฌ ์ ์ธ) | |
Object.keys(this.warningAudios).forEach(key => { | |
if (key !== 'normal' && this.warningAudios[key]) { | |
this.warningAudios[key].addEventListener('ended', () => { | |
this.updateWarningAudios(); | |
}); | |
} | |
}); | |
} catch (e) { | |
console.log('Warning 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.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 !== 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; | |
} | |
}); | |
} | |
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 ์ ํฌ๊ธฐ ๋ชจ๋ธ ์์ฑ ์๋ฃ'); | |
} | |
updateMouseInput(deltaX, deltaY) { | |
const sensitivity = GAME_CONSTANTS.MOUSE_SENSITIVITY * 1.0; | |
// ๋ง์ฐ์ค Y์ถ: ํผ์น(๊ธฐ์ ์ํ) | |
this.targetPitch -= deltaY * sensitivity; | |
// ๋ง์ฐ์ค X์ถ: ์(Yaw) - ๋ชธ์ฒด๋ฅผ ์ข์ฐ๋ก ํ์ | |
this.targetYaw += deltaX * sensitivity * 0.8; // ์ ํ์ ๊ฐ๋ ์กฐ์ | |
// ์ ํ์ ์ ๋ฐ๋ฅธ ์๋ ๋กค (๋ฑ ํฌ ํด) | |
// ์ข์ฐ๋ก ํ์ ํ ๋ ์์ฐ์ค๋ฝ๊ฒ ๋ ๊ฐ๊ฐ ๊ธฐ์ธ์ด์ง | |
const yawRate = deltaX * sensitivity * 0.8; | |
this.targetRoll = -yawRate * 15; // ์ ํ์ ๋์ ๋น๋กํ์ฌ ๋กค ๋ฐ์ | |
// ๊ฐ๋ ์ ํ | |
const maxPitchAngle = Math.PI / 3; // 60๋ | |
const maxRollAngle = Math.PI * 0.5; // 90๋๋ก ์ ํ (์๋ ๋กค) | |
this.targetPitch = Math.max(-maxPitchAngle, Math.min(maxPitchAngle, this.targetPitch)); | |
// ์๋ ๋กค์ ์ ํ๋ ๋ฒ์ ๋ด์์๋ง ์๋ | |
if (Math.abs(this.targetRoll) < maxRollAngle) { | |
// ๋กค ๊ฐ๋ ์ ํ ์ ์ฉ | |
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 * 0.4; // ๊ฐ์๋ ๋ฌ๋ ํจ๊ณผ | |
} | |
if (keys.d) { | |
this.targetYaw += deltaTime * 0.4; // ๊ฐ์๋ ๋ฌ๋ ํจ๊ณผ | |
} | |
} | |
updatePhysics(deltaTime) { | |
if (!this.mesh) return; | |
// ๋ถ๋๋ฌ์ด ํ์ ๋ณด๊ฐ | |
const rotationSpeed = deltaTime * 2.0; | |
this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetPitch, rotationSpeed); | |
this.rotation.y = THREE.MathUtils.lerp(this.rotation.y, this.targetYaw, rotationSpeed); | |
// ๋กค ์๋ ๋ณต๊ท ์์คํ | |
if (Math.abs(this.targetYaw - this.rotation.y) < 0.05) { | |
// ์ ํ์ ์ด ๊ฑฐ์ ์์ ๋ ๋กค์ 0์ผ๋ก ๋ณต๊ท | |
this.targetRoll *= 0.95; | |
} | |
this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRoll, rotationSpeed * 1.5); // ๋กค์ ๋ ๋น ๋ฅด๊ฒ ๋ฐ์ | |
// ์์ฌ๋ ์คํ์ผ: ์ ํ์ ์ด ์ฃผ๋์ , ๋กค์ ๋ณด์กฐ์ | |
// ๋กค์ ๋ฐ๋ฅธ ์ถ๊ฐ ์ ํ์ ์ ์ ๊ฑฐํ๊ฑฐ๋ ์ต์ํ | |
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; | |
} | |
// ํ์ค์ ์ธ ์๋ ๊ณ์ฐ | |
const minSpeed = 0; // ์ต์ ์๋ 0kt | |
const maxSpeed = 600; // ์ต๋ ์๋ 600kt | |
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 ์ถ๊ฐ | |
let abnormalG = 0; | |
// ๋ค์งํ ์ํ (๋กค์ด 90๋ ์ด์) | |
const isInverted = Math.abs(this.rotation.z) > Math.PI / 2; | |
if (isInverted) { | |
abnormalG += (3.0 + Math.abs(Math.abs(this.rotation.z) - Math.PI / 2) * 2) * altitudeMultiplier; // ๊ณ ๋ ๋ฐฐ์จ ์ ์ฉ | |
} | |
// ๊ธฐ์๊ฐ ๊ณ์ ์๋ฅผ ํฅํ๊ณ ์๋ ๊ฒฝ์ฐ (ํผ์น๊ฐ -30๋ ์ดํ) | |
if (pitchAngle < -Math.PI / 6) { | |
abnormalG += (2.0 + Math.abs(pitchAngle + Math.PI / 6) * 3) * altitudeMultiplier; // ๊ณ ๋ ๋ฐฐ์จ ์ ์ฉ | |
} | |
// ๊ธฐ์๊ฐ ๊ณผ๋ํ๊ฒ ์๋๋ฅผ ํฅํ๊ณ ์๋ ๊ฒฝ์ฐ (ํผ์น๊ฐ 60๋ ์ด์) | |
if (pitchAngle > Math.PI / 3) { | |
abnormalG += (2.0 + Math.abs(pitchAngle - Math.PI / 3) * 3) * altitudeMultiplier; // ๊ณ ๋ ๋ฐฐ์จ ์ ์ฉ | |
} | |
// ๊ธ๊ฒฉํ ๊ธฐ๋์ ์ํ G-Force | |
const maneuverG = turnRate + pitchRate + (Math.abs(this.rotation.z) * 3); | |
// ์ด G-Force ๊ณ์ฐ | |
this.gForce = 1.0 + maneuverG + abnormalG; | |
// ์ ์ ์์ธ๋ก ๋์์ค๋ฉด G-Force ์์ฐ์ค๋ฝ๊ฒ ๊ฐ์ | |
if (!isInverted && pitchAngle > -Math.PI / 6 && pitchAngle < Math.PI / 6) { | |
// ์ ์ ์์ธ์ผ ๋ G-Force๋ฅผ ๋ถ๋๋ฝ๊ฒ ๊ฐ์ | |
this.gForce = THREE.MathUtils.lerp(this.gForce, 1.0 + maneuverG, deltaTime * 2.0); | |
} | |
this.overG = this.gForce > this.maxGForce; | |
// Over-G ํ์ด๋จธ ์ ๋ฐ์ดํธ | |
if (this.overG) { | |
this.overGTimer += deltaTime; | |
// 3์ด ์ด์ Over-G ์ํ์ผ ๊ฒฝ์ฐ | |
if (this.overGTimer > 3.0) { | |
// ์๋ ๊ธ๊ฒฉํ ๊ฐ์ (0kt๊น์ง) | |
targetSpeed *= Math.max(0, 1 - (this.overGTimer - 3.0) * 0.5); | |
// ์์ผ ํ๋ฆผ ํจ๊ณผ๋ UI์์ ์ฒ๋ฆฌ | |
} | |
} 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; | |
} | |
// ์คํจ ํ์ถ ์กฐ๊ฑด: Fํค๋ฅผ ๋๋ฅด๊ณ ์๊ณ , ๊ธฐ์๊ฐ ์๋๋ฅผ ํฅํ๊ณ ์๊ณ , ์๋๊ฐ ์ถฉ๋ถํ ๋ | |
if (this.stallWarning && this.escapeKeyPressed && pitchAngle > 0.2 && speedKnots > GAME_CONSTANTS.STALL_SPEED + 50) { | |
// Fํค๋ฅผ ๋๋ฅด๊ณ ์์ ๋๋ง ์คํจ ํ์ถ | |
this.stallWarning = false; | |
} | |
// ์๋ ๋ณํ ์ ์ฉ | |
if (this.stallWarning) { | |
// ์คํจ ์ํ์์์ ์๋ ๋ณํ | |
if (pitchAngle > 0.1) { // ๊ธฐ์๊ฐ ์๋๋ฅผ ํฅํ ๋ | |
// ๋ค์ด๋น์ผ๋ก ์ธํ ์๋ ์ฆ๊ฐ | |
const diveSpeedGain = Math.min(pitchAngle * 300, 200); // ์ต๋ 200m/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 / 3, this.targetPitch + deltaTime * 2.0); // ๊ธฐ์๊ฐ ๋น ๋ฅด๊ฒ ์๋๋ก | |
// ์กฐ์ข ๋ถ๋ฅ ์ํ | |
this.rotation.x += (Math.random() - 0.5) * deltaTime * 0.5; | |
this.rotation.z += (Math.random() - 0.5) * deltaTime * 0.5; | |
// ์ค๋ ฅ์ ์ํ ๊ฐ์ | |
const gravityAcceleration = GAME_CONSTANTS.GRAVITY * deltaTime * 3.0; // 3๋ฐฐ ์ค๋ ฅ | |
this.velocity.y -= gravityAcceleration; | |
} | |
// ์๋ ๋ฒกํฐ ๊ณ์ฐ | |
const noseDirection = new THREE.Vector3(0, 0, 1); | |
noseDirection.applyEuler(this.rotation); | |
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 = (this.speed / maxSpeed) * 0.8; | |
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; | |
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); | |
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; | |
// ๊ฒฝ๊ณ ๊น๋นก์ ํ์ด๋จธ | |
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 | |
} | |
} | |
shoot(scene) { | |
const currentTime = Date.now(); | |
if (currentTime - this.lastShootTime < 100 || this.ammo <= 0) return; | |
this.lastShootTime = currentTime; | |
this.ammo--; | |
const bulletGeometry = new THREE.SphereGeometry(0.2); | |
const bulletMaterial = new THREE.MeshBasicMaterial({ | |
color: 0xffff00, | |
emissive: 0xffff00, | |
emissiveIntensity: 0.7 | |
}); | |
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); | |
const muzzleOffset = new THREE.Vector3(0, 0, 8); | |
muzzleOffset.applyEuler(this.rotation); | |
bullet.position.copy(this.position).add(muzzleOffset); | |
const bulletSpeed = 1000; | |
const direction = new THREE.Vector3(0, 0, 1); | |
direction.applyEuler(this.rotation); | |
bullet.velocity = direction.multiplyScalar(bulletSpeed).add(this.velocity); | |
scene.add(bullet); | |
this.bullets.push(bullet); | |
try { | |
const audio = new Audio('sounds/gunfire.ogg'); | |
if (audio) { | |
audio.volume = 0.3; | |
audio.play().catch(e => console.log('Gunfire sound failed to play')); | |
} | |
} catch (e) {} | |
} | |
updateBullets(scene, deltaTime) { | |
for (let i = this.bullets.length - 1; i >= 0; i--) { | |
const bullet = this.bullets[i]; | |
bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime)); | |
if (bullet.position.distanceTo(this.position) > 8000 || | |
bullet.position.y < 0 || | |
bullet.position.y > GAME_CONSTANTS.MAX_ALTITUDE + 500) { | |
scene.remove(bullet); | |
this.bullets.splice(i, 1); | |
} | |
} | |
} | |
takeDamage(damage) { | |
this.health -= damage; | |
return this.health <= 0; | |
} | |
getCameraPosition() { | |
const backward = new THREE.Vector3(0, 0, -1); | |
const up = new THREE.Vector3(0, 1, 0); | |
backward.applyEuler(this.rotation); | |
up.applyEuler(this.rotation); | |
const cameraPosition = this.position.clone() | |
.add(backward.multiplyScalar(this.cameraDistance)) | |
.add(up.multiplyScalar(this.cameraHeight)); | |
return cameraPosition; | |
} | |
getCameraTarget() { | |
return this.position.clone(); | |
} | |
} | |
// ์ ์ ํฌ๊ธฐ ํด๋์ค | |
class EnemyFighter { | |
constructor(scene, position) { | |
this.mesh = null; | |
this.isLoaded = false; | |
this.scene = scene; | |
this.position = position.clone(); | |
this.velocity = new THREE.Vector3(0, 0, 120); | |
this.rotation = new THREE.Euler(0, 0, 0); | |
this.health = 100; | |
this.speed = 120; | |
this.bullets = []; | |
this.lastShootTime = 0; | |
this.aiState = 'patrol'; | |
this.targetPosition = position.clone(); | |
this.patrolCenter = position.clone(); | |
this.patrolRadius = 2000; | |
this.lastStateChange = 0; | |
} | |
async initialize(loader) { | |
try { | |
const result = await loader.loadAsync('models/mig-29.glb'); | |
this.mesh = result.scene; | |
this.mesh.position.copy(this.position); | |
this.mesh.scale.set(1.5, 1.5, 1.5); | |
this.mesh.rotation.y = 3 * Math.PI / 2; | |
this.mesh.traverse((child) => { | |
if (child.isMesh) { | |
child.castShadow = true; | |
child.receiveShadow = true; | |
} | |
}); | |
this.scene.add(this.mesh); | |
this.isLoaded = true; | |
console.log('MiG-29 ์ ๊ธฐ ๋ก๋ฉ ์๋ฃ'); | |
} catch (error) { | |
console.error('MiG-29 ๋ชจ๋ธ ๋ก๋ฉ ์คํจ:', error); | |
this.createFallbackModel(); | |
} | |
} | |
createFallbackModel() { | |
const group = new THREE.Group(); | |
const fuselageGeometry = new THREE.CylinderGeometry(0.6, 1.0, 8, 8); | |
const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x800000 }); | |
const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial); | |
fuselage.rotation.x = -Math.PI / 2; | |
group.add(fuselage); | |
const wingGeometry = new THREE.BoxGeometry(12, 0.3, 3); | |
const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x600000 }); | |
const wings = new THREE.Mesh(wingGeometry, wingMaterial); | |
wings.position.z = -0.5; | |
group.add(wings); | |
this.mesh = group; | |
this.mesh.position.copy(this.position); | |
this.mesh.scale.set(1.5, 1.5, 1.5); | |
this.scene.add(this.mesh); | |
this.isLoaded = true; | |
console.log('Fallback ์ ๊ธฐ ๋ชจ๋ธ ์์ฑ ์๋ฃ'); | |
} | |
update(playerPosition, deltaTime) { | |
if (!this.mesh || !this.isLoaded) return; | |
const currentTime = Date.now(); | |
const distanceToPlayer = this.position.distanceTo(playerPosition); | |
if (distanceToPlayer < 5000) { | |
const direction = new THREE.Vector3() | |
.subVectors(playerPosition, this.position) | |
.normalize(); | |
this.velocity = direction.multiplyScalar(this.speed); | |
this.rotation.y = Math.atan2(direction.x, direction.z); | |
if (distanceToPlayer < 2000 && currentTime - this.lastShootTime > 1500) { | |
this.shoot(); | |
} | |
} else { | |
if (this.position.distanceTo(this.targetPosition) < 300) { | |
const angle = Math.random() * Math.PI * 2; | |
this.targetPosition = this.patrolCenter.clone().add( | |
new THREE.Vector3( | |
Math.cos(angle) * this.patrolRadius, | |
(Math.random() - 0.5) * 1000, | |
Math.sin(angle) * this.patrolRadius | |
) | |
); | |
} | |
const direction = new THREE.Vector3() | |
.subVectors(this.targetPosition, this.position) | |
.normalize(); | |
this.velocity = direction.multiplyScalar(this.speed * 0.7); | |
this.rotation.y = Math.atan2(direction.x, direction.z); | |
} | |
this.position.add(this.velocity.clone().multiplyScalar(deltaTime)); | |
if (this.position.y < GAME_CONSTANTS.MIN_ALTITUDE) { | |
this.position.y = GAME_CONSTANTS.MIN_ALTITUDE; | |
} | |
if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) { | |
this.position.y = GAME_CONSTANTS.MAX_ALTITUDE; | |
} | |
const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2; | |
if (this.position.x > mapLimit) this.position.x = -mapLimit; | |
if (this.position.x < -mapLimit) this.position.x = mapLimit; | |
if (this.position.z > mapLimit) this.position.z = -mapLimit; | |
if (this.position.z < -mapLimit) this.position.z = mapLimit; | |
this.mesh.position.copy(this.position); | |
this.mesh.rotation.x = this.rotation.x; | |
this.mesh.rotation.y = this.rotation.y + 3 * Math.PI / 2; | |
this.mesh.rotation.z = this.rotation.z; | |
this.updateBullets(deltaTime); | |
} | |
shoot() { | |
this.lastShootTime = Date.now(); | |
const bulletGeometry = new THREE.SphereGeometry(0.15); | |
const bulletMaterial = new THREE.MeshBasicMaterial({ | |
color: 0xff0000, | |
emissive: 0xff0000, | |
emissiveIntensity: 0.5 | |
}); | |
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); | |
const muzzleOffset = new THREE.Vector3(0, 0, 6); | |
muzzleOffset.applyEuler(this.rotation); | |
bullet.position.copy(this.position).add(muzzleOffset); | |
const direction = new THREE.Vector3(0, 0, 1); | |
direction.applyEuler(this.rotation); | |
bullet.velocity = direction.multiplyScalar(800); | |
this.scene.add(bullet); | |
this.bullets.push(bullet); | |
} | |
updateBullets(deltaTime) { | |
for (let i = this.bullets.length - 1; i >= 0; i--) { | |
const bullet = this.bullets[i]; | |
bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime)); | |
if (bullet.position.distanceTo(this.position) > 5000 || | |
bullet.position.y < 0) { | |
this.scene.remove(bullet); | |
this.bullets.splice(i, 1); | |
} | |
} | |
} | |
takeDamage(damage) { | |
this.health -= damage; | |
return this.health <= 0; | |
} | |
destroy() { | |
if (this.mesh) { | |
this.scene.remove(this.mesh); | |
this.bullets.forEach(bullet => this.scene.remove(bullet)); | |
this.bullets = []; | |
this.isLoaded = false; | |
} | |
} | |
} | |
// ๋ฉ์ธ ๊ฒ์ ํด๋์ค | |
class Game { | |
constructor() { | |
this.scene = new THREE.Scene(); | |
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000); | |
this.renderer = new THREE.WebGLRenderer({ antialias: true }); | |
this.renderer.setSize(window.innerWidth, window.innerHeight); | |
this.renderer.shadowMap.enabled = true; | |
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
this.renderer.setClearColor(0x87CEEB); | |
this.renderer.setPixelRatio(window.devicePixelRatio); | |
document.getElementById('gameContainer').appendChild(this.renderer.domElement); | |
this.loader = new GLTFLoader(); | |
this.fighter = new Fighter(); | |
this.enemies = []; | |
this.isLoaded = false; | |
this.isBGMReady = false; | |
this.isGameOver = false; | |
this.gameTime = GAME_CONSTANTS.MISSION_DURATION; | |
this.score = 0; | |
this.lastTime = performance.now(); | |
this.gameTimer = null; | |
this.animationFrameId = null; | |
this.bgm = null; | |
this.bgmPlaying = false; | |
this.keys = { w: false, a: false, s: false, d: false, f: false }; | |
this.isStarted = false; | |
this.setupScene(); | |
this.setupEventListeners(); | |
this.preloadGame(); | |
} | |
async preloadGame() { | |
try { | |
console.log('๊ฒ์ ๋ฆฌ์์ค ์ฌ์ ๋ก๋ฉ ์ค...'); | |
await this.fighter.initialize(this.scene, this.loader); | |
if (!this.fighter.isLoaded) { | |
throw new Error('์ ํฌ๊ธฐ ๋ก๋ฉ ์คํจ'); | |
} | |
await this.preloadEnemies(); | |
this.isLoaded = true; | |
console.log('๊ฒ์ ๋ฆฌ์์ค ๋ก๋ฉ ์๋ฃ'); | |
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); | |
await enemy.initialize(this.loader); | |
this.enemies.push(enemy); | |
} | |
} | |
setupScene() { | |
this.scene.background = new THREE.Color(0x87CEEB); | |
this.scene.fog = new THREE.Fog(0x87CEEB, 1000, 30000); | |
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
this.scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); | |
directionalLight.position.set(8000, 10000, 8000); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.mapSize.width = 2048; | |
directionalLight.shadow.mapSize.height = 2048; | |
directionalLight.shadow.camera.near = 0.5; | |
directionalLight.shadow.camera.far = 20000; | |
directionalLight.shadow.camera.left = -10000; | |
directionalLight.shadow.camera.right = 10000; | |
directionalLight.shadow.camera.top = 10000; | |
directionalLight.shadow.camera.bottom = -10000; | |
this.scene.add(directionalLight); | |
const groundGeometry = new THREE.PlaneGeometry(GAME_CONSTANTS.MAP_SIZE, GAME_CONSTANTS.MAP_SIZE); | |
const groundMaterial = new THREE.MeshLambertMaterial({ | |
color: 0x8FBC8F, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
this.scene.add(ground); | |
this.addClouds(); | |
} | |
addClouds() { | |
const cloudGeometry = new THREE.SphereGeometry(100, 8, 6); | |
const cloudMaterial = new THREE.MeshLambertMaterial({ | |
color: 0xffffff, | |
transparent: true, | |
opacity: 0.5 | |
}); | |
for (let i = 0; i < 100; i++) { | |
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial); | |
cloud.position.set( | |
(Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE, | |
Math.random() * 4000 + 1000, | |
(Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE | |
); | |
cloud.scale.set( | |
Math.random() * 3 + 1, | |
Math.random() * 2 + 0.5, | |
Math.random() * 3 + 1 | |
); | |
this.scene.add(cloud); | |
} | |
} | |
setupEventListeners() { | |
document.addEventListener('keydown', (event) => { | |
if (this.isGameOver || !gameStarted) return; | |
switch(event.code) { | |
case 'KeyW': this.keys.w = true; break; | |
case 'KeyA': this.keys.a = true; break; | |
case 'KeyS': this.keys.s = true; break; | |
case 'KeyD': this.keys.d = true; break; | |
case 'KeyF': this.keys.f = true; break; | |
} | |
}); | |
document.addEventListener('keyup', (event) => { | |
if (this.isGameOver || !gameStarted) 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; | |
} | |
}); | |
document.addEventListener('mousemove', (event) => { | |
if (!document.pointerLockElement || this.isGameOver || !gameStarted) return; | |
const deltaX = event.movementX || 0; | |
const deltaY = event.movementY || 0; | |
this.fighter.updateMouseInput(deltaX, deltaY); | |
}); | |
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(); | |
console.log('๊ฒ์ ์์!'); | |
} | |
startBGM() { | |
if (this.bgmPlaying || !this.bgm) return; | |
console.log('BGM ์ฌ์ ์๋...'); | |
const playPromise = this.bgm.play(); | |
if (playPromise !== undefined) { | |
playPromise.then(() => { | |
this.bgmPlaying = true; | |
console.log('BGM ์ฌ์ ์์ ์ฑ๊ณต!'); | |
}).catch(error => { | |
console.log('์๋ ์ฌ์์ด ์ฐจ๋จ๋จ:', error); | |
console.log('ํด๋ฆญ ํ ์ฌ์ ์๋ ๋๊ธฐ ์ค...'); | |
const tryPlayOnInteraction = () => { | |
if (!this.bgmPlaying && this.bgm) { | |
console.log('์ฌ์ฉ์ ์ํธ์์ฉ์ผ๋ก BGM ์ฌ์ ์๋...'); | |
this.bgm.play().then(() => { | |
this.bgmPlaying = true; | |
console.log('BGM ์ฌ์ ์์ ์ฑ๊ณต (ํด๋ฆญ ํ)!'); | |
document.removeEventListener('click', tryPlayOnInteraction); | |
document.removeEventListener('keydown', tryPlayOnInteraction); | |
}).catch(e => console.log('BGM ์ฌ์ ์คํจ:', e)); | |
} | |
}; | |
document.addEventListener('click', tryPlayOnInteraction); | |
document.addEventListener('keydown', tryPlayOnInteraction); | |
}); | |
} | |
} | |
startGameTimer() { | |
this.gameTimer = setInterval(() => { | |
if (!this.isGameOver) { | |
this.gameTime--; | |
if (this.gameTime <= 0) { | |
this.endGame(true); | |
} | |
} | |
}, 1000); | |
} | |
updateUI() { | |
if (this.fighter.isLoaded) { | |
const speedKnots = Math.round(this.fighter.speed * 1.94384); | |
const altitudeFeet = Math.round(this.fighter.altitude * 3.28084); | |
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'); | |
if (scoreElement) scoreElement.textContent = `Score: ${this.score}`; | |
if (timeElement) timeElement.textContent = `Time: ${this.gameTime}s`; | |
if (healthElement) healthElement.style.width = `${this.fighter.health}%`; | |
if (ammoElement) ammoElement.textContent = `AMMO: ${this.fighter.ammo}`; | |
if (gameStatsElement) { | |
gameStatsElement.innerHTML = ` | |
<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.rotation.y * (180 / Math.PI)) + 360) % 360); | |
// ์ ํ์จ ๊ณ์ฐ (๋/์ด) | |
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'); | |
// 2000m ์ด๋ด๋ฉด ๋ฝ์จ | |
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`; | |
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()); | |
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 W 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; } | |
} | |
`; | |
document.head.appendChild(style); | |
} | |
} | |
// Over-G 3์ด ์ด์์ผ ๋ ์์ผ ํ๋ฆผ ํจ๊ณผ | |
if (this.fighter.overG && this.fighter.overGTimer > 3.0) { | |
let blurEffect = document.getElementById('overGBlurEffect'); | |
if (!blurEffect) { | |
blurEffect = document.createElement('div'); | |
blurEffect.id = 'overGBlurEffect'; | |
document.body.appendChild(blurEffect); | |
} | |
// Over-G ์ง์ ์๊ฐ์ ๋ฐ๋ผ ํ๋ฆผ ์ ๋ ์ฆ๊ฐ | |
const blurIntensity = Math.min((this.fighter.overGTimer - 3.0) * 0.3, 0.8); | |
blurEffect.style.cssText = ` | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: radial-gradient(ellipse at center, | |
transparent 20%, | |
rgba(0, 0, 0, ${blurIntensity}) 50%, | |
rgba(0, 0, 0, ${blurIntensity + 0.2}) 100%); | |
pointer-events: none; | |
z-index: 1400; | |
`; | |
} else { | |
// Over-G ์ํ๊ฐ ์๋๋ฉด ํจ๊ณผ ์ ๊ฑฐ | |
const blurEffect = document.getElementById('overGBlurEffect'); | |
if (blurEffect) { | |
blurEffect.remove(); | |
} | |
} | |
} | |
updateRadar() { | |
const radar = document.getElementById('radar'); | |
if (!radar) return; | |
const oldDots = radar.getElementsByClassName('enemy-dot'); | |
while (oldDots[0]) { | |
oldDots[0].remove(); | |
} | |
const radarCenter = { x: 100, y: 100 }; | |
const radarRange = 10000; | |
this.enemies.forEach(enemy => { | |
if (!enemy.mesh || !enemy.isLoaded) return; | |
const distance = this.fighter.position.distanceTo(enemy.position); | |
if (distance <= radarRange) { | |
const relativePos = enemy.position.clone().sub(this.fighter.position); | |
const angle = Math.atan2(relativePos.x, relativePos.z); | |
const relativeDistance = distance / radarRange; | |
const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance); | |
const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance); | |
const dot = document.createElement('div'); | |
dot.className = 'enemy-dot'; | |
dot.style.left = `${dotX}px`; | |
dot.style.top = `${dotY}px`; | |
radar.appendChild(dot); | |
} | |
}); | |
} | |
checkCollisions() { | |
for (let i = this.fighter.bullets.length - 1; i >= 0; i--) { | |
const bullet = this.fighter.bullets[i]; | |
for (let j = this.enemies.length - 1; j >= 0; j--) { | |
const enemy = this.enemies[j]; | |
if (!enemy.mesh || !enemy.isLoaded) continue; | |
const distance = bullet.position.distanceTo(enemy.position); | |
if (distance < 25) { | |
this.scene.remove(bullet); | |
this.fighter.bullets.splice(i, 1); | |
if (enemy.takeDamage(30)) { | |
enemy.destroy(); | |
this.enemies.splice(j, 1); | |
this.score += 100; | |
} | |
break; | |
} | |
} | |
} | |
this.enemies.forEach(enemy => { | |
enemy.bullets.forEach((bullet, index) => { | |
const distance = bullet.position.distanceTo(this.fighter.position); | |
if (distance < 30) { | |
this.scene.remove(bullet); | |
enemy.bullets.splice(index, 1); | |
if (this.fighter.takeDamage(20)) { | |
this.endGame(false); | |
} | |
} | |
}); | |
}); | |
} | |
animate() { | |
if (this.isGameOver) return; | |
this.animationFrameId = requestAnimationFrame(() => this.animate()); | |
const currentTime = performance.now(); | |
const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.1); | |
this.lastTime = currentTime; | |
if (this.isLoaded && this.fighter.isLoaded) { | |
// Fํค ์ํ๋ฅผ Fighter์ ์ ๋ฌ | |
this.fighter.escapeKeyPressed = this.keys.f; | |
this.fighter.updateControls(this.keys, deltaTime); | |
this.fighter.updatePhysics(deltaTime); | |
this.fighter.updateBullets(this.scene, deltaTime); | |
if (this.isStarted) { | |
this.enemies.forEach(enemy => { | |
enemy.update(this.fighter.position, deltaTime); | |
}); | |
this.checkCollisions(); | |
if (this.fighter.health <= 0 && this.fighter.position.y <= 0) { | |
this.endGame(false, "GROUND COLLISION"); | |
return; | |
} | |
this.updateUI(); | |
this.updateRadar(); | |
if (this.enemies.length === 0) { | |
this.endGame(true); | |
} | |
} | |
const targetCameraPos = this.fighter.getCameraPosition(); | |
const targetCameraTarget = this.fighter.getCameraTarget(); | |
this.camera.position.lerp(targetCameraPos, this.fighter.cameraLag); | |
this.camera.lookAt(targetCameraTarget); | |
} else { | |
if (this.fighter.isLoaded) { | |
const initialCameraPos = this.fighter.getCameraPosition(); | |
const initialTarget = this.fighter.getCameraTarget(); | |
this.camera.position.copy(initialCameraPos); | |
this.camera.lookAt(initialTarget); | |
} | |
} | |
this.renderer.render(this.scene, this.camera); | |
} | |
endGame(victory = false, 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'); | |
existingWarnings.forEach(w => w.remove()); | |
const blurEffect = document.getElementById('overGBlurEffect'); | |
if (blurEffect) { | |
blurEffect.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(); | |
} | |
else if (window.gameInstance.fighter.isLoaded) { | |
window.gameInstance.fighter.shoot(window.gameInstance.scene); | |
} | |
} | |
}, true); | |
window.addEventListener('gameReady', () => { | |
gameCanStart = true; | |
}); | |
document.addEventListener('DOMContentLoaded', () => { | |
console.log('์ ํฌ๊ธฐ ์๋ฎฌ๋ ์ดํฐ ์ด๊ธฐํ ์ค...'); | |
window.gameInstance = new Game(); | |
}); |