cutechicken's picture
Update game.js
42dc3ed verified
raw
history blame
65.7 kB
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/r17/Stats.min.js"></script>
<title>JET FIGHT SIMULATER - FPS Mode</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #000;
font-family: 'Courier New', monospace;
}
#enemyLabels {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.enemy-label {
position: absolute;
background-color: rgba(255, 0, 0, 0.7);
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
font-family: Arial, sans-serif;
transform: translate(-50%, -50%);
white-space: nowrap;
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.8);
padding: 20px;
border-radius: 10px;
z-index: 2000;
text-align: center;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #0f0;
border-top: 5px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
color: #0f0;
font-size: 24px;
text-align: center;
}
#gameContainer {
position: relative;
width: 100vw;
height: 100vh;
cursor: crosshair;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: #0f0;
background: rgba(0,20,0,0.7);
padding: 10px;
font-size: 14px;
z-index: 1001;
border: 1px solid #0f0;
border-radius: 5px;
user-select: none;
}
#crosshair {
position: fixed;
top: 25%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
border: 2px solid #00ff00;
border-radius: 50%;
z-index: 1001;
pointer-events: none;
transition: all 0.2s ease;
}
/* ๋ฌผ์ฒด๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ์Šคํƒ€์ผ */
#crosshair.target-detected {
border-color: #ff0000;
border-width: 3px;
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
}
#crosshair::before,
#crosshair::after {
content: '';
position: absolute;
background: #00ff00;
transition: all 0.2s ease;
}
#crosshair::before {
top: 50%;
left: -10px;
right: -10px;
height: 2px;
transform: translateY(-50%);
}
#crosshair::after {
left: 50%;
top: -10px;
bottom: -10px;
width: 2px;
transform: translateX(-50%);
}
/* ๋ฌผ์ฒด๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ํฌ๋กœ์Šค ๋ผ์ธ ์Šคํƒ€์ผ */
#crosshair.target-detected::before,
#crosshair.target-detected::after {
background: #ff0000;
}
#healthBar {
position: absolute;
bottom: 20px;
left: 20px;
width: 200px;
height: 20px;
background: rgba(0,20,0,0.7);
border: 2px solid #0f0;
z-index: 1001;
border-radius: 10px;
overflow: hidden;
}
#health {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #0f0, #00ff00);
transition: width 0.3s;
}
#ammo {
position: absolute;
bottom: 20px;
right: 20px;
color: #0f0;
background: rgba(0,20,0,0.7);
padding: 10px;
font-size: 20px;
z-index: 1001;
border: 1px solid #0f0;
border-radius: 5px;
}
#gameTitle {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
color: #0f0;
background: rgba(0,20,0,0.7);
padding: 10px 20px;
font-size: 20px;
z-index: 1001;
border: 1px solid #0f0;
border-radius: 5px;
text-transform: uppercase;
letter-spacing: 2px;
}
#ammoDisplay {
position: absolute;
bottom: 20px;
right: 20px;
color: #0f0;
background: rgba(0,20,0,0.7);
padding: 10px;
font-size: 20px;
z-index: 1001;
border: 1px solid #0f0;
border-radius: 5px;
}
#reloadingText {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ff0000;
font-size: 24px;
font-weight: bold;
display: none;
z-index: 1002;
}
#radar {
position: absolute;
bottom: 60px; /* ์ฒด๋ ฅ๋ฐ” ์œ„๋กœ ์ด๋™ */
left: 20px;
width: 200px;
height: 200px;
background: rgba(0,20,0,0.3);
border: 2px solid #0f0;
border-radius: 50%;
z-index: 1001;
overflow: hidden;
}
#mission {
position: absolute;
top: 10px;
left: 10px;
color: #0f0;
background: rgba(0,20,0,0.7);
padding: 10px;
font-size: 16px;
z-index: 1001;
border: 1px solid #0f0;
border-radius: 5px;
}
#radarLine {
position: absolute;
top: 50%;
left: 50%;
width: 50%;
height: 2px;
background: #0f0;
transform-origin: left center;
animation: radar-sweep 4s infinite linear;
}
.enemy-dot {
position: absolute;
width: 6px;
height: 6px;
background: #ff0000;
border-radius: 50%;
transform: translate(-50%, -50%);
}
@keyframes radar-sweep {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
#gameStats {
position: absolute;
top: 10px;
right: 20px;
color: #0f0;
background: rgba(0,20,0,0.7);
padding: 10px;
font-size: 16px;
z-index: 1001;
border: 1px solid #0f0;
border-radius: 5px;
text-align: right;
}
.start-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
display: none; /* ์ดˆ๊ธฐ์—๋Š” ์ˆจ๊น€ */
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 2000;
}
.start-button {
padding: 15px 30px;
font-size: 24px;
background: #0f0;
color: #000;
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 20px;
transition: transform 0.2s;
}
.start-button:hover {
transform: scale(1.1);
}
#minimap {
position: absolute;
bottom: 20px;
right: 20px;
width: 200px;
height: 200px;
background: rgba(0,20,0,0.7);
border: 2px solid #0f0;
border-radius: 5px;
z-index: 1001;
}
</style>
</head>
<body>
<!-- ๋กœ๋”ฉ ํ™”๋ฉด (์ฒ˜์Œ์— ํ‘œ์‹œ๋จ) -->
<div id="loading">
<div class="loading-spinner"></div>
<div class="loading-text">Loading Fighter Resources...</div>
<div style="color: #0f0; font-size: 16px; margin-top: 10px;">
<p>Loading Aircraft Models...</p>
<p>Loading Audio Assets...</p>
<p>Preparing Game Environment...</p>
</div>
</div>
<!-- ๊ฒŒ์ž„ ์‹œ์ž‘ ํ™”๋ฉด (๋กœ๋”ฉ ์™„๋ฃŒ ํ›„ ํ‘œ์‹œ๋จ) -->
<div class="start-screen" id="startScreen">
<h1 style="color: #0f0; font-size: 48px; margin-bottom: 20px;">JET FIGHT SIMULATER</h1>
<button class="start-button" onclick="startGame()">Start Game</button>
<div style="color: #0f0; margin-top: 20px; text-align: center;">
<p>Controls:</p>
<p>W/S - Throttle Control</p>
<p>A/D - Rudder Control</p>
<p>Mouse - Aircraft Control</p>
<p>Left Click - Fire</p>
</div>
</div>
<!-- ๊ฒŒ์ž„ ํ™”๋ฉด -->
<div id="gameContainer">
<div id="gameTitle">JET FIGHT SIMULATER</div>
<div id="mission">MISSION: DESTROY ENEMY JET</div>
<div id="gameStats">
<div id="score">Score: 0</div>
<div id="time">Time: 180s</div>
</div>
<div id="crosshair"></div>
<div id="detected" style="position: absolute; top: 55%; left: 50%; transform: translate(-50%, -50%); color: red; font-size: 16px; display: none;">DETECTED</div>
<div id="enemyLabels"></div>
<!-- ์ฒด๋ ฅ๋ฐ” -->
<div id="healthBar">
<div id="health"></div>
</div>
<!-- ํƒ„์•ฝ ํ‘œ์‹œ -->
<div id="ammoDisplay">AMMO: 300</div>
<!-- ๋ฆฌ๋กœ๋”ฉ ํ…์ŠคํŠธ -->
<div id="reloadingText">RELOADING...</div>
<!-- ๋ ˆ์ด๋” -->
<div id="radar">
<div id="radarLine"></div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
}
</script>
<script>
// ๊ฒŒ์ž„ ์ƒํƒœ ์ถ”์  ๋ณ€์ˆ˜
let gameCanStart = false;
let gameStarted = false;
function startGame() {
// ๊ฒŒ์ž„๊ณผ BGM์ด ๋ชจ๋‘ ์ค€๋น„๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
if (!gameInstance || !gameInstance.isLoaded || !gameInstance.isBGMReady) {
console.log('๊ฒŒ์ž„์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค...');
return;
}
gameStarted = true;
document.getElementById('startScreen').style.display = 'none';
// ๊ฒŒ์ž„์ด ์‹œ์ž‘๋  ๋•Œ๋งŒ ํฌ์ธํ„ฐ ๋ฝ ์š”์ฒญ
document.body.requestPointerLock();
// ์ฆ‰์‹œ BGM ์žฌ์ƒ ์‹œ๋„ (์‚ฌ์šฉ์ž ํด๋ฆญ ์‹œ์ )
gameInstance.startBGM();
gameInstance.startGame();
}
// ํฌ์ธํ„ฐ ๋ฝ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement === document.body) {
console.log('Pointer locked');
} else {
console.log('Pointer unlocked');
// ๊ฒŒ์ž„์ด ์‹œ์ž‘๋œ ์ƒํƒœ์—์„œ ํฌ์ธํ„ฐ ๋ฝ์ด ํ•ด์ œ๋˜๋ฉด ์•Œ๋ฆผ ํ‘œ์‹œ
if (gameStarted && gameInstance && !gameInstance.isGameOver) {
console.log('๊ฒŒ์ž„ ์ค‘ ํฌ์ธํ„ฐ ๋ฝ ํ•ด์ œ๋จ - ํด๋ฆญํ•˜์—ฌ ๋‹ค์‹œ ์ž ๊ทธ์„ธ์š”');
// ๊ฐ„๋‹จํ•œ ์•Œ๋ฆผ ํ‘œ์‹œ (์„ ํƒ์‚ฌํ•ญ)
showPointerLockNotification();
}
}
});
// ํฌ์ธํ„ฐ ๋ฝ ํ•ด์ œ ์•Œ๋ฆผ ํ•จ์ˆ˜
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('click', (event) => {
// Start Game ๋ฒ„ํŠผ์ด ์•„๋‹ˆ๊ณ , ๊ฒŒ์ž„์ด ์•„์ง ์‹œ์ž‘๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ํฌ์ธํ„ฐ ๋ฝ ๋ฐฉ์ง€
if (!gameStarted && !event.target.classList.contains('start-button')) {
event.preventDefault();
event.stopPropagation();
return false;
}
// ๊ฒŒ์ž„์ด ์‹œ์ž‘๋œ ํ›„์˜ ํด๋ฆญ ์ฒ˜๋ฆฌ
if (gameStarted && gameInstance && !gameInstance.isGameOver) {
// ํฌ์ธํ„ฐ๊ฐ€ ์ž ๊ธฐ์ง€ ์•Š์€ ์ƒํƒœ๋ผ๋ฉด ๋‹ค์‹œ ์ž ๊ทธ๊ธฐ ์‹œ๋„
if (!document.pointerLockElement) {
console.log('๊ฒŒ์ž„ ์ค‘ ํด๋ฆญ - ํฌ์ธํ„ฐ ๋ฝ ์žฌ์š”์ฒญ');
document.body.requestPointerLock();
}
// ํฌ์ธํ„ฐ๊ฐ€ ์ž ๊ธด ์ƒํƒœ๋ผ๋ฉด ์ด ๋ฐœ์‚ฌ
else if (gameInstance.fighter.isLoaded) {
gameInstance.fighter.shoot(gameInstance.scene);
}
}
}, true); // ์บก์ฒ˜ ๋‹จ๊ณ„์—์„œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
// ๊ฒŒ์ž„ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ค€๋น„๋˜์—ˆ์„ ๋•Œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
window.addEventListener('gameReady', () => {
gameCanStart = true;
});
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// ๊ฒŒ์ž„ ์ƒ์ˆ˜
const GAME_CONSTANTS = {
MISSION_DURATION: 180,
MAP_SIZE: 15000,
MAX_ALTITUDE: 15000, // 15km๋กœ ๋ณ€๊ฒฝ
MIN_ALTITUDE: 0, // 0m๋กœ ๋ณ€๊ฒฝ
MAX_SPEED: 800,
STALL_SPEED: 120,
GRAVITY: 9.8,
MOUSE_SENSITIVITY: 0.001,
MAX_G_FORCE: 12.0,
ENEMY_COUNT: 4,
MISSILE_COUNT: 6,
AMMO_COUNT: 300
};
// ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค
class Fighter {
constructor() {
this.mesh = null;
this.isLoaded = false;
// ๋ฌผ๋ฆฌ ์†์„ฑ
this.position = new THREE.Vector3(0, 2000, 0);
this.velocity = new THREE.Vector3(0, 0, 150);
this.acceleration = new THREE.Vector3(0, 0, 0);
this.rotation = new THREE.Euler(0, 0, 0);
// ๋น„ํ–‰ ์ œ์–ด
this.baseSpeed = 150;
this.throttle = 0.5;
this.speed = 150;
this.altitude = 2000;
this.gForce = 1.0;
this.health = 100;
// ์กฐ์ข… ์ž…๋ ฅ ์‹œ์Šคํ…œ
this.pitchInput = 0;
this.rollInput = 0;
this.yawInput = 0;
// ๋งˆ์šฐ์Šค ๋ˆ„์  ์ž…๋ ฅ
this.mousePitch = 0;
this.mouseRoll = 0;
// ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „์„ ์œ„ํ•œ ๋ชฉํ‘œ๊ฐ’
this.targetPitch = 0;
this.targetRoll = 0;
this.targetYaw = 0;
// ๋ฌด๊ธฐ
this.missiles = GAME_CONSTANTS.MISSILE_COUNT;
this.ammo = GAME_CONSTANTS.AMMO_COUNT;
this.bullets = [];
this.lastShootTime = 0;
// ์นด๋ฉ”๋ผ ์„ค์ •
this.cameraDistance = 250; // ์ ์ ˆํ•œ ๊ฑฐ๋ฆฌ
this.cameraHeight = 30; // ์•ฝ๊ฐ„ ์œ„์ชฝ์—์„œ
this.cameraLag = 0.06; // ๋” ๋น ๋ฅธ ์ถ”์ 
// ๊ฒฝ๊ณ  ์‹œ์Šคํ…œ
this.altitudeWarning = false;
this.stallWarning = false;
this.warningBlinkTimer = 0;
this.warningBlinkState = false;
}
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);
// ๋ชจ๋ธ ๋ฐฉํ–ฅ ์กฐ์ • (45๋„ ํšŒ์ „)
this.mesh.rotation.y = Math.PI / 4; // 45๋„ ํšŒ์ „
// ๊ทธ๋ฆผ์ž ์„ค์ •
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();
// ๋™์ฒด - Z์ถ• ๋ฐฉํ–ฅ์œผ๋กœ ์ƒ์„ฑํ•˜์—ฌ ์•ž์„ ํ–ฅํ•˜๊ฒŒ ํ•จ
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; // X์ถ•์œผ๋กœ 90๋„ ํšŒ์ „ํ•˜์—ฌ ๋จธ๋ฆฌ๊ฐ€ ์•ž์„ ํ–ฅํ•˜๊ฒŒ ํ•จ
group.add(fuselage);
// ์ฃผ์ต
const wingGeometry = new THREE.BoxGeometry(16, 0.3, 4);
const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 });
const wings = new THREE.Mesh(wingGeometry, wingMaterial);
wings.position.z = -1;
group.add(wings);
// ์ˆ˜์ง ์•ˆ์ •ํŒ
const tailGeometry = new THREE.BoxGeometry(0.3, 4, 3);
const tailMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 });
const tail = new THREE.Mesh(tailGeometry, tailMaterial);
tail.position.z = -5;
tail.position.y = 1.5;
group.add(tail);
// ์ˆ˜ํ‰ ์•ˆ์ •ํŒ
const horizontalTailGeometry = new THREE.BoxGeometry(6, 0.2, 2);
const horizontalTail = new THREE.Mesh(horizontalTailGeometry, tailMaterial);
horizontalTail.position.z = -5;
horizontalTail.position.y = 0.5;
group.add(horizontalTail);
this.mesh = group;
this.mesh.position.copy(this.position);
this.mesh.scale.set(2, 2, 2);
scene.add(this.mesh);
this.isLoaded = true;
console.log('Fallback ์ „ํˆฌ๊ธฐ ๋ชจ๋ธ ์ƒ์„ฑ ์™„๋ฃŒ');
}
updateMouseInput(deltaX, deltaY) {
// ๋งˆ์šฐ์Šค ๊ฐ๋„ ์กฐ์ •
const sensitivity = GAME_CONSTANTS.MOUSE_SENSITIVITY * 2.0;
// ๋งˆ์šฐ์Šค ์›€์ง์ž„์„ ์ง์ ‘ ํšŒ์ „ ๊ฐ๋„์— ๋ˆ„์  ์ ์šฉ (์ƒ๋Œ€์  ์ž…๋ ฅ)
// X์ถ• (์ขŒ/์šฐ) -> ๋กค ๋ณ€ํ™”๋Ÿ‰
this.targetRoll += deltaX * sensitivity;
// Y์ถ• (์œ„/์•„๋ž˜) -> ํ”ผ์น˜ ๋ณ€ํ™”๋Ÿ‰ (๋ฐ˜์ „: ๋งˆ์šฐ์Šค ์•„๋ž˜ = ๊ธฐ์ˆ˜ ์œ„๋กœ)
this.targetPitch -= deltaY * sensitivity;
// ์—„๊ฒฉํ•œ ๊ฐ๋„ ์ œํ•œ (์›Œ์ฌ๋” ํ˜„์‹ค์„ฑ)
const maxPitchAngle = Math.PI / 3.5; // ์•ฝ 51๋„ (๋งค์šฐ ์ œํ•œ์ )
const maxRollAngle = Math.PI; // 180๋„ (๋ฐฐ๋Ÿด๋กค ๊ฐ€๋Šฅ)
this.targetPitch = Math.max(-maxPitchAngle, Math.min(maxPitchAngle, this.targetPitch));
this.targetRoll = Math.max(-maxRollAngle, Math.min(maxRollAngle, this.targetRoll));
// ๋กค ๊ฐ๋„ ์ •๊ทœํ™” (360๋„ ํšŒ์ „ ์‹œ 0๋„๋กœ ๋ฆฌ์…‹)
if (this.targetRoll > Math.PI) {
this.targetRoll -= 2 * Math.PI;
} else if (this.targetRoll < -Math.PI) {
this.targetRoll += 2 * Math.PI;
}
}
updateControls(keys, deltaTime) {
// ์Šค๋กœํ‹€ ์ œ์–ด
if (keys.w) {
this.throttle = Math.min(1.0, this.throttle + deltaTime * 0.8);
}
if (keys.s) {
this.throttle = Math.max(0.1, this.throttle - deltaTime * 0.8);
}
// ๋Ÿฌ๋” ์ œ์–ด (ํ‚ค๋ณด๋“œ A/D) - ์ˆœ์ˆ˜ ์š” ํšŒ์ „๋งŒ
if (keys.a) {
this.targetYaw -= deltaTime * 0.8; // ์™ผ์ชฝ ๋Ÿฌ๋”
}
if (keys.d) {
this.targetYaw += deltaTime * 0.8; // ์˜ค๋ฅธ์ชฝ ๋Ÿฌ๋”
}
// ๋งˆ์šฐ์Šค ์ž…๋ ฅ ๋ณ€์ˆ˜๋“ค ์ดˆ๊ธฐํ™” (์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ)
this.pitchInput = 0;
this.rollInput = 0;
this.yawInput = 0;
this.mousePitch = 0;
this.mouseRoll = 0;
}
updatePhysics(deltaTime) {
if (!this.mesh) return;
// ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „ ๋ณด๊ฐ„
const rotationSpeed = deltaTime * 3.0;
this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetPitch, rotationSpeed);
this.rotation.y = THREE.MathUtils.lerp(this.rotation.y, this.targetYaw, rotationSpeed);
this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRoll, rotationSpeed);
// ๊ธฐ๋ณธ ์†๋„ ๊ณ„์‚ฐ
const minSpeed = 100;
const maxSpeed = 300;
let targetSpeed = minSpeed + (maxSpeed - minSpeed) * this.throttle;
// ๊ฐ•ํ™”๋œ ์Šคํ†จ ์‹œ์Šคํ…œ: ๋†’์€ ๋ฐ›์Œ๊ฐ์—์„œ ๊ธ‰๊ฒฉํ•œ ์†๋„ ๊ฐ์†Œ
const pitchAngle = Math.abs(this.rotation.x);
if (pitchAngle > Math.PI / 6) { // 30๋„ ์ด์ƒ๋ถ€ํ„ฐ ์†๋„ ๊ฐ์†Œ ์‹œ์ž‘
const stallFactor = Math.max(0.2, 1 - (pitchAngle - Math.PI / 6) * 3);
targetSpeed *= stallFactor;
}
// ์ˆ˜์ง์— ๊ฐ€๊นŒ์šธ์ˆ˜๋ก ๋” ํฐ ์†๋„ ๊ฐ์†Œ
if (pitchAngle > Math.PI / 4) { // 45๋„ ์ด์ƒ์—์„œ ๋” ํฐ ํŒจ๋„ํ‹ฐ
const severeStallFactor = Math.max(0.1, 1 - (pitchAngle - Math.PI / 4) * 4);
targetSpeed *= severeStallFactor;
}
this.speed = THREE.MathUtils.lerp(this.speed, targetSpeed, deltaTime * 3); // ๋” ๋น ๋ฅธ ์†๋„ ๋ณ€ํ™”
// ์Šคํ†จ ๊ฒฝ๊ณ  (์†๋„ 60% ์ดํ•˜๋กœ ๊ฐ•ํ™”)
const normalSpeed = minSpeed + (maxSpeed - minSpeed) * this.throttle;
this.stallWarning = (this.speed / normalSpeed) < 0.6;
// ์›Œ์ฌ๋” ์Šคํƒ€์ผ: ๋กค์— ๋”ฐ๋ฅธ ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฐฉํ–ฅ ์ „ํ™˜
// ๋กค์ด ์žˆ์„ ๋•Œ๋งŒ ์ž์—ฐ์Šค๋Ÿฌ์šด ์„ ํšŒ ๋ฐœ์ƒ
const rollAngle = Math.abs(this.rotation.z);
if (rollAngle > 0.1 && pitchAngle < Math.PI / 4) { // 45๋„ ์ดํ•˜์—์„œ๋งŒ ์ •์ƒ ์„ ํšŒ
const rollEffect = Math.sin(this.rotation.z);
const turnRate = rollEffect * deltaTime * 0.3; // ์„ ํšŒ์œจ ๊ฐ์†Œ
this.targetYaw += turnRate;
}
// ํ•ต์‹ฌ: ์ „ํˆฌ๊ธฐ๋Š” ๊ธฐ์ˆ˜๊ฐ€ ํ–ฅํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์ง์ ‘ ์ด๋™
const noseDirection = new THREE.Vector3(0, 0, 1);
noseDirection.applyEuler(this.rotation);
// ๊ธฐ์ˆ˜ ๋ฐฉํ–ฅ์œผ๋กœ ์†๋„ ๋ฒกํ„ฐ ์„ค์ •
this.velocity = noseDirection.multiplyScalar(this.speed);
// ๊ฐ•ํ™”๋œ ์ค‘๋ ฅ ํšจ๊ณผ (๊ธ‰๊ธฐ๋™ ์‹œ ๋” ํฐ ์˜ํ–ฅ)
let gravityEffect = GAME_CONSTANTS.GRAVITY * deltaTime * 0.05;
if (pitchAngle > Math.PI / 6) {
gravityEffect *= (1 + pitchAngle * 2); // ๊ธ‰๊ธฐ๋™ ์‹œ ์ค‘๋ ฅ ์˜ํ–ฅ ์ฆ๊ฐ€
}
this.velocity.y -= gravityEffect;
// ์œ„์น˜ ์—…๋ฐ์ดํŠธ
this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
// ์ง€๋ฉด ์ถฉ๋Œ (0m์—์„œ ํญ๋ฐœ)
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;
}
// ๊ณ ๋„ ๋ฐ G-Force ๊ณ„์‚ฐ
this.altitude = this.position.y;
this.gForce = 1.0 + (Math.abs(this.rotation.x) + Math.abs(this.rotation.z)) * 3;
}
shoot(scene) {
const currentTime = Date.now();
if (currentTime - this.lastShootTime < 100 || this.ammo <= 0) return;
this.lastShootTime = currentTime;
this.ammo--;
const bulletGeometry = new THREE.SphereGeometry(0.2);
const bulletMaterial = new THREE.MeshBasicMaterial({
color: 0xffff00,
emissive: 0xffff00,
emissiveIntensity: 0.7
});
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
// ์ด๊ตฌ ์œ„์น˜ ๊ณ„์‚ฐ (์ „ํˆฌ๊ธฐ ์•ž์ชฝ)
const muzzleOffset = new THREE.Vector3(0, 0, 8);
muzzleOffset.applyEuler(this.rotation);
bullet.position.copy(this.position).add(muzzleOffset);
const bulletSpeed = 1000;
const direction = new THREE.Vector3(0, 0, 1);
direction.applyEuler(this.rotation);
bullet.velocity = direction.multiplyScalar(bulletSpeed).add(this.velocity);
scene.add(bullet);
this.bullets.push(bullet);
// ๋ฐœ์‚ฌ์Œ (์—๋Ÿฌ ๋ฐฉ์ง€)
try {
const audio = new Audio('sounds/gunfire.ogg');
if (audio) {
audio.volume = 0.3;
audio.play().catch(e => console.log('Gunfire sound failed to play'));
}
} catch (e) {
// ์กฐ์šฉํžˆ ๋ฌด์‹œ
}
}
updateBullets(scene, deltaTime) {
for (let i = this.bullets.length - 1; i >= 0; i--) {
const bullet = this.bullets[i];
bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
if (bullet.position.distanceTo(this.position) > 8000 ||
bullet.position.y < 0 ||
bullet.position.y > GAME_CONSTANTS.MAX_ALTITUDE + 500) {
scene.remove(bullet);
this.bullets.splice(i, 1);
}
}
}
takeDamage(damage) {
this.health -= damage;
return this.health <= 0;
}
// ๋งค์šฐ ์•ˆ์ •ํ™”๋œ ์นด๋ฉ”๋ผ ์‹œ์Šคํ…œ (๊ทน๋‹จ์  ์ƒํ™ฉ ์™„์ „ ๋ฐฉ์ง€)
getCameraPosition() {
// ์ „ํˆฌ๊ธฐ์˜ ํ˜„์žฌ ๋ฐฉํ–ฅ ๋ฒกํ„ฐ๋“ค
const backward = new THREE.Vector3(0, 0, -1);
let up = new THREE.Vector3(0, 1, 0);
// ํ”ผ์น˜ ๊ฐ๋„ ์ฒดํฌ
const pitchAngle = Math.abs(this.rotation.x);
// ๊ทน๋‹จ์ ์ธ ํ”ผ์น˜์—์„œ ์นด๋ฉ”๋ผ ๊ฐ•์ œ ์•ˆ์ •ํ™”
if (pitchAngle > Math.PI / 6) { // 30๋„ ์ด์ƒ์—์„œ ์•ˆ์ •ํ™” ์‹œ์ž‘
// ์›”๋“œ ์—… ๋ฒกํ„ฐ๋ฅผ ๊ฐ•์ œ๋กœ ์‚ฌ์šฉํ•˜์—ฌ ์นด๋ฉ”๋ผ ๋’ค์ง‘ํž˜ ๋ฐฉ์ง€
const worldUp = new THREE.Vector3(0, 1, 0);
const stabilizeFactor = Math.min((pitchAngle - Math.PI / 6) / (Math.PI / 12), 0.9);
up = up.clone().applyEuler(this.rotation).lerp(worldUp, stabilizeFactor);
} else {
// ์ •์ƒ ๋ฒ”์œ„์—์„œ๋Š” ์ „ํˆฌ๊ธฐ ๋ฐฉํ–ฅ ๋”ฐ๋ผ๊ฐ€๊ธฐ
up.applyEuler(this.rotation);
}
// ๋ฐฑ์›Œ๋“œ ๋ฒกํ„ฐ๋Š” ํ•ญ์ƒ ์ „ํˆฌ๊ธฐ ๋ฐฉํ–ฅ ์ ์šฉ
backward.applyEuler(this.rotation);
// ์นด๋ฉ”๋ผ ์œ„์น˜ ๊ณ„์‚ฐ (๋” ๋ฉ€๋ฆฌ, ๋” ๋†’๊ฒŒ)
const cameraPosition = this.position.clone()
.add(backward.multiplyScalar(this.cameraDistance))
.add(up.multiplyScalar(this.cameraHeight));
return cameraPosition;
}
getCameraTarget() {
// ์ „ํˆฌ๊ธฐ ์ค‘์‹ฌ์ ์„ ์•ˆ์ •์ ์œผ๋กœ ๋ฐ”๋ผ๋ณด๊ธฐ
return this.position.clone();
}
}
// ์  ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค
class EnemyFighter {
constructor(scene, position) {
this.mesh = null;
this.isLoaded = false;
this.scene = scene;
this.position = position.clone();
this.velocity = new THREE.Vector3(0, 0, 120);
this.rotation = new THREE.Euler(0, 0, 0);
this.health = 100;
this.speed = 120;
this.bullets = [];
this.lastShootTime = 0;
this.aiState = 'patrol';
this.targetPosition = position.clone();
this.patrolCenter = position.clone();
this.patrolRadius = 2000;
this.lastStateChange = 0;
}
async initialize(loader) {
try {
const result = await loader.loadAsync('models/mig-29.glb');
this.mesh = result.scene;
this.mesh.position.copy(this.position);
this.mesh.scale.set(1.5, 1.5, 1.5);
// ์  ์ „ํˆฌ๊ธฐ๋„ 270๋„ ํšŒ์ „
this.mesh.rotation.y = 3 * Math.PI / 2;
this.mesh.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
this.scene.add(this.mesh);
this.isLoaded = true;
console.log('MiG-29 ์ ๊ธฐ ๋กœ๋”ฉ ์™„๋ฃŒ');
} catch (error) {
console.error('MiG-29 ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ:', error);
this.createFallbackModel();
}
}
createFallbackModel() {
const group = new THREE.Group();
const fuselageGeometry = new THREE.CylinderGeometry(0.6, 1.0, 8, 8);
const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x800000 });
const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial);
fuselage.rotation.x = -Math.PI / 2; // ๋™์ผํ•œ ๋ฐฉํ–ฅ ์กฐ์ •
group.add(fuselage);
const wingGeometry = new THREE.BoxGeometry(12, 0.3, 3);
const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x600000 });
const wings = new THREE.Mesh(wingGeometry, wingMaterial);
wings.position.z = -0.5;
group.add(wings);
this.mesh = group;
this.mesh.position.copy(this.position);
this.mesh.scale.set(1.5, 1.5, 1.5);
this.scene.add(this.mesh);
this.isLoaded = true;
console.log('Fallback ์ ๊ธฐ ๋ชจ๋ธ ์ƒ์„ฑ ์™„๋ฃŒ');
}
update(playerPosition, deltaTime) {
if (!this.mesh || !this.isLoaded) return;
const currentTime = Date.now();
const distanceToPlayer = this.position.distanceTo(playerPosition);
if (distanceToPlayer < 5000) {
const direction = new THREE.Vector3()
.subVectors(playerPosition, this.position)
.normalize();
this.velocity = direction.multiplyScalar(this.speed);
this.rotation.y = Math.atan2(direction.x, direction.z);
if (distanceToPlayer < 2000 && currentTime - this.lastShootTime > 1500) {
this.shoot();
}
} else {
if (this.position.distanceTo(this.targetPosition) < 300) {
const angle = Math.random() * Math.PI * 2;
this.targetPosition = this.patrolCenter.clone().add(
new THREE.Vector3(
Math.cos(angle) * this.patrolRadius,
(Math.random() - 0.5) * 1000,
Math.sin(angle) * this.patrolRadius
)
);
}
const direction = new THREE.Vector3()
.subVectors(this.targetPosition, this.position)
.normalize();
this.velocity = direction.multiplyScalar(this.speed * 0.7);
this.rotation.y = Math.atan2(direction.x, direction.z);
}
this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
if (this.position.y < GAME_CONSTANTS.MIN_ALTITUDE) {
this.position.y = GAME_CONSTANTS.MIN_ALTITUDE;
}
if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) {
this.position.y = GAME_CONSTANTS.MAX_ALTITUDE;
}
const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
if (this.position.x > mapLimit) this.position.x = -mapLimit;
if (this.position.x < -mapLimit) this.position.x = mapLimit;
if (this.position.z > mapLimit) this.position.z = -mapLimit;
if (this.position.z < -mapLimit) this.position.z = mapLimit;
// ์  ์ „ํˆฌ๊ธฐ ๋ฉ”์‹œ ์—…๋ฐ์ดํŠธ ์‹œ์—๋„ ๋ฐฉํ–ฅ ์กฐ์ •
this.mesh.position.copy(this.position);
this.mesh.rotation.x = this.rotation.x;
this.mesh.rotation.y = this.rotation.y + 3 * Math.PI / 2; // ๊ธฐ๋ณธ 270๋„ ํšŒ์ „ ์œ ์ง€
this.mesh.rotation.z = this.rotation.z;
this.updateBullets(deltaTime);
}
shoot() {
this.lastShootTime = Date.now();
const bulletGeometry = new THREE.SphereGeometry(0.15);
const bulletMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
emissive: 0xff0000,
emissiveIntensity: 0.5
});
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
const muzzleOffset = new THREE.Vector3(0, 0, 6);
muzzleOffset.applyEuler(this.rotation);
bullet.position.copy(this.position).add(muzzleOffset);
const direction = new THREE.Vector3(0, 0, 1);
direction.applyEuler(this.rotation);
bullet.velocity = direction.multiplyScalar(800);
this.scene.add(bullet);
this.bullets.push(bullet);
}
updateBullets(deltaTime) {
for (let i = this.bullets.length - 1; i >= 0; i--) {
const bullet = this.bullets[i];
bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
if (bullet.position.distanceTo(this.position) > 5000 ||
bullet.position.y < 0) {
this.scene.remove(bullet);
this.bullets.splice(i, 1);
}
}
}
takeDamage(damage) {
this.health -= damage;
return this.health <= 0;
}
destroy() {
if (this.mesh) {
this.scene.remove(this.mesh);
this.bullets.forEach(bullet => this.scene.remove(bullet));
this.bullets = [];
this.isLoaded = false;
}
}
}
// ๋ฉ”์ธ ๊ฒŒ์ž„ ํด๋ž˜์Šค
class Game {
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.setClearColor(0x87CEEB);
this.renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('gameContainer').appendChild(this.renderer.domElement);
this.loader = new GLTFLoader();
this.fighter = new Fighter();
this.enemies = [];
this.isLoaded = false;
this.isBGMReady = false;
this.isGameOver = false;
this.gameTime = GAME_CONSTANTS.MISSION_DURATION;
this.score = 0;
this.lastTime = performance.now();
this.gameTimer = null;
this.animationFrameId = null;
this.bgm = null;
this.bgmPlaying = false;
this.keys = { w: false, a: false, s: false, d: false };
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('๊ฒŒ์ž„ ๋ฆฌ์†Œ์Šค ๋กœ๋”ฉ ์™„๋ฃŒ');
// BGM ์‚ฌ์ „ ๋กœ๋”ฉ
await this.preloadBGM();
// ๋ชจ๋“  ๋ฆฌ์†Œ์Šค ๋กœ๋”ฉ ์™„๋ฃŒ ํ›„ UI ์—…๋ฐ์ดํŠธ
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';
}
// Start Game ํ™”๋ฉด ํ‘œ์‹œ
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.5;
this.bgm.loop = true;
// BGM ๋กœ๋”ฉ ์™„๋ฃŒ ์ด๋ฒคํŠธ
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();
// ํƒ€์ž„์•„์›ƒ ์„ค์ • (3์ดˆ ํ›„์—๋„ ๋กœ๋”ฉ์ด ์•ˆ๋˜๋ฉด ๊ฐ•์ œ๋กœ ์™„๋ฃŒ)
setTimeout(() => {
if (!this.isBGMReady) {
console.log('BGM ๋กœ๋”ฉ ํƒ€์ž„์•„์›ƒ - ๊ฒŒ์ž„ ์ง„ํ–‰');
this.isBGMReady = true;
resolve();
}
}, 3000);
} catch (error) {
console.log('BGM ์‚ฌ์ „ ๋กœ๋”ฉ ์‹คํŒจ:', error);
this.isBGMReady = true; // ์—๋Ÿฌ๊ฐ€ ๋‚˜๋”๋ผ๋„ ๊ฒŒ์ž„์€ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ
resolve();
}
});
}
async preloadEnemies() {
for (let i = 0; i < GAME_CONSTANTS.ENEMY_COUNT; i++) {
const angle = (i / GAME_CONSTANTS.ENEMY_COUNT) * Math.PI * 2;
const distance = 6000 + Math.random() * 3000;
const position = new THREE.Vector3(
Math.cos(angle) * distance,
2500 + Math.random() * 2000,
Math.sin(angle) * distance
);
const enemy = new EnemyFighter(this.scene, position);
await enemy.initialize(this.loader);
this.enemies.push(enemy);
}
}
setupScene() {
this.scene.background = new THREE.Color(0x87CEEB);
this.scene.fog = new THREE.Fog(0x87CEEB, 1000, 30000);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(8000, 10000, 8000);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 20000;
directionalLight.shadow.camera.left = -10000;
directionalLight.shadow.camera.right = 10000;
directionalLight.shadow.camera.top = 10000;
directionalLight.shadow.camera.bottom = -10000;
this.scene.add(directionalLight);
const groundGeometry = new THREE.PlaneGeometry(GAME_CONSTANTS.MAP_SIZE, GAME_CONSTANTS.MAP_SIZE);
const groundMaterial = new THREE.MeshLambertMaterial({
color: 0x8FBC8F,
transparent: true,
opacity: 0.8
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
this.scene.add(ground);
this.addClouds();
}
addClouds() {
const cloudGeometry = new THREE.SphereGeometry(100, 8, 6);
const cloudMaterial = new THREE.MeshLambertMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.5
});
for (let i = 0; i < 100; i++) {
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
cloud.position.set(
(Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE,
Math.random() * 4000 + 1000,
(Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE
);
cloud.scale.set(
Math.random() * 3 + 1,
Math.random() * 2 + 0.5,
Math.random() * 3 + 1
);
this.scene.add(cloud);
}
}
setupEventListeners() {
document.addEventListener('keydown', (event) => {
if (this.isGameOver || !gameStarted) return;
switch(event.code) {
case 'KeyW': this.keys.w = true; break;
case 'KeyA': this.keys.a = true; break;
case 'KeyS': this.keys.s = true; break;
case 'KeyD': this.keys.d = true; break;
}
});
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;
}
});
document.addEventListener('mousemove', (event) => {
if (!document.pointerLockElement || this.isGameOver || !gameStarted) return;
const deltaX = event.movementX || 0;
const deltaY = event.movementY || 0;
this.fighter.updateMouseInput(deltaX, deltaY);
});
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
}
startGame() {
if (!this.isLoaded) {
console.log('๊ฒŒ์ž„์ด ์•„์ง ๋กœ๋”ฉ ์ค‘์ž…๋‹ˆ๋‹ค...');
return;
}
this.isStarted = true;
this.startGameTimer();
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);
// ์•ˆ์ „ํ•œ DOM ์š”์†Œ ์—…๋ฐ์ดํŠธ
const scoreElement = document.getElementById('score');
const timeElement = document.getElementById('time');
const healthElement = document.getElementById('health');
const ammoElement = document.getElementById('ammoDisplay');
const gameStatsElement = document.getElementById('gameStats');
if (scoreElement) scoreElement.textContent = `Score: ${this.score}`;
if (timeElement) timeElement.textContent = `Time: ${this.gameTime}s`;
if (healthElement) healthElement.style.width = `${this.fighter.health}%`;
if (ammoElement) ammoElement.textContent = `AMMO: ${this.fighter.ammo}`;
if (gameStatsElement) {
gameStatsElement.innerHTML = `
<div>Score: ${this.score}</div>
<div>Time: ${this.gameTime}s</div>
<div>Speed: ${speedKnots} KT</div>
<div>Alt: ${altitudeMeters}m (${altitudeFeet} FT)</div>
<div>Throttle: ${Math.round(this.fighter.throttle * 100)}%</div>
<div>G-Force: ${this.fighter.gForce.toFixed(1)}</div>
<div>Targets: ${this.enemies.length}</div>
`;
}
// ๊ฒฝ๊ณ  ์‹œ์Šคํ…œ UI ์—…๋ฐ์ดํŠธ
this.updateWarnings();
}
}
updateWarnings() {
// ๊ธฐ์กด ๊ฒฝ๊ณ  ์ œ๊ฑฐ
const existingWarnings = document.querySelectorAll('.warning-message');
existingWarnings.forEach(w => w.remove());
// ๊นœ๋นก์ž„ ์ƒํƒœ์ผ ๋•Œ๋งŒ ๊ฒฝ๊ณ  ํ‘œ์‹œ
if (this.fighter.warningBlinkState) {
const warningContainer = document.createElement('div');
warningContainer.className = 'warning-message';
warningContainer.style.cssText = `
position: fixed;
top: 30%;
left: 50%;
transform: translateX(-50%);
color: #ff0000;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
z-index: 1500;
text-align: center;
`;
let warningText = '';
if (this.fighter.altitudeWarning) {
warningText += 'ALTITUDE WARNING\n';
}
if (this.fighter.stallWarning) {
warningText += 'STALL WARNING\n';
}
if (warningText) {
warningContainer.innerHTML = warningText.replace(/\n/g, '<br>');
document.body.appendChild(warningContainer);
}
}
}
updateRadar() {
const radar = document.getElementById('radar');
if (!radar) return;
const oldDots = radar.getElementsByClassName('enemy-dot');
while (oldDots[0]) {
oldDots[0].remove();
}
const radarCenter = { x: 100, y: 100 };
const radarRange = 10000;
this.enemies.forEach(enemy => {
if (!enemy.mesh || !enemy.isLoaded) return;
const distance = this.fighter.position.distanceTo(enemy.position);
if (distance <= radarRange) {
const relativePos = enemy.position.clone().sub(this.fighter.position);
const angle = Math.atan2(relativePos.x, relativePos.z);
const relativeDistance = distance / radarRange;
const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance);
const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance);
const dot = document.createElement('div');
dot.className = 'enemy-dot';
dot.style.left = `${dotX}px`;
dot.style.top = `${dotY}px`;
radar.appendChild(dot);
}
});
}
checkCollisions() {
for (let i = this.fighter.bullets.length - 1; i >= 0; i--) {
const bullet = this.fighter.bullets[i];
for (let j = this.enemies.length - 1; j >= 0; j--) {
const enemy = this.enemies[j];
if (!enemy.mesh || !enemy.isLoaded) continue;
const distance = bullet.position.distanceTo(enemy.position);
if (distance < 25) {
this.scene.remove(bullet);
this.fighter.bullets.splice(i, 1);
if (enemy.takeDamage(30)) {
enemy.destroy();
this.enemies.splice(j, 1);
this.score += 100;
}
break;
}
}
}
this.enemies.forEach(enemy => {
enemy.bullets.forEach((bullet, index) => {
const distance = bullet.position.distanceTo(this.fighter.position);
if (distance < 30) {
this.scene.remove(bullet);
enemy.bullets.splice(index, 1);
if (this.fighter.takeDamage(20)) {
this.endGame(false);
}
}
});
});
}
animate() {
if (this.isGameOver) return;
this.animationFrameId = requestAnimationFrame(() => this.animate());
const currentTime = performance.now();
const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.1);
this.lastTime = currentTime;
if (this.isLoaded && this.fighter.isLoaded) {
this.fighter.updateControls(this.keys, deltaTime);
this.fighter.updatePhysics(deltaTime);
this.fighter.updateBullets(this.scene, deltaTime);
if (this.isStarted) {
this.enemies.forEach(enemy => {
enemy.update(this.fighter.position, deltaTime);
});
this.checkCollisions();
// ์ง€๋ฉด ์ถฉ๋Œ ์ฒดํฌ (0m์—์„œ ํญ๋ฐœ)
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.bgm) {
this.bgm.pause();
this.bgm = null;
this.bgmPlaying = false;
}
if (this.gameTimer) {
clearInterval(this.gameTimer);
}
document.exitPointerLock();
// ๊ธฐ์กด ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
const existingWarnings = document.querySelectorAll('.warning-message');
existingWarnings.forEach(w => w.remove());
const gameOverDiv = document.createElement('div');
gameOverDiv.className = 'start-screen';
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;
// ๊ฒŒ์ž„ ์ดˆ๊ธฐํ™”
document.addEventListener('DOMContentLoaded', () => {
console.log('์ „ํˆฌ๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘...');
window.gameInstance = new Game();
});
</script>
</body>
</html>