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 = 1000; | |
| 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 = 3; | |
| 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, scene) { | |
| if (!this.body || !this.turretGroup) return; | |
| // ν¬ν νμ - λ§μ°μ€ μμΉμ λ°λΌ νμ | |
| const screenCenter = new THREE.Vector2(0, 0); | |
| const mousePosition = new THREE.Vector2(mouseX, mouseY); | |
| const angle = Math.atan2(mousePosition.x - screenCenter.x, mousePosition.y - screenCenter.y); | |
| this.turretGroup.rotation.y = angle; | |
| // νλ μ΄μ΄ μ΄μ μ λ°μ΄νΈ | |
| 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; | |
| } | |
| } | |
| // Enemy ν΄λμ€ | |
| 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; | |
| } | |
| } | |
| } | |
| // Particle ν΄λμ€ | |
| 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); | |
| } | |
| } | |
| // 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.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.gameTimer = null; | |
| this.animationFrameId = null; | |
| this.mouse = { x: 0, y: 0 }; | |
| this.keys = { | |
| forward: false, | |
| backward: false, | |
| left: false, | |
| right: false | |
| }; | |
| this.setupEventListeners(); | |
| this.initialize(); | |
| } | |
| async initialize() { | |
| try { | |
| // μκ° ν¨κ³Ό μ κ±° | |
| this.scene.fog = null; | |
| this.scene.background = new THREE.Color(0x87CEEB); | |
| // μ£Όλ³κ΄ μ€μ - λ λ°κ² | |
| const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); | |
| this.scene.add(ambientLight); | |
| // νμκ΄ μ€μ | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(100, 100, 50); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.mapSize.width = 1024; | |
| directionalLight.shadow.mapSize.height = 1024; | |
| this.scene.add(directionalLight); | |
| // μ¬λ§ μ§ν μμ± (ν¬λ¦¬μ½₯ν° κ²μ μ€νμΌ) | |
| const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100); | |
| const groundMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xD2B48C, | |
| roughness: 0.8, | |
| metalness: 0.2 | |
| }); | |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.receiveShadow = true; | |
| // μ§νμ 기볡 μΆκ° (λ¨μν) | |
| const vertices = ground.geometry.attributes.position.array; | |
| let seed = Math.random() * 100; | |
| for (let i = 0; i < vertices.length; i += 3) { | |
| vertices[i + 2] = Math.sin(vertices[i] * 0.01) * Math.cos(vertices[i + 1] * 0.01) * 20; | |
| } | |
| ground.geometry.attributes.position.needsUpdate = true; | |
| ground.geometry.computeVertexNormals(); | |
| this.scene.add(ground); | |
| // λλ¨Έμ§ μ΄κΈ°ν μ½λ | |
| await this.addDesertDecorations(); | |
| await this.tank.initialize(this.scene, this.loader); | |
| if (!this.tank.isLoaded) { | |
| throw new Error('Tank loading failed'); | |
| } | |
| const tankPosition = this.tank.getPosition(); | |
| this.camera.position.set( | |
| tankPosition.x, | |
| tankPosition.y + 15, | |
| tankPosition.z - 30 | |
| ); | |
| this.camera.lookAt(tankPosition); | |
| this.isLoading = false; | |
| document.getElementById('loading').style.display = 'none'; | |
| this.animate(); | |
| this.spawnEnemies(); | |
| this.startGameTimer(); | |
| } catch (error) { | |
| console.error('Game initialization error:', error); | |
| this.handleLoadingError(); | |
| } | |
| } | |
| // μ¬λ§ μ₯μλ¬Ό μΆκ° λ©μλ | |
| async addDesertDecorations() { | |
| // λ°μ μμ± | |
| const rockGeometries = [ | |
| new THREE.DodecahedronGeometry(3), | |
| new THREE.DodecahedronGeometry(2), | |
| new THREE.DodecahedronGeometry(4) | |
| ]; | |
| const rockMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x8B4513, | |
| roughness: 0.9, | |
| metalness: 0.1 | |
| }); | |
| for (let i = 0; i < 100; i++) { | |
| const rockGeometry = rockGeometries[Math.floor(Math.random() * rockGeometries.length)]; | |
| const rock = new THREE.Mesh(rockGeometry, rockMaterial); | |
| rock.position.set( | |
| (Math.random() - 0.5) * MAP_SIZE * 0.9, | |
| Math.random() * 2, | |
| (Math.random() - 0.5) * MAP_SIZE * 0.9 | |
| ); | |
| rock.rotation.set( | |
| Math.random() * Math.PI, | |
| Math.random() * Math.PI, | |
| Math.random() * Math.PI | |
| ); | |
| rock.scale.set( | |
| 1 + Math.random() * 0.5, | |
| 1 + Math.random() * 0.5, | |
| 1 + Math.random() * 0.5 | |
| ); | |
| rock.castShadow = true; | |
| rock.receiveShadow = true; | |
| this.scene.add(rock); | |
| } | |
| // μ μΈμ₯ μΆκ° (κ°λ¨ν geometryλ‘ νν) | |
| const cactusGeometry = new THREE.CylinderGeometry(0.5, 0.7, 4, 8); | |
| const cactusMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x2F4F2F, | |
| roughness: 0.8 | |
| }); | |
| for (let i = 0; i < 50; i++) { | |
| const cactus = new THREE.Mesh(cactusGeometry, cactusMaterial); | |
| cactus.position.set( | |
| (Math.random() - 0.5) * MAP_SIZE * 0.8, | |
| 2, | |
| (Math.random() - 0.5) * MAP_SIZE * 0.8 | |
| ); | |
| cactus.castShadow = true; | |
| cactus.receiveShadow = true; | |
| this.scene.add(cactus); | |
| } | |
| } | |
| setupEventListeners() { | |
| document.addEventListener('keydown', (event) => { | |
| if (this.isLoading || this.isGameOver) return; | |
| 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) => { | |
| if (this.isLoading || this.isGameOver) return; | |
| 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) => { | |
| if (this.isLoading || this.isGameOver || !document.pointerLockElement) return; | |
| // λ§μ°μ€ μμ§μμ νλ©΄ μ€μ κΈ°μ€μΌλ‘ μ κ·ν | |
| const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; | |
| const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; | |
| this.mouse.x += movementX * 0.002; | |
| this.mouse.y += movementY * 0.002; | |
| // λ§μ°μ€ μ΄λ | |
| }); | |
| document.addEventListener('click', () => { | |
| if (!document.pointerLockElement) { | |
| document.body.requestPointerLock(); | |
| } else if (!this.isGameOver) { | |
| const bullet = this.tank.shoot(this.scene); | |
| if (bullet) { | |
| // Shooting effects could be added here | |
| } | |
| } | |
| }); | |
| document.addEventListener('pointerlockchange', () => { | |
| if (!document.pointerLockElement) { | |
| this.mouse.x = 0; | |
| this.mouse.y = 0; | |
| } | |
| }); | |
| window.addEventListener('resize', () => { | |
| this.camera.aspect = window.innerWidth / window.innerHeight; | |
| this.camera.updateProjectionMatrix(); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| } | |
| handleMovement() { | |
| if (!this.tank.isLoaded || this.isGameOver) return; | |
| const direction = new THREE.Vector3(); | |
| if (this.keys.forward) direction.z += 1; | |
| if (this.keys.backward) direction.z -= 1; | |
| if (this.keys.left) direction.x -= 1; | |
| if (this.keys.right) direction.x += 1; | |
| if (direction.length() > 0) { | |
| direction.normalize(); | |
| if (this.keys.left) this.tank.rotate(-1); | |
| if (this.keys.right) this.tank.rotate(1); | |
| direction.applyEuler(this.tank.body.rotation); | |
| this.tank.move(direction); | |
| } | |
| // μΉ΄λ©λΌ μμΉ μ λ°μ΄νΈ - νμ ν±ν¬ λ€μμ λ°λΌλ³΄λλ‘ μ€μ | |
| const tankPos = this.tank.getPosition(); | |
| const tankRotation = this.tank.body.rotation.y; | |
| const cameraDistance = 30; | |
| const cameraHeight = 15; | |
| // ν±ν¬μ λ€μͺ½ μμΉμ μΉ΄λ©λΌ λ°°μΉ | |
| this.camera.position.set( | |
| tankPos.x - Math.sin(tankRotation) * cameraDistance, | |
| tankPos.y + cameraHeight, | |
| tankPos.z - Math.cos(tankRotation) * cameraDistance | |
| ); | |
| // μΉ΄λ©λΌκ° νμ ν±ν¬λ₯Ό λ°λΌλ³΄λλ‘ μ€μ | |
| this.camera.lookAt(new THREE.Vector3( | |
| tankPos.x, | |
| tankPos.y, | |
| tankPos.z | |
| )); | |
| } | |
| this.camera.lookAt(lookAtPoint); | |
| } | |
| createBuildings() { | |
| const buildingTypes = [ | |
| { width: 10, height: 30, depth: 10, color: 0x808080 }, | |
| { width: 15, height: 40, depth: 15, color: 0x606060 }, | |
| { width: 20, height: 50, depth: 20, color: 0x404040 } | |
| ]; | |
| for (let i = 0; i < BUILDING_COUNT; i++) { | |
| const type = buildingTypes[Math.floor(Math.random() * buildingTypes.length)]; | |
| const building = this.createBuilding(type); | |
| let position; | |
| let attempts = 0; | |
| do { | |
| position = new THREE.Vector3( | |
| (Math.random() - 0.5) * (MAP_SIZE - type.width), | |
| type.height / 2, | |
| (Math.random() - 0.5) * (MAP_SIZE - type.depth) | |
| ); | |
| attempts++; | |
| } while (this.checkBuildingCollision(position, type) && attempts < 50); | |
| if (attempts < 50) { | |
| building.position.copy(position); | |
| this.buildings.push(building); | |
| this.scene.add(building); | |
| } | |
| } | |
| return Promise.resolve(); | |
| } | |
| createBuilding(type) { | |
| const geometry = new THREE.BoxGeometry(type.width, type.height, type.depth); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: type.color, | |
| emissive: 0x222222, | |
| specular: 0x111111, | |
| shininess: 30 | |
| }); | |
| const building = new THREE.Mesh(geometry, material); | |
| building.castShadow = true; | |
| building.receiveShadow = true; | |
| return building; | |
| } | |
| checkBuildingCollision(position, type) { | |
| const margin = 5; | |
| const bbox = new THREE.Box3( | |
| new THREE.Vector3( | |
| position.x - (type.width / 2 + margin), | |
| 0, | |
| position.z - (type.depth / 2 + margin) | |
| ), | |
| new THREE.Vector3( | |
| position.x + (type.width / 2 + margin), | |
| type.height, | |
| position.z + (type.depth / 2 + margin) | |
| ) | |
| ); | |
| return this.buildings.some(building => { | |
| const buildingBox = new THREE.Box3().setFromObject(building); | |
| return bbox.intersectsBox(buildingBox); | |
| }); | |
| } | |
| handleLoadingError() { | |
| this.isLoading = false; | |
| const loadingElement = document.getElementById('loading'); | |
| if (loadingElement) { | |
| loadingElement.innerHTML = ` | |
| <div class="loading-text" style="color: red;"> | |
| Loading failed. Please refresh the page. | |
| </div> | |
| `; | |
| } | |
| } | |
| startGameTimer() { | |
| if (this.gameTimer) { | |
| clearInterval(this.gameTimer); | |
| } | |
| this.gameTimer = setInterval(() => { | |
| if (this.isLoading || this.isGameOver) { | |
| clearInterval(this.gameTimer); | |
| return; | |
| } | |
| this.gameTime--; | |
| document.getElementById('time').textContent = `Time: ${this.gameTime}s`; | |
| if (this.gameTime <= 0) { | |
| clearInterval(this.gameTimer); | |
| this.endGame(); | |
| } | |
| }, 1000); | |
| } | |
| spawnEnemies() { | |
| const spawnEnemy = () => { | |
| if (this.enemies.length < 3 && !this.isGameOver) { // μ΅λ 3λλ‘ μ ν | |
| const position = this.getValidEnemySpawnPosition(); | |
| if (position) { | |
| const type = Math.random() < 0.7 ? 'tank' : 'heavy'; | |
| const enemy = new Enemy(this.scene, position, type); | |
| enemy.initialize(this.loader); | |
| this.enemies.push(enemy); | |
| } | |
| } | |
| if (!this.isGameOver) { | |
| setTimeout(spawnEnemy, 10000); // 10μ΄λ§λ€ μ€ν° | |
| } | |
| }; | |
| spawnEnemy(); | |
| } | |
| getValidEnemySpawnPosition() { | |
| const margin = 20; | |
| let position; | |
| let attempts = 0; | |
| const maxAttempts = 50; | |
| do { | |
| position = new THREE.Vector3( | |
| (Math.random() - 0.5) * (MAP_SIZE - margin * 2), | |
| ENEMY_GROUND_HEIGHT, | |
| (Math.random() - 0.5) * (MAP_SIZE - margin * 2) | |
| ); | |
| const distanceToPlayer = position.distanceTo(this.tank.getPosition()); | |
| if (distanceToPlayer < 100) continue; | |
| let collisionFound = false; | |
| for (const building of this.buildings) { | |
| const buildingBox = new THREE.Box3().setFromObject(building); | |
| if (buildingBox.containsPoint(position)) { | |
| collisionFound = true; | |
| break; | |
| } | |
| } | |
| if (!collisionFound) return position; | |
| attempts++; | |
| } while (attempts < maxAttempts); | |
| return null; | |
| } | |
| 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() { | |
| if (this.isLoading || !this.tank.isLoaded) return; | |
| const tankPosition = this.tank.getPosition(); | |
| // μ μ΄μκ³Ό νλ μ΄μ΄ ν±ν¬ μΆ©λ μ²΄ν¬ | |
| this.enemies.forEach(enemy => { | |
| if (!enemy.mesh || !enemy.isLoaded) 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); | |
| this.createExplosion(bullet.position); | |
| document.getElementById('health').style.width = | |
| `${(this.tank.health / MAX_HEALTH) * 100}%`; | |
| } | |
| }); | |
| }); | |
| // νλ μ΄μ΄ μ΄μκ³Ό μ μΆ©λ μ²΄ν¬ | |
| this.tank.bullets.forEach((bullet, bulletIndex) => { | |
| this.enemies.forEach((enemy, enemyIndex) => { | |
| if (!enemy.mesh || !enemy.isLoaded) return; | |
| const distance = bullet.position.distanceTo(enemy.mesh.position); | |
| if (distance < 2) { | |
| if (enemy.takeDamage(50)) { | |
| enemy.destroy(); | |
| this.enemies.splice(enemyIndex, 1); | |
| this.score += 100; | |
| document.getElementById('score').textContent = `Score: ${this.score}`; | |
| } | |
| this.scene.remove(bullet); | |
| this.tank.bullets.splice(bulletIndex, 1); | |
| this.createExplosion(bullet.position); | |
| } | |
| }); | |
| }); | |
| // νλ μ΄μ΄ ν±ν¬μ 건물 μΆ©λ μ²΄ν¬ | |
| const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body); | |
| for (const building of this.buildings) { | |
| const buildingBox = new THREE.Box3().setFromObject(building); | |
| if (tankBoundingBox.intersectsBox(buildingBox)) { | |
| this.tank.body.position.copy(this.previousTankPosition); | |
| break; | |
| } | |
| } | |
| // μ΄μ μμΉ μ μ₯ | |
| this.previousTankPosition.copy(this.tank.body.position); | |
| } | |
| endGame() { | |
| if (this.isGameOver) return; | |
| this.isGameOver = true; | |
| if (this.gameTimer) { | |
| clearInterval(this.gameTimer); | |
| } | |
| if (this.animationFrameId) { | |
| cancelAnimationFrame(this.animationFrameId); | |
| } | |
| document.exitPointerLock(); | |
| 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 = '#0f0'; | |
| gameOverDiv.style.fontSize = '48px'; | |
| gameOverDiv.style.backgroundColor = 'rgba(0, 20, 0, 0.7)'; | |
| gameOverDiv.style.padding = '20px'; | |
| gameOverDiv.style.borderRadius = '10px'; | |
| gameOverDiv.style.textAlign = 'center'; | |
| gameOverDiv.innerHTML = ` | |
| Game Over<br> | |
| Score: ${this.score}<br> | |
| Time Survived: ${GAME_DURATION - this.gameTime}s<br> | |
| <button onclick="location.reload()" | |
| style="font-size: 24px; padding: 10px; margin-top: 20px; | |
| cursor: pointer; background: #0f0; border: none; | |
| color: black; border-radius: 5px;"> | |
| Play Again | |
| </button> | |
| `; | |
| document.body.appendChild(gameOverDiv); | |
| } | |
| updateUI() { | |
| if (!this.isGameOver) { | |
| const healthBar = document.getElementById('health'); | |
| if (healthBar) { | |
| healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`; | |
| } | |
| const timeElement = document.getElementById('time'); | |
| if (timeElement) { | |
| timeElement.textContent = `Time: ${this.gameTime}s`; | |
| } | |
| const scoreElement = document.getElementById('score'); | |
| if (scoreElement) { | |
| scoreElement.textContent = `Score: ${this.score}`; | |
| } | |
| } | |
| } | |
| animate() { | |
| if (this.isGameOver) { | |
| if (this.animationFrameId) { | |
| cancelAnimationFrame(this.animationFrameId); | |
| } | |
| return; | |
| } | |
| this.animationFrameId = requestAnimationFrame(() => this.animate()); | |
| const currentTime = performance.now(); | |
| const deltaTime = (currentTime - this.lastTime) / 1000; | |
| this.lastTime = currentTime; | |
| if (!this.isLoading) { | |
| this.handleMovement(); | |
| this.tank.update(this.mouse.x, this.mouse.y, this.scene); // scene κ°μ²΄ μ λ¬ | |
| const tankPosition = this.tank.getPosition(); | |
| this.enemies.forEach(enemy => { | |
| enemy.update(tankPosition); | |
| if (enemy.isLoaded && enemy.mesh.position.distanceTo(tankPosition) < ENEMY_CONFIG.ATTACK_RANGE) { | |
| enemy.shoot(tankPosition); | |
| } | |
| }); | |
| this.updateParticles(); | |
| this.checkCollisions(); | |
| this.updateUI(); | |
| } | |
| this.renderer.render(this.scene, this.camera); | |
| } | |
| } | |
| // Start game | |
| window.startGame = function() { | |
| document.getElementById('startScreen').style.display = 'none'; | |
| document.body.requestPointerLock(); | |
| // Create new game instance or restart existing one | |
| if (!window.gameInstance) { | |
| window.gameInstance = new Game(); | |
| } | |
| }; | |
| // Initialize game | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const game = new Game(); | |
| }); |