import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; // 게임 상수 const GAME_DURATION = 180; const MAP_SIZE = 2000; const HELICOPTER_HEIGHT = 30; const ENEMY_GROUND_HEIGHT = 0; const ENEMY_SCALE = 10; const MAX_HEALTH = 1000; const ENEMY_MOVE_SPEED = 0.1; const ENEMY_COUNT_MAX = 5; const PARTICLE_COUNT = 15; const OBSTACLE_COUNT = 50; const ENEMY_CONFIG = { ATTACK_RANGE: 100, ATTACK_INTERVAL: 2000, BULLET_SPEED: 2 }; // 게임 변수 let scene, camera, renderer, controls; let enemies = []; let bullets = []; let enemyBullets = []; let playerHealth = MAX_HEALTH; let ammo = 30; let currentStage = 1; let isGameOver = false; let lastTime = performance.now(); let lastRender = 0; // 오실레이터 기반 총소리 생성기 class GunSoundGenerator { constructor() { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } createGunshot() { const currentTime = this.audioContext.currentTime; // 메인 오실레이터 const osc = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); osc.type = 'square'; osc.frequency.setValueAtTime(200, currentTime); osc.frequency.exponentialRampToValueAtTime(50, currentTime + 0.1); gainNode.gain.setValueAtTime(0.5, currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.1); osc.connect(gainNode); gainNode.connect(this.audioContext.destination); osc.start(currentTime); osc.stop(currentTime + 0.1); // 노이즈 추가 const bufferSize = this.audioContext.sampleRate * 0.1; const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1; } const noise = this.audioContext.createBufferSource(); const noiseGain = this.audioContext.createGain(); noise.buffer = buffer; noiseGain.gain.setValueAtTime(0.2, currentTime); noiseGain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.05); noise.connect(noiseGain); noiseGain.connect(this.audioContext.destination); noise.start(currentTime); } resume() { if (this.audioContext.state === 'suspended') { this.audioContext.resume(); } } } // 사운드 시스템 초기화 const gunSound = new GunSoundGenerator(); async function init() { document.getElementById('loading').style.display = 'block'; try { // Scene 초기화 scene = new THREE.Scene(); scene.background = new THREE.Color(0x87ceeb); scene.fog = new THREE.Fog(0x87ceeb, 0, 1000); // Renderer 최적화 renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.BasicShadowMap; document.body.appendChild(renderer.domElement); // Camera 설정 camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, HELICOPTER_HEIGHT, 0); // 기본 조명 scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(100, 100, 50); dirLight.castShadow = true; dirLight.shadow.mapSize.width = 1024; dirLight.shadow.mapSize.height = 1024; scene.add(dirLight); // Controls 설정 controls = new PointerLockControls(camera, document.body); // 이벤트 리스너 setupEventListeners(); // 모델 테스트 먼저 실행 await testModelLoading(); // 게임 요소 초기화 await Promise.all([ createTerrain(), createEnemies() ]); document.getElementById('loading').style.display = 'none'; console.log('Game initialized successfully'); } catch (error) { console.error('Initialization error:', error); document.getElementById('loading').innerHTML = `
Error loading models. Please check console and file paths.
`; throw error; } } function setupEventListeners() { document.addEventListener('click', onClick); document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); window.addEventListener('resize', onWindowResize); } async function testModelLoading() { const loader = new GLTFLoader(); try { const modelPath = 'models/enemy1.glb'; console.log('Testing model loading:', modelPath); const gltf = await loader.loadAsync(modelPath); console.log('Test model loaded successfully:', gltf); } catch (error) { console.error('Test model loading failed:', error); throw error; } } function createTerrain() { return new Promise((resolve) => { const geometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100); const material = new THREE.MeshStandardMaterial({ color: 0xD2B48C, roughness: 0.8, metalness: 0.2 }); const vertices = geometry.attributes.position.array; for (let i = 0; i < vertices.length; i += 3) { vertices[i + 2] = Math.sin(vertices[i] * 0.01) * Math.cos(vertices[i + 1] * 0.01) * 20; } geometry.attributes.position.needsUpdate = true; geometry.computeVertexNormals(); const terrain = new THREE.Mesh(geometry, material); terrain.rotation.x = -Math.PI / 2; terrain.receiveShadow = true; scene.add(terrain); addObstacles(); resolve(); }); } function addObstacles() { const rockGeometry = new THREE.DodecahedronGeometry(10); const rockMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9 }); for (let i = 0; i < OBSTACLE_COUNT; i++) { const rock = new THREE.Mesh(rockGeometry, rockMaterial); rock.position.set( (Math.random() - 0.5) * MAP_SIZE * 0.9, Math.random() * 10, (Math.random() - 0.5) * MAP_SIZE * 0.9 ); rock.rotation.set( Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI ); rock.castShadow = true; rock.receiveShadow = true; scene.add(rock); } } async function createEnemies() { console.log('Creating enemies...'); const loader = new GLTFLoader(); const enemyCount = Math.min(3 + currentStage, ENEMY_COUNT_MAX); for (let i = 0; i < enemyCount; i++) { const angle = (i / enemyCount) * Math.PI * 2; const radius = 200; const position = new THREE.Vector3( Math.cos(angle) * radius, ENEMY_GROUND_HEIGHT, Math.sin(angle) * radius ); // 임시 적 생성 const tempEnemy = createTemporaryEnemy(position); scene.add(tempEnemy.model); enemies.push(tempEnemy); // GLB 모델 로드 try { const modelIndex = i % 4 + 1; const modelPath = `models/enemy${modelIndex}.glb`; console.log(`Loading model: ${modelPath}`); const gltf = await loader.loadAsync(modelPath); const model = gltf.scene; // 모델 설정 model.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE); model.position.copy(position); // 모델 재질 및 그림자 설정 model.traverse((node) => { if (node.isMesh) { node.castShadow = true; node.receiveShadow = true; node.material.metalness = 0.2; node.material.roughness = 0.8; } }); // 임시 모델 교체 scene.remove(tempEnemy.model); scene.add(model); enemies[enemies.indexOf(tempEnemy)].model = model; console.log(`Successfully loaded enemy model ${modelIndex}`); } catch (error) { console.error(`Error loading enemy model:`, error); } } } function createTemporaryEnemy(position) { const geometry = new THREE.BoxGeometry(5, 10, 5); const material = new THREE.MeshPhongMaterial({ color: 0xff0000, transparent: true, opacity: 0.8 }); const model = new THREE.Mesh(geometry, material); model.position.copy(position); model.castShadow = true; model.receiveShadow = true; return { model: model, health: 100, speed: ENEMY_MOVE_SPEED, lastAttackTime: 0 }; } function createExplosion(position) { const particles = []; for (let i = 0; i < PARTICLE_COUNT; i++) { const particle = new THREE.Mesh( new THREE.SphereGeometry(0.3), new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 1 }) ); particle.position.copy(position); particle.velocity = new THREE.Vector3( (Math.random() - 0.5) * 2, Math.random() * 2, (Math.random() - 0.5) * 2 ); particles.push(particle); scene.add(particle); } // 폭발 광원 효과 const explosionLight = new THREE.PointLight(0xff4400, 2, 20); explosionLight.position.copy(position); scene.add(explosionLight); let opacity = 1; const animate = () => { opacity -= 0.05; if (opacity <= 0) { particles.forEach(p => scene.remove(p)); scene.remove(explosionLight); return; } particles.forEach(particle => { particle.position.add(particle.velocity); particle.material.opacity = opacity; }); requestAnimationFrame(animate); }; animate(); } function onClick() { if (!controls.isLocked) { controls.lock(); gunSound.resume(); } else if (ammo > 0) { shoot(); } } function onKeyDown(event) { switch(event.code) { case 'KeyW': moveState.forward = true; break; case 'KeyS': moveState.backward = true; break; case 'KeyA': moveState.left = true; break; case 'KeyD': moveState.right = true; break; case 'KeyR': reload(); break; } } function onKeyUp(event) { switch(event.code) { case 'KeyW': moveState.forward = false; break; case 'KeyS': moveState.backward = false; break; case 'KeyA': moveState.left = false; break; case 'KeyD': moveState.right = false; break; } } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } // 이동 상태 const moveState = { forward: false, backward: false, left: false, right: false }; function shoot() { if (ammo <= 0) return; ammo--; updateAmmoDisplay(); const bullet = createBullet(); bullets.push(bullet); gunSound.createGunshot(); // 총구 화염 효과 const muzzleFlash = new THREE.PointLight(0xffff00, 3, 10); muzzleFlash.position.copy(camera.position); scene.add(muzzleFlash); setTimeout(() => scene.remove(muzzleFlash), 50); } function createBullet() { const bullet = new THREE.Mesh( new THREE.SphereGeometry(0.5), new THREE.MeshBasicMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 1 }) ); bullet.position.copy(camera.position); const direction = new THREE.Vector3(); camera.getWorldDirection(direction); bullet.velocity = direction.multiplyScalar(5); scene.add(bullet); return bullet; } function createEnemyBullet(enemy) { const bullet = new THREE.Mesh( new THREE.SphereGeometry(0.5), new THREE.MeshBasicMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 1 }) ); bullet.position.copy(enemy.model.position); bullet.position.y += 5; const direction = new THREE.Vector3(); direction.subVectors(camera.position, enemy.model.position).normalize(); bullet.velocity = direction.multiplyScalar(ENEMY_CONFIG.BULLET_SPEED); scene.add(bullet); return bullet; } function updateMovement() { if (controls.isLocked) { const speed = 2.0; if (moveState.forward) controls.moveForward(speed); if (moveState.backward) controls.moveForward(-speed); if (moveState.left) controls.moveRight(-speed); if (moveState.right) controls.moveRight(speed); // 고도 제한 if (camera.position.y < HELICOPTER_HEIGHT) { camera.position.y = HELICOPTER_HEIGHT; } else if (camera.position.y > HELICOPTER_HEIGHT + 10) { camera.position.y = HELICOPTER_HEIGHT + 10; } } } function updateBullets() { for (let i = bullets.length - 1; i >= 0; i--) { if (!bullets[i]) continue; bullets[i].position.add(bullets[i].velocity); // 적과의 충돌 검사 for (let j = enemies.length - 1; j >= 0; j--) { const enemy = enemies[j]; if (!enemy || !enemy.model) continue; if (bullets[i] && bullets[i].position.distanceTo(enemy.model.position) < 10) { scene.remove(bullets[i]); bullets.splice(i, 1); enemy.health -= 25; createExplosion(enemy.model.position.clone()); if (enemy.health <= 0) { createExplosion(enemy.model.position.clone()); scene.remove(enemy.model); enemies.splice(j, 1); } break; } } // 범위 벗어난 총알 제거 if (bullets[i] && bullets[i].position.distanceTo(camera.position) > 1000) { scene.remove(bullets[i]); bullets.splice(i, 1); } } } function updateEnemyBullets() { for (let i = enemyBullets.length - 1; i >= 0; i--) { if (!enemyBullets[i]) continue; enemyBullets[i].position.add(enemyBullets[i].velocity); if (enemyBullets[i].position.distanceTo(camera.position) < 3) { playerHealth -= 10; updateHealthBar(); createExplosion(enemyBullets[i].position.clone()); scene.remove(enemyBullets[i]); enemyBullets.splice(i, 1); if (playerHealth <= 0) { gameOver(false); } continue; } if (enemyBullets[i].position.distanceTo(camera.position) > 1000) { scene.remove(enemyBullets[i]); enemyBullets.splice(i, 1); } } } function updateEnemies() { const currentTime = Date.now(); enemies.forEach(enemy => { if (!enemy || !enemy.model) return; // 적 이동 로직 const direction = new THREE.Vector3(); direction.subVectors(camera.position, enemy.model.position); direction.y = 0; direction.normalize(); const newPosition = enemy.model.position.clone() .add(direction.multiplyScalar(enemy.speed)); newPosition.y = ENEMY_GROUND_HEIGHT; enemy.model.position.copy(newPosition); // 적이 플레이어를 바라보도록 설정 enemy.model.lookAt(new THREE.Vector3( camera.position.x, enemy.model.position.y, camera.position.z )); // 공격 로직 const distanceToPlayer = enemy.model.position.distanceTo(camera.position); if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE && currentTime - enemy.lastAttackTime > ENEMY_CONFIG.ATTACK_INTERVAL) { enemyBullets.push(createEnemyBullet(enemy)); enemy.lastAttackTime = currentTime; // 공격 시 발광 효과 const attackFlash = new THREE.PointLight(0xff0000, 2, 20); attackFlash.position.copy(enemy.model.position); scene.add(attackFlash); setTimeout(() => scene.remove(attackFlash), 100); } }); } function reload() { ammo = 30; updateAmmoDisplay(); } function updateAmmoDisplay() { document.getElementById('ammo').textContent = `Ammo: ${ammo}/30`; } function updateHealthBar() { const healthElement = document.getElementById('health'); const healthPercentage = (playerHealth / MAX_HEALTH) * 100; healthElement.style.width = `${healthPercentage}%`; } function updateHelicopterHUD() { document.querySelector('#altitude-indicator span').textContent = Math.round(camera.position.y); const speed = Math.round( Math.sqrt( moveState.forward * moveState.forward + moveState.right * moveState.right ) * 100 ); document.querySelector('#speed-indicator span').textContent = speed; const heading = Math.round( (camera.rotation.y * (180 / Math.PI) + 360) % 360 ); document.querySelector('#compass span').textContent = heading; updateRadar(); } function updateRadar() { const radarTargets = document.querySelector('.radar-targets'); radarTargets.innerHTML = ''; enemies.forEach(enemy => { if (!enemy || !enemy.model) return; const relativePos = enemy.model.position.clone().sub(camera.position); const distance = relativePos.length(); if (distance < 500) { const playerAngle = camera.rotation.y; const enemyAngle = Math.atan2(relativePos.x, relativePos.z); const relativeAngle = enemyAngle - playerAngle; const normalizedDistance = distance / 500; const dot = document.createElement('div'); dot.className = 'radar-dot'; dot.style.left = `${50 + Math.sin(relativeAngle) * normalizedDistance * 45}%`; dot.style.top = `${50 + Math.cos(relativeAngle) * normalizedDistance * 45}%`; radarTargets.appendChild(dot); } }); } function checkGameStatus() { if (enemies.length === 0 && currentStage < 5) { currentStage++; document.getElementById('stage').style.display = 'block'; document.getElementById('stage').textContent = `Stage ${currentStage}`; setTimeout(() => { document.getElementById('stage').style.display = 'none'; createEnemies(); }, 2000); } } function cleanupResources() { bullets.forEach(bullet => scene.remove(bullet)); bullets = []; enemyBullets.forEach(bullet => scene.remove(bullet)); enemyBullets = []; enemies.forEach(enemy => { if (enemy && enemy.model) { scene.remove(enemy.model); } }); enemies = []; } function gameOver(won) { isGameOver = true; controls.unlock(); cleanupResources(); setTimeout(() => { alert(won ? 'Mission Complete!' : 'Game Over!'); location.reload(); }, 100); } function gameLoop(timestamp) { requestAnimationFrame(gameLoop); // 프레임 제한 (60fps) if (timestamp - lastRender < 16) { return; } lastRender = timestamp; if (controls.isLocked && !isGameOver) { updateMovement(); updateBullets(); updateEnemies(); updateEnemyBullets(); updateHelicopterHUD(); checkGameStatus(); } renderer.render(scene, camera); } // 성능 모니터링 let lastFpsUpdate = 0; let frameCount = 0; function updateFPS(timestamp) { frameCount++; if (timestamp - lastFpsUpdate >= 1000) { const fps = Math.round(frameCount * 1000 / (timestamp - lastFpsUpdate)); console.log('FPS:', fps); frameCount = 0; lastFpsUpdate = timestamp; } requestAnimationFrame(updateFPS); } // 게임 시작 window.addEventListener('load', async () => { try { await init(); console.log('Game started'); console.log('Active enemies:', enemies.length); gameLoop(performance.now()); updateFPS(performance.now()); } catch (error) { console.error('Game initialization error:', error); document.getElementById('loading').innerHTML = `
Error loading game. Please check console and file paths.
`; } }); // 디버깅을 위한 전역 접근 window.debugGame = { scene, camera, enemies, gunSound, reloadEnemies: createEnemies };