feat(physics): movement blockers the server's movers can't walk through

Enemies are moved along navmesh paths without collision, so a wall only stops
them if their path stops at it. dpWorld now keeps a list of such walls, each
with the collision filter it blocks with, and cuts a path where it first walks
into one (a little short of the wall; a mover inside one can walk out).

Which objects block comes from data (dpMovementBlockers::BlockingFilter):
- navmesh carvers (navmesh_carver in the level config, which the client reads
  with add_to_navmesh and carver_only) block every mover;
- solid objects whose collision group touches enemies and not players (group 18,
  e.g. "FV - Enemy Blocking Volume", "PR - Pet Blocker") block what that group
  touches.

dpShapeBox gains SegmentEntry, a segment test against the rotated box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 17:37:37 -05:00
parent 69ee6b0bc6
commit 425b913848
10 changed files with 305 additions and 0 deletions

View File

@@ -8,6 +8,7 @@
#include "NiQuaternion.h"
#include <algorithm>
#include <cmath>
#include <iostream>
dpShapeBox::dpShapeBox(dpEntity* parentEntity, float width, float height, float depth) :
@@ -108,6 +109,37 @@ float dpShapeBox::SquaredDistanceTo(const NiPoint3& point) const {
return dX * dX + dY * dY + dZ * dZ;
}
std::optional<float> dpShapeBox::SegmentEntry(const NiPoint3& a, const NiPoint3& b) const {
// Into the box's frame (origin the middle of its bottom face), then clip the segment against each pair of faces
const auto inverse = glm::conjugate(m_Orientation);
const auto localA = (a - m_Origin).RotateByQuaternion(inverse);
const auto localB = (b - m_Origin).RotateByQuaternion(inverse);
const float start[3]{ localA.x, localA.y, localA.z };
const float delta[3]{ localB.x - localA.x, localB.y - localA.y, localB.z - localA.z };
const float min[3]{ -m_Width, 0.0f, -m_Depth };
const float max[3]{ m_Width, m_Height * 2.0f, m_Depth };
bool startsInside = true;
float enter = 0.0f;
float exit = 1.0f;
for (int axis = 0; axis < 3; axis++) {
if (start[axis] < min[axis] || start[axis] > max[axis]) startsInside = false;
if (std::abs(delta[axis]) < 1e-6f) {
if (start[axis] < min[axis] || start[axis] > max[axis]) return std::nullopt;
continue;
}
float tNear = (min[axis] - start[axis]) / delta[axis];
float tFar = (max[axis] - start[axis]) / delta[axis];
if (tNear > tFar) std::swap(tNear, tFar);
enter = std::max(enter, tNear);
exit = std::min(exit, tFar);
if (enter > exit) return std::nullopt;
}
if (startsInside) return std::nullopt;
return enter;
}
void dpShapeBox::InitVertices() {
//The four top verts
m_TopMinLeft = NiPoint3(GetMinWidth(), GetTop(), GetMinDepth());