|
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 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 = 5; |
|
const PARTICLE_COUNT = 15; |
|
const OBSTACLE_COUNT = 50; |
|
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(); |
|
} |
|
|
|
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 = TANK_HEIGHT; |
|
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); |
|
|
|
} catch (error) { |
|
console.error('Error loading tank models:', error); |
|
} |
|
} |
|
|
|
update(mouseX, mouseY) { |
|
if (!this.body || !this.turretGroup) return; |
|
|
|
|
|
const targetAngle = Math.atan2(mouseX, mouseY); |
|
|
|
|
|
const currentRotation = this.turretGroup.rotation.y; |
|
const rotationDiff = targetAngle - currentRotation; |
|
this.turretGroup.rotation.y += rotationDiff * 0.1; |
|
} |
|
|
|
move(direction) { |
|
if (!this.body) return; |
|
|
|
const moveVector = new THREE.Vector3(); |
|
moveVector.x = direction.x * this.moveSpeed; |
|
moveVector.z = direction.z * this.moveSpeed; |
|
|
|
moveVector.applyEuler(this.body.rotation); |
|
this.body.position.add(moveVector); |
|
} |
|
|
|
rotate(angle) { |
|
if (!this.body) return; |
|
this.body.rotation.y += angle * this.turnSpeed; |
|
} |
|
} |
|
|
|
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; |
|
} |
|
|
|
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); |
|
|
|
} catch (error) { |
|
console.error('Error loading tank models:', error); |
|
} |
|
} |
|
|
|
update(mouseX, mouseY) { |
|
if (!this.body || !this.turretGroup) return; |
|
|
|
const targetAngle = Math.atan2(mouseX, mouseY); |
|
const currentRotation = this.turretGroup.rotation.y; |
|
const rotationDiff = targetAngle - currentRotation; |
|
|
|
|
|
let normalizedDiff = rotationDiff; |
|
while (normalizedDiff > Math.PI) normalizedDiff -= Math.PI * 2; |
|
while (normalizedDiff < -Math.PI) normalizedDiff += Math.PI * 2; |
|
|
|
this.turretGroup.rotation.y += normalizedDiff * 0.1; |
|
} |
|
|
|
move(direction) { |
|
if (!this.body) return; |
|
|
|
const moveVector = new THREE.Vector3(); |
|
moveVector.x = direction.x * this.moveSpeed; |
|
moveVector.z = direction.z * this.moveSpeed; |
|
|
|
moveVector.applyEuler(this.body.rotation); |
|
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) { |
|
this.scene = scene; |
|
this.position = position; |
|
this.mesh = null; |
|
this.health = 100; |
|
this.lastAttackTime = 0; |
|
this.bullets = []; |
|
} |
|
|
|
async initialize(loader) { |
|
try { |
|
const result = await loader.loadAsync('/models/enemy.glb'); |
|
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); |
|
} catch (error) { |
|
console.error('Error loading enemy model:', error); |
|
} |
|
} |
|
|
|
update(playerPosition) { |
|
if (!this.mesh) return; |
|
|
|
|
|
const direction = new THREE.Vector3() |
|
.subVectors(playerPosition, this.mesh.position) |
|
.normalize(); |
|
|
|
this.mesh.lookAt(playerPosition); |
|
|
|
|
|
this.mesh.position.add(direction.multiplyScalar(ENEMY_MOVE_SPEED)); |
|
|
|
|
|
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(); |
|
if (currentTime - this.lastAttackTime < ENEMY_CONFIG.ATTACK_INTERVAL) return; |
|
|
|
const bulletGeometry = new THREE.SphereGeometry(0.2); |
|
const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); |
|
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); |
|
|
|
bullet.position.copy(this.mesh.position); |
|
|
|
const direction = new THREE.Vector3() |
|
.subVectors(playerPosition, this.mesh.position) |
|
.normalize(); |
|
|
|
bullet.velocity = direction.multiplyScalar(ENEMY_CONFIG.BULLET_SPEED); |
|
|
|
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 = []; |
|
} |
|
} |
|
} |
|
|
|
|
|
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.body.appendChild(this.renderer.domElement); |
|
|
|
|
|
this.tank = new TankPlayer(); |
|
this.enemies = []; |
|
this.particles = []; |
|
this.obstacles = []; |
|
this.loader = new GLTFLoader(); |
|
this.controls = null; |
|
this.gameTime = GAME_DURATION; |
|
this.score = 0; |
|
this.isGameOver = false; |
|
|
|
|
|
this.mouse = { |
|
x: 0, |
|
y: 0 |
|
}; |
|
|
|
|
|
this.keys = { |
|
forward: false, |
|
backward: false, |
|
left: false, |
|
right: false |
|
}; |
|
|
|
|
|
this.setupEventListeners(); |
|
|
|
|
|
this.initialize(); |
|
} |
|
|
|
async initialize() { |
|
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); |
|
this.scene.add(ambientLight); |
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); |
|
directionalLight.position.set(50, 50, 50); |
|
directionalLight.castShadow = true; |
|
this.scene.add(directionalLight); |
|
|
|
|
|
const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE); |
|
const groundMaterial = new THREE.MeshStandardMaterial({ |
|
color: 0x808080, |
|
roughness: 0.8, |
|
metalness: 0.2 |
|
}); |
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial); |
|
ground.rotation.x = -Math.PI / 2; |
|
ground.receiveShadow = true; |
|
this.scene.add(ground); |
|
|
|
|
|
await this.tank.initialize(this.scene, this.loader); |
|
|
|
|
|
this.createObstacles(); |
|
|
|
|
|
this.camera.position.set(0, 10, -10); |
|
this.camera.lookAt(0, 0, 0); |
|
|
|
|
|
this.controls = new PointerLockControls(this.camera, document.body); |
|
|
|
|
|
this.animate(); |
|
this.spawnEnemies(); |
|
this.startGameTimer(); |
|
} |
|
|
|
createObstacles() { |
|
for (let i = 0; i < OBSTACLE_COUNT; i++) { |
|
const geometry = new THREE.BoxGeometry(2, 2, 2); |
|
const material = new THREE.MeshStandardMaterial({ color: 0x808080 }); |
|
const obstacle = new THREE.Mesh(geometry, material); |
|
|
|
obstacle.position.x = (Math.random() - 0.5) * MAP_SIZE; |
|
obstacle.position.z = (Math.random() - 0.5) * MAP_SIZE; |
|
obstacle.position.y = 1; |
|
|
|
obstacle.castShadow = true; |
|
obstacle.receiveShadow = true; |
|
|
|
this.obstacles.push(obstacle); |
|
this.scene.add(obstacle); |
|
} |
|
} |
|
|
|
spawnEnemies() { |
|
const spawnEnemy = () => { |
|
if (this.enemies.length < ENEMY_COUNT_MAX && !this.isGameOver) { |
|
const position = new THREE.Vector3( |
|
(Math.random() - 0.5) * MAP_SIZE, |
|
ENEMY_GROUND_HEIGHT, |
|
(Math.random() - 0.5) * MAP_SIZE |
|
); |
|
|
|
const enemy = new Enemy(this.scene, position); |
|
enemy.initialize(this.loader); |
|
this.enemies.push(enemy); |
|
} |
|
setTimeout(spawnEnemy, 3000); |
|
}; |
|
|
|
spawnEnemy(); |
|
} |
|
|
|
startGameTimer() { |
|
const timer = setInterval(() => { |
|
this.gameTime--; |
|
if (this.gameTime <= 0 || this.isGameOver) { |
|
clearInterval(timer); |
|
this.endGame(); |
|
} |
|
}, 1000); |
|
} |
|
|
|
setupEventListeners() { |
|
|
|
document.addEventListener('keydown', (event) => { |
|
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) => { |
|
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) => { |
|
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; |
|
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; |
|
}); |
|
|
|
|
|
window.addEventListener('resize', () => { |
|
this.camera.aspect = window.innerWidth / window.innerHeight; |
|
this.camera.updateProjectionMatrix(); |
|
this.renderer.setSize(window.innerWidth, window.innerHeight); |
|
}); |
|
} |
|
|
|
handleMovement() { |
|
const direction = new THREE.Vector3(); |
|
|
|
if (this.keys.forward) direction.z += 1; |
|
if (this.keys.backward) direction.z -= 1; |
|
if (this.keys.left) this.tank.rotate(-1); |
|
if (this.keys.right) this.tank.rotate(1); |
|
|
|
if (direction.length() > 0) { |
|
direction.normalize(); |
|
this.tank.move(direction); |
|
} |
|
} |
|
|
|
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() { |
|
const tankPosition = this.tank.getPosition(); |
|
|
|
|
|
this.enemies.forEach(enemy => { |
|
if (!enemy.mesh) 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); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
endGame() { |
|
this.isGameOver = true; |
|
|
|
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 = 'white'; |
|
gameOverDiv.style.fontSize = '48px'; |
|
gameOverDiv.innerHTML = `Game Over<br>Score: ${this.score}`; |
|
document.body.appendChild(gameOverDiv); |
|
} |
|
|
|
animate() { |
|
if (this.isGameOver) return; |
|
|
|
requestAnimationFrame(() => this.animate()); |
|
|
|
|
|
this.tank.update(this.mouse.x, this.mouse.y); |
|
this.handleMovement(); |
|
|
|
|
|
const tankPosition = this.tank.getPosition(); |
|
this.enemies.forEach(enemy => { |
|
enemy.update(tankPosition); |
|
const distance = enemy.mesh?.position.distanceTo(tankPosition) || Infinity; |
|
if (distance < ENEMY_CONFIG.ATTACK_RANGE) { |
|
enemy.shoot(tankPosition); |
|
} |
|
}); |
|
|
|
|
|
this.updateParticles(); |
|
|
|
|
|
this.checkCollisions(); |
|
|
|
|
|
this.renderer.render(this.scene, this.camera); |
|
} |
|
} |
|
|
|
|
|
const game = new Game(); |