diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp
index add7c6a32..9d90b1b89 100644
--- a/game/world/objects/npc.cpp
+++ b/game/world/objects/npc.cpp
@@ -2296,16 +2296,22 @@ void Npc::tickTimedEvt(Animation::EvCount& ev) {
}
void Npc::tickRegen(int32_t& v, const int32_t max, const int32_t chg, const uint64_t dt) {
- uint64_t tick = owner.tickCount();
+ const uint64_t tick = owner.tickCount();
if(tick
0 ? uint64_t(chg) : uint64_t(-int64_t(chg));
+ const uint64_t period = amount*1000u;
+ const uint64_t prev = tick-dt;
+ const uint64_t n = tick/period-prev/period;
+ if(n==0)
+ return;
- int32_t nextV = std::max(0,std::min(v+val1-val0,max));
+ const int64_t step = chg>0 ? int64_t(n) : -int64_t(n);
+ const int64_t upper = std::max(0,max);
+ int32_t nextV = int32_t(std::clamp(int64_t(v)+step,0,upper));
if(v!=nextV) {
v = nextV;
// check health, in case of negative chg
@@ -4662,16 +4668,26 @@ bool Npc::canRayHitPoint(const Tempest::Vec3 self, const Tempest::Vec3 pos, floa
if(qDistTo(pos)>range*range)
return false;
- static const double ref = std::cos(100*M_PI/180.0); // spec requires +-100 view angle range
+ // The vector below points from the target back to this NPC. A >=100 degree separation from
+ // that reverse vector is the intended <=80 degree front arc.
+ static const double ref = std::cos(100*M_PI/180.0);
const DynamicWorld* w = owner.physic();
bool freeLos = angOverride>=180.f;
if(freeLos) {
return !w->ray(self, pos).hasCol;
}
- float dx = self.x-pos.x, dz=self.z-pos.z;
+ // cone from the object position, not the animated head bone: in melee the head dives into
+ // the target and the direction to it collapses to noise. a false negative here reads to some
+ // mods as a point-blank stealth hit and triggers a monster self-heal.
+ float dx = x-pos.x, dz = z-pos.z;
float dir = angleDir(dx,dz);
- float da = float(M_PI)*(visual.viewDirection()-dir)/180.f;
+
+ // Use the logical heading during normal movement/combat. While attached to a MOBSI the
+ // interaction animation owns the root rotation, so the visible pose remains authoritative.
+ // Flying monsters are not in that state and their model yaw must not flip the cone.
+ float viewAng = (interactive()!=nullptr) ? visual.viewDirection() : angle;
+ float da = float(M_PI)*(viewAng-dir)/180.f;
auto ca = angOverride > 0 ? std::cos(angOverride*M_PI/180.0) : ref;
if(double(std::cos(da))<=ca) {
if(!w->ray(self, pos).hasCol)