Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	| import * as THREE from "three"; | |
| import { Projectile } from "../../Projectile.js"; | |
| import { findClosestToExitInRange } from "../common/targeting.js"; | |
| const VISUAL_TOP_INCREMENT = 0.08; | |
| const VISUAL_TOP_CAP = 0.4; | |
| export default { | |
| key: "sniper", | |
| buildHead(tower) { | |
| const headGeo = new THREE.ConeGeometry(0.7, 0.9, 3); | |
| const headMat = new THREE.MeshStandardMaterial({ | |
| color: 0xb0bec5, | |
| metalness: 0.35, | |
| roughness: 0.4, | |
| emissive: 0x330000, | |
| emissiveIntensity: 0.4, | |
| side: THREE.DoubleSide, | |
| }); | |
| const head = new THREE.Mesh(headGeo, headMat); | |
| head.castShadow = true; | |
| head.position.set(0, 0.95, 0); | |
| head.rotation.x = 0; | |
| tower.baseMesh.add(head); | |
| tower.headMesh = head; | |
| tower.head = head; | |
| const headTopOffset = 0.55; | |
| tower.headTopY = tower.mesh.position.y + head.position.y + headTopOffset; | |
| }, | |
| tryFire(tower, dt, enemies, projectiles, projectileSpeed) { | |
| tower.fireCooldown -= dt; | |
| if (tower.aiming) { | |
| const t = tower.aimedTarget; | |
| const targetPos = t?.mesh?.position || t?.position; | |
| const alive = t && !t.isDead(); | |
| const within = | |
| alive && | |
| targetPos?.distanceToSquared(tower.position) <= | |
| tower.cancelThreshold * tower.cancelThreshold; | |
| if (!alive || !within) { | |
| this.removeLaser(tower); | |
| tower.stopAimingTone(); | |
| tower.aiming = false; | |
| tower.aimedTarget = null; | |
| } else { | |
| const dir = new THREE.Vector3().subVectors(targetPos, tower.position); | |
| const yaw = Math.atan2(dir.x, dir.z); | |
| tower.mesh.rotation.y = yaw; | |
| const start = tower.position | |
| .clone() | |
| .add(new THREE.Vector3(0, tower.headTopY, 0)); | |
| const end = targetPos.clone(); | |
| if (tower.laserLine) this.updateLaser(tower.laserLine, start, end); | |
| tower.aimingTimer -= dt; | |
| if (tower.aimingTimer <= 0) { | |
| const spawnY = | |
| typeof tower.headTopY === "number" ? tower.headTopY - 0.1 : 0.9; | |
| const proj = new Projectile( | |
| tower.position.clone().add(new THREE.Vector3(0, spawnY, 0)), | |
| t, | |
| tower.sniperProjectileSpeed ?? projectileSpeed, | |
| tower.scene, | |
| null | |
| ); | |
| proj.damage = tower.damage; | |
| projectiles.push(proj); | |
| this.removeLaser(tower); | |
| tower.stopAimingTone(); | |
| tower.playFireCrack(); | |
| tower.fireCooldown = 1 / tower.rate; | |
| tower.aiming = false; | |
| tower.aimedTarget = null; | |
| } | |
| } | |
| return; | |
| } | |
| if (tower.fireCooldown > 0) return; | |
| const target = findClosestToExitInRange(tower, enemies); | |
| if (!target) return; | |
| const dir = new THREE.Vector3().subVectors( | |
| target.mesh.position, | |
| tower.position | |
| ); | |
| const yaw = Math.atan2(dir.x, dir.z); | |
| tower.mesh.rotation.y = yaw; | |
| tower.aimedTarget = target; | |
| tower.aiming = true; | |
| tower.aimingTimer = Math.max(0.01, tower.aimTime || 0.01); | |
| const start = tower.position | |
| .clone() | |
| .add(new THREE.Vector3(0, tower.headTopY, 0)); | |
| const end = target.mesh.position.clone(); | |
| tower.laserLine = this.createLaser(tower, start, end); | |
| tower.playAimingTone(); | |
| }, | |
| createLaser(tower, start, end) { | |
| const points = [start.clone(), end.clone()]; | |
| const geometry = new THREE.BufferGeometry().setFromPoints(points); | |
| const material = new THREE.LineBasicMaterial({ | |
| color: 0xff3b30, | |
| transparent: true, | |
| opacity: 0.9, | |
| linewidth: 2, | |
| }); | |
| const line = new THREE.Line(geometry, material); | |
| line.position.y += 0.01; | |
| tower.scene.add(line); | |
| return line; | |
| }, | |
| updateLaser(line, start, end) { | |
| const positions = line.geometry.attributes.position; | |
| positions.setXYZ(0, start.x, start.y, start.z); | |
| positions.setXYZ(1, end.x, end.y, end.z); | |
| positions.needsUpdate = true; | |
| }, | |
| removeLaser(tower) { | |
| if (tower.laserLine) { | |
| tower.scene.remove(tower.laserLine); | |
| if (tower.laserLine.geometry) tower.laserLine.geometry.dispose(); | |
| if (tower.laserLine.material?.dispose) tower.laserLine.material.dispose(); | |
| tower.laserLine = null; | |
| } | |
| }, | |
| applyVisualLevel(tower) { | |
| const lvl = tower.level; | |
| const head = tower.headMesh; | |
| if (!head) return; | |
| const baseMat = tower.baseMesh?.material; | |
| const headMat = head.material; | |
| // Remove previous ring if any | |
| if (tower.levelRing) { | |
| tower.scene.remove(tower.levelRing); | |
| tower.levelRing.geometry.dispose(); | |
| if (tower.levelRing.material?.dispose) tower.levelRing.material.dispose(); | |
| tower.levelRing = null; | |
| } | |
| // Compute visual-only extra height based on level (starts at level 2) | |
| const extraRaw = Math.max(0, (lvl - 1) * VISUAL_TOP_INCREMENT); | |
| const visualExtra = Math.min(VISUAL_TOP_CAP, extraRaw); | |
| if (lvl <= 1) { | |
| // Default look for sniper | |
| if (baseMat) { | |
| baseMat.color?.set?.(0x6d6f73); | |
| baseMat.emissive?.set?.(0x000000); | |
| baseMat.emissiveIntensity = 0.0; | |
| baseMat.metalness = 0.5; | |
| baseMat.roughness = 0.35; | |
| } | |
| // Keep original head height/position; recompute top using current head position | |
| tower.headTopY = (tower.mesh?.position.y ?? 0.25) + head.position.y + 0.55; | |
| headMat.color?.set?.(0xb0bec5); | |
| headMat.emissive?.set?.(0x330000); | |
| headMat.emissiveIntensity = 0.4; | |
| headMat.metalness = 0.35; | |
| headMat.roughness = 0.4; | |
| } else { | |
| // Level 2+ look for sniper | |
| if (baseMat) { | |
| baseMat.color?.set?.(0x5d5f63); | |
| baseMat.emissive?.set?.(0x2a0a1a); | |
| baseMat.emissiveIntensity = 0.08; | |
| baseMat.metalness = 0.55; | |
| baseMat.roughness = 0.3; | |
| } | |
| // Keep original head height/position; recompute top using current head position | |
| tower.headTopY = (tower.mesh?.position.y ?? 0.25) + head.position.y + 0.55; | |
| headMat.color?.set?.(0x90a4ae); | |
| headMat.emissive?.set?.(0x550000); | |
| headMat.emissiveIntensity = 0.5; | |
| headMat.metalness = 0.4; | |
| headMat.roughness = 0.35; | |
| // Optional thin ring on top (same as basic tower) | |
| const ringGeom = new THREE.TorusGeometry(0.45, 0.035, 8, 24); | |
| const ringMat = new THREE.MeshStandardMaterial({ | |
| color: 0x00ffff, // Cyan for sniper | |
| emissive: 0xe01a6b, | |
| emissiveIntensity: 0.55, | |
| metalness: 0.3, | |
| roughness: 0.45, | |
| }); | |
| const ring = new THREE.Mesh(ringGeom, ringMat); | |
| ring.castShadow = false; | |
| ring.receiveShadow = false; | |
| const topY = tower.headTopY ?? head.position.y + 0.8; | |
| ring.position.set( | |
| tower.mesh.position.x, | |
| topY + 0.02, | |
| tower.mesh.position.z | |
| ); | |
| ring.rotation.x = Math.PI / 2; | |
| ring.name = "tower_level_ring"; | |
| tower.levelRing = ring; | |
| tower.scene.add(ring); | |
| } | |
| // Recompute headTopY using current head position | |
| const headTopOffset = 0.55; | |
| tower.headTopY = tower.mesh.position.y + head.position.y + headTopOffset; | |
| // Sniper-specific upgrades | |
| if (lvl > 1) { | |
| const minAimTime = (tower.aimTime ?? 0) * 0.4; | |
| tower.aimTime = Math.max(minAimTime, (tower.aimTime ?? 0) * 0.9); | |
| tower.pierceChance = Math.min(0.15, (tower.pierceChance ?? 0) + 0.03); | |
| } | |
| }, | |
| onDestroy(tower) { | |
| this.removeLaser(tower); | |
| }, | |
| }; | |
