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; // 사운드 풀 클래스 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; const playPromise = sound.play(); if (playPromise !== undefined) { playPromise.catch(error => { console.error("Sound play error:", error); 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), explosion: new SoundPool('explosion.wav', 10) // 폭발 사운드 추가 }; sounds.bgm.loop = true; // 이동 상태 const moveState = { forward: false, backward: false, left: false, right: false }; function init() { console.log('Game initialized'); console.log('Available enemy models:', ENEMY_MODELS); scene = new THREE.Scene(); scene.background = new THREE.Color(0x87ceeb); scene.fog = new THREE.Fog(0x87ceeb, 0, 1500); camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000); camera.position.set(0, HELICOPTER_HEIGHT, 0); 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 = new PointerLockControls(camera, document.body); 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 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); } // 폭발 애니메이션 const 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(); sounds.explosion.play(); } 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, ENEMY_GROUND_HEIGHT, Math.sin(angle) * radius ); const tempEnemy = createTemporaryEnemy(position); scene.add(tempEnemy); const modelPath = ENEMY_MODELS[i % ENEMY_MODELS.length]; console.log('Loading enemy model:', modelPath); loader.load( modelPath, (gltf) => { console.log('Successfully loaded enemy model:', modelPath); const enemy = gltf.scene; enemy.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE); enemy.position.copy(position); enemy.rotation.y = Math.PI; 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; enemies[index].muzzleFlash = createMuzzleFlash(); enemy.add(enemies[index].muzzleFlash); } }, (xhr) => { console.log(`${modelPath}: ${(xhr.loaded / xhr.total * 100)}% loaded`); }, (error) => { console.error('Error loading enemy model:', modelPath, error); } ); enemies.push({ model: tempEnemy, health: 100, speed: ENEMY_MOVE_SPEED, lastAttackTime: 0 }); } } // 나머지 함수들은 이전과 동일하게 유지되며, updateEnemies와 updateBullets 함수만 수정됩니다. function updateEnemies() { const currentTime = Date.now(); enemies.forEach(enemy => { 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) { if (enemy.muzzleFlash) { enemy.muzzleFlash.material.opacity = 1; enemy.muzzleFlash.visible = true; setTimeout(() => { enemy.muzzleFlash.visible = false; enemy.muzzleFlash.material.opacity = 0; }, 100); } enemyBullets.push(createEnemyBullet(enemy)); enemy.lastAttackTime = currentTime; } }); } 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; // 피격 효과 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); } } }); if (bullets[i] && bullets[i].position.distanceTo(camera.position) > 1000) { scene.remove(bullets[i]); bullets.splice(i, 1); } } } // 게임 시작 init(); gameLoop();