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); | |
| } | |
| } | |