|
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 = 1000; |
|
const TANK_HEIGHT = 0.5; |
|
const ENEMY_GROUND_HEIGHT = 0; |
|
const ENEMY_SCALE = 10; |
|
const MAX_HEALTH = 1000; |
|
const ENEMY_MOVE_SPEED = 0.1; |
|
const ENEMY_COUNT_MAX = 3; |
|
const PARTICLE_COUNT = 15; |
|
const BUILDING_COUNT = 30; |
|
const ENEMY_CONFIG = { |
|
ATTACK_RANGE: 100, |
|
ATTACK_INTERVAL: 2000, |
|
BULLET_SPEED: 2 |
|
}; |
|
|
|
|
|
class TankPlayer { |
|
constructor() { |
|
this.body = null; |
|
this.turret = null; |
|
this.position = new THREE.Vector3(0, 0, 0); |
|
this.rotation = new THREE.Euler(0, 0, 0); |
|
this.turretRotation = 0; |
|
this.moveSpeed = 0.5; |
|
this.turnSpeed = 0.03; |
|
this.turretGroup = new THREE.Group(); |
|
this.health = MAX_HEALTH; |
|
this.isLoaded = false; |
|
this.ammo = 10; |
|
this.lastShootTime = 0; |
|
this.shootInterval = 1000; |
|
this.bullets = []; |
|
} |
|
|
|
async initialize(scene, loader) { |
|
try { |
|
const bodyResult = await loader.loadAsync('/models/abramsBody.glb'); |
|
this.body = bodyResult.scene; |
|
this.body.position.copy(this.position); |
|
|
|
const turretResult = await loader.loadAsync('/models/abramsTurret.glb'); |
|
this.turret = turretResult.scene; |
|
|
|
this.turretGroup.position.y = 0.2; |
|
this.turretGroup.add(this.turret); |
|
this.body.add(this.turretGroup); |
|
|
|
this.body.traverse((child) => { |
|
if (child.isMesh) { |
|
child.castShadow = true; |
|
child.receiveShadow = true; |
|
} |
|
}); |
|
|
|
this.turret.traverse((child) => { |
|
if (child.isMesh) { |
|
child.castShadow = true; |
|
child.receiveShadow = true; |
|
} |
|
}); |
|
|
|
scene.add(this.body); |
|
this.isLoaded = true; |
|
|
|
} catch (error) { |
|
console.error('Error loading tank models:', error); |
|
this.isLoaded = false; |
|
} |
|
} |
|
|
|
shoot(scene) { |
|
const currentTime = Date.now(); |
|
if (currentTime - this.lastShootTime < this.shootInterval || this.ammo <= 0) return null; |
|
|
|
const bulletGeometry = new THREE.SphereGeometry(0.2); |
|
const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); |
|
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); |
|
|
|
const bulletOffset = new THREE.Vector3(0, 0.5, 2); |
|
bulletOffset.applyQuaternion(this.turretGroup.quaternion); |
|
bulletOffset.applyQuaternion(this.body.quaternion); |
|
bullet.position.copy(this.body.position).add(bulletOffset); |
|
|
|
const direction = new THREE.Vector3(0, 0, 1); |
|
direction.applyQuaternion(this.turretGroup.quaternion); |
|
direction.applyQuaternion(this.body.quaternion); |
|
bullet.velocity = direction.multiplyScalar(2); |
|
|
|
scene.add(bullet); |
|
this.bullets.push(bullet); |
|
this.ammo--; |
|
this.lastShootTime = currentTime; |
|
|
|
document.getElementById('ammo').textContent = `Ammo: ${this.ammo}/10`; |
|
|
|
return bullet; |
|
} |
|
|
|
update(mouseX, mouseY) { |
|
if (!this.body || !this.turretGroup) return; |
|
|
|
for (let i = this.bullets.length - 1; i >= 0; i--) { |
|
const bullet = this.bullets[i]; |
|
bullet.position.add(bullet.velocity); |
|
|
|
if (Math.abs(bullet.position.x) > MAP_SIZE/2 || |
|
Math.abs(bullet.position.z) > MAP_SIZE/2) { |
|
scene.remove(bullet); |
|
this.bullets.splice(i, 1); |
|
} |
|
} |
|
|
|
|
|
if (this.turretGroup) { |
|
|
|
const rotationAngle = -Math.atan2(mouseX, mouseY); |
|
this.turretRotation = rotationAngle; |
|
this.turretGroup.rotation.y = this.turretRotation; |
|
} |
|
} |
|
|
|
move(direction) { |
|
if (!this.body) return; |
|
|
|
const moveVector = new THREE.Vector3(); |
|
moveVector.x = direction.x * this.moveSpeed; |
|
moveVector.z = direction.z * this.moveSpeed; |
|
|
|
this.body.position.add(moveVector); |
|
} |
|
|
|
rotate(angle) { |
|
if (!this.body) return; |
|
this.body.rotation.y += angle * this.turnSpeed; |
|
} |
|
|
|
getPosition() { |
|
return this.body ? this.body.position : new THREE.Vector3(); |
|
} |
|
|
|
takeDamage(damage) { |
|
this.health -= damage; |
|
return this.health <= 0; |
|
} |
|
} |
|
|
|
|
|
class Enemy { |
|
constructor(scene, position, type = 'tank') { |
|
this.scene = scene; |
|
this.position = position; |
|
this.mesh = null; |
|
this.type = type; |
|
this.health = type === 'tank' ? 100 : 200; |
|
this.lastAttackTime = 0; |
|
this.bullets = []; |
|
this.isLoaded = false; |
|
this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7; |
|
} |
|
|
|
async initialize(loader) { |
|
try { |
|
const modelPath = this.type === 'tank' ? '/models/enemy1.glb' : '/models/enemy4.glb'; |
|
const result = await loader.loadAsync(modelPath); |
|
this.mesh = result.scene; |
|
this.mesh.position.copy(this.position); |
|
this.mesh.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE); |
|
|
|
this.mesh.traverse((child) => { |
|
if (child.isMesh) { |
|
child.castShadow = true; |
|
child.receiveShadow = true; |
|
} |
|
}); |
|
|
|
this.scene.add(this.mesh); |
|
this.isLoaded = true; |
|
} catch (error) { |
|
console.error('Error loading enemy model:', error); |
|
this.isLoaded = false; |
|
} |
|
} |
|
|
|
update(playerPosition) { |
|
if (!this.mesh || !this.isLoaded) return; |
|
|
|
const direction = new THREE.Vector3() |
|
.subVectors(playerPosition, this.mesh.position) |
|
.normalize(); |
|
|
|
this.mesh.lookAt(playerPosition); |
|
this.mesh.position.add(direction.multiplyScalar(this.moveSpeed)); |
|
|
|
for (let i = this.bullets.length - 1; i >= 0; i--) { |
|
const bullet = this.bullets[i]; |
|
bullet.position.add(bullet.velocity); |
|
|
|
if (Math.abs(bullet.position.x) > MAP_SIZE || |
|
Math.abs(bullet.position.z) > MAP_SIZE) { |
|
this.scene.remove(bullet); |
|
this.bullets.splice(i, 1); |
|
} |
|
} |
|
} |
|
|
|
shoot(playerPosition) { |
|
const currentTime = Date.now(); |
|
const attackInterval = this.type === 'tank' ? |
|
ENEMY_CONFIG.ATTACK_INTERVAL : |
|
ENEMY_CONFIG.ATTACK_INTERVAL * 1.5; |
|
|
|
if (currentTime - this.lastAttackTime < attackInterval) return; |
|
|
|
const bulletGeometry = new THREE.SphereGeometry(this.type === 'tank' ? 0.2 : 0.3); |
|
const bulletMaterial = new THREE.MeshBasicMaterial({ |
|
color: this.type === 'tank' ? 0xff0000 : 0xff6600 |
|
}); |
|
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); |
|
|
|
bullet.position.copy(this.mesh.position); |
|
|
|
const direction = new THREE.Vector3() |
|
.subVectors(playerPosition, this.mesh.position) |
|
.normalize(); |
|
|
|
const bulletSpeed = this.type === 'tank' ? |
|
ENEMY_CONFIG.BULLET_SPEED : |
|
ENEMY_CONFIG.BULLET_SPEED * 0.8; |
|
|
|
bullet.velocity = direction.multiplyScalar(bulletSpeed); |
|
|
|
this.scene.add(bullet); |
|
this.bullets.push(bullet); |
|
this.lastAttackTime = currentTime; |
|
} |
|
|
|
takeDamage(damage) { |
|
this.health -= damage; |
|
return this.health <= 0; |
|
} |
|
|
|
destroy() { |
|
if (this.mesh) { |
|
this.scene.remove(this.mesh); |
|
this.bullets.forEach(bullet => this.scene.remove(bullet)); |
|
this.bullets = []; |
|
this.isLoaded = false; |
|
} |
|
} |
|
} |
|
|
|
|
|
class Particle { |
|
constructor(scene, position) { |
|
const geometry = new THREE.SphereGeometry(0.1); |
|
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); |
|
this.mesh = new THREE.Mesh(geometry, material); |
|
this.mesh.position.copy(position); |
|
|
|
this.velocity = new THREE.Vector3( |
|
(Math.random() - 0.5) * 0.3, |
|
Math.random() * 0.2, |
|
(Math.random() - 0.5) * 0.3 |
|
); |
|
|
|
this.gravity = -0.01; |
|
this.lifetime = 60; |
|
this.age = 0; |
|
|
|
scene.add(this.mesh); |
|
} |
|
|
|
update() { |
|
this.velocity.y += this.gravity; |
|
this.mesh.position.add(this.velocity); |
|
this.age++; |
|
return this.age < this.lifetime; |
|
} |
|
|
|
destroy(scene) { |
|
scene.remove(this.mesh); |
|
} |
|
} |
|
|
|
|
|
class Game { |
|
constructor() { |
|
this.scene = new THREE.Scene(); |
|
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
|
this.renderer = new THREE.WebGLRenderer({ antialias: true }); |
|
this.renderer.setSize(window.innerWidth, window.innerHeight); |
|
this.renderer.shadowMap.enabled = true; |
|
document.getElementById('gameContainer').appendChild(this.renderer.domElement); |
|
|
|
this.tank = new TankPlayer(); |
|
this.enemies = []; |
|
this.particles = []; |
|
this.buildings = []; |
|
this.loader = new GLTFLoader(); |
|
this.controls = null; |
|
this.gameTime = GAME_DURATION; |
|
this.score = 0; |
|
this.isGameOver = false; |
|
this.isLoading = true; |
|
this.previousTankPosition = new THREE.Vector3(); |
|
this.lastTime = performance.now(); |
|
this.gameTimer = null; |
|
this.animationFrameId = null; |
|
|
|
this.mouse = { x: 0, y: 0 }; |
|
this.keys = { |
|
forward: false, |
|
backward: false, |
|
left: false, |
|
right: false |
|
}; |
|
|
|
this.setupEventListeners(); |
|
this.initialize(); |
|
} |
|
|
|
async initialize() { |
|
try { |
|
|
|
this.scene.fog = new THREE.Fog(0xC2B280, 1, 150); |
|
this.scene.background = new THREE.Color(0x87CEEB); |
|
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); |
|
this.scene.add(ambientLight); |
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); |
|
directionalLight.position.set(50, 50, 50); |
|
directionalLight.castShadow = true; |
|
directionalLight.shadow.mapSize.width = 2048; |
|
directionalLight.shadow.mapSize.height = 2048; |
|
this.scene.add(directionalLight); |
|
|
|
|
|
const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 200, 200); |
|
const groundTexture = new THREE.TextureLoader().load('/textures/sand.jpg'); |
|
groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping; |
|
groundTexture.repeat.set(50, 50); |
|
|
|
|
|
const groundMaterial = new THREE.MeshStandardMaterial({ |
|
map: groundTexture, |
|
color: 0xD2B48C, |
|
roughness: 1.0, |
|
metalness: 0.0, |
|
bumpScale: 0.5 |
|
}); |
|
|
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial); |
|
ground.rotation.x = -Math.PI / 2; |
|
ground.receiveShadow = true; |
|
|
|
|
|
const vertices = ground.geometry.attributes.position.array; |
|
let seed = Math.random() * 100; |
|
for (let i = 0; i < vertices.length; i += 3) { |
|
|
|
const x = vertices[i] / 100; |
|
const y = vertices[i + 1] / 100; |
|
vertices[i + 2] = |
|
(Math.sin(x + seed) * Math.cos(y + seed) * 2.0) + |
|
(Math.sin(x * 2 + seed) * Math.cos(y * 2 + seed) * 1.0) + |
|
(Math.sin(x * 4 + seed) * Math.cos(y * 4 + seed) * 0.5); |
|
} |
|
ground.geometry.attributes.position.needsUpdate = true; |
|
ground.geometry.computeVertexNormals(); |
|
|
|
this.scene.add(ground); |
|
|
|
|
|
await this.addDesertDecorations(); |
|
await this.tank.initialize(this.scene, this.loader); |
|
|
|
if (!this.tank.isLoaded) { |
|
throw new Error('Tank loading failed'); |
|
} |
|
|
|
const tankPosition = this.tank.getPosition(); |
|
this.camera.position.set( |
|
tankPosition.x, |
|
tankPosition.y + 15, |
|
tankPosition.z - 30 |
|
); |
|
this.camera.lookAt(tankPosition); |
|
|
|
this.isLoading = false; |
|
document.getElementById('loading').style.display = 'none'; |
|
|
|
this.animate(); |
|
this.spawnEnemies(); |
|
this.startGameTimer(); |
|
|
|
} catch (error) { |
|
console.error('Game initialization error:', error); |
|
this.handleLoadingError(); |
|
} |
|
} |
|
|
|
|
|
async addDesertDecorations() { |
|
|
|
const rockGeometries = [ |
|
new THREE.DodecahedronGeometry(3), |
|
new THREE.DodecahedronGeometry(2), |
|
new THREE.DodecahedronGeometry(4) |
|
]; |
|
|
|
const rockMaterial = new THREE.MeshStandardMaterial({ |
|
color: 0x8B4513, |
|
roughness: 0.9, |
|
metalness: 0.1 |
|
}); |
|
|
|
for (let i = 0; i < 100; i++) { |
|
const rockGeometry = rockGeometries[Math.floor(Math.random() * rockGeometries.length)]; |
|
const rock = new THREE.Mesh(rockGeometry, rockMaterial); |
|
|
|
rock.position.set( |
|
(Math.random() - 0.5) * MAP_SIZE * 0.9, |
|
Math.random() * 2, |
|
(Math.random() - 0.5) * MAP_SIZE * 0.9 |
|
); |
|
|
|
rock.rotation.set( |
|
Math.random() * Math.PI, |
|
Math.random() * Math.PI, |
|
Math.random() * Math.PI |
|
); |
|
|
|
rock.scale.set( |
|
1 + Math.random() * 0.5, |
|
1 + Math.random() * 0.5, |
|
1 + Math.random() * 0.5 |
|
); |
|
|
|
rock.castShadow = true; |
|
rock.receiveShadow = true; |
|
this.scene.add(rock); |
|
} |
|
|
|
|
|
const cactusGeometry = new THREE.CylinderGeometry(0.5, 0.7, 4, 8); |
|
const cactusMaterial = new THREE.MeshStandardMaterial({ |
|
color: 0x2F4F2F, |
|
roughness: 0.8 |
|
}); |
|
|
|
for (let i = 0; i < 50; i++) { |
|
const cactus = new THREE.Mesh(cactusGeometry, cactusMaterial); |
|
cactus.position.set( |
|
(Math.random() - 0.5) * MAP_SIZE * 0.8, |
|
2, |
|
(Math.random() - 0.5) * MAP_SIZE * 0.8 |
|
); |
|
cactus.castShadow = true; |
|
cactus.receiveShadow = true; |
|
this.scene.add(cactus); |
|
} |
|
} |
|
|
|
setupEventListeners() { |
|
document.addEventListener('keydown', (event) => { |
|
if (this.isLoading || this.isGameOver) return; |
|
switch(event.code) { |
|
case 'KeyW': this.keys.forward = true; break; |
|
case 'KeyS': this.keys.backward = true; break; |
|
case 'KeyA': this.keys.left = true; break; |
|
case 'KeyD': this.keys.right = true; break; |
|
} |
|
}); |
|
|
|
document.addEventListener('keyup', (event) => { |
|
if (this.isLoading || this.isGameOver) return; |
|
switch(event.code) { |
|
case 'KeyW': this.keys.forward = false; break; |
|
case 'KeyS': this.keys.backward = false; break; |
|
case 'KeyA': this.keys.left = false; break; |
|
case 'KeyD': this.keys.right = false; break; |
|
} |
|
}); |
|
|
|
document.addEventListener('mousemove', (event) => { |
|
if (this.isLoading || this.isGameOver || !document.pointerLockElement) return; |
|
this.mouse.x += event.movementX * 0.002; |
|
this.mouse.y += event.movementY * 0.002; |
|
}); |
|
|
|
document.addEventListener('click', () => { |
|
if (!document.pointerLockElement) { |
|
document.body.requestPointerLock(); |
|
} else if (!this.isGameOver) { |
|
const bullet = this.tank.shoot(this.scene); |
|
if (bullet) { |
|
|
|
} |
|
} |
|
}); |
|
|
|
document.addEventListener('pointerlockchange', () => { |
|
if (!document.pointerLockElement) { |
|
this.mouse.x = 0; |
|
this.mouse.y = 0; |
|
} |
|
}); |
|
|
|
window.addEventListener('resize', () => { |
|
this.camera.aspect = window.innerWidth / window.innerHeight; |
|
this.camera.updateProjectionMatrix(); |
|
this.renderer.setSize(window.innerWidth, window.innerHeight); |
|
}); |
|
} |
|
|
|
handleMovement() { |
|
if (!this.tank.isLoaded || this.isGameOver) return; |
|
|
|
const direction = new THREE.Vector3(); |
|
|
|
if (this.keys.forward) direction.z += 1; |
|
if (this.keys.backward) direction.z -= 1; |
|
if (this.keys.left) direction.x -= 1; |
|
if (this.keys.right) direction.x += 1; |
|
|
|
if (direction.length() > 0) { |
|
direction.normalize(); |
|
|
|
if (this.keys.left) this.tank.rotate(-1); |
|
if (this.keys.right) this.tank.rotate(1); |
|
|
|
direction.applyEuler(this.tank.body.rotation); |
|
this.tank.move(direction); |
|
} |
|
|
|
|
|
const mouseVector = new THREE.Vector2(this.mouse.x, -this.mouse.y); |
|
const rotationAngle = -Math.atan2(mouseVector.x, mouseVector.y); |
|
|
|
if (this.tank.turretGroup) { |
|
this.tank.turretGroup.rotation.y = rotationAngle; |
|
} |
|
|
|
|
|
const tankPos = this.tank.getPosition(); |
|
const cameraDistance = 30; |
|
const cameraHeight = 15; |
|
const lookAtHeight = 5; |
|
|
|
const tankRotation = this.tank.body.rotation.y; |
|
|
|
this.camera.position.set( |
|
tankPos.x - Math.sin(tankRotation) * cameraDistance, |
|
tankPos.y + cameraHeight, |
|
tankPos.z - Math.cos(tankRotation) * cameraDistance |
|
); |
|
|
|
const lookAtPoint = new THREE.Vector3( |
|
tankPos.x + Math.sin(tankRotation) * 10, |
|
tankPos.y + lookAtHeight, |
|
tankPos.z + Math.cos(tankRotation) * 10 |
|
); |
|
|
|
this.camera.lookAt(lookAtPoint); |
|
} |
|
|
|
createBuildings() { |
|
const buildingTypes = [ |
|
{ width: 10, height: 30, depth: 10, color: 0x808080 }, |
|
{ width: 15, height: 40, depth: 15, color: 0x606060 }, |
|
{ width: 20, height: 50, depth: 20, color: 0x404040 } |
|
]; |
|
|
|
for (let i = 0; i < BUILDING_COUNT; i++) { |
|
const type = buildingTypes[Math.floor(Math.random() * buildingTypes.length)]; |
|
const building = this.createBuilding(type); |
|
|
|
let position; |
|
let attempts = 0; |
|
do { |
|
position = new THREE.Vector3( |
|
(Math.random() - 0.5) * (MAP_SIZE - type.width), |
|
type.height / 2, |
|
(Math.random() - 0.5) * (MAP_SIZE - type.depth) |
|
); |
|
attempts++; |
|
} while (this.checkBuildingCollision(position, type) && attempts < 50); |
|
|
|
if (attempts < 50) { |
|
building.position.copy(position); |
|
this.buildings.push(building); |
|
this.scene.add(building); |
|
} |
|
} |
|
return Promise.resolve(); |
|
} |
|
|
|
createBuilding(type) { |
|
const geometry = new THREE.BoxGeometry(type.width, type.height, type.depth); |
|
const material = new THREE.MeshPhongMaterial({ |
|
color: type.color, |
|
emissive: 0x222222, |
|
specular: 0x111111, |
|
shininess: 30 |
|
}); |
|
const building = new THREE.Mesh(geometry, material); |
|
building.castShadow = true; |
|
building.receiveShadow = true; |
|
return building; |
|
} |
|
|
|
checkBuildingCollision(position, type) { |
|
const margin = 5; |
|
const bbox = new THREE.Box3( |
|
new THREE.Vector3( |
|
position.x - (type.width / 2 + margin), |
|
0, |
|
position.z - (type.depth / 2 + margin) |
|
), |
|
new THREE.Vector3( |
|
position.x + (type.width / 2 + margin), |
|
type.height, |
|
position.z + (type.depth / 2 + margin) |
|
) |
|
); |
|
|
|
return this.buildings.some(building => { |
|
const buildingBox = new THREE.Box3().setFromObject(building); |
|
return bbox.intersectsBox(buildingBox); |
|
}); |
|
} |
|
|
|
handleLoadingError() { |
|
this.isLoading = false; |
|
const loadingElement = document.getElementById('loading'); |
|
if (loadingElement) { |
|
loadingElement.innerHTML = ` |
|
<div class="loading-text" style="color: red;"> |
|
Loading failed. Please refresh the page. |
|
</div> |
|
`; |
|
} |
|
} |
|
|
|
startGameTimer() { |
|
if (this.gameTimer) { |
|
clearInterval(this.gameTimer); |
|
} |
|
|
|
this.gameTimer = setInterval(() => { |
|
if (this.isLoading || this.isGameOver) { |
|
clearInterval(this.gameTimer); |
|
return; |
|
} |
|
|
|
this.gameTime--; |
|
document.getElementById('time').textContent = `Time: ${this.gameTime}s`; |
|
|
|
if (this.gameTime <= 0) { |
|
clearInterval(this.gameTimer); |
|
this.endGame(); |
|
} |
|
}, 1000); |
|
} |
|
|
|
spawnEnemies() { |
|
const spawnEnemy = () => { |
|
if (this.enemies.length < 3 && !this.isGameOver) { |
|
const position = this.getValidEnemySpawnPosition(); |
|
if (position) { |
|
const type = Math.random() < 0.7 ? 'tank' : 'heavy'; |
|
const enemy = new Enemy(this.scene, position, type); |
|
enemy.initialize(this.loader); |
|
this.enemies.push(enemy); |
|
} |
|
} |
|
if (!this.isGameOver) { |
|
setTimeout(spawnEnemy, 10000); |
|
} |
|
}; |
|
|
|
spawnEnemy(); |
|
} |
|
|
|
|
|
getValidEnemySpawnPosition() { |
|
const margin = 20; |
|
let position; |
|
let attempts = 0; |
|
const maxAttempts = 50; |
|
|
|
do { |
|
position = new THREE.Vector3( |
|
(Math.random() - 0.5) * (MAP_SIZE - margin * 2), |
|
ENEMY_GROUND_HEIGHT, |
|
(Math.random() - 0.5) * (MAP_SIZE - margin * 2) |
|
); |
|
|
|
const distanceToPlayer = position.distanceTo(this.tank.getPosition()); |
|
if (distanceToPlayer < 100) continue; |
|
|
|
let collisionFound = false; |
|
for (const building of this.buildings) { |
|
const buildingBox = new THREE.Box3().setFromObject(building); |
|
if (buildingBox.containsPoint(position)) { |
|
collisionFound = true; |
|
break; |
|
} |
|
} |
|
|
|
if (!collisionFound) return position; |
|
|
|
attempts++; |
|
} while (attempts < maxAttempts); |
|
|
|
return null; |
|
} |
|
|
|
updateParticles() { |
|
for (let i = this.particles.length - 1; i >= 0; i--) { |
|
const particle = this.particles[i]; |
|
if (!particle.update()) { |
|
particle.destroy(this.scene); |
|
this.particles.splice(i, 1); |
|
} |
|
} |
|
} |
|
|
|
createExplosion(position) { |
|
for (let i = 0; i < PARTICLE_COUNT; i++) { |
|
this.particles.push(new Particle(this.scene, position)); |
|
} |
|
} |
|
|
|
checkCollisions() { |
|
if (this.isLoading || !this.tank.isLoaded) return; |
|
|
|
const tankPosition = this.tank.getPosition(); |
|
|
|
|
|
this.enemies.forEach(enemy => { |
|
if (!enemy.mesh || !enemy.isLoaded) return; |
|
|
|
enemy.bullets.forEach(bullet => { |
|
const distance = bullet.position.distanceTo(tankPosition); |
|
if (distance < 1) { |
|
if (this.tank.takeDamage(10)) { |
|
this.endGame(); |
|
} |
|
this.scene.remove(bullet); |
|
enemy.bullets = enemy.bullets.filter(b => b !== bullet); |
|
|
|
this.createExplosion(bullet.position); |
|
document.getElementById('health').style.width = |
|
`${(this.tank.health / MAX_HEALTH) * 100}%`; |
|
} |
|
}); |
|
}); |
|
|
|
|
|
this.tank.bullets.forEach((bullet, bulletIndex) => { |
|
this.enemies.forEach((enemy, enemyIndex) => { |
|
if (!enemy.mesh || !enemy.isLoaded) return; |
|
|
|
const distance = bullet.position.distanceTo(enemy.mesh.position); |
|
if (distance < 2) { |
|
if (enemy.takeDamage(50)) { |
|
enemy.destroy(); |
|
this.enemies.splice(enemyIndex, 1); |
|
this.score += 100; |
|
document.getElementById('score').textContent = `Score: ${this.score}`; |
|
} |
|
this.scene.remove(bullet); |
|
this.tank.bullets.splice(bulletIndex, 1); |
|
this.createExplosion(bullet.position); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body); |
|
for (const building of this.buildings) { |
|
const buildingBox = new THREE.Box3().setFromObject(building); |
|
if (tankBoundingBox.intersectsBox(buildingBox)) { |
|
this.tank.body.position.copy(this.previousTankPosition); |
|
break; |
|
} |
|
} |
|
|
|
|
|
this.previousTankPosition.copy(this.tank.body.position); |
|
} |
|
|
|
endGame() { |
|
if (this.isGameOver) return; |
|
|
|
this.isGameOver = true; |
|
|
|
if (this.gameTimer) { |
|
clearInterval(this.gameTimer); |
|
} |
|
|
|
if (this.animationFrameId) { |
|
cancelAnimationFrame(this.animationFrameId); |
|
} |
|
|
|
document.exitPointerLock(); |
|
|
|
const gameOverDiv = document.createElement('div'); |
|
gameOverDiv.style.position = 'absolute'; |
|
gameOverDiv.style.top = '50%'; |
|
gameOverDiv.style.left = '50%'; |
|
gameOverDiv.style.transform = 'translate(-50%, -50%)'; |
|
gameOverDiv.style.color = '#0f0'; |
|
gameOverDiv.style.fontSize = '48px'; |
|
gameOverDiv.style.backgroundColor = 'rgba(0, 20, 0, 0.7)'; |
|
gameOverDiv.style.padding = '20px'; |
|
gameOverDiv.style.borderRadius = '10px'; |
|
gameOverDiv.style.textAlign = 'center'; |
|
gameOverDiv.innerHTML = ` |
|
Game Over<br> |
|
Score: ${this.score}<br> |
|
Time Survived: ${GAME_DURATION - this.gameTime}s<br> |
|
<button onclick="location.reload()" |
|
style="font-size: 24px; padding: 10px; margin-top: 20px; |
|
cursor: pointer; background: #0f0; border: none; |
|
color: black; border-radius: 5px;"> |
|
Play Again |
|
</button> |
|
`; |
|
document.body.appendChild(gameOverDiv); |
|
} |
|
|
|
updateUI() { |
|
if (!this.isGameOver) { |
|
const healthBar = document.getElementById('health'); |
|
if (healthBar) { |
|
healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`; |
|
} |
|
|
|
const timeElement = document.getElementById('time'); |
|
if (timeElement) { |
|
timeElement.textContent = `Time: ${this.gameTime}s`; |
|
} |
|
|
|
const scoreElement = document.getElementById('score'); |
|
if (scoreElement) { |
|
scoreElement.textContent = `Score: ${this.score}`; |
|
} |
|
} |
|
} |
|
|
|
animate() { |
|
if (this.isGameOver) { |
|
if (this.animationFrameId) { |
|
cancelAnimationFrame(this.animationFrameId); |
|
} |
|
return; |
|
} |
|
|
|
this.animationFrameId = requestAnimationFrame(() => this.animate()); |
|
|
|
const currentTime = performance.now(); |
|
const deltaTime = (currentTime - this.lastTime) / 1000; |
|
this.lastTime = currentTime; |
|
|
|
if (!this.isLoading) { |
|
this.handleMovement(); |
|
this.tank.update(this.mouse.x, this.mouse.y); |
|
|
|
const tankPosition = this.tank.getPosition(); |
|
this.enemies.forEach(enemy => { |
|
enemy.update(tankPosition); |
|
|
|
if (enemy.isLoaded && enemy.mesh.position.distanceTo(tankPosition) < ENEMY_CONFIG.ATTACK_RANGE) { |
|
enemy.shoot(tankPosition); |
|
} |
|
}); |
|
|
|
this.updateParticles(); |
|
this.checkCollisions(); |
|
this.updateUI(); |
|
} |
|
|
|
this.renderer.render(this.scene, this.camera); |
|
} |
|
} |
|
|
|
|
|
window.startGame = function() { |
|
document.getElementById('startScreen').style.display = 'none'; |
|
document.body.requestPointerLock(); |
|
|
|
|
|
if (!window.gameInstance) { |
|
window.gameInstance = new Game(); |
|
} |
|
}; |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
const game = new Game(); |
|
}); |