diff --git a/Source/Framework/Constants/SharedConst.cs b/Source/Framework/Constants/SharedConst.cs index 15e216dee..52ce9bc6c 100644 --- a/Source/Framework/Constants/SharedConst.cs +++ b/Source/Framework/Constants/SharedConst.cs @@ -208,7 +208,7 @@ namespace Framework.Constants /// public const int MaxPetStables = 4; public const float PetFollowDist = 1.0f; - public const float PetFollowAngle = MathFunctions.PiOver2; + public const float PetFollowAngle = MathF.PI; public const int MaxSpellCharm = 4; public const int ActionBarIndexStart = 0; public const byte ActionBarIndexPetSpellStart = 3; diff --git a/Source/Game/AI/CoreAI/PetAI.cs b/Source/Game/AI/CoreAI/PetAI.cs index 095193efc..d86a64ffa 100644 --- a/Source/Game/AI/CoreAI/PetAI.cs +++ b/Source/Game/AI/CoreAI/PetAI.cs @@ -474,7 +474,7 @@ namespace Game.AI ClearCharmInfoFlags(); me.GetCharmInfo().SetIsCommandAttack(oldCmdAttack); // For passive pets commanded to attack so they will use spells me.GetMotionMaster().Clear(); - me.GetMotionMaster().MoveChase(target, me.GetPetChaseDistance()); + me.GetMotionMaster().MoveChase(target, me.GetPetChaseDistance(), MathF.PI); } else { diff --git a/Source/Game/Chat/Commands/MiscCommands.cs b/Source/Game/Chat/Commands/MiscCommands.cs index 17896839e..f0f61cdee 100644 --- a/Source/Game/Chat/Commands/MiscCommands.cs +++ b/Source/Game/Chat/Commands/MiscCommands.cs @@ -1245,11 +1245,7 @@ namespace Game.Chat break; case MovementGeneratorType.Chase: { - Unit target; - if (unit.IsTypeId(TypeId.Player)) - target = ((ChaseMovementGenerator)movementGenerator).GetTarget(); - else - target = ((ChaseMovementGenerator)movementGenerator).GetTarget(); + Unit target = ((ChaseMovementGenerator)movementGenerator).GetTarget(); if (!target) handler.SendSysMessage(CypherStrings.MovegensChaseNull); @@ -1261,11 +1257,7 @@ namespace Game.Chat } case MovementGeneratorType.Follow: { - Unit target; - if (unit.IsTypeId(TypeId.Player)) - target = ((FollowMovementGenerator)movementGenerator).GetTarget(); - else - target = ((FollowMovementGenerator)movementGenerator).GetTarget(); + Unit target = ((FollowMovementGenerator)movementGenerator).GetTarget(); if (!target) handler.SendSysMessage(CypherStrings.MovegensFollowNull); diff --git a/Source/Game/Chat/Commands/NPCCommands.cs b/Source/Game/Chat/Commands/NPCCommands.cs index fb62bacf9..07fd68236 100644 --- a/Source/Game/Chat/Commands/NPCCommands.cs +++ b/Source/Game/Chat/Commands/NPCCommands.cs @@ -982,8 +982,7 @@ namespace Game.Chat return false; } - FollowMovementGenerator mgen = (FollowMovementGenerator)creature.GetMotionMaster().Top(); - + FollowMovementGenerator mgen = (FollowMovementGenerator)creature.GetMotionMaster().Top(); if (mgen.GetTarget() != player) { handler.SendSysMessage(CypherStrings.CreatureNotFollowYou, creature.GetName()); diff --git a/Source/Game/Entities/Object/Position.cs b/Source/Game/Entities/Object/Position.cs index db173d1d9..bfb9a4673 100644 --- a/Source/Game/Entities/Object/Position.cs +++ b/Source/Game/Entities/Object/Position.cs @@ -235,7 +235,7 @@ namespace Game.Entities return GetAbsoluteAngle(pos.GetPositionX(), pos.GetPositionY()); } - float ToAbsoluteAngle(float relAngle) + public float ToAbsoluteAngle(float relAngle) { return NormalizeOrientation(relAngle + Orientation); } diff --git a/Source/Game/Entities/Unit/Unit.Combat.cs b/Source/Game/Entities/Unit/Unit.Combat.cs index 7af670f14..0491cd243 100644 --- a/Source/Game/Entities/Unit/Unit.Combat.cs +++ b/Source/Game/Entities/Unit/Unit.Combat.cs @@ -1471,14 +1471,16 @@ namespace Game.Entities } } - public bool IsWithinMeleeRange(Unit obj) + public bool IsWithinMeleeRange(Unit obj) { return IsWithinMeleeRangeAt(GetPosition(), obj); } + + public bool IsWithinMeleeRangeAt(Position pos, Unit obj) { if (!obj || !IsInMap(obj) || !IsInPhase(obj)) return false; - float dx = GetPositionX() - obj.GetPositionX(); - float dy = GetPositionY() - obj.GetPositionY(); - float dz = GetPositionZ() - obj.GetPositionZ(); + float dx = pos.GetPositionX() - obj.GetPositionX(); + float dy = pos.GetPositionY() - obj.GetPositionY(); + float dz = pos.GetPositionZ() - obj.GetPositionZ(); float distsq = (dx * dx) + (dy * dy) + (dz * dz); float maxdist = GetMeleeRange(obj) + GetTotalAuraModifier(AuraType.ModAutoAttackRange); diff --git a/Source/Game/Entities/Unit/Unit.Fields.cs b/Source/Game/Entities/Unit/Unit.Fields.cs index e1b79b4d5..8d62f36bb 100644 --- a/Source/Game/Entities/Unit/Unit.Fields.cs +++ b/Source/Game/Entities/Unit/Unit.Fields.cs @@ -41,7 +41,7 @@ namespace Game.Entities //Movement protected float[] m_speed_rate = new float[(int)UnitMoveType.Max]; - RefManager m_FollowingRefManager; + List m_followingMe = new(); public MoveSpline MoveSpline { get; set; } MotionMaster i_motionMaster; public uint m_movementCounter; //< Incrementing counter used in movement packets diff --git a/Source/Game/Entities/Unit/Unit.Movement.cs b/Source/Game/Entities/Unit/Unit.Movement.cs index d24096664..fc682d669 100644 --- a/Source/Game/Entities/Unit/Unit.Movement.cs +++ b/Source/Game/Entities/Unit/Unit.Movement.cs @@ -406,57 +406,57 @@ namespace Game.Entities case UnitMoveType.Walk: return; case UnitMoveType.Run: + { + if (IsMounted()) // Use on mount auras { - if (IsMounted()) // Use on mount auras - { - main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseMountedSpeed); - stack_bonus = GetTotalAuraMultiplier(AuraType.ModMountedSpeedAlways); - non_stack_bonus += GetMaxPositiveAuraModifier(AuraType.ModMountedSpeedNotStack) / 100.0f; - } - else - { - main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseSpeed); - stack_bonus = GetTotalAuraMultiplier(AuraType.ModSpeedAlways); - non_stack_bonus += GetMaxPositiveAuraModifier(AuraType.ModSpeedNotStack) / 100.0f; - } - break; + main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseMountedSpeed); + stack_bonus = GetTotalAuraMultiplier(AuraType.ModMountedSpeedAlways); + non_stack_bonus += GetMaxPositiveAuraModifier(AuraType.ModMountedSpeedNotStack) / 100.0f; } + else + { + main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseSpeed); + stack_bonus = GetTotalAuraMultiplier(AuraType.ModSpeedAlways); + non_stack_bonus += GetMaxPositiveAuraModifier(AuraType.ModSpeedNotStack) / 100.0f; + } + break; + } case UnitMoveType.Swim: - { - main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseSwimSpeed); - break; - } + { + main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseSwimSpeed); + break; + } case UnitMoveType.Flight: + { + if (IsTypeId(TypeId.Unit) && IsControlledByPlayer()) // not sure if good for pet { - if (IsTypeId(TypeId.Unit) && IsControlledByPlayer()) // not sure if good for pet - { - main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseVehicleFlightSpeed); - stack_bonus = GetTotalAuraMultiplier(AuraType.ModVehicleSpeedAlways); + main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseVehicleFlightSpeed); + stack_bonus = GetTotalAuraMultiplier(AuraType.ModVehicleSpeedAlways); - // for some spells this mod is applied on vehicle owner - int owner_speed_mod = 0; + // for some spells this mod is applied on vehicle owner + int owner_speed_mod = 0; - Unit owner = GetCharmer(); - if (owner != null) - owner_speed_mod = owner.GetMaxPositiveAuraModifier(AuraType.ModIncreaseVehicleFlightSpeed); + Unit owner = GetCharmer(); + if (owner != null) + owner_speed_mod = owner.GetMaxPositiveAuraModifier(AuraType.ModIncreaseVehicleFlightSpeed); - main_speed_mod = Math.Max(main_speed_mod, owner_speed_mod); - } - else if (IsMounted()) - { - main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseMountedFlightSpeed); - stack_bonus = GetTotalAuraMultiplier(AuraType.ModMountedFlightSpeedAlways); - } - else // Use not mount (shapeshift for example) auras (should stack) - main_speed_mod = GetTotalAuraModifier(AuraType.ModIncreaseFlightSpeed) + GetTotalAuraModifier(AuraType.ModIncreaseVehicleFlightSpeed); - - non_stack_bonus += GetMaxPositiveAuraModifier(AuraType.ModFlightSpeedNotStack) / 100.0f; - - // Update speed for vehicle if available - if (IsTypeId(TypeId.Player) && GetVehicle() != null) - GetVehicleBase().UpdateSpeed(UnitMoveType.Flight); - break; + main_speed_mod = Math.Max(main_speed_mod, owner_speed_mod); } + else if (IsMounted()) + { + main_speed_mod = GetMaxPositiveAuraModifier(AuraType.ModIncreaseMountedFlightSpeed); + stack_bonus = GetTotalAuraMultiplier(AuraType.ModMountedFlightSpeedAlways); + } + else // Use not mount (shapeshift for example) auras (should stack) + main_speed_mod = GetTotalAuraModifier(AuraType.ModIncreaseFlightSpeed) + GetTotalAuraModifier(AuraType.ModIncreaseVehicleFlightSpeed); + + non_stack_bonus += GetMaxPositiveAuraModifier(AuraType.ModFlightSpeedNotStack) / 100.0f; + + // Update speed for vehicle if available + if (IsTypeId(TypeId.Player) && GetVehicle() != null) + GetVehicleBase().UpdateSpeed(UnitMoveType.Flight); + break; + } default: Log.outError(LogFilter.Unit, "Unit.UpdateSpeed: Unsupported move type ({0})", mtype); return; @@ -472,71 +472,70 @@ namespace Game.Entities case UnitMoveType.Run: case UnitMoveType.Swim: case UnitMoveType.Flight: + { + // Set creature speed rate + if (IsTypeId(TypeId.Unit)) + speed *= ToCreature().GetCreatureTemplate().SpeedRun; // at this point, MOVE_WALK is never reached + + // Normalize speed by 191 aura SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED if need + // @todo possible affect only on MOVE_RUN + int normalization = GetMaxPositiveAuraModifier(AuraType.UseNormalMovementSpeed); + if (normalization != 0) { - // Set creature speed rate - if (IsTypeId(TypeId.Unit)) + Creature creature1 = ToCreature(); + if (creature1) { - Unit pOwner = GetCharmerOrOwner(); - if ((IsPet() || IsGuardian()) && !IsInCombat() && pOwner != null) // Must check for owner or crash on "Tame Beast" - { - // For every yard over 5, increase speed by 0.01 - // to help prevent pet from lagging behind and despawning - float dist = GetDistance(pOwner); - float base_rate = 1.00f; // base speed is 100% of owner speed - - if (dist < 5) - dist = 5; - - float mult = base_rate + ((dist - 5) * 0.01f); - - speed *= pOwner.GetSpeedRate(mtype) * mult; // pets derive speed from owner when not in combat - } - else - speed *= ToCreature().GetCreatureTemplate().SpeedRun; // at this point, MOVE_WALK is never reached + uint immuneMask = creature1.GetCreatureTemplate().MechanicImmuneMask; + if (Convert.ToBoolean(immuneMask & (1 << ((int)Mechanics.Snare - 1))) || Convert.ToBoolean(immuneMask & (1 << ((int)Mechanics.Daze - 1)))) + break; } - // Normalize speed by 191 aura SPELL_AURA_USE_NORMAL_MOVEMENT_SPEED if need - // @todo possible affect only on MOVE_RUN - int normalization = GetMaxPositiveAuraModifier(AuraType.UseNormalMovementSpeed); - if (normalization != 0) - { - Creature creature = ToCreature(); - if (creature) - { - uint immuneMask = creature.GetCreatureTemplate().MechanicImmuneMask; - if (Convert.ToBoolean(immuneMask & (1 << ((int)Mechanics.Snare - 1))) || Convert.ToBoolean(immuneMask & (1 << ((int)Mechanics.Daze - 1)))) - break; - } - - // Use speed from aura - float max_speed = normalization / (IsControlledByPlayer() ? SharedConst.playerBaseMoveSpeed[(int)mtype] : SharedConst.baseMoveSpeed[(int)mtype]); - if (speed > max_speed) - speed = max_speed; - } - - if (mtype == UnitMoveType.Run) - { - // force minimum speed rate @ aura 437 SPELL_AURA_MOD_MINIMUM_SPEED_RATE - int minSpeedMod1 = GetMaxPositiveAuraModifier(AuraType.ModMinimumSpeedRate); - if (minSpeedMod1 != 0) - { - float minSpeed = minSpeedMod1 / (IsControlledByPlayer() ? SharedConst.playerBaseMoveSpeed[(int)mtype] : SharedConst.baseMoveSpeed[(int)mtype]); - if (speed < minSpeed) - speed = minSpeed; - } - } - - break; + // Use speed from aura + float max_speed = normalization / (IsControlledByPlayer() ? SharedConst.playerBaseMoveSpeed[(int)mtype] : SharedConst.baseMoveSpeed[(int)mtype]); + if (speed > max_speed) + speed = max_speed; } + + if (mtype == UnitMoveType.Run) + { + // force minimum speed rate @ aura 437 SPELL_AURA_MOD_MINIMUM_SPEED_RATE + int minSpeedMod1 = GetMaxPositiveAuraModifier(AuraType.ModMinimumSpeedRate); + if (minSpeedMod1 != 0) + { + float minSpeed = minSpeedMod1 / (IsControlledByPlayer() ? SharedConst.playerBaseMoveSpeed[(int)mtype] : SharedConst.baseMoveSpeed[(int)mtype]); + if (speed < minSpeed) + speed = minSpeed; + } + } + + break; + } default: break; } - // for creature case, we check explicit if mob searched for assistance - if (IsTypeId(TypeId.Unit)) + Creature creature = ToCreature(); + if (creature != null) { + // for creature case, we check explicit if mob searched for assistance if (ToCreature().HasSearchedAssistance()) speed *= 0.66f; // best guessed value, so this will be 33% reduction. Based off initial speed, mob can then "run", "walk fast" or "walk". + + if (creature.HasUnitTypeMask(UnitTypeMask.Minion) && !creature.IsInCombat()) + { + var top = creature.GetMotionMaster().Top(); + if (top != null && top.GetMovementGeneratorType() == MovementGeneratorType.Follow) + { + Unit followed = ((AbstractFollower)top).GetTarget(); + if (followed != null && followed.GetGUID() == GetOwnerGUID() && !followed.IsInCombat()) + { + float ownerSpeed = followed.GetSpeedRate(mtype); + if (speed < ownerSpeed || creature.IsWithinDist3d(followed, 10.0f)) + speed = ownerSpeed; + speed *= Math.Min(Math.Max(1.0f, 0.75f + (GetDistance(followed) - SharedConst.PetFollowDist) * 0.05f), 1.3f); + } + } + } } // Apply strongest slow aura mod to speed diff --git a/Source/Game/Entities/Unit/Unit.cs b/Source/Game/Entities/Unit/Unit.cs index 4d7f7730a..17ed9e212 100644 --- a/Source/Game/Entities/Unit/Unit.cs +++ b/Source/Game/Entities/Unit/Unit.cs @@ -44,7 +44,6 @@ namespace Game.Entities m_combatManager = new CombatManager(this); m_threatManager = new ThreatManager(this); _spellHistory = new SpellHistory(this); - m_FollowingRefManager = new RefManager(); ObjectTypeId = TypeId.Unit; ObjectTypeMask |= TypeMask.Unit; @@ -454,6 +453,8 @@ namespace Game.Entities RemoveAreaAurasDueToLeaveWorld(); + RemoveAllFollowers(); + if (!GetCharmerGUID().IsEmpty()) { Log.outFatal(LogFilter.Unit, "Unit {0} has charmer guid when removed from world", GetEntry()); @@ -1312,6 +1313,13 @@ namespace Game.Entities public Totem ToTotem() { return IsTotem() ? (this as Totem) : null; } public TempSummon ToTempSummon() { return IsSummon() ? (this as TempSummon) : null; } + + void RemoveAllFollowers() + { + while (!m_followingMe.Empty()) + m_followingMe[0].SetTarget(null); + } + public virtual void SetDeathState(DeathState s) { // Death state needs to be updated before RemoveAllAurasOnDeath() is called, to prevent entering combat @@ -1563,11 +1571,8 @@ namespace Game.Entities public bool IsCharmedOwnedByPlayerOrPlayer() { return GetCharmerOrOwnerOrOwnGUID().IsPlayer(); } - public void AddFollower(FollowerReference pRef) - { - m_FollowingRefManager.InsertFirst(pRef); - } - public void RemoveFollower(FollowerReference pRef) { } //nothing to do yet + public void FollowerAdded(AbstractFollower f) { m_followingMe.Add(f); } + public void FollowerRemoved(AbstractFollower f) { m_followingMe.Remove(f); } public uint GetCreatureTypeMask() { diff --git a/Source/Game/Movement/AbstractFollower.cs b/Source/Game/Movement/AbstractFollower.cs new file mode 100644 index 000000000..52721f1fb --- /dev/null +++ b/Source/Game/Movement/AbstractFollower.cs @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012-2020 CypherCore + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Game.Entities; + +namespace Game.Movement +{ + public class AbstractFollower + { + Unit _target; + + public AbstractFollower(Unit target = null) { SetTarget(target); } + + public void SetTarget(Unit unit) + { + if (unit == _target) + return; + + if (_target) + _target.FollowerRemoved(this); + + _target = unit; + + if (_target) + _target.FollowerAdded(this); + } + + public Unit GetTarget() { return _target; } + } +} diff --git a/Source/Game/Movement/Generators/ChaseMovementGenerator.cs b/Source/Game/Movement/Generators/ChaseMovementGenerator.cs new file mode 100644 index 000000000..14dd457cb --- /dev/null +++ b/Source/Game/Movement/Generators/ChaseMovementGenerator.cs @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2012-2020 CypherCore + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using Framework.Constants; +using Game.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Framework.GameMath; + +namespace Game.Movement +{ + class ChaseMovementGenerator : AbstractFollower, IMovementGenerator + { + static uint RANGE_CHECK_INTERVAL = 100; // time (ms) until we attempt to recalculate + + ChaseRange? _range; + ChaseAngle? _angle; + + PathGenerator _path; + Position _lastTargetPosition; + uint _rangeCheckTimer = RANGE_CHECK_INTERVAL; + bool _movingTowards = true; + bool _mutualChase = true; + + public ChaseMovementGenerator(Unit target, ChaseRange? range, ChaseAngle? angle) : base(target) + { + _range = range; + _angle = angle; + } + + public void Initialize(Unit owner) + { + owner.AddUnitState(UnitState.Chase); + owner.SetWalk(false); + } + + public bool Update(Unit owner, uint diff) + { + // owner might be dead or gone (can we even get nullptr here?) + if (!owner || !owner.IsAlive()) + return false; + + // our target might have gone away + Unit target = GetTarget(); + if (!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; + + // the owner might be unable to move (rooted or casting), pause movement + if (owner.HasUnitState(UnitState.NotMove) || owner.IsMovementPreventedByCasting()) + { + owner.StopMoving(); + return true; + } + + bool mutualChase = IsMutualChase(owner, target); + float hitboxSum = owner.GetCombatReach() + target.GetCombatReach(); + float minRange = _range.HasValue ? _range.Value.MinRange + hitboxSum : SharedConst.ContactDistance; + float minTarget = (_range.HasValue ? _range.Value.MinTolerance : 0.0f) + hitboxSum; + float maxRange = _range.HasValue ? _range.Value.MaxRange + hitboxSum : owner.GetMeleeRange(target); // melee range already includes hitboxes + float maxTarget = _range.HasValue ? _range.Value.MaxTolerance + hitboxSum : SharedConst.ContactDistance + hitboxSum; + ChaseAngle? angle = mutualChase ? null : _angle; + + // if we're already moving, periodically check if we're already in the expected range... + if (owner.HasUnitState(UnitState.ChaseMove)) + { + if (_rangeCheckTimer > diff) + _rangeCheckTimer -= diff; + else + { + _rangeCheckTimer = RANGE_CHECK_INTERVAL; + if (PositionOkay(owner, target, _movingTowards ? null : minTarget, _movingTowards ? maxTarget : null, angle)) + { + _path = null; + owner.StopMoving(); + owner.SetInFront(target); + return true; + } + } + } + + // if we're done moving, we want to clean up + if (owner.HasUnitState(UnitState.ChaseMove) && owner.MoveSpline.Finalized()) + { + _path = null; + owner.ClearUnitState(UnitState.ChaseMove); + owner.SetInFront(target); + } + + // if the target moved, we have to consider whether to adjust + if (_lastTargetPosition != target.GetPosition() || mutualChase != _mutualChase) + { + _lastTargetPosition = target.GetPosition(); + _mutualChase = mutualChase; + if (owner.HasUnitState(UnitState.ChaseMove) || !PositionOkay(owner, target, minRange, maxRange, angle)) + { + Creature cOwner = owner.ToCreature(); + // can we get to the target? + if (cOwner != null && !target.IsInAccessiblePlaceFor(cOwner)) + { + cOwner.SetCannotReachTarget(true); + cOwner.StopMoving(); + _path = null; + return true; + } + + // figure out which way we want to move + bool moveToward = !owner.IsInDist(target, maxRange); + + // make a new path if we have to... + if (_path == null || moveToward != _movingTowards) + _path = new PathGenerator(owner); + + float x, y, z; + bool shortenPath; + // if we want to move toward the target and there's no fixed angle... + if (moveToward && !angle.HasValue) + { + // ...we'll pathfind to the center, then shorten the path + target.GetPosition(out x, out y, out z); + shortenPath = true; + } + else + { + // otherwise, we fall back to nearpoint finding + target.GetNearPoint(owner, out x, out y, out z, (moveToward ? maxTarget : minTarget) - hitboxSum, angle.HasValue ? target.ToAbsoluteAngle(angle.Value.RelativeAngle) : target.GetAbsoluteAngle(owner)); + shortenPath = false; + } + + if (owner.IsHovering()) + owner.UpdateAllowedPositionZ(x, y, ref z); + + bool success = _path.CalculatePath(x, y, z); + if (!success || _path.GetPathType().HasFlag(PathType.NoPath)) + { + if (cOwner) + cOwner.SetCannotReachTarget(true); + owner.StopMoving(); + return true; + } + + if (shortenPath) + _path.ShortenPathUntilDist(target, maxTarget); + + if (cOwner) + cOwner.SetCannotReachTarget(false); + owner.AddUnitState(UnitState.ChaseMove); + + MoveSplineInit init = new(owner); + init.MovebyPath(_path.GetPath()); + init.SetWalk(false); + init.SetFacing(target); + + init.Launch(); + } + } + + // and then, finally, we're done for the tick + return true; + } + + public void Finalize(Unit owner) + { + owner.ClearUnitState(UnitState.Chase | UnitState.ChaseMove); + Creature cOwner = owner.ToCreature(); + if (cOwner != null) + cOwner.SetCannotReachTarget(false); + } + + public MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Chase; } + + public void Reset(Unit owner) { Initialize(owner); } + + public void UnitSpeedChanged() { _lastTargetPosition.Relocate(0.0f, 0.0f, 0.0f); } + + static bool IsMutualChase(Unit owner, Unit target) + { + IMovementGenerator gen = target.GetMotionMaster().Top(); + if (gen == null || gen.GetMovementGeneratorType() != MovementGeneratorType.Chase) + return false; + return ((ChaseMovementGenerator)gen).GetTarget() == owner; + } + + static bool PositionOkay(Unit owner, Unit target, float? minDistance, float? maxDistance, ChaseAngle? angle) + { + float distSq = owner.GetExactDistSq(target); + if (minDistance.HasValue && distSq < MathF.Sqrt(minDistance.Value)) + return false; + if (maxDistance.HasValue && distSq > MathF.Sqrt(maxDistance.Value)) + return false; + return !angle.HasValue || angle.Value.IsAngleOkay(target.GetRelativeAngle(owner)); + } + } +} diff --git a/Source/Game/Movement/Generators/FollowMovementGenerator.cs b/Source/Game/Movement/Generators/FollowMovementGenerator.cs new file mode 100644 index 000000000..6b17082c5 --- /dev/null +++ b/Source/Game/Movement/Generators/FollowMovementGenerator.cs @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2012-2020 CypherCore + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using Framework.Constants; +using Framework.GameMath; +using Game.Entities; +using System; + +namespace Game.Movement +{ + public class FollowMovementGenerator : AbstractFollower, IMovementGenerator + { + static uint CHECK_INTERVAL = 500; + static float FOLLOW_RANGE_TOLERANCE = 1.0f; + + float _range; + ChaseAngle _angle; + + uint _checkTimer = CHECK_INTERVAL; + PathGenerator _path; + Position _lastTargetPosition; + + public FollowMovementGenerator(Unit target, float range, ChaseAngle angle) : base(target) + { + _range = range; + _angle = angle; + } + + public void Initialize(Unit owner) + { + owner.AddUnitState(UnitState.Follow); + UpdatePetSpeed(owner); + } + + public bool Update(Unit owner, uint diff) + { + // owner might be dead or gone + if (!owner.IsAlive()) + return false; + + // our target might have gone away + Unit target = GetTarget(); + if (target == null) + return false; + + if (owner.HasUnitState(UnitState.NotMove) || owner.IsMovementPreventedByCasting()) + { + owner.StopMoving(); + return true; + } + + if (owner.HasUnitState(UnitState.FollowMove)) + { + if (_checkTimer > diff) + _checkTimer -= diff; + else + { + _checkTimer = CHECK_INTERVAL; + if (PositionOkay(owner, target, _range, _angle)) + { + _path = null; + owner.StopMoving(); + return true; + } + } + } + + if (owner.HasUnitState(UnitState.FollowMove) && owner.MoveSpline.Finalized()) + { + _path = null; + owner.ClearUnitState(UnitState.FollowMove); + } + + if (_lastTargetPosition.GetExactDistSq(target.GetPosition()) > 0.0f) + { + _lastTargetPosition = target.GetPosition(); + if (owner.HasUnitState(UnitState.FollowMove) || !PositionOkay(owner, target, _range + FOLLOW_RANGE_TOLERANCE)) + { + if (_path == null) + _path = new PathGenerator(owner); + + float x, y, z; + + // select angle + float tAngle; + float curAngle = target.GetRelativeAngle(owner); + if (_angle.IsAngleOkay(curAngle)) + tAngle = curAngle; + else + { + float diffUpper = Position.NormalizeOrientation(curAngle - _angle.UpperBound()); + float diffLower = Position.NormalizeOrientation(_angle.LowerBound() - curAngle); + if (diffUpper < diffLower) + tAngle = _angle.UpperBound(); + else + tAngle = _angle.LowerBound(); + } + + target.GetNearPoint(owner, out x, out y, out z, _range, target.ToAbsoluteAngle(tAngle)); + + if (owner.IsHovering()) + owner.UpdateAllowedPositionZ(x, y, ref z); + + // pets are allowed to "cheat" on pathfinding when following their master + bool allowShortcut = false; + Pet oPet = owner.ToPet(); + if (oPet != null) + if (target.GetGUID() == oPet.GetOwnerGUID()) + allowShortcut = true; + + bool success = _path.CalculatePath(x, y, z, allowShortcut); + if (!success || _path.GetPathType().HasFlag(PathType.NoPath)) + { + owner.StopMoving(); + return true; + } + + owner.AddUnitState(UnitState.FollowMove); + + MoveSplineInit init = new(owner); + init.MovebyPath(_path.GetPath()); + init.SetWalk(target.IsWalking()); + init.SetFacing(target.GetOrientation()); + init.Launch(); + } + } + return true; + } + + public void Finalize(Unit owner) + { + owner.ClearUnitState(UnitState.Follow | UnitState.FollowMove); + UpdatePetSpeed(owner); + } + + void UpdatePetSpeed(Unit owner) + { + Pet oPet = owner.ToPet(); + if (oPet != null) + { + if (!GetTarget() || GetTarget().GetGUID() == owner.GetOwnerGUID()) + { + oPet.UpdateSpeed(UnitMoveType.Run); + oPet.UpdateSpeed(UnitMoveType.Walk); + oPet.UpdateSpeed(UnitMoveType.Swim); + } + } + } + + public void Reset(Unit owner) { } + + public MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Follow; } + + public void UnitSpeedChanged() { _lastTargetPosition.Relocate(0.0f, 0.0f, 0.0f); } + + static bool PositionOkay(Unit owner, Unit target, float range, ChaseAngle? angle = null) + { + if (owner.GetExactDistSq(target) > MathF.Sqrt(owner.GetCombatReach() + target.GetCombatReach() + range)) + return false; + + return !angle.HasValue || angle.Value.IsAngleOkay(target.GetRelativeAngle(owner)); + } + } +} diff --git a/Source/Game/Movement/Generators/FormationMovement.cs b/Source/Game/Movement/Generators/FormationMovement.cs index e06d52247..82c384440 100644 --- a/Source/Game/Movement/Generators/FormationMovement.cs +++ b/Source/Game/Movement/Generators/FormationMovement.cs @@ -133,7 +133,7 @@ namespace Game.Movement MovementInform(owner); } - public override void UnitSpeedChanged() + public void UnitSpeedChanged() { _recalculateSpeed = true; } diff --git a/Source/Game/Movement/Generators/IdleMovement.cs b/Source/Game/Movement/Generators/IdleMovement.cs index 2ffc05569..e0ea8ad71 100644 --- a/Source/Game/Movement/Generators/IdleMovement.cs +++ b/Source/Game/Movement/Generators/IdleMovement.cs @@ -22,27 +22,27 @@ namespace Game.Movement { public class IdleMovementGenerator : IMovementGenerator { - public override void Initialize(Unit owner) + public void Initialize(Unit owner) { Reset(owner); } - public override void Reset(Unit owner) + public void Reset(Unit owner) { if (!owner.IsStopped()) owner.StopMoving(); } - public override bool Update(Unit owner, uint diff) + public bool Update(Unit owner, uint diff) { return true; } - public override void Finalize(Unit owner) + public void Finalize(Unit owner) { } - public override MovementGeneratorType GetMovementGeneratorType() + public MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Idle; } @@ -59,7 +59,7 @@ namespace Game.Movement _direction = direction; } - public override void Initialize(Unit owner) + public void Initialize(Unit owner) { if (!owner.IsStopped()) owner.StopMoving(); @@ -71,9 +71,9 @@ namespace Game.Movement owner.AttackStop(); } - public override void Reset(Unit owner) { } + public void Reset(Unit owner) { } - public override bool Update(Unit owner, uint diff) + public bool Update(Unit owner, uint diff) { float angle = owner.GetOrientation(); angle += diff * MathFunctions.TwoPi / _maxDuration * (_direction == RotateDirection.Left ? 1.0f : -1.0f); @@ -90,14 +90,14 @@ namespace Game.Movement return true; } - public override void Finalize(Unit owner) + public void Finalize(Unit owner) { owner.ClearUnitState(UnitState.Rotating); if (owner.IsTypeId(TypeId.Unit)) owner.ToCreature().GetAI().MovementInform(MovementGeneratorType.Rotate, 0); } - public override MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Rotate; } + public MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Rotate; } uint _duration; uint _maxDuration; @@ -111,7 +111,7 @@ namespace Game.Movement _timer = timer; } - public override void Initialize(Unit owner) + public void Initialize(Unit owner) { // Distracted creatures stand up if not standing if (!owner.IsStandState()) @@ -120,9 +120,9 @@ namespace Game.Movement owner.AddUnitState(UnitState.Distracted); } - public override void Reset(Unit owner) { } + public void Reset(Unit owner) { } - public override bool Update(Unit owner, uint diff) + public bool Update(Unit owner, uint diff) { if (diff > _timer) return false; @@ -131,7 +131,7 @@ namespace Game.Movement return true; } - public override void Finalize(Unit owner) + public virtual void Finalize(Unit owner) { owner.ClearUnitState(UnitState.Distracted); @@ -143,7 +143,7 @@ namespace Game.Movement } } - public override MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Distract; } + public virtual MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Distract; } uint _timer; } diff --git a/Source/Game/Movement/Generators/MovementGenerators.cs b/Source/Game/Movement/Generators/MovementGenerators.cs index fde14882e..d8d38c36c 100644 --- a/Source/Game/Movement/Generators/MovementGenerators.cs +++ b/Source/Game/Movement/Generators/MovementGenerators.cs @@ -18,51 +18,52 @@ using Framework.Constants; using Framework.Dynamic; using Game.Entities; +using Framework.GameMath; namespace Game.Movement { - public abstract class IMovementGenerator + public interface IMovementGenerator { - public abstract void Finalize(Unit owner); + public void Finalize(Unit owner); - public abstract void Initialize(Unit owner); + public void Initialize(Unit owner); - public abstract void Reset(Unit owner); + public void Reset(Unit owner); - public abstract bool Update(Unit owner, uint time_diff); + public bool Update(Unit owner, uint time_diff); - public abstract MovementGeneratorType GetMovementGeneratorType(); + public MovementGeneratorType GetMovementGeneratorType(); - public virtual void UnitSpeedChanged() { } + public void UnitSpeedChanged() { } - public virtual void Pause(uint timer = 0) { } + public void Pause(uint timer = 0) { } - public virtual void Resume(uint overrideTimer = 0) { } + public void Resume(uint overrideTimer = 0) { } // used by Evade code for select point to evade with expected restart default movement - public virtual bool GetResetPosition(Unit u, out float x, out float y, out float z) + public bool GetResetPosition(Unit u, out float x, out float y, out float z) { - x = y = z = 0.0f; + x = y = z = 0.0f; return false; } } public abstract class MovementGeneratorMedium : IMovementGenerator where T : Unit { - public override void Initialize(Unit owner) + public virtual void Initialize(Unit owner) { DoInitialize((T)owner); IsActive = true; } - public override void Finalize(Unit owner) + public virtual void Finalize(Unit owner) { DoFinalize((T)owner); } - public override void Reset(Unit owner) + public virtual void Reset(Unit owner) { DoReset((T)owner); } - public override bool Update(Unit owner, uint diff) + public virtual bool Update(Unit owner, uint diff) { return DoUpdate((T)owner, diff); } @@ -73,23 +74,7 @@ namespace Game.Movement public abstract void DoFinalize(T owner); public abstract void DoReset(T owner); public abstract bool DoUpdate(T owner, uint diff); - } - public class FollowerReference : Reference - { - public override void TargetObjectBuildLink() - { - GetTarget().AddFollower(this); - } - - public override void TargetObjectDestroyLink() - { - GetTarget().RemoveFollower(this); - } - - public override void SourceObjectDestroyLink() - { - GetSource().StopFollowing(); - } + public virtual MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Max; } } } diff --git a/Source/Game/Movement/Generators/PathGenerator.cs b/Source/Game/Movement/Generators/PathGenerator.cs index 6ac1f8cb1..ad21b85bf 100644 --- a/Source/Game/Movement/Generators/PathGenerator.cs +++ b/Source/Game/Movement/Generators/PathGenerator.cs @@ -864,43 +864,52 @@ namespace Game.Movement return (p1 - p2).GetLengthSquared(); } - public void ReducePathLenghtByDist(float dist) + public void ShortenPathUntilDist(Position pos, float dist) { ShortenPathUntilDist(new Vector3(pos.posX, pos.posY, pos.posZ), dist); } + + public void ShortenPathUntilDist(Vector3 target, float dist) { - if (GetPathType() == PathType.Blank) + if (GetPathType() == PathType.Blank || _pathPoints.Length < 2) { - Log.outError(LogFilter.Maps, "PathGenerator.ReducePathLenghtByDist called before path was built"); + Log.outError(LogFilter.Maps, "PathGenerator.ReducePathLengthByDist called before path was successfully built"); return; } - if (_pathPoints.Length < 2) // path building failure + float distSq = dist * dist; + + // the first point of the path must be outside the specified range + // (this should have really been checked by the caller...) + if ((_pathPoints[0] - target).GetLengthSquared() < distSq) return; - int i = _pathPoints.Length; - Vector3 nextVec = _pathPoints[--i]; - while (i > 0) + // check if we even need to do anything + if ((_pathPoints[0] - target).GetLengthSquared() >= distSq) + return; + + int i = _pathPoints.Length - 1; + // find the first i s.t.: + // - _pathPoints[i] is still too close + // - _pathPoints[i-1] is too far away + // => the end point is somewhere on the line between the two + while (true) { - Vector3 currVec = _pathPoints[--i]; - Vector3 diffVec = (nextVec - currVec); - float len = diffVec.GetLength(); - if (len > dist) + // we know that pathPoints[i] is too close already (from the previous iteration) + if ((_pathPoints[i - 1] - target).GetLengthSquared() >= distSq) + break; // bingo! + + if (--i == 0) { - float step = dist / len; - // same as nextVec - _pathPoints[i + 1] -= diffVec * step; - _sourceUnit.UpdateAllowedPositionZ(_pathPoints[i + 1].X, _pathPoints[i + 1].Y, ref _pathPoints[i + 1].Z); - Array.Resize(ref _pathPoints, i + 2); - break; - } - else if (i == 0) // at second point - { - _pathPoints[1] = _pathPoints[0]; + // no point found that fulfills the condition + _pathPoints[0] = _pathPoints[1]; Array.Resize(ref _pathPoints, 2); - break; + return; } - - dist -= len; - nextVec = currVec; // we're going backwards } + + // ok, _pathPoints[i] is too close, _pathPoints[i-1] is not, so our target point is somewhere between the two... + // ... settle for a guesstimate since i'm not confident in doing trig on every chase motion tick... + // (@todo review this) + _pathPoints[i] += (_pathPoints[i - 1] - _pathPoints[i]).directionOrZero() * (dist - (_pathPoints[i] - target).GetLength()); + Array.Resize(ref _pathPoints, i + 1); } public bool IsInvalidDestinationZ(Unit target) diff --git a/Source/Game/Movement/Generators/PointMovement.cs b/Source/Game/Movement/Generators/PointMovement.cs index df30e5a0e..fa8f34b7f 100644 --- a/Source/Game/Movement/Generators/PointMovement.cs +++ b/Source/Game/Movement/Generators/PointMovement.cs @@ -135,7 +135,7 @@ namespace Game.Movement } } - public override void UnitSpeedChanged() + public void UnitSpeedChanged() { _recalculateSpeed = true; } @@ -183,16 +183,16 @@ namespace Game.Movement _arrivalSpellTargetGuid = arrivalSpellTargetGuid; } - public override void Initialize(Unit owner) { } + public void Initialize(Unit owner) { } - public override void Reset(Unit owner) { } + public void Reset(Unit owner) { } - public override bool Update(Unit owner, uint diff) + public bool Update(Unit owner, uint diff) { return !owner.MoveSpline.Finalized(); } - public override void Finalize(Unit owner) + public void Finalize(Unit owner) { MovementInform(owner); } @@ -206,7 +206,7 @@ namespace Game.Movement owner.ToCreature().GetAI().MovementInform(MovementGeneratorType.Effect, _pointId); } - public override MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Effect; } + public MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Effect; } uint _pointId; uint _arrivalSpellId; diff --git a/Source/Game/Movement/Generators/SplineChainMovementGenerator.cs b/Source/Game/Movement/Generators/SplineChainMovementGenerator.cs index f33b5afe3..de84ce491 100644 --- a/Source/Game/Movement/Generators/SplineChainMovementGenerator.cs +++ b/Source/Game/Movement/Generators/SplineChainMovementGenerator.cs @@ -75,7 +75,7 @@ namespace Game.Movement } } - public override void Initialize(Unit me) + public void Initialize(Unit me) { if (_chainSize != 0) { @@ -114,7 +114,7 @@ namespace Game.Movement } } - public override void Finalize(Unit me) + public void Finalize(Unit me) { if (!finished) return; @@ -124,7 +124,7 @@ namespace Game.Movement cMe.GetAI().MovementInform(MovementGeneratorType.SplineChain, _id); } - public override bool Update(Unit me, uint diff) + public bool Update(Unit me, uint diff) { if (finished) return false; @@ -169,9 +169,9 @@ namespace Game.Movement return new SplineChainResumeInfo(_id, _chain, _walk, (byte)(_nextIndex - 1), (byte)(me.MoveSpline.CurrentSplineIdx()), _msToNext); } - public override void Reset(Unit owner) { } + public void Reset(Unit owner) { } - public override MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.SplineChain; } + public MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.SplineChain; } uint _id; List _chain = new(); diff --git a/Source/Game/Movement/Generators/TargetMovement.cs b/Source/Game/Movement/Generators/TargetMovement.cs deleted file mode 100644 index 7c5254bc5..000000000 --- a/Source/Game/Movement/Generators/TargetMovement.cs +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright (C) 2012-2020 CypherCore - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -using Framework.Constants; -using Framework.GameMath; -using Game.Entities; -using System; - -namespace Game.Movement -{ - public interface ITargetedMovementGeneratorBase - { - bool IsTargetValid(); - Unit GetTarget(); - void StopFollowing(); - } - - public abstract class TargetedMovementGenerator : MovementGeneratorMedium, ITargetedMovementGeneratorBase where T : Unit - { - protected static float MOVE_FOLLOW_REPOSITIONING_DISTANCE = 1.5f; - - protected TargetedMovementGenerator(Unit target, float offset = 0, float angle = 0) - { - _target = new FollowerReference(); - _target.Link(target, this); - _timer = new TimeTracker(); - _offset = offset; - _angle = angle; - _recalculateTravel = false; - _targetReached = false; - } - - public override bool DoUpdate(T owner, uint diff) - { - if (!IsTargetValid() || !GetTarget().IsInWorld) - return false; - - if (owner == null || !owner.IsAlive()) - return false; - - if (owner.HasUnitState(UnitState.NotMove) || owner.IsMovementPreventedByCasting() || HasLostTarget(owner)) - { - _interrupt = true; - owner.StopMoving(); - return true; - } - - if (_interrupt || _recalculateTravel) - { - _interrupt = false; - SetTargetLocation(owner, true); - return true; - } - - - bool targetMoved = false; - _timer.Update((int)diff); - if (_timer.Passed()) - { - _timer.Reset(100); - - // the allowed distance between target and mover before the mover needs to reposition in order to keep attacking - float distance = GetMaxDistanceBeforeRepositioning(owner); - if (owner.IsPet() && (owner.GetCharmerOrOwnerGUID() == GetTarget().GetGUID())) - distance = 1.0f + GetTarget().GetCombatReach(); // pet following owner - - Vector3 destination = owner.MoveSpline.FinalDestination(); - if (owner.MoveSpline.onTransport) - { - float o = 0; - ITransport transport = owner.GetDirectTransport(); - if (transport != null) - transport.CalculatePassengerPosition(ref destination.X, ref destination.Y, ref destination.Z, ref o); - } - - // First check distance - targetMoved = !GetTarget().IsInDist(destination.X, destination.Y, destination.Z, distance); - - // then, if the target is in range, check also Line of Sight. - if (!targetMoved) - targetMoved = !GetTarget().IsWithinLOSInMap(owner); - } - - if (targetMoved) - SetTargetLocation(owner, true); - else if (_speedChanged) - SetTargetLocation(owner, false); - - if (!_targetReached && owner.MoveSpline.Finalized()) - { - MovementInform(owner); - if (_angle == 0.0f && !owner.HasInArc(0.01f, GetTarget())) - owner.SetInFront(GetTarget()); - - if (!_targetReached) - { - _targetReached = true; - ReachTarget(owner); - } - } - - return true; - } - - public void SetTargetLocation(T owner, bool updateDestination) - { - if (!IsTargetValid() || !GetTarget().IsInWorld) - return; - - if (!owner || !owner.IsAlive()) - return; - - - if (owner.HasUnitState(UnitState.NotMove) || owner.IsMovementPreventedByCasting() || HasLostTarget(owner)) - { - _interrupt = true; - owner.StopMoving(); - return; - } - - if (owner.IsTypeId(TypeId.Unit) && !GetTarget().IsInAccessiblePlaceFor(owner.ToCreature())) - { - owner.ToCreature().SetCannotReachTarget(true); - return; - } - - float x, y, z; - if (updateDestination || _path == null) - { - if (_offset == 0) - { - if (GetTarget().IsWithinDistInMap(owner, SharedConst.ContactDistance)) - return; - - GetTarget().GetNearPoint(owner, out x, out y, out z, SharedConst.ContactDistance, GetTarget().GetAbsoluteAngle(owner)); - } - else - { - float distance = _offset + 1.0f; - - if (owner.IsPet() && GetTarget().GetTypeId() == TypeId.Player) - distance = 1.0f; - - if (GetTarget().IsWithinDistInMap(owner, distance)) - return; - - GetTarget().GetClosePoint(out x, out y, out z, _offset, _angle); - } - - if (owner.IsHovering()) - owner.UpdateAllowedPositionZ(x, y, ref z); - } - else - { - // the destination has not changed, we just need to refresh the path (usually speed change) - var end = _path.GetEndPosition(); - x = end.X; - y = end.Y; - z = end.Z; - } - - if (_path == null) - _path = new PathGenerator(owner); - - // allow pets to use shortcut if no path found when following their master - bool forceDest = owner.IsTypeId(TypeId.Unit) && owner.IsPet() && owner.HasUnitState(UnitState.Follow); - - bool result = _path.CalculatePath(x, y, z, forceDest); - if (!result && Convert.ToBoolean(_path.GetPathType() & PathType.NoPath)) - { - // Can't reach target - _recalculateTravel = true; - if (owner.IsTypeId(TypeId.Unit)) - owner.ToCreature().SetCannotReachTarget(true); - return; - } - - _targetReached = false; - _recalculateTravel = false; - _speedChanged = false; - - AddUnitStateMove(owner); - - if (owner.IsTypeId(TypeId.Unit)) - owner.ToCreature().SetCannotReachTarget(false); - - MoveSplineInit init = new(owner); - init.MovebyPath(_path.GetPath()); - init.SetWalk(EnableWalking()); - // Using the same condition for facing target as the one that is used for SetInFront on movement end - // - applies to ChaseMovementGenerator mostly - if (_angle == 0.0f) - init.SetFacing(GetTarget()); - - init.Launch(); - } - - bool IsReachable() - { - return _path != null ? _path.GetPathType().HasAnyFlag(PathType.Normal) : true; - } - - public override void UnitSpeedChanged() - { - _speedChanged = true; - } - - public abstract void ClearUnitStateMove(T owner); - public abstract void AddUnitStateMove(T owner); - public virtual bool HasLostTarget(T owner) { return false; } - public abstract void ReachTarget(T owner); - public virtual bool EnableWalking() { return false; } - public abstract void MovementInform(T owner); - public virtual float GetMaxDistanceBeforeRepositioning(T owner) { return 0.0f; } - - public void StopFollowing() { } - - public bool IsTargetValid() { return _target.IsValid(); } - public Unit GetTarget() { return _target.GetTarget(); } - - FollowerReference _target; - PathGenerator _path; - TimeTracker _timer; - float _offset; - float _angle; - bool _recalculateTravel; - bool _speedChanged; - bool _targetReached; - bool _interrupt; - } - - public class ChaseMovementGenerator : TargetedMovementGenerator where T : Unit - { - public ChaseMovementGenerator(Unit target) - : base(target) - { - } - - public ChaseMovementGenerator(Unit target, float offset, float angle) - : base(target, offset, angle) - { - } - - public override void DoInitialize(T owner) - { - if (owner.IsTypeId(TypeId.Unit)) - owner.SetWalk(false); - - owner.AddUnitState(UnitState.Chase); - SetTargetLocation(owner, true); - } - - public override void DoReset(T owner) - { - DoInitialize(owner); - } - - public override void DoFinalize(T owner) - { - owner.ClearUnitState(UnitState.Chase | UnitState.ChaseMove); - } - - public override void ClearUnitStateMove(T owner) - { - owner.ClearUnitState(UnitState.ChaseMove); - } - - public override void AddUnitStateMove(T owner) - { - owner.AddUnitState(UnitState.ChaseMove); - } - - public override bool HasLostTarget(T owner) - { - return owner.GetVictim() != GetTarget(); - } - - public override void ReachTarget(T owner) - { - ClearUnitStateMove(owner); - - if (owner.IsWithinMeleeRange(GetTarget())) - owner.Attack(GetTarget(), true); - - if (owner.IsTypeId(TypeId.Unit)) - owner.ToCreature().SetCannotReachTarget(false); - } - - public override float GetMaxDistanceBeforeRepositioning(T owner) - { - // the notion of melee range and melee attack is clearly separated from the notion of the movement. - // As a seperation of concern refactoring, this value should be passed into parameter by the caller (during creation of ChaseMoveGen) instead of defining it - // here (or a callback should be used since this value depends on dynamic fields (combat reach). - return owner.GetMeleeRange(base.GetTarget()); - } - - public override void MovementInform(T owner) - { - if (owner.IsTypeId(TypeId.Unit)) - { - // Pass back the GUIDLow of the target. If it is pet's owner then PetAI will handle - if (owner.ToCreature().GetAI() != null) - owner.ToCreature().GetAI().MovementInform(MovementGeneratorType.Chase, (uint)GetTarget().GetGUID().GetCounter()); - } - } - - public override MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Chase; } - } - - public class FollowMovementGenerator : TargetedMovementGenerator where T : Unit - { - public FollowMovementGenerator(Unit target) : base(target) { } - - public FollowMovementGenerator(Unit target, float offset, float angle) : base(target, offset, angle) { } - - public override void DoInitialize(T owner) - { - owner.AddUnitState(UnitState.Follow); - UpdateSpeed(owner); - SetTargetLocation(owner, true); - } - - public override void DoReset(T owner) - { - DoInitialize(owner); - } - - public override void DoFinalize(T owner) - { - owner.ClearUnitState(UnitState.Follow | UnitState.FollowMove); - UpdateSpeed(owner); - } - - public override void ClearUnitStateMove(T owner) - { - owner.ClearUnitState(UnitState.FollowMove); - } - - public override void AddUnitStateMove(T owner) - { - owner.AddUnitState(UnitState.FollowMove); - } - - public override void ReachTarget(T owner) - { - ClearUnitStateMove(owner); - } - - public override bool EnableWalking() - { - if (typeof(T) == typeof(Player)) - return false; - else - return IsTargetValid() && GetTarget().IsWalking(); - } - - public override float GetMaxDistanceBeforeRepositioning(T owner) - { - return owner.GetCombatReach() + base.GetTarget().GetCombatReach() + MOVE_FOLLOW_REPOSITIONING_DISTANCE; - } - - public override void MovementInform(T owner) - { - if (owner.IsTypeId(TypeId.Player)) - return; - - // Pass back the GUIDLow of the target. If it is pet's owner then PetAI will handle - if (owner.ToCreature().GetAI() != null) - owner.ToCreature().GetAI().MovementInform(MovementGeneratorType.Follow, (uint)GetTarget().GetGUID().GetCounter()); - } - - public override bool HasLostTarget(T u) { return false; } - - public override MovementGeneratorType GetMovementGeneratorType() - { - return MovementGeneratorType.Follow; - } - - void UpdateSpeed(T owner) - { - if (owner.IsTypeId(TypeId.Player)) - return; - - if (!owner.IsPet() || !owner.IsInWorld || !IsTargetValid() && GetTarget().GetGUID() != owner.GetOwnerGUID()) - return; - - owner.UpdateSpeed(UnitMoveType.Run); - owner.UpdateSpeed(UnitMoveType.Walk); - owner.UpdateSpeed(UnitMoveType.Swim); - } - } -} diff --git a/Source/Game/Movement/Generators/WaypointMovement.cs b/Source/Game/Movement/Generators/WaypointMovement.cs index 57cf473c6..add5be7d4 100644 --- a/Source/Game/Movement/Generators/WaypointMovement.cs +++ b/Source/Game/Movement/Generators/WaypointMovement.cs @@ -284,7 +284,7 @@ namespace Game.Movement creature.GetAI().MovementInform(MovementGeneratorType.Waypoint, (uint)_currentNode); } - public override bool GetResetPosition(Unit u, out float x, out float y, out float z) + public bool GetResetPosition(Unit u, out float x, out float y, out float z) { x = y = z = 0; // prevent a crash at empty waypoint path. @@ -301,13 +301,13 @@ namespace Game.Movement return true; } - public override void Pause(uint timer = 0) + public void Pause(uint timer = 0) { _stalled = timer == 0; _nextMoveTime.Reset(timer != 0 ? (int)timer : 1); } - public override void Resume(uint overrideTimer = 0) + public void Resume(uint overrideTimer = 0) { _stalled = false; if (overrideTimer != 0) @@ -321,7 +321,7 @@ namespace Game.Movement public override MovementGeneratorType GetMovementGeneratorType() { return MovementGeneratorType.Waypoint; } - public override void UnitSpeedChanged() { _recalculateSpeed = true; } + public void UnitSpeedChanged() { _recalculateSpeed = true; } TimeTrackerSmall _nextMoveTime; bool _recalculateSpeed; diff --git a/Source/Game/Movement/MotionMaster.cs b/Source/Game/Movement/MotionMaster.cs index 4523eb5e4..caa60eda1 100644 --- a/Source/Game/Movement/MotionMaster.cs +++ b/Source/Game/Movement/MotionMaster.cs @@ -28,7 +28,7 @@ namespace Game.Movement public class MotionMaster { public const double gravity = 19.29110527038574; - public const float SPEED_CHARGE = 42.0f; + public const float SPEED_CHARGE = 42.0f; IdleMovementGenerator staticIdleMovement = new(); public MotionMaster(Unit me) @@ -139,7 +139,7 @@ namespace Game.Movement { if (Empty() || slot >= MovementSlot.Max || _slot[(int)slot] == null) return MovementGeneratorType.Max; - + return _slot[(int)slot].GetMovementGeneratorType(); } @@ -202,7 +202,7 @@ namespace Game.Movement { Unit target = _owner.ToCreature().GetCharmerOrOwner(); if (target) - StartMovement(new FollowMovementGenerator(target, SharedConst.PetFollowDist, SharedConst.PetFollowAngle), MovementSlot.Active); + StartMovement(new FollowMovementGenerator(target, SharedConst.PetFollowDist, new ChaseAngle(SharedConst.PetFollowAngle)), MovementSlot.Active); } } @@ -212,27 +212,29 @@ namespace Game.Movement StartMovement(new RandomMovementGenerator(spawndist), MovementSlot.Idle); } - public void MoveFollow(Unit target, float dist = 0.0f, float angle = 0.0f, MovementSlot slot = MovementSlot.Idle) + public void MoveFollow(Unit target, float dist, float angle = 0.0f, MovementSlot slot = MovementSlot.Idle) { MoveFollow(target, dist, new ChaseAngle(angle), slot); } + + public void MoveFollow(Unit target, float dist, ChaseAngle angle, MovementSlot slot = MovementSlot.Idle) { // ignore movement request if target not exist if (!target || target == _owner) return; - if (_owner.IsTypeId(TypeId.Player)) - StartMovement(new FollowMovementGenerator(target, dist, angle), slot); - else - StartMovement(new FollowMovementGenerator(target, dist, angle), slot); + Log.outDebug(LogFilter.Misc, $"{(_owner.IsPlayer() ? "Player" : "Creature")} (Entry: {_owner.GetEntry()}, {_owner.GetGUID()}) follows {(target.IsPlayer() ? "player" : "creature")} (Entry {target.GetEntry()}, {target.GetGUID()})."); + + StartMovement(new FollowMovementGenerator(target, dist, angle), slot); } - public void MoveChase(Unit target, float dist = 0.0f, float angle = 0.0f) + public void MoveChase(Unit target, float dist, float angle = 0.0f) { MoveChase(target, new ChaseRange(dist), new ChaseAngle(angle)); } + + public void MoveChase(Unit target, ChaseRange? dist = null, ChaseAngle? angle = null) { if (!target || target == _owner) return; - if (_owner.IsTypeId(TypeId.Player)) - StartMovement(new ChaseMovementGenerator(target, dist, angle), MovementSlot.Active); - else - StartMovement(new ChaseMovementGenerator(target, dist, angle), MovementSlot.Active); + Log.outDebug(LogFilter.Misc, $"{(_owner.IsPlayer() ? "Player" : "Creature")} (Entry: {_owner.GetEntry()}, {_owner.GetGUID()}) chase to {(target.IsPlayer() ? "player" : "creature")} (Entry: {target.GetEntry()}, {target.GetGUID()})"); + + StartMovement(new ChaseMovementGenerator(target, dist, angle), MovementSlot.Active); } public void MoveConfused() @@ -840,4 +842,59 @@ namespace Game.Movement Update = 1, // Clear or Expire called from update Reset = 2 // Flag if need top().Reset() } + + public struct ChaseRange + { + // this contains info that informs how we should path! + public float MinRange; // we have to move if we are within this range... (min. attack range) + public float MinTolerance; // ...and if we are, we will move this far away + public float MaxRange; // we have to move if we are outside this range... (max. attack range) + public float MaxTolerance; // ...and if we are, we will move into this range + + public ChaseRange(float range) + { + MinRange = range > SharedConst.ContactDistance ? 0 : range - SharedConst.ContactDistance; + MinTolerance = range; + MaxRange = range + SharedConst.ContactDistance; + MaxTolerance = range; + } + + public ChaseRange(float min, float max) + { + MinRange = min; + MinTolerance = Math.Min(min + SharedConst.ContactDistance, (min + max) / 2); + MaxRange = max; + MaxTolerance = Math.Max(max - SharedConst.ContactDistance, MinTolerance); + } + + public ChaseRange(float min, float tMin, float tMax, float max) + { + MinRange = min; + MinTolerance = tMin; + MaxRange = max; + MaxTolerance = tMax; + } + } + + public struct ChaseAngle + { + public float RelativeAngle; // we want to be at this angle relative to the target (0 = front, M_PI = back) + public float Tolerance; // but we'll tolerate anything within +- this much + + public ChaseAngle(float angle, float tol = MathFunctions.PiOver4) + { + RelativeAngle = Position.NormalizeOrientation(angle); + Tolerance = tol; + } + + public float UpperBound() { return Position.NormalizeOrientation(RelativeAngle + Tolerance); } + + public float LowerBound() { return Position.NormalizeOrientation(RelativeAngle - Tolerance); } + + public bool IsAngleOkay(float relAngle) + { + float diff = Math.Abs(relAngle - RelativeAngle); + return (Math.Min(diff, (2 * MathF.PI) - diff) <= Tolerance); + } + } } diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index 7edc7fb0a..afd8bb37d 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -4887,7 +4887,7 @@ namespace Game.Spells else if (m_preGeneratedPath.IsInvalidDestinationZ(target)) // Check position z, if in a straight line return SpellCastResult.NoPath; - m_preGeneratedPath.ReducePathLenghtByDist(objSize); //move back + m_preGeneratedPath.ShortenPathUntilDist(target, objSize); //move back } break; }