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 = 100; const ENEMY_GROUND_HEIGHT = 5; const ENEMY_SCALE = 3; const MAX_HEALTH = 1000; const ENEMY_MOVE_SPEED = 0.1; const ENEMY_MODELS = [ '/models/enemy1.glb', '/models/enemy12.glb', '/models/enemy13.glb', '/models/enemy14.glb' ]; 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(); // 사운드 시스템 class SoundSystem { constructor() { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); this.buffers = new Map(); this.pools = new Map(); } async loadSound(name, url) { try { const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer); this.buffers.set(name, audioBuffer); this.createPool(name, audioBuffer); console.log(`Sound loaded: ${name}`); } catch (error) { console.error(`Error loading sound ${name}:`, error); } } createPool(name, buffer, size = 10) { const pool = []; for (let i = 0; i < size; i++) { pool.push({ source: null, gainNode: this.audioContext.createGain(), lastPlayed: 0 }); } this.pools.set(name, pool); } play(name) { const pool = this.pools.get(name); const buffer = this.buffers.get(name); if (!pool || !buffer) { console.warn(`Sound not found: ${name}`); return; } const now = this.audioContext.currentTime; const poolItem = pool.find(item => !item.source || item.lastPlayed + buffer.duration <= now); if (poolItem) { if (poolItem.source) { poolItem.source.disconnect(); } poolItem.source = this.audioContext.createBufferSource(); poolItem.source.buffer = buffer; poolItem.source.connect(poolItem.gainNode); poolItem.gainNode.connect(this.audioContext.destination); poolItem.source.start(0); poolItem.lastPlayed = now; } } } // 사운드 시스템 초기화 const soundSystem = new SoundSystem(); // 이동 상태 const moveState = { forward: false, backward: false, left: false, right: false }; async function initSounds() { try { await soundSystem.loadSound('gunshot', 'gun.wav'); await soundSystem.loadSound('explosion', 'explosion.wav'); console.log('All sounds loaded successfully'); } catch (error) { console.error('Error initializing sounds:', error); } } async function init() { console.log('Initializing game...'); // Scene 초기화 scene = new THREE.Scene(); scene.background = new THREE.Color(0x87ceeb); scene.fog = new THREE.Fog(0x87ceeb, 0, 1500); // Camera 설정 camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000); camera.position.set(0, HELICOPTER_HEIGHT, 0); // Renderer 설정 renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.body.appendChild(renderer.domElement); // 조명 설정 const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const dirLight = new THREE.DirectionalLight(0xffffff, 1); dirLight.position.set(100, 100, 50); dirLight.castShadow = true; dirLight.shadow.mapSize.width = 2048; dirLight.shadow.mapSize.height = 2048; scene.add(dirLight); // Controls 설정 controls = new PointerLockControls(camera, document.body); // 이벤트 리스너 document.addEventListener('click', onClick); document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); window.addEventListener('resize', onWindowResize); try { await initSounds(); await createTerrain(); await loadEnemies(); await testModelLoading(); document.getElementById('loading').style.display = 'none'; console.log('Game initialized successfully'); console.log('Active enemies:', enemies.length); } catch (error) { console.error('Initialization error:', error); } } async function testModelLoading() { const loader = new GLTFLoader(); const testPath = ENEMY_MODELS[0]; try { const gltf = await new Promise((resolve, reject) => { loader.load(testPath, resolve, undefined, reject); }); console.log('Test model loaded successfully:', gltf); } catch (error) { console.error('Test model loading failed:', error); console.error('Test path was:', testPath); } } function createTerrain() { return new Promise((resolve) => { const geometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 200, 200); 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 < 100; 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 loadEnemies() { console.log('Starting enemy loading...'); const loader = new GLTFLoader(); const enemyCount = 3 + currentStage; const loadPromises = []; 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 ); console.log(`Creating enemy ${i} at position:`, position); // 임시 적 생성 const tempGeometry = new THREE.BoxGeometry(5, 5, 5); const tempMaterial = new THREE.MeshPhongMaterial({ color: 0xff0000, transparent: true, opacity: 0.8 }); const tempEnemy = new THREE.Mesh(tempGeometry, tempMaterial); tempEnemy.position.copy(position); tempEnemy.castShadow = true; tempEnemy.receiveShadow = true; scene.add(tempEnemy); const enemy = { model: tempEnemy, health: 100, speed: ENEMY_MOVE_SPEED, lastAttackTime: 0, position: position.clone() }; enemies.push(enemy); // GLB 모델 로딩 const modelPath = ENEMY_MODELS[i % ENEMY_MODELS.length]; const loadPromise = new Promise((resolve, reject) => { loader.load( modelPath, (gltf) => { console.log(`Successfully loaded model: ${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); scene.add(model); enemy.model = model; resolve(); }, (xhr) => { console.log(`${modelPath}: ${(xhr.loaded / xhr.total * 100)}% loaded`); }, (error) => { console.error(`Error loading model ${modelPath}:`, error); reject(error); } ); }); loadPromises.push(loadPromise); } try { await Promise.all(loadPromises); console.log('All enemies loaded successfully'); } catch (error) { console.error('Error loading enemies:', error); } } function createExplosion(position) { const particleCount = 30; const particles = []; for (let i = 0; i < particleCount; 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); } function animateExplosion() { particles.forEach((particle, i) => { particle.position.add(particle.velocity); particle.material.opacity -= 0.02; if (particle.material.opacity <= 0) { scene.remove(particle); particles.splice(i, 1); } }); if (particles.length > 0) { requestAnimationFrame(animateExplosion); } } animateExplosion(); soundSystem.play('explosion'); } // 이벤트 핸들러 function onClick() { if (!controls.isLocked) { controls.lock(); // 사운드 컨텍스트 시작 (많은 브라우저에서 필요) soundSystem.audioContext.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); } function shoot() { if (ammo <= 0) return; ammo--; updateAmmoDisplay(); const bullet = createBullet(); bullets.push(bullet); soundSystem.play('gunshot'); } function createBullet() { const bulletGeometry = new THREE.SphereGeometry(0.5); const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 1 }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); bullet.position.copy(camera.position); const direction = new THREE.Vector3(); camera.getWorldDirection(direction); bullet.velocity = direction.multiplyScalar(3); scene.add(bullet); return bullet; } function createEnemyBullet(enemy) { const bulletGeometry = new THREE.SphereGeometry(0.5); const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 1 }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); bullet.position.copy(enemy.model.position); bullet.position.y += 2; // 총구 높이 조정 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 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() { const altitude = Math.round(camera.position.y); document.querySelector('#altitude-indicator span').textContent = altitude; 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 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; } } } function updateBullets() { for (let i = bullets.length - 1; i >= 0; i--) { if (!bullets[i]) continue; bullets[i].position.add(bullets[i].velocity); enemies.forEach(enemy => { if (!enemy || !enemy.model) return; if (bullets[i] && bullets[i].position.distanceTo(enemy.model.position) < 5) { 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()); createExplosion(enemy.model.position.clone().add(new THREE.Vector3(2, 0, 2))); createExplosion(enemy.model.position.clone().add(new THREE.Vector3(-2, 0, -2))); scene.remove(enemy.model); enemies = enemies.filter(e => e !== enemy); console.log('Enemy destroyed, remaining enemies:', enemies.length); } } }); 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(); 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) { console.warn('Invalid enemy detected'); return; } // 적의 현재 위치에서 플레이어 방향으로 향하는 벡터 계산 const direction = new THREE.Vector3(); direction.subVectors(camera.position, enemy.model.position); direction.y = 0; // y축 이동 제한 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) { console.log('Enemy attacking!'); enemyBullets.push(createEnemyBullet(enemy)); enemy.lastAttackTime = currentTime; } }); } 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'; loadEnemies(); }, 2000); } } function gameOver(won) { isGameOver = true; controls.unlock(); soundSystem.audioContext.suspend(); alert(won ? 'Mission Complete!' : 'Game Over!'); location.reload(); } function gameLoop() { requestAnimationFrame(gameLoop); const time = performance.now(); const delta = (time - lastTime) / 1000; lastTime = time; if (controls.isLocked && !isGameOver) { updateMovement(); updateBullets(); updateEnemies(); updateEnemyBullets(); updateHelicopterHUD(); checkGameStatus(); } renderer.render(scene, camera); } // 게임 시작 window.addEventListener('load', async () => { try { await init(); console.log('Game started'); console.log('Active enemies:', enemies.length); gameLoop(); } catch (error) { console.error('Game initialization error:', error); alert('Error loading game. Please check console for details.'); } }); // 디버깅을 위한 전역 접근 window.debugGame = { scene, camera, enemies, soundSystem, reloadEnemies: loadEnemies };