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