From eb17018252cfbc4da9b6e825491d5b7730ccc53d Mon Sep 17 00:00:00 2001 From: Try Date: Thu, 26 Mar 2026 20:40:23 +0100 Subject: [PATCH 01/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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; } From d0eae2ec934a6f1f4320a673e35ff98d619729f0 Mon Sep 17 00:00:00 2001 From: Try Date: Sat, 11 Apr 2026 19:37:21 +0200 Subject: [PATCH 21/29] optimize physics rays --- game/game/movealgo.cpp | 64 +++++++++++++++++++++++++----------------- game/game/movealgo.h | 1 + 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 7b33ddd0b..91ba37208 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -126,7 +126,7 @@ void MoveAlgo::tickClimb(uint64_t dt) { npc.tryTranslate(Tempest::Vec3(climbPos0.x,climbHeight,climbPos0.z)); npc.tryTranslate(p); } - clearSpeed(); + //clearSpeed(); return; } @@ -188,7 +188,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { const auto bs = npc.bodyStateMasked(); const auto pos0 = npc.position(); const auto dp = (!grav && state!=Slide) ? npcMoveSpeed(dt,moveFlg) : npcFallSpeed(dt); - const bool walk = bool(npc.walkMode() & WalkBit::WM_Walk); + const bool walk = bool(npc.walkMode() & WalkBit::WM_Walk) && (state==Run); DynamicWorld::CollisionTest info; if(!tryMove(dp,info)) { @@ -228,14 +228,15 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { //if(dp==Tempest::Vec3() && !grav && state!=Jump && state!=Slide) // return false; - const auto pos = npc.position(); + 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); - auto water = waterRay(pos); + auto ground = dropRay (pos, gValid); + auto normal = normalRay(pos); + auto water = waterRay (pos); float dY = pos.y-ground; if(canFlyOverWater() && ground=climbHeight) { - // } } // blood-fly over water if(canFlyOverWater() && !dead && ground= 1.f; setState(Swim); if(splash) emitWaterSplash(water); - if(!npc.hasSwimAnimations()) - npc.takeDrownDamage(); - clearSpeed(); + // if(!npc.hasSwimAnimations()) + // npc.takeDrownDamage(); return true; } } else if(gpos + knee <= water && state!=InAir && state!=Slide && state!=Dive) { // swimming toward cliff-slide - if(swim && testSlide(pos,info)) { + if(swim && testSlide(pos,normal,info)) { npc.setPosition(pos0); onMoveFailed(dp,info,dt); return false; @@ -349,7 +345,9 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { if(!gValid && swim) { // sea monster condition? } - if(walk || swim) { + + const bool lowHeight = (dY < falldownHeight()*0.75f); + if((walk || swim) && !(lowHeight && !npc.isPlayer())) { npc.setPosition(pos0); info.normal = dp; @@ -387,11 +385,11 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { } if(state==Slide && !npc.isDown()) { - if(!testSlide(pos,info)) { + const auto norm = normalRay(pos); + if(!testSlide(pos,norm,info)) { setState(Run); 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); @@ -413,7 +411,6 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { if(state==InAir || state==Falling) { // attach to ground npc.takeFallDamage(fallSpeed); - clearSpeed(); setState(Run); } @@ -426,18 +423,25 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { return false; } - if(testSlide(pos,info)) { + if(testSlide(pos,normal,info)) { if(state==InWater || state==Swim) { npc.setPosition(pos0); return false; } - if(ground>=pos.y) { + + if(state!=InAir && Tempest::Vec3::dotProduct(normal,dp)<0.45f) { // same as wall npc.setPosition(pos0); info.preFall = false; onMoveFailed(dp,info,dt); return false; } + if(state==InAir && ground>=pos.y) { + // rough landing + npc.tryMove(Tempest::Vec3(0,-dY,0)); + //npc.setAnim(AnimationSolver::Idle); + npc.setAnim(AnimationSolver::SlideA); + } if(walk) { npc.setPosition(pos0); @@ -446,8 +450,6 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { onMoveFailed(dp,info,dt); return false; } - fallSpeed = Tempest::Vec3(); - fallCount = 0; setState(Slide); return true; } @@ -488,7 +490,7 @@ void MoveAlgo::clearSpeed() { } void MoveAlgo::accessDamFly(float dx, float dz) { - if(flags==0) { + if(flags==Run) { float len = std::sqrt(dx*dx+dz*dz); auto vec = Tempest::Vec3(dx,len*0.5f,dz); vec = vec/vec.length(); @@ -582,7 +584,11 @@ Tempest::Vec3 MoveAlgo::go2WpMoveSpeed(Tempest::Vec3 dp, const Tempest::Vec3& to bool MoveAlgo::testSlide(const Tempest::Vec3& pos, DynamicWorld::CollisionTest& out) const { // check ground - const auto norm = normalRay(pos); + const auto norm = normalRay(pos); + return testSlide(pos, norm, out); + } + +bool MoveAlgo::testSlide(const Tempest::Vec3& pos, const Tempest::Vec3& norm, DynamicWorld::CollisionTest& out) const { const float slideBegin = std::min(slideAngle(), 1.f); const float slideEnd = slideAngle2(); @@ -790,6 +796,14 @@ void MoveAlgo::setState(State f) { assertStateChange(f); #endif + auto isFlyLike = [](State f) { + return f==InAir || f==Falling || f==Slide || f==Jump || f==JumpUp || f==ClimbUp; + }; + + if(isFlyLike(flags) && !isFlyLike(f)) { + clearSpeed(); + } + if((f==Swim) && !(flags==Swim)) { auto ws = npc.weaponState(); npc.setAnim(Npc::Anim::NoAnim); @@ -805,8 +819,6 @@ void MoveAlgo::setState(State f) { diveStart = npc.world().tickCount(); } - // handle fly-speed here? - flags = f; } diff --git a/game/game/movealgo.h b/game/game/movealgo.h index f07ec41f7..aa8e66874 100644 --- a/game/game/movealgo.h +++ b/game/game/movealgo.h @@ -62,6 +62,7 @@ class MoveAlgo final { void accessDamFly(float dx,float dz); bool testSlide(const Tempest::Vec3& p, DynamicWorld::CollisionTest& out) const; + bool testSlide(const Tempest::Vec3& pos, const Tempest::Vec3& norm, DynamicWorld::CollisionTest& out) const; bool startClimb(JumpStatus ani); void startDive(); From 72c2ad28442fb352c5a2485b7c46336b8a1b6ef0 Mon Sep 17 00:00:00 2001 From: Try Date: Mon, 13 Apr 2026 22:49:39 +0200 Subject: [PATCH 22/29] rework angles --- game/camera.cpp | 58 ++++++++++++++--------------- game/game/fightalgo.cpp | 18 ++++++++- game/game/fightalgo.h | 1 + game/game/gamescript.cpp | 2 +- game/game/movealgo.cpp | 4 +- game/game/playercontrol.cpp | 4 +- game/game/serialize.h | 2 +- game/graphics/inventoryrenderer.cpp | 7 ++-- game/graphics/mdlvisual.cpp | 2 +- game/graphics/mesh/pose.cpp | 4 +- game/graphics/mesh/protomesh.cpp | 10 ++--- game/graphics/mesh/protomesh.h | 4 +- game/physics/dynamicworld.cpp | 2 +- game/ui/documentmenu.cpp | 2 +- game/world/objects/interactive.cpp | 6 +-- game/world/objects/item.cpp | 6 +-- game/world/objects/npc.cpp | 21 ++++++++--- game/world/triggers/cscamera.cpp | 4 +- game/world/world.cpp | 4 +- game/world/worldobjects.cpp | 4 +- 20 files changed, 93 insertions(+), 72 deletions(-) diff --git a/game/camera.cpp b/game/camera.cpp index 9b6b9659c..41988a797 100644 --- a/game/camera.cpp +++ b/game/camera.cpp @@ -109,7 +109,7 @@ void Camera::save(Serialize &s) { void Camera::load(Serialize &s, Npc* pl) { reset(pl); - if(s.version()<54) + if(s.version()<55) return; s.read(state.range, state.spin, state.target); s.read(inter.target, inter.rotOffset); @@ -375,7 +375,7 @@ Matrix4x4 Camera::mkViewShadowVsm(const Vec3& cameraPos, const Vec3& ldir) const Matrix4x4 Camera::viewShadow(const Vec3& lightDir, size_t layer) const { auto vp = viewProj(); - float rotation = (180+angles.y); + float rotation = (angles.y-90); // if(layer==0) // return viewShadowVsm(cameraPos,rotation,vp,lightDir); return mkViewShadow(inter.target,rotation,vp,lightDir,layer); @@ -383,7 +383,7 @@ Matrix4x4 Camera::viewShadow(const Vec3& lightDir, size_t layer) const { Matrix4x4 Camera::viewShadowLwc(const Tempest::Vec3& lightDir, size_t layer) const { auto vp = viewProjLwc(); - float rotation = (180+angles.y); + float rotation = (angles.y-90); // if(layer==0) // return viewShadowVsm(cameraPos-origin,rotation,vp,lightDir); return mkViewShadow(inter.target-origin,rotation,vp,lightDir,layer); @@ -566,26 +566,26 @@ void Camera::implMove(Tempest::Event::KeyType key, uint64_t dt) { float dpos = float(dt); float dRot = dpos/15.f; float k = float(M_PI/180.0); - float s = std::sin(angles.y*k), c=std::cos(angles.y*k); + float s = std::sin(-angles.y*k), c=std::cos(-angles.y*k); float sx = std::sin(angles.x*k); float cx = std::cos(angles.x*k); if(key==KeyEvent::K_A) { - origin.x += dpos*c; - origin.z += dpos*s; + origin.x += dpos*s; + origin.z += dpos*c; } if(key==KeyEvent::K_D) { - origin.x -= dpos*c; - origin.z -= dpos*s; + origin.x -= dpos*s; + origin.z -= dpos*c; } if(key==KeyEvent::K_W) { - origin.x += dpos*s*cx; - origin.z -= dpos*c*cx; + origin.x += dpos*c*cx; + origin.z -= dpos*s*cx; origin.y -= dpos*sx; } if(key==KeyEvent::K_S) { - origin.x -= dpos*s*cx; - origin.z += dpos*c*cx; + origin.x -= dpos*c*cx; + origin.z += dpos*s*cx; origin.y += dpos*sx; } if(key==KeyEvent::K_Q) @@ -783,12 +783,10 @@ void Camera::tickFirstPerson(float /*dtF*/) { void Camera::tickThirdPerson(float dtF) { const auto& def = cameraDef(); - auto mkRotMatrix = [](Vec3 spin){ - auto rotOffsetMat = Matrix4x4::mkIdentity(); - rotOffsetMat.rotateOY(180-spin.y); - rotOffsetMat.rotateOX(spin.x); - rotOffsetMat.rotateOZ(spin.z); - return rotOffsetMat; + auto mkRotMatrix = [this](Vec3 spin){ + auto view = Camera::mkRotation(spin); + view.inverse(); + return view; }; auto targetOffset = Vec3(def.target_offset_x, @@ -822,7 +820,7 @@ void Camera::tickThirdPerson(float dtF) { inter.target = followTarget(inter.target, state.target+targetOffset, dtF); } - auto dir = Vec3{0,0,-1}; + auto dir = Vec3{0,0,1}; rotOffsetMat.project(dir); if(true && def.collision!=0) { @@ -835,7 +833,7 @@ void Camera::tickThirdPerson(float dtF) { rotation.x = 80; rotation.y = state.spin.y; const auto rotOffsetMat = mkRotMatrix(rotation); - dir = Vec3{0,0,-1}; + dir = Vec3{0,0,1}; rotOffsetMat.project(dir); } } @@ -898,16 +896,16 @@ float Camera::calcCameraColision(const Tempest::Vec3& target, const Tempest::Vec } Vec3 Camera::calcLookAtAngles(const Tempest::Vec3& origin, const Tempest::Vec3& target, const Vec3& rotOffset, const Vec3& defSpin) const { - auto sXZ = (origin - target); + auto sXZ = (target - origin); float lenXZ = Vec2(sXZ.x,sXZ.z).length(); - float y0 = std::atan2(sXZ.x, sXZ.z)*180.f/float(M_PI); + float y0 = std::atan2(sXZ.z, sXZ.x)*180.f/float(M_PI); float x0 = std::atan2(sXZ.y, lenXZ)*180.f/float(M_PI); if(lenXZ < 4.f) - y0 = -defSpin.y; + y0 = defSpin.y; - return Vec3(x0,-y0,0) - rotOffset; + return Vec3(-x0,y0,0) - rotOffset; } Vec3 Camera::calcCameraColision(const Tempest::Vec3& from, const Tempest::Vec3& dir) const { @@ -948,11 +946,9 @@ Vec3 Camera::clampRotation(Tempest::Vec3 spin) { } Matrix4x4 Camera::mkView(const Vec3& pos, const Vec3& spin) const { - Matrix4x4 view; - view.identity(); + auto view = Matrix4x4::mkIdentity(); view.scale(-1,-1,-1); - // view.translate(0,0,-zNear()); view.mul(mkRotation(spin)); view.translate(-pos); @@ -960,11 +956,11 @@ Matrix4x4 Camera::mkView(const Vec3& pos, const Vec3& spin) const { } Matrix4x4 Camera::mkRotation(const Vec3& spin) const { - Matrix4x4 view; - view.identity(); + auto view = Matrix4x4::mkIdentity(); view.rotateOX(spin.x); view.rotateOY(spin.y); view.rotateOZ(spin.z); + view.rotateOY(90); // X forward return view; } @@ -977,7 +973,7 @@ void Camera::debugDraw(DbgPainter& p) { p.drawLine(inter.target, origin); if(auto pl = Gothic::inst().player()) { - float a = pl->rotationRad(); + float a = pl->rotationRad()+float(M_PI*0.5); float c = std::cos(a), s = std::sin(a); auto ln = Vec3(c,0,s)*25.f; p.drawLine(inter.target-ln, inter.target+ln); @@ -998,7 +994,7 @@ void Camera::debugDraw(DbgPainter& p) { buf = string_frm("Range To Player : ", (inter.target-origin).length()); p.drawText(8,y,buf); y += fnt.pixelSize(); - buf = string_frm("Azimuth : ", angleMod(state.spin.y-angles.y)); + buf = string_frm("Azimuth : ", angleMod(angles.y-state.spin.y)); p.drawText(8,y,buf); y += fnt.pixelSize(); buf = string_frm("Elevation : ", inter.rotOffset.x+angles.x); p.drawText(8,y,buf); y += fnt.pixelSize(); diff --git a/game/game/fightalgo.cpp b/game/game/fightalgo.cpp index 1b8a56091..08e0cd423 100644 --- a/game/game/fightalgo.cpp +++ b/game/game/fightalgo.cpp @@ -301,8 +301,22 @@ bool FightAlgo::isInGRange(const Npc &npc, const Npc &tg, GameScript &owner) con bool FightAlgo::isInFocusAngle(const Npc &npc, const Npc &tg) const { static const float maxAngle = std::cos(float(30.0*M_PI/180.0)); - const auto dpos = npc.centerPosition()-tg.centerPosition(); - const float plAng = npc.rotationRad()+float(M_PI/2); + const auto dpos = tg.centerPosition() - npc.centerPosition(); + const float plAng = npc.rotationRad(); + + const float da = plAng-std::atan2(dpos.z,dpos.x); + const float c = std::cos(da); + + if(c npcRef, int cls (void)lifeTime; for(int32_t i=0;iposition()); - fixNpcPosition(*npc,at->rotation()-90 + 360.f*float(i)/float(count),100); + fixNpcPosition(*npc,at->rotation() + 360.f*float(i)/float(count),100); } } diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 91ba37208..b9dfc7bf9 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -512,7 +512,7 @@ void MoveAlgo::applyRotation(Tempest::Vec3& out, const Tempest::Vec3& dpos) cons } else { out.y = dpos.y; } - float rot = npc.rotationRad(); + float rot = npc.rotationRad()+float(M_PI/2); applyRotation(out,dpos,rot); out.x *= -mul; out.z *= -mul; @@ -858,7 +858,7 @@ void MoveAlgo::assertStateChange(State f) { } bool MoveAlgo::slideDir() const { - float a = std::atan2(fallSpeed.x,fallSpeed.z)+float(M_PI/2); + float a = std::atan2(fallSpeed.x,fallSpeed.z); float b = npc.rotationRad(); auto s = std::sin(a-b); diff --git a/game/game/playercontrol.cpp b/game/game/playercontrol.cpp index 4102adb30..55cb2e017 100644 --- a/game/game/playercontrol.cpp +++ b/game/game/playercontrol.cpp @@ -452,7 +452,7 @@ void PlayerControl::marvinF8(uint64_t dt) { float rot = pl.rotationRad(); float s = std::sin(rot), c = std::cos(rot); - Tempest::Vec3 dp(s,0.8f,-c); + Tempest::Vec3 dp(c,0.8f,s); pos += dp*6000*float(dt)/1000.f; pl.changeAttribute(ATR_HITPOINTS,pl.attribute(ATR_HITPOINTSMAX),false); @@ -478,7 +478,7 @@ void PlayerControl::marvinK(uint64_t dt) { float rot = pl.rotationRad(); float s = std::sin(rot), c = std::cos(rot); - Tempest::Vec3 dp(s, 0.0f, -c); + Tempest::Vec3 dp(c, 0.0f, s); pos += dp * 6000 * float(dt) / 1000.f; pl.clearState(false); diff --git a/game/game/serialize.h b/game/game/serialize.h index d7ea4211f..8d2b5a29d 100644 --- a/game/game/serialize.h +++ b/game/game/serialize.h @@ -33,7 +33,7 @@ class SaveGameHeader; class Serialize { public: enum Version : uint16_t { - Current = 54, + Current = 55, MinVersion = 36, Last_2025 = 53, diff --git a/game/graphics/inventoryrenderer.cpp b/game/graphics/inventoryrenderer.cpp index d6923db54..8692d09fe 100644 --- a/game/graphics/inventoryrenderer.cpp +++ b/game/graphics/inventoryrenderer.cpp @@ -52,9 +52,10 @@ void InventoryRenderer::reset(bool full) { void InventoryRenderer::drawItem(int x, int y, int w, int h, const ::Item& item) { auto& itData = item.handle(); if(auto mesh = Resources::loadMesh(itData.visual)) { - float sz = (mesh->bbox[1]-mesh->bbox[0]).length(); - auto mv = (mesh->bbox[1]+mesh->bbox[0])*0.5f; - ItmFlags flg = ItmFlags(item.mainFlag()); + auto bbox = mesh->bbox(); + float sz = (bbox[1]-bbox[0]).length(); + auto mv = (bbox[1]+bbox[0])*0.5f; + ItmFlags flg = ItmFlags(item.mainFlag()); mv = Vec3(mv.x,mv.y,mv.z); diff --git a/game/graphics/mdlvisual.cpp b/game/graphics/mdlvisual.cpp index b0078a022..745e0fc1c 100644 --- a/game/graphics/mdlvisual.cpp +++ b/game/graphics/mdlvisual.cpp @@ -803,7 +803,7 @@ float MdlVisual::viewDirection() const { } float rx = p.at(2,0); float rz = p.at(2,2); - return float(std::atan2(rz,rx)) * 180.f / float(M_PI); + return float(std::atan2(rz,rx)) * 180.f / float(M_PI) - 90.f; } const Animation::Sequence* MdlVisual::continueCombo(Npc& npc, AnimationSolver::Anim a, BodyState bs, diff --git a/game/graphics/mesh/pose.cpp b/game/graphics/mesh/pose.cpp index 0ead07d24..f5e7e30dc 100644 --- a/game/graphics/mesh/pose.cpp +++ b/game/graphics/mesh/pose.cpp @@ -20,7 +20,7 @@ Pose::Pose() { uint8_t Pose::calcAniComb(const Vec3& dpos, float rotation) { float l = std::sqrt(dpos.x*dpos.x+dpos.z*dpos.z); - float dir = 90+180.f*std::atan2(dpos.z,dpos.x)/float(M_PI); + float dir = 180.f*std::atan2(dpos.z,dpos.x)/float(M_PI); float aXZ = (rotation-dir); float aY = -std::atan2(dpos.y,l)*180.f/float(M_PI); @@ -397,7 +397,7 @@ bool Pose::updateFrame(const Animation::Sequence &s, BodyState bs, uint64_t sBle else if(bs==BS_SWIM || bs==BS_DIVE) smp.position.y = trY; else if(bs==BS_JUMP) //else if(s.isFly()) - smp.position.y = trY; //d.translate.y; + smp.position.y = trY; } switch(hasSamples[idx]) { diff --git a/game/graphics/mesh/protomesh.cpp b/game/graphics/mesh/protomesh.cpp index 486c1df7d..5f9be6aad 100644 --- a/game/graphics/mesh/protomesh.cpp +++ b/game/graphics/mesh/protomesh.cpp @@ -37,8 +37,8 @@ ProtoMesh::ProtoMesh(PackedMesh&& pm, std::string_view fname) nodes.back().submeshIdE = submeshId.size(); nodes.back().transform.identity(); - bbox[0] = pm.bbox().first; - bbox[1] = pm.bbox().second; + bboxMesh[0] = pm.bbox().first; + bboxMesh[1] = pm.bbox().second; setupScheme(fname); } @@ -324,10 +324,10 @@ size_t ProtoMesh::findNode(std::string_view name, size_t def) const { return skeleton->findNode(name,def); } -const Vec3* ProtoMesh::bboxCol() const { +const Vec3* ProtoMesh::bbox() const { if(skeleton==nullptr) - return bbox; - return skeleton->bboxCol; + return bboxMesh; + return skeleton->bbox; } void ProtoMesh::setupScheme(std::string_view s) { diff --git a/game/graphics/mesh/protomesh.h b/game/graphics/mesh/protomesh.h index da684c1a7..b3e78f88e 100644 --- a/game/graphics/mesh/protomesh.h +++ b/game/graphics/mesh/protomesh.h @@ -84,13 +84,13 @@ class ProtoMesh { std::vector submeshId; std::vector pos; - Tempest::Vec3 bbox[2]; + Tempest::Vec3 bboxMesh[2]; std::string scheme, fname; size_t skinedNodesCount() const; size_t findNode(std::string_view name,size_t def=size_t(-1)) const; - const Tempest::Vec3* bboxCol() const; + const Tempest::Vec3* bbox() const; private: void setupScheme(std::string_view s); diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index e6e220ab3..692d5bb64 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -678,7 +678,7 @@ DynamicWorld::RayLandResult DynamicWorld::ray(const Tempest::Vec3& from, const T ret.mat = callback.matId; ret.hasCol = callback.hasHit(); ret.hitFraction = callback.m_closestHitFraction; - ret.sector = callback.sector; + ret.sector = callback.sector; return ret; } diff --git a/game/ui/documentmenu.cpp b/game/ui/documentmenu.cpp index ccaef1c20..26d2e1df5 100644 --- a/game/ui/documentmenu.cpp +++ b/game/ui/documentmenu.cpp @@ -108,7 +108,7 @@ void DocumentMenu::paintEvent(PaintEvent &e) { p.pushState(); p.translate(cx,cy); - p.rotate(-pl->rotation()); + p.rotate(-pl->rotation()-90); p.drawRect(-cursor->w()/2,-cursor->h()/2, cursor->w(),cursor->h()); p.popState(); } diff --git a/game/world/objects/interactive.cpp b/game/world/objects/interactive.cpp index dc73f6b1b..ca9e803a4 100644 --- a/game/world/objects/interactive.cpp +++ b/game/world/objects/interactive.cpp @@ -181,7 +181,7 @@ void Interactive::drawVobBox(DbgPainter& p) const { p.setPen(Tempest::Color(1,0,0)); //p.drawAabb(bbox[0], bbox[1]); if(auto mesh = visual.protoMesh()) { - p.drawObb(transform(), mesh->bboxCol()); + p.drawObb(transform(), mesh->bbox()); } for(auto& i:attPos) { @@ -194,7 +194,7 @@ void Interactive::drawVobRay(DbgPainter& p, const Npc& npc) const { auto head = npc.mapHeadBone(); if(auto mesh = visual.protoMesh()) { - auto bbox = mesh->bboxCol(); + auto bbox = mesh->bbox(); auto boxMin = bbox[0]; auto boxMax = bbox[1]; auto at = (boxMin+boxMax)*0.5f; @@ -578,7 +578,7 @@ uint32_t Interactive::stateMask() const { bool Interactive::canSeeNpc(const Npc& npc, bool freeLos) const { auto head = npc.mapHeadBone(); if(auto mesh = visual.protoMesh()) { - auto bbox = mesh->bboxCol(); + auto bbox = mesh->bbox(); auto at = (bbox[0]+bbox[1])*0.5f; transform().project(at); diff --git a/game/world/objects/item.cpp b/game/world/objects/item.cpp index b1cb2df99..1642d507a 100644 --- a/game/world/objects/item.cpp +++ b/game/world/objects/item.cpp @@ -85,7 +85,7 @@ Item::~Item() { void Item::drawVobBox(DbgPainter& p) const { p.setPen(Tempest::Color(1,0,0)); if(auto mesh = visual.protoMesh()) { - p.drawObb(transform(), mesh->bboxCol()); + p.drawObb(transform(), mesh->bbox()); auto v = midPosition(); p.setPen(Tempest::Color(0,1,0)); @@ -223,12 +223,12 @@ Tempest::Vec3 Item::position() const { const Vec3* Item::bBox() const { if(visual.protoMesh()==nullptr) return nullptr; - return visual.protoMesh()->bboxCol(); + return visual.protoMesh()->bbox(); } Vec3 Item::midPosition() const { if(auto mesh = visual.protoMesh()) { - auto bbox = mesh->bboxCol(); + auto bbox = mesh->bbox(); auto v = (bbox[0] + bbox[1])*0.5; transform().project(v); // doesn't use to work for Karibik mod return v; diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index dd635ed24..26103ab18 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -264,6 +264,8 @@ void Npc::load(Serialize &fin, size_t id, std::string_view directory) { fin.read(x,y,z,angle,sz); fin.read(wlkMode,trGuild,talentsSk,talentsVl,refuseTalkMilis); durtyTranform = TR_Pos|TR_Rot|TR_Scale; + if(fin.version()<55) + angle -= 90; fin.read(permAttitude,tmpAttitude); fin.read(perceptionTime,perceptionNextTime); @@ -462,9 +464,9 @@ void Npc::setRunAngle(float angle) { } float Npc::angleDir(float x, float z) { - float a=-90; + float a = 0; if(x!=0.f || z!=0.f) - a = 90+180.f*std::atan2(z,x)/float(M_PI); + a = 180.f*std::atan2(z,x)/float(M_PI); return a; } @@ -688,6 +690,13 @@ Vec3 Npc::centerPosition() const { p.y += visual.pose().translateY(); p.y += 15; // seem to be off by ~15 centimeters, according to comparations vanilla testing return p; + /* + // p.y += 15; // seem to be off by ~15 centimeters, according to comparations vanilla testing + if(auto sk = visual.visualSkeleton()) { + p.y += (sk->bboxCol[0].y + sk->bboxCol[1].y)*0.5f; + } + return p; + */ } Vec3 Npc::collosionCenter() const { @@ -1889,7 +1898,7 @@ void Npc::takeDamage(Npc &other, const Bullet* b) { assert(b==nullptr || !b->isSpell()); const auto& pose = visual.pose(); - const bool isJumpb = pose.isJumpBack(owner.tickCount()); + const bool isJumpb = pose.isJumpBack(owner.tickCount()) && fghAlgo.isInJumpBackAngle(*this,other); const bool isBlock = (!other.isMonster() || other.inventory().activeWeapon()!=nullptr) && fghAlgo.isInFocusAngle(*this,other) && pose.isDefence(owner.tickCount()); @@ -4258,7 +4267,7 @@ Npc::JumpStatus Npc::tryJump() { float len = MoveAlgo::climbMove; float rot = rotationRad(); float s = std::sin(rot), c = std::cos(rot); - Vec3 dp = Vec3{len*s, 0, -len*c}; + Vec3 dp = Vec3{len*c, 0, len*s}; auto& g = owner.script().guildVal(); auto gl = guild(); @@ -4613,7 +4622,7 @@ Matrix4x4 Npc::mkPositionMatrix() const { if(align) { float rot = rotationRad(); float s = std::sin(rot), c = std::cos(rot); - auto dir = Vec3(s,0,-c); + auto dir = Vec3(c,0,s); auto norm = Vec3::normalize(ground); float cx = Vec3::dotProduct(norm,dir); @@ -4623,7 +4632,7 @@ Matrix4x4 Npc::mkPositionMatrix() const { Matrix4x4 mt = Matrix4x4(); mt.identity(); mt.translate(x,y,z); - mt.rotateOY(180-angle); + mt.rotateOY(90-angle); if(angY!=0) mt.rotateOX(-angY); if(isPlayer() && !align) { diff --git a/game/world/triggers/cscamera.cpp b/game/world/triggers/cscamera.cpp index 368c31aa3..923bdc08e 100644 --- a/game/world/triggers/cscamera.cpp +++ b/game/world/triggers/cscamera.cpp @@ -234,9 +234,9 @@ PointF CsCamera::spin(Tempest::Vec3& d) { float k = 180.f/float(M_PI); float spinX = k * std::asin(d.y/d.length()); - float spinY = -90; + float spinY = 0; if(d.x!=0.f || d.z!=0.f) - spinY = 90 + k * std::atan2(d.z,d.x); + spinY = k * std::atan2(d.z,d.x); return {-spinX, spinY}; } diff --git a/game/world/world.cpp b/game/world/world.cpp index df859acf0..f0d3c3342 100644 --- a/game/world/world.cpp +++ b/game/world/world.cpp @@ -657,7 +657,7 @@ Bullet& World::shootSpell(const Item &itm, const Npc &npc, const Npc *target) { } dir = tgPos-pos; } else { - float a = npc.rotationRad()-float(M_PI/2); + float a = npc.rotationRad(); dir.x = std::cos(a); dir.z = std::sin(a); pos = npc.mapWeaponBone(); @@ -693,7 +693,7 @@ Bullet& World::shootBullet(const Item &itm, const Npc &npc, const Npc *target, c dir/=t; dir.y += 0.5f*DynamicWorld::gravity*t; } else { - float a = npc.rotationRad()-float(M_PI/2); + float a = npc.rotationRad(); dir.x = std::cos(a); dir.z = std::sin(a); } diff --git a/game/world/worldobjects.cpp b/game/world/worldobjects.cpp index 7aa6b17be..68ac0d966 100644 --- a/game/world/worldobjects.cpp +++ b/game/world/worldobjects.cpp @@ -1106,7 +1106,7 @@ template bool WorldObjects::testObj(T &src, const Npc &pl, const WorldObjects::SearchOpt &opt,float& rlen){ const float qmax = opt.rangeMax*opt.rangeMax; const float qmin = opt.rangeMin*opt.rangeMin; - const float plAng = pl.rotationRad()+float(M_PI/2); + const float plAng = pl.rotationRad(); const float ang = float(std::cos(double(opt.azi)*M_PI/180.0)); auto& npc=deref(src); @@ -1123,7 +1123,7 @@ bool WorldObjects::testObj(T &src, const Npc &pl, const WorldObjects::SearchOpt return false; auto pos = npc.position(); - auto dpos = pl.position()-pos; + auto dpos = pos - pl.position(); auto angle = std::atan2(dpos.z,dpos.x); if(std::cos(plAng-angle) Date: Thu, 16 Apr 2026 19:40:03 +0200 Subject: [PATCH 23/29] npc-to-npc collision --- game/physics/dynamicworld.cpp | 54 ++++++++++++----------------------- game/physics/dynamicworld.h | 1 - game/world/objects/npc.cpp | 2 +- 3 files changed, 20 insertions(+), 37 deletions(-) diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index 692d5bb64..320bf0af8 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -18,8 +18,7 @@ //#include "BulletCollision/CollisionShapes/btCylinderShape.h" -const float DynamicWorld::ghostPadding = 90;//55.f; //50-22.5f; -const float DynamicWorld::worldHeight = 20000; +const float DynamicWorld::worldHeight = 20000; //TODO: remove struct DynamicWorld::HumShape:btCapsuleShape { //NOTE: total height is height+2*radius @@ -57,17 +56,18 @@ 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; + return h*0.5f + gPadd*0.5f + extPadding; + } + + auto centerPosition() const { + float padd = groundOffset(); + return Tempest::Vec3(pos.x, pos.y+padd, pos.z); } void setPosition(const Tempest::Vec3& p) { @@ -96,6 +96,7 @@ struct DynamicWorld::NpcBodyList final { 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; + // ghostPadding = 90; float ghostPadding = height*0.5f; float cHeight = std::max(height-2.f*radius-ghostPadding, 0.f); @@ -111,8 +112,6 @@ struct DynamicWorld::NpcBodyList final { obj->setUserIndex(C_Ghost); obj->setCollisionFlags(btCollisionObject::CF_NO_CONTACT_RESPONSE); - // 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; @@ -279,43 +278,28 @@ struct DynamicWorld::NpcBodyList final { } bool hasCollision(const NpcBody& a, const NpcBody& b, Tempest::Vec3& normal){ + // fixme: asymetric npc-npc collision 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); + auto direction = a.centerPosition() - b.centerPosition(); + auto R = a.r+b.r, H = a.h+b.h; + if(std::abs(direction.x)>R || std::abs(direction.z)>R || std::abs(direction.y)>H*0.5f) + return false; + + auto ellipsoidQuadRadius = [](Tempest::Vec3 radius, Tempest::Vec3 direction) -> float { 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) { + float qDistance = direction.length(); + auto radiusA = ellipsoidQuadRadius(a.ellipsoidSize(), direction); + auto radiusB = ellipsoidQuadRadius(b.ellipsoidSize(), direction); + if(qDistance < std::pow(radiusA + radiusB, 2.0)) { 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; - - if(dx*dx+dz*dz>r*r) - return false; - if(dy>b.h || dy<-a.h) - return false; - - normal.x += dx; - normal.y += dy; - normal.z += dz; -#endif - return true; } void adjustSort() { diff --git a/game/physics/dynamicworld.h b/game/physics/dynamicworld.h index 684d4005a..e0014f6a7 100644 --- a/game/physics/dynamicworld.h +++ b/game/physics/dynamicworld.h @@ -39,7 +39,6 @@ class DynamicWorld final { static constexpr float gravity = gravityMS*100.f/(1000.f*1000.f); // centimeters per milliseconds^2 static constexpr float bulletSpeed = 3; // centimeters per milliseconds static constexpr float spellSpeed = 1; // centimeters per milliseconds - static const float ghostPadding; DynamicWorld(World &world, const zenkit::Mesh& mesh); DynamicWorld(const DynamicWorld&)=delete; diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index 26103ab18..8a3b4ff77 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -681,7 +681,7 @@ Bounds Npc::bounds() const { auto Npc::bBox() const -> const Vec3* { if(visual.visualSkeleton()==nullptr) return nullptr; - return visual.visualSkeleton()->bboxCol; + return visual.visualSkeleton()->bbox; } Vec3 Npc::centerPosition() const { From 737496bc1c54f5c79b33903c861f80a0b5124430 Mon Sep 17 00:00:00 2001 From: Try Date: Thu, 16 Apr 2026 19:40:32 +0200 Subject: [PATCH 24/29] npc: drawing weapons near water --- game/world/objects/npc.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index 8a3b4ff77..84ff2e089 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -1525,7 +1525,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() && mvAlgo.state()==MoveAlgo::Run) { + if(ws==WeaponState::NoWeapon && isAiQueueEmpty() && canSwitchWeapon()) { drawWeaponMelee(); return true; } @@ -2556,26 +2556,26 @@ void Npc::nextAiAction(AiQueue& queue, uint64_t dt) { } break; case AI_DrawWeapon: - if(!isDead()) { + if(canSwitchWeapon()) { if(!drawWeaponMelee() && !drawWeaponBow()) queue.pushFront(std::move(act)); } break; case AI_DrawWeaponMelee: - if(!isDead()) { + if(canSwitchWeapon()) { if(!drawWeaponMelee()) queue.pushFront(std::move(act)); } break; case AI_DrawWeaponRange: - if(!isDead()) { + if(canSwitchWeapon()) { if(!drawWeaponBow()) queue.pushFront(std::move(act)); } break; case AI_DrawSpell: { - if(!isDead()) { + if(canSwitchWeapon()) { const int32_t spell = act.i0; if(drawSpell(spell)) aiExpectedInvest = act.i1; else @@ -3514,7 +3514,13 @@ void Npc::unequipItem(size_t item) { } bool Npc::canSwitchWeapon() const { - return !(mvAlgo.isFalling() || mvAlgo.isInAir() || mvAlgo.isSlide() || mvAlgo.isSwim()); + if(isUnconscious()) + return false; + auto bs = bodyStateMasked(); + if(bs==BS_STAND || bs==BS_WALK || bs==BS_RUN || bs==BS_SNEAK || bs==BS_NONE) + return true; + return false; + // return !(mvAlgo.isFalling() || mvAlgo.isInAir() || mvAlgo.isSlide() || mvAlgo.isSwim()); } bool Npc::closeWeapon(bool noAnim) { From 3f9971bd3507228d1886f5c37e34e4d0be37600a Mon Sep 17 00:00:00 2001 From: Try Date: Thu, 16 Apr 2026 20:15:46 +0200 Subject: [PATCH 25/29] cleanups --- game/game/gamesession.cpp | 2 ++ game/graphics/mesh/skeleton.cpp | 11 ++--------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/game/game/gamesession.cpp b/game/game/gamesession.cpp index 2b75a987b..a3fc1676a 100644 --- a/game/game/gamesession.cpp +++ b/game/game/gamesession.cpp @@ -90,6 +90,7 @@ GameSession::GameSession(std::string file) { //std::string_view hero = "PC_HERO"; //std::string_view hero = "FireGolem"; //std::string_view hero = "Dragon_Undead"; + //std::string_view hero = "Wolf"; //std::string_view hero = "Sheep"; //std::string_view hero = "Giant_Bug"; //std::string_view hero = "OrcWarrior_Rest"; @@ -101,6 +102,7 @@ GameSession::GameSession(std::string file) { //std::string_view hero = "FireWaran"; //std::string_view hero = "Bloodfly"; //std::string_view hero = "Gobbo_Skeleton"; + //std::string_view hero = "Swampshark"; if(!Gothic::inst().isBenchmarkMode()) wrld->createPlayer(hero); wrld->postInit(); diff --git a/game/graphics/mesh/skeleton.cpp b/game/graphics/mesh/skeleton.cpp index 8a16ca91e..1485ccc6b 100644 --- a/game/graphics/mesh/skeleton.cpp +++ b/game/graphics/mesh/skeleton.cpp @@ -8,17 +8,11 @@ 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}; + 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 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}; -#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()); @@ -92,7 +86,6 @@ 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); } From 3682f4d84497a6196278c0661e8c5908a7dcb82a Mon Sep 17 00:00:00 2001 From: Try Date: Thu, 16 Apr 2026 20:20:31 +0200 Subject: [PATCH 26/29] cleanup --- game/game/movealgo.cpp | 9 ++++----- game/physics/dynamicworld.cpp | 15 +++++++++------ game/world/objects/npc.cpp | 3 +-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index b9dfc7bf9..0d0d0a7a6 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -126,7 +126,6 @@ void MoveAlgo::tickClimb(uint64_t dt) { npc.tryTranslate(Tempest::Vec3(climbPos0.x,climbHeight,climbPos0.z)); npc.tryTranslate(p); } - //clearSpeed(); return; } @@ -172,7 +171,7 @@ void MoveAlgo::tick(uint64_t dt, MvFlags moveFlg) { bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { if(flags==ClimbUp) { - tickClimb(dt); //fixup: collision + tickClimb(dt); return true; } if(flags==JumpUp) { @@ -846,7 +845,7 @@ void MoveAlgo::assertStateChange(State f) { assert(f==Run); break; case InWater: - assert(f==Run || f==Slide || f==JumpUp || f==Swim || f==Dive); + assert(f==Run || f==Slide || f==Jump || f==JumpUp || f==Swim || f==Dive); break; case Swim: assert(f==Run || f==InAir || f==InWater || f==Dive); @@ -979,12 +978,12 @@ void MoveAlgo::onGravityFailed(const DynamicWorld::CollisionTest& info, uint64_t float len = fallSpeed.length()/std::max(1.f,fallCount); if(isInAir() && Tempest::Vec2::dotProduct({fallSpeed.x, fallSpeed.z}, {norm.x, norm.z})<0.f) { float lx = Tempest::Vec2({fallSpeed.x, fallSpeed.z}).length(); - lx *= 0.25f; + lx *= 0.5f; fallSpeed.x = norm.x*lx; fallSpeed.z = norm.z*lx; //fallSpeed = Tempest::Vec3::normalize(fallSpeed)*len; } else { - len *= 0.25f; + len *= 0.5f; len = std::max(len, 0.1f); fallSpeed = Tempest::Vec3::normalize(fallSpeed+norm)*len; } diff --git a/game/physics/dynamicworld.cpp b/game/physics/dynamicworld.cpp index 320bf0af8..eb79cb70d 100644 --- a/game/physics/dynamicworld.cpp +++ b/game/physics/dynamicworld.cpp @@ -283,20 +283,23 @@ struct DynamicWorld::NpcBodyList final { return false; auto direction = a.centerPosition() - b.centerPosition(); + if(direction.quadLength()<1.f) + direction.x += 5.f; // avoid zero based distance + auto R = a.r+b.r, H = a.h+b.h; if(std::abs(direction.x)>R || std::abs(direction.z)>R || std::abs(direction.y)>H*0.5f) return false; auto ellipsoidQuadRadius = [](Tempest::Vec3 radius, Tempest::Vec3 direction) -> float { - direction = Tempest::Vec3::normalize(direction); return (direction * radius).length(); }; - float qDistance = direction.length(); - auto radiusA = ellipsoidQuadRadius(a.ellipsoidSize(), direction); - auto radiusB = ellipsoidQuadRadius(b.ellipsoidSize(), direction); - if(qDistance < std::pow(radiusA + radiusB, 2.0)) { - normal += direction; + float distance = direction.length(); + auto normDir = Tempest::Vec3::normalize(direction); + auto radiusA = ellipsoidQuadRadius(a.ellipsoidSize(), normDir); + auto radiusB = ellipsoidQuadRadius(b.ellipsoidSize(), normDir); + if(distance < radiusA + radiusB) { + normal += normDir; return true; } return false; diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index 84ff2e089..795eeee53 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -686,9 +686,8 @@ auto Npc::bBox() const -> const Vec3* { Vec3 Npc::centerPosition() const { auto p = position(); - //p.y = physic.centerY(); + p.y += 25; // seem to be off by ~25 centimeters, according to comparations vanilla testing p.y += visual.pose().translateY(); - p.y += 15; // seem to be off by ~15 centimeters, according to comparations vanilla testing return p; /* // p.y += 15; // seem to be off by ~15 centimeters, according to comparations vanilla testing From 1542d7f7f4fe74d94abf88bce33a80c7b34b0add Mon Sep 17 00:00:00 2001 From: Try Date: Thu, 16 Apr 2026 23:20:51 +0200 Subject: [PATCH 27/29] better "in fly" collisions --- game/game/movealgo.cpp | 29 +++++++++++++++++------------ game/world/objects/npc.cpp | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/game/game/movealgo.cpp b/game/game/movealgo.cpp index 0d0d0a7a6..7f92ffc34 100644 --- a/game/game/movealgo.cpp +++ b/game/game/movealgo.cpp @@ -422,7 +422,7 @@ bool MoveAlgo::implTick(uint64_t dt, MvFlags moveFlg) { return false; } - if(testSlide(pos,normal,info)) { + if(!dead && testSlide(pos,normal,info)) { if(state==InWater || state==Swim) { npc.setPosition(pos0); return false; @@ -823,6 +823,7 @@ void MoveAlgo::setState(State f) { void MoveAlgo::assertStateChange(State f) { // assert possible transitions + const bool dead = npc.isDead(); switch(flags) { case Run: assert(f!=Falling); @@ -834,14 +835,18 @@ void MoveAlgo::assertStateChange(State f) { assert(f==Run || f==InAir || f==InWater || f==Swim || f==Dive); break; case Slide: + assert(!dead); break; case Jump: + assert(!dead); assert(f==Run || f==InAir || f==Falling || f==Swim || f==Dive); break; case JumpUp: + assert(!dead); assert(f==Run || f==InAir || f==ClimbUp); break; case ClimbUp: + assert(!dead); assert(f==Run); break; case InWater: @@ -851,6 +856,7 @@ void MoveAlgo::assertStateChange(State f) { assert(f==Run || f==InAir || f==InWater || f==Dive); break; case Dive: + assert(!dead); assert(f==InAir || f==Swim || f==InWater); break; } @@ -974,18 +980,17 @@ void MoveAlgo::onGravityFailed(const DynamicWorld::CollisionTest& info, uint64_t norm.y = normXZ *norm.y/normXZInv; } - if(Tempest::Vec3::dotProduct(fallSpeed,norm)<0.f || fallCount>0) { - float len = fallSpeed.length()/std::max(1.f,fallCount); - if(isInAir() && Tempest::Vec2::dotProduct({fallSpeed.x, fallSpeed.z}, {norm.x, norm.z})<0.f) { - float lx = Tempest::Vec2({fallSpeed.x, fallSpeed.z}).length(); - lx *= 0.5f; - fallSpeed.x = norm.x*lx; - fallSpeed.z = norm.z*lx; - //fallSpeed = Tempest::Vec3::normalize(fallSpeed)*len; + auto reflect = [](const Tempest::Vec3& I, const Tempest::Vec3& N) { + return I - 2.f*Tempest::Vec3::dotProduct(N,I) * N; + }; + + assert(fallCount==0.f); + if(true || Tempest::Vec3::dotProduct(fallSpeed,norm)<0.f) { + //float len = fallSpeed.length()/std::max(1.f,fallCount); + if(Tempest::Vec2::dotProduct({fallSpeed.x, fallSpeed.z}, {norm.x, norm.z})<0.f) { + fallSpeed = reflect(fallSpeed, norm)*0.75f; } else { - len *= 0.5f; - len = std::max(len, 0.1f); - fallSpeed = Tempest::Vec3::normalize(fallSpeed+norm)*len; + fallSpeed = norm*fallSpeed.length(); } fallCount = 0; } else { diff --git a/game/world/objects/npc.cpp b/game/world/objects/npc.cpp index 795eeee53..70deeef9a 100644 --- a/game/world/objects/npc.cpp +++ b/game/world/objects/npc.cpp @@ -1050,7 +1050,7 @@ bool Npc::isFalling() const { } bool Npc::isFallingDeep() const { - return mvAlgo.isFalling() && (visual.pose().isInAnim("S_FALL") || visual.pose().isInAnim("S_FALLB")); + return (mvAlgo.isInAir() || mvAlgo.isFalling()) && (visual.pose().isInAnim("S_FALL") || visual.pose().isInAnim("S_FALLB")); } bool Npc::isSlide() const { From d74621271519393e461a1dacbb8da5f65cfe44a1 Mon Sep 17 00:00:00 2001 From: Try Date: Fri, 17 Apr 2026 01:16:13 +0200 Subject: [PATCH 28/29] fixup --- game/graphics/mdlvisual.cpp | 2 +- game/world/waymatrix.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/game/graphics/mdlvisual.cpp b/game/graphics/mdlvisual.cpp index 745e0fc1c..26cf2b153 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.6f,0}; return {0.f,0.f,0.f}; } diff --git a/game/world/waymatrix.cpp b/game/world/waymatrix.cpp index 1db4176ff..d5cbdf03c 100644 --- a/game/world/waymatrix.cpp +++ b/game/world/waymatrix.cpp @@ -198,6 +198,9 @@ void WayMatrix::adjustWaypoints(std::vector &wp) { if(ray.hasCol) { //NOTE: what about water? w.groundPos = ray.v; + } else { + // some way-point are underground + w.groundPos = w.pos; } indexPoints.push_back(&w); } From 947754485edfaa4e4a1a0c2bfb15ba1abea88981 Mon Sep 17 00:00:00 2001 From: Try Date: Fri, 17 Apr 2026 01:16:22 +0200 Subject: [PATCH 29/29] cleanup --- game/world/triggers/touchdamage.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/game/world/triggers/touchdamage.cpp b/game/world/triggers/touchdamage.cpp index d3bea23f6..cdfa0bf7c 100644 --- a/game/world/triggers/touchdamage.cpp +++ b/game/world/triggers/touchdamage.cpp @@ -6,15 +6,15 @@ TouchDamage::TouchDamage(Vob* parent, World &world, const zenkit::VTouchDamage& dmg, Flags flags) :AbstractTrigger(parent,world,dmg,flags) { - barrier = dmg.barrier; - blunt = dmg.blunt; - edge = dmg.edge; - fire = dmg.fire; - fly = dmg.fly; - magic = dmg.magic; - point = dmg.point; - fall = dmg.fall; - damage = dmg.damage; + barrier = dmg.barrier; + blunt = dmg.blunt; + edge = dmg.edge; + fire = dmg.fire; + fly = dmg.fly; + magic = dmg.magic; + point = dmg.point; + fall = dmg.fall; + damage = dmg.damage; repeatDelaySec = dmg.repeat_delay_sec; }