Spaces:
Running
Running
File size: 4,650 Bytes
b29710c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
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);
}
}
|