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 = 50; const ENEMY_SCALE = 3; const MAX_HEALTH = 1000; const ENEMY_MODELS = [ './models/enemy1.glb', './models/enemy2.glb', './models/enemy3.glb', './models/enemy4.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; // 사운드 풀 클래스 class SoundPool { constructor(soundUrl, poolSize = 10) { this.sounds = []; this.currentIndex = 0; this.poolSize = poolSize; for (let i = 0; i < poolSize; i++) { const sound = new Audio(soundUrl); sound.preload = 'auto'; this.sounds.push(sound); } } play() { const sound = this.sounds[this.currentIndex]; // 현재 재생 중인 사운드 초기화 sound.pause(); sound.currentTime = 0; // 새로운 Promise로 사운드 재생 const playPromise = sound.play(); if (playPromise !== undefined) { playPromise.catch(error => { console.error("Sound play error:", error); // 오류 발생 시 새로운 Audio 객체로 교체 this.sounds[this.currentIndex] = new Audio(sound.src); }); } this.currentIndex = (this.currentIndex + 1) % this.poolSize; } } // 사운드 초기화 const sounds = { bgm: new Audio('Music.wav'), gunshot: new SoundPool('gun.wav', 20) }; sounds.bgm.loop = true; // 이동 상태 const moveState = { forward: false, backward: false, left: false, right: false }; function init() { // 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 }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; document.body.appendChild(renderer.domElement); // 조명 설정 const ambientLight = new THREE.AmbientLight(0xffffff, 1.0); scene.add(ambientLight); const dirLight = new THREE.DirectionalLight(0xffffff, 1.0); dirLight.position.set(100, 100, 50); dirLight.castShadow = true; scene.add(dirLight); // Controls 설정 controls = new PointerLockControls(camera, document.body); // 디버그 헬퍼 추가 const axesHelper = new THREE.AxesHelper(5); scene.add(axesHelper); const gridHelper = new THREE.GridHelper(1000, 100); scene.add(gridHelper); // 이벤트 리스너 설정 document.addEventListener('click', onClick); document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); window.addEventListener('resize', onWindowResize); // 지형 생성 createTerrain(); console.log('Starting to load enemies...'); loadEnemies(); } function createTerrain() { 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(); } function createTemporaryEnemy(position) { const geometry = new THREE.BoxGeometry(5, 5, 5); const material = new THREE.MeshPhongMaterial({ color: 0xff0000 }); const cube = new THREE.Mesh(geometry, material); cube.position.copy(position); return cube; } function loadEnemies() { const loader = new GLTFLoader(); const enemyCount = 3 + currentStage; 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, 10, Math.sin(angle) * radius ); // 임시 적 생성 const tempEnemy = createTemporaryEnemy(position); scene.add(tempEnemy); enemies.push({ model: tempEnemy, health: 100, speed: 0.3 + (currentStage * 0.1), lastAttackTime: 0 }); // GLB 모델 로드 시도 const modelPath = ENEMY_MODELS[i % ENEMY_MODELS.length]; loader.load(modelPath, (gltf) => { console.log('Enemy model loaded:', modelPath); const enemy = gltf.scene; enemy.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE); enemy.position.copy(position); enemy.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(enemy); const index = enemies.findIndex(e => e.model === tempEnemy); if (index !== -1) { enemies[index].model = enemy; } }, (xhr) => { console.log((xhr.loaded / xhr.total * 100) + '% loaded'); }, (error) => { console.error('Error loading enemy model:', error); } ); } } 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); } } function onClick() { if (!controls.isLocked) { controls.lock(); sounds.bgm.play(); } 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); sounds.gunshot.play(); } function createBullet() { const bulletGeometry = new THREE.SphereGeometry(0.5); const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); 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 }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); bullet.position.copy(enemy.model.position); 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 => { const relativePos = enemy.model.position.clone().sub(camera.position); const distance = relativePos.length(); if (distance < 500) { const angle = Math.atan2(relativePos.x, relativePos.z); const normalizedDistance = distance / 500; const dot = document.createElement('div'); dot.className = 'radar-dot'; dot.style.left = `${50 + Math.sin(angle) * normalizedDistance * 45}%`; dot.style.top = `${50 + Math.cos(angle) * 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); } } function updateBullets() { for (let i = bullets.length - 1; i >= 0; i--) { bullets[i].position.add(bullets[i].velocity); enemies.forEach(enemy => { if (bullets[i].position.distanceTo(enemy.model.position) < 5) { scene.remove(bullets[i]); bullets.splice(i, 1); enemy.health -= 25; if (enemy.health <= 0) { scene.remove(enemy.model); enemies = enemies.filter(e => e !== enemy); } } }); 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--) { 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 => { const direction = new THREE.Vector3(); direction.subVectors(camera.position, enemy.model.position); direction.normalize(); enemy.model.position.add(direction.multiplyScalar(enemy.speed)); enemy.model.lookAt(camera.position); 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; } if (distanceToPlayer < 10) { gameOver(false); } }); } 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(); sounds.bgm.pause(); alert(won ? 'Mission Complete!' : 'Game Over!'); location.reload(); } let lastTime = performance.now(); function 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); requestAnimationFrame(gameLoop); } // 게임 시작 init(); gameLoop();