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