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(); |