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 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 BUILDING_COUNT = 30; | |
const ENEMY_CONFIG = { | |
ATTACK_RANGE: 100, | |
ATTACK_INTERVAL: 2000, | |
BULLET_SPEED: 2 | |
}; | |
// TankPlayer ν΄λμ€ | |
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); | |
} | |
} | |
} | |
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.mouse = { x: 0, y: 0 }; | |
this.keys = { | |
forward: false, | |
backward: false, | |
left: false, | |
right: false | |
}; | |
this.setupEventListeners(); | |
this.initialize(); | |
this.disposedObjects = new Set(); | |
} | |
// μ¬κΈ°μ Game ν΄λμ€μ λλ¨Έμ§ λ©μλλ€μ΄ λ€μ΄κ°λλ€... | |
// (initialize, setupEventListeners, handleMovement, animate, checkCollisions, updateUI, endGame λ±) | |
createExplosion(position) { | |
for (let i = 0; i < PARTICLE_COUNT; i++) { | |
this.particles.push(new Particle(this.scene, position)); | |
} | |
} | |
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); | |
} | |
} | |
} | |
async createBuildings() { | |
for (let i = 0; i < BUILDING_COUNT; i++) { | |
const x = (Math.random() - 0.5) * MAP_SIZE; | |
const z = (Math.random() - 0.5) * MAP_SIZE; | |
const geometry = new THREE.BoxGeometry(10, 20, 10); | |
const material = new THREE.MeshStandardMaterial({ color: 0x808080 }); | |
const building = new THREE.Mesh(geometry, material); | |
building.position.set(x, 10, z); | |
building.castShadow = true; | |
building.receiveShadow = true; | |
this.scene.add(building); | |
this.buildings.push(building); | |
} | |
} | |
spawnEnemies() { | |
const spawnInterval = setInterval(() => { | |
if (this.isGameOver || this.enemies.length >= ENEMY_COUNT_MAX) return; | |
const angle = Math.random() * Math.PI * 2; | |
const distance = 100 + Math.random() * 50; | |
const x = Math.cos(angle) * distance; | |
const z = Math.sin(angle) * distance; | |
const enemyType = Math.random() > 0.7 ? 'heavy' : 'tank'; | |
const enemy = new Enemy(this.scene, new THREE.Vector3(x, ENEMY_GROUND_HEIGHT, z), enemyType); | |
enemy.initialize(this.loader); | |
this.enemies.push(enemy); | |
}, 3000); | |
} | |
startGameTimer() { | |
const timerInterval = setInterval(() => { | |
if (this.isGameOver) { | |
clearInterval(timerInterval); | |
return; | |
} | |
this.gameTime--; | |
if (this.gameTime <= 0) { | |
this.endGame(); | |
clearInterval(timerInterval); | |
} | |
}, 1000); | |
} | |
handleLoadingError() { | |
this.isLoading = false; | |
document.getElementById('loading').style.display = 'none'; | |
document.getElementById('error').style.display = 'block'; | |
} | |
} | |
// κ²μ μμ | |
window.startGame = function() { | |
document.getElementById('startScreen').style.display = 'none'; | |
document.body.requestPointerLock(); | |
}; | |
const game = new Game(); |