From eb17018252cfbc4da9b6e825491d5b7730ccc53d Mon Sep 17 00:00:00 2001 From: Try Date: Thu, 26 Mar 2026 20:40:23 +0100 Subject: [PATCH 01/20] rework bounding boxes --- game/game/movealgo.cpp | 5 +- game/graphics/bounds.cpp | 74 +++------------ game/graphics/bounds.h | 9 +- game/graphics/mdlvisual.cpp | 2 +- game/graphics/mesh/skeleton.cpp | 15 ++- game/graphics/mesh/skeleton.h | 1 + game/mainwindow.cpp | 20 ++-- game/physics/dynamicworld.cpp | 156 +++++++++++++++++--------------- game/physics/dynamicworld.h | 6 +- game/world/objects/item.cpp | 4 +- game/world/objects/npc.cpp | 14 ++- game/world/objects/npc.h | 1 + 12 files changed, 141 insertions(+), 166 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 74c92ecd3..aa350f1e4 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -252,7 +252,8 @@ void MoveAlgo::tickClimb(uint64_t dt) { p += climbPos0; p.y = climbHeight; if(!npc.tryTranslate(p)) { - npc.setPosition(Tempest::Vec3(climbPos0.x,climbHeight,climbPos0.z)); + // npc.setPosition(Tempest::Vec3(climbPos0.x,climbHeight,climbPos0.z)); + npc.tryTranslate(Tempest::Vec3(climbPos0.x,climbHeight,climbPos0.z)); npc.tryTranslate(p); } clearSpeed(); @@ -981,7 +982,7 @@ void MoveAlgo::onMoveFailed(const Tempest::Vec3& dp, const DynamicWorld::Collisi case Npc::GT_EnemyG: case Npc::GT_Way: case Npc::GT_Point: { - if(info.npcCol || info.preFall) { + if(info.npc!=nullptr || info.preFall) { npc.setDirection(npc.rotation()+stp); } else { auto jc = npc.tryJump(); diff --git a/game/graphics/bounds.cpp b/game/graphics/bounds.cpp index 36cf5777d..d4ee7aef1 100644 --- a/game/graphics/bounds.cpp +++ b/game/graphics/bounds.cpp @@ -4,14 +4,9 @@ using namespace Tempest; -Bounds::Bounds(){ - } - void Bounds::assign(const Vec3& cen, float sizeSz) { bbox[0] = cen-Vec3(sizeSz,sizeSz,sizeSz); bbox[1] = cen+Vec3(sizeSz,sizeSz,sizeSz); - midTr = cen; - mid = cen; calcR(); } @@ -22,34 +17,28 @@ void Bounds::assign(const Bounds& a, const Bounds& b) { bbox[1].x = std::max(a.bbox[1].x,b.bbox[1].x); bbox[1].y = std::max(a.bbox[1].y,b.bbox[1].y); bbox[1].z = std::max(a.bbox[1].z,b.bbox[1].z); - mid = (bbox[0]+bbox[1])/2; - midTr = mid; calcR(); } void Bounds::assign(const Vec3* src) { + if(src==nullptr) { + *this = Bounds(); + return; + } bbox[0] = src[0]; bbox[1] = src[1]; - mid = (bbox[0]+bbox[1])/2; - midTr = mid; calcR(); } void Bounds::assign(const std::pair& src) { bbox[0] = src.first; bbox[1] = src.second; - mid = (bbox[0]+bbox[1])/2; - midTr = mid; calcR(); } void Bounds::assign(const std::vector& vbo) { if(vbo.size()==0) { - bbox[0] = Vec3(); - bbox[1] = Vec3(); - mid = Vec3(); - midTr = Vec3(); - r = 0; + *this = Bounds(); return; } bbox[0].x = vbo[0].pos[0]; @@ -64,27 +53,21 @@ void Bounds::assign(const std::vector& vbo) { bbox[1].y = std::max(bbox[1].y,i.pos[1]); bbox[1].z = std::max(bbox[1].z,i.pos[2]); } - mid = (bbox[0]+bbox[1])/2; - midTr = mid; calcR(); } void Bounds::assign(const std::vector& vbo, const std::vector& ibo, size_t iboOffset, size_t iboLenght) { if(ibo.size()==0){ - bbox[0] = Vec3(); - bbox[1] = Vec3(); - mid = Vec3(); - midTr = Vec3(); - r = 0; + *this = Bounds(); return; } - bbox[0].x = vbo[ibo[0]].pos[0]; - bbox[0].y = vbo[ibo[0]].pos[1]; - bbox[0].z = vbo[ibo[0]].pos[2]; + bbox[0].x = vbo[ibo[iboOffset]].pos[0]; + bbox[0].y = vbo[ibo[iboOffset]].pos[1]; + bbox[0].z = vbo[ibo[iboOffset]].pos[2]; bbox[1] = bbox[0]; - for(size_t id=0; id& vbo, const std::vector bbox[1].y = std::max(bbox[1].y,i.pos[1]); bbox[1].z = std::max(bbox[1].z,i.pos[2]); } - mid = (bbox[0]+bbox[1])/2; - midTr = mid; calcR(); } -void Bounds::setObjMatrix(const Matrix4x4& m) { - // transformBbox(m); - midTr = mid; - m.project(midTr); - } - -void Bounds::transformBbox(const Matrix4x4& m) { - auto* b = bbox; - Vec3 pt[8] = { - {b[0].x,b[0].y,b[0].z}, - {b[1].x,b[0].y,b[0].z}, - {b[1].x,b[1].y,b[0].z}, - {b[0].x,b[1].y,b[0].z}, - - {b[0].x,b[0].y,b[1].z}, - {b[1].x,b[0].y,b[1].z}, - {b[1].x,b[1].y,b[1].z}, - {b[0].x,b[1].y,b[1].z}, - }; - - for(auto& i:pt) - m.project(i.x,i.y,i.z); - - midTr = mid; - m.project(midTr); - } - void Bounds::calcR() { - float dx = std::fabs(bbox[0].x-bbox[1].x); - float dy = std::fabs(bbox[0].y-bbox[1].y); - float dz = std::fabs(bbox[0].z-bbox[1].z); - r = std::sqrt(dx*dx+dy*dy+dz*dz)*0.5f; + // float dx = std::fabs(bbox[0].x-bbox[1].x); + // float dy = std::fabs(bbox[0].y-bbox[1].y); + // float dz = std::fabs(bbox[0].z-bbox[1].z); + // r = std::sqrt(dx*dx+dy*dy+dz*dz)*0.5f; float x = std::max(std::abs(bbox[0].x),std::abs(bbox[1].x)); float y = std::max(std::abs(bbox[0].y),std::abs(bbox[1].y)); diff --git a/game/graphics/bounds.h b/game/graphics/bounds.h index 60f5f1a88..c81b423f7 100644 --- a/game/graphics/bounds.h +++ b/game/graphics/bounds.h @@ -9,7 +9,7 @@ class Bounds final { public: - Bounds(); + Bounds() = default; void assign(const Tempest::Vec3& cen, float sizeSz); void assign(const Bounds& a, const Bounds& b); @@ -17,16 +17,11 @@ class Bounds final { void assign(const std::pair& bbox); void assign(const std::vector& vbo); void assign(const std::vector& vbo, const std::vector& ibo, size_t iboOffset, size_t iboLenght); - void setObjMatrix(const Tempest::Matrix4x4& m); Tempest::Vec3 bbox[2]; - Tempest::Vec3 midTr; - float r = 0, rConservative = 0; + float rConservative = 0; private: - void transformBbox(const Tempest::Matrix4x4& m); void calcR(); - - Tempest::Vec3 mid; }; diff --git a/game/graphics/mdlvisual.cpp b/game/graphics/mdlvisual.cpp index b0078a022..2f636c292 100644 --- a/game/graphics/mdlvisual.cpp +++ b/game/graphics/mdlvisual.cpp @@ -790,7 +790,7 @@ void MdlVisual::interrupt() { Tempest::Vec3 MdlVisual::displayPosition() const { if(skeleton!=nullptr) - return {0,skeleton->colisionHeight()*1.5f,0}; + return {0,skeleton->colisionHeight()*1.15f,0}; return {0.f,0.f,0.f}; } diff --git a/game/graphics/mesh/skeleton.cpp b/game/graphics/mesh/skeleton.cpp index 18f994943..19be7f745 100644 --- a/game/graphics/mesh/skeleton.cpp +++ b/game/graphics/mesh/skeleton.cpp @@ -8,12 +8,21 @@ using namespace Tempest; Skeleton::Skeleton(const zenkit::ModelHierarchy& src, const Animation* anim, std::string_view name) :fileName(name), anim(anim) { + bbox[0] = {src.bbox.min.x, src.bbox.min.y, src.bbox.min.z}; + bbox[1] = {src.bbox.max.x, src.bbox.max.y, src.bbox.max.z}; + +#if 0 bboxCol[0] = {src.collision_bbox.min.x, src.collision_bbox.min.y, src.collision_bbox.min.z}; bboxCol[1] = {src.collision_bbox.max.x, src.collision_bbox.max.y, src.collision_bbox.max.z}; // bbox size apears to be halfed in source file - bboxCol[0] *= 2.f; - bboxCol[1] *= 2.f; + // bboxCol[0] *= 2.f; + // bboxCol[1] *= 2.f; +#else + //NOTE: 'collision_bbox' doesn't match marvin view + bboxCol[0] = {src.bbox.min.x, src.bbox.min.y, src.bbox.min.z}; + bboxCol[1] = {src.bbox.max.x, src.bbox.max.y, src.bbox.max.z}; +#endif nodes.resize(src.nodes.size()); tr.resize(src.nodes.size()); @@ -88,7 +97,7 @@ std::string_view Skeleton::defaultMesh() const { float Skeleton::colisionHeight() const { // scale by 0.5, to be compatible with old behaviour for now - return std::fabs(bboxCol[1].y-bboxCol[0].y) * 0.5f; + return std::fabs(bboxCol[1].y-bboxCol[0].y); } void Skeleton::mkSkeleton() { diff --git a/game/graphics/mesh/skeleton.h b/game/graphics/mesh/skeleton.h index e8510d210..d1121fb6c 100644 --- a/game/graphics/mesh/skeleton.h +++ b/game/graphics/mesh/skeleton.h @@ -26,6 +26,7 @@ class Skeleton final { size_t BIP01_HEAD = size_t(-1); + Tempest::Vec3 bbox[2] ={}; Tempest::Vec3 bboxCol[2]={}; size_t findNode(std::string_view name, size_t def=size_t(-1)) const; diff --git a/game/mainwindow.cpp b/game/mainwindow.cpp index f58339f41..924521453 100644 --- a/game/mainwindow.cpp +++ b/game/mainwindow.cpp @@ -607,16 +607,18 @@ void MainWindow::paintFocus(Painter& p, const Focus& focus, const Matrix4x4& vp) auto tr = vp; tr.mul(focus.npc->transform()); - auto b = focus.npc->bounds(); + const auto b = focus.npc->bounds(); + const auto bbox = b.bbox; //focus.npc->bBox(); + Vec3 bx[] = { - {b.bbox[0].x,b.bbox[0].y,b.bbox[0].z}, - {b.bbox[1].x,b.bbox[0].y,b.bbox[0].z}, - {b.bbox[1].x,b.bbox[1].y,b.bbox[0].z}, - {b.bbox[0].x,b.bbox[1].y,b.bbox[0].z}, - {b.bbox[0].x,b.bbox[0].y,b.bbox[1].z}, - {b.bbox[1].x,b.bbox[0].y,b.bbox[1].z}, - {b.bbox[1].x,b.bbox[1].y,b.bbox[1].z}, - {b.bbox[0].x,b.bbox[1].y,b.bbox[1].z}, + {bbox[0].x, bbox[0].y, bbox[0].z}, + {bbox[1].x, bbox[0].y, bbox[0].z}, + {bbox[1].x, bbox[1].y, bbox[0].z}, + {bbox[0].x, bbox[1].y, bbox[0].z}, + {bbox[0].x, bbox[0].y, bbox[1].z}, + {bbox[1].x, bbox[0].y, bbox[1].z}, + {bbox[1].x, bbox[1].y, bbox[1].z}, + {bbox[0].x, bbox[1].y, bbox[1].z}, }; int min[2]={ix,iy-20}, max[2]={ix,iy-20}; diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index 5ca191fe5..18ecfa984 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -16,20 +16,21 @@ #include "utils/dbgpainter.h" -const float DynamicWorld::ghostPadding=50-22.5f; -const float DynamicWorld::ghostHeight =140; -const float DynamicWorld::worldHeight =20000; +//#include "BulletCollision/CollisionShapes/btCylinderShape.h" + +const float DynamicWorld::ghostPadding = 90;//55.f; //50-22.5f; +const float DynamicWorld::worldHeight = 20000; struct DynamicWorld::HumShape:btCapsuleShape { - HumShape(btScalar radius, btScalar height):btCapsuleShape( - CollisionWorld::toMeters(height<=0.f ? 0.f : radius), - CollisionWorld::toMeters(height)) {} + //NOTE: total height is height+2*radius + HumShape(btScalar radius, btScalar height):btCapsuleShape(CollisionWorld::toMeters(radius), + CollisionWorld::toMeters(height)) {} // "human" object mush have identyty scale/rotation matrix. Only translation allowed. void getAabb(const btTransform& t, btVector3& aabbMin, btVector3& aabbMax) const override { const btScalar rad = getRadius(); btVector3 extent(rad,rad,rad); - extent[m_upAxis] = rad + getHalfHeight(); + extent[m_upAxis] = getHalfHeight() + rad; btVector3 center = t.getOrigin(); aabbMin = center - extent; @@ -43,22 +44,25 @@ struct DynamicWorld::NpcBody : btRigidBody { delete m_collisionShape; } - Tempest::Vec3 pos={}; - float r=0, h=0, rX=0, rZ=0; - bool enable=true; - size_t frozen=size_t(-1); - uint64_t lastMove=0; + Tempest::Vec3 pos = {}; + float r = 0; + float h = 0; + float gPadd = 0.f; + bool enable = true; + size_t frozen = size_t(-1); + uint64_t lastMove = 0; Npc* toNpc() { return reinterpret_cast(getUserPointer()); } void setPosition(const Tempest::Vec3& p) { - auto m = CollisionWorld::toMeters(p+Tempest::Vec3(0,(h-r-ghostPadding)*0.5f+r+ghostPadding,0)); + const float ghostPadding = gPadd; + auto m = p + Tempest::Vec3(0,(h+ghostPadding)*0.5f,0); pos = p; btTransform trans; trans.setIdentity(); - trans.setOrigin(m); + trans.setOrigin(CollisionWorld::toMeters(m)); setWorldTransform(trans); } }; @@ -75,17 +79,20 @@ struct DynamicWorld::NpcBodyList final { } NpcBody* create(const Tempest::Vec3 &min, const Tempest::Vec3 &max) { - static const float dimMax = 45.f; + //Tested: stonegolem in Xardas'es tower + static const float dimMax = 55.f; - float dx = max.x-min.x; - float dz = max.z-min.z; - float dim = (dx+dz)*0.5f; // npc-to-landscape collision size - float height = max.y-min.y; + auto size = max - min; + float radius = std::min(size.y*0.25f, std::min(size.x, size.z)*0.5f); // npc-to-landscape collision size + float height = size.y; - if(dim>dimMax) - dim = dimMax; + radius = std::min(radius, dimMax); + float ghostPadding = std::max(radius*2.f, 55.f); + float cHeight = std::max(height-2.f*radius-ghostPadding, 0.f); - btCollisionShape* shape = new HumShape(dim*0.5f, std::max(height-ghostPadding,0.f)*0.5f); + btCollisionShape* shape = new HumShape(radius, cHeight); + //btCollisionShape* shape = new btCylinderShape(CollisionWorld::toMeters(Tempest::Vec3(radius, height*0.5f, radius))); + //btCollisionShape* shape = new btCapsuleShape(CollisionWorld::toMeters(radius), CollisionWorld::toMeters(height)); NpcBody* obj = new NpcBody(shape); btTransform trans; @@ -94,8 +101,12 @@ struct DynamicWorld::NpcBodyList final { obj->setUserIndex(C_Ghost); obj->setCollisionFlags(btCollisionObject::CF_NO_CONTACT_RESPONSE); + obj->r = radius; + obj->h = height; + obj->gPadd = ghostPadding; + maxR = std::max(maxR, radius); + add(obj); - resize(*obj,height,dx,dz); return obj; } @@ -140,17 +151,6 @@ struct DynamicWorld::NpcBodyList final { } } - void resize(NpcBody& n, float h, float dx, float dz){ - n.rX = dx; - n.rZ = dz; - - // n.r = (dx+dz)*0.25f; - n.r = std::max((dx+dz)*0.5f, dz)*0.5f; - n.h = h; - - maxR = std::max(maxR,n.r); - } - void onMove(NpcBody& n){ if(n.frozen!=size_t(-1)) { if(delMisordered(&n,frozen)){ @@ -185,7 +185,7 @@ struct DynamicWorld::NpcBodyList final { auto nr = ln*proj + s; auto dp = nr - pos; - float R = 0.5f*(npc.rX + npc.rZ) + extR; + float R = npc.r + extR; if(dp.x*dp.x+dp.z*dp.z > R*R) return false; if(npc.hupdateAabbs(); if(maxDy==0) maxDy = worldHeight; + float ghostPadding = 50.f; return ray(Tempest::Vec3(from.x,from.y+ghostPadding,from.z), Tempest::Vec3(from.x,from.y-maxDy,from.z)); } @@ -701,13 +702,11 @@ float DynamicWorld::soundOclusion(const Tempest::Vec3& from, const Tempest::Vec3 DynamicWorld::NpcItem DynamicWorld::ghostObj(std::string_view visual) { Tempest::Vec3 min={0,0,0}, max={0,0,0}; if(auto sk = Resources::loadSkeleton(visual)) { - // scale by 0.5, to be compatible with old behaviour for now - min = sk->bboxCol[0] * 0.5f; - max = sk->bboxCol[1] * 0.5f; + min = sk->bboxCol[0]; + max = sk->bboxCol[1]; } - auto obj = npcList->create(min,max); - float dim = std::max(obj->rX,obj->rZ); - return NpcItem(this,obj,dim*0.5f); + auto obj = npcList->create(min,max); + return NpcItem(this,obj); } DynamicWorld::Item DynamicWorld::staticObj(const PhysicMeshShape *shape, const Tempest::Matrix4x4 &m) { @@ -969,12 +968,19 @@ std::string_view DynamicWorld::validateSectorName(std::string_view name) const { } bool DynamicWorld::hasCollision(const NpcItem& it, CollisionTest& out) { + bool ret = false; if(npcList->hasCollision(it,out.normal,out.npc)){ - out.normal /= out.normal.length(); - out.npcCol = true; - return true; + ret = true; + } + if(world->hasCollision(*it.obj,out.normal,out.vob)) { + out.landCol = true; + ret = true; } - return world->hasCollision(*it.obj,out.normal,out.vob); + + if(!ret) + return false; + out.normal /= out.normal.length(); + return true; } DynamicWorld::NpcItem::~NpcItem() { @@ -1031,20 +1037,6 @@ const Tempest::Vec3& DynamicWorld::NpcItem::position() const { } void DynamicWorld::NpcItem::debugDraw(DbgPainter& p) const { - p.setBrush(Tempest::Color(0,1,0)); - p.drawPoint(obj->pos); - - const auto cen = Tempest::Vec3(obj->pos.x, centerY(), obj->pos.z); - p.setBrush(Tempest::Color(0,0,1)); - p.drawPoint(cen); - - p.setPen(Tempest::Color(1,1,1)); - p.drawLine(cen, cen+Tempest::Vec3(0,25,0)); - p.setPen(Tempest::Color(1,1,0)); - p.drawLine(cen, cen+Tempest::Vec3(25,0,0)); - p.setPen(Tempest::Color(1,0.5f,0)); - p.drawLine(cen, cen+Tempest::Vec3(0,0,25)); - btVector3 aabb0, aabb1; obj->getAabb(aabb0, aabb1); @@ -1111,26 +1103,42 @@ DynamicWorld::MoveCode DynamicWorld::NpcItem::implTryMove(const Tempest::Vec3& t count = std::max(countXZ,countY); } - auto prev = initial; + bool skipNpc = false; + bool skipLnd = false; + bool secondPass = false; for(int i=1; i<=count; ++i) { - auto pos = initial+(dp*float(i))/float(count); + const auto pos = initial+(dp*float(i))/float(count); implSetPosition(pos); - if(owner->hasCollision(*this,out)) { - if(i>1) { - // moved a bit - out.partial = prev; - return MoveCode::MC_Partial; - } - implSetPosition(initial); - if(owner->hasCollision(*this,out)) { - // was in collision from the start - implSetPosition(to); - return MoveCode::MC_OK; + + if(!owner->hasCollision(*this,out)) + continue; + + if((out.npc==nullptr || skipNpc) && (!out.landCol || skipLnd)) + continue; + + if(i>1) { + // moved a bit + out.partial = initial+(dp*float(i-1))/float(count); + implSetPosition(out.partial); + return MoveCode::MC_Partial; + } + + implSetPosition(initial); + if(i==1 && !secondPass) { + // maybe we were stuck into something(npc) from the start? + CollisionTest tmpOut = {}; + if(!owner->hasCollision(*this,tmpOut)) { + return MoveCode::MC_Fail; } - return MoveCode::MC_Fail; + skipNpc = tmpOut.npc!=nullptr; + skipLnd = tmpOut.landCol; + secondPass = true; + i = 0; + continue; } - } + return MoveCode::MC_Fail; + } return MoveCode::MC_OK; } diff --git a/game/physics/dynamicworld.h b/game/physics/dynamicworld.h index d236f8d57..a616df62d 100644 --- a/game/physics/dynamicworld.h +++ b/game/physics/dynamicworld.h @@ -64,16 +64,17 @@ class DynamicWorld final { struct CollisionTest { Tempest::Vec3 partial = {}; Tempest::Vec3 normal = {}; - bool npcCol = false; bool preFall = false; + Interactive* vob = nullptr; Npc* npc = nullptr; + bool landCol = false; }; struct NpcItem { public: NpcItem()=default; - NpcItem(DynamicWorld* owner,NpcBody* obj,float r):owner(owner),obj(obj){} + NpcItem(DynamicWorld* owner, NpcBody* obj):owner(owner),obj(obj){} NpcItem(NpcItem&& it):owner(it.owner),obj(it.obj){it.obj=nullptr;} ~NpcItem(); @@ -299,6 +300,5 @@ class DynamicWorld final { std::unique_ptr bulletList; std::unique_ptr bboxList; - static const float ghostHeight; static const float worldHeight; }; diff --git a/game/world/objects/item.cpp b/game/world/objects/item.cpp index 78a09cf7b..b1cb2df99 100644 --- a/game/world/objects/item.cpp +++ b/game/world/objects/item.cpp @@ -189,11 +189,11 @@ void Item::setPhysicsEnable(const MeshObjects::Mesh& view) { } void Item::setPhysicsEnable(const ProtoMesh* mesh) { - if(mesh==nullptr) + if(bBox()==nullptr) return; auto& p = *world.physic(); Bounds b; - b.assign(mesh->bbox); + b.assign(bBox()); physic = p.dynamicObj(transform(),b,zenkit::MaterialGroup(hitem->material)); physic.setItem(this); } diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index 36ffa8960..ecad8caa8 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -673,9 +673,13 @@ float Npc::rotationYRad() const { } Bounds Npc::bounds() const { - auto b = visual.bounds(); - b.setObjMatrix(transform()); - return b; + return visual.bounds(); + } + +auto Npc::bBox() const -> const Vec3* { + if(visual.visualSkeleton()==nullptr) + return nullptr; + return visual.visualSkeleton()->bboxCol; } Vec3 Npc::centerPosition() const { @@ -4288,7 +4292,7 @@ Npc::JumpStatus Npc::tryJump() { if(!physic.testMove(pos1,pos0,info) || !physic.testMove(pos2,pos1,info)) { // check approximate path of climb failed - ret.anim = Anim::Jump; + ret.anim = Anim::JumpUp; ret.noClimb = true; return ret; } @@ -4514,7 +4518,7 @@ SensesBit Npc::canSenseNpc(const Npc &oth, bool freeLos, float extRange) const { // NOTE2: interacting with chest(lockpicking) or some MOBSI should not produce 'noise' // NOTE3: seem npc can't hear player in general case, and hearing relevant only for sendImmediatePerc cases const bool isNoisy = false; - const auto mid = oth.bounds().midTr; + const auto mid = oth.centerPosition(); return canSenseNpc(mid,freeLos,isNoisy,extRange); } diff --git a/game/world/objects/npc.h b/game/world/objects/npc.h index 53945ac7b..af240d061 100644 --- a/game/world/objects/npc.h +++ b/game/world/objects/npc.h @@ -103,6 +103,7 @@ class Npc final { float runAngle() const { return runAng; } float fatness() const { return bdFatness; } Bounds bounds() const; + auto bBox() const -> const Tempest::Vec3*; void stopDlgAnim(); void clearSpeed(); From 5aebccaa4210288b26fec3cfc68e96b663b17672 Mon Sep 17 00:00:00 2001 From: Try Date: Tue, 31 Mar 2026 00:20:28 +0200 Subject: [PATCH 02/20] iterating on move-algo --- game/game/movealgo.cpp | 338 ++++++++++++++++++++++++++++++---- game/game/movealgo.h | 9 +- game/physics/dynamicworld.cpp | 3 +- 3 files changed, 307 insertions(+), 43 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index aa350f1e4..751cb8634 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -1,5 +1,7 @@ #include "movealgo.h" +#include + #include "world/objects/npc.h" #include "world/objects/interactive.h" #include "world/world.h" @@ -75,17 +77,21 @@ bool MoveAlgo::tryMove(float x,float y,float z, DynamicWorld::CollisionTest& out return npc.tryMove({x,y,z},out); } +bool MoveAlgo::tryMove(const Tempest::Vec3& dp, DynamicWorld::CollisionTest& out) { + return npc.tryMove(dp,out); + } + bool MoveAlgo::tickSlide(uint64_t dt) { float fallThreshold = stepHeight(); - auto pos = npc.position(); + auto gpos = npc.position(); + auto pos = npc.collosionCenter(); - auto norm = normalRay(pos+Tempest::Vec3(0,fallThreshold,0)); // check ground - float pY = pos.y; - bool valid = false; - auto ground = dropRay (pos+Tempest::Vec3(0,fallThreshold,0), valid); - auto water = waterRay(pos); - float dY = pY-ground; + bool gValid = false; + auto ground = dropRay (pos, gValid); + auto water = waterRay (pos); + auto norm = normalRay(pos); + float dY = gpos.y-ground; if(ground+waterDepthChest()=0.99f || !testSlide(pos+Tempest::Vec3(0,fallThreshold,0),info,true)) { + if(norm.y<=0 || norm.y>=0.99f || !testSlide(gpos,info,true)) { setAsSlide(false); return false; } @@ -108,7 +114,7 @@ bool MoveAlgo::tickSlide(uint64_t dt) { const auto slide = Tempest::Vec3::crossProduct(norm, tangent); auto dp = fallSpeed*float(dt); - if(tryMove(dp.x,dp.y,dp.z,info)) { + if(tryMove(dp,info)) { fallSpeed += slide*float(dt)*gravity; fallCount = 1; } @@ -140,7 +146,15 @@ bool MoveAlgo::tickSlide(uint64_t dt) { return true; } -void MoveAlgo::tickGravity(uint64_t dt) { +void MoveAlgo::tickGravity(uint64_t dt, MvFlags moveFlg) { + if(npc.isJumpAnim()) { + auto dp = npcMoveSpeed(dt,moveFlg); + tryMove(dp.x,dp.y,dp.z); + fallSpeed += dp; + fallCount += float(dt); + return; + } + float fallThreshold = stepHeight(); // falling if(0.fpos.y && dY > stickThreshold)) { + if(walk) { + npc.setPosition(pos0); + + info.normal = dp; + info.preFall = true; + onMoveFailed(dp,info,dt); + return false; + } + setInAir(true); + return true; + } + + if(testSlide(pos,info)) { + if(ground>=pos.y) { + // same as wall + npc.setPosition(pos0); + info.preFall = false; + onMoveFailed(dp,info,dt); + return false; + } + if(walk) { + npc.setPosition(pos0); + + info.normal = dp; + info.preFall = true; + onMoveFailed(dp,info,dt); + return false; + } + fallSpeed = Tempest::Vec3(); + fallCount = 0; + setAsSlide(true); + return true; + } + + const auto adjPos = npc.position() + Tempest::Vec3(0,-dY,0); + if(gValid && (dY>0 || npc.testMove(adjPos))) { + if(ground==pos.y) + return true; + if(ground<=pos.y) { + // step up + npc.setPosition(adjPos); + /* + if(testSlide(npc.collosionCenter(),info)){ + Tempest::Log::d(""); + npc.setPosition(pos0); + } + */ + return true; + } + if(ground>=pos.y) { + // inside ground + npc.setPosition(adjPos); + return true; + } + } + + // something went wrong - back to origin then + npc.setPosition(pos0); + return true; + } + +bool MoveAlgo::_tickRun(uint64_t dt, MvFlags moveFlg) { const auto dp = npcMoveSpeed(dt,moveFlg); - const auto pos = npc.position(); + const auto pos = npc.centerPosition(); const float fallThreshold = stepHeight(); // moving NPC, by animation bool valid = false; - auto ground = dropRay (pos+dp+Tempest::Vec3(0,fallThreshold,0), valid); + auto ground = dropRay (pos+dp, valid); auto water = waterRay(pos+dp); float dY = pos.y-ground; bool onGound = true; @@ -372,7 +486,7 @@ bool MoveAlgo::tickRun(uint64_t dt, MvFlags moveFlg) { else if(0.f<=dY && dYground && dY > stickThreshold)) { + if(!gValid && swim) { + // sea monster condition? + } + if(walk || swim) { + npc.setPosition(pos0); + + info.normal = dp; + info.preFall = true; + onMoveFailed(dp,info,dt); + return false; + } + setInAir(true); + return true; + } + + if(ground+chest < water && !npc.hasSwimAnimations()) { + // no swim animations + npc.setPosition(pos0); + DynamicWorld::CollisionTest info; + info.preFall = true; + onMoveFailed(dp,info,dt); + return false; + } + + if(testSlide(pos,info)) { + //TODO + } + + const auto adjPos = npc.position() + Tempest::Vec3(0,-dY,0); + if(gValid && (dY>0 || npc.testMove(adjPos))) { + setInAir(false); + if(ground==pos.y) + return true; + if(ground<=pos.y) { + // step up + npc.setPosition(adjPos); + return true; + } + if(ground>=pos.y) { + // inside ground + npc.setPosition(adjPos); + return true; + } + } + + // something went wrong - back to origin then + npc.setPosition(pos0); + return true; + } + +void MoveAlgo::_implTick(uint64_t dt, MvFlags moveFlg) { if(npc.interactive()!=nullptr) return tickMobsi(dt); @@ -462,25 +728,17 @@ void MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { if(isJumpup()) return tickJumpup(dt); - if(isSwim()) - return tickSwim(dt); - - if(isInAir()) { - if(npc.isJumpAnim()) { - auto dp = npcMoveSpeed(dt,moveFlg); - tryMove(dp.x,dp.y,dp.z); - fallSpeed += dp; - fallCount += float(dt); - return; - } - return tickGravity(dt); + if(isSwim()) { + tickSwim(dt); + return; } + if(isInAir()) + return tickGravity(dt,moveFlg); + if(isSlide()) { - if(tickSlide(dt)) - return; - if(isInAir()) - return; + tickSlide(dt); + return; } const auto pos0 = npc.position(); @@ -620,7 +878,7 @@ Tempest::Vec3 MoveAlgo::go2WpMoveSpeed(Tempest::Vec3 dp, const Tempest::Vec3& to bool MoveAlgo::testSlide(const Tempest::Vec3& pos, DynamicWorld::CollisionTest& out, bool cont) const { if(isInAir() || npc.bodyStateMasked()==BS_JUMP) - return false; + return false; //note: unused? // check ground const auto norm = normalRay(pos); @@ -946,7 +1204,7 @@ void MoveAlgo::onMoveFailed(const Tempest::Vec3& dp, const DynamicWorld::Collisi for(int i=5; i<=35; i+=5) { for(float angle:{float(i),-float(i)}) { applyRotation(corr,dp,float(angle*M_PI)/180.f); - if(npc.tryMove(corr)) { + if(npc.testMove(corr)) { if(forward) npc.setDirection(npc.rotation()+angle); return; @@ -1044,10 +1302,12 @@ float MoveAlgo::waterRay(const Tempest::Vec3& p, bool* hasCol) const { void MoveAlgo::rayMain(const Tempest::Vec3& pos) const { if(std::fabs(cache.x-pos.x)>eps || std::fabs(cache.y-pos.y)>eps || std::fabs(cache.z-pos.z)>eps) { - float dy = waterDepthChest()+100; // 1 meter extra offset + float threshold = waterDepthChest(); + float dy = threshold*2+100; // 1 meter extra offset if(fallSpeed.y<0) dy = 0; // whole world - static_cast(cache) = npc.world().physic()->landRay(pos,dy); + const auto spos = Tempest::Vec3(pos.x, pos.y+threshold, pos.z); + static_cast(cache) = npc.world().physic()->landRay(spos,dy); cache.x = pos.x; cache.y = pos.y; cache.z = pos.z; diff --git a/game/game/movealgo.h b/game/game/movealgo.h index be3d6c19d..4639f0a88 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -78,14 +78,18 @@ class MoveAlgo final { private: void tickMobsi (uint64_t dt); bool tickSlide (uint64_t dt); - void tickGravity(uint64_t dt); + void tickGravity(uint64_t dt, MvFlags moveFlg); void tickSwim (uint64_t dt); void tickClimb (uint64_t dt); void tickJumpup (uint64_t dt); bool tickRun(uint64_t dt, MvFlags moveFlg); + // deprecated + bool _tickRun (uint64_t dt, MvFlags moveFlg); + bool tryMove (float x, float y, float z); bool tryMove (float x, float y, float z, DynamicWorld::CollisionTest& out); + bool tryMove (const Tempest::Vec3& dp, DynamicWorld::CollisionTest& out); enum Flags : uint32_t { NoFlags = 0, @@ -118,7 +122,8 @@ class MoveAlgo final { auto npcMoveSpeed (uint64_t dt, MvFlags moveFlg) -> Tempest::Vec3; auto go2NpcMoveSpeed (const Tempest::Vec3& dp, const Npc &tg) -> Tempest::Vec3; auto go2WpMoveSpeed (Tempest::Vec3 dp, const Tempest::Vec3& to) -> Tempest::Vec3; - void implTick(uint64_t dt,MvFlags fai=NoFlag); + bool implTick(uint64_t dt, MvFlags fai); + void _implTick(uint64_t dt, MvFlags fai); void onMoveFailed(const Tempest::Vec3& dp, const DynamicWorld::CollisionTest& info, uint64_t dt); void onGravityFailed(const DynamicWorld::CollisionTest& info, uint64_t dt); diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index 18ecfa984..52da7989d 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -500,8 +500,7 @@ DynamicWorld::RayLandResult DynamicWorld::landRay(const Tempest::Vec3& from, flo world->updateAabbs(); if(maxDy==0) maxDy = worldHeight; - float ghostPadding = 50.f; - return ray(Tempest::Vec3(from.x,from.y+ghostPadding,from.z), Tempest::Vec3(from.x,from.y-maxDy,from.z)); + return ray(from, Tempest::Vec3(from.x,from.y-maxDy,from.z)); } DynamicWorld::RayWaterResult DynamicWorld::waterRay(const Tempest::Vec3& from) const { From be9be20511deabd924ac2a230480ac9efd6b4101 Mon Sep 17 00:00:00 2001 From: Try Date: Tue, 31 Mar 2026 22:52:38 +0200 Subject: [PATCH 03/20] move algo in progress --- game/game/movealgo.cpp | 184 +++++++++++++++++++++++++++--------- game/game/movealgo.h | 4 + game/game/playercontrol.cpp | 2 +- game/world/objects/npc.cpp | 6 +- game/world/objects/npc.h | 1 + 5 files changed, 152 insertions(+), 45 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 751cb8634..d8fee7b9a 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -569,18 +569,17 @@ void MoveAlgo::tick(uint64_t dt, MvFlags moveFlg) { auto ground = dropRay (pos, gValid); auto water = waterRay(pos); - if(ground+chest < water && npc.hasSwimAnimations()) { - setInAir(false); - setInWater(true); - setAsSwim(true); - } - else if(pos.y+knee < water) { - setAsSwim(false); - setInWater(true); - } - else { - setAsSwim(false); - setInWater(false); + if(chest!=flyOverWaterHint && !npc.isDead()) { + if(std::max(pos.y, ground) + chest <= water+0.01f && npc.hasSwimAnimations()) { + const bool splash = isInAir(); + if(flags!=Dive) + setState(Swim); + if(splash) + emitWaterSplash(water); + } + else if(std::max(pos.y, ground) + knee <= water && flags!=InAir) { + setState(InWater); + } } if(cache.sector!=nullptr && portal!=cache.sector) { @@ -608,12 +607,15 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { return fallSpeed*float(dt); }; - const bool swim = isSwim(); - const bool air = isInAir(); - const bool jump = npc.isJumpAnim(); - const auto pos0 = npc.position(); - const auto dp = (!air || jump) ? npcMoveSpeed(dt,moveFlg) : npcFallSpeed(dt); - const bool walk = bool(npc.walkMode() & WalkBit::WM_Walk); + const auto state = flags; + const bool swim = (state==Swim); + const bool dive = (state==Dive); + const bool air = (state==InAir); + const bool jump = (state==Jump); + const auto bs = npc.bodyStateMasked(); + const auto pos0 = npc.position(); + const auto dp = (!air) ? npcMoveSpeed(dt,moveFlg) : npcFallSpeed(dt); + const bool walk = bool(npc.walkMode() & WalkBit::WM_Walk); DynamicWorld::CollisionTest info; if(!tryMove(dp,info)) { @@ -624,12 +626,12 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { return false; } - if(air && !jump) { + if(state==InAir) { fallSpeed.y -= gravity*float(dt); } const auto pos = npc.position(); - const float stickThreshold = stepHeight(); + const float stickThreshold = air ? 0.f : stepHeight(); bool gValid = false; auto ground = dropRay (pos, gValid); @@ -641,33 +643,68 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { //NOTE: should disable slide } - if(dp==Tempest::Vec3() && pos.y==ground && !air) + if(dp==Tempest::Vec3() && pos.y==ground) return false; + // jump animation (lift off) + if(bs==BS_JUMP && state!=InAir && state!=Jump) { + setState(Jump); + return true; + } + // jump animation - if(jump) { - fallSpeed += dp; - fallCount += float(dt); - setInAir (true); - setAsSlide(false); + if(state==Jump) { + if(npc.isJumpAnim()) { + fallSpeed += dp; + fallCount += float(dt); + } else { + setState(InAir); + } return true; } // blood-fly if(canFlyOverWater() && ground water) { + npc.tryTranslate(Tempest::Vec3(pos.x, water-chest, pos.z)); + if(npc.world().tickCount()-diveStart>2000) { + setState(Swim); + } + } + return true; + } + + if(state==InAir && !npc.isDead()) { + const float h0 = falldownHeight(); + + float fallTime = fallSpeed.y/gravity; + float height = 0.5f*std::abs(gravity)*fallTime*fallTime; + + if(height>h0) { + npc.setAnim(AnimationSolver::FallDeep); + npc.setAnimRotate(0); + setState(Falling); + } else + if(fallSpeed.y<-0.3f && bs!=BS_JUMP && bs!=BS_FALL) { + npc.setAnim(AnimationSolver::Fall); + npc.setAnimRotate(0); + } + } + // above ground/void - if(!gValid || (pos.y>ground && dY > stickThreshold)) { + if(!gValid || (pos.y>ground && dY > stickThreshold && state!=InWater)) { if(!gValid && swim) { // sea monster condition? } @@ -679,10 +716,19 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { onMoveFailed(dp,info,dt); return false; } - setInAir(true); + if(!swim && !dive) + setState(InAir); return true; } + // no longer in air + if(air && !jump) { + // attach to ground + npc.takeFallDamage(fallSpeed); + clearSpeed(); + setState(NoFlags); + } + if(ground+chest < water && !npc.hasSwimAnimations()) { // no swim animations npc.setPosition(pos0); @@ -692,13 +738,31 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { return false; } - if(testSlide(pos,info)) { - //TODO + if(false && testSlide(pos,info)) { + if(ground>=pos.y) { + // same as wall + npc.setPosition(pos0); + info.preFall = false; + onMoveFailed(dp,info,dt); + return false; + } + if(walk) { + npc.setPosition(pos0); + + info.normal = dp; + info.preFall = true; + onMoveFailed(dp,info,dt); + return false; + } + fallSpeed = Tempest::Vec3(); + fallCount = 0; + setState(Slide); + return true; } const auto adjPos = npc.position() + Tempest::Vec3(0,-dY,0); if(gValid && (dY>0 || npc.testMove(adjPos))) { - setInAir(false); + setState(NoFlags); if(ground==pos.y) return true; if(ground<=pos.y) { @@ -929,6 +993,11 @@ float MoveAlgo::waterDepthChest() const { return float(npc.world().script().guildVal().water_depth_chest[gl]); } +float MoveAlgo::falldownHeight() const { + auto gl = npc.guild(); + return float(npc.world().script().guildVal().falldown_height[gl]); + } + bool MoveAlgo::canFlyOverWater() const { auto gl = npc.guild(); auto& g = npc.world().script().guildVal(); @@ -992,6 +1061,7 @@ bool MoveAlgo::isClose(const Npc& npc, const Tempest::Vec3& p, float dist) { } bool MoveAlgo::startClimb(JumpStatus jump) { + /* auto sq = npc.setAnimAngGet(jump.anim); if(sq==nullptr) return false; @@ -1028,20 +1098,22 @@ bool MoveAlgo::startClimb(JumpStatus jump) { else { return false; } + */ return true; } void MoveAlgo::startDive() { - if(isSwim() && !isDive()) { - if(npc.world().tickCount()-diveStart>1000) { - setAsDive(true); - - auto pos = npc.position(); - float pY = pos.y; - float chest = canFlyOverWater() ? 0 : waterDepthChest(); - auto water = waterRay(pos); - tryMove(0,water-chest-pY,0); - } + if(!isSwim()) + return; + + if(npc.world().tickCount()-diveStart>1000) { + setState(Dive); + + auto pos = npc.position(); + float pY = pos.y; + float chest = canFlyOverWater() ? 0 : waterDepthChest(); + auto water = waterRay(pos); + tryMove(0,water-chest-pY,0); } } @@ -1077,6 +1149,32 @@ bool MoveAlgo::isDive() const { return flags&Dive; } +bool MoveAlgo::isJump() const { + return flags&Jump; + } + +void MoveAlgo::setState(Flags f) { + if(f==flags) + return; + + if((f&Swim) && !(flags&Swim)) { + auto ws = npc.weaponState(); + npc.setAnim(Npc::Anim::NoAnim); + if(ws!=WeaponState::NoWeapon && ws!=WeaponState::Fist) + npc.closeWeapon(true); + npc.dropTorch(true); + } + + if((f&Dive) && !(flags&Dive)) { + npc.setDirectionY(-40); + } + if((f&Dive) != (flags&Dive)) { + diveStart = npc.world().tickCount(); + } + + flags = f; + } + void MoveAlgo::setInAir(bool f) { if(f==isInAir()) return; diff --git a/game/game/movealgo.h b/game/game/movealgo.h index 4639f0a88..f20caec30 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -61,6 +61,7 @@ class MoveAlgo final { bool isInWater() const; bool isSwim() const; bool isDive() const; + bool isJump() const; zenkit::MaterialGroup groundMaterial() const; auto groundNormal() const -> Tempest::Vec3; @@ -71,6 +72,7 @@ class MoveAlgo final { float waterDepthKnee() const; float waterDepthChest() const; + float falldownHeight() const; bool canFlyOverWater() const; bool checkLastBounce() const; @@ -101,8 +103,10 @@ class MoveAlgo final { InWater = 1<<6, Swim = 1<<7, Dive = 1<<8, + Jump = 1<<9, }; + void setState(Flags f); void setInAir (bool f); void setAsJumpup (bool f); void setAsClimb (bool f); diff --git a/game/game/playercontrol.cpp b/game/game/playercontrol.cpp index 5aa89b4c9..1496c6749 100644 --- a/game/game/playercontrol.cpp +++ b/game/game/playercontrol.cpp @@ -700,7 +700,7 @@ void PlayerControl::implMove(uint64_t dt) { } pl.setDirectionY(rotY); - if(pl.isFalling() || pl.isSlide() || pl.isInAir()){ + if(pl.isFalling() || pl.isSlide() || pl.isInAir() || pl.isJump()){ pl.setDirection(rot); runAngleDest = 0; return; diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index ecad8caa8..c01d876ee 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -450,7 +450,7 @@ void Npc::setDirectionY(float rotation) { if(rotation<-90) rotation = -90; rotation = std::fmod(rotation,360.f); - if(!mvAlgo.isSwim() && !(interactive()!=nullptr && interactive()->isLadder())) + if(!mvAlgo.isDive() && !(interactive()!=nullptr && interactive()->isLadder())) return; angleY = rotation; durtyTranform |= TR_Rot; @@ -1053,6 +1053,10 @@ bool Npc::isInAir() const { return mvAlgo.isInAir(); } +bool Npc::isJump() const { + return mvAlgo.isJump(); + } + void Npc::invalidateTalentOverlays() { const Talent tl[] = {TALENT_1H, TALENT_2H, TALENT_BOW, TALENT_CROSSBOW, TALENT_ACROBAT}; for(Talent i:tl) { diff --git a/game/world/objects/npc.h b/game/world/objects/npc.h index af240d061..ede31acce 100644 --- a/game/world/objects/npc.h +++ b/game/world/objects/npc.h @@ -187,6 +187,7 @@ class Npc final { bool isFallingDeep() const; bool isSlide() const; bool isInAir() const; + bool isJump() const; bool isStanding() const; bool isSwim() const; bool isInWater() const; From 93e45bbe4f67c7bd3a2e04d802e69286e31a4ecf Mon Sep 17 00:00:00 2001 From: Try Date: Wed, 1 Apr 2026 00:34:54 +0200 Subject: [PATCH 04/20] progress --- game/game/movealgo.cpp | 382 +++++++--------------------------- game/game/movealgo.h | 8 +- game/game/playercontrol.cpp | 2 +- game/physics/dynamicworld.cpp | 7 +- game/world/objects/npc.cpp | 12 +- game/world/objects/npc.h | 1 + 6 files changed, 87 insertions(+), 325 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index d8fee7b9a..26b9a242a 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -229,7 +229,12 @@ void MoveAlgo::tickGravity(uint64_t dt, MvFlags moveFlg) { } void MoveAlgo::tickJumpup(uint64_t dt) { - auto pos = npc.position(); + auto pos = npc.position(); + auto climb = npc.tryJump(); + + if(climb.anim==Npc::Anim::JumpHang) + climbHeight = pos.y; + if(pos.ypos.y && dY > stickThreshold)) { - if(walk) { - npc.setPosition(pos0); - - info.normal = dp; - info.preFall = true; - onMoveFailed(dp,info,dt); - return false; - } - setInAir(true); - return true; - } - - if(testSlide(pos,info)) { - if(ground>=pos.y) { - // same as wall - npc.setPosition(pos0); - info.preFall = false; - onMoveFailed(dp,info,dt); - return false; - } - if(walk) { - npc.setPosition(pos0); - - info.normal = dp; - info.preFall = true; - onMoveFailed(dp,info,dt); - return false; - } - fallSpeed = Tempest::Vec3(); - fallCount = 0; - setAsSlide(true); - return true; - } - - const auto adjPos = npc.position() + Tempest::Vec3(0,-dY,0); - if(gValid && (dY>0 || npc.testMove(adjPos))) { - if(ground==pos.y) - return true; - if(ground<=pos.y) { - // step up - npc.setPosition(adjPos); - /* - if(testSlide(npc.collosionCenter(),info)){ - Tempest::Log::d(""); - npc.setPosition(pos0); - } - */ - return true; - } - if(ground>=pos.y) { - // inside ground - npc.setPosition(adjPos); - return true; - } - } - - // something went wrong - back to origin then - npc.setPosition(pos0); - return true; - } - -bool MoveAlgo::_tickRun(uint64_t dt, MvFlags moveFlg) { - const auto dp = npcMoveSpeed(dt,moveFlg); - const auto pos = npc.centerPosition(); - const float fallThreshold = stepHeight(); - - // moving NPC, by animation - bool valid = false; - auto ground = dropRay (pos+dp, valid); - auto water = waterRay(pos+dp); - float dY = pos.y-ground; - bool onGound = true; - - if(canFlyOverWater() && ground=climbHeight) { + // } + } + // blood-fly if(canFlyOverWater() && ground water) { + if(pos.y+chest > water && std::isfinite(water)) { npc.tryTranslate(Tempest::Vec3(pos.x, water-chest, pos.z)); if(npc.world().tickCount()-diveStart>2000) { setState(Swim); @@ -704,7 +532,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { } // above ground/void - if(!gValid || (pos.y>ground && dY > stickThreshold && state!=InWater)) { + if(!gValid || (pos.y>plain && dY > stickThreshold && state!=InWater)) { if(!gValid && swim) { // sea monster condition? } @@ -722,7 +550,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { } // no longer in air - if(air && !jump) { + if(state==InAir) { // attach to ground npc.takeFallDamage(fallSpeed); clearSpeed(); @@ -778,77 +606,10 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { } // something went wrong - back to origin then - npc.setPosition(pos0); + // npc.setPosition(pos0); return true; } -void MoveAlgo::_implTick(uint64_t dt, MvFlags moveFlg) { - if(npc.interactive()!=nullptr) - return tickMobsi(dt); - - if(isClimb()) - return tickClimb(dt); - - if(isJumpup()) - return tickJumpup(dt); - - if(isSwim()) { - tickSwim(dt); - return; - } - - if(isInAir()) - return tickGravity(dt,moveFlg); - - if(isSlide()) { - tickSlide(dt); - return; - } - - const auto pos0 = npc.position(); - if(!tickRun(dt,moveFlg)) - return; - - if(npc.isDead() || npc.bodyStateMasked()==BS_JUMP) - return; - - auto pos1 = npc.position(); - float fallThreshold = stepHeight(); - - bool valid = false; - auto ground = dropRay (pos1+Tempest::Vec3(0,fallThreshold,0), valid); - auto water = waterRay(pos1); - - if(canFlyOverWater() && ground0.f){ - setAsJumpup(true); - setInAir(true); - + setState(JumpUp); float t = std::sqrt(2.f*dHeight/gravity); fallSpeed.y = gravity*t; } else if(jump.anim==Npc::Anim::JumpUpMid || jump.anim==Npc::Anim::JumpUpLow) { - setAsJumpup(false); - setAsClimb(true); - setInAir(true); + setState(ClimbUp); } - else if(isJumpup() && jump.anim==Npc::Anim::JumpHang) { - setAsJumpup(false); - setAsClimb(true); - setInAir(true); + else if(isJumpUp() && jump.anim==Npc::Anim::JumpHang) { + setState(ClimbUp); } else { return false; } - */ return true; } @@ -1129,7 +886,7 @@ bool MoveAlgo::isInAir() const { return flags&InAir; } -bool MoveAlgo::isJumpup() const { +bool MoveAlgo::isJumpUp() const { return flags&JumpUp; } @@ -1381,14 +1138,15 @@ void MoveAlgo::onGravityFailed(const DynamicWorld::CollisionTest& info, uint64_t } fallCount = 0; } else { - fallSpeed += norm*gravity; + fallSpeed += 10.f*norm*gravity*float(dt); } } -float MoveAlgo::waterRay(const Tempest::Vec3& p, bool* hasCol) const { - auto pos = p - Tempest::Vec3(0,waterPadd,0); +float MoveAlgo::waterRay(const Tempest::Vec3& pos, bool* hasCol) const { if(std::fabs(cacheW.x-pos.x)>eps || std::fabs(cacheW.y-pos.y)>eps || std::fabs(cacheW.z-pos.z)>eps) { - static_cast(cacheW) = npc.world().physic()->waterRay(pos); + const float threshold = -0.1f; //npc.visual.pose().translateY()+1.f; + const auto spos = Tempest::Vec3(pos.x, pos.y+threshold, pos.z); + static_cast(cacheW) = npc.world().physic()->waterRay(spos); cacheW.x = pos.x; cacheW.y = pos.y; cacheW.z = pos.z; @@ -1400,8 +1158,8 @@ float MoveAlgo::waterRay(const Tempest::Vec3& p, bool* hasCol) const { void MoveAlgo::rayMain(const Tempest::Vec3& pos) const { if(std::fabs(cache.x-pos.x)>eps || std::fabs(cache.y-pos.y)>eps || std::fabs(cache.z-pos.z)>eps) { - float threshold = waterDepthChest(); - float dy = threshold*2+100; // 1 meter extra offset + float threshold = npc.visual.pose().translateY()+1.f; + float dy = threshold+100; // 1 meter extra offset if(fallSpeed.y<0) dy = 0; // whole world const auto spos = Tempest::Vec3(pos.x, pos.y+threshold, pos.z); diff --git a/game/game/movealgo.h b/game/game/movealgo.h index f20caec30..bd997ffaf 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -56,7 +56,7 @@ class MoveAlgo final { bool isFalling() const; bool isSlide() const; bool isInAir() const; - bool isJumpup() const; + bool isJumpUp() const; bool isClimb() const; bool isInWater() const; bool isSwim() const; @@ -74,6 +74,7 @@ class MoveAlgo final { float waterDepthChest() const; float falldownHeight() const; bool canFlyOverWater() const; + bool canFallByGravity() const; bool checkLastBounce() const; @@ -84,10 +85,6 @@ class MoveAlgo final { void tickSwim (uint64_t dt); void tickClimb (uint64_t dt); void tickJumpup (uint64_t dt); - bool tickRun(uint64_t dt, MvFlags moveFlg); - - // deprecated - bool _tickRun (uint64_t dt, MvFlags moveFlg); bool tryMove (float x, float y, float z); bool tryMove (float x, float y, float z, DynamicWorld::CollisionTest& out); @@ -127,7 +124,6 @@ class MoveAlgo final { auto go2NpcMoveSpeed (const Tempest::Vec3& dp, const Npc &tg) -> Tempest::Vec3; auto go2WpMoveSpeed (Tempest::Vec3 dp, const Tempest::Vec3& to) -> Tempest::Vec3; bool implTick(uint64_t dt, MvFlags fai); - void _implTick(uint64_t dt, MvFlags fai); void onMoveFailed(const Tempest::Vec3& dp, const DynamicWorld::CollisionTest& info, uint64_t dt); void onGravityFailed(const DynamicWorld::CollisionTest& info, uint64_t dt); diff --git a/game/game/playercontrol.cpp b/game/game/playercontrol.cpp index 1496c6749..4102adb30 100644 --- a/game/game/playercontrol.cpp +++ b/game/game/playercontrol.cpp @@ -700,7 +700,7 @@ void PlayerControl::implMove(uint64_t dt) { } pl.setDirectionY(rotY); - if(pl.isFalling() || pl.isSlide() || pl.isInAir() || pl.isJump()){ + if(pl.isFalling() || pl.isSlide() || pl.isInAir() || pl.isJump() || pl.isJumpUp()){ pl.setDirection(rot); runAngleDest = 0; return; diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index 52da7989d..204644724 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -543,7 +543,7 @@ DynamicWorld::RayWaterResult DynamicWorld::implWaterRay(const Tempest::Vec3& fro float waterY = callback.m_hitPointWorld.y()*100.f; auto cave = ray(from,Tempest::Vec3(to.x,waterY,to.z)); if(cave.hasCol && cave.v.y::infinity(); ret.hasCol = false; } else { ret.wdepth = waterY; @@ -552,7 +552,7 @@ DynamicWorld::RayWaterResult DynamicWorld::implWaterRay(const Tempest::Vec3& fro return ret; } - ret.wdepth = from.y-worldHeight; + ret.wdepth = -std::numeric_limits::infinity(); ret.hasCol = false; return ret; } @@ -632,7 +632,10 @@ DynamicWorld::RayLandResult DynamicWorld::ray(const Tempest::Vec3& from, const T hitNorm.y = callback.m_hitNormalWorld.y(); hitNorm.z = callback.m_hitNormalWorld.z(); } + } else { + hitPos.y = -std::numeric_limits::infinity(); } + RayLandResult ret; ret.v = hitPos; ret.n = hitNorm; diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index c01d876ee..9d184318a 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -1042,7 +1042,7 @@ bool Npc::isFalling() const { } bool Npc::isFallingDeep() const { - return mvAlgo.isInAir() && (visual.pose().isInAnim("S_FALL") || visual.pose().isInAnim("S_FALLB")); + return mvAlgo.isFalling() && (visual.pose().isInAnim("S_FALL") || visual.pose().isInAnim("S_FALLB")); } bool Npc::isSlide() const { @@ -1057,6 +1057,10 @@ bool Npc::isJump() const { return mvAlgo.isJump(); } +bool Npc::isJumpUp() const { + return mvAlgo.isJumpUp(); + } + void Npc::invalidateTalentOverlays() { const Talent tl[] = {TALENT_1H, TALENT_2H, TALENT_BOW, TALENT_CROSSBOW, TALENT_ACROBAT}; for(Talent i:tl) { @@ -4271,7 +4275,7 @@ Npc::JumpStatus Npc::tryJump() { JumpStatus ret; DynamicWorld::CollisionTest info; - if(!isInAir() && physic.testMove(pos0+dp,info)) { + if(!mvAlgo.isJumpUp() && physic.testMove(pos0+dp,info)) { // jump forward ret.anim = Anim::Jump; ret.noClimb = true; @@ -4316,14 +4320,14 @@ Npc::JumpStatus Npc::tryJump() { return ret; } - if(isInAir() && dY<=jumpLow + visual.pose().translateY()) { + if(mvAlgo.isJumpUp() && dY<=jumpLow + visual.pose().translateY()) { // jumpup -> climb ret.anim = Anim::JumpHang; ret.height = jumpY; return ret; } - if(isInAir()) { + if(mvAlgo.isJumpUp()) { ret.anim = Anim::Idle; ret.noClimb = true; return ret; diff --git a/game/world/objects/npc.h b/game/world/objects/npc.h index ede31acce..2e5ac4ed8 100644 --- a/game/world/objects/npc.h +++ b/game/world/objects/npc.h @@ -188,6 +188,7 @@ class Npc final { bool isSlide() const; bool isInAir() const; bool isJump() const; + bool isJumpUp() const; bool isStanding() const; bool isSwim() const; bool isInWater() const; From 0992d67bb20441051e9537e5ad2b5f4302ed9a19 Mon Sep 17 00:00:00 2001 From: Try Date: Thu, 2 Apr 2026 00:25:49 +0200 Subject: [PATCH 05/20] align "water" camera --- game/camera.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/game/camera.cpp b/game/camera.cpp index 071e2c5bb..689c268d9 100644 --- a/game/camera.cpp +++ b/game/camera.cpp @@ -753,7 +753,8 @@ void Camera::tick(uint64_t dt) { auto pl = isFree() ? nullptr : world->player(); auto& physic = *world->physic(); - if(pl!=nullptr && !pl->isInWater()) { + if(pl!=nullptr && !(pl->isInWater() || pl->isSwim() || pl->isDive())) { + // NOTE: not quite correct inWater = physic.cameraRay(inter.target, origin).waterCol % 2; } else { // NOTE: find a way to avoid persistent tracking From ce3bcfb5ae1b5c1c152e5bbc080e913f09fc0c37 Mon Sep 17 00:00:00 2001 From: Try Date: Thu, 2 Apr 2026 16:51:06 +0200 Subject: [PATCH 06/20] start to cleanup legacy move code --- game/game/movealgo.cpp | 381 ++++++++--------------------------------- game/game/movealgo.h | 27 +-- 2 files changed, 82 insertions(+), 326 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 26b9a242a..2ba31cade 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -64,8 +64,7 @@ void MoveAlgo::tickMobsi(uint64_t dt) { auto pos = npc.position(); npc.setPosition(pos+dp); } - setAsSlide(false); - setInAir (false); + setState(NoFlags); } bool MoveAlgo::tryMove(float x, float y, float z) { @@ -81,153 +80,6 @@ bool MoveAlgo::tryMove(const Tempest::Vec3& dp, DynamicWorld::CollisionTest& out return npc.tryMove(dp,out); } -bool MoveAlgo::tickSlide(uint64_t dt) { - float fallThreshold = stepHeight(); - auto gpos = npc.position(); - auto pos = npc.collosionCenter(); - - // check ground - bool gValid = false; - auto ground = dropRay (pos, gValid); - auto water = waterRay (pos); - auto norm = normalRay(pos); - float dY = gpos.y-ground; - - if(ground+waterDepthChest()fallThreshold*1.1) { - setInAir (true); - setAsSlide(false); - return false; - } - - DynamicWorld::CollisionTest info; - if(norm.y<=0 || norm.y>=0.99f || !testSlide(gpos,info,true)) { - setAsSlide(false); - return false; - } - - const auto tangent = Tempest::Vec3::crossProduct(norm, Tempest::Vec3(0,1,0)); - const auto slide = Tempest::Vec3::crossProduct(norm, tangent); - - auto dp = fallSpeed*float(dt); - if(tryMove(dp,info)) { - fallSpeed += slide*float(dt)*gravity; - fallCount = 1; - } - else if(tryMove(dp.x,0.f,dp.z,info)) { - fallSpeed += Tempest::Vec3(slide.x, 0.f, slide.z)*float(dt)*gravity; - fallCount = 1; - } - else { - onGravityFailed(info,dt); - } - - /* - if(fallCount>0 && !tryMove(dp.x,dp.y,dp.z,info)) { - onGravityFailed(info,dt); - } else { - fallSpeed += slide*float(dt)*gravity; - fallCount = 1; - }*/ - - npc.setAnimRotate(0); - if(!npc.isDown()) { - if(slideDir()) - npc.setAnim(AnimationSolver::SlideA); else - npc.setAnim(AnimationSolver::SlideB); - } - - setInAir (false); - setAsSlide(true); - return true; - } - -void MoveAlgo::tickGravity(uint64_t dt, MvFlags moveFlg) { - if(npc.isJumpAnim()) { - auto dp = npcMoveSpeed(dt,moveFlg); - tryMove(dp.x,dp.y,dp.z); - fallSpeed += dp; - fallCount += float(dt); - return; - } - - float fallThreshold = stepHeight(); - // falling - if(0.ffallStop || dp.y>0) { - // continue falling - DynamicWorld::CollisionTest info; - if(!tryMove(dp.x,dp.y,dp.z,info)) { - if(!npc.isDead()) - npc.setAnim(AnimationSolver::Fall); - onGravityFailed(info,dt); - fallSpeed.y = std::max(fallSpeed.y, 0.f); - } else { - fallSpeed.y -= gravity*float(dt); - } - - auto gl = npc.guild(); - auto h0 = float(npc.world().script().guildVal().falldown_height[gl]); - float gravity = DynamicWorld::gravity; - float fallTime = fallSpeed.y/gravity; - float height = 0.5f*std::abs(gravity)*fallTime*fallTime; - auto bs = npc.bodyStateMasked(); - - if(height>h0 && !npc.isDead()) { - npc.setAnim(AnimationSolver::FallDeep); - npc.setAnimRotate(0); - setAsFalling(true); - } else - if(fallSpeed.y<-0.3f && !npc.isDead() && bs!=BS_JUMP && bs!=BS_FALL) { - npc.setAnim(AnimationSolver::Fall); - npc.setAnimRotate(0); - } - } else { - if(ground+chest=water && !(!validW && isSwim())) { - DynamicWorld::CollisionTest info; - if(testSlide(pos+dp+Tempest::Vec3(0,fallThreshold,0),info)) - return; - setAsSwim(false); - setAsDive(false); - tryMove(dp.x,ground-pY,dp.z); - return; - } - - if(isDive() && pos.y+chest>water && validW) { - if(npc.world().tickCount()-diveStart>2000) { - setAsDive(false); - return; - } - } - - // swim on top of water - if(!isDive() && validW) { - // Khorinis port hack - for(int i=0; i<=50; i+=10) { - if(tryMove(dp.x,water-chest-pY+float(i),dp.z)) - break; - } - return; - } - - if(!isDive() && !validW) { - setAsDive(false); - setAsSwim(false); - setInAir (groundplain && dY > stickThreshold && state!=InWater)) { + if(!gValid || (pos.y>ground && dY > stickThreshold && state!=InWater)) { if(!gValid && swim) { // sea monster condition? } @@ -549,6 +357,29 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { return true; } + if(state==Slide && !npc.isDown()) { + if(!testSlide(pos,info)) { + setState(NoFlags); + return true; + } + const auto norm = normalRay(pos); + const auto tangent = Tempest::Vec3::crossProduct(norm, Tempest::Vec3(0,1,0)); + const auto slide = Tempest::Vec3::crossProduct(norm, tangent); + + fallSpeed += slide*float(dt)*gravity; + //fallCount = 1; + if(gValid && std::abs(dY)=pos.y) { // same as wall npc.setPosition(pos0); @@ -589,7 +420,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { } const auto adjPos = npc.position() + Tempest::Vec3(0,-dY,0); - if(gValid && (dY>0 || npc.testMove(adjPos))) { + if(gValid && dY <= stickThreshold && npc.testMove(adjPos)) { setState(NoFlags); if(ground==pos.y) return true; @@ -625,7 +456,7 @@ void MoveAlgo::accessDamFly(float dx, float dz) { fallSpeed = vec*1.f; fallCount = 0; - setInAir(true); + setState(InAir); } } @@ -684,6 +515,15 @@ Tempest::Vec3 MoveAlgo::npcMoveSpeed(uint64_t dt, MvFlags moveFlg) { return dp; } +Tempest::Vec3 MoveAlgo::npcFallSpeed(uint64_t dt) { + // falling + if(0.feps || std::fabs(cache.y-pos.y)>eps || std::fabs(cache.z-pos.z)>eps) { float threshold = npc.visual.pose().translateY()+1.f; float dy = threshold+100; // 1 meter extra offset - if(fallSpeed.y<0) + if(fallSpeed.y<0 || true) dy = 0; // whole world const auto spos = Tempest::Vec3(pos.x, pos.y+threshold, pos.z); static_cast(cache) = npc.world().physic()->landRay(spos,dy); diff --git a/game/game/movealgo.h b/game/game/movealgo.h index bd997ffaf..804466660 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -79,16 +79,14 @@ class MoveAlgo final { bool checkLastBounce() const; private: - void tickMobsi (uint64_t dt); - bool tickSlide (uint64_t dt); - void tickGravity(uint64_t dt, MvFlags moveFlg); - void tickSwim (uint64_t dt); - void tickClimb (uint64_t dt); - void tickJumpup (uint64_t dt); + void tickMobsi (uint64_t dt); + void tickClimb (uint64_t dt); + void tickJumpup(uint64_t dt); + bool implTick (uint64_t dt, MvFlags fai); - bool tryMove (float x, float y, float z); - bool tryMove (float x, float y, float z, DynamicWorld::CollisionTest& out); - bool tryMove (const Tempest::Vec3& dp, DynamicWorld::CollisionTest& out); + bool tryMove (float x, float y, float z); + bool tryMove (float x, float y, float z, DynamicWorld::CollisionTest& out); + bool tryMove (const Tempest::Vec3& dp, DynamicWorld::CollisionTest& out); enum Flags : uint32_t { NoFlags = 0, @@ -102,16 +100,7 @@ class MoveAlgo final { Dive = 1<<8, Jump = 1<<9, }; - void setState(Flags f); - void setInAir (bool f); - void setAsJumpup (bool f); - void setAsClimb (bool f); - void setAsSlide (bool f); - void setInWater (bool f); - void setAsSwim (bool f); - void setAsDive (bool f); - void setAsFalling(bool f); bool slideDir() const; bool isForward(const Tempest::Vec3& dp) const; @@ -121,9 +110,9 @@ class MoveAlgo final { void applyRotation(Tempest::Vec3& out, const Tempest::Vec3& in, float radians) const; auto animMoveSpeed(uint64_t dt) const -> Tempest::Vec3; auto npcMoveSpeed (uint64_t dt, MvFlags moveFlg) -> Tempest::Vec3; + auto npcFallSpeed (uint64_t dt) -> Tempest::Vec3; auto go2NpcMoveSpeed (const Tempest::Vec3& dp, const Npc &tg) -> Tempest::Vec3; auto go2WpMoveSpeed (Tempest::Vec3 dp, const Tempest::Vec3& to) -> Tempest::Vec3; - bool implTick(uint64_t dt, MvFlags fai); void onMoveFailed(const Tempest::Vec3& dp, const DynamicWorld::CollisionTest& info, uint64_t dt); void onGravityFailed(const DynamicWorld::CollisionTest& info, uint64_t dt); From 860fafc3a3bb8fc86305d3fe769a7454d2d45605 Mon Sep 17 00:00:00 2001 From: Try Date: Fri, 3 Apr 2026 01:35:52 +0200 Subject: [PATCH 07/20] cleanups --- game/game/movealgo.cpp | 107 ++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 33 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 2ba31cade..b44bd4797 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -145,9 +145,20 @@ void MoveAlgo::tickClimb(uint64_t dt) { } void MoveAlgo::tick(uint64_t dt, MvFlags moveFlg) { + if(npc.isDown() && (flags==Swim || flags==Dive)) { + // 'falling' to bottom of the lake + setState(InAir); + } + + if(npc.interactive()!=nullptr) { + tickMobsi(dt); + return; + } + if(!implTick(dt,moveFlg)) return; + /* const auto pos = npc.position(); const float chest = waterDepthChest(); const float knee = waterDepthKnee(); @@ -178,6 +189,7 @@ void MoveAlgo::tick(uint64_t dt, MvFlags moveFlg) { setState(InWater); } } + */ if(cache.sector!=nullptr && portal!=cache.sector) { formerPortal = portal; @@ -190,10 +202,6 @@ void MoveAlgo::tick(uint64_t dt, MvFlags moveFlg) { } bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { - if(npc.interactive()!=nullptr) { - tickMobsi(dt); - return true; - } if(flags==ClimbUp) { tickClimb(dt); //fixup: collision return true; @@ -207,7 +215,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { const bool dead = npc.isDead(); const bool swim = (state==Swim); const bool dive = (state==Dive); - const bool grav = (state==InAir || state==JumpUp); + const bool grav = (state==InAir || state==Falling || state==JumpUp); const auto bs = npc.bodyStateMasked(); const auto pos0 = npc.position(); const auto dp = (!grav && state!=Slide) ? npcMoveSpeed(dt,moveFlg) : npcFallSpeed(dt); @@ -247,6 +255,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { const auto pos = npc.position(); const float stickThreshold = grav ? 0.f : stepHeight(); const float chest = waterDepthChest(); + const float knee = waterDepthKnee(); bool gValid = false; auto ground = dropRay (pos, gValid); @@ -263,7 +272,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { dY = std::min((pos.y+chest)-water, stickThreshold); } - if(dp==Tempest::Vec3() && pos.y==ground) + if(dp==Tempest::Vec3() && pos.y==ground && !grav) return false; // jump animation (lift off) @@ -294,19 +303,44 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { // } } - // blood-fly - if(canFlyOverWater() && groundh0) { - npc.setAnim(AnimationSolver::FallDeep); - npc.setAnimRotate(0); - setState(Falling); - } else - if(fallSpeed.y<-0.3f && bs!=BS_JUMP && bs!=BS_FALL) { - npc.setAnim(AnimationSolver::Fall); - npc.setAnimRotate(0); - } - } - // above ground/void - if(!gValid || (pos.y>ground && dY > stickThreshold && state!=InWater)) { + if(!gValid || (pos.y>ground && dY >= stickThreshold && state!=InWater)) { if(!gValid && swim) { // sea monster condition? } @@ -352,8 +369,28 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { onMoveFailed(dp,info,dt); return false; } - if(!swim && !dive) - setState(InAir); + if(!swim && !dive && !dead) { + // fall animations + const float h0 = falldownHeight(); + + float fallTime = fallSpeed.y/gravity; + float height = 0.5f*std::abs(gravity)*fallTime*fallTime; + + if(height>h0) { + npc.setAnim(AnimationSolver::FallDeep); + npc.setAnimRotate(0); + setState(Falling); + } + else if(fallSpeed.y<-0.3f && bs!=BS_JUMP && bs!=BS_FALL) { + npc.setAnim(AnimationSolver::Fall); + npc.setAnimRotate(0); + setState(InAir); + } + else { + npc.setAnimRotate(0); + setState(InAir); + } + } return true; } @@ -380,8 +417,8 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { return true; } - // no longer in air - if(state==InAir) { + // no longer in air - ground code + if(state==InAir || state==Falling) { // attach to ground npc.takeFallDamage(fallSpeed); clearSpeed(); @@ -398,6 +435,10 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { } if(testSlide(pos,info)) { + if(state==InWater || state==Swim) { + npc.setPosition(pos0); + return false; + } if(ground>=pos.y) { // same as wall npc.setPosition(pos0); From 3f0d07c6e0f1a34f367db83d8fa3610cc481b0ae Mon Sep 17 00:00:00 2001 From: Try Date: Fri, 3 Apr 2026 14:03:56 +0200 Subject: [PATCH 08/20] use persistent water tracking more often --- game/camera.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/camera.cpp b/game/camera.cpp index 689c268d9..40564317d 100644 --- a/game/camera.cpp +++ b/game/camera.cpp @@ -753,7 +753,7 @@ void Camera::tick(uint64_t dt) { auto pl = isFree() ? nullptr : world->player(); auto& physic = *world->physic(); - if(pl!=nullptr && !(pl->isInWater() || pl->isSwim() || pl->isDive())) { + if(pl!=nullptr && (pl->isInAir() || pl->isJump())) { // NOTE: not quite correct inWater = physic.cameraRay(inter.target, origin).waterCol % 2; } else { From 8df66bc997c6efab6fd5203817f70a1c2abe80fd Mon Sep 17 00:00:00 2001 From: Try Date: Sat, 4 Apr 2026 20:44:29 +0200 Subject: [PATCH 09/20] refactor state flags for move --- game/game/movealgo.cpp | 137 +++++++++++++++++++++-------------------- game/game/movealgo.h | 26 ++++---- 2 files changed, 84 insertions(+), 79 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index b44bd4797..2c29e0418 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -64,7 +64,7 @@ void MoveAlgo::tickMobsi(uint64_t dt) { auto pos = npc.position(); npc.setPosition(pos+dp); } - setState(NoFlags); + setState(Run); } bool MoveAlgo::tryMove(float x, float y, float z) { @@ -113,7 +113,7 @@ void MoveAlgo::tickJumpup(uint64_t dt) { void MoveAlgo::tickClimb(uint64_t dt) { if(npc.bodyStateMasked()!=BS_CLIMB) { - setState(NoFlags); + setState(Run); Tempest::Vec3 p={}, v={0,0,climbMove}; applyRotation(p,v); @@ -158,39 +158,6 @@ void MoveAlgo::tick(uint64_t dt, MvFlags moveFlg) { if(!implTick(dt,moveFlg)) return; - /* - const auto pos = npc.position(); - const float chest = waterDepthChest(); - const float knee = waterDepthKnee(); - - // from cache, but not ideal - bool gValid = false; - auto ground = dropRay (pos, gValid); - auto water = waterRay(pos); - - if(!std::isfinite(water) && flags==Swim) { - setState(NoFlags); - } - else if(!canFlyOverWater() && !npc.isDead()) { - if(std::max(pos.y, ground) + 3.f*chest <= water) { - // underwater walk bug-like case: can switch to dive here - // setState(Dive); - } - else if(std::max(pos.y, ground) + chest <= water+0.01f) { - const bool splash = isInAir(); - if(flags!=Dive) - setState(Swim); - if(splash) - emitWaterSplash(water); - if(!npc.hasSwimAnimations()) - npc.takeDrownDamage(); - } - else if(std::max(pos.y, ground) + knee <= water && flags!=InAir && flags!=Slide) { - setState(InWater); - } - } - */ - if(cache.sector!=nullptr && portal!=cache.sector) { formerPortal = portal; portal = cache.sector; @@ -228,10 +195,10 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { onGravityFailed(info,dt); setState(InAir); } - else if(state==InAir) { + else if(grav) { onGravityFailed(info,dt); } - else if(state==Swim) { + else if(swim) { // Khorinis port hack for(int i=0; i<=50; i+=10) { if(tryMove(Tempest::Vec3(dp.x,dp.y+float(i),dp.z), info)) @@ -272,7 +239,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { dY = std::min((pos.y+chest)-water, stickThreshold); } - if(dp==Tempest::Vec3() && pos.y==ground && !grav) + if(dp==Tempest::Vec3() && pos.y==ground && !grav && state!=Jump) return false; // jump animation (lift off) @@ -305,7 +272,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { // blood-fly over water if(canFlyOverWater() && !npc.isDead() && ground= 1.f; setState(Swim); - if(splash) - emitWaterSplash(water); - if(!npc.hasSwimAnimations()) - npc.takeDrownDamage(); - return true; + if(splash) + emitWaterSplash(water); + if(!npc.hasSwimAnimations()) + npc.takeDrownDamage(); + clearSpeed(); + return true; + } } - else if(std::max(pos.y, ground) + knee <= water && state!=InAir && state!=Slide) { + else if(gpos + knee <= water && state!=InAir && state!=Slide && state!=Dive) { // swimming toward cliff-slide if(swim && testSlide(pos,info)) { npc.setPosition(pos0); @@ -339,7 +309,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { if(swim) { if(dead || !std::isfinite(water)) { - setState(NoFlags); + setState(Run); } else { npc.tryTranslate(Tempest::Vec3(pos.x, water-chest, pos.z)); } @@ -396,7 +366,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { if(state==Slide && !npc.isDown()) { if(!testSlide(pos,info)) { - setState(NoFlags); + setState(Run); return true; } const auto norm = normalRay(pos); @@ -422,7 +392,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { // attach to ground npc.takeFallDamage(fallSpeed); clearSpeed(); - setState(NoFlags); + setState(Run); } if(ground+chest < water && !npc.hasSwimAnimations()) { @@ -462,7 +432,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { const auto adjPos = npc.position() + Tempest::Vec3(0,-dY,0); if(gValid && dY <= stickThreshold && npc.testMove(adjPos)) { - setState(NoFlags); + setState(Run); if(ground==pos.y) return true; if(ground<=pos.y) { @@ -756,46 +726,79 @@ void MoveAlgo::startDive() { } bool MoveAlgo::isFalling() const { - return flags&Falling; + return flags==Falling; } bool MoveAlgo::isSlide() const { - return flags&Slide; + return flags==Slide; } bool MoveAlgo::isInAir() const { - return flags&InAir; + return flags==InAir; } bool MoveAlgo::isJumpUp() const { - return flags&JumpUp; + return flags==JumpUp; } bool MoveAlgo::isClimb() const { - return flags&ClimbUp; + return flags==ClimbUp; } bool MoveAlgo::isInWater() const { - return flags&InWater; + return flags==InWater; } bool MoveAlgo::isSwim() const { - return flags&Swim; + return flags==Swim; } bool MoveAlgo::isDive() const { - return flags&Dive; + return flags==Dive; } bool MoveAlgo::isJump() const { - return flags&Jump; + return flags==Jump; } -void MoveAlgo::setState(Flags f) { +void MoveAlgo::setState(State f) { if(f==flags) return; - if((f&Swim) && !(flags&Swim)) { + // assert possible transitions + switch(flags) { + case Run: + assert(f!=Falling); + break; + case InAir: + assert(f==Run || f==InWater || f==Swim || f==Dive); + break; + case Falling: + assert(f==Run || f==InWater || f==Dive); + break; + case Slide: + break; + case Jump: + assert(f==Run || f==InAir || f==Falling || f==Swim || f==Dive); + break; + case JumpUp: + assert(f==Run || f==InAir || f==ClimbUp); + break; + case ClimbUp: + assert(f==Run); + break; + case InWater: + assert(f==Run || f==Slide || f==JumpUp || f==Swim || f==Dive); + break; + case Swim: + assert(f==Run || f==InWater || f==Dive); + break; + case Dive: + assert(f==Swim || f==InWater); + break; + } + + if((f==Swim) && !(flags==Swim)) { auto ws = npc.weaponState(); npc.setAnim(Npc::Anim::NoAnim); if(ws!=WeaponState::NoWeapon && ws!=WeaponState::Fist) @@ -803,13 +806,15 @@ void MoveAlgo::setState(Flags f) { npc.dropTorch(true); } - if((f&Dive) && !(flags&Dive)) { + if((f==Dive) && !(flags==Dive)) { npc.setDirectionY(-40); } - if((f&Dive) != (flags&Dive)) { + if((f==Dive) != (flags==Dive)) { diveStart = npc.world().tickCount(); } + // handle fly-speed here? + flags = f; } diff --git a/game/game/movealgo.h b/game/game/movealgo.h index 804466660..da0f953f9 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -88,19 +88,19 @@ class MoveAlgo final { bool tryMove (float x, float y, float z, DynamicWorld::CollisionTest& out); bool tryMove (const Tempest::Vec3& dp, DynamicWorld::CollisionTest& out); - enum Flags : uint32_t { - NoFlags = 0, - InAir = 1<<1, - Falling = 1<<2, - Slide = 1<<3, - JumpUp = 1<<4, - ClimbUp = 1<<5, - InWater = 1<<6, - Swim = 1<<7, - Dive = 1<<8, - Jump = 1<<9, + enum State : uint32_t { + Run = 0, + InAir, + Falling, + Slide, + Jump, + JumpUp, + ClimbUp, + InWater, + Swim, + Dive, }; - void setState(Flags f); + void setState(State f); bool slideDir() const; bool isForward(const Tempest::Vec3& dp) const; @@ -141,7 +141,7 @@ class MoveAlgo final { std::string_view portal; std::string_view formerPortal; - Flags flags = NoFlags; + State flags = Run; float mulSpeed =1.f; Tempest::Vec3 fallSpeed ={}; From 4d34af46b83cb4085f43d95458456ff635a53dcb Mon Sep 17 00:00:00 2001 From: Try Date: Sat, 4 Apr 2026 23:23:08 +0200 Subject: [PATCH 10/20] cleanup water walk code --- game/camera.cpp | 8 ++++++-- game/game/movealgo.cpp | 30 ++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/game/camera.cpp b/game/camera.cpp index 40564317d..ad9c5aea2 100644 --- a/game/camera.cpp +++ b/game/camera.cpp @@ -753,10 +753,14 @@ void Camera::tick(uint64_t dt) { auto pl = isFree() ? nullptr : world->player(); auto& physic = *world->physic(); - if(pl!=nullptr && (pl->isInAir() || pl->isJump())) { + if(pl!=nullptr && pl->isSwim()) { + inWater = (angles.x < 0); + } + else if(pl!=nullptr && (pl->isInAir() || pl->isJump())) { // NOTE: not quite correct inWater = physic.cameraRay(inter.target, origin).waterCol % 2; - } else { + } + else { // NOTE: find a way to avoid persistent tracking inWater = inWater ^ (physic.cameraRay(prev, origin).waterCol % 2); } diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 2c29e0418..542e0fcb8 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -271,7 +271,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { } // blood-fly over water - if(canFlyOverWater() && !npc.isDead() && ground Date: Sat, 4 Apr 2026 23:42:37 +0200 Subject: [PATCH 11/20] move cleanups --- game/camera.cpp | 2 +- game/game/movealgo.cpp | 46 ++++++++++++++++++++++++------------------ game/game/movealgo.h | 1 + 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/game/camera.cpp b/game/camera.cpp index ad9c5aea2..f28f748c0 100644 --- a/game/camera.cpp +++ b/game/camera.cpp @@ -754,7 +754,7 @@ void Camera::tick(uint64_t dt) { auto& physic = *world->physic(); if(pl!=nullptr && pl->isSwim()) { - inWater = (angles.x < 0); + inWater = (angles.x < -8.f); } else if(pl!=nullptr && (pl->isInAir() || pl->isJump())) { // NOTE: not quite correct diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 542e0fcb8..93d6a06e4 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -779,6 +779,31 @@ void MoveAlgo::setState(State f) { if(f==flags) return; +#ifndef NDEBUG + assertStateChange(f); +#endif + + if((f==Swim) && !(flags==Swim)) { + auto ws = npc.weaponState(); + npc.setAnim(Npc::Anim::NoAnim); + if(ws!=WeaponState::NoWeapon && ws!=WeaponState::Fist) + npc.closeWeapon(true); + npc.dropTorch(true); + } + + if((f==Dive) && !(flags==Dive)) { + npc.setDirectionY(-40); + } + if((f==Dive) != (flags==Dive)) { + diveStart = npc.world().tickCount(); + } + + // handle fly-speed here? + + flags = f; + } + +void MoveAlgo::assertStateChange(State f) { // assert possible transitions switch(flags) { case Run: @@ -788,7 +813,7 @@ void MoveAlgo::setState(State f) { assert(f==Run || f==Falling || f==InWater || f==Swim || f==Dive); break; case Falling: - assert(f==Run || f==InWater || f==Dive); + assert(f==Run || f==InAir || f==InWater || f==Swim || f==Dive); break; case Slide: break; @@ -811,25 +836,6 @@ void MoveAlgo::setState(State f) { assert(f==Swim || f==InWater); break; } - - if((f==Swim) && !(flags==Swim)) { - auto ws = npc.weaponState(); - npc.setAnim(Npc::Anim::NoAnim); - if(ws!=WeaponState::NoWeapon && ws!=WeaponState::Fist) - npc.closeWeapon(true); - npc.dropTorch(true); - } - - if((f==Dive) && !(flags==Dive)) { - npc.setDirectionY(-40); - } - if((f==Dive) != (flags==Dive)) { - diveStart = npc.world().tickCount(); - } - - // handle fly-speed here? - - flags = f; } bool MoveAlgo::slideDir() const { diff --git a/game/game/movealgo.h b/game/game/movealgo.h index da0f953f9..2cb45b04f 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -101,6 +101,7 @@ class MoveAlgo final { Dive, }; void setState(State f); + void assertStateChange(State f); bool slideDir() const; bool isForward(const Tempest::Vec3& dp) const; From 47f5bdc6c5e5adabaa057ee48c59d9c7208d2874 Mon Sep 17 00:00:00 2001 From: Try Date: Sun, 5 Apr 2026 00:54:04 +0200 Subject: [PATCH 12/20] perf --- game/game/movealgo.cpp | 24 +++++++++++++----------- game/game/movealgo.h | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 93d6a06e4..870b9cb13 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -219,6 +219,9 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { fallSpeed.y -= gravity*float(dt); } + //if(dp==Tempest::Vec3() && !grav && state!=Jump && state!=Slide) + // return false; + const auto pos = npc.position(); const float stickThreshold = grav ? 0.f : stepHeight(); const float chest = waterDepthChest(); @@ -227,7 +230,6 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { bool gValid = false; auto ground = dropRay (pos, gValid); auto water = waterRay(pos); - // auto plain = canFlyOverWater() ? std::max(water,ground) : ground; float dY = pos.y-ground; if(canFlyOverWater() && ground=pos.y) { // inside ground - npc.setPosition(adjPos); + // npc.setPosition(adjPos); + npc.tryMove(Tempest::Vec3(0,-dY,0)); return true; } } @@ -886,7 +890,7 @@ void MoveAlgo::onMoveFailed(const Tempest::Vec3& dp, const DynamicWorld::Collisi if(npc.processPolicy()!=NpcProcessPolicy::Player) lastBounce = npc.world().tickCount(); - if(std::abs(val)>=threshold && !info.preFall) { + if(std::abs(val)>=threshold && !info.preFall && checkLastBounce()) { // emulate bouncing behaviour of original game Tempest::Vec3 corr; for(int i=5; i<=35; i+=5) { @@ -975,7 +979,7 @@ void MoveAlgo::onGravityFailed(const DynamicWorld::CollisionTest& info, uint64_t } } -float MoveAlgo::waterRay(const Tempest::Vec3& pos, bool* hasCol) const { +float MoveAlgo::waterRay(const Tempest::Vec3& pos) const { if(std::fabs(cacheW.x-pos.x)>eps || std::fabs(cacheW.y-pos.y)>eps || std::fabs(cacheW.z-pos.z)>eps) { const float threshold = -0.1f; //npc.visual.pose().translateY()+1.f; const auto spos = Tempest::Vec3(pos.x, pos.y+threshold, pos.z); @@ -984,8 +988,6 @@ float MoveAlgo::waterRay(const Tempest::Vec3& pos, bool* hasCol) const { cacheW.y = pos.y; cacheW.z = pos.z; } - if(hasCol!=nullptr) - *hasCol = cacheW.hasCol; return cacheW.wdepth; } @@ -993,7 +995,7 @@ void MoveAlgo::rayMain(const Tempest::Vec3& pos) const { if(std::fabs(cache.x-pos.x)>eps || std::fabs(cache.y-pos.y)>eps || std::fabs(cache.z-pos.z)>eps) { float threshold = npc.visual.pose().translateY()+1.f; float dy = threshold+100; // 1 meter extra offset - if(fallSpeed.y<0 || true) + if(fallSpeed.y<0 || false) dy = 0; // whole world const auto spos = Tempest::Vec3(pos.x, pos.y+threshold, pos.z); static_cast(cache) = npc.world().physic()->landRay(spos,dy); diff --git a/game/game/movealgo.h b/game/game/movealgo.h index 2cb45b04f..3cad6de18 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -126,7 +126,7 @@ class MoveAlgo final { void rayMain (const Tempest::Vec3& pos) const; float dropRay (const Tempest::Vec3& pos, bool& hasCol) const; - float waterRay (const Tempest::Vec3& pos, bool* hasCol = nullptr) const; + float waterRay (const Tempest::Vec3& pos) const; auto normalRay(const Tempest::Vec3& pos) const -> Tempest::Vec3; struct CacheLand : DynamicWorld::RayLandResult { From 05cad00da2a1971b40314ece22244126dc326d7e Mon Sep 17 00:00:00 2001 From: Try Date: Mon, 6 Apr 2026 15:02:00 +0200 Subject: [PATCH 13/20] refactor move states --- game/game/movealgo.cpp | 18 +++++++++++------- game/game/movealgo.h | 28 +++++++++++++++------------- game/world/objects/npc.cpp | 16 ++++++++-------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 870b9cb13..76812d8a0 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -200,6 +200,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { } else if(swim) { // Khorinis port hack + /* for(int i=0; i<=50; i+=10) { if(tryMove(Tempest::Vec3(dp.x,dp.y+float(i),dp.z), info)) break; @@ -208,6 +209,9 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { return false; } } + */ + onMoveFailed(dp,info,dt); + return false; } else { onMoveFailed(dp,info,dt); @@ -295,7 +299,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { } else if(gpos + chest <= water+0.01f) { if(state!=Swim && state!=Dive) { - const bool splash = isInAir() || fallSpeed.quadLength() >= 1.f; + const bool splash = grav || fallSpeed.quadLength() >= 1.f; setState(Swim); if(splash) emitWaterSplash(water); @@ -368,6 +372,10 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { npc.setAnimRotate(0); setState(InAir); } + else if(state==InWater) { + npc.setAnimRotate(0); + setState(Swim); + } else { npc.setAnimRotate(0); setState(InAir); @@ -775,10 +783,6 @@ bool MoveAlgo::isDive() const { return flags==Dive; } -bool MoveAlgo::isJump() const { - return flags==Jump; - } - void MoveAlgo::setState(State f) { if(f==flags) return; @@ -834,10 +838,10 @@ void MoveAlgo::assertStateChange(State f) { assert(f==Run || f==Slide || f==JumpUp || f==Swim || f==Dive); break; case Swim: - assert(f==Run || f==InWater || f==Dive); + assert(f==Run || f==InAir || f==InWater || f==Dive); break; case Dive: - assert(f==Swim || f==InWater); + assert(f==InAir || f==Swim || f==InWater); break; } } diff --git a/game/game/movealgo.h b/game/game/movealgo.h index 3cad6de18..a5b862922 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -34,6 +34,19 @@ class MoveAlgo final { WaitMove = 1<<1, }; + enum State : uint32_t { + Run = 0, + InAir, + Falling, + Slide, + Jump, + JumpUp, + ClimbUp, + InWater, + Swim, + Dive, + }; + static bool isClose(const Npc& npc, const Npc& p, float dist); static bool isClose(const Npc& npc, const WayPoint& p); static bool isClose(const Npc& npc, const WayPoint& p, float dist); @@ -61,7 +74,8 @@ class MoveAlgo final { bool isInWater() const; bool isSwim() const; bool isDive() const; - bool isJump() const; + + auto state() const { return flags; } zenkit::MaterialGroup groundMaterial() const; auto groundNormal() const -> Tempest::Vec3; @@ -88,18 +102,6 @@ class MoveAlgo final { bool tryMove (float x, float y, float z, DynamicWorld::CollisionTest& out); bool tryMove (const Tempest::Vec3& dp, DynamicWorld::CollisionTest& out); - enum State : uint32_t { - Run = 0, - InAir, - Falling, - Slide, - Jump, - JumpUp, - ClimbUp, - InWater, - Swim, - Dive, - }; void setState(State f); void assertStateChange(State f); diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index 9d184318a..44de70d1f 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -1038,7 +1038,7 @@ bool Npc::isFlyAnim() const { } bool Npc::isFalling() const { - return mvAlgo.isFalling(); + return mvAlgo.state()==MoveAlgo::Falling; } bool Npc::isFallingDeep() const { @@ -1046,19 +1046,19 @@ bool Npc::isFallingDeep() const { } bool Npc::isSlide() const { - return mvAlgo.isSlide(); + return mvAlgo.state()==MoveAlgo::Slide; } bool Npc::isInAir() const { - return mvAlgo.isInAir(); + return mvAlgo.state()==MoveAlgo::InAir; } bool Npc::isJump() const { - return mvAlgo.isJump(); + return mvAlgo.state()==MoveAlgo::Jump; } bool Npc::isJumpUp() const { - return mvAlgo.isJumpUp(); + return mvAlgo.state()==MoveAlgo::JumpUp; } void Npc::invalidateTalentOverlays() { @@ -1516,7 +1516,7 @@ bool Npc::implAttack(uint64_t dt) { const auto act = fghAlgo.nextFromQueue(*this,*currentTarget,owner.script()); // vanilla behavior, required for orcs in G1 orcgraveyard - if(ws==WeaponState::NoWeapon && isAiQueueEmpty()) { + if(ws==WeaponState::NoWeapon && isAiQueueEmpty() && mvAlgo.state()==MoveAlgo::Run) { drawWeaponMelee(); return true; } @@ -1960,7 +1960,7 @@ void Npc::takeDamage(Npc& other, const Bullet* b, const CollideMask bMask, int32 } if(hitResult.hasHit) { - if(bodyStateMasked()!=BS_UNCONSCIOUS && interactive()==nullptr && !isSwim() && !mvAlgo.isClimb()) { + if(bodyStateMasked()!=BS_UNCONSCIOUS && interactive()==nullptr && !mvAlgo.isSwim() && !mvAlgo.isClimb()) { const bool noInter = (hnpc->bodystate_interruptable_override!=0); if(!noInter) { //NOTE: kepp rotation animation: this results in more accurate fight with trolls @@ -3616,7 +3616,7 @@ bool Npc::drawMage(uint8_t slot) { } bool Npc::drawSpell(int32_t spell) { - if(isFalling() || mvAlgo.isSwim() || bodyStateMasked()==BS_CASTING) + if(mvAlgo.isFalling() || mvAlgo.isSwim() || bodyStateMasked()==BS_CASTING) return false; auto weaponSt=weaponState(); if(weaponSt!=WeaponState::NoWeapon && weaponSt!=WeaponState::Mage) { From 58f7934392c376319c44431503e5c6a6b69df561 Mon Sep 17 00:00:00 2001 From: Try Date: Mon, 6 Apr 2026 15:02:57 +0200 Subject: [PATCH 14/20] use actual col-box --- game/graphics/mesh/skeleton.cpp | 6 +----- game/physics/dynamicworld.cpp | 11 ++++------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/game/graphics/mesh/skeleton.cpp b/game/graphics/mesh/skeleton.cpp index 19be7f745..8a16ca91e 100644 --- a/game/graphics/mesh/skeleton.cpp +++ b/game/graphics/mesh/skeleton.cpp @@ -11,13 +11,9 @@ Skeleton::Skeleton(const zenkit::ModelHierarchy& src, const Animation* anim, std bbox[0] = {src.bbox.min.x, src.bbox.min.y, src.bbox.min.z}; bbox[1] = {src.bbox.max.x, src.bbox.max.y, src.bbox.max.z}; -#if 0 +#if 1 bboxCol[0] = {src.collision_bbox.min.x, src.collision_bbox.min.y, src.collision_bbox.min.z}; bboxCol[1] = {src.collision_bbox.max.x, src.collision_bbox.max.y, src.collision_bbox.max.z}; - - // bbox size apears to be halfed in source file - // bboxCol[0] *= 2.f; - // bboxCol[1] *= 2.f; #else //NOTE: 'collision_bbox' doesn't match marvin view bboxCol[0] = {src.bbox.min.x, src.bbox.min.y, src.bbox.min.z}; diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index 204644724..78f425478 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -57,8 +57,9 @@ struct DynamicWorld::NpcBody : btRigidBody { } void setPosition(const Tempest::Vec3& p) { + const float extPadding = 10.f; // Khorinis port hack const float ghostPadding = gPadd; - auto m = p + Tempest::Vec3(0,(h+ghostPadding)*0.5f,0); + auto m = p + Tempest::Vec3(0,h*0.5f + ghostPadding*0.5f + extPadding,0); pos = p; btTransform trans; trans.setIdentity(); @@ -79,15 +80,11 @@ struct DynamicWorld::NpcBodyList final { } NpcBody* create(const Tempest::Vec3 &min, const Tempest::Vec3 &max) { - //Tested: stonegolem in Xardas'es tower - static const float dimMax = 55.f; - auto size = max - min; - float radius = std::min(size.y*0.25f, std::min(size.x, size.z)*0.5f); // npc-to-landscape collision size + float radius = std::min(size.y*0.5f, std::min(size.x, size.z))*0.5f; // npc-to-landscape collision size float height = size.y; - radius = std::min(radius, dimMax); - float ghostPadding = std::max(radius*2.f, 55.f); + float ghostPadding = height*0.5f; float cHeight = std::max(height-2.f*radius-ghostPadding, 0.f); btCollisionShape* shape = new HumShape(radius, cHeight); From 36393560e542e2b8db5d1ce59624e2fc2e016c27 Mon Sep 17 00:00:00 2001 From: Try Date: Mon, 6 Apr 2026 18:45:56 +0200 Subject: [PATCH 15/20] fix lurker-2-water interaction --- game/graphics/mesh/pose.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/graphics/mesh/pose.cpp b/game/graphics/mesh/pose.cpp index f76a20942..bcd20b306 100644 --- a/game/graphics/mesh/pose.cpp +++ b/game/graphics/mesh/pose.cpp @@ -183,7 +183,7 @@ bool Pose::startAnim(const AnimationSolver& solver, const Animation::Sequence *s return false; } const Animation::Sequence* tr=nullptr; - if(i.seq->shortName!=nullptr && sq->shortName!=nullptr) { + if(i.seq->shortName!=nullptr && sq->shortName!=nullptr && i.sAnim!=tickCount) { string_frm tansition("T_",i.seq->shortName,"_2_",sq->shortName); tr = solver.solveFrm(tansition); } From cafba1e4cb1c5fde3801bc070862ffd3d6d54058 Mon Sep 17 00:00:00 2001 From: Try Date: Mon, 6 Apr 2026 22:24:08 +0200 Subject: [PATCH 16/20] npc collision wip --- game/physics/dynamicworld.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index 78f425478..b48b56990 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -48,6 +48,7 @@ struct DynamicWorld::NpcBody : btRigidBody { float r = 0; float h = 0; float gPadd = 0.f; + float stepSz = 0.f; bool enable = true; size_t frozen = size_t(-1); uint64_t lastMove = 0; @@ -87,6 +88,7 @@ struct DynamicWorld::NpcBodyList final { float ghostPadding = height*0.5f; float cHeight = std::max(height-2.f*radius-ghostPadding, 0.f); + //NOTE: it seem vanilla uses elipsoids at some point, at least for npc-2-npc collisions btCollisionShape* shape = new HumShape(radius, cHeight); //btCollisionShape* shape = new btCylinderShape(CollisionWorld::toMeters(Tempest::Vec3(radius, height*0.5f, radius))); //btCollisionShape* shape = new btCapsuleShape(CollisionWorld::toMeters(radius), CollisionWorld::toMeters(height)); @@ -98,10 +100,13 @@ struct DynamicWorld::NpcBodyList final { obj->setUserIndex(C_Ghost); obj->setCollisionFlags(btCollisionObject::CF_NO_CONTACT_RESPONSE); - obj->r = radius; - obj->h = height; - obj->gPadd = ghostPadding; - maxR = std::max(maxR, radius); + // obj->r = radius * 2.f; + // obj->r = std::max(size.x, size.z) * 0.5f; + obj->r = std::min(size.x, size.z); // best so far + obj->h = height; + obj->gPadd = ghostPadding; + obj->stepSz = std::min(cHeight*0.5f, radius); // safe tunneling size + maxR = std::max(maxR, obj->r); add(obj); return obj; @@ -190,7 +195,7 @@ struct DynamicWorld::NpcBodyList final { return true; } - NpcBody* rayTest(const Tempest::Vec3& s, const Tempest::Vec3& e, float extR, const Npc* except) { + auto rayTest(const Tempest::Vec3& s, const Tempest::Vec3& e, float extR, const Npc* except) { NpcBody* ret = nullptr; float minProj = 2; @@ -1091,7 +1096,7 @@ DynamicWorld::MoveCode DynamicWorld::NpcItem::tryMove(const Tempest::Vec3& to, C DynamicWorld::MoveCode DynamicWorld::NpcItem::implTryMove(const Tempest::Vec3& to, const Tempest::Vec3& pos0, CollisionTest& out) { auto initial = pos0; - auto r = obj->r; + auto r = obj->stepSz; int count = 1; auto dp = to-initial; From 3e08a7b8cece5b6b9ab002ed71a7fee16855fdef Mon Sep 17 00:00:00 2001 From: Try Date: Tue, 7 Apr 2026 22:00:13 +0200 Subject: [PATCH 17/20] more cleanups --- game/camera.cpp | 2 +- game/game/movealgo.cpp | 9 ++++----- game/game/movealgo.h | 2 +- game/graphics/mdlvisual.cpp | 2 +- game/graphics/mesh/animationsolver.cpp | 4 ++++ game/graphics/mesh/pose.cpp | 3 ++- game/world/objects/npc.cpp | 6 ++++-- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/game/camera.cpp b/game/camera.cpp index f28f748c0..9b6b9659c 100644 --- a/game/camera.cpp +++ b/game/camera.cpp @@ -756,7 +756,7 @@ void Camera::tick(uint64_t dt) { if(pl!=nullptr && pl->isSwim()) { inWater = (angles.x < -8.f); } - else if(pl!=nullptr && (pl->isInAir() || pl->isJump())) { + else if(pl!=nullptr && (pl->isInAir() || pl->isJump()) && !pl->isDead()) { // NOTE: not quite correct inWater = physic.cameraRay(inter.target, origin).waterCol % 2; } diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 76812d8a0..3bafb8bf3 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -113,6 +113,8 @@ void MoveAlgo::tickJumpup(uint64_t dt) { void MoveAlgo::tickClimb(uint64_t dt) { if(npc.bodyStateMasked()!=BS_CLIMB) { + //NOTE: climb allows npc to violate collision detection, need to readjust + npc.owner.script().fixNpcPosition(npc, 0, 0); setState(Run); Tempest::Vec3 p={}, v={0,0,climbMove}; @@ -578,13 +580,10 @@ Tempest::Vec3 MoveAlgo::go2WpMoveSpeed(Tempest::Vec3 dp, const Tempest::Vec3& to return dp; } -bool MoveAlgo::testSlide(const Tempest::Vec3& pos, DynamicWorld::CollisionTest& out, bool cont) const { - if(isInAir() || npc.bodyStateMasked()==BS_JUMP) - return false; //note: unused? - +bool MoveAlgo::testSlide(const Tempest::Vec3& pos, DynamicWorld::CollisionTest& out) const { // check ground const auto norm = normalRay(pos); - const float slideBegin = std::min(slideAngle() + (cont ? 0.1f : 0.f), 1.f); + const float slideBegin = std::min(slideAngle(), 1.f); const float slideEnd = slideAngle2(); out.normal = norm; diff --git a/game/game/movealgo.h b/game/game/movealgo.h index a5b862922..f07ec41f7 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -61,7 +61,7 @@ class MoveAlgo final { void clearSpeed(); void accessDamFly(float dx,float dz); - bool testSlide(const Tempest::Vec3& p, DynamicWorld::CollisionTest& out, bool cont = false) const; + bool testSlide(const Tempest::Vec3& p, DynamicWorld::CollisionTest& out) const; bool startClimb(JumpStatus ani); void startDive(); diff --git a/game/graphics/mdlvisual.cpp b/game/graphics/mdlvisual.cpp index 2f636c292..b0078a022 100644 --- a/game/graphics/mdlvisual.cpp +++ b/game/graphics/mdlvisual.cpp @@ -790,7 +790,7 @@ void MdlVisual::interrupt() { Tempest::Vec3 MdlVisual::displayPosition() const { if(skeleton!=nullptr) - return {0,skeleton->colisionHeight()*1.15f,0}; + return {0,skeleton->colisionHeight()*1.5f,0}; return {0.f,0.f,0.f}; } diff --git a/game/graphics/mesh/animationsolver.cpp b/game/graphics/mesh/animationsolver.cpp index 8a7eb6b0b..6a3fddc83 100644 --- a/game/graphics/mesh/animationsolver.cpp +++ b/game/graphics/mesh/animationsolver.cpp @@ -357,6 +357,10 @@ const Animation::Sequence* AnimationSolver::implSolveAnim(AnimationSolver::Anim return solveFrm("T_STUMBLE"); if(a==Anim::StumbleB) return solveFrm("T_STUMBLEB"); + + if((a==Anim::DeadA || a==Anim::DeadB) && bool(wlkMode & WalkBit::WM_Dive)){ + return solveFrm("S_DROWNED"); + } if(a==Anim::DeadA) { if(pose.isInAnim("S_WOUNDED") || pose.isInAnim("T_STAND_2_WOUNDED") || pose.isInAnim("S_WOUNDEDB") || pose.isInAnim("T_STAND_2_WOUNDEDB")) diff --git a/game/graphics/mesh/pose.cpp b/game/graphics/mesh/pose.cpp index bcd20b306..8752c8390 100644 --- a/game/graphics/mesh/pose.cpp +++ b/game/graphics/mesh/pose.cpp @@ -396,7 +396,8 @@ bool Pose::updateFrame(const Animation::Sequence &s, BodyState bs, uint64_t sBle smp.position.y = trY; else if(bs==BS_SWIM || bs==BS_DIVE) smp.position.y = trY; - else if(s.isFly()) + //else if(s.isFly()) + else if(bs==BS_JUMP) smp.position.y = trY; //d.translate.y; } diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index 44de70d1f..dd635ed24 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -596,7 +596,7 @@ void Npc::onNoHealth(bool death, HitSound sndMask) { // Note: clear perceptions for William in Jarkentar for(size_t i=0;ivoice>0 && sndMask!=HS_NoSound) { + if(hnpc->voice>0 && sndMask!=HS_NoSound && !isDive()) { emitSoundSVM(svm); } @@ -2223,8 +2223,10 @@ void Npc::tick(uint64_t dt) { if(tickSz>0) { t-=v; int dmg = t/tickSz - (t-int(dt))/tickSz; - if(dmg>0) + if(dmg>0) { + lastHit = nullptr; changeAttribute(ATR_HITPOINTS,-dmg,false); + } } } } From b8cd915da2217a1e5e7abad9179726897ec8b93f Mon Sep 17 00:00:00 2001 From: Try Date: Wed, 8 Apr 2026 22:23:56 +0200 Subject: [PATCH 18/20] rework ground offset for land-ray --- game/game/movealgo.cpp | 4 ++-- game/physics/dynamicworld.cpp | 25 ++++++++++++++++++++----- game/physics/dynamicworld.h | 1 + 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 3bafb8bf3..7b33ddd0b 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -984,7 +984,7 @@ void MoveAlgo::onGravityFailed(const DynamicWorld::CollisionTest& info, uint64_t float MoveAlgo::waterRay(const Tempest::Vec3& pos) const { if(std::fabs(cacheW.x-pos.x)>eps || std::fabs(cacheW.y-pos.y)>eps || std::fabs(cacheW.z-pos.z)>eps) { - const float threshold = -0.1f; //npc.visual.pose().translateY()+1.f; + const float threshold = -0.1f; const auto spos = Tempest::Vec3(pos.x, pos.y+threshold, pos.z); static_cast(cacheW) = npc.world().physic()->waterRay(spos); cacheW.x = pos.x; @@ -996,7 +996,7 @@ float MoveAlgo::waterRay(const Tempest::Vec3& pos) const { void MoveAlgo::rayMain(const Tempest::Vec3& pos) const { if(std::fabs(cache.x-pos.x)>eps || std::fabs(cache.y-pos.y)>eps || std::fabs(cache.z-pos.z)>eps) { - float threshold = npc.visual.pose().translateY()+1.f; + float threshold = npc.physic.groundOffset() + 1.f; float dy = threshold+100; // 1 meter extra offset if(fallSpeed.y<0 || false) dy = 0; // whole world diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index b48b56990..cedf506c9 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -57,10 +57,21 @@ struct DynamicWorld::NpcBody : btRigidBody { return reinterpret_cast(getUserPointer()); } + auto centerPosition() const { + return Tempest::Vec3(pos.x, pos.y+h*0.5f, pos.z); + } + + auto ellipsoidSize() const { + return Tempest::Vec3(r, h*0.5f, r); + } + + auto groundOffset() const { + const float extPadding = 10.f; // Khorinis port hack + return h*0.5f + ghostPadding*0.5f + extPadding; + } + void setPosition(const Tempest::Vec3& p) { - const float extPadding = 10.f; // Khorinis port hack - const float ghostPadding = gPadd; - auto m = p + Tempest::Vec3(0,h*0.5f + ghostPadding*0.5f + extPadding,0); + auto m = p + Tempest::Vec3(0,groundOffset(),0); pos = p; btTransform trans; trans.setIdentity(); @@ -105,7 +116,7 @@ struct DynamicWorld::NpcBodyList final { obj->r = std::min(size.x, size.z); // best so far obj->h = height; obj->gPadd = ghostPadding; - obj->stepSz = std::min(cHeight*0.5f, radius); // safe tunneling size + obj->stepSz = std::min(height*0.5f, radius); // safe tunneling size maxR = std::max(maxR, obj->r); add(obj); @@ -129,7 +140,7 @@ struct DynamicWorld::NpcBodyList final { return false; } - bool del(void* b,std::vector& arr){ + bool del(void* b, std::vector& arr){ for(size_t i=0;igroundOffset(); + } + const Tempest::Vec3& DynamicWorld::NpcItem::position() const { return obj->pos; } diff --git a/game/physics/dynamicworld.h b/game/physics/dynamicworld.h index a616df62d..684d4005a 100644 --- a/game/physics/dynamicworld.h +++ b/game/physics/dynamicworld.h @@ -94,6 +94,7 @@ class DynamicWorld final { auto center() const -> Tempest::Vec3; float centerY() const; + float groundOffset() const; bool testMove(const Tempest::Vec3& to, CollisionTest& out); bool testMove(const Tempest::Vec3& to, const Tempest::Vec3& from, CollisionTest& out); From e1c09caf5691ffecc8c9ccd76952c7d4578e29a3 Mon Sep 17 00:00:00 2001 From: Try Date: Wed, 8 Apr 2026 22:24:11 +0200 Subject: [PATCH 19/20] fix incorrect elevation on some animations --- game/graphics/mesh/pose.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/game/graphics/mesh/pose.cpp b/game/graphics/mesh/pose.cpp index 8752c8390..0ead07d24 100644 --- a/game/graphics/mesh/pose.cpp +++ b/game/graphics/mesh/pose.cpp @@ -396,8 +396,7 @@ bool Pose::updateFrame(const Animation::Sequence &s, BodyState bs, uint64_t sBle smp.position.y = trY; else if(bs==BS_SWIM || bs==BS_DIVE) smp.position.y = trY; - //else if(s.isFly()) - else if(bs==BS_JUMP) + else if(bs==BS_JUMP) //else if(s.isFly()) smp.position.y = trY; //d.translate.y; } From dc4bb829189d3c81b1804b36eb6dbc258a294b2a Mon Sep 17 00:00:00 2001 From: Try Date: Wed, 8 Apr 2026 22:49:45 +0200 Subject: [PATCH 20/20] npc-npc collision in progress --- game/physics/dynamicworld.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index cedf506c9..e6e220ab3 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -281,6 +281,28 @@ struct DynamicWorld::NpcBodyList final { bool hasCollision(const NpcBody& a, const NpcBody& b, Tempest::Vec3& normal){ if(&a==&b) return false; +#if 1 + auto ellipsoidRadius = [](Tempest::Vec3 radius, Tempest::Vec3 direction) { + direction.x /= std::max(radius.x, 1.f); + direction.y /= std::max(radius.y, 1.f); + direction.z /= std::max(radius.z, 1.f); + + direction = Tempest::Vec3::normalize(direction); + return (direction * radius).length(); + }; + + auto direction = a.centerPosition() - b.centerPosition(); + float distance = direction.length(); + + float radiusA = ellipsoidRadius(a.ellipsoidSize(), direction); + float radiusB = ellipsoidRadius(b.ellipsoidSize(), direction); + + if(distance < radiusA + radiusB) { + normal += direction; + return true; + } + return false; +#else auto dx = a.pos.x-b.pos.x, dy = a.pos.y-b.pos.y, dz = a.pos.z-b.pos.z; auto r = a.r+b.r; @@ -292,6 +314,7 @@ struct DynamicWorld::NpcBodyList final { normal.x += dx; normal.y += dy; normal.z += dz; +#endif return true; }