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 = { TANK: { MODEL: '/models/enemy1.glb', HEALTH: 120, SPEED: 0.1, ATTACK_RANGE: 100, ATTACK_INTERVAL: 2000, BULLET_SPEED: 2 }, HEAVY_TANK: { MODEL: '/models/enemy4.glb', HEALTH: 200, SPEED: 0.05, ATTACK_RANGE: 150, ATTACK_INTERVAL: 3000, BULLET_SPEED: 1.5 } }; // Building 클래스 추가 class Building { constructor(scene, position, size) { const geometry = new THREE.BoxGeometry(size.width, size.height, size.depth); const material = new THREE.MeshStandardMaterial({ color: 0x808080, roughness: 0.7, metalness: 0.3 }); this.mesh = new THREE.Mesh(geometry, material); this.mesh.position.copy(position); this.mesh.position.y = size.height / 2; this.mesh.castShadow = true; this.mesh.receiveShadow = true; scene.add(this.mesh); } } // Enemy 클래스 수정 class Enemy { constructor(scene, position, type) { this.scene = scene; this.position = position; this.type = type; this.mesh = null; this.health = type === 'TANK' ? ENEMY_CONFIG.TANK.HEALTH : ENEMY_CONFIG.HEAVY_TANK.HEALTH; this.lastAttackTime = 0; this.bullets = []; this.config = ENEMY_CONFIG[type]; } async initialize(loader) { try { const modelPath = this.type === 'TANK' ? ENEMY_CONFIG.TANK.MODEL : ENEMY_CONFIG.HEAVY_TANK.MODEL; 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); } 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); // 타입별 이동 속도 적용 const moveSpeed = this.type === 'TANK' ? ENEMY_CONFIG.TANK.SPEED : ENEMY_CONFIG.HEAVY_TANK.SPEED; this.mesh.position.add(direction.multiplyScalar(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.TANK.ATTACK_INTERVAL : ENEMY_CONFIG.HEAVY_TANK.ATTACK_INTERVAL; if (currentTime - this.lastAttackTime < attackInterval) return; const bulletGeometry = new THREE.SphereGeometry(0.2); 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.TANK.BULLET_SPEED : ENEMY_CONFIG.HEAVY_TANK.BULLET_SPEED; 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 = []; } } } // Game 클래스 수정 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.buildings = []; // 건물 배열 추가 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: 0x333333, // 어두운 회색 (아스팔트) roughness: 0.9, metalness: 0.1 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; this.scene.add(ground); // 건물 생성 this.createBuildings(); // 탱크 초기화 await this.tank.initialize(this.scene, this.loader); // 카메라 설정 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(); } createBuildings() { for (let i = 0; i < BUILDING_COUNT; i++) { const size = { width: 10 + Math.random() * 20, height: 20 + Math.random() * 80, depth: 10 + Math.random() * 20 }; const position = new THREE.Vector3( (Math.random() - 0.5) * (MAP_SIZE - size.width), 0, (Math.random() - 0.5) * (MAP_SIZE - size.depth) ); const building = new Building(this.scene, position, size); this.buildings.push(building); } } 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 type = Math.random() > 0.7 ? 'HEAVY_TANK' : 'TANK'; const enemy = new Enemy(this.scene, position, type); enemy.initialize(this.loader); this.enemies.push(enemy); } setTimeout(spawnEnemy, 3000); }; spawnEnemy(); } // Game 클래스 계속... 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(enemy.type === 'HEAVY_TANK' ? 15 : 10)) { this.endGame(); } this.scene.remove(bullet); enemy.bullets = enemy.bullets.filter(b => b !== bullet); } }); // 건물과의 충돌 체크 this.buildings.forEach(building => { // 간단한 충돌 체크 (실제 게임에서는 더 정교한 충돌 체크가 필요할 수 있습니다) const distance = enemy.mesh.position.distanceTo(building.mesh.position); if (distance < 5) { // 임의의 충돌 거리 const pushDirection = new THREE.Vector3() .subVectors(enemy.mesh.position, building.mesh.position) .normalize(); enemy.mesh.position.add(pushDirection); } }); }); } 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
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();