From 328829818e18c3b3524b392baf122757f748af7f Mon Sep 17 00:00:00 2001 From: Patrick Hausmann Date: Fri, 24 Jul 2026 00:41:15 +0200 Subject: [PATCH 1/2] Fix ATR_REGENERATE* semantics: value is an interval, not a rate The engine treated ATR_REGENERATEHP/ATR_REGENERATEMANA as points per second, but in the original engine the value means 1 point every N seconds. Mods like L'Hiver use this attribute for monster regeneration: a value of e.g. 20 healed 20 HP per second instead of 1 HP every 20 seconds, so enemies regenerated faster than the player could damage them. --- game/world/objects/npc.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index add7c6a32..ef543278c 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 From 69c4cc9c7b9d20181ff2f19e4e3b1794a9bcdb41 Mon Sep 17 00:00:00 2001 From: Patrick Hausmann Date: Sun, 26 Jul 2026 23:19:45 +0200 Subject: [PATCH 2/2] Npc_CanSeeNpc: measure view cone from object position and heading The cone used the animated head bone as origin and the model rotation as heading. In melee the head bone dives into the target (direction becomes noise) and flying monsters' model yaw points away from it, so canSeeNpc flickered false at point-blank range; some mods read that as a stealth hit and healed the monster. Use the object position and logical angle instead, keeping model rotation only for seated MOBSI. --- game/world/objects/npc.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index ef543278c..9d90b1b89 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -4668,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)