|
|
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 = new THREE.Scene(); |
|
|
scene.background = new THREE.Color(0x87ceeb); |
|
|
scene.fog = new THREE.Fog(0x87ceeb, 0, 1000); |
|
|
|
|
|
|
|
|
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 = 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 = 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); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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 |
|
|
}; |