import * as THREE from "three"; import { Projectile } from "../../Projectile.js"; import { findNearestWithinRange } from "../common/targeting.js"; const VISUAL_TOP_INCREMENT = 0.08; const VISUAL_TOP_CAP = 0.4; export default { key: "basic", buildHead(tower) { const headGeo = new THREE.BoxGeometry(0.8, 0.4, 0.8); const headMat = new THREE.MeshStandardMaterial({ color: 0x90caf9, metalness: 0.18, roughness: 0.45, emissive: 0x000000, emissiveIntensity: 0.6, side: THREE.DoubleSide, }); const head = new THREE.Mesh(headGeo, headMat); head.castShadow = true; head.position.set(0, 0.8, 0); tower.baseMesh.add(head); tower.headMesh = head; tower.head = head; tower.headTopY = tower.mesh.position.y + head.position.y + 0.4; }, tryFire(tower, dt, enemies, projectiles, projectileSpeed) { tower.fireCooldown -= dt; if (tower.fireCooldown > 0) return; const target = findNearestWithinRange(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.fireCooldown = 1 / tower.rate; 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)), target, projectileSpeed, tower.scene, tower.projectileEffect || null ); proj.damage = tower.damage; projectiles.push(proj); tower.playShootSound(); }, applyVisualLevel(tower) { const lvl = tower.level; const head = tower.headMesh; if (!head) return; const baseMat = tower.baseMesh?.material; const headMat = head.material; if (tower.levelRing) { tower.scene.remove(tower.levelRing); tower.levelRing.geometry.dispose(); if (tower.levelRing.material?.dispose) tower.levelRing.material.dispose(); tower.levelRing = null; } // No height increment on level gain — only rings should reflect level if (lvl <= 1) { if (baseMat) { baseMat.color?.set?.(0x5c6bc0); baseMat.emissive?.set?.(0x000000); baseMat.emissiveIntensity = 0.0; } // Keep original head height/position; recompute top using current head position tower.headTopY = (tower.mesh?.position.y ?? 0.25) + head.position.y + 0.4; headMat.color?.set?.(0x90caf9); headMat.emissive?.set?.(0x4a0a2a); headMat.emissiveIntensity = 0.2; } else { if (baseMat) { baseMat.color?.set?.(0x6f7bd6); baseMat.emissive?.set?.(0x2a0a1a); baseMat.emissiveIntensity = 0.08; } // Keep original head height/position; recompute top using current head position tower.headTopY = (tower.mesh?.position.y ?? 0.25) + head.position.y + 0.4; headMat.color?.set?.(0xa5d6ff); headMat.emissive?.set?.(0x9a135a); headMat.emissiveIntensity = 0.35; const ringGeom = new THREE.TorusGeometry(0.45, 0.035, 8, 24); const ringMat = new THREE.MeshStandardMaterial({ color: 0x3aa6ff, 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); } }, };