Spaces:
Running
Running
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 = ` | |
<div class="loading-text" style="color: #ff0000;"> | |
Error loading models. Please check console and file paths. | |
</div> | |
`; | |
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 = ` | |
<div class="loading-text" style="color: #ff0000;"> | |
Error loading game. Please check console and file paths. | |
</div> | |
`; | |
} | |
}); | |
// ๋๋ฒ๊น ์ ์ํ ์ ์ญ ์ ๊ทผ | |
window.debugGame = { | |
scene, | |
camera, | |
enemies, | |
gunSound, | |
reloadEnemies: createEnemies | |
}; |