Spaces:
Running
Running
| <!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> |