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