Spaces:
Running
Running
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); | |
} | |
} | |