cutechicken's picture
Update game.js
a23a84b verified
raw
history blame
88.1 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: 25, // ๋ฐœ๋‹น 25 ๋ฐ๋ฏธ์ง€
MAX_HEALTH: 1000 // ์ฒด๋ ฅ 1000
};
// ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค
class Fighter {
constructor() {
this.mesh = null;
this.isLoaded = false;
// ๋ฌผ๋ฆฌ ์†์„ฑ
this.position = new THREE.Vector3(0, 2000, 0);
this.velocity = new THREE.Vector3(0, 0, 350); // ์ดˆ๊ธฐ ์†๋„ 350kt
this.acceleration = new THREE.Vector3(0, 0, 0);
this.rotation = new THREE.Euler(0, 0, 0);
// ๋น„ํ–‰ ์ œ์–ด
this.throttle = 0.6; // ์ดˆ๊ธฐ ์Šค๋กœํ‹€ 60%
this.speed = 350; // ์ดˆ๊ธฐ ์†๋„ 350kt
this.altitude = 2000;
this.gForce = 1.0;
this.health = 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๊ฐœ)
// ์Šคํ†จ ํƒˆ์ถœ์„ ์œ„ํ•œ 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 // ์—”์ง„ ์†Œ๋ฆฌ ์ถ”๊ฐ€
};
this.initializeWarningAudios();
}
initializeWarningAudios() {
try {
this.warningAudios.altitude = new Audio('sounds/altitude.ogg');
this.warningAudios.altitude.volume = 0.75;
this.warningAudios.pullup = new Audio('sounds/pullup.ogg');
this.warningAudios.pullup.volume = 0.9;
this.warningAudios.overg = new Audio('sounds/overg.ogg');
this.warningAudios.overg.volume = 0.75;
this.warningAudios.stall = new Audio('sounds/alert.ogg');
this.warningAudios.stall.volume = 0.75;
// ์—”์ง„ ์†Œ๋ฆฌ ์„ค์ •
this.warningAudios.normal = new Audio('sounds/normal.ogg');
this.warningAudios.normal.volume = 0.5;
this.warningAudios.normal.loop = true; // ์—”์ง„ ์†Œ๋ฆฌ๋Š” ๊ณ„์† ๋ฐ˜๋ณต
// ๊ฒฝ๊ณ ์Œ์—๋งŒ ended ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€ (์—”์ง„ ์†Œ๋ฆฌ ์ œ์™ธ)
Object.keys(this.warningAudios).forEach(key => {
if (key !== 'normal' && this.warningAudios[key]) {
this.warningAudios[key].addEventListener('ended', () => {
this.updateWarningAudios();
});
}
});
} catch (e) {
console.log('Warning audio initialization failed:', e);
}
}
startEngineSound() {
// ์—”์ง„ ์†Œ๋ฆฌ ์‹œ์ž‘
if (this.warningAudios.normal) {
this.warningAudios.normal.play().catch(e => {
console.log('Engine sound failed to start:', e);
});
}
}
updateWarningAudios() {
let currentWarning = null;
if (this.altitude < 250) {
currentWarning = 'pullup';
}
else if (this.altitude < 500) {
currentWarning = 'altitude';
}
else if (this.overG) {
currentWarning = 'overg';
}
else if (this.stallWarning) {
currentWarning = 'stall';
}
// ๊ฒฝ๊ณ ์Œ๋งŒ ๊ด€๋ฆฌ (์—”์ง„ ์†Œ๋ฆฌ๋Š” ์ œ์™ธ)
Object.keys(this.warningAudios).forEach(key => {
if (key !== 'normal' && key !== currentWarning && this.warningAudios[key] && !this.warningAudios[key].paused) {
this.warningAudios[key].pause();
this.warningAudios[key].currentTime = 0;
}
});
if (currentWarning && this.warningAudios[currentWarning]) {
if (this.warningAudios[currentWarning].paused) {
this.warningAudios[currentWarning].play().catch(e => {});
}
}
}
stopAllWarningAudios() {
// ๋ชจ๋“  ์˜ค๋””์˜ค ์ •์ง€ (์—”์ง„ ์†Œ๋ฆฌ ํฌํ•จ)
Object.values(this.warningAudios).forEach(audio => {
if (audio && !audio.paused) {
audio.pause();
audio.currentTime = 0;
}
});
}
async initialize(scene, loader) {
try {
const result = await loader.loadAsync('models/f-15.glb');
this.mesh = result.scene;
this.mesh.position.copy(this.position);
this.mesh.scale.set(2, 2, 2);
this.mesh.rotation.y = Math.PI / 4;
this.mesh.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(this.mesh);
this.isLoaded = true;
console.log('F-15 ์ „ํˆฌ๊ธฐ ๋กœ๋”ฉ ์™„๋ฃŒ');
} catch (error) {
console.error('F-15 ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ:', error);
this.createFallbackModel(scene);
}
}
createFallbackModel(scene) {
const group = new THREE.Group();
const fuselageGeometry = new THREE.CylinderGeometry(0.8, 1.5, 12, 8);
const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x606060 });
const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial);
fuselage.rotation.x = Math.PI / 2;
group.add(fuselage);
const wingGeometry = new THREE.BoxGeometry(16, 0.3, 4);
const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 });
const wings = new THREE.Mesh(wingGeometry, wingMaterial);
wings.position.z = -1;
group.add(wings);
const tailGeometry = new THREE.BoxGeometry(0.3, 4, 3);
const tailMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 });
const tail = new THREE.Mesh(tailGeometry, tailMaterial);
tail.position.z = -5;
tail.position.y = 1.5;
group.add(tail);
const horizontalTailGeometry = new THREE.BoxGeometry(6, 0.2, 2);
const horizontalTail = new THREE.Mesh(horizontalTailGeometry, tailMaterial);
horizontalTail.position.z = -5;
horizontalTail.position.y = 0.5;
group.add(horizontalTail);
this.mesh = group;
this.mesh.position.copy(this.position);
this.mesh.scale.set(2, 2, 2);
scene.add(this.mesh);
this.isLoaded = true;
console.log('Fallback ์ „ํˆฌ๊ธฐ ๋ชจ๋ธ ์ƒ์„ฑ ์™„๋ฃŒ');
}
updateMouseInput(deltaX, deltaY) {
// 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;
// ํ”ผ์น˜๋Š” ์กฐ์ž‘ ๋ถˆ๊ฐ€
// ๋กค๋„ ์ œํ•œ์ ์œผ๋กœ๋งŒ ํ—ˆ์šฉ
const yawRate = deltaX * sensitivity * 0.3;
this.targetRoll = -yawRate * 5; // ๋งค์šฐ ์ œํ•œ๋œ ๋กค
return; // ์ถ”๊ฐ€ ์ฒ˜๋ฆฌ ์ค‘๋‹จ
}
const sensitivity = GAME_CONSTANTS.MOUSE_SENSITIVITY * 1.0;
// ๋งˆ์šฐ์Šค Y์ถ•: ํ”ผ์น˜(๊ธฐ์ˆ˜ ์ƒํ•˜)
this.targetPitch -= deltaY * sensitivity;
// ๋งˆ์šฐ์Šค X์ถ•: ์š”(Yaw) - ๋ชธ์ฒด๋ฅผ ์ขŒ์šฐ๋กœ ํšŒ์ „
this.targetYaw += deltaX * sensitivity * 0.8; // ์š” ํšŒ์ „ ๊ฐ๋„ ์กฐ์ •
// ์š” ํšŒ์ „์— ๋”ฐ๋ฅธ ์ž๋™ ๋กค (๋ฑ…ํฌ ํ„ด)
// ์ขŒ์šฐ๋กœ ํšŒ์ „ํ•  ๋•Œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋‚ ๊ฐœ๊ฐ€ ๊ธฐ์šธ์–ด์ง
const yawRate = deltaX * sensitivity * 0.8;
this.targetRoll = -yawRate * 15; // ์š” ํšŒ์ „๋Ÿ‰์— ๋น„๋ก€ํ•˜์—ฌ ๋กค ๋ฐœ์ƒ
// ๊ฐ๋„ ์ œํ•œ
const maxPitchAngle = Math.PI / 3; // 60๋„
const maxRollAngle = Math.PI * 0.5; // 90๋„๋กœ ์ œํ•œ (์ž๋™ ๋กค)
this.targetPitch = Math.max(-maxPitchAngle, Math.min(maxPitchAngle, this.targetPitch));
// ์ž๋™ ๋กค์€ ์ œํ•œ๋œ ๋ฒ”์œ„ ๋‚ด์—์„œ๋งŒ ์ž‘๋™
if (Math.abs(this.targetRoll) < maxRollAngle) {
// ๋กค ๊ฐ๋„ ์ œํ•œ ์ ์šฉ
this.targetRoll = Math.max(-maxRollAngle, Math.min(maxRollAngle, this.targetRoll));
}
}
updateControls(keys, deltaTime) {
// W/S: ์Šค๋กœํ‹€๋งŒ ์ œ์–ด (๊ฐ€์†/๊ฐ์†)
if (keys.w) {
this.throttle = Math.min(1.0, this.throttle + deltaTime * 0.5); // ์ฒœ์ฒœํžˆ ๊ฐ€์†
}
if (keys.s) {
this.throttle = Math.max(0.1, this.throttle - deltaTime * 0.5); // ์ฒœ์ฒœํžˆ ๊ฐ์†
}
// A/D: ๋ณด์กฐ ์š” ์ œ์–ด (๋Ÿฌ๋”) - ๋ฐ˜์‘์„ฑ ๊ฐœ์„ 
if (keys.a) {
this.targetYaw -= deltaTime * 1.2; // 0.4์—์„œ 1.2๋กœ ์ฆ๊ฐ€ (3๋ฐฐ)
}
if (keys.d) {
this.targetYaw += deltaTime * 1.2; // 0.4์—์„œ 1.2๋กœ ์ฆ๊ฐ€ (3๋ฐฐ)
}
}
updatePhysics(deltaTime) {
if (!this.mesh) return;
// ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „ ๋ณด๊ฐ„ - Yaw ํšŒ์ „ ์†๋„ ๊ฐœ์„ 
const rotationSpeed = deltaTime * 2.0;
const yawRotationSpeed = deltaTime * 3.0; // Yaw๋Š” ๋” ๋น ๋ฅด๊ฒŒ ๋ฐ˜์‘ํ•˜๋„๋ก ๋ณ„๋„ ์„ค์ •
this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetPitch, rotationSpeed);
this.rotation.y = THREE.MathUtils.lerp(this.rotation.y, this.targetYaw, yawRotationSpeed); // ๊ฐœ์„ ๋œ ์†๋„
// ๋กค ์ž๋™ ๋ณต๊ท€ ์‹œ์Šคํ…œ
if (Math.abs(this.targetYaw - this.rotation.y) < 0.05) {
// ์š” ํšŒ์ „์ด ๊ฑฐ์˜ ์—†์„ ๋•Œ ๋กค์„ 0์œผ๋กœ ๋ณต๊ท€
this.targetRoll *= 0.95;
}
this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRoll, rotationSpeed * 1.5); // ๋กค์€ ๋” ๋น ๋ฅด๊ฒŒ ๋ฐ˜์‘
// ์›Œ์ฌ๋” ์Šคํƒ€์ผ: ์š” ํšŒ์ „์ด ์ฃผ๋„์ , ๋กค์€ ๋ณด์กฐ์ 
// ๋กค์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ์š” ํšŒ์ „์€ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ์ตœ์†Œํ™”
let bankTurnRate = 0;
if (Math.abs(this.rotation.z) > 0.3) { // ๋กค์ด ์ถฉ๋ถ„ํžˆ ํด ๋•Œ๋งŒ
const bankAngle = this.rotation.z;
bankTurnRate = Math.sin(bankAngle) * deltaTime * 0.1; // ๋งค์šฐ ์ž‘์€ ์„ ํšŒ์œจ
this.targetYaw += bankTurnRate;
}
// ํ˜„์‹ค์ ์ธ ์†๋„ ๊ณ„์‚ฐ
const minSpeed = 0; // ์ตœ์†Œ ์†๋„ 0kt
const maxSpeed = 600; // ์ตœ๋Œ€ ์†๋„ 600kt
let targetSpeed = minSpeed + (maxSpeed - minSpeed) * this.throttle;
// ํ”ผ์น˜ ๊ฐ๋„์— ๋”ฐ๋ฅธ ์†๋„ ๋ณ€ํ™”
const pitchAngle = this.rotation.x;
const pitchDegrees = Math.abs(pitchAngle) * (180 / Math.PI);
// ๊ธฐ์ˆ˜๊ฐ€ ์œ„๋ฅผ ํ–ฅํ•˜๊ณ  ์žˆ์„ ๊ฒฝ์šฐ ๋น ๋ฅธ ์†๋„ ๊ฐ์†Œ
if (pitchAngle < -0.1 && !this.stallWarning) { // ์Šคํ†จ์ด ์•„๋‹ ๋•Œ๋งŒ ์ƒ์Šน์œผ๋กœ ์ธํ•œ ๊ฐ์†
const climbFactor = Math.abs(pitchAngle) / (Math.PI / 2); // 90๋„ ๊ธฐ์ค€
if (pitchDegrees > 30) { // 30๋„ ์ด์ƒ์ผ ๋•Œ ๊ธ‰๊ฒฉํ•œ ๊ฐ์†
targetSpeed *= Math.max(0, 1 - climbFactor * 1.5); // ์ตœ๋Œ€ 150% ๊ฐ์† (0kt๊นŒ์ง€)
} else {
targetSpeed *= (1 - climbFactor * 0.3); // ์ •์ƒ์ ์ธ ๊ฐ์†
}
} else if (pitchAngle > 0.1) { // ๊ธฐ์ˆ˜๊ฐ€ ์•„๋ž˜๋กœ (ํ•˜๊ฐ•) - ์Šคํ†จ ์ƒํƒœ์—์„œ๋„ ์ ์šฉ
const diveFactor = pitchAngle / (Math.PI / 3);
targetSpeed *= (1 + diveFactor * 0.4); // ํ•˜๊ฐ• ์‹œ ๊ฐ€์† ์ฆ๊ฐ€ (0.2 -> 0.4)
}
// G-Force ๊ณ„์‚ฐ ๊ฐœ์„ 
const turnRate = Math.abs(bankTurnRate) * 100;
const pitchRate = Math.abs(this.rotation.x - this.targetPitch) * 10;
// ๊ณ ๋„์— ๋”ฐ๋ฅธ G-Force ์ฆ๊ฐ€ ๋ฐฐ์œจ ๊ณ„์‚ฐ
const altitudeInKm = this.position.y / 1000; // ๋ฏธํ„ฐ๋ฅผ ํ‚ฌ๋กœ๋ฏธํ„ฐ๋กœ ๋ณ€ํ™˜
const altitudeMultiplier = 1 + (altitudeInKm * 0.2); // 1km๋‹น 20% ์ฆ๊ฐ€
// ์Šค๋กœํ‹€์— ๋”ฐ๋ฅธ G-Force ์ฆ๊ฐ€ ๋ฐฐ์œจ ๊ณ„์‚ฐ
// 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 && this.escapeKeyPressed && pitchAngle > 0.2 && speedKnots > GAME_CONSTANTS.STALL_SPEED + 50) {
// Fํ‚ค๋ฅผ ๋ˆ„๋ฅด๊ณ  ์žˆ๊ณ  ์กฐ๊ฑด์ด ๋งž์œผ๋ฉด ์ง„ํ–‰๋„ ์ฆ๊ฐ€
this.stallEscapeProgress += deltaTime;
// 2์ดˆ ์ด์ƒ Fํ‚ค๋ฅผ ๋ˆ„๋ฅด๊ณ  ์žˆ์œผ๋ฉด ์Šคํ†จ ํƒˆ์ถœ
if (this.stallEscapeProgress >= 2.0) {
this.stallWarning = false;
this.stallEscapeProgress = 0;
}
} else if (this.stallWarning && !this.escapeKeyPressed) {
// Fํ‚ค๋ฅผ ๋†“์œผ๋ฉด ์ง„ํ–‰๋„ ๊ฐ์†Œ
this.stallEscapeProgress = Math.max(0, this.stallEscapeProgress - deltaTime * 2);
}
// ์†๋„ ๋ณ€ํ™” ์ ์šฉ
if (this.stallWarning) {
// ์Šคํ†จ ์ƒํƒœ์—์„œ์˜ ์†๋„ ๋ณ€ํ™”
if (pitchAngle > 0.1) { // ๊ธฐ์ˆ˜๊ฐ€ ์•„๋ž˜๋ฅผ ํ–ฅํ•  ๋•Œ
// ๋‹ค์ด๋น™์œผ๋กœ ์ธํ•œ ์†๋„ ์ฆ๊ฐ€
const diveSpeedGain = Math.min(pitchAngle * 300, 200); // ์ตœ๋Œ€ 200m/s ์ฆ๊ฐ€
this.speed = Math.min(maxSpeed, this.speed + diveSpeedGain * deltaTime);
} else {
// ๊ธฐ์ˆ˜๊ฐ€ ์œ„๋ฅผ ํ–ฅํ•˜๊ฑฐ๋‚˜ ์ˆ˜ํ‰์ผ ๋•Œ๋Š” ์†๋„ ๊ฐ์†Œ
this.speed = Math.max(0, this.speed - deltaTime * 100);
}
} else {
// ์ •์ƒ ๋น„ํ–‰ ์‹œ ์†๋„ ๋ณ€ํ™”
this.speed = THREE.MathUtils.lerp(this.speed, targetSpeed, deltaTime * 0.5);
}
// ์Šคํ†จ ์ƒํƒœ์—์„œ์˜ ๋ฌผ๋ฆฌ ํšจ๊ณผ
if (this.stallWarning) {
// ๋ฐ”๋‹ฅ์œผ๋กœ ์ถ”๋ฝํ•˜๋ฉฐ ๊ฐ€์†๋„๊ฐ€ ๋น ๋ฅด๊ฒŒ ๋ถ™์Œ
this.targetPitch = Math.min(Math.PI / 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);
noseDirection.applyEuler(this.rotation);
if (!this.stallWarning) {
// ์ •์ƒ ๋น„ํ–‰ ์‹œ
this.velocity = noseDirection.multiplyScalar(this.speed);
} else {
// ์Šคํ†จ ์‹œ์—๋Š” ์ค‘๋ ฅ์ด ์ฃผ๋„์ ์ด์ง€๋งŒ ๋‹ค์ด๋น™ ์†๋„๋„ ๋ฐ˜์˜
this.velocity.x = noseDirection.x * this.speed * 0.5; // ์ „๋ฐฉ ์†๋„ ์ฆ๊ฐ€
this.velocity.z = noseDirection.z * this.speed * 0.5;
// y ์†๋„๋Š” ์œ„์—์„œ ์ค‘๋ ฅ์œผ๋กœ ์ฒ˜๋ฆฌ๋จ
}
// ์ •์ƒ ๋น„ํ–‰ ์‹œ ์ค‘๋ ฅ ํšจ๊ณผ
if (!this.stallWarning) {
const gravityEffect = GAME_CONSTANTS.GRAVITY * deltaTime * 0.15;
this.velocity.y -= gravityEffect;
// ์–‘๋ ฅ ํšจ๊ณผ (์†๋„์— ๋น„๋ก€)
const liftFactor = (this.speed / maxSpeed) * 0.8;
const lift = gravityEffect * liftFactor;
this.velocity.y += lift;
}
// ์œ„์น˜ ์—…๋ฐ์ดํŠธ
this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
// ์ง€๋ฉด ์ถฉ๋Œ
if (this.position.y <= GAME_CONSTANTS.MIN_ALTITUDE) {
this.position.y = GAME_CONSTANTS.MIN_ALTITUDE;
this.health = 0;
return;
}
// ์ตœ๋Œ€ ๊ณ ๋„ ์ œํ•œ
if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) {
this.position.y = GAME_CONSTANTS.MAX_ALTITUDE;
this.altitudeWarning = true;
if (this.velocity.y > 0) this.velocity.y = 0;
} else {
this.altitudeWarning = false;
}
// ๋งต ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ
const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
if (this.position.x > mapLimit) this.position.x = -mapLimit;
if (this.position.x < -mapLimit) this.position.x = mapLimit;
if (this.position.z > mapLimit) this.position.z = -mapLimit;
if (this.position.z < -mapLimit) this.position.z = mapLimit;
// ๋ฉ”์‹œ ์œ„์น˜ ๋ฐ ํšŒ์ „ ์—…๋ฐ์ดํŠธ
this.mesh.position.copy(this.position);
this.mesh.rotation.x = this.rotation.x;
this.mesh.rotation.y = this.rotation.y + 3 * Math.PI / 2;
this.mesh.rotation.z = this.rotation.z;
// ๊ฒฝ๊ณ  ๊นœ๋นก์ž„ ํƒ€์ด๋จธ
this.warningBlinkTimer += deltaTime;
if (this.warningBlinkTimer >= 1.0) {
this.warningBlinkTimer = 0;
this.warningBlinkState = !this.warningBlinkState;
}
// ๊ณ ๋„ ๊ณ„์‚ฐ
this.altitude = this.position.y;
// ๊ฒฝ๊ณ ์Œ ์—…๋ฐ์ดํŠธ (์—”์ง„ ์†Œ๋ฆฌ๋Š” ๊ณ„์† ์œ ์ง€)
this.updateWarningAudios();
// ์—”์ง„ ์†Œ๋ฆฌ ๋ณผ๋ฅจ์„ ์Šค๋กœํ‹€์— ์—ฐ๋™
if (this.warningAudios.normal && !this.warningAudios.normal.paused) {
this.warningAudios.normal.volume = 0.3 + this.throttle * 0.4; // 0.3~0.7
}
}
shoot(scene) {
// ํƒ„์•ฝ์ด ์—†์œผ๋ฉด ๋ฐœ์‚ฌํ•˜์ง€ ์•Š์Œ
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,
emissive: 0xffff00,
emissiveIntensity: 1.0
});
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
// ๊ธฐ์ˆ˜ ๋์—์„œ ๋ฐœ์‚ฌ (๋” ์•ž์ชฝ์—์„œ)
const muzzleOffset = new THREE.Vector3(0, 0, 10);
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();
const bulletSpeed = 1500; // 1000์—์„œ 1500์œผ๋กœ ์ฆ๊ฐ€ (50% ๋น ๋ฅด๊ฒŒ)
const direction = new THREE.Vector3(0, 0, 1);
direction.applyEuler(this.rotation);
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);
}
}
updateBullets(scene, deltaTime) {
for (let i = this.bullets.length - 1; i >= 0; i--) {
const bullet = this.bullets[i];
bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
// ํƒ„ํ™˜๋„ ๊ฐ™์€ ๋ฐฉํ–ฅ์„ ์œ ์ง€ํ•˜๋„๋ก ํšŒ์ „ ์—…๋ฐ์ดํŠธ
const direction = bullet.velocity.clone().normalize();
const angle = Math.atan2(direction.x, direction.z);
bullet.rotation.y = angle;
// 6000m ์ด์ƒ ๋‚ ์•„๊ฐ€๋ฉด ์ œ๊ฑฐ
if (bullet.position.distanceTo(bullet.startPosition) > 6000 ||
bullet.position.y < 0 ||
bullet.position.y > GAME_CONSTANTS.MAX_ALTITUDE + 500) {
scene.remove(bullet);
this.bullets.splice(i, 1);
}
}
}
takeDamage(damage) {
this.health -= damage;
return this.health <= 0;
}
getCameraPosition() {
const backward = new THREE.Vector3(0, 0, -1);
const up = new THREE.Vector3(0, 1, 0);
backward.applyEuler(this.rotation);
up.applyEuler(this.rotation);
const cameraPosition = this.position.clone()
.add(backward.multiplyScalar(this.cameraDistance))
.add(up.multiplyScalar(this.cameraHeight));
return cameraPosition;
}
getCameraTarget() {
return this.position.clone();
}
}
// ์  ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค
class EnemyFighter {
constructor(scene, position) {
this.mesh = null;
this.isLoaded = false;
this.scene = scene;
this.position = position.clone();
this.velocity = new THREE.Vector3(0, 0, 120);
this.rotation = new THREE.Euler(0, 0, 0);
this.health = GAME_CONSTANTS.MAX_HEALTH; // ์ฒด๋ ฅ 1000
this.speed = 514; // 1000kt in m/s (1000 * 0.514444)
this.bullets = [];
this.lastShootTime = 0;
// ๊ฐœ์„ ๋œ AI ์ƒํƒœ
this.aiState = 'patrol';
this.targetPosition = this.generateRandomTarget();
this.lastDirectionChange = Date.now();
this.playerFighter = null; // ํ”Œ๋ ˆ์ด์–ด ์ฐธ์กฐ ์ €์žฅ์šฉ
// ๋ถ€๋“œ๋Ÿฌ์šด ์„ ํšŒ๋ฅผ ์œ„ํ•œ ๋ณ€์ˆ˜
this.targetRotation = new THREE.Euler(0, Math.random() * Math.PI * 2, 0);
this.turnRadius = 1500; // ์„ ํšŒ ๋ฐ˜๊ฒฝ
this.isTurning = false;
this.turnDirection = 1; // 1: ์šฐํšŒ์ „, -1: ์ขŒํšŒ์ „
// ๋‹ค๋ฅธ ์ ๊ธฐ๋“ค๊ณผ์˜ ์ถฉ๋Œ ํšŒํ”ผ
this.nearbyEnemies = [];
this.separationRadius = 300; // ์ตœ์†Œ ๊ฑฐ๋ฆฌ 300m
// ์ „ํˆฌ ๊ด€๋ จ ๋ณ€์ˆ˜
this.isEngaged = false; // ์ „ํˆฌ ์ค‘ ์ƒํƒœ
this.burstCount = 0; // ํ˜„์žฌ ์—ฐ๋ฐœ ์ˆ˜
this.lastBurstTime = 0; // ๋งˆ์ง€๋ง‰ ์—ฐ๋ฐœ ์‹œ์ž‘ ์‹œ๊ฐ„
this.predictedTargetPos = new THREE.Vector3(); // ์˜ˆ์ธก ์‚ฌ๊ฒฉ ์œ„์น˜
}
generateRandomTarget() {
// ๋งต ๋ฒ”์œ„ ๋‚ด ๋žœ๋ค ๋ชฉํ‘œ ์ง€์  ์ƒ์„ฑ
const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2 * 0.8;
return new THREE.Vector3(
(Math.random() - 0.5) * 2 * mapLimit,
1000 + Math.random() * 3000, // 1000m ~ 4000m ๊ณ ๋„
(Math.random() - 0.5) * 2 * mapLimit
);
}
async initialize(loader) {
try {
const result = await loader.loadAsync('models/mig-29.glb');
this.mesh = result.scene;
this.mesh.position.copy(this.position);
this.mesh.scale.set(1.5, 1.5, 1.5);
this.mesh.rotation.y = 3 * Math.PI / 2;
this.mesh.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
this.scene.add(this.mesh);
this.isLoaded = true;
console.log('MiG-29 ์ ๊ธฐ ๋กœ๋”ฉ ์™„๋ฃŒ');
} catch (error) {
console.error('MiG-29 ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ:', error);
this.createFallbackModel();
}
}
createFallbackModel() {
const group = new THREE.Group();
const fuselageGeometry = new THREE.CylinderGeometry(0.6, 1.0, 8, 8);
const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x800000 });
const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial);
fuselage.rotation.x = -Math.PI / 2;
group.add(fuselage);
const wingGeometry = new THREE.BoxGeometry(12, 0.3, 3);
const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x600000 });
const wings = new THREE.Mesh(wingGeometry, wingMaterial);
wings.position.z = -0.5;
group.add(wings);
this.mesh = group;
this.mesh.position.copy(this.position);
this.mesh.scale.set(1.5, 1.5, 1.5);
this.scene.add(this.mesh);
this.isLoaded = true;
console.log('Fallback ์ ๊ธฐ ๋ชจ๋ธ ์ƒ์„ฑ ์™„๋ฃŒ');
}
update(playerPosition, deltaTime) {
if (!this.mesh || !this.isLoaded) return;
const currentTime = Date.now();
const distanceToPlayer = this.position.distanceTo(playerPosition);
// ํ”Œ๋ ˆ์ด์–ด ๊ฐ์ง€ ๋ฐ ์ „ํˆฌ ์ƒํƒœ ์ „ํ™˜
if (distanceToPlayer < 3000) {
this.isEngaged = true;
this.aiState = 'combat';
// ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์‹œ์•ผ๊ฐ ๋‚ด์— ์žˆ๋Š”์ง€ ํ™•์ธ
const toPlayer = playerPosition.clone().sub(this.position).normalize();
const forward = new THREE.Vector3(0, 0, 1);
forward.applyEuler(this.rotation);
const angle = forward.dot(toPlayer);
// ์˜ˆ์ธก ์‚ฌ๊ฒฉ์„ ์œ„ํ•œ ํ”Œ๋ ˆ์ด์–ด ์†๋„ ๊ณ„์‚ฐ
if (this.playerFighter) {
const bulletSpeed = 1200; // ํƒ„ํ™˜ ์†๋„
const timeToHit = distanceToPlayer / bulletSpeed;
// ํ”Œ๋ ˆ์ด์–ด์˜ ์˜ˆ์ƒ ์œ„์น˜ ๊ณ„์‚ฐ
this.predictedTargetPos = playerPosition.clone().add(
this.playerFighter.velocity.clone().multiplyScalar(timeToHit)
);
}
// ์‚ฌ๊ฒฉ ์กฐ๊ฑด: ์‹œ์•ผ๊ฐ ๋‚ด์— ์žˆ๊ณ  ๊ฑฐ๋ฆฌ๊ฐ€ 2000m ์ด๋‚ด
if (angle > 0.7 && distanceToPlayer < 2000) { // ์‹œ์•ผ๊ฐ์„ ์ขํ˜€์„œ ์ •ํ™•๋„ ํ–ฅ์ƒ
// ์—ฐ๋ฐœ ์‚ฌ๊ฒฉ ์‹œ์Šคํ…œ
const timeSinceLastBurst = currentTime - this.lastBurstTime;
// 2์ดˆ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ์—ฐ๋ฐœ ์‹œ์ž‘
if (timeSinceLastBurst >= 2000) {
this.burstCount = 0;
this.lastBurstTime = currentTime;
}
// 10๋ฐœ ์—ฐ๋ฐœ (0.1์ดˆ ๊ฐ„๊ฒฉ)
if (this.burstCount < 10 && currentTime - this.lastShootTime >= 100) {
this.shoot();
this.burstCount++;
}
}
} else {
this.isEngaged = false;
this.aiState = 'patrol';
}
// ์ „ํˆฌ ์ค‘์ผ ๋•Œ ํ”Œ๋ ˆ์ด์–ด ์ถ”์ 
if (this.isEngaged && this.playerFighter) {
// ํ”Œ๋ ˆ์ด์–ด์˜ ๊ธฐ๋™ ํŒจํ„ด ๋ชจ๋ฐฉ
const playerVelocity = this.playerFighter.velocity.clone().normalize();
const playerRoll = this.playerFighter.rotation.z;
const playerPitch = this.playerFighter.rotation.x;
// ์˜ˆ์ธก๋œ ์œ„์น˜๋กœ ํ–ฅํ•˜๋„๋ก ์„ค์ •
this.targetPosition = this.predictedTargetPos.clone();
// ํ”Œ๋ ˆ์ด์–ด์™€ ๋น„์Šทํ•œ ๋กค ์ ์šฉ (์•ฝ๊ฐ„์˜ ์ง€์—ฐ์œผ๋กœ)
this.targetRotation.z = playerRoll * 0.7;
// ๊ณ ๋„ ์ฐจ์ด์— ๋”ฐ๋ฅธ ํ”ผ์น˜ ์กฐ์ ˆ (๋” ๊ณต๊ฒฉ์ ์œผ๋กœ)
const altitudeDiff = this.targetPosition.y - this.position.y;
this.targetRotation.x = Math.max(-0.5, Math.min(0.5, altitudeDiff * 0.0003));
} else {
// ์ˆœ์ฐฐ ๋ชจ๋“œ
if (this.position.distanceTo(this.targetPosition) < 500 ||
currentTime - this.lastDirectionChange > 15000) {
this.targetPosition = this.generateRandomTarget();
this.lastDirectionChange = currentTime;
this.isTurning = false;
}
}
// ๋ชฉํ‘œ ๋ฐฉํ–ฅ ๊ณ„์‚ฐ
const toTarget = this.targetPosition.clone().sub(this.position);
const targetAngle = Math.atan2(toTarget.x, toTarget.z);
let angleDiff = targetAngle - this.rotation.y;
// ๊ฐ๋„ ์ฐจ์ด๋ฅผ -PI ~ PI ๋ฒ”์œ„๋กœ ์ •๊ทœํ™”
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
// ์ „ํˆฌ ์ค‘์—๋Š” ๋” ๋น ๋ฅธ ์„ ํšŒ
const turnRateMultiplier = this.isEngaged ? 2.0 : 1.0;
// ํฐ ๊ฐ๋„ ์ฐจ์ด๊ฐ€ ์žˆ์œผ๋ฉด ์›ํ˜• ์„ ํšŒ
if (Math.abs(angleDiff) > Math.PI / 6) {
this.isTurning = true;
this.turnDirection = angleDiff > 0 ? 1 : -1;
}
// ์„ ํšŒ ์ค‘์ผ ๋•Œ
if (this.isTurning) {
// ์›ํ˜• ์„ ํšŒ ๊ตฌํ˜„ (์ „ํˆฌ ์ค‘์—๋Š” ๋” ๋น ๋ฅด๊ฒŒ)
const turnRate = (this.speed / this.turnRadius) * this.turnDirection * turnRateMultiplier;
this.targetRotation.y += turnRate * deltaTime;
this.targetRotation.z = this.turnDirection * 0.5 * (this.isEngaged ? 1.5 : 1); // ์ „ํˆฌ ์ค‘ ๋” ํฐ ๋กค
// ๋ชฉํ‘œ ๊ฐ๋„์— ๊ทผ์ ‘ํ•˜๋ฉด ์„ ํšŒ ์ข…๋ฃŒ
const newAngleDiff = targetAngle - this.targetRotation.y;
if (Math.abs(newAngleDiff) < Math.PI / 12) {
this.isTurning = false;
}
} else {
// ์ง์ง„ ๋น„ํ–‰ (์ „ํˆฌ ์ค‘์—๋Š” ๋” ๋น ๋ฅธ ๋ฐ˜์‘)
const lerpSpeed = this.isEngaged ? 1.0 : 0.5;
this.targetRotation.y = THREE.MathUtils.lerp(this.targetRotation.y, targetAngle, deltaTime * lerpSpeed);
this.targetRotation.z = THREE.MathUtils.lerp(this.targetRotation.z, 0, deltaTime * 2);
}
// ๋‹ค๋ฅธ ์ ๊ธฐ์™€์˜ ์ถฉ๋Œ ํšŒํ”ผ
this.avoidOtherEnemies(deltaTime);
// ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „ ์ ์šฉ (์ „ํˆฌ ์ค‘์—๋Š” ๋” ๋น ๋ฅด๊ฒŒ)
const rotationSpeed = this.isEngaged ? 3.0 : 2.0;
this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetRotation.x, deltaTime * rotationSpeed);
this.rotation.y = THREE.MathUtils.lerp(this.rotation.y, this.targetRotation.y, deltaTime * rotationSpeed);
this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRotation.z, deltaTime * rotationSpeed * 1.25);
// ์†๋„ ๋ฒกํ„ฐ ๊ณ„์‚ฐ
const forward = new THREE.Vector3(0, 0, 1);
forward.applyEuler(this.rotation);
this.velocity = forward.multiplyScalar(this.speed);
// ์œ„์น˜ ์—…๋ฐ์ดํŠธ
this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
// ๊ณ ๋„ ์ œํ•œ
if (this.position.y < 500) {
this.position.y = 500;
this.targetRotation.x = -0.2; // ์ƒ์Šน
}
if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE - 500) {
this.position.y = GAME_CONSTANTS.MAX_ALTITUDE - 500;
this.targetRotation.x = 0.2; // ํ•˜๊ฐ•
}
// ๋งต ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ
const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
if (Math.abs(this.position.x) > mapLimit * 0.9 || Math.abs(this.position.z) > mapLimit * 0.9) {
// ๋งต ๊ฒฝ๊ณ„์— ๊ฐ€๊นŒ์›Œ์ง€๋ฉด ์ค‘์•™์œผ๋กœ ํ–ฅํ•˜๋Š” ์ƒˆ ๋ชฉํ‘œ ์„ค์ •
this.targetPosition = new THREE.Vector3(
(Math.random() - 0.5) * mapLimit * 0.5,
1000 + Math.random() * 3000,
(Math.random() - 0.5) * mapLimit * 0.5
);
this.lastDirectionChange = currentTime;
}
// ๋ฉ”์‹œ ์—…๋ฐ์ดํŠธ
this.mesh.position.copy(this.position);
this.mesh.rotation.x = this.rotation.x;
this.mesh.rotation.y = this.rotation.y + 3 * Math.PI / 2;
this.mesh.rotation.z = this.rotation.z;
this.updateBullets(deltaTime);
}
avoidOtherEnemies(deltaTime) {
if (!this.nearbyEnemies) return;
let closestDistance = Infinity;
let avoidanceNeeded = false;
let avoidDirection = new THREE.Vector3();
this.nearbyEnemies.forEach(enemy => {
if (enemy === this || !enemy.position) return;
const distance = this.position.distanceTo(enemy.position);
if (distance < this.separationRadius && distance > 0) {
avoidanceNeeded = true;
if (distance < closestDistance) {
closestDistance = distance;
// ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์œผ๋กœ ํšŒํ”ผ
avoidDirection = this.position.clone().sub(enemy.position).normalize();
}
}
});
if (avoidanceNeeded) {
// ๊ธด๊ธ‰ ํšŒํ”ผ: ๊ธ‰์„ ํšŒ
const avoidAngle = Math.atan2(avoidDirection.x, avoidDirection.z);
this.targetRotation.y = avoidAngle;
this.isTurning = true;
// ๊ณ ๋„๋„ ๋ณ€๊ฒฝ
if (avoidDirection.y > 0) {
this.targetRotation.x = -0.3; // ์ƒ์Šน
} else {
this.targetRotation.x = 0.3; // ํ•˜๊ฐ•
}
}
}
shoot() {
this.lastShootTime = Date.now();
// ์ง์„  ๋ชจ์–‘์˜ ํƒ„ํ™˜ (100% ๋” ํฌ๊ฒŒ)
const bulletGeometry = new THREE.CylinderGeometry(0.8, 0.8, 12, 8); // ๋ฐ˜์ง€๋ฆ„ 0.6โ†’0.8, ๊ธธ์ด 9โ†’12
const bulletMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
emissive: 0xff0000,
emissiveIntensity: 1.0
});
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
const muzzleOffset = new THREE.Vector3(0, 0, 8);
muzzleOffset.applyEuler(this.rotation);
bullet.position.copy(this.position).add(muzzleOffset);
// ํƒ„ํ™˜์„ ๋ฐœ์‚ฌ ๋ฐฉํ–ฅ์œผ๋กœ ํšŒ์ „
bullet.rotation.copy(this.rotation);
bullet.rotateX(Math.PI / 2);
const direction = new THREE.Vector3(0, 0, 1);
direction.applyEuler(this.rotation);
bullet.velocity = direction.multiplyScalar(1200); // 800์—์„œ 1200์œผ๋กœ ์ฆ๊ฐ€ (50% ๋น ๋ฅด๊ฒŒ)
this.scene.add(bullet);
this.bullets.push(bullet);
// MGLAUNCH.ogg ์†Œ๋ฆฌ ์žฌ์ƒ - ํ”Œ๋ ˆ์ด์–ด๊ฐ€ 3000m ์ด๋‚ด์— ์žˆ์„ ๋•Œ๋งŒ
if (this.playerFighter) {
const distanceToPlayer = this.position.distanceTo(this.playerFighter.position);
if (distanceToPlayer < 3000) {
try {
const audio = new Audio('sounds/MGLAUNCH.ogg');
audio.volume = 0.5; // 50% ์Œ๋Ÿ‰
// ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ฅธ ์Œ๋Ÿ‰ ์กฐ์ ˆ (๊ฑฐ๋ฆฌ๊ฐ€ ๋ฉ€์ˆ˜๋ก ์ž‘๊ฒŒ)
const volumeMultiplier = 1 - (distanceToPlayer / 3000);
audio.volume = 0.5 * volumeMultiplier;
audio.play().catch(e => console.log('Enemy gunfire sound failed to play'));
// ์žฌ์ƒ ์™„๋ฃŒ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
audio.addEventListener('ended', () => {
audio.remove();
});
} catch (e) {
console.log('Audio error:', 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.distanceTo(this.position) > 5000 ||
bullet.position.y < 0) {
this.scene.remove(bullet);
this.bullets.splice(i, 1);
}
}
}
takeDamage(damage) {
this.health -= damage;
return this.health <= 0;
}
destroy() {
if (this.mesh) {
this.scene.remove(this.mesh);
this.bullets.forEach(bullet => this.scene.remove(bullet));
this.bullets = [];
this.isLoaded = false;
}
}
}
// ๋ฉ”์ธ ๊ฒŒ์ž„ ํด๋ž˜์Šค
class Game {
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.setClearColor(0x87CEEB);
this.renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('gameContainer').appendChild(this.renderer.domElement);
this.loader = new GLTFLoader();
this.fighter = new Fighter();
this.enemies = [];
this.isLoaded = false;
this.isBGMReady = false;
this.isGameOver = false;
this.gameTime = GAME_CONSTANTS.MISSION_DURATION;
this.score = 0;
this.lastTime = performance.now();
this.gameTimer = null;
this.animationFrameId = null;
this.lastShootTime = 0;
this.bgm = null;
this.bgmPlaying = false;
this.keys = { w: false, a: false, s: false, d: false, f: false };
this.isStarted = false;
this.setupScene();
this.setupEventListeners();
this.preloadGame();
}
async preloadGame() {
try {
console.log('๊ฒŒ์ž„ ๋ฆฌ์†Œ์Šค ์‚ฌ์ „ ๋กœ๋”ฉ ์ค‘...');
await this.fighter.initialize(this.scene, this.loader);
if (!this.fighter.isLoaded) {
throw new Error('์ „ํˆฌ๊ธฐ ๋กœ๋”ฉ ์‹คํŒจ');
}
await this.preloadEnemies();
this.isLoaded = true;
console.log('๊ฒŒ์ž„ ๋ฆฌ์†Œ์Šค ๋กœ๋”ฉ ์™„๋ฃŒ');
await this.preloadBGM();
this.showStartScreen();
this.animate();
} catch (error) {
console.error('๊ฒŒ์ž„ ์‚ฌ์ „ ๋กœ๋”ฉ ์‹คํŒจ:', error);
document.getElementById('loading').innerHTML =
'<div class="loading-text" style="color: red;">๋กœ๋”ฉ ์‹คํŒจ. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด์ฃผ์„ธ์š”.</div>';
}
}
showStartScreen() {
if (this.isLoaded && this.isBGMReady) {
const loadingElement = document.getElementById('loading');
if (loadingElement) {
loadingElement.style.display = 'none';
}
const startScreen = document.getElementById('startScreen');
if (startScreen) {
startScreen.style.display = 'flex';
}
window.dispatchEvent(new Event('gameReady'));
console.log('๋ชจ๋“  ๋ฆฌ์†Œ์Šค ์ค€๋น„ ์™„๋ฃŒ - Start Game ๋ฒ„ํŠผ ํ‘œ์‹œ');
}
}
async preloadBGM() {
console.log('BGM ์‚ฌ์ „ ๋กœ๋”ฉ...');
return new Promise((resolve) => {
try {
this.bgm = new Audio('sounds/main.ogg');
this.bgm.volume = 0.25;
this.bgm.loop = true;
this.bgm.addEventListener('canplaythrough', () => {
console.log('BGM ์žฌ์ƒ ์ค€๋น„ ์™„๋ฃŒ');
this.isBGMReady = true;
resolve();
});
this.bgm.addEventListener('error', (e) => {
console.log('BGM ์—๋Ÿฌ:', e);
this.isBGMReady = true;
resolve();
});
this.bgm.load();
setTimeout(() => {
if (!this.isBGMReady) {
console.log('BGM ๋กœ๋”ฉ ํƒ€์ž„์•„์›ƒ - ๊ฒŒ์ž„ ์ง„ํ–‰');
this.isBGMReady = true;
resolve();
}
}, 3000);
} catch (error) {
console.log('BGM ์‚ฌ์ „ ๋กœ๋”ฉ ์‹คํŒจ:', error);
this.isBGMReady = true;
resolve();
}
});
}
async preloadEnemies() {
for (let i = 0; i < GAME_CONSTANTS.ENEMY_COUNT; i++) {
const angle = (i / GAME_CONSTANTS.ENEMY_COUNT) * Math.PI * 2;
const distance = 6000 + Math.random() * 3000;
const position = new THREE.Vector3(
Math.cos(angle) * distance,
2500 + Math.random() * 2000,
Math.sin(angle) * distance
);
const enemy = new EnemyFighter(this.scene, position);
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(8000, 10000, 8000);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 20000;
directionalLight.shadow.camera.left = -10000;
directionalLight.shadow.camera.right = 10000;
directionalLight.shadow.camera.top = 10000;
directionalLight.shadow.camera.bottom = -10000;
this.scene.add(directionalLight);
const groundGeometry = new THREE.PlaneGeometry(GAME_CONSTANTS.MAP_SIZE, GAME_CONSTANTS.MAP_SIZE);
const groundMaterial = new THREE.MeshLambertMaterial({
color: 0x8FBC8F,
transparent: true,
opacity: 0.8
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
this.scene.add(ground);
this.addClouds();
}
addClouds() {
const cloudGeometry = new THREE.SphereGeometry(100, 8, 6);
const cloudMaterial = new THREE.MeshLambertMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.5
});
for (let i = 0; i < 100; i++) {
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
cloud.position.set(
(Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE,
Math.random() * 4000 + 1000,
(Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE
);
cloud.scale.set(
Math.random() * 3 + 1,
Math.random() * 2 + 0.5,
Math.random() * 3 + 1
);
this.scene.add(cloud);
}
}
setupEventListeners() {
document.addEventListener('keydown', (event) => {
if (this.isGameOver || !gameStarted) return;
switch(event.code) {
case 'KeyW': this.keys.w = true; break;
case 'KeyA': this.keys.a = true; break;
case 'KeyS': this.keys.s = true; break;
case 'KeyD': this.keys.d = true; break;
case 'KeyF': this.keys.f = true; break;
}
});
document.addEventListener('keyup', (event) => {
if (this.isGameOver || !gameStarted) return;
switch(event.code) {
case 'KeyW': this.keys.w = false; break;
case 'KeyA': this.keys.a = false; break;
case 'KeyS': this.keys.s = false; break;
case 'KeyD': this.keys.d = false; break;
case 'KeyF': this.keys.f = false; break;
}
});
document.addEventListener('mousemove', (event) => {
if (!document.pointerLockElement || this.isGameOver || !gameStarted) return;
const deltaX = event.movementX || 0;
const deltaY = event.movementY || 0;
this.fighter.updateMouseInput(deltaX, deltaY);
});
// ๋งˆ์šฐ์Šค ๋‹ค์šด/์—… ์ด๋ฒคํŠธ ์ถ”๊ฐ€
document.addEventListener('mousedown', (event) => {
if (!document.pointerLockElement || this.isGameOver || !gameStarted) 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();
console.log('๊ฒŒ์ž„ ์‹œ์ž‘!');
}
startBGM() {
if (this.bgmPlaying || !this.bgm) return;
console.log('BGM ์žฌ์ƒ ์‹œ๋„...');
const playPromise = this.bgm.play();
if (playPromise !== undefined) {
playPromise.then(() => {
this.bgmPlaying = true;
console.log('BGM ์žฌ์ƒ ์‹œ์ž‘ ์„ฑ๊ณต!');
}).catch(error => {
console.log('์ž๋™ ์žฌ์ƒ์ด ์ฐจ๋‹จ๋จ:', error);
console.log('ํด๋ฆญ ํ›„ ์žฌ์ƒ ์‹œ๋„ ๋Œ€๊ธฐ ์ค‘...');
const tryPlayOnInteraction = () => {
if (!this.bgmPlaying && this.bgm) {
console.log('์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์œผ๋กœ BGM ์žฌ์ƒ ์‹œ๋„...');
this.bgm.play().then(() => {
this.bgmPlaying = true;
console.log('BGM ์žฌ์ƒ ์‹œ์ž‘ ์„ฑ๊ณต (ํด๋ฆญ ํ›„)!');
document.removeEventListener('click', tryPlayOnInteraction);
document.removeEventListener('keydown', tryPlayOnInteraction);
}).catch(e => console.log('BGM ์žฌ์ƒ ์‹คํŒจ:', e));
}
};
document.addEventListener('click', tryPlayOnInteraction);
document.addEventListener('keydown', tryPlayOnInteraction);
});
}
}
startGameTimer() {
this.gameTimer = setInterval(() => {
if (!this.isGameOver) {
this.gameTime--;
if (this.gameTime <= 0) {
this.endGame(true);
}
}
}, 1000);
}
updateUI() {
if (this.fighter.isLoaded) {
const speedKnots = Math.round(this.fighter.speed * 1.94384);
const altitudeFeet = Math.round(this.fighter.altitude * 3.28084);
const altitudeMeters = Math.round(this.fighter.altitude);
const scoreElement = document.getElementById('score');
const timeElement = document.getElementById('time');
const healthElement = document.getElementById('health');
const ammoElement = document.getElementById('ammoDisplay');
const gameStatsElement = document.getElementById('gameStats');
// ์ฒด๋ ฅ๋ฐ”์— ์ˆ˜์น˜ ํ‘œ์‹œ
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) ammoElement.textContent = `AMMO: ${this.fighter.ammo}`;
if (gameStatsElement) {
gameStatsElement.innerHTML = `
<div>Score: ${this.score}</div>
<div>Time: ${this.gameTime}s</div>
<div>Speed: ${speedKnots} KT</div>
<div>Alt: ${altitudeMeters}m (${altitudeFeet} FT)</div>
<div>Throttle: ${Math.round(this.fighter.throttle * 100)}%</div>
<div>G-Force: ${this.fighter.gForce.toFixed(1)}</div>
<div>Targets: ${this.enemies.length}</div>
`;
}
this.updateWarnings();
this.updateHUD();
}
}
updateHUD() {
// HUD ํฌ๋กœ์Šคํ—ค์–ด ์—…๋ฐ์ดํŠธ
const hudElement = document.getElementById('hudCrosshair');
if (!hudElement) return;
// ๋กค ๊ฐ๋„์— ๋”ฐ๋ผ HUD ํšŒ์ „
const rollDegrees = this.fighter.rotation.z * (180 / Math.PI);
hudElement.style.transform = `translate(-50%, -50%) rotate(${-rollDegrees}deg)`;
// ํ”ผ์น˜ ๋ž˜๋” ์—…๋ฐ์ดํŠธ - ์ˆ˜์ •๋œ ๋ถ€๋ถ„
const pitchLadder = document.getElementById('pitchLadder');
if (pitchLadder) {
const pitchDegrees = this.fighter.rotation.x * (180 / Math.PI);
// ์ˆ˜์ •: ์Œ์ˆ˜๋ฅผ ๊ณฑํ•ด์„œ ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์œผ๋กœ, 10๋„๋‹น 20ํ”ฝ์…€
const pitchOffset = -pitchDegrees * 2;
pitchLadder.style.transform = `translateY(${pitchOffset}px)`;
}
// ๋น„ํ–‰ ์ •๋ณด ์—…๋ฐ์ดํŠธ
const speedKnots = Math.round(this.fighter.speed * 1.94384);
const altitudeMeters = Math.round(this.fighter.altitude);
const pitchDegrees = Math.round(this.fighter.rotation.x * (180 / Math.PI));
const rollDegreesRounded = Math.round(rollDegrees);
const headingDegrees = Math.round(((this.fighter.rotation.y * (180 / Math.PI)) + 360) % 360);
// ์„ ํšŒ์œจ ๊ณ„์‚ฐ (๋„/์ดˆ)
if (!this.lastHeading) this.lastHeading = headingDegrees;
let turnRate = (headingDegrees - this.lastHeading);
if (turnRate > 180) turnRate -= 360;
if (turnRate < -180) turnRate += 360;
this.lastHeading = headingDegrees;
const turnRateDegPerSec = Math.round(turnRate / (1/60)); // 60 FPS ๊ธฐ์ค€
// HUD ์ •๋ณด ์—…๋ฐ์ดํŠธ
const hudSpeed = document.getElementById('hudSpeed');
const hudAltitude = document.getElementById('hudAltitude');
const hudHeading = document.getElementById('hudHeading');
const hudPitch = document.getElementById('hudPitch');
const hudRoll = document.getElementById('hudRoll');
const hudTurnRate = document.getElementById('hudTurnRate');
if (hudSpeed) hudSpeed.textContent = `SPD: ${speedKnots} KT`;
if (hudAltitude) hudAltitude.textContent = `ALT: ${altitudeMeters} M`;
if (hudHeading) hudHeading.textContent = `HDG: ${String(headingDegrees).padStart(3, '0')}ยฐ`;
if (hudPitch) hudPitch.textContent = `PITCH: ${pitchDegrees}ยฐ`;
if (hudRoll) hudRoll.textContent = `ROLL: ${rollDegreesRounded}ยฐ`;
if (hudTurnRate) hudTurnRate.textContent = `TURN: ${Math.abs(turnRateDegPerSec) > 1 ? turnRateDegPerSec : 0}ยฐ/s`;
// ์  ํƒ€๊ฒŸ ๋งˆ์ปค ์—…๋ฐ์ดํŠธ
const targetMarkers = document.getElementById('targetMarkers');
if (targetMarkers) {
targetMarkers.innerHTML = '';
// ๋ชจ๋“  ์ ์— ๋Œ€ํ•ด ์ฒ˜๋ฆฌ
this.enemies.forEach(enemy => {
if (!enemy.mesh || !enemy.isLoaded) return;
const distance = this.fighter.position.distanceTo(enemy.position);
if (distance > 10000) return; // 10km ์ด์ƒ์€ ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ
// ์ ์˜ ํ™”๋ฉด ์ขŒํ‘œ ๊ณ„์‚ฐ
const enemyScreenPos = this.getScreenPosition(enemy.position);
if (!enemyScreenPos) return;
// ํ™”๋ฉด ์ค‘์•™์œผ๋กœ๋ถ€ํ„ฐ์˜ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const distFromCenter = Math.sqrt(
Math.pow(enemyScreenPos.x - centerX, 2) +
Math.pow(enemyScreenPos.y - centerY, 2)
);
// ํฌ๋กœ์Šคํ—ค์–ด ๋ฐ˜๊ฒฝ (75px)
const crosshairRadius = 75;
const isInCrosshair = distFromCenter < crosshairRadius;
// ํƒ€๊ฒŸ ๋งˆ์ปค ์ƒ์„ฑ
const marker = document.createElement('div');
marker.className = 'target-marker';
if (isInCrosshair) {
marker.classList.add('in-crosshair');
// 2000m ์ด๋‚ด๋ฉด ๋ฝ์˜จ
if (distance < 2000) {
marker.classList.add('locked');
}
// ํƒ€๊ฒŸ ๋ฐ•์Šค ์ถ”๊ฐ€
const targetBox = document.createElement('div');
targetBox.className = 'target-box';
marker.appendChild(targetBox);
// ๊ฑฐ๋ฆฌ ์ •๋ณด ์ถ”๊ฐ€
const targetInfo = document.createElement('div');
targetInfo.className = 'target-info';
targetInfo.textContent = `${Math.round(distance)}m`;
marker.appendChild(targetInfo);
}
marker.style.left = `${enemyScreenPos.x}px`;
marker.style.top = `${enemyScreenPos.y}px`;
targetMarkers.appendChild(marker);
});
}
}
getScreenPosition(worldPosition) {
// 3D ์ขŒํ‘œ๋ฅผ ํ™”๋ฉด ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜
const vector = worldPosition.clone();
vector.project(this.camera);
// ์นด๋ฉ”๋ผ ๋’ค์— ์žˆ๋Š” ๊ฐ์ฒด๋Š” ํ‘œ์‹œํ•˜์ง€ ์•Š์Œ
if (vector.z > 1) return null;
const x = (vector.x * 0.5 + 0.5) * window.innerWidth;
const y = (-vector.y * 0.5 + 0.5) * window.innerHeight;
return { x, y };
}
// Game ํด๋ž˜์Šค์˜ updateWarnings ๋ฉ”์„œ๋“œ ์ˆ˜์ •
updateWarnings() {
// ๊ธฐ์กด ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
const existingWarnings = document.querySelectorAll('.warning-message');
existingWarnings.forEach(w => w.remove());
// ์Šคํ†จ ํƒˆ์ถœ ๊ฒฝ๊ณ  ์ œ๊ฑฐ
const existingStallWarnings = document.querySelectorAll('.stall-escape-warning');
existingStallWarnings.forEach(w => w.remove());
// ๊ณ ๋„ ๊ฒฝ๊ณ  ์™ธ๊ณฝ ํšจ๊ณผ
let altitudeEdgeEffect = document.getElementById('altitudeEdgeEffect');
if (this.fighter.altitude < 500) {
if (!altitudeEdgeEffect) {
altitudeEdgeEffect = document.createElement('div');
altitudeEdgeEffect.id = 'altitudeEdgeEffect';
document.body.appendChild(altitudeEdgeEffect);
}
let edgeIntensity;
if (this.fighter.altitude < 250) {
// PULL UP ๊ฒฝ๊ณ  - ๊ฐ•ํ•œ ๋ถ‰์€ ํšจ๊ณผ
edgeIntensity = 0.6;
altitudeEdgeEffect.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1300;
background: radial-gradient(ellipse at center,
transparent 40%,
rgba(255, 0, 0, ${edgeIntensity * 0.3}) 60%,
rgba(255, 0, 0, ${edgeIntensity}) 100%);
box-shadow: inset 0 0 150px rgba(255, 0, 0, ${edgeIntensity}),
inset 0 0 100px rgba(255, 0, 0, ${edgeIntensity * 0.8});
animation: pulse-red 0.5s infinite;
`;
} else {
// LOW ALTITUDE ๊ฒฝ๊ณ  - ์•ฝํ•œ ๋ถ‰์€ ํšจ๊ณผ
edgeIntensity = 0.3;
altitudeEdgeEffect.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1300;
background: radial-gradient(ellipse at center,
transparent 50%,
rgba(255, 0, 0, ${edgeIntensity * 0.2}) 70%,
rgba(255, 0, 0, ${edgeIntensity}) 100%);
box-shadow: inset 0 0 100px rgba(255, 0, 0, ${edgeIntensity}),
inset 0 0 50px rgba(255, 0, 0, ${edgeIntensity * 0.5});
`;
}
} else {
// ๊ณ ๋„๊ฐ€ ์•ˆ์ „ํ•˜๋ฉด ํšจ๊ณผ ์ œ๊ฑฐ
if (altitudeEdgeEffect) {
altitudeEdgeEffect.remove();
}
}
if (this.fighter.warningBlinkState) {
const warningContainer = document.createElement('div');
warningContainer.className = 'warning-message';
warningContainer.style.cssText = `
position: fixed;
top: 30%;
left: 50%;
transform: translateX(-50%);
color: #ff0000;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
z-index: 1500;
text-align: center;
`;
let warningText = '';
if (this.fighter.altitude < 250) {
warningText += 'PULL UP! PULL UP!\n';
} else if (this.fighter.altitude < 500) {
warningText += 'LOW ALTITUDE WARNING\n';
}
if (this.fighter.altitudeWarning) {
warningText += 'ALTITUDE LIMIT\n';
}
if (this.fighter.stallWarning) {
warningText += 'STALL WARNING\n';
}
if (this.fighter.overG) {
warningText += 'OVER-G! OVER-G!\n';
}
if (warningText) {
warningContainer.innerHTML = warningText.replace(/\n/g, '<br>');
document.body.appendChild(warningContainer);
}
}
// ์Šคํ†จ ์ƒํƒœ์ผ ๋•Œ๋งŒ "Press F to Escape" ๊ฒฝ๊ณ  ํ‘œ์‹œ
if (this.fighter.stallWarning) {
const stallEscapeWarning = document.createElement('div');
stallEscapeWarning.className = 'stall-escape-warning';
stallEscapeWarning.style.cssText = `
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 0, 0, 0.8);
color: #ffffff;
font-size: 28px;
font-weight: bold;
padding: 15px 30px;
border: 3px solid #ff0000;
border-radius: 10px;
z-index: 1600;
text-align: center;
animation: blink 0.5s infinite;
`;
stallEscapeWarning.innerHTML = 'PRESS F TO ESCAPE';
document.body.appendChild(stallEscapeWarning);
// ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ ์ถ”๊ฐ€
if (!document.getElementById('blinkAnimation')) {
const style = document.createElement('style');
style.id = 'blinkAnimation';
style.innerHTML = `
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.3; }
}
@keyframes pulse-green {
0%, 100% {
opacity: 1;
transform: translateX(-50%) scale(1);
}
50% {
opacity: 0.8;
transform: translateX(-50%) scale(1.1);
}
}
@keyframes box-pulse {
0%, 100% {
background: rgba(255, 0, 0, 0.9);
box-shadow: 0 0 20px rgba(255, 0, 0, 0.8),
0 0 40px rgba(255, 0, 0, 0.4);
}
50% {
background: rgba(255, 50, 50, 1);
box-shadow: 0 0 30px rgba(255, 100, 100, 1),
0 0 60px rgba(255, 0, 0, 0.8);
}
}
@keyframes pulse-red {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
`;
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);
}
});
}
checkCollisions() {
for (let i = this.fighter.bullets.length - 1; i >= 0; i--) {
const bullet = this.fighter.bullets[i];
for (let j = this.enemies.length - 1; j >= 0; j--) {
const enemy = this.enemies[j];
if (!enemy.mesh || !enemy.isLoaded) continue;
const distance = bullet.position.distanceTo(enemy.position);
if (distance < 90) { // 60์—์„œ 90์œผ๋กœ ์ฆ๊ฐ€ (100% ํ™•๋Œ€)
this.scene.remove(bullet);
this.fighter.bullets.splice(i, 1);
// ํžˆํŠธ ํ‘œ์‹œ ์ถ”๊ฐ€
this.showHitMarker(enemy.position);
// ํ”ผ๊ฒฉ ์ดํŽ™ํŠธ ์ถ”๊ฐ€
this.createHitEffect(enemy.position);
if (enemy.takeDamage(GAME_CONSTANTS.BULLET_DAMAGE)) { // 25 ๋ฐ๋ฏธ์ง€
enemy.destroy();
this.enemies.splice(j, 1);
this.score += 100;
}
break;
}
}
}
this.enemies.forEach(enemy => {
enemy.bullets.forEach((bullet, index) => {
const distance = bullet.position.distanceTo(this.fighter.position);
if (distance < 100) { // 65์—์„œ 100์œผ๋กœ ์ฆ๊ฐ€ (100% ํ™•๋Œ€)
this.scene.remove(bullet);
enemy.bullets.splice(index, 1);
// ํ”Œ๋ ˆ์ด์–ด ํ”ผ๊ฒฉ ์ดํŽ™ํŠธ
this.createHitEffect(this.fighter.position);
if (this.fighter.takeDamage(GAME_CONSTANTS.BULLET_DAMAGE)) { // 25 ๋ฐ๋ฏธ์ง€
this.endGame(false);
}
}
});
});
}
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, // ์ฃผํ™ฉ์ƒ‰๊ณผ ๋ถ‰์€์ƒ‰ ํ˜ผํ•ฉ
emissive: 0xffaa00,
emissiveIntensity: 1.0,
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,
emissive: 0xffff00,
emissiveIntensity: 2.0,
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 => {});
} catch (e) {}
}
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) {
// Fํ‚ค ์ƒํƒœ๋ฅผ Fighter์— ์ „๋‹ฌ
this.fighter.escapeKeyPressed = this.keys.f;
this.fighter.updateControls(this.keys, deltaTime);
this.fighter.updatePhysics(deltaTime);
this.fighter.updateBullets(this.scene, deltaTime);
// ๋งˆ์šฐ์Šค ๋ˆ„๋ฆ„ ์ƒํƒœ์ผ ๋•Œ ์—ฐ์† ๋ฐœ์‚ฌ
if (this.fighter.isMouseDown && this.isStarted) {
const currentShootTime = Date.now();
if (!this.lastShootTime || currentShootTime - this.lastShootTime >= 100) { // 0.1์ดˆ๋งˆ๋‹ค
this.fighter.shoot(this.scene);
this.lastShootTime = currentShootTime;
}
}
if (this.isStarted) {
// ์ ๊ธฐ๋“ค์—๊ฒŒ ์„œ๋กœ์˜ ์ฐธ์กฐ ์ „๋‹ฌ (์ถฉ๋Œ ํšŒํ”ผ์šฉ)
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 && this.fighter.position.y <= 0) {
this.endGame(false, "GROUND COLLISION");
return;
}
this.updateUI();
this.updateRadar();
if (this.enemies.length === 0) {
this.endGame(true);
}
}
const targetCameraPos = this.fighter.getCameraPosition();
const targetCameraTarget = this.fighter.getCameraTarget();
this.camera.position.lerp(targetCameraPos, this.fighter.cameraLag);
this.camera.lookAt(targetCameraTarget);
} else {
if (this.fighter.isLoaded) {
const initialCameraPos = this.fighter.getCameraPosition();
const initialTarget = this.fighter.getCameraTarget();
this.camera.position.copy(initialCameraPos);
this.camera.lookAt(initialTarget);
}
}
this.renderer.render(this.scene, this.camera);
}
endGame(victory = false, reason = "") {
this.isGameOver = true;
if (this.fighter && this.fighter.stopAllWarningAudios) {
this.fighter.stopAllWarningAudios();
}
if (this.bgm) {
this.bgm.pause();
this.bgm = null;
this.bgmPlaying = false;
}
if (this.gameTimer) {
clearInterval(this.gameTimer);
}
document.exitPointerLock();
// ๋ชจ๋“  ๊ฒฝ๊ณ  ๋ฐ ํšจ๊ณผ ์ œ๊ฑฐ
const existingWarnings = document.querySelectorAll('.warning-message, .stall-escape-warning');
existingWarnings.forEach(w => w.remove());
const blurEffect = document.getElementById('overGBlurEffect');
if (blurEffect) {
blurEffect.remove();
}
const 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('์ „ํˆฌ๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘...');
window.gameInstance = new Game();
});