victor's picture
victor HF Staff
Initial commit
b29710c
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);
}
}