import * as THREE from "three"; export class Enemy { constructor(hp, speed, reward, pathPoints, scene) { this.hp = hp; this.maxHp = hp; // Keep original speed as baseSpeed; speed becomes derived this.baseSpeed = speed; this.reward = reward; this.currentSeg = 0; this.pathPoints = pathPoints; this.scene = scene; this.position = pathPoints[0].clone(); this.target = pathPoints[1].clone(); // Slow status (non-stacking, refreshes on re-hit) this.slowMult = 1.0; // 0.6 means 40% slow this.slowRemaining = 0.0; // seconds remaining // Mesh const geo = new THREE.ConeGeometry(0.6, 1.6, 6); const mat = new THREE.MeshStandardMaterial({ color: 0xff5555, roughness: 0.7, }); const mesh = new THREE.Mesh(geo, mat); mesh.castShadow = true; mesh.position.copy(this.position); mesh.rotation.x = Math.PI; // Health bar const hbBgGeo = new THREE.PlaneGeometry(1.2, 0.15); const hbBgMat = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide, depthWrite: false, depthTest: false, // ensure bar not occluded by ground transparent: true, opacity: 0.8, }); const hbBg = new THREE.Mesh(hbBgGeo, hbBgMat); // Lift the bar higher so it's clearly above the enemy and ground // Keep it centered in local Z; we'll face it to camera each frame hbBg.position.set(0, 2.0, 0.0); // Remove fixed -90deg pitch; use camera-facing billboard instead hbBg.rotation.set(0, 0, 0); // Billboard: always face the active camera hbBg.onBeforeRender = (renderer, scene, camera) => { hbBg.quaternion.copy(camera.quaternion); }; const hbGeo = new THREE.PlaneGeometry(1.2, 0.15); const hbMat = new THREE.MeshBasicMaterial({ color: 0x00ff00, side: THREE.DoubleSide, depthWrite: false, depthTest: false, // ensure bar not occluded by ground transparent: true, opacity: 0.95, }); const hb = new THREE.Mesh(hbGeo, hbMat); // Slight offset to avoid z-fighting with bg hb.position.set(0, 0.002, 0); hbBg.add(hb); mesh.add(hbBg); // Ensure bars render above the enemy and ground mesh.renderOrder = 1; hbBg.renderOrder = 2000; hb.renderOrder = 2001; this.mesh = mesh; this.hbBg = hbBg; this.hb = hb; // For validation: briefly show bars at spawn so we can confirm visibility. // This will be overridden as soon as takeDamage() runs or update() enforces state. this.hbBg.visible = true; scene.add(mesh); } takeDamage(dmg) { this.hp -= dmg; this.hp = Math.max(this.hp, 0); const ratio = Math.max(0, Math.min(1, this.hp / this.maxHp)); this.hb.scale.x = ratio; this.hb.position.x = -0.6 * (1 - ratio) + 0; // anchor left // Show bar only when not at full health and still alive this.hbBg.visible = this.hp > 0 && this.hp < this.maxHp; } applySlow(mult, duration) { // Non-stacking: overwrite multiplier and refresh duration this.slowMult = mult; this.slowRemaining = duration; } isDead() { return this.hp <= 0; } update(dt) { // Tick slow timer if (this.slowRemaining > 0) { this.slowRemaining -= dt; if (this.slowRemaining <= 0) { this.slowRemaining = 0; this.slowMult = 1.0; } } const toTarget = new THREE.Vector3().subVectors(this.target, this.position); const dist = toTarget.length(); const epsilon = 0.01; if (dist < epsilon) { // Advance to next waypoint this.currentSeg++; if (this.currentSeg >= this.pathPoints.length - 1) { // Reached end return "end"; } this.position.copy(this.target); this.target = this.pathPoints[this.currentSeg + 1].clone(); } else { toTarget.normalize(); const effectiveSpeed = this.baseSpeed * (this.slowRemaining > 0 ? this.slowMult : 1.0); this.position.addScaledVector(toTarget, effectiveSpeed * dt); } this.mesh.position.copy(this.position); // Keep health bar visibility consistent (in case hp changes elsewhere) if (this.hbBg) { // Only show when damaged; if you don't see bars, they will appear after first damage. this.hbBg.visible = this.hp > 0 && this.hp < this.maxHp; } // Face movement direction if (toTarget.lengthSq() > 0.0001) { const angle = Math.atan2( this.target.x - this.position.x, this.target.z - this.position.z ); this.mesh.rotation.y = angle; } return "ok"; } destroy() { this.scene.remove(this.mesh); } }