Core/Movement: reworked chase movement

* the destination prediction is now much more reliable
* the movement generator will now more frequently check for valid positions to match retail behavior
* the catch-up mechanic will now adapt its speed to the target's one when being close enough to hit it
* temporarily disabled backwards movement when being too close to a target to avoid some edge cases that need additional research
* fixed the pathfinding reaching behavior so that they no longer follow you to unreachable obstacles and return home when they can't reach you anymore

closes #345
closes #260
This commit is contained in:
Ovahlord committed 2023-08-24 20:16:41 +02:00
1 parent 63be5a888c
commit 0840143137
9 files changed
+204 -120

No files matched your search

+4 -3
View File
@@ -485,9 +485,10 @@ void PetAI::DoAttack(Unit* target, bool chase)
float chaseDistance = me->GetPetChaseDistance();
// Pets with ranged attacks should not care about the chase angle at all.
ChaseAngle angle = ChaseAngle(chaseDistance == 0.f ? float(M_PI) : 0.f, chaseDistance == 0.f ? float(M_PI_4) : float(M_PI * 2));
me->GetMotionMaster()->MoveChase(target, chaseDistance, angle);
if (chaseDistance == 0.f) // @todo: the chase angle of pets is relative to the angle between target and owner so it always faces away from the owner
me->GetMotionMaster()->MoveChase(target/*, std::nullopt, ChaseAngle(float(M_PI), float(M_PI_4)) */);
else
me->GetMotionMaster()->MoveChase(target, chaseDistance);
}
else // (Stay && ((Aggressive || Defensive) && In Melee Range)))
{
@@ -156,7 +156,7 @@ void ScriptedAI::UpdateAI(uint32 /*diff*/)
DoMeleeAttackIfReady();
}
void ScriptedAI::DoStartMovement(Unit* victim, float distance, float angle)
void ScriptedAI::DoStartMovement(Unit* victim, Optional<float> distance /*= std::nullopt*/, Optional<float> angle /*= std::nullopt*/)
{
if (victim)
me->GetMotionMaster()->MoveChase(victim, distance, angle);
@@ -162,7 +162,7 @@ struct TC_GAME_API ScriptedAI : public CreatureAI
// *************
//Start movement toward victim
void DoStartMovement(Unit* target, float distance = 0.0f, float angle = 0.0f);
void DoStartMovement(Unit* target, Optional<float> distance = std::nullopt, Optional<float> angle = std::nullopt);
//Start no movement on victim
void DoStartNoMovement(Unit* target);
@@ -1669,12 +1669,15 @@ void SmartScript::ProcessAction(SmartScriptHolder& e, Unit* unit, uint32 var0, u
float attackDistance = float(e.action.setRangedMovement.distance);
float attackAngle = float(e.action.setRangedMovement.angle) / 180.0f * float(M_PI);
Optional<ChaseAngle> angle;
if (attackAngle != 0.f)
angle = ChaseAngle(attackAngle);
for (WorldObject* target : targets)
if (Creature* creature = target->ToCreature())
if (IsSmart(creature) && creature->GetVictim())
if (ENSURE_AI(SmartAI, creature->AI())->CanCombatMove())
creature->GetMotionMaster()->MoveChase(creature->GetVictim(), attackDistance, attackAngle);
creature->GetMotionMaster()->MoveChase(creature->GetVictim(), attackDistance, angle);
break;
}
+1 -1
View File
@@ -268,7 +268,7 @@ void MotionMaster::MoveFollow(Unit* target, float dist, float angle, bool joinFo
Mutate(new FollowMovementGenerator(target, dist, angle, joinFormation, catchUpToTarget, faceTarget), slot);
}
void MotionMaster::MoveChase(Unit* target, float dist, Optional<ChaseAngle> angle)
void MotionMaster::MoveChase(Unit* target, Optional<float> dist /*= std::nullopt*/, Optional<ChaseAngle> angle /*= std::nullopt*/)
{
// ignore movement request if target not exist
if (!target || target == _owner)
+1 -2
View File
@@ -150,8 +150,7 @@ class TC_GAME_API MotionMaster
void MoveRandom(float wanderDistance = 0.0f);
void MoveFollow(Unit* target, float dist, float angle, bool joinFormation = false, bool catchUpToTarget = false, bool faceTarget = false, MovementSlot slot = MOTION_SLOT_IDLE);
void MoveChase(Unit* target, float dist = 0.f, Optional<ChaseAngle> angle = {});
void MoveChase(Unit* target, float dist, float angle) { MoveChase(target, dist, ChaseAngle(angle)); }
void MoveChase(Unit* target, Optional<float> dist = std::nullopt, Optional<ChaseAngle> angle = std::nullopt);
void MoveConfused();
void MoveFleeing(Unit* enemy, uint32 time = 0);
@@ -27,6 +27,7 @@
#include "Util.h"
#include "Vehicle.h"
/*
static bool IsMutualChase(Unit const* owner, Unit const* target)
{
MovementGenerator* gen = target->GetMotionMaster()->topOrNull();
@@ -35,39 +36,38 @@ static bool IsMutualChase(Unit const* owner, Unit const* target)
return (static_cast<ChaseMovementGenerator*>(gen)->GetTarget() == owner);
}
*/
static void DoMovementInform(Unit* owner, Unit const* target)
enum ChaseMovementPositionCheckResult : uint8
{
if (owner->GetTypeId() != TYPEID_UNIT)
CHASE_POSITION_CHECK_RESULT_VALID = 0x0,
CHASE_POSITION_CHECK_RESULT_TOO_FAR = 0x1,
CHASE_POSITION_CHECK_RESULT_TOO_CLOSE = 0x2,
CHASE_POSITION_CHECK_RESULT_ANGLE_INVALID = 0x4,
CHASE_POSITION_CHECK_RESULT_NOT_IN_LOS = 0x8,
};
enum ChasePositionCheckOptions : uint8
{
CHASE_POSITION_CHECK_NONE = 0x0, // everything's fine
CHASE_POSITION_CHECK_DISTANCE = 0x1, // the target is too far away
CHASE_POSITION_CHECK_BOUNDARY = 0x2, // the target is too close
CHASE_POSITION_CHECK_ANGLE = 0x4, // our angle towards the target is out of bounds
CHASE_POSITION_CHECK_LOS = 0x8 // our target is not in line of sight
};
inline void DoMovementInform(Unit* owner, Unit const* target)
{
if (owner->IsCreature())
return;
if (CreatureAI* AI = owner->ToCreature()->AI())
AI->MovementInform(CHASE_MOTION_TYPE, target->GetGUID().GetCounter());
}
static bool PositionOkay(Unit const* owner, Unit const* target, float distance, Optional<ChaseAngle> angle)
{
float const dist = owner->GetExactDist2d(target);
// owner's distance to its chase target is outside of its range
if (dist > distance)
return false;
// owner's relative angle to its target is not within boundaries
if (angle && !angle->IsAngleOkay(target->GetRelativeAngle(owner)))
return false;
// owner cannot see its target
if (!owner->IsWithinLOSInMap(target))
return false;
return true;
}
inline float GetChaseRange(Unit const* owner, Unit const* target)
inline float GetHitboxSum(Unit const* owner, Unit const* target)
{
float hitboxSum = owner->GetCombatReach() + target->GetCombatReach();
float hoverDelta = owner->GetHoverOffset() - target->GetHoverOffset();
if (hoverDelta != 0.0f)
return std::sqrt(std::max(hitboxSum * hitboxSum - hoverDelta * hoverDelta, 0.0f));
@@ -75,16 +75,25 @@ inline float GetChaseRange(Unit const* owner, Unit const* target)
return hitboxSum;
}
ChaseMovementGenerator::ChaseMovementGenerator(Unit* target, float range, Optional<ChaseAngle> angle) : AbstractPursuer(PursuingType::Chase, ASSERT_NOTNULL(target)), _range(range), _angle(angle) { }
ChaseMovementGenerator::~ChaseMovementGenerator() = default;
inline float SelectMovementSpeed(Unit const* target)
{
UnitMoveType moveType = MOVE_RUN;
if (target->IsFlying())
moveType = MOVE_FLIGHT;
return target->GetSpeed(moveType);
}
ChaseMovementGenerator::ChaseMovementGenerator(Unit* target, Optional<float> range /*= {}*/, Optional<ChaseAngle> angle /*= {}*/) :
AbstractPursuer(PursuingType::Chase, ASSERT_NOTNULL(target)),
_range(range), _angle(angle), _previousChaseSplineId(0), _moveTimer(0), _positionCheckTimer(0) { }
void ChaseMovementGenerator::Initialize(Unit* owner)
{
owner->AddUnitState(UNIT_STATE_CHASE);
owner->SetWalk(false);
_lastTargetPosition.reset();
_nextMovementTimer.Reset(0);
_nextRepositioningTimer.Reset(0);
_pathGenerator = std::make_unique<PathGenerator>(owner);
_moveTimer.Reset(0);
}
bool ChaseMovementGenerator::Update(Unit* owner, uint32 diff)
@@ -93,40 +102,40 @@ bool ChaseMovementGenerator::Update(Unit* owner, uint32 diff)
if (!owner || !owner->IsAlive())
return false;
// validate our target
Unit* target = GetTarget();
// our target might have gone away
if (!target || !target->IsInWorld())
if (!target || !target->IsInWorld() || owner->GetVictim() != target)
return false;
// the owner might've selected a different target (feels like we shouldn't check this here...)
if (owner->GetVictim() != target)
return false;
_moveTimer.Update(diff);
_positionCheckTimer.Update(diff);
// ------------------------ This segment will handle the clearing of the chase state and the movement inform of the previous updtae cycle
// Make sure that the spline that we are moving along right now is our current chase spline
if (owner->movespline->Finalized() || owner->movespline->GetId() != _previousChaseSplineId)
{
if (owner->HasUnitState(UNIT_STATE_CHASE_MOVE))
{
owner->ClearUnitState(UNIT_STATE_CHASE_MOVE);
// trigger movement inform if we have just reached our target
if (owner->movespline->GetId() == _previousChaseSplineId)
DoMovementInform(owner, target);
}
}
// ------------------------ Next up we will perform somce basic checks that prevent the chasing unit to move
// the owner might be unable to move (rooted or casting), pause movement
if (owner->HasUnitState(UNIT_STATE_NOT_MOVE) || owner->IsMovementPreventedByCasting())
{
owner->StopMoving();
_lastTargetPosition.reset();
if (Creature* cOwner = owner->ToCreature())
cOwner->SetCannotReachTarget(false);
return true;
}
// We are done moving. Trigger movement inform hook and clear chase move state
if (owner->HasUnitState(UNIT_STATE_CHASE_MOVE) && owner->movespline->Finalized())
{
if (Creature* cOwner = owner->ToCreature())
cOwner->SetCannotReachTarget(false);
owner->ClearUnitState(UNIT_STATE_CHASE_MOVE);
DoMovementInform(owner, target);
return true;
}
// Owner cannot reach target (example: target is in water and owner cannot swim)
Creature* creature = owner->ToCreature();
if (owner->HasUnitState(UNIT_STATE_CHASE_MOVE) && creature)
// check if the chase target is in an accessible place
if (creature)
{
if (!target->isInAccessiblePlaceFor(creature))
{
@@ -135,58 +144,50 @@ bool ChaseMovementGenerator::Update(Unit* owner, uint32 diff)
return true;
}
}
else if (creature && !target->isInAccessiblePlaceFor(creature))
// ------------------------ Now we can finally perform the movement actions
if (_positionCheckTimer.Passed())
{
creature->SetCannotReachTarget(true);
return true;
}
_positionCheckTimer.Reset(POSITION_CHECK_INTERVAL);
bool const mutualChase = IsMutualChase(owner, target);
float const chaseRange = GetChaseRange(owner, target);
float const rangeTolerance = _range > 0.f ? _range : chaseRange;
Optional<ChaseAngle> chaseAngle = mutualChase ? Optional<ChaseAngle>() : _angle;
// Check the target angle and update the destination
Position destination = target->GetVehicle() ? target->GetVehicle()->GetBase()->GetPosition() : target->GetPosition();
uint8 result = checkPosition(CHASE_POSITION_CHECK_ANGLE, owner, target);
// Update Movement
_nextMovementTimer.Update(diff);
_nextRepositioningTimer.Update(diff);
// Handle repositioning and scattering arround target
if (_nextRepositioningTimer.Passed())
{
_nextRepositioningTimer.Reset(REPOSITION_MOVEMENT_INTERVAL);
if (!owner->HasUnitState(UNIT_STATE_CHASE_MOVE) && owner->IsCreature() && !target->isMoving())
// The target is still moving so we move our chase destination ahead of the target to keep up
if (target->isMoving())
{
// Owner is too close to its target. Step back.
if (owner->GetBoundaryRadius() <= 5.f && owner->GetExactDist2d(target) < owner->GetBoundaryRadius())
{
LaunchMovement(owner, chaseRange, true, mutualChase);
return true;
}
float angle = target->IsPlayer() ? target->m_movementInfo.GetMovementDirection() : 0.f;
if (result & CHASE_POSITION_CHECK_RESULT_ANGLE_INVALID)
angle = Position::NormalizeOrientation(_angle->RelativeAngle + float(M_PI));
target->MovePositionToFirstCollision(destination, SelectMovementSpeed(owner) * 0.5f, angle);
}
}
if (_nextMovementTimer.Passed())
{
_nextMovementTimer.Reset(CHASE_MOVEMENT_INTERVAL);
// Target has moved since we last checked its position. Handle new cases
if (!_lastTargetPosition || target->GetPosition() != _lastTargetPosition.value())
else
{
// Create new snapshot of our target's position
_lastTargetPosition = target->GetPosition();
float angle = target->GetRelativeAngle(owner);
if (result & CHASE_POSITION_CHECK_RESULT_ANGLE_INVALID)
angle = _angle->RelativeAngle;
if (PositionOkay(owner, target, rangeTolerance, chaseAngle))
{
if (owner->HasUnitState(UNIT_STATE_CHASE_MOVE) && !target->isMoving() && !mutualChase)
{
// Our current position is fine. Stop movement.
owner->StopMoving();
return true;
}
}
else if (owner->GetExactDist2d(target) > rangeTolerance + 0.1f) // 0.1f here to avoid edge cases when the owner has stepped back before
LaunchMovement(owner, chaseRange, false, mutualChase);
target->MovePositionToFirstCollision(destination, GetHitboxSum(owner, target), angle);
}
owner->UpdateAllowedPositionZ(destination.GetPositionX(), destination.GetPositionY(), destination.m_positionZ);
// Destination set and ready. Now the usual checks
result |= checkPosition(ChasePositionCheckOptions(CHASE_POSITION_CHECK_DISTANCE | CHASE_POSITION_CHECK_LOS), owner, target, &destination);
if (result == CHASE_POSITION_CHECK_RESULT_VALID)
{
owner->StopMoving();
return true;
}
if (_moveTimer.Passed())
{
_moveTimer.Reset(CHASE_MOVEMENT_INTERVAL);
launchSpline(owner, target, destination);
}
}
@@ -200,7 +201,82 @@ void ChaseMovementGenerator::Finalize(Unit* owner)
cOwner->SetCannotReachTarget(false);
}
void ChaseMovementGenerator::LaunchMovement(Unit* owner, float chaseRange, bool backward /*= false*/, bool mutualChase /*= false*/)
ChaseMovementPositionCheckResult ChaseMovementGenerator::checkPosition(ChasePositionCheckOptions checkOptions, Unit* owner, Unit* target, Position const* destination /*nullptr*/) const
{
uint8 result = CHASE_POSITION_CHECK_RESULT_VALID;
if (checkOptions & CHASE_POSITION_CHECK_DISTANCE)
{
float range = _range.has_value() ? *_range : (GetHitboxSum(owner, target) + float(CONTACT_DISTANCE));
// We have yet to reach the destination of the target so keep moving
if (owner->HasUnitState(UNIT_STATE_CHASE_MOVE) && target->isMoving() && destination)
{
if (owner->GetExactDist(destination) >= range)
result |= CHASE_POSITION_CHECK_RESULT_TOO_FAR;
}
else if (owner->GetExactDist(target) >= range)
result |= CHASE_POSITION_CHECK_RESULT_TOO_FAR;
}
//if (checkOptions & CHASE_POSITION_CHECK_BOUNDARY)
{
}
if (checkOptions & CHASE_POSITION_CHECK_ANGLE)
{
// owner's relative angle to its target is not within boundaries
if (_angle.has_value() && !_angle->IsAngleOkay(target->GetRelativeAngle(owner)))
result |= CHASE_POSITION_CHECK_RESULT_ANGLE_INVALID;
}
if (checkOptions & CHASE_POSITION_CHECK_LOS)
{
// check if the target is in line of sight
if (!owner->IsWithinLOSInMap(target))
result |= CHASE_POSITION_CHECK_RESULT_NOT_IN_LOS;
}
return static_cast<ChaseMovementPositionCheckResult>(result);
}
void ChaseMovementGenerator::launchSpline(Unit* owner, Unit* target, Position& destination)
{
bool success = _pathGenerator->CalculatePath(destination.GetPositionX(), destination.GetPositionY(), destination.GetPositionZ(), owner->IsFlying());
uint32 deniedPathResultTypes = PATHFIND_NOPATH | PATHFIND_INCOMPLETE;
if (!owner->IsFlying() || (target->IsInWater() && !owner->CanEnterWater())) // only flying and swimming units may use shortcuts
deniedPathResultTypes |= PATHFIND_SHORTCUT;
bool cantReachTarget = !success || (_pathGenerator->GetPathType() & deniedPathResultTypes);
if (cantReachTarget)
{
owner->ToCreature()->SetCannotReachTarget(true);
owner->StopMoving();
return;
}
else
owner->ToCreature()->SetCannotReachTarget(false);
Movement::MoveSplineInit init(owner);
init.MovebyPath(_pathGenerator->GetPath());
init.SetWalk(false);
init.SetFacing(target);
if (target->isMoving())
{
// When the target is still moving but the chaser is already within the combat reach of the target, it adapts its speed to the target's one
float targetSpeed = SelectMovementSpeed(target);
if (owner->GetExactDist(target) <= (GetHitboxSum(owner, target) + CONTACT_DISTANCE) && SelectMovementSpeed(owner) > targetSpeed)
init.SetVelocity(targetSpeed);
}
if (init.Launch())
{
_previousChaseSplineId = owner->movespline->GetId();
owner->AddUnitState(UNIT_STATE_CHASE_MOVE);
}
}
/*
{
Unit* target = GetTarget();
@@ -241,7 +317,7 @@ void ChaseMovementGenerator::LaunchMovement(Unit* owner, float chaseRange, bool
PathGenerator path(owner);
bool success = path.CalculatePath(dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), owner->CanFly());
if (!success || (path.GetPathType() & (PATHFIND_NOPATH /*| PATHFIND_INCOMPLETE*/)))
if (!success || (path.GetPathType() & (PATHFIND_NOPATH /*| PATHFIND_INCOMPLETE)))
{
if (creature)
creature->SetCannotReachTarget(true);
@@ -265,3 +341,4 @@ void ChaseMovementGenerator::LaunchMovement(Unit* owner, float chaseRange, bool
if (creature)
creature->SetCannotReachTarget(false);
}
*/
@@ -24,34 +24,38 @@
#include "Timer.h"
class Unit;
enum ChaseMovementPositionCheckResult : uint8;
enum ChasePositionCheckOptions : uint8;
static constexpr uint32 CHASE_MOVEMENT_INTERVAL = 400; // sniffed value (1 batch update cyclice)
static constexpr uint32 POSITION_CHECK_INTERVAL = 100; // the interval at which we check if we already have reached the target
static constexpr uint32 REPOSITION_MOVEMENT_INTERVAL = 1200; // (3 batch update cycles) TODO: verify
class ChaseMovementGenerator : public MovementGenerator, public AbstractPursuer
{
public:
MovementGeneratorType GetMovementGeneratorType() const override { return CHASE_MOTION_TYPE; }
ChaseMovementGenerator(Unit* target, float range, Optional<ChaseAngle> angle = {});
~ChaseMovementGenerator();
explicit ChaseMovementGenerator(Unit* target, Optional<float> range = {}, Optional<ChaseAngle> angle = {});
~ChaseMovementGenerator() = default;
void Initialize(Unit* owner) override;
void Reset(Unit* owner) override { Initialize(owner); }
bool Update(Unit* owner, uint32 diff) override;
void Finalize(Unit* owner) override;
void UnitSpeedChanged() override { _lastTargetPosition.reset(); }
void UnitSpeedChanged() override { }
private:
void LaunchMovement(Unit* owner, float chaseRange, bool backward = false, bool mutualChase = false);
Optional<float> _range;
Optional<ChaseAngle> _angle;
uint32 _previousChaseSplineId;
TimeTrackerSmall _moveTimer;
TimeTracker _positionCheckTimer;
std::unique_ptr<PathGenerator> _pathGenerator;
static constexpr uint32 CHASE_MOVEMENT_INTERVAL = 400; // sniffed value (1 batch update cyclice)
static constexpr uint32 REPOSITION_MOVEMENT_INTERVAL = 1200; // (3 batch update cycles) TODO: verify
TimeTrackerSmall _nextMovementTimer;
TimeTrackerSmall _nextRepositioningTimer;
Optional<Position> _lastTargetPosition;
float const _range;
Optional<ChaseAngle> const _angle;
ChaseMovementPositionCheckResult checkPosition(ChasePositionCheckOptions checkOptions, Unit* owner, Unit* target, Position const* destination = nullptr) const;
void launchSpline(Unit* owner, Unit* target, Position& destination);
};
#endif
@@ -490,7 +490,7 @@ struct npc_drahga_invoked_flaming_spirit : public ScriptedAI
AddThreat(target, 1500000.0f); // Todo: this should be handled by serverside spell 95207
me->GetThreatManager().FixateTarget(target);
me->ClearUnitState(UNIT_STATE_CASTING);
me->GetMotionMaster()->MoveChase(target, 0.f, 0.f);
me->GetMotionMaster()->MoveChase(target);
}
break;
default: