diff --git "a/game.js" "b/game.js" --- "a/game.js" +++ "b/game.js" @@ -19,8 +19,8 @@ const ENEMY_CONFIG = { BULLET_SPEED: 2 }; -// TankPlayer 클래스 -class TankPlayer { + // TankPlayer 클래스 + class TankPlayer { constructor() { this.body = null; this.turret = null; @@ -42,524 +42,277 @@ class TankPlayer { 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 }); - } - // 별도의 메서드로 분리 - createExplosionEffect(scene, position) { - // 폭발 중심 플래시 - const flashGeometry = new THREE.SphereGeometry(3); - const flashMaterial = new THREE.MeshBasicMaterial({ - color: 0xffff00, - transparent: true, - opacity: 1 - }); - const flash = new THREE.Mesh(flashGeometry, flashMaterial); - flash.position.copy(position); - scene.add(flash); - - // 폭발 파티클 - for (let i = 0; i < 30; i++) { - const size = Math.random() * 0.5 + 0.3; - const geometry = new THREE.SphereGeometry(size); - - // 다양한 색상의 파티클 - const colors = [0xff4500, 0xff8c00, 0xff0000, 0xffd700]; - const material = new THREE.MeshBasicMaterial({ - color: colors[Math.floor(Math.random() * colors.length)], + } + // 별도의 메서드로 분리 + createExplosionEffect(scene, position) { + // 폭발 중심 플래시 + const flashGeometry = new THREE.SphereGeometry(3); + const flashMaterial = new THREE.MeshBasicMaterial({ + color: 0xffff00, transparent: true, opacity: 1 }); - - const particle = new THREE.Mesh(geometry, material); - particle.position.copy(position); - - // 파티클 속도와 방향 설정 - const speed = Math.random() * 0.5 + 0.3; - const angle = Math.random() * Math.PI * 2; - const elevation = Math.random() * Math.PI - Math.PI / 2; - - particle.velocity = new THREE.Vector3( - Math.cos(angle) * Math.cos(elevation) * speed, - Math.sin(elevation) * speed, - Math.sin(angle) * Math.cos(elevation) * speed - ); - - particle.gravity = -0.015; - particle.life = Math.random() * 30 + 30; - particle.fadeRate = 0.97; - - scene.add(particle); - window.gameInstance.particles.push({ - mesh: particle, - velocity: particle.velocity, - gravity: particle.gravity, - life: particle.life, - fadeRate: particle.fadeRate - }); - } - - // 폭발 링 이펙트 - const ringGeometry = new THREE.RingGeometry(0.1, 2, 32); - const ringMaterial = new THREE.MeshBasicMaterial({ - color: 0xff8c00, - transparent: true, - opacity: 1, - side: THREE.DoubleSide - }); - const ring = new THREE.Mesh(ringGeometry, ringMaterial); - ring.position.copy(position); - ring.lookAt(new THREE.Vector3(0, 1, 0)); - scene.add(ring); - - // 링 확장 애니메이션 - const expandRing = () => { - ring.scale.x += 0.2; - ring.scale.y += 0.2; - ring.material.opacity *= 0.95; - - if (ring.material.opacity > 0.01) { - requestAnimationFrame(expandRing); - } else { - scene.remove(ring); + const flash = new THREE.Mesh(flashGeometry, flashMaterial); + flash.position.copy(position); + scene.add(flash); + + // 폭발 파티클 + for (let i = 0; i < 30; i++) { + const size = Math.random() * 0.5 + 0.3; + const geometry = new THREE.SphereGeometry(size); + + // 다양한 색상의 파티클 + const colors = [0xff4500, 0xff8c00, 0xff0000, 0xffd700]; + const material = new THREE.MeshBasicMaterial({ + color: colors[Math.floor(Math.random() * colors.length)], + transparent: true, + opacity: 1 + }); + + const particle = new THREE.Mesh(geometry, material); + particle.position.copy(position); + + // 파티클 속도와 방향 설정 + const speed = Math.random() * 0.5 + 0.3; + const angle = Math.random() * Math.PI * 2; + const elevation = Math.random() * Math.PI - Math.PI / 2; + + particle.velocity = new THREE.Vector3( + Math.cos(angle) * Math.cos(elevation) * speed, + Math.sin(elevation) * speed, + Math.sin(angle) * Math.cos(elevation) * speed + ); + + particle.gravity = -0.015; + particle.life = Math.random() * 30 + 30; + particle.fadeRate = 0.97; + + scene.add(particle); + window.gameInstance.particles.push({ + mesh: particle, + velocity: particle.velocity, + gravity: particle.gravity, + life: particle.life, + fadeRate: particle.fadeRate + }); } - }; - expandRing(); - - // 폭발음 효과 - const explosionSound = new Audio('sounds/bang.ogg'); - explosionSound.volume = 0.4; - explosionSound.play(); - - // 카메라 흔들림 효과 - if (window.gameInstance && window.gameInstance.camera) { - const camera = window.gameInstance.camera; - const originalPosition = camera.position.clone(); - let shakeTime = 0; - const shakeIntensity = 0.3; - const shakeDuration = 500; - - const shakeCamera = () => { - if (shakeTime < shakeDuration) { - camera.position.x = originalPosition.x + (Math.random() - 0.5) * shakeIntensity; - camera.position.y = originalPosition.y + (Math.random() - 0.5) * shakeIntensity; - camera.position.z = originalPosition.z + (Math.random() - 0.5) * shakeIntensity; - - shakeTime += 16; - requestAnimationFrame(shakeCamera); - } else { - camera.position.copy(originalPosition); - } - }; - shakeCamera(); - } - - // 중심 플래시 제거 - setTimeout(() => { - scene.remove(flash); - }, 100); -} - - async initialize(scene, loader) { - try { - const bodyResult = await loader.loadAsync('/models/abramsBody.glb'); - this.body = bodyResult.scene; - - 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; - child.material.shadowSide = THREE.BackSide; - child.material.needsUpdate = true; - } - }); - - this.turret.traverse((child) => { - if (child.isMesh) { - child.castShadow = true; - child.receiveShadow = true; - child.material.shadowSide = THREE.BackSide; - child.material.needsUpdate = true; - } - }); - - // 그림자 평면 - const shadowPlaneGeometry = new THREE.PlaneGeometry(8, 8); - const shadowPlaneMaterial = new THREE.ShadowMaterial({ - opacity: 0.3 + + // 폭발 링 이펙트 + const ringGeometry = new THREE.RingGeometry(0.1, 2, 32); + const ringMaterial = new THREE.MeshBasicMaterial({ + color: 0xff8c00, + transparent: true, + opacity: 1, + side: THREE.DoubleSide }); - this.shadowPlane = new THREE.Mesh(shadowPlaneGeometry, shadowPlaneMaterial); - this.shadowPlane.receiveShadow = true; - this.shadowPlane.rotation.x = -Math.PI / 2; - this.shadowPlane.position.y = 0.1; - this.body.add(this.shadowPlane); - - // 간단한 스폰 위치 설정 - const spawnPosition = new THREE.Vector3( - (Math.random() - 0.5) * (MAP_SIZE * 0.8), // MAP_SIZE의 80%만 사용 - TANK_HEIGHT, // 고정된 높이 사용 - (Math.random() - 0.5) * (MAP_SIZE * 0.8) - ); - - this.body.position.copy(spawnPosition); - - // 폭발 이펙트 메서드 추가 - this.createExplosionEffect = (scene, position) => { - // 폭발 파티클 - for (let i = 0; i < 15; i++) { - const size = Math.random() * 0.2 + 0.1; - const geometry = new THREE.SphereGeometry(size); - const material = new THREE.MeshBasicMaterial({ - color: Math.random() < 0.5 ? 0xff4500 : 0xff8c00 - }); - const particle = new THREE.Mesh(geometry, material); - particle.position.copy(position); - - const speed = Math.random() * 0.3 + 0.2; - const angle = Math.random() * Math.PI * 2; - const elevation = Math.random() * Math.PI - Math.PI / 2; - - particle.velocity = new THREE.Vector3( - Math.cos(angle) * Math.cos(elevation) * speed, - Math.sin(elevation) * speed, - Math.sin(angle) * Math.cos(elevation) * speed - ); - - particle.gravity = -0.01; - particle.life = Math.random() * 20 + 20; - particle.fadeRate = 1 / particle.life; - - scene.add(particle); - window.gameInstance.particles.push({ - mesh: particle, - velocity: particle.velocity, - gravity: particle.gravity, - life: particle.life, - fadeRate: particle.fadeRate - }); + const ring = new THREE.Mesh(ringGeometry, ringMaterial); + ring.position.copy(position); + ring.lookAt(new THREE.Vector3(0, 1, 0)); + scene.add(ring); + + // 링 확장 애니메이션 + const expandRing = () => { + ring.scale.x += 0.2; + ring.scale.y += 0.2; + ring.material.opacity *= 0.95; + + if (ring.material.opacity > 0.01) { + requestAnimationFrame(expandRing); + } else { + scene.remove(ring); } - - // 충돌 사운드 재생 - const explosionSound = new Audio('sounds/explosion.ogg'); - explosionSound.volume = 0.3; - explosionSound.play(); }; - - scene.add(this.body); - this.isLoaded = true; - this.updateAmmoDisplay(); - - } catch (error) { - console.error('Error loading tank models:', error); - this.isLoaded = false; - } -} - - shoot(scene) { - // 재장전 중이거나 탄약이 없으면 발사하지 않음 - if (this.isReloading || this.ammo <= 0) { - return null; - } - - // 발사 딜레이 체크 (연속 발사 방지) - const currentTime = Date.now(); - if (currentTime - this.lastShootTime < 100) { // 100ms의 발사 딜레이 - return null; - } - this.lastShootTime = currentTime; - - // 발사음 효과 (한 번만 재생) - const sounds = ['sounds/mbtfire1.ogg', 'sounds/mbtfire2.ogg', 'sounds/mbtfire3.ogg', 'sounds/mbtfire4.ogg']; - const randomSound = sounds[Math.floor(Math.random() * sounds.length)]; - const audio = new Audio(randomSound); - audio.volume = 0.5; - - // 이전 오디오 인스턴스가 있다면 중지 - if (this.lastAudio) { - this.lastAudio.pause(); - this.lastAudio.currentTime = 0; - } + expandRing(); - this.lastAudio = audio; - audio.play(); - - // 발사 이펙트 - this.createMuzzleFlash(scene); - - // 포탄 생성 - const bullet = this.createBullet(scene); - if (bullet) { - this.ammo--; - this.updateAmmoDisplay(); - - // 탄약을 모두 소진했을 때만 재장전 시작 - if (this.ammo <= 0) { - this.startReload(); + // 폭발음 효과 + const explosionSound = new Audio('sounds/bang.ogg'); + explosionSound.volume = 0.4; + explosionSound.play(); + + // 카메라 흔들림 효과 + if (window.gameInstance && window.gameInstance.camera) { + const camera = window.gameInstance.camera; + const originalPosition = camera.position.clone(); + let shakeTime = 0; + const shakeIntensity = 0.3; + const shakeDuration = 500; + + const shakeCamera = () => { + if (shakeTime < shakeDuration) { + camera.position.x = originalPosition.x + (Math.random() - 0.5) * shakeIntensity; + camera.position.y = originalPosition.y + (Math.random() - 0.5) * shakeIntensity; + camera.position.z = originalPosition.z + (Math.random() - 0.5) * shakeIntensity; + + shakeTime += 16; + requestAnimationFrame(shakeCamera); + } else { + camera.position.copy(originalPosition); + } + }; + shakeCamera(); } - } - return bullet; -} - -startReload() { - if (this.isReloading) return; // 이미 재장전 중이면 무시 - - this.isReloading = true; - const reloadingText = document.getElementById('reloadingText'); - reloadingText.style.display = 'block'; - - setTimeout(() => { - this.ammo = this.maxAmmo; - this.isReloading = false; - reloadingText.style.display = 'none'; - this.updateAmmoDisplay(); - }, this.reloadTime); -} - - createMuzzleFlash(scene) { - if (!this.turret) return; - const flashGroup = new THREE.Group(); - - // 화염 크기 증가 - const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8); - const flameMaterial = new THREE.MeshBasicMaterial({ - color: 0xffa500, - transparent: true, - opacity: 0.8 - }); - const flame = new THREE.Mesh(flameGeometry, flameMaterial); - flame.scale.set(2, 2, 3); - flashGroup.add(flame); - - // 연기 효과 크기 증가 - const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8); - const smokeMaterial = new THREE.MeshBasicMaterial({ - color: 0x555555, - transparent: true, - opacity: 0.5 - }); - for (let i = 0; i < 5; i++) { // 연기 파티클 수 증가 - const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial); - smoke.position.set( - Math.random() * 1 - 0.5, - Math.random() * 1 - 0.5, - -1 - Math.random() - ); - smoke.scale.set(1.5, 1.5, 1.5); - flashGroup.add(smoke); + // 중심 플래시 제거 + setTimeout(() => { + scene.remove(flash); + }, 100); } - - // 포구 위치 계산 - const muzzleOffset = new THREE.Vector3(0, 0.5, 4); - const muzzlePosition = new THREE.Vector3(); - const turretWorldQuaternion = new THREE.Quaternion(); - - this.turret.getWorldPosition(muzzlePosition); - this.turret.getWorldQuaternion(turretWorldQuaternion); - muzzleOffset.applyQuaternion(turretWorldQuaternion); - muzzlePosition.add(muzzleOffset); - - flashGroup.position.copy(muzzlePosition); - flashGroup.quaternion.copy(turretWorldQuaternion); - - scene.add(flashGroup); - - // 이펙트 지속 시간 증가 - setTimeout(() => { - scene.remove(flashGroup); - }, 500); -} - - - - createBullet(scene) { - if (!this.turret) return null; // 터렛이 없으면 null 반환 - // 포탄 크기 증가 - const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8); - const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffd700 }); - const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); - - // 포탑의 월드 위치와 방향 가져오기 - const muzzleOffset = new THREE.Vector3(0, 0.5, 4); // 포구 위치 조정 (앞쪽으로 더 이동) - const muzzlePosition = new THREE.Vector3(); - const turretWorldQuaternion = new THREE.Quaternion(); - - // 포탑의 월드 변환 행렬 가져오기 - this.turret.getWorldPosition(muzzlePosition); - this.turret.getWorldQuaternion(turretWorldQuaternion); - - // 포구 오프셋 적용 - muzzleOffset.applyQuaternion(turretWorldQuaternion); - muzzlePosition.add(muzzleOffset); - - // 포탄 위치와 회전 설정 - bullet.position.copy(muzzlePosition); - bullet.quaternion.copy(turretWorldQuaternion); - - // 발사 방향 설정 - const direction = new THREE.Vector3(0, 0, 1); - direction.applyQuaternion(turretWorldQuaternion); - bullet.velocity = direction.multiplyScalar(5); - - scene.add(bullet); - this.bullets.push(bullet); - - return bullet; -} - - - update(mouseX, mouseY, scene) { - if (!this.body || !this.turretGroup) return; - - const absoluteTurretRotation = mouseX; - this.turretGroup.rotation.y = absoluteTurretRotation - this.body.rotation.y; - this.turretRotation = absoluteTurretRotation; - - // 총알 업데이트 및 충돌 체크 - for (let i = this.bullets.length - 1; i >= 0; i--) { - const bullet = this.bullets[i]; - const oldPosition = bullet.position.clone(); - bullet.position.add(bullet.velocity); - - // 지형 높이 체크 - const terrainHeight = window.gameInstance.getHeightAtPosition( - bullet.position.x, - bullet.position.z - ); - - if (bullet.position.y < terrainHeight || - Math.abs(bullet.position.x) > MAP_SIZE / 2 || - Math.abs(bullet.position.z) > MAP_SIZE / 2) { + async initialize(scene, loader) { + try { + const bodyResult = await loader.loadAsync('/models/abramsBody.glb'); + this.body = bodyResult.scene; - // 폭발 이펙트 생성 - this.createExplosionEffect(scene, bullet.position); + const turretResult = await loader.loadAsync('/models/abramsTurret.glb'); + this.turret = turretResult.scene; - scene.remove(bullet); - this.bullets.splice(i, 1); - } - } -} - - move(direction) { - if (!this.body) return; + 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; + child.material.shadowSide = THREE.BackSide; + child.material.needsUpdate = true; + } + }); + + this.turret.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + child.material.shadowSide = THREE.BackSide; + child.material.needsUpdate = true; + } + }); + + // 그림자 평면 + const shadowPlaneGeometry = new THREE.PlaneGeometry(8, 8); + const shadowPlaneMaterial = new THREE.ShadowMaterial({ + opacity: 0.3 + }); + this.shadowPlane = new THREE.Mesh(shadowPlaneGeometry, shadowPlaneMaterial); + this.shadowPlane.receiveShadow = true; + this.shadowPlane.rotation.x = -Math.PI / 2; + this.shadowPlane.position.y = 0.1; + this.body.add(this.shadowPlane); + + // 간단한 스폰 위치 설정 + const spawnPosition = new THREE.Vector3( + (Math.random() - 0.5) * (MAP_SIZE * 0.8), // MAP_SIZE의 80%만 사용 + TANK_HEIGHT, // 고정된 높이 사용 + (Math.random() - 0.5) * (MAP_SIZE * 0.8) + ); + + this.body.position.copy(spawnPosition); - const moveVector = new THREE.Vector3(); - moveVector.x = direction.x * this.moveSpeed; - moveVector.z = direction.z * this.moveSpeed; + // 폭발 이펙트 메서드 추가 + this.createExplosionEffect = (scene, position) => { + // 폭발 파티클 + for (let i = 0; i < 15; i++) { + const size = Math.random() * 0.2 + 0.1; + const geometry = new THREE.SphereGeometry(size); + const material = new THREE.MeshBasicMaterial({ + color: Math.random() < 0.5 ? 0xff4500 : 0xff8c00 + }); + const particle = new THREE.Mesh(geometry, material); + particle.position.copy(position); - // 새로운 위치 계산 - const newPosition = this.body.position.clone().add(moveVector); + const speed = Math.random() * 0.3 + 0.2; + const angle = Math.random() * Math.PI * 2; + const elevation = Math.random() * Math.PI - Math.PI / 2; - // 새로운 위치의 지형 높이 가져오기 - const heightAtNewPos = window.gameInstance.getHeightAtPosition(newPosition.x, newPosition.z); + particle.velocity = new THREE.Vector3( + Math.cos(angle) * Math.cos(elevation) * speed, + Math.sin(elevation) * speed, + Math.sin(angle) * Math.cos(elevation) * speed + ); - // 탱크 높이만큼 더해서 지형 위에 위치시킴 - newPosition.y = heightAtNewPos + TANK_HEIGHT; + particle.gravity = -0.01; + particle.life = Math.random() * 20 + 20; + particle.fadeRate = 1 / particle.life; - // 경사가 너무 가파른지 체크 - const currentHeight = this.body.position.y; - const heightDifference = Math.abs(newPosition.y - currentHeight); - const maxClimbAngle = 0.5; // 최대 등반 각도 + scene.add(particle); + window.gameInstance.particles.push({ + mesh: particle, + velocity: particle.velocity, + gravity: particle.gravity, + life: particle.life, + fadeRate: particle.fadeRate + }); + } - if (heightDifference / this.moveSpeed < maxClimbAngle) { - this.body.position.copy(newPosition); - - // 탱크의 방향 벡터 계산 - const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.body.quaternion); - const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.body.quaternion); - - // 탱크 주변의 높이 계산 - const frontHeight = window.gameInstance.getHeightAtPosition( - newPosition.x + forwardVector.x, - newPosition.z + forwardVector.z - ); - const backHeight = window.gameInstance.getHeightAtPosition( - newPosition.x - forwardVector.x, - newPosition.z - forwardVector.z - ); - const rightHeight = window.gameInstance.getHeightAtPosition( - newPosition.x + rightVector.x, - newPosition.z + rightVector.z - ); - const leftHeight = window.gameInstance.getHeightAtPosition( - newPosition.x - rightVector.x, - newPosition.z - rightVector.z - ); - - // 탱크의 기울기 계산 및 적용 - const pitch = Math.atan2(frontHeight - backHeight, 2); - const roll = Math.atan2(rightHeight - leftHeight, 2); - - // 기존 y축 회전은 유지하면서 새로운 기울기 적용 - const currentYRotation = this.body.rotation.y; - this.body.rotation.set(pitch, currentYRotation, roll); + // 충돌 사운드 재생 + const explosionSound = new Audio('sounds/explosion.ogg'); + explosionSound.volume = 0.3; + explosionSound.play(); + }; + + scene.add(this.body); + this.isLoaded = true; + this.updateAmmoDisplay(); + + } catch (error) { + console.error('Error loading tank models:', error); + this.isLoaded = false; + } } -} - - rotate(angle) { - if (!this.body) return; - - // y축 회전 적용 - this.body.rotation.y += angle * this.turnSpeed; - - // 현재 위치에서의 지형에 맞춰 탱크 기울기 조정 - const position = this.body.position; - const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.body.quaternion); - const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.body.quaternion); - - // 탱크 주변의 높이 계산 - const frontHeight = window.gameInstance.getHeightAtPosition( - position.x + forwardVector.x, - position.z + forwardVector.z - ); - const backHeight = window.gameInstance.getHeightAtPosition( - position.x - forwardVector.x, - position.z - forwardVector.z - ); - const rightHeight = window.gameInstance.getHeightAtPosition( - position.x + rightVector.x, - position.z + rightVector.z - ); - const leftHeight = window.gameInstance.getHeightAtPosition( - position.x - rightVector.x, - position.z - rightVector.z - ); - - // 탱크의 기울기 계산 및 적용 - const pitch = Math.atan2(frontHeight - backHeight, 2); - const roll = Math.atan2(rightHeight - leftHeight, 2); + + shoot(scene) { + // 재장전 중이거나 탄약이 없으면 발사하지 않음 + if (this.isReloading || this.ammo <= 0) { + return null; + } + + // 발사 딜레이 체크 (연속 발사 방지) + const currentTime = Date.now(); + if (currentTime - this.lastShootTime < 100) { // 100ms의 발사 딜레이 + return null; + } + this.lastShootTime = currentTime; + + // 발사음 효과 (한 번만 재생) + const sounds = ['sounds/mbtfire1.ogg', 'sounds/mbtfire2.ogg', 'sounds/mbtfire3.ogg', 'sounds/mbtfire4.ogg']; + const randomSound = sounds[Math.floor(Math.random() * sounds.length)]; + const audio = new Audio(randomSound); + audio.volume = 0.5; + + // 이전 오디오 인스턴스가 있다면 중지 + if (this.lastAudio) { + this.lastAudio.pause(); + this.lastAudio.currentTime = 0; + } - // 현재 y축 회전은 유지하면서 새로운 기울기 적용 - const currentYRotation = this.body.rotation.y; - this.body.rotation.set(pitch, currentYRotation, roll); - } + this.lastAudio = audio; + audio.play(); - getPosition() { - return this.body ? this.body.position : new THREE.Vector3(); - } + // 발사 이펙트 + this.createMuzzleFlash(scene); - takeDamage(damage) { - this.health -= damage; - - // 사망 여부 체크 - if (this.health <= 0) { - const deathSound = new Audio('sounds/bang.ogg'); - deathSound.play(); - return true; // 사망 + // 포탄 생성 + const bullet = this.createBullet(scene); + if (bullet) { + this.ammo--; + this.updateAmmoDisplay(); + + // 탄약을 모두 소진했을 때만 재장전 시작 + if (this.ammo <= 0) { + this.startReload(); + } + } + return bullet; } - return false; // 생존 -} + startReload() { + if (this.isReloading) return; // 이미 재장전 중이면 무시 + this.isReloading = true; const reloadingText = document.getElementById('reloadingText'); reloadingText.style.display = 'block'; + setTimeout(() => { this.ammo = this.maxAmmo; this.isReloading = false; @@ -568,385 +321,548 @@ startReload() { }, this.reloadTime); } - updateAmmoDisplay() { - document.getElementById('ammoDisplay').textContent = `APFSDS: ${this.ammo}/${this.maxAmmo}`; - } -} - -// 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.alternativePath = null; - this.pathFindingTimeout = 0; - this.lastPathUpdateTime = 0; - this.pathUpdateInterval = 3000; // 3초마다 경로 업데이트 - this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7; + createMuzzleFlash(scene) { + if (!this.turret) return; + const flashGroup = new THREE.Group(); - // AI 상태 관리 - this.aiState = { - mode: 'pursue', - lastStateChange: 0, - stateChangeCooldown: 3000, - lastVisibilityCheck: 0, - visibilityCheckInterval: 500, - canSeePlayer: false, - lastKnownPlayerPosition: null, - searchStartTime: null, - targetRotation: 0, - currentRotation: 0, - isAiming: false, - aimingTime: 0, - requiredAimTime: 1000 // 조준에 필요한 시간 - }; - - // 경로 탐색 및 회피 시스템 - this.pathfinding = { - currentPath: [], - pathUpdateInterval: 1000, - lastPathUpdate: 0, - isAvoidingObstacle: false, - avoidanceDirection: null, - obstacleCheckDistance: 10, - avoidanceTime: 0, - maxAvoidanceTime: 3000, // 최대 회피 시간 - sensorAngles: [-45, 0, 45], // 전방 감지 각도 - sensorDistance: 15 // 감지 거리 - }; - - // 전투 시스템 - this.combat = { - minEngagementRange: 30, - maxEngagementRange: 150, - optimalRange: 80, - aimThreshold: 0.1, // 조준 정확도 임계값 - lastShotAccuracy: 0, - consecutiveHits: 0, - maxConsecutiveHits: 3 - }; - } - // 장애물 감지 시스템 - detectObstacles() { - const obstacles = []; - const position = this.mesh.position.clone(); - position.y += 1; // 센서 높이 조정 - - this.pathfinding.sensorAngles.forEach(angle => { - const direction = new THREE.Vector3(0, 0, 1) - .applyQuaternion(this.mesh.quaternion) - .applyAxisAngle(new THREE.Vector3(0, 1, 0), angle * Math.PI / 180); - - const raycaster = new THREE.Raycaster(position, direction, 0, this.pathfinding.sensorDistance); - const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true); - - if (intersects.length > 0) { - obstacles.push({ - angle: angle, - distance: intersects[0].distance, - point: intersects[0].point - }); - } + // 화염 크기 증가 + const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8); + const flameMaterial = new THREE.MeshBasicMaterial({ + color: 0xffa500, + transparent: true, + opacity: 0.8 }); - - return obstacles; - } - - // 회피 방향 계산 - calculateAvoidanceDirection(obstacles) { - if (obstacles.length === 0) return null; - - // 모든 장애물의 방향을 고려하여 최적의 회피 방향 계산 - const avoidanceVector = new THREE.Vector3(); - obstacles.forEach(obstacle => { - const avoidDir = new THREE.Vector3() - .subVectors(this.mesh.position, obstacle.point) - .normalize() - .multiplyScalar(1 / obstacle.distance); // 거리에 반비례하는 가중치 - avoidanceVector.add(avoidDir); + const flame = new THREE.Mesh(flameGeometry, flameMaterial); + flame.scale.set(2, 2, 3); + flashGroup.add(flame); + + // 연기 효과 크기 증가 + const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8); + const smokeMaterial = new THREE.MeshBasicMaterial({ + color: 0x555555, + transparent: true, + opacity: 0.5 }); - - return avoidanceVector.normalize(); - } - - // 조준 시스템 - updateAiming(playerPosition) { - const targetDirection = new THREE.Vector3() - .subVectors(playerPosition, this.mesh.position) - .normalize(); - - // 목표 회전각 계산 - this.aiState.targetRotation = Math.atan2(targetDirection.x, targetDirection.z); - - // 현재 회전각 부드럽게 조정 - 선회 속도를 느리게 하기 위해 조정 - const rotationDiff = this.aiState.targetRotation - this.aiState.currentRotation; - let rotationStep = Math.sign(rotationDiff) * Math.min(Math.abs(rotationDiff), 0.05); // 기존 0.02에서 0.05로 수정 - this.aiState.currentRotation += rotationStep; - - // 메시 회전 적용 - this.mesh.rotation.y = this.aiState.currentRotation; - - // 조준 정확도 계산 - const aimAccuracy = 1 - Math.abs(rotationDiff) / Math.PI; - return aimAccuracy > this.combat.aimThreshold; -} - - // 전투 거리 관리 - maintainCombatDistance(playerPosition) { - const distanceToPlayer = this.mesh.position.distanceTo(playerPosition); - let moveDirection = new THREE.Vector3(); - - if (distanceToPlayer < this.combat.minEngagementRange) { - // 너무 가까우면 후진 - moveDirection.subVectors(this.mesh.position, playerPosition).normalize(); - } else if (distanceToPlayer > this.combat.maxEngagementRange) { - // 너무 멀면 전진 - moveDirection.subVectors(playerPosition, this.mesh.position).normalize(); - } else if (Math.abs(distanceToPlayer - this.combat.optimalRange) > 10) { - // 최적 거리로 조정 - const targetDistance = this.combat.optimalRange; - moveDirection.subVectors(playerPosition, this.mesh.position).normalize(); - if (distanceToPlayer > targetDistance) { - moveDirection.multiplyScalar(1); - } else { - moveDirection.multiplyScalar(-1); - } - } - - return moveDirection; - } - - // 발사 조건 확인 - canShoot(playerPosition) { - const distance = this.mesh.position.distanceTo(playerPosition); - const hasLineOfSight = this.checkLineOfSight(playerPosition); - const isAimed = this.updateAiming(playerPosition); - - return distance <= this.combat.maxEngagementRange && - distance >= this.combat.minEngagementRange && - hasLineOfSight && - isAimed; - } - - // 메인 업데이트 함수 - update(playerPosition) { - if (!this.mesh || !this.isLoaded) return; - - // AI 상태 업데이트 - this.updateAIState(playerPosition); - - // 장애물 감지 및 시야 체크 - const obstacles = this.detectObstacles(); - const currentTime = Date.now(); - const hasLineOfSight = this.checkLineOfSight(playerPosition); - const distanceToPlayer = this.mesh.position.distanceTo(playerPosition); - - // 경로 업데이트 주기 체크 - if (currentTime - this.lastPathUpdateTime > this.pathUpdateInterval) { - if (!hasLineOfSight) { - this.alternativePath = this.findAlternativePath(playerPosition); + + for (let i = 0; i < 5; i++) { // 연기 파티클 수 증가 + const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial); + smoke.position.set( + Math.random() * 1 - 0.5, + Math.random() * 1 - 0.5, + -1 - Math.random() + ); + smoke.scale.set(1.5, 1.5, 1.5); + flashGroup.add(smoke); } - this.lastPathUpdateTime = currentTime; - } - - // 장애물 회피 로직 - if (obstacles.length > 0 && !this.pathfinding.isAvoidingObstacle) { - this.pathfinding.isAvoidingObstacle = true; - this.pathfinding.avoidanceDirection = this.calculateAvoidanceDirection(obstacles); - this.pathfinding.avoidanceTime = 0; + + // 포구 위치 계산 + const muzzleOffset = new THREE.Vector3(0, 0.5, 4); + const muzzlePosition = new THREE.Vector3(); + const turretWorldQuaternion = new THREE.Quaternion(); + + this.turret.getWorldPosition(muzzlePosition); + this.turret.getWorldQuaternion(turretWorldQuaternion); + + muzzleOffset.applyQuaternion(turretWorldQuaternion); + muzzlePosition.add(muzzleOffset); + + flashGroup.position.copy(muzzlePosition); + flashGroup.quaternion.copy(turretWorldQuaternion); + + scene.add(flashGroup); + + // 이펙트 지속 시간 증가 + setTimeout(() => { + scene.remove(flashGroup); + }, 500); } - - // 이동 로직 실행 - if (this.pathfinding.isAvoidingObstacle) { - // 회피 동작 - this.pathfinding.avoidanceTime += 16; - if (this.pathfinding.avoidanceTime >= this.pathfinding.maxAvoidanceTime) { - this.pathfinding.isAvoidingObstacle = false; - } else { - const avoidMove = this.pathfinding.avoidanceDirection.multiplyScalar(this.moveSpeed); - this.mesh.position.add(avoidMove); - } - } else if (!hasLineOfSight) { - // 시야가 없을 때의 이동 - if (this.alternativePath) { - const pathDirection = new THREE.Vector3() - .subVectors(this.alternativePath, this.mesh.position) - .normalize(); - this.mesh.position.add(pathDirection.multiplyScalar(this.moveSpeed)); - - const targetRotation = Math.atan2(pathDirection.x, pathDirection.z); - this.mesh.rotation.y = this.smoothRotation(this.mesh.rotation.y, targetRotation, 0.1); - } - } else { - // 시야가 있을 때의 이동 - this.alternativePath = null; - - // AI 상태에 따른 이동 - switch (this.aiState.mode) { - case 'pursue': - if (distanceToPlayer > ENEMY_CONFIG.ATTACK_RANGE * 0.7) { - const moveDirection = new THREE.Vector3() - .subVectors(playerPosition, this.mesh.position) - .normalize(); - this.mesh.position.add(moveDirection.multiplyScalar(this.moveSpeed)); - } - break; - - case 'flank': - const flankPosition = this.calculateFlankPosition(playerPosition); - this.findPathToTarget(flankPosition); - this.moveAlongPath(); - break; - - case 'retreat': - if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE * 0.3) { - const retreatDirection = new THREE.Vector3() - .subVectors(this.mesh.position, playerPosition) - .normalize(); - this.mesh.position.add(retreatDirection.multiplyScalar(this.moveSpeed)); - } - break; - } - - // 플레이어 방향으로 회전 - const directionToPlayer = new THREE.Vector3() - .subVectors(playerPosition, this.mesh.position) - .normalize(); - const targetRotation = Math.atan2(directionToPlayer.x, directionToPlayer.z); - this.mesh.rotation.y = this.smoothRotation(this.mesh.rotation.y, targetRotation, 0.1); + + + + createBullet(scene) { + if (!this.turret) return null; // 터렛이 없으면 null 반환 + // 포탄 크기 증가 + const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8); + const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffd700 }); + const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); + + // 포탑의 월드 위치와 방향 가져오기 + const muzzleOffset = new THREE.Vector3(0, 0.5, 4); // 포구 위치 조정 (앞쪽으로 더 이동) + const muzzlePosition = new THREE.Vector3(); + const turretWorldQuaternion = new THREE.Quaternion(); + + // 포탑의 월드 변환 행렬 가져오기 + this.turret.getWorldPosition(muzzlePosition); + this.turret.getWorldQuaternion(turretWorldQuaternion); + + // 포구 오프셋 적용 + muzzleOffset.applyQuaternion(turretWorldQuaternion); + muzzlePosition.add(muzzleOffset); + + // 포탄 위치와 회전 설정 + bullet.position.copy(muzzlePosition); + bullet.quaternion.copy(turretWorldQuaternion); + + // 발사 방향 설정 + const direction = new THREE.Vector3(0, 0, 1); + direction.applyQuaternion(turretWorldQuaternion); + bullet.velocity = direction.multiplyScalar(5); + + scene.add(bullet); + this.bullets.push(bullet); + + return bullet; } - - // 전투 거리 조정 - const combatMove = this.maintainCombatDistance(playerPosition); - if (combatMove.length() > 0) { - this.mesh.position.add(combatMove.multiplyScalar(this.moveSpeed)); + + + update(mouseX, mouseY, scene) { + if (!this.body || !this.turretGroup) return; + + const absoluteTurretRotation = mouseX; + this.turretGroup.rotation.y = absoluteTurretRotation - this.body.rotation.y; + this.turretRotation = absoluteTurretRotation; + + // 총알 업데이트 및 충돌 체크 + for (let i = this.bullets.length - 1; i >= 0; i--) { + const bullet = this.bullets[i]; + const oldPosition = bullet.position.clone(); + bullet.position.add(bullet.velocity); + + // 지형 높이 체크 + const terrainHeight = window.gameInstance.getHeightAtPosition( + bullet.position.x, + bullet.position.z + ); + + if (bullet.position.y < terrainHeight || + Math.abs(bullet.position.x) > MAP_SIZE / 2 || + Math.abs(bullet.position.z) > MAP_SIZE / 2) { + + // 폭발 이펙트 생성 + this.createExplosionEffect(scene, bullet.position); + + scene.remove(bullet); + this.bullets.splice(i, 1); + } + } } - - // 공격 처리 - if (hasLineOfSight && distanceToPlayer <= ENEMY_CONFIG.ATTACK_RANGE && this.canShoot(playerPosition)) { - this.shoot(playerPosition); + + move(direction) { + if (!this.body) return; + + const moveVector = new THREE.Vector3(); + moveVector.x = direction.x * this.moveSpeed; + moveVector.z = direction.z * this.moveSpeed; + + // 새로운 위치 계산 + const newPosition = this.body.position.clone().add(moveVector); + + // 새로운 위치의 지형 높이 가져오기 + const heightAtNewPos = window.gameInstance.getHeightAtPosition(newPosition.x, newPosition.z); + + // 탱크 높이만큼 더해서 지형 위에 위치시킴 + newPosition.y = heightAtNewPos + TANK_HEIGHT; + + // 경사가 너무 가파른지 체크 + const currentHeight = this.body.position.y; + const heightDifference = Math.abs(newPosition.y - currentHeight); + const maxClimbAngle = 0.5; // 최대 등반 각도 + + if (heightDifference / this.moveSpeed < maxClimbAngle) { + this.body.position.copy(newPosition); + + // 탱크의 방향 벡터 계산 + const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.body.quaternion); + const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.body.quaternion); + + // 탱크 주변의 높이 계산 + const frontHeight = window.gameInstance.getHeightAtPosition( + newPosition.x + forwardVector.x, + newPosition.z + forwardVector.z + ); + const backHeight = window.gameInstance.getHeightAtPosition( + newPosition.x - forwardVector.x, + newPosition.z - forwardVector.z + ); + const rightHeight = window.gameInstance.getHeightAtPosition( + newPosition.x + rightVector.x, + newPosition.z + rightVector.z + ); + const leftHeight = window.gameInstance.getHeightAtPosition( + newPosition.x - rightVector.x, + newPosition.z - rightVector.z + ); + + // 탱크의 기울기 계산 및 적용 + const pitch = Math.atan2(frontHeight - backHeight, 2); + const roll = Math.atan2(rightHeight - leftHeight, 2); + + // 기존 y축 회전은 유지하면서 새로운 기울기 적용 + const currentYRotation = this.body.rotation.y; + this.body.rotation.set(pitch, currentYRotation, roll); + } } - - // 총알 업데이트 - this.updateBullets(); - - // 탱크 기울기 조정 - this.adjustTankTilt(); -} - -checkLineOfSight(targetPosition) { - if (!this.mesh) return false; - - const startPos = this.mesh.position.clone(); - startPos.y += 2; // 포탑 높이 - const direction = new THREE.Vector3() - .subVectors(targetPosition, startPos) - .normalize(); - const distance = startPos.distanceTo(targetPosition); - - const raycaster = new THREE.Raycaster(startPos, direction, 0, distance); - const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true); - - return intersects.length === 0; -} - -findAlternativePath(playerPosition) { - const currentPos = this.mesh.position.clone(); - const directionToPlayer = new THREE.Vector3() - .subVectors(playerPosition, currentPos) - .normalize(); - - // 좌우 90도 방향 계산 - const leftDirection = new THREE.Vector3() - .copy(directionToPlayer) - .applyAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2); - const rightDirection = new THREE.Vector3() - .copy(directionToPlayer) - .applyAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2); - - // 좌우 30미터 지점 확인 - const checkDistance = 30; - const leftPoint = currentPos.clone().add(leftDirection.multiplyScalar(checkDistance)); - const rightPoint = currentPos.clone().add(rightDirection.multiplyScalar(checkDistance)); - - // 각 방향의 장애물 체크 - const leftClear = this.checkPathClear(currentPos, leftPoint); - const rightClear = this.checkPathClear(currentPos, rightPoint); - - if (leftClear && rightClear) { - // 둘 다 가능하면 랜덤 선택 - return Math.random() < 0.5 ? leftPoint : rightPoint; - } else if (leftClear) { - return leftPoint; - } else if (rightClear) { - return rightPoint; + + rotate(angle) { + if (!this.body) return; + + // y축 회전 적용 + this.body.rotation.y += angle * this.turnSpeed; + + // 현재 위치에서의 지형에 맞춰 탱크 기울기 조정 + const position = this.body.position; + const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.body.quaternion); + const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.body.quaternion); + + // 탱크 주변의 높이 계산 + const frontHeight = window.gameInstance.getHeightAtPosition( + position.x + forwardVector.x, + position.z + forwardVector.z + ); + const backHeight = window.gameInstance.getHeightAtPosition( + position.x - forwardVector.x, + position.z - forwardVector.z + ); + const rightHeight = window.gameInstance.getHeightAtPosition( + position.x + rightVector.x, + position.z + rightVector.z + ); + const leftHeight = window.gameInstance.getHeightAtPosition( + position.x - rightVector.x, + position.z - rightVector.z + ); + + // 탱크의 기울기 계산 및 적용 + const pitch = Math.atan2(frontHeight - backHeight, 2); + const roll = Math.atan2(rightHeight - leftHeight, 2); + + // 현재 y축 회전은 유지하면서 새로운 기울기 적용 + const currentYRotation = this.body.rotation.y; + this.body.rotation.set(pitch, currentYRotation, roll); + } + + getPosition() { + return this.body ? this.body.position : new THREE.Vector3(); + } + + takeDamage(damage) { + this.health -= damage; + + // 사망 여부 체크 + if (this.health <= 0) { + const deathSound = new Audio('sounds/bang.ogg'); + deathSound.play(); + return true; // 사망 + } + return false; // 생존 + } + startReload() { + this.isReloading = true; + const reloadingText = document.getElementById('reloadingText'); + reloadingText.style.display = 'block'; + setTimeout(() => { + this.ammo = this.maxAmmo; + this.isReloading = false; + reloadingText.style.display = 'none'; + this.updateAmmoDisplay(); + }, this.reloadTime); + } + + updateAmmoDisplay() { + document.getElementById('ammoDisplay').textContent = `APFSDS: ${this.ammo}/${this.maxAmmo}`; + } } - - return null; -} - -checkPathClear(start, end) { - const direction = new THREE.Vector3().subVectors(end, start).normalize(); - const distance = start.distanceTo(end); - const raycaster = new THREE.Raycaster(start, direction, 0, distance); - const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true); - return intersects.length === 0; -} - - async initialize(loader) { - try { - const modelPath = this.type === 'tank' ? '/models/t90.glb' : '/models/t90.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); + + // 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.alternativePath = null; + this.pathFindingTimeout = 0; + this.lastPathUpdateTime = 0; + this.pathUpdateInterval = 3000; // 3초마다 경로 업데이트 + this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7; - this.mesh.traverse((child) => { - if (child.isMesh) { - child.castShadow = true; - child.receiveShadow = true; + // AI 상태 관리 + this.aiState = { + mode: 'pursue', + lastStateChange: 0, + stateChangeCooldown: 3000, + lastVisibilityCheck: 0, + visibilityCheckInterval: 500, + canSeePlayer: false, + lastKnownPlayerPosition: null, + searchStartTime: null, + targetRotation: 0, + currentRotation: 0, + isAiming: false, + aimingTime: 0, + requiredAimTime: 1000 // 조준에 필요한 시간 + }; + + // 경로 탐색 및 회피 시스템 + this.pathfinding = { + currentPath: [], + pathUpdateInterval: 1000, + lastPathUpdate: 0, + isAvoidingObstacle: false, + avoidanceDirection: null, + obstacleCheckDistance: 10, + avoidanceTime: 0, + maxAvoidanceTime: 3000, // 최대 회피 시간 + sensorAngles: [-45, 0, 45], // 전방 감지 각도 + sensorDistance: 15 // 감지 거리 + }; + + // 전투 시스템 + this.combat = { + minEngagementRange: 30, + maxEngagementRange: 150, + optimalRange: 80, + aimThreshold: 0.1, // 조준 정확도 임계값 + lastShotAccuracy: 0, + consecutiveHits: 0, + maxConsecutiveHits: 3 + }; + } + // 장애물 감지 시스템 + detectObstacles() { + const obstacles = []; + const position = this.mesh.position.clone(); + position.y += 1; // 센서 높이 조정 + + this.pathfinding.sensorAngles.forEach(angle => { + const direction = new THREE.Vector3(0, 0, 1) + .applyQuaternion(this.mesh.quaternion) + .applyAxisAngle(new THREE.Vector3(0, 1, 0), angle * Math.PI / 180); + + const raycaster = new THREE.Raycaster(position, direction, 0, this.pathfinding.sensorDistance); + const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true); + + if (intersects.length > 0) { + obstacles.push({ + angle: angle, + distance: intersects[0].distance, + point: intersects[0].point + }); } }); - - this.scene.add(this.mesh); - this.isLoaded = true; - } catch (error) { - console.error('Error loading enemy model:', error); - this.isLoaded = false; + + return obstacles; + } + + // 회피 방향 계산 + calculateAvoidanceDirection(obstacles) { + if (obstacles.length === 0) return null; + + // 모든 장애물의 방향을 고려하여 최적의 회피 방향 계산 + const avoidanceVector = new THREE.Vector3(); + obstacles.forEach(obstacle => { + const avoidDir = new THREE.Vector3() + .subVectors(this.mesh.position, obstacle.point) + .normalize() + .multiplyScalar(1 / obstacle.distance); // 거리에 반비례하는 가중치 + avoidanceVector.add(avoidDir); + }); + + return avoidanceVector.normalize(); + } + + // 조준 시스템 + updateAiming(playerPosition) { + const targetDirection = new THREE.Vector3() + .subVectors(playerPosition, this.mesh.position) + .normalize(); + + // 목표 회전각 계산 + this.aiState.targetRotation = Math.atan2(targetDirection.x, targetDirection.z); + + // 현재 회전각 부드럽게 조정 - 선회 속도를 느리게 하기 위해 조정 + const rotationDiff = this.aiState.targetRotation - this.aiState.currentRotation; + let rotationStep = Math.sign(rotationDiff) * Math.min(Math.abs(rotationDiff), 0.05); // 기존 0.02에서 0.05로 수정 + this.aiState.currentRotation += rotationStep; + + // 메시 회전 적용 + this.mesh.rotation.y = this.aiState.currentRotation; + + // 조준 정확도 계산 + const aimAccuracy = 1 - Math.abs(rotationDiff) / Math.PI; + return aimAccuracy > this.combat.aimThreshold; + } + + // 전투 거리 관리 + maintainCombatDistance(playerPosition) { + const distanceToPlayer = this.mesh.position.distanceTo(playerPosition); + let moveDirection = new THREE.Vector3(); + + if (distanceToPlayer < this.combat.minEngagementRange) { + // 너무 가까우면 후진 + moveDirection.subVectors(this.mesh.position, playerPosition).normalize(); + } else if (distanceToPlayer > this.combat.maxEngagementRange) { + // 너무 멀면 전진 + moveDirection.subVectors(playerPosition, this.mesh.position).normalize(); + } else if (Math.abs(distanceToPlayer - this.combat.optimalRange) > 10) { + // 최적 거리로 조정 + const targetDistance = this.combat.optimalRange; + moveDirection.subVectors(playerPosition, this.mesh.position).normalize(); + if (distanceToPlayer > targetDistance) { + moveDirection.multiplyScalar(1); + } else { + moveDirection.multiplyScalar(-1); + } + } + + return moveDirection; + } + + // 발사 조건 확인 + canShoot(playerPosition) { + const distance = this.mesh.position.distanceTo(playerPosition); + const hasLineOfSight = this.checkLineOfSight(playerPosition); + const isAimed = this.updateAiming(playerPosition); + + return distance <= this.combat.maxEngagementRange && + distance >= this.combat.minEngagementRange && + hasLineOfSight && + isAimed; + } + + // 메인 업데이트 함수 + update(playerPosition) { + if (!this.mesh || !this.isLoaded) return; + + // AI 상태 업데이트 + this.updateAIState(playerPosition); + + // 장애물 감지 및 시야 체크 + const obstacles = this.detectObstacles(); + const currentTime = Date.now(); + const hasLineOfSight = this.checkLineOfSight(playerPosition); + const distanceToPlayer = this.mesh.position.distanceTo(playerPosition); + + // 경로 업데이트 주기 체크 + if (currentTime - this.lastPathUpdateTime > this.pathUpdateInterval) { + if (!hasLineOfSight) { + this.alternativePath = this.findAlternativePath(playerPosition); + } + this.lastPathUpdateTime = currentTime; + } + + // 장애물 회피 로직 + if (obstacles.length > 0 && !this.pathfinding.isAvoidingObstacle) { + this.pathfinding.isAvoidingObstacle = true; + this.pathfinding.avoidanceDirection = this.calculateAvoidanceDirection(obstacles); + this.pathfinding.avoidanceTime = 0; + } + + // 이동 로직 실행 + if (this.pathfinding.isAvoidingObstacle) { + // 회피 동작 + this.pathfinding.avoidanceTime += 16; + if (this.pathfinding.avoidanceTime >= this.pathfinding.maxAvoidanceTime) { + this.pathfinding.isAvoidingObstacle = false; + } else { + const avoidMove = this.pathfinding.avoidanceDirection.multiplyScalar(this.moveSpeed); + this.mesh.position.add(avoidMove); + } + } else if (!hasLineOfSight) { + // 시야가 없을 때의 이동 + if (this.alternativePath) { + const pathDirection = new THREE.Vector3() + .subVectors(this.alternativePath, this.mesh.position) + .normalize(); + this.mesh.position.add(pathDirection.multiplyScalar(this.moveSpeed)); + + const targetRotation = Math.atan2(pathDirection.x, pathDirection.z); + this.mesh.rotation.y = this.smoothRotation(this.mesh.rotation.y, targetRotation, 0.1); + } + } else { + // 시야가 있을 때의 이동 + this.alternativePath = null; + + // AI 상태에 따른 이동 + switch (this.aiState.mode) { + case 'pursue': + if (distanceToPlayer > ENEMY_CONFIG.ATTACK_RANGE * 0.7) { + const moveDirection = new THREE.Vector3() + .subVectors(playerPosition, this.mesh.position) + .normalize(); + this.mesh.position.add(moveDirection.multiplyScalar(this.moveSpeed)); + } + break; + + case 'flank': + const flankPosition = this.calculateFlankPosition(playerPosition); + this.findPathToTarget(flankPosition); + this.moveAlongPath(); + break; + + case 'retreat': + if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE * 0.3) { + const retreatDirection = new THREE.Vector3() + .subVectors(this.mesh.position, playerPosition) + .normalize(); + this.mesh.position.add(retreatDirection.multiplyScalar(this.moveSpeed)); + } + break; + } + + // 플레이어 방향으로 회전 + const directionToPlayer = new THREE.Vector3() + .subVectors(playerPosition, this.mesh.position) + .normalize(); + const targetRotation = Math.atan2(directionToPlayer.x, directionToPlayer.z); + this.mesh.rotation.y = this.smoothRotation(this.mesh.rotation.y, targetRotation, 0.1); + } + + // 전투 거리 조정 + const combatMove = this.maintainCombatDistance(playerPosition); + if (combatMove.length() > 0) { + this.mesh.position.add(combatMove.multiplyScalar(this.moveSpeed)); + } + + // 공격 처리 + if (hasLineOfSight && distanceToPlayer <= ENEMY_CONFIG.ATTACK_RANGE && this.canShoot(playerPosition)) { + this.shoot(playerPosition); } + + // 총알 업데이트 + this.updateBullets(); + + // 탱크 기울기 조정 + this.adjustTankTilt(); } - - // 시야 확인 메서드 (기존 코드 수정) - checkLineOfSight(playerPosition) { + + checkLineOfSight(targetPosition) { if (!this.mesh) return false; - + const startPos = this.mesh.position.clone(); startPos.y += 2; // 포탑 높이 const direction = new THREE.Vector3() - .subVectors(playerPosition, startPos) + .subVectors(targetPosition, startPos) .normalize(); - const distance = startPos.distanceTo(playerPosition); - + const distance = startPos.distanceTo(targetPosition); + const raycaster = new THREE.Raycaster(startPos, direction, 0, distance); const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true); - - // 장애물과의 충돌이 있는지 확인 + return intersects.length === 0; } - // 대체 경로 찾기 메서드 + findAlternativePath(playerPosition) { const currentPos = this.mesh.position.clone(); const directionToPlayer = new THREE.Vector3() .subVectors(playerPosition, currentPos) .normalize(); - + // 좌우 90도 방향 계산 const leftDirection = new THREE.Vector3() .copy(directionToPlayer) @@ -954,16 +870,16 @@ checkPathClear(start, end) { const rightDirection = new THREE.Vector3() .copy(directionToPlayer) .applyAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2); - + // 좌우 30미터 지점 확인 const checkDistance = 30; const leftPoint = currentPos.clone().add(leftDirection.multiplyScalar(checkDistance)); const rightPoint = currentPos.clone().add(rightDirection.multiplyScalar(checkDistance)); - + // 각 방향의 장애물 체크 const leftClear = this.checkPathClear(currentPos, leftPoint); const rightClear = this.checkPathClear(currentPos, rightPoint); - + if (leftClear && rightClear) { // 둘 다 가능하면 랜덤 선택 return Math.random() < 0.5 ? leftPoint : rightPoint; @@ -972,10 +888,10 @@ checkPathClear(start, end) { } else if (rightClear) { return rightPoint; } - + return null; } - // 경로 유효성 확인 + checkPathClear(start, end) { const direction = new THREE.Vector3().subVectors(end, start).normalize(); const distance = start.distanceTo(end); @@ -983,1690 +899,1774 @@ checkPathClear(start, end) { const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true); return intersects.length === 0; } - - // 부드러운 회전 처리 - smoothRotation(current, target, factor) { - let delta = target - current; - - // 각도 차이를 -PI에서 PI 사이로 정규화 - while (delta > Math.PI) delta -= Math.PI * 2; - while (delta < -Math.PI) delta += Math.PI * 2; - - return current + delta * factor; - } - - - updateAIState(playerPosition) { - const currentTime = Date.now(); - const distanceToPlayer = this.mesh.position.distanceTo(playerPosition); - - if (currentTime - this.aiState.lastVisibilityCheck > this.aiState.visibilityCheckInterval) { - this.aiState.canSeePlayer = this.checkLineOfSight(playerPosition); - this.aiState.lastVisibilityCheck = currentTime; - - if (this.aiState.canSeePlayer) { - this.aiState.lastKnownPlayerPosition = playerPosition.clone(); - this.aiState.searchStartTime = null; + + async initialize(loader) { + try { + const modelPath = this.type === 'tank' ? '/models/t90.glb' : '/models/t90.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; } } - // 상태 변경 쿨다운을 2초로 설정 - const stateChangeCooldown = 2000; - - if (currentTime - this.aiState.lastStateChange > this.aiState.stateChangeCooldown) { - if (this.health < 30) { - this.aiState.mode = 'retreat'; - } else if (distanceToPlayer < 30 && this.aiState.canSeePlayer) { - this.aiState.mode = 'flank'; - } else { - this.aiState.mode = 'pursue'; - } - this.aiState.lastStateChange = currentTime; + + // 시야 확인 메서드 (기존 코드 수정) + checkLineOfSight(playerPosition) { + if (!this.mesh) return false; + + const startPos = this.mesh.position.clone(); + startPos.y += 2; // 포탑 높이 + const direction = new THREE.Vector3() + .subVectors(playerPosition, startPos) + .normalize(); + const distance = startPos.distanceTo(playerPosition); + + const raycaster = new THREE.Raycaster(startPos, direction, 0, distance); + const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true); + + // 장애물과의 충돌이 있는지 확인 + return intersects.length === 0; } - } - - findPathToTarget(targetPosition) { - const currentTime = Date.now(); - if (currentTime - this.pathfinding.lastPathUpdate < this.pathfinding.pathUpdateInterval) { - return; + // 대체 경로 찾기 메서드 + findAlternativePath(playerPosition) { + const currentPos = this.mesh.position.clone(); + const directionToPlayer = new THREE.Vector3() + .subVectors(playerPosition, currentPos) + .normalize(); + + // 좌우 90도 방향 계산 + const leftDirection = new THREE.Vector3() + .copy(directionToPlayer) + .applyAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2); + const rightDirection = new THREE.Vector3() + .copy(directionToPlayer) + .applyAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2); + + // 좌우 30미터 지점 확인 + const checkDistance = 30; + const leftPoint = currentPos.clone().add(leftDirection.multiplyScalar(checkDistance)); + const rightPoint = currentPos.clone().add(rightDirection.multiplyScalar(checkDistance)); + + // 각 방향의 장애물 체크 + const leftClear = this.checkPathClear(currentPos, leftPoint); + const rightClear = this.checkPathClear(currentPos, rightPoint); + + if (leftClear && rightClear) { + // 둘 다 가능하면 랜덤 선택 + return Math.random() < 0.5 ? leftPoint : rightPoint; + } else if (leftClear) { + return leftPoint; + } else if (rightClear) { + return rightPoint; + } + + return null; } - - this.pathfinding.currentPath = this.generatePathPoints(this.mesh.position.clone(), targetPosition); - this.pathfinding.lastPathUpdate = currentTime; - } - - generatePathPoints(start, end) { - const points = []; - const direction = new THREE.Vector3().subVectors(end, start).normalize(); - const distance = start.distanceTo(end); - const steps = Math.ceil(distance / 10); - - for (let i = 0; i <= steps; i++) { - const point = start.clone().add(direction.multiplyScalar(i * 10)); - points.push(point); + // 경로 유효성 확인 + checkPathClear(start, end) { + const direction = new THREE.Vector3().subVectors(end, start).normalize(); + const distance = start.distanceTo(end); + const raycaster = new THREE.Raycaster(start, direction, 0, distance); + const intersects = raycaster.intersectObjects(window.gameInstance.obstacles, true); + return intersects.length === 0; } - - return points; - } - - moveAlongPath() { - if (this.pathfinding.currentPath.length === 0) return; - - const targetPoint = this.pathfinding.currentPath[0]; - const direction = new THREE.Vector3() - .subVectors(targetPoint, this.mesh.position) - .normalize(); - - const moveVector = direction.multiplyScalar(this.moveSpeed); - this.mesh.position.add(moveVector); - - if (this.mesh.position.distanceTo(targetPoint) < 2) { - this.pathfinding.currentPath.shift(); + + // 부드러운 회전 처리 + smoothRotation(current, target, factor) { + let delta = target - current; + + // 각도 차이를 -PI에서 PI 사이로 정규화 + while (delta > Math.PI) delta -= Math.PI * 2; + while (delta < -Math.PI) delta += Math.PI * 2; + + return current + delta * factor; } - } - - calculateFlankPosition(playerPosition) { - const angle = Math.random() * Math.PI * 2; - const radius = 40; - return new THREE.Vector3( - playerPosition.x + Math.cos(angle) * radius, - playerPosition.y, - playerPosition.z + Math.sin(angle) * radius - ); - } - - calculateRetreatPosition(playerPosition) { - const direction = new THREE.Vector3() - .subVectors(this.mesh.position, playerPosition) - .normalize(); - return this.mesh.position.clone().add(direction.multiplyScalar(50)); - } - - adjustTankTilt() { - const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion); - const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.mesh.quaternion); - - const frontHeight = window.gameInstance.getHeightAtPosition( - this.mesh.position.x + forwardVector.x, - this.mesh.position.z + forwardVector.z - ); - const backHeight = window.gameInstance.getHeightAtPosition( - this.mesh.position.x - forwardVector.x, - this.mesh.position.z - forwardVector.z - ); - const rightHeight = window.gameInstance.getHeightAtPosition( - this.mesh.position.x + rightVector.x, - this.mesh.position.z + rightVector.z - ); - const leftHeight = window.gameInstance.getHeightAtPosition( - this.mesh.position.x - rightVector.x, - this.mesh.position.z - rightVector.z - ); - - const pitch = Math.atan2(frontHeight - backHeight, 2); - const roll = Math.atan2(rightHeight - leftHeight, 2); - - const currentRotation = this.mesh.rotation.y; - this.mesh.rotation.set(pitch, currentRotation, roll); - } - - updateBullets() { - 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) { - this.scene.remove(bullet); - this.bullets.splice(i, 1); - continue; + + + updateAIState(playerPosition) { + const currentTime = Date.now(); + const distanceToPlayer = this.mesh.position.distanceTo(playerPosition); + + if (currentTime - this.aiState.lastVisibilityCheck > this.aiState.visibilityCheckInterval) { + this.aiState.canSeePlayer = this.checkLineOfSight(playerPosition); + this.aiState.lastVisibilityCheck = currentTime; + + if (this.aiState.canSeePlayer) { + this.aiState.lastKnownPlayerPosition = playerPosition.clone(); + this.aiState.searchStartTime = null; + } + } + // 상태 변경 쿨다운을 2초로 설정 + const stateChangeCooldown = 2000; + + if (currentTime - this.aiState.lastStateChange > this.aiState.stateChangeCooldown) { + if (this.health < 30) { + this.aiState.mode = 'retreat'; + } else if (distanceToPlayer < 30 && this.aiState.canSeePlayer) { + this.aiState.mode = 'flank'; + } else { + this.aiState.mode = 'pursue'; + } + this.aiState.lastStateChange = currentTime; } + } + + findPathToTarget(targetPosition) { + const currentTime = Date.now(); + if (currentTime - this.pathfinding.lastPathUpdate < this.pathfinding.pathUpdateInterval) { + return; + } + + this.pathfinding.currentPath = this.generatePathPoints(this.mesh.position.clone(), targetPosition); + this.pathfinding.lastPathUpdate = currentTime; + } + + generatePathPoints(start, end) { + const points = []; + const direction = new THREE.Vector3().subVectors(end, start).normalize(); + const distance = start.distanceTo(end); + const steps = Math.ceil(distance / 10); + + for (let i = 0; i <= steps; i++) { + const point = start.clone().add(direction.multiplyScalar(i * 10)); + points.push(point); + } + + return points; + } + + moveAlongPath() { + if (this.pathfinding.currentPath.length === 0) return; + + const targetPoint = this.pathfinding.currentPath[0]; + const direction = new THREE.Vector3() + .subVectors(targetPoint, this.mesh.position) + .normalize(); + + const moveVector = direction.multiplyScalar(this.moveSpeed); + this.mesh.position.add(moveVector); + + if (this.mesh.position.distanceTo(targetPoint) < 2) { + this.pathfinding.currentPath.shift(); + } + } + + calculateFlankPosition(playerPosition) { + const angle = Math.random() * Math.PI * 2; + const radius = 40; + return new THREE.Vector3( + playerPosition.x + Math.cos(angle) * radius, + playerPosition.y, + playerPosition.z + Math.sin(angle) * radius + ); + } + + calculateRetreatPosition(playerPosition) { + const direction = new THREE.Vector3() + .subVectors(this.mesh.position, playerPosition) + .normalize(); + return this.mesh.position.clone().add(direction.multiplyScalar(50)); + } + + adjustTankTilt() { + const forwardVector = new THREE.Vector3(0, 0, 1).applyQuaternion(this.mesh.quaternion); + const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(this.mesh.quaternion); - const bulletBox = new THREE.Box3().setFromObject(bullet); - for (const obstacle of window.gameInstance.obstacles) { - const obstacleBox = new THREE.Box3().setFromObject(obstacle); - if (bulletBox.intersectsBox(obstacleBox)) { + const frontHeight = window.gameInstance.getHeightAtPosition( + this.mesh.position.x + forwardVector.x, + this.mesh.position.z + forwardVector.z + ); + const backHeight = window.gameInstance.getHeightAtPosition( + this.mesh.position.x - forwardVector.x, + this.mesh.position.z - forwardVector.z + ); + const rightHeight = window.gameInstance.getHeightAtPosition( + this.mesh.position.x + rightVector.x, + this.mesh.position.z + rightVector.z + ); + const leftHeight = window.gameInstance.getHeightAtPosition( + this.mesh.position.x - rightVector.x, + this.mesh.position.z - rightVector.z + ); + + const pitch = Math.atan2(frontHeight - backHeight, 2); + const roll = Math.atan2(rightHeight - leftHeight, 2); + + const currentRotation = this.mesh.rotation.y; + this.mesh.rotation.set(pitch, currentRotation, roll); + } + + updateBullets() { + 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) { this.scene.remove(bullet); this.bullets.splice(i, 1); - break; + continue; + } + + const bulletBox = new THREE.Box3().setFromObject(bullet); + for (const obstacle of window.gameInstance.obstacles) { + const obstacleBox = new THREE.Box3().setFromObject(obstacle); + if (bulletBox.intersectsBox(obstacleBox)) { + this.scene.remove(bullet); + this.bullets.splice(i, 1); + break; + } } } } - } - - createMuzzleFlash() { - if (!this.mesh) return; - - const flashGroup = new THREE.Group(); - - const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8); - const flameMaterial = new THREE.MeshBasicMaterial({ - color: 0xffa500, - transparent: true, - opacity: 0.8 - }); - const flame = new THREE.Mesh(flameGeometry, flameMaterial); - flame.scale.set(2, 2, 3); - flashGroup.add(flame); - - const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8); - const smokeMaterial = new THREE.MeshBasicMaterial({ - color: 0x555555, + + createMuzzleFlash() { + if (!this.mesh) return; + + const flashGroup = new THREE.Group(); + + const flameGeometry = new THREE.SphereGeometry(1.0, 8, 8); + const flameMaterial = new THREE.MeshBasicMaterial({ + color: 0xffa500, + transparent: true, + opacity: 0.8 + }); + const flame = new THREE.Mesh(flameGeometry, flameMaterial); + flame.scale.set(2, 2, 3); + flashGroup.add(flame); + + const smokeGeometry = new THREE.SphereGeometry(0.8, 8, 8); + const smokeMaterial = new THREE.MeshBasicMaterial({ + color: 0x555555, + transparent: true, + opacity: 0.5 + }); + + for (let i = 0; i < 5; i++) { + const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial); + smoke.position.set( + Math.random() * 1 - 0.5, + Math.random() * 1 - 0.5, + -1 - Math.random() + ); + smoke.scale.set(1.5, 1.5, 1.5); + flashGroup.add(smoke); + } + + const muzzleOffset = new THREE.Vector3(0, 0.5, 4); + const muzzlePosition = new THREE.Vector3(); + const meshWorldQuaternion = new THREE.Quaternion(); + + this.mesh.getWorldPosition(muzzlePosition); + this.mesh.getWorldQuaternion(meshWorldQuaternion); + + muzzleOffset.applyQuaternion(meshWorldQuaternion); + muzzlePosition.add(muzzleOffset); + + flashGroup.position.copy(muzzlePosition); + flashGroup.quaternion.copy(meshWorldQuaternion); + + this.scene.add(flashGroup); + + setTimeout(() => { + this.scene.remove(flashGroup); + }, 500); + } + createMassiveExplosion() { + // 중심 폭발 + const explosionGroup = new THREE.Group(); + + // 주 폭발 플래시 (크기 감소) + const flashGeometry = new THREE.SphereGeometry(4); // 8에서 4로 감소 + const flashMaterial = new THREE.MeshBasicMaterial({ + color: 0xffff00, transparent: true, - opacity: 0.5 + opacity: 1 }); - - for (let i = 0; i < 5; i++) { - const smoke = new THREE.Mesh(smokeGeometry, smokeMaterial); - smoke.position.set( - Math.random() * 1 - 0.5, - Math.random() * 1 - 0.5, - -1 - Math.random() + const flash = new THREE.Mesh(flashGeometry, flashMaterial); + flash.position.copy(this.mesh.position); + this.scene.add(flash); + + // 다중 폭발 파티클 (수와 크기 감소) + for (let i = 0; i < 50; i++) { // 100에서 50으로 감소 + const size = Math.random() * 1 + 0.5; // 크기 범위 감소 + const geometry = new THREE.SphereGeometry(size); + + // 다양한 폭발 색상 + const colors = [0xff4500, 0xff8c00, 0xff0000, 0xffd700]; + const material = new THREE.MeshBasicMaterial({ + color: colors[Math.floor(Math.random() * colors.length)], + transparent: true, + opacity: 1 + }); + + const particle = new THREE.Mesh(geometry, material); + particle.position.copy(this.mesh.position); + + // 파티클 속도 감소 + const speed = Math.random() * 1 + 0.5; // 속도 범위 감소 + const angle = Math.random() * Math.PI * 2; + const elevation = Math.random() * Math.PI - Math.PI / 2; + + particle.velocity = new THREE.Vector3( + Math.cos(angle) * Math.cos(elevation) * speed, + Math.sin(elevation) * speed, + Math.sin(angle) * Math.cos(elevation) * speed ); - smoke.scale.set(1.5, 1.5, 1.5); - flashGroup.add(smoke); + + particle.gravity = -0.05; // 중력 효과 감소 + particle.life = Math.random() * 30 + 30; // 수명 감소 + particle.fadeRate = 0.98; + + this.scene.add(particle); + window.gameInstance.particles.push({ + mesh: particle, + velocity: particle.velocity, + gravity: particle.gravity, + life: particle.life, + fadeRate: particle.fadeRate + }); } - + + // 폭발 링 이펙트 (크기와 확장 속도 감소) + for (let i = 0; i < 2; i++) { // 3에서 2로 감소 + const ringGeometry = new THREE.RingGeometry(0.1, 2, 32); + const ringMaterial = new THREE.MeshBasicMaterial({ + color: 0xff8c00, + transparent: true, + opacity: 1, + side: THREE.DoubleSide + }); + const ring = new THREE.Mesh(ringGeometry, ringMaterial); + ring.position.copy(this.mesh.position); + ring.rotation.x = Math.random() * Math.PI; + ring.rotation.y = Math.random() * Math.PI; + this.scene.add(ring); + + // 링 확장 애니메이션 (확장 속도 감소) + const expandRing = () => { + ring.scale.x += 0.15; // 0.3에서 0.15로 감소 + ring.scale.y += 0.15; + ring.material.opacity *= 0.96; + + if (ring.material.opacity > 0.01) { + requestAnimationFrame(expandRing); + } else { + this.scene.remove(ring); + } + }; + expandRing(); + } + + // 화염 기둥 효과 (크기와 수 감소) + const fireColumn = new THREE.Group(); + for (let i = 0; i < 10; i++) { // 20에서 10으로 감소 + const fireGeometry = new THREE.ConeGeometry(1, 4, 8); // 크기 감소 + const fireMaterial = new THREE.MeshBasicMaterial({ + color: 0xff4500, + transparent: true, + opacity: 0.8 + }); + const fire = new THREE.Mesh(fireGeometry, fireMaterial); + fire.position.y = i * 0.3; // 0.5에서 0.3으로 감소 + fire.rotation.x = Math.random() * Math.PI; + fire.rotation.z = Math.random() * Math.PI; + fireColumn.add(fire); + } + fireColumn.position.copy(this.mesh.position); + this.scene.add(fireColumn); + + // 화염 기둥 애니메이션 (확장 속도 감소) + const animateFireColumn = () => { + fireColumn.scale.y += 0.05; // 0.1에서 0.05로 감소 + fireColumn.children.forEach(fire => { + fire.material.opacity *= 0.95; + }); + + if (fireColumn.children[0].material.opacity > 0.01) { + requestAnimationFrame(animateFireColumn); + } else { + this.scene.remove(fireColumn); + } + }; + animateFireColumn(); + + // 폭발 사운드 효과 + const explosionSounds = [ + new Audio('sounds/explosion.ogg'), + new Audio('sounds/bang.ogg') + ]; + explosionSounds.forEach(sound => { + sound.volume = 0.5; + sound.play(); + }); + + // 카메라 흔들림 효과 (강도 감소) + if (window.gameInstance && window.gameInstance.camera) { + const camera = window.gameInstance.camera; + const originalPosition = camera.position.clone(); + let shakeTime = 0; + const shakeIntensity = 0.5; // 1.0에서 0.5로 감소 + const shakeDuration = 500; // 1000에서 500으로 감소 + + const shakeCamera = () => { + if (shakeTime < shakeDuration) { + camera.position.x = originalPosition.x + (Math.random() - 0.5) * shakeIntensity; + camera.position.y = originalPosition.y + (Math.random() - 0.5) * shakeIntensity; + camera.position.z = originalPosition.z + (Math.random() - 0.5) * shakeIntensity; + + shakeTime += 16; + requestAnimationFrame(shakeCamera); + } else { + camera.position.copy(originalPosition); + } + }; + shakeCamera(); + } + + // 중심 플래시 제거 + setTimeout(() => { + this.scene.remove(flash); + }, 100); // 200에서 100으로 감소 + } + + + 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 directionToPlayer = new THREE.Vector3() + .subVectors(playerPosition, this.mesh.position) + .normalize(); + const forwardDirection = new THREE.Vector3(0, 0, 1) + .applyQuaternion(this.mesh.quaternion) + .normalize(); + + const dotProduct = forwardDirection.dot(directionToPlayer); + const angleToPlayer = Math.acos(dotProduct); + + // 일정 각도 이하일 경우에만 공격 + const attackAngleThreshold = Math.PI / 8; // 약 22.5도 + if (angleToPlayer > attackAngleThreshold) return; + + this.createMuzzleFlash(); + + const enemyFireSound = new Audio('sounds/mbtfire5.ogg'); + enemyFireSound.volume = 0.3; + enemyFireSound.play(); + + const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8); + const bulletMaterial = new THREE.MeshBasicMaterial({ + color: 0xff0000, + emissive: 0xff0000, + emissiveIntensity: 0.5 + }); + const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); + const muzzleOffset = new THREE.Vector3(0, 0.5, 4); const muzzlePosition = new THREE.Vector3(); - const meshWorldQuaternion = new THREE.Quaternion(); - this.mesh.getWorldPosition(muzzlePosition); - this.mesh.getWorldQuaternion(meshWorldQuaternion); - - muzzleOffset.applyQuaternion(meshWorldQuaternion); + muzzleOffset.applyQuaternion(this.mesh.quaternion); muzzlePosition.add(muzzleOffset); - - flashGroup.position.copy(muzzlePosition); - flashGroup.quaternion.copy(meshWorldQuaternion); - - this.scene.add(flashGroup); - - setTimeout(() => { - this.scene.remove(flashGroup); - }, 500); - } - createMassiveExplosion() { - // 중심 폭발 - const explosionGroup = new THREE.Group(); - - // 주 폭발 플래시 (크기 감소) - const flashGeometry = new THREE.SphereGeometry(4); // 8에서 4로 감소 - const flashMaterial = new THREE.MeshBasicMaterial({ - color: 0xffff00, - transparent: true, - opacity: 1 - }); - const flash = new THREE.Mesh(flashGeometry, flashMaterial); - flash.position.copy(this.mesh.position); - this.scene.add(flash); - - // 다중 폭발 파티클 (수와 크기 감소) - for (let i = 0; i < 50; i++) { // 100에서 50으로 감소 - const size = Math.random() * 1 + 0.5; // 크기 범위 감소 - const geometry = new THREE.SphereGeometry(size); - // 다양한 폭발 색상 - const colors = [0xff4500, 0xff8c00, 0xff0000, 0xffd700]; - const material = new THREE.MeshBasicMaterial({ - color: colors[Math.floor(Math.random() * colors.length)], - transparent: true, - opacity: 1 - }); + bullet.position.copy(muzzlePosition); + bullet.quaternion.copy(this.mesh.quaternion); - const particle = new THREE.Mesh(geometry, material); - particle.position.copy(this.mesh.position); - - // 파티클 속도 감소 - const speed = Math.random() * 1 + 0.5; // 속도 범위 감소 - const angle = Math.random() * Math.PI * 2; - const elevation = Math.random() * Math.PI - Math.PI / 2; - - particle.velocity = new THREE.Vector3( - Math.cos(angle) * Math.cos(elevation) * speed, - Math.sin(elevation) * speed, - Math.sin(angle) * Math.cos(elevation) * speed - ); - - particle.gravity = -0.05; // 중력 효과 감소 - particle.life = Math.random() * 30 + 30; // 수명 감소 - particle.fadeRate = 0.98; - - this.scene.add(particle); - window.gameInstance.particles.push({ - mesh: particle, - velocity: particle.velocity, - gravity: particle.gravity, - life: particle.life, - fadeRate: particle.fadeRate - }); - } - - // 폭발 링 이펙트 (크기와 확장 속도 감소) - for (let i = 0; i < 2; i++) { // 3에서 2로 감소 - const ringGeometry = new THREE.RingGeometry(0.1, 2, 32); - const ringMaterial = new THREE.MeshBasicMaterial({ - color: 0xff8c00, - transparent: true, - opacity: 1, - side: THREE.DoubleSide - }); - const ring = new THREE.Mesh(ringGeometry, ringMaterial); - ring.position.copy(this.mesh.position); - ring.rotation.x = Math.random() * Math.PI; - ring.rotation.y = Math.random() * Math.PI; - this.scene.add(ring); - - // 링 확장 애니메이션 (확장 속도 감소) - const expandRing = () => { - ring.scale.x += 0.15; // 0.3에서 0.15로 감소 - ring.scale.y += 0.15; - ring.material.opacity *= 0.96; - - if (ring.material.opacity > 0.01) { - requestAnimationFrame(expandRing); - } else { - this.scene.remove(ring); - } - }; - expandRing(); - } - - // 화염 기둥 효과 (크기와 수 감소) - const fireColumn = new THREE.Group(); - for (let i = 0; i < 10; i++) { // 20에서 10으로 감소 - const fireGeometry = new THREE.ConeGeometry(1, 4, 8); // 크기 감소 - const fireMaterial = new THREE.MeshBasicMaterial({ - color: 0xff4500, + const direction = new THREE.Vector3() + .subVectors(playerPosition, muzzlePosition) + .normalize(); + + const bulletSpeed = this.type === 'tank' ? + ENEMY_CONFIG.BULLET_SPEED : + ENEMY_CONFIG.BULLET_SPEED * 0.8; + + bullet.velocity = direction.multiplyScalar(bulletSpeed); + + const trailGeometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8); + const trailMaterial = new THREE.MeshBasicMaterial({ + color: 0xff4444, transparent: true, - opacity: 0.8 + opacity: 0.5 }); - const fire = new THREE.Mesh(fireGeometry, fireMaterial); - fire.position.y = i * 0.3; // 0.5에서 0.3으로 감소 - fire.rotation.x = Math.random() * Math.PI; - fire.rotation.z = Math.random() * Math.PI; - fireColumn.add(fire); + + const trail = new THREE.Mesh(trailGeometry, trailMaterial); + trail.position.z = -1; + bullet.add(trail); + + this.scene.add(bullet); + this.bullets.push(bullet); + this.lastAttackTime = currentTime; } - fireColumn.position.copy(this.mesh.position); - this.scene.add(fireColumn); - - // 화염 기둥 애니메이션 (확장 속도 감소) - const animateFireColumn = () => { - fireColumn.scale.y += 0.05; // 0.1에서 0.05로 감소 - fireColumn.children.forEach(fire => { - fire.material.opacity *= 0.95; - }); - - if (fireColumn.children[0].material.opacity > 0.01) { - requestAnimationFrame(animateFireColumn); - } else { - this.scene.remove(fireColumn); + + takeDamage(damage) { + this.health -= damage; + + // 사망 여부 체크 + if (this.health <= 0) { + const deathSound = new Audio('sounds/bang.ogg'); + deathSound.play(); + return true; // 사망 } - }; - animateFireColumn(); - - // 폭발 사운드 효과 - const explosionSounds = [ - new Audio('sounds/explosion.ogg'), - new Audio('sounds/bang.ogg') - ]; - explosionSounds.forEach(sound => { - sound.volume = 0.5; - sound.play(); - }); - - // 카메라 흔들림 효과 (강도 감소) - if (window.gameInstance && window.gameInstance.camera) { - const camera = window.gameInstance.camera; - const originalPosition = camera.position.clone(); - let shakeTime = 0; - const shakeIntensity = 0.5; // 1.0에서 0.5로 감소 - const shakeDuration = 500; // 1000에서 500으로 감소 - - const shakeCamera = () => { - if (shakeTime < shakeDuration) { - camera.position.x = originalPosition.x + (Math.random() - 0.5) * shakeIntensity; - camera.position.y = originalPosition.y + (Math.random() - 0.5) * shakeIntensity; - camera.position.z = originalPosition.z + (Math.random() - 0.5) * shakeIntensity; - - shakeTime += 16; - requestAnimationFrame(shakeCamera); - } else { - camera.position.copy(originalPosition); + return false; // 생존 + } + destroy() { + if (this.mesh) { + this.scene.remove(this.mesh); + this.bullets.forEach(bullet => this.scene.remove(bullet)); + this.bullets = []; + this.isLoaded = false; } - }; - shakeCamera(); + } } - - // 중심 플래시 제거 - setTimeout(() => { - this.scene.remove(flash); - }, 100); // 200에서 100으로 감소 -} - - - 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 directionToPlayer = new THREE.Vector3() - .subVectors(playerPosition, this.mesh.position) - .normalize(); - const forwardDirection = new THREE.Vector3(0, 0, 1) - .applyQuaternion(this.mesh.quaternion) - .normalize(); - - const dotProduct = forwardDirection.dot(directionToPlayer); - const angleToPlayer = Math.acos(dotProduct); - - // 일정 각도 이하일 경우에만 공격 - const attackAngleThreshold = Math.PI / 8; // 약 22.5도 - if (angleToPlayer > attackAngleThreshold) return; - - this.createMuzzleFlash(); - - const enemyFireSound = new Audio('sounds/mbtfire5.ogg'); - enemyFireSound.volume = 0.3; - enemyFireSound.play(); - - const bulletGeometry = new THREE.CylinderGeometry(0.2, 0.2, 2, 8); - const bulletMaterial = new THREE.MeshBasicMaterial({ - color: 0xff0000, - emissive: 0xff0000, - emissiveIntensity: 0.5 - }); - const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); - - const muzzleOffset = new THREE.Vector3(0, 0.5, 4); - const muzzlePosition = new THREE.Vector3(); - this.mesh.getWorldPosition(muzzlePosition); - muzzleOffset.applyQuaternion(this.mesh.quaternion); - muzzlePosition.add(muzzleOffset); - - bullet.position.copy(muzzlePosition); - bullet.quaternion.copy(this.mesh.quaternion); - - const direction = new THREE.Vector3() - .subVectors(playerPosition, muzzlePosition) - .normalize(); - - const bulletSpeed = this.type === 'tank' ? - ENEMY_CONFIG.BULLET_SPEED : - ENEMY_CONFIG.BULLET_SPEED * 0.8; - - bullet.velocity = direction.multiplyScalar(bulletSpeed); - - const trailGeometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8); - const trailMaterial = new THREE.MeshBasicMaterial({ - color: 0xff4444, - transparent: true, - opacity: 0.5 - }); - const trail = new THREE.Mesh(trailGeometry, trailMaterial); - trail.position.z = -1; - bullet.add(trail); + // 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); + } - this.scene.add(bullet); - this.bullets.push(bullet); - this.lastAttackTime = currentTime; -} - - takeDamage(damage) { - this.health -= damage; - - // 사망 여부 체크 - if (this.health <= 0) { - const deathSound = new Audio('sounds/bang.ogg'); - deathSound.play(); - return true; // 사망 - } - return false; // 생존 -} - destroy() { - if (this.mesh) { - this.scene.remove(this.mesh); - this.bullets.forEach(bullet => this.scene.remove(bullet)); - this.bullets = []; - this.isLoaded = false; + 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); } } -} - -// 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.isStarted = false; - // 오디오 관련 속성 추가 - this.bgmPlaying = false; // BGM 재생 상태 추적 - this.bgm = null; // BGM 오디오 객체 저장 - this.engineSound = null; - this.engineStopSound = null; - this.isEngineRunning = false; - 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; - this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 부드러운 그림자 - this.renderer.outputColorSpace = THREE.SRGBColorSpace; - this.enemyLabels = new Map(); // 적 라벨을 추적하기 위한 Map 추가 - this.raycaster = new THREE.Raycaster(); - this.crosshair = document.getElementById('crosshair'); - document.getElementById('gameContainer').appendChild(this.renderer.domElement); - - - // 레이더 관련 속성 추가 - this.radarUpdateInterval = 100; // 100ms마다 레이더 업데이트 - this.lastRadarUpdate = 0; - this.radarRange = 200; // 레이더 감지 범위 - - 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.lastAudio = null; // 마지막 발사음 추적을 위한 속성 추가 - - this.mouse = { x: 0, y: 0 }; - this.keys = { - forward: false, - backward: false, - left: false, - right: false - }; - - this.setupEventListeners(); - this.initialize(); - this.obstacles = []; // obstacles 배열 추가 - } - setupScene() { - // 씬 초기화 - this.scene.background = new THREE.Color(0x87CEEB); // 하늘색 배경 - - // 안개 설정 - this.scene.fog = new THREE.FogExp2(0x87CEEB, 0.0008); - - // 렌더러 설정 - this.renderer.shadowMap.enabled = true; - this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; - this.renderer.setPixelRatio(window.devicePixelRatio); - - // 카메라 초기 설정 - this.camera.position.set(0, 15, -30); - this.camera.lookAt(0, 0, 0); + + // Game 클래스 + class Game { + constructor() { + // 게임 시작 여부를 추적하는 플래그 추가 + this.isStarted = false; + // 오디오 관련 속성 추가 + this.bgmPlaying = false; // BGM 재생 상태 추적 + this.bgm = null; // BGM 오디오 객체 저장 + this.engineSound = null; + this.engineStopSound = null; + this.isEngineRunning = false; + 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; + this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 부드러운 그림자 + this.renderer.outputColorSpace = THREE.SRGBColorSpace; + this.enemyLabels = new Map(); // 적 라벨을 추적하기 위한 Map 추가 + this.raycaster = new THREE.Raycaster(); + this.crosshair = document.getElementById('crosshair'); + document.getElementById('gameContainer').appendChild(this.renderer.domElement); + + + // 레이더 관련 속성 추가 + this.radarUpdateInterval = 100; // 100ms마다 레이더 업데이트 + this.lastRadarUpdate = 0; + this.radarRange = 200; // 레이더 감지 범위 + + 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.lastAudio = null; // 마지막 발사음 추적을 위한 속성 추가 + + this.mouse = { x: 0, y: 0 }; + this.keys = { + forward: false, + backward: false, + left: false, + right: false + }; + + this.setupEventListeners(); + this.initialize(); + this.obstacles = []; // obstacles 배열 추가 + } + setupScene() { + // 씬 초기화 + this.scene.background = new THREE.Color(0x87CEEB); // 하늘색 배경 + + // 안개 설정 + this.scene.fog = new THREE.FogExp2(0x87CEEB, 0.0008); + + // 렌더러 설정 + this.renderer.shadowMap.enabled = true; + this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; + this.renderer.setPixelRatio(window.devicePixelRatio); + + // 카메라 초기 설정 + this.camera.position.set(0, 15, -30); + this.camera.lookAt(0, 0, 0); + + // 씬 경계 설정 + const mapBoundary = MAP_SIZE / 2; + this.sceneBounds = { + minX: -mapBoundary, + maxX: mapBoundary, + minZ: -mapBoundary, + maxZ: mapBoundary + }; + } - // 씬 경계 설정 - const mapBoundary = MAP_SIZE / 2; - this.sceneBounds = { - minX: -mapBoundary, - maxX: mapBoundary, - minZ: -mapBoundary, - maxZ: mapBoundary - }; - } + async initialize() { + try { + // BGM이 아직 재생되지 않은 경우에만 재생 + if (!this.bgmPlaying && !this.bgm) { + this.bgm = new Audio('sounds/BGM.ogg'); + this.bgm.volume = 0.5; + this.bgm.loop = true; + this.bgm.play(); + this.bgmPlaying = true; + } + + // 시작 사운드 재생 + const startSounds = ['sounds/start1.ogg', 'sounds/start2.ogg', 'sounds/start3.ogg']; + const randomStartSound = startSounds[Math.floor(Math.random() * startSounds.length)]; + const startAudio = new Audio(randomStartSound); + startAudio.volume = 0.5; + startAudio.play(); + + // 렌더러 설정 + this.renderer.shadowMap.enabled = true; + this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; + this.renderer.outputColorSpace = THREE.SRGBColorSpace; + + // 기본 씬 설정 + this.setupScene(); + + // 안개 효과 + this.scene.fog = new THREE.FogExp2(0x87CEEB, 0.0008); + this.scene.background = new THREE.Color(0x87CEEB); + + // 조명 설정 + // 주변광 + const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); + this.scene.add(ambientLight); + + // 메인 태양광 + const mainLight = new THREE.DirectionalLight(0xffffff, 1.0); + mainLight.position.set(MAP_SIZE/2, MAP_SIZE/2, MAP_SIZE/2); + mainLight.castShadow = true; + + // 그림자 설정 + mainLight.shadow.mapSize.width = 4096; + mainLight.shadow.mapSize.height = 4096; + mainLight.shadow.camera.near = 0.5; + mainLight.shadow.camera.far = MAP_SIZE * 2; + mainLight.shadow.camera.left = -MAP_SIZE; + mainLight.shadow.camera.right = MAP_SIZE; + mainLight.shadow.camera.top = MAP_SIZE; + mainLight.shadow.camera.bottom = -MAP_SIZE; + mainLight.shadow.bias = -0.001; + mainLight.shadow.radius = 2; + mainLight.shadow.normalBias = 0.02; + + this.scene.add(mainLight); + + // 보조 태양광 + const secondaryLight = new THREE.DirectionalLight(0xffffff, 0.3); + secondaryLight.position.set(-50, 50, -50); + this.scene.add(secondaryLight); + + // 환경광 + const hemisphereLight = new THREE.HemisphereLight( + 0x87CEEB, + 0xFFE87C, + 0.3 + ); + this.scene.add(hemisphereLight); + // 지형 생성 + const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100); + const groundMaterial = new THREE.MeshStandardMaterial({ + color: 0xD2B48C, + roughness: 0.8, + metalness: 0.2, + envMapIntensity: 1.0 + }); + + const ground = new THREE.Mesh(groundGeometry, groundMaterial); + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + + // 지형 높이 설정 + const vertices = ground.geometry.attributes.position.array; + for (let i = 0; i < vertices.length; i += 3) { + vertices[i + 2] = 0; // 모든 높이를 0으로 설정 + } + + ground.geometry.attributes.position.needsUpdate = true; + ground.geometry.computeVertexNormals(); + this.ground = ground; + this.scene.add(ground); + + // 격자 효과 추가 + const gridHelper = new THREE.GridHelper(MAP_SIZE, 50, 0x000000, 0x000000); + gridHelper.material.opacity = 0.1; + gridHelper.material.transparent = true; + gridHelper.position.y = 0.1; + this.scene.add(gridHelper); + + // 사막 장식 추가 + await this.addDesertDecorations(); + + // 탱크 초기화 + await this.tank.initialize(this.scene, this.loader); + if (!this.tank.isLoaded) { + throw new Error('Tank loading failed'); + } + + // 스폰 위치 검증 + const spawnPos = this.findValidSpawnPosition(); + const heightAtSpawn = this.getHeightAtPosition(spawnPos.x, spawnPos.z); + const slopeCheckPoints = [ + { x: spawnPos.x + 2, z: spawnPos.z }, + { x: spawnPos.x - 2, z: spawnPos.z }, + { x: spawnPos.x, z: spawnPos.z + 2 }, + { x: spawnPos.x, z: spawnPos.z - 2 } + ]; + + const slopes = slopeCheckPoints.map(point => { + const pointHeight = this.getHeightAtPosition(point.x, point.z); + return Math.abs(pointHeight - heightAtSpawn) / 2; + }); + + const maxSlope = Math.max(...slopes); + if (maxSlope > 0.3) { + location.reload(); + return; + } + + // 카메라 초기 설정 + 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(); + } + } + + // 레이더 업데이트 메서드 추가 + updateRadar() { + const currentTime = Date.now(); + if (currentTime - this.lastRadarUpdate < this.radarUpdateInterval) return; + + const radar = document.getElementById('radar'); + const radarRect = radar.getBoundingClientRect(); + const radarCenter = { + x: radarRect.width / 2, + y: radarRect.height / 2 + }; + + // 기존 적 도트 제거 + const oldDots = radar.getElementsByClassName('enemy-dot'); + while (oldDots[0]) { + oldDots[0].remove(); + } + + // 탱크 위치 가져오기 + const tankPos = this.tank.getPosition(); + + // 모든 적에 대해 레이더에 표시 + this.enemies.forEach(enemy => { + if (!enemy.mesh || !enemy.isLoaded) return; + + const enemyPos = enemy.mesh.position; + const distance = tankPos.distanceTo(enemyPos); - async initialize() { - try { - // BGM이 아직 재생되지 않은 경우에만 재생 - if (!this.bgmPlaying && !this.bgm) { - this.bgm = new Audio('sounds/BGM.ogg'); - this.bgm.volume = 0.5; - this.bgm.loop = true; - this.bgm.play(); - this.bgmPlaying = true; - } - - // 시작 사운드 재생 - const startSounds = ['sounds/start1.ogg', 'sounds/start2.ogg', 'sounds/start3.ogg']; - const randomStartSound = startSounds[Math.floor(Math.random() * startSounds.length)]; - const startAudio = new Audio(randomStartSound); - startAudio.volume = 0.5; - startAudio.play(); - - // 렌더러 설정 - this.renderer.shadowMap.enabled = true; - this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; - this.renderer.outputColorSpace = THREE.SRGBColorSpace; - - // 기본 씬 설정 - this.setupScene(); - - // 안개 효과 - this.scene.fog = new THREE.FogExp2(0x87CEEB, 0.0008); - this.scene.background = new THREE.Color(0x87CEEB); - - // 조명 설정 - // 주변광 - const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); - this.scene.add(ambientLight); - - // 메인 태양광 - const mainLight = new THREE.DirectionalLight(0xffffff, 1.0); - mainLight.position.set(MAP_SIZE/2, MAP_SIZE/2, MAP_SIZE/2); - mainLight.castShadow = true; - - // 그림자 설정 - mainLight.shadow.mapSize.width = 4096; - mainLight.shadow.mapSize.height = 4096; - mainLight.shadow.camera.near = 0.5; - mainLight.shadow.camera.far = MAP_SIZE * 2; - mainLight.shadow.camera.left = -MAP_SIZE; - mainLight.shadow.camera.right = MAP_SIZE; - mainLight.shadow.camera.top = MAP_SIZE; - mainLight.shadow.camera.bottom = -MAP_SIZE; - mainLight.shadow.bias = -0.001; - mainLight.shadow.radius = 2; - mainLight.shadow.normalBias = 0.02; - - this.scene.add(mainLight); - - // 보조 태양광 - const secondaryLight = new THREE.DirectionalLight(0xffffff, 0.3); - secondaryLight.position.set(-50, 50, -50); - this.scene.add(secondaryLight); - - // 환경광 - const hemisphereLight = new THREE.HemisphereLight( - 0x87CEEB, - 0xFFE87C, - 0.3 - ); - this.scene.add(hemisphereLight); - // 지형 생성 - const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100); - const groundMaterial = new THREE.MeshStandardMaterial({ - color: 0xD2B48C, - roughness: 0.8, - metalness: 0.2, - envMapIntensity: 1.0 - }); - - const ground = new THREE.Mesh(groundGeometry, groundMaterial); - ground.rotation.x = -Math.PI / 2; - ground.receiveShadow = true; - - // 지형 높이 설정 - const vertices = ground.geometry.attributes.position.array; - for (let i = 0; i < vertices.length; i += 3) { - vertices[i + 2] = 0; // 모든 높이를 0으로 설정 + // 레이더 범위 내에 있는 경우만 표시 + if (distance <= this.radarRange) { + // 탱크 기준 상대 각도 계산 + const angle = Math.atan2( + enemyPos.x - tankPos.x, + enemyPos.z - tankPos.z + ); + + // 상대 거리를 레이더 크기에 맞게 스케일링 + const relativeDistance = distance / this.radarRange; + const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance); + const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance); + + // 적 도트 생성 및 추가 + const dot = document.createElement('div'); + dot.className = 'enemy-dot'; + dot.style.left = `${dotX}px`; + dot.style.top = `${dotY}px`; + radar.appendChild(dot); + } + }); + + this.lastRadarUpdate = currentTime; } - - ground.geometry.attributes.position.needsUpdate = true; - ground.geometry.computeVertexNormals(); - this.ground = ground; - this.scene.add(ground); - - // 격자 효과 추가 - const gridHelper = new THREE.GridHelper(MAP_SIZE, 50, 0x000000, 0x000000); - gridHelper.material.opacity = 0.1; - gridHelper.material.transparent = true; - gridHelper.position.y = 0.1; - this.scene.add(gridHelper); - - // 사막 장식 추가 - await this.addDesertDecorations(); - - // 탱크 초기화 - await this.tank.initialize(this.scene, this.loader); - if (!this.tank.isLoaded) { - throw new Error('Tank loading failed'); + + async addDesertDecorations() { + if (!this.obstacles) { + this.obstacles = []; } - - // 스폰 위치 검증 - const spawnPos = this.findValidSpawnPosition(); - const heightAtSpawn = this.getHeightAtPosition(spawnPos.x, spawnPos.z); - const slopeCheckPoints = [ - { x: spawnPos.x + 2, z: spawnPos.z }, - { x: spawnPos.x - 2, z: spawnPos.z }, - { x: spawnPos.x, z: spawnPos.z + 2 }, - { x: spawnPos.x, z: spawnPos.z - 2 } + + const BUILDING_COUNT = 25; + const buildingModels = [ + 'models/house1.glb', + 'models/house2.glb', + 'models/house3.glb', + 'models/house4.glb' ]; - - const slopes = slopeCheckPoints.map(point => { - const pointHeight = this.getHeightAtPosition(point.x, point.z); - return Math.abs(pointHeight - heightAtSpawn) / 2; + + // 충돌 박스 시각화를 위한 재질 + const collisionBoxMaterial = new THREE.LineBasicMaterial({ + color: 0xff0000, + linewidth: 2 }); - - const maxSlope = Math.max(...slopes); - if (maxSlope > 0.3) { - location.reload(); - return; - } - - // 카메라 초기 설정 - 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(); - } - } - - // 레이더 업데이트 메서드 추가 - updateRadar() { - const currentTime = Date.now(); - if (currentTime - this.lastRadarUpdate < this.radarUpdateInterval) return; - - const radar = document.getElementById('radar'); - const radarRect = radar.getBoundingClientRect(); - const radarCenter = { - x: radarRect.width / 2, - y: radarRect.height / 2 - }; - - // 기존 적 도트 제거 - const oldDots = radar.getElementsByClassName('enemy-dot'); - while (oldDots[0]) { - oldDots[0].remove(); - } - - // 탱크 위치 가져오기 - const tankPos = this.tank.getPosition(); - - // 모든 적에 대해 레이더에 표시 - this.enemies.forEach(enemy => { - if (!enemy.mesh || !enemy.isLoaded) return; - - const enemyPos = enemy.mesh.position; - const distance = tankPos.distanceTo(enemyPos); - - // 레이더 범위 내에 ���는 경우만 표시 - if (distance <= this.radarRange) { - // 탱크 기준 상대 각도 계산 - const angle = Math.atan2( - enemyPos.x - tankPos.x, - enemyPos.z - tankPos.z - ); - - // 상대 거리를 레이더 크기에 맞게 스케일링 - const relativeDistance = distance / this.radarRange; - const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance); - const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance); - - // 적 도트 생성 및 추가 - const dot = document.createElement('div'); - dot.className = 'enemy-dot'; - dot.style.left = `${dotX}px`; - dot.style.top = `${dotY}px`; - radar.appendChild(dot); + + // 플레이어 충돌 반경 및 최소 거리 계산 + const playerCollisionRadius = 2; // 플레이어 충돌 반경 (탱크의 크기 기준) + const clearanceBuffer = 1; // 여유 공간 + const minimumDistance = playerCollisionRadius * 2 + clearanceBuffer; // 건물 간 최소 거리 + + for (let i = 0; i < BUILDING_COUNT; i++) { + try { + const modelPath = buildingModels[Math.floor(Math.random() * buildingModels.length)]; + const result = await this.loader.loadAsync(modelPath); + const building = result.scene; + + let x, z; + const edgeSpawn = Math.random() < 0.7; + + if (edgeSpawn) { + if (Math.random() < 0.5) { + x = (Math.random() < 0.5 ? -1 : 1) * (MAP_SIZE * 0.4 + Math.random() * MAP_SIZE * 0.1); + z = (Math.random() - 0.5) * MAP_SIZE * 0.9; + } else { + x = (Math.random() - 0.5) * MAP_SIZE * 0.9; + z = (Math.random() < 0.5 ? -1 : 1) * (MAP_SIZE * 0.4 + Math.random() * MAP_SIZE * 0.1); + } + } else { + x = (Math.random() - 0.5) * MAP_SIZE * 0.6; + z = (Math.random() - 0.5) * MAP_SIZE * 0.6; + } + + building.position.set(x, 0, z); + building.rotation.y = Math.random() * Math.PI * 2; + building.scale.set(1, 1, 1); + + // 그림자 설정 + building.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + // 충돌 박스 생성 및 시각화 + const boundingBox = new THREE.Box3().setFromObject(building); + const boxSize = boundingBox.getSize(new THREE.Vector3()); + + // 충돌 박스의 와이어프레임 생성 + const boxGeometry = new THREE.BufferGeometry(); + const positions = new Float32Array([ + // 바닥 사각형 + -boxSize.x / 2, 0.1, -boxSize.z / 2, + -boxSize.x / 2, 0.1, boxSize.z / 2, + boxSize.x / 2, 0.1, boxSize.z / 2, + boxSize.x / 2, 0.1, -boxSize.z / 2, + -boxSize.x / 2, 0.1, -boxSize.z / 2 + ]); + + boxGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + const collisionBoxLine = new THREE.Line(boxGeometry, collisionBoxMaterial); + collisionBoxLine.position.copy(building.position); + collisionBoxLine.rotation.y = building.rotation.y; + + // 충돌 데이터 설정 + building.userData.isCollidable = true; + building.userData.type = 'building'; + building.userData.collisionBox = collisionBoxLine; + + let tooClose = false; + for (const obstacle of this.obstacles) { + const distance = building.position.distanceTo(obstacle.position); + if (distance < minimumDistance) { // 최소 거리 조건 추가 + tooClose = true; + break; + } + } + + if (!tooClose) { + this.obstacles.push(building); + this.scene.add(building); + this.scene.add(collisionBoxLine); // 충돌 박스 라인 추가 + } + + } catch (error) { + console.error('Error loading building model:', error); } + } + + // 선인장 추가 (기존 코드 유지) + const cactusGeometry = new THREE.CylinderGeometry(0.5, 0.7, 4, 8); + const cactusMaterial = new THREE.MeshStandardMaterial({ + color: 0x2F4F2F, + roughness: 0.8 }); - - this.lastRadarUpdate = currentTime; + + 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; + cactus.userData.isCollidable = false; + cactus.userData.type = 'cactus'; + + this.scene.add(cactus); + } } - - async addDesertDecorations() { - if (!this.obstacles) { - this.obstacles = []; + + + + getHeightAtPosition(x, z) { + return 0; // 항상 높이 0 반환 } - - const BUILDING_COUNT = 25; - const buildingModels = [ - 'models/house1.glb', - 'models/house2.glb', - 'models/house3.glb', - 'models/house4.glb' - ]; - - // 충돌 박스 시각화를 위한 재질 - const collisionBoxMaterial = new THREE.LineBasicMaterial({ - color: 0xff0000, - linewidth: 2 - }); - - // 플레이어 충돌 반경 및 최소 거리 계산 - const playerCollisionRadius = 2; // 플레이어 충돌 반경 (탱크의 크기 기준) - const clearanceBuffer = 1; // 여유 공간 - const minimumDistance = playerCollisionRadius * 2 + clearanceBuffer; // 건물 간 최소 거리 - - for (let i = 0; i < BUILDING_COUNT; i++) { - try { - const modelPath = buildingModels[Math.floor(Math.random() * buildingModels.length)]; - const result = await this.loader.loadAsync(modelPath); - const building = result.scene; - - let x, z; - const edgeSpawn = Math.random() < 0.7; - - if (edgeSpawn) { - if (Math.random() < 0.5) { - x = (Math.random() < 0.5 ? -1 : 1) * (MAP_SIZE * 0.4 + Math.random() * MAP_SIZE * 0.1); - z = (Math.random() - 0.5) * MAP_SIZE * 0.9; - } else { - x = (Math.random() - 0.5) * MAP_SIZE * 0.9; - z = (Math.random() < 0.5 ? -1 : 1) * (MAP_SIZE * 0.4 + Math.random() * MAP_SIZE * 0.1); - } - } else { - x = (Math.random() - 0.5) * MAP_SIZE * 0.6; - z = (Math.random() - 0.5) * MAP_SIZE * 0.6; - } - - building.position.set(x, 0, z); - building.rotation.y = Math.random() * Math.PI * 2; - building.scale.set(1, 1, 1); - - // 그림자 설정 - building.traverse((child) => { - if (child.isMesh) { - child.castShadow = true; - child.receiveShadow = true; - } - }); - - // 충돌 박스 생성 및 시각화 - const boundingBox = new THREE.Box3().setFromObject(building); - const boxSize = boundingBox.getSize(new THREE.Vector3()); - - // 충돌 박스의 와이어프레임 생성 - const boxGeometry = new THREE.BufferGeometry(); - const positions = new Float32Array([ - // 바닥 사각형 - -boxSize.x / 2, 0.1, -boxSize.z / 2, - -boxSize.x / 2, 0.1, boxSize.z / 2, - boxSize.x / 2, 0.1, boxSize.z / 2, - boxSize.x / 2, 0.1, -boxSize.z / 2, - -boxSize.x / 2, 0.1, -boxSize.z / 2 - ]); - - boxGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); - const collisionBoxLine = new THREE.Line(boxGeometry, collisionBoxMaterial); - collisionBoxLine.position.copy(building.position); - collisionBoxLine.rotation.y = building.rotation.y; - - // 충돌 데이터 설정 - building.userData.isCollidable = true; - building.userData.type = 'building'; - building.userData.collisionBox = collisionBoxLine; - - let tooClose = false; - for (const obstacle of this.obstacles) { - const distance = building.position.distanceTo(obstacle.position); - if (distance < minimumDistance) { // 최소 거리 조건 추가 - tooClose = true; - break; + + findValidSpawnPosition() { + const margin = 50; + let position; + let attempts = 0; + const maxAttempts = 50; + const maxSlope = 0.3; // 최대 허용 경사 + + while (attempts < maxAttempts) { + position = new THREE.Vector3( + (Math.random() - 0.5) * (MAP_SIZE - margin * 2), + 0, + (Math.random() - 0.5) * (MAP_SIZE - margin * 2) + ); + + // 현재 위치의 높이 가져오기 + const height = this.getHeightAtPosition(position.x, position.z); + position.y = height + TANK_HEIGHT; + + // 주변 지형의 경사 체크 + const checkPoints = [ + { x: position.x + 2, z: position.z }, + { x: position.x - 2, z: position.z }, + { x: position.x, z: position.z + 2 }, + { x: position.x, z: position.z - 2 } + ]; + + const slopes = checkPoints.map(point => { + const pointHeight = this.getHeightAtPosition(point.x, point.z); + return Math.abs(pointHeight - height) / 2; + }); + + const maxCurrentSlope = Math.max(...slopes); + + if (maxCurrentSlope <= maxSlope) { + return position; } + + attempts++; } - - if (!tooClose) { - this.obstacles.push(building); - this.scene.add(building); - this.scene.add(collisionBoxLine); // 충돌 박스 라인 추가 + + // 실패 시 기본 위치 반환 + return new THREE.Vector3(0, TANK_HEIGHT, 0); + } + + 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; + this.mouse.x += movementX * 0.002; + this.mouse.y = 0; + + while (this.mouse.x > Math.PI) this.mouse.x -= Math.PI * 2; + while (this.mouse.x < -Math.PI) this.mouse.x += Math.PI * 2; + }); + + document.addEventListener('click', () => { + if (!document.pointerLockElement) { + document.body.requestPointerLock(); + } else if (!this.isGameOver && this.tank && this.tank.isLoaded) { + const bullet = this.tank.shoot(this.scene); + if (bullet) { + // Shooting effects... } - - } catch (error) { - console.error('Error loading building model:', error); } - } - - // 선인장 추가 (기존 코드 유지) - 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; - cactus.userData.isCollidable = false; - cactus.userData.type = 'cactus'; - - this.scene.add(cactus); + + 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(); + const isMoving = this.keys.forward || this.keys.backward; + // 이동 시작할 때 사운드 처리 + if (isMoving && !this.isEngineRunning) { + this.isEngineRunning = true; + + // 이전 사운드 정지 + if (this.engineStopSound) { + this.engineStopSound.pause(); + } + if (this.engineSound) { + this.engineSound.pause(); + } + + // 엔진 정지 사운드 재생 + this.engineStopSound = new Audio('sounds/engine.ogg'); + this.engineStopSound.play(); + + // 엔진 정지 사운드 종료 후 엔진 사운드 시작 + this.engineStopSound.onended = () => { + this.engineSound = new Audio('sounds/engine.ogg'); + this.engineSound.loop = true; + this.engineSound.play(); + }; + } + // 이동 멈출 때 사운드 처리 + else if (!isMoving && this.isEngineRunning) { + this.isEngineRunning = false; + if (this.engineSound) { + this.engineSound.pause(); + this.engineSound = null; + } + if (this.engineStopSound) { + this.engineStopSound.pause(); + this.engineStopSound = null; + } + const stopSound = new Audio('sounds/enginestop.ogg'); + stopSound.play(); + } + + + // 차체 이동과 회전은 유지 + 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(); + direction.applyEuler(this.tank.body.rotation); + this.tank.move(direction); + } + + // 탱크 위치 가져오기 + const tankPos = this.tank.getPosition(); + + // 카메라는 마우스 X 회전에만 따라감 + const cameraDistance = 15; + const cameraHeight = 8; + const cameraAngle = this.mouse.x + Math.PI; // 항상 포탑의 뒤쪽에 위치 + + const cameraX = tankPos.x + Math.sin(cameraAngle) * cameraDistance; + const cameraZ = tankPos.z + Math.cos(cameraAngle) * cameraDistance; + + this.camera.position.set( + cameraX, + tankPos.y + cameraHeight, + cameraZ + ); + + // 카메라가 탱크를 바라보도록 설정 + const lookAtPoint = new THREE.Vector3( + tankPos.x, + tankPos.y + 1, + 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 = ` +