diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index 2fd788229..fb3f627a5 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -130,7 +130,7 @@ namespace Game.Entities return m_formation.IsLeader(this); } - public void SignalFormationMovement(Position destination, uint id = 0, WaypointMoveType moveType = 0, bool orientation = false) + public void SignalFormationMovement() { if (m_formation == null) return; @@ -138,7 +138,7 @@ namespace Game.Entities if (!m_formation.IsLeader(this)) return; - m_formation.LeaderMoveTo(destination, id, moveType, orientation); + m_formation.LeaderStartedMoving(); } public bool IsFormationLeaderMoveAllowed() diff --git a/Source/Game/Entities/Creature/CreatureGroups.cs b/Source/Game/Entities/Creature/CreatureGroups.cs index 41914f702..8429cdafa 100644 --- a/Source/Game/Entities/Creature/CreatureGroups.cs +++ b/Source/Game/Entities/Creature/CreatureGroups.cs @@ -274,51 +274,23 @@ namespace Game.Entities _formed = !dismiss; } - public void LeaderMoveTo(Position destination, uint id = 0, WaypointMoveType moveType = 0, bool orientation = false) + public void LeaderStartedMoving() { - //! To do: This should probably get its own movement generator or use WaypointMovementGenerator. - //! If the leader's path is known, member's path can be plotted as well using formation offsets. if (_leader == null) return; - Position pos = new(destination); - float pathangle = (float)Math.Atan2(_leader.GetPositionY() - pos.GetPositionY(), _leader.GetPositionX() - pos.GetPositionX()); - foreach (var pair in _members) { Creature member = pair.Key; if (member == _leader || !member.IsAlive() || member.IsEngaged() || !pair.Value.GroupAI.HasAnyFlag((uint)GroupAIFlags.IdleInFormation)) continue; - if (pair.Value.LeaderWaypointIDs[0] != 0) - { - for (var i = 0; i < 2; ++i) - { - if (_leader.GetCurrentWaypointInfo().nodeId == pair.Value.LeaderWaypointIDs[i]) - { - pair.Value.FollowAngle = MathF.PI * 2f - pair.Value.FollowAngle; - break; - } - } - } - - float angle = pair.Value.FollowAngle; + float angle = pair.Value.FollowAngle + MathF.PI; // for some reason, someone thought it was a great idea to invert relativ angles... float dist = pair.Value.FollowDist; - float dx = pos.GetPositionX() + MathF.Cos(angle + pathangle) * dist; - float dy = pos.GetPositionY() + MathF.Sin(angle + pathangle) * dist; - float dz = pos.GetPositionZ(); - - GridDefines.NormalizeMapCoord(ref dx); - GridDefines.NormalizeMapCoord(ref dy); - - if (!member.IsFlying()) - member.UpdateGroundPositionZ(dx, dy, ref dz); - - member.SetHomePosition(dx, dy, dz, pathangle); - - Position point = new(dx, dy, dz, destination.GetOrientation()); - member.GetMotionMaster().MoveFormation(id, point, moveType, !member.IsWithinDist(_leader, dist + 5.0f), orientation); + var moveGen = member.GetMotionMaster().GetMovementGenerator(movement => { return movement.GetMovementGeneratorType() == MovementGeneratorType.Formation; }, MovementSlot.Default); + if (moveGen == null) + member.GetMotionMaster().MoveFormation(_leader, dist, angle, pair.Value.LeaderWaypointIDs[0], pair.Value.LeaderWaypointIDs[1]); } } diff --git a/Source/Game/Movement/Generators/FormationMovement.cs b/Source/Game/Movement/Generators/FormationMovement.cs index 397ad71a6..fa6e31118 100644 --- a/Source/Game/Movement/Generators/FormationMovement.cs +++ b/Source/Game/Movement/Generators/FormationMovement.cs @@ -17,18 +17,32 @@ using Framework.Constants; using Game.Entities; +using System; namespace Game.Movement { public class FormationMovementGenerator : MovementGeneratorMedium { - public FormationMovementGenerator(uint id, Position destination, WaypointMoveType moveType, bool run, bool orientation) + AbstractFollower _abstractFollower; + + static int FORMATION_MOVEMENT_INTERVAL = 1200; // sniffed (3 batch update cycles) + float _range; + float _angle; + uint _point1; + uint _point2; + uint _lastLeaderSplineID; + bool _hasPredictedDestination; + + Position _lastLeaderPosition; + TimeTrackerSmall _nextMoveTimer = new(); + + public FormationMovementGenerator(Unit leader, float range, float angle, uint point1, uint point2) { - _movementId = id; - _destination = destination; - _moveType = moveType; - _run = run; - _orientation = orientation; + _abstractFollower = new(leader); + _range = range; + _angle = angle; + _point1 = point1; + _point2 = point2; Mode = MovementGeneratorMode.Default; Priority = MovementGeneratorPriority.Normal; @@ -48,33 +62,7 @@ namespace Game.Movement return; } - owner.AddUnitState(UnitState.RoamingMove); - - MoveSplineInit init = new(owner); - init.MoveTo(_destination.GetPositionX(), _destination.GetPositionY(), _destination.GetPositionZ()); - if (_orientation) - init.SetFacing(_destination.GetOrientation()); - - switch (_moveType) - { - case WaypointMoveType.Land: - init.SetAnimation(AnimType.ToGround); - break; - case WaypointMoveType.Takeoff: - init.SetAnimation(AnimType.ToFly); - break; - case WaypointMoveType.Run: - init.SetWalk(false); - break; - case WaypointMoveType.Walk: - init.SetWalk(true); - break; - } - - if (_run) - init.SetWalk(false); - - init.Launch(); + _nextMoveTimer.Reset(0); } public override void DoReset(Creature owner) @@ -86,57 +74,154 @@ namespace Game.Movement public override bool DoUpdate(Creature owner, uint diff) { - if (!owner) + Unit target = _abstractFollower.GetTarget(); + + if (owner == null || target == null) return false; + // Owner cannot move. Reset all fields and wait for next action if (owner.HasUnitState(UnitState.NotMove) || owner.IsMovementPreventedByCasting()) { AddFlag(MovementGeneratorFlags.Interrupted); + owner.ClearUnitState(UnitState.RoamingMove); owner.StopMoving(); return true; } - if ((HasFlag(MovementGeneratorFlags.Interrupted) && owner.MoveSpline.Finalized()) || (HasFlag(MovementGeneratorFlags.SpeedUpdatePending) && !owner.MoveSpline.Finalized())) + // Leader has stopped moving, so do we as well + if (target.MoveSpline.Finalized() && target.MoveSpline.GetId() == _lastLeaderSplineID && _hasPredictedDestination) { - RemoveFlag(MovementGeneratorFlags.Interrupted | MovementGeneratorFlags.SpeedUpdatePending); + owner.ClearUnitState(UnitState.RoamingMove); + owner.StopMoving(); + _nextMoveTimer.Reset(0); + _hasPredictedDestination = false; + return true; + } - owner.AddUnitState(UnitState.RoamingMove); + // Update home position + owner.SetHomePosition(owner.GetPosition()); + if (HasFlag(MovementGeneratorFlags.Interrupted)) + RemoveFlag(MovementGeneratorFlags.Interrupted); - MoveSplineInit init = new(owner); - init.MoveTo(_destination.GetPositionX(), _destination.GetPositionY(), _destination.GetPositionZ()); - if (_orientation) - init.SetFacing(_destination.GetOrientation()); + // Leader has stopped moving, so do we as well + if (owner.HasUnitState(UnitState.RoamingMove) && _hasPredictedDestination && target.MoveSpline.Finalized() && target.MoveSpline.GetId() == _lastLeaderSplineID) + { + owner.StopMoving(); + _nextMoveTimer.Reset(0); + _hasPredictedDestination = false; + return true; + } - switch (_moveType) + // Formation leader has launched a new spline, launch a new one for our member as well + // This action does not reset the regular movement launch cycle interval + if (!target.MoveSpline.Finalized() && target.MoveSpline.GetId() != _lastLeaderSplineID) + { + // Update formation angle + if (_point1 != 0 && target.IsCreature()) { - case WaypointMoveType.Land: - init.SetAnimation(AnimType.ToGround); - break; - case WaypointMoveType.Takeoff: - init.SetAnimation(AnimType.ToFly); - break; - case WaypointMoveType.Run: - init.SetWalk(false); - break; - case WaypointMoveType.Walk: - init.SetWalk(true); - break; + CreatureGroup formation = target.ToCreature().GetFormation(); + if (formation != null) + { + Creature leader = formation.GetLeader(); + if (leader != null) + { + uint currentWaypoint = leader.GetCurrentWaypointInfo().nodeId; + if (currentWaypoint == _point1 || currentWaypoint == _point2) + _angle = MathF.PI * 2 - _angle; + } + } } - if (_run) - init.SetWalk(false); - init.Launch(); + LaunchMovement(owner, target); + _lastLeaderSplineID = target.MoveSpline.GetId(); + return true; } - if (owner.MoveSpline.Finalized()) + _nextMoveTimer.Update((int)diff); + if (_nextMoveTimer.Passed()) { - RemoveFlag(MovementGeneratorFlags.Transitory); - AddFlag(MovementGeneratorFlags.InformEnabled); - return false; + _nextMoveTimer.Reset(FORMATION_MOVEMENT_INTERVAL); + + // Our leader has a different position than on our last check, launch movement. + if (_lastLeaderPosition != target.GetPosition()) + { + LaunchMovement(owner, target); + return true; + } } + + // We have reached our destination before launching a new movement. Alling facing with leader + if (owner.HasUnitState(UnitState.RoamingMove) && owner.MoveSpline.Finalized()) + { + owner.ClearUnitState(UnitState.RoamingMove); + owner.SetFacingTo(target.GetOrientation()); + MovementInform(owner); + } + return true; } + void LaunchMovement(Creature owner, Unit target) + { + float relativeAngle = 0.0f; + + // Determine our relative angle to our current spline destination point + if (!target.MoveSpline.Finalized()) + relativeAngle = target.GetRelativeAngle(new Position(target.MoveSpline.CurrentDestination())); + + // Destination calculation + /* + According to sniff data, formation members have a periodic move interal of 1,2s. + Each of these splines has a exact duration of 1650ms +- 1ms when no pathfinding is involved. + To get a representative result like that we have to predict our formation leader's path + and apply our formation shape based on that destination. + */ + Position dest = target.GetPosition(); + float velocity = 0.0f; + + // Formation leader is moving. Predict our destination + if (!target.MoveSpline.Finalized()) + { + // Pick up leader's spline velocity + velocity = target.MoveSpline.velocity; + + // Calculate travel distance to get a 1650ms result + float travelDist = velocity * 1.65f; + + // Move destination ahead... + target.MovePositionToFirstCollision(dest, travelDist, relativeAngle); + // ... and apply formation shape + target.MovePositionToFirstCollision(dest, _range, _angle + relativeAngle); + + float distance = owner.GetExactDist(dest); + + // Calculate catchup speed mod (Limit to a maximum of 50% of our original velocity + float velocityMod = Math.Min(distance / travelDist, 1.5f); + + // Now we will always stay synch with our leader + velocity *= velocityMod; + _hasPredictedDestination = true; + } + else + { + // Formation leader is not moving. Just apply the base formation shape on his position. + target.MovePositionToFirstCollision(dest, _range, _angle + relativeAngle); + _hasPredictedDestination = false; + } + + // Leader is not moving, so just pick up his default walk speed + if (velocity == 0.0f) + velocity = target.GetSpeed(UnitMoveType.Walk); + + MoveSplineInit init = new(owner); + init.MoveTo(dest); + init.SetVelocity(velocity); + init.Launch(); + + _lastLeaderPosition = target.GetPosition(); + owner.AddUnitState(UnitState.RoamingMove); + } + public override void DoDeactivate(Creature owner) { AddFlag(MovementGeneratorFlags.Deactivated); @@ -166,13 +251,7 @@ namespace Game.Movement void MovementInform(Creature owner) { if (owner.GetAI() != null) - owner.GetAI().MovementInform(MovementGeneratorType.Formation, _movementId); + owner.GetAI().MovementInform(MovementGeneratorType.Formation, 0); } - - uint _movementId; - Position _destination; - WaypointMoveType _moveType; - bool _run; - bool _orientation; } } diff --git a/Source/Game/Movement/Generators/PointMovement.cs b/Source/Game/Movement/Generators/PointMovement.cs index 6024f4e45..9b0ef38f6 100644 --- a/Source/Game/Movement/Generators/PointMovement.cs +++ b/Source/Game/Movement/Generators/PointMovement.cs @@ -77,7 +77,7 @@ namespace Game.Movement // Call for creature group update Creature creature = owner.ToCreature(); if (creature != null) - creature.SignalFormationMovement(_destination, _movementId); + creature.SignalFormationMovement(); } public override void DoReset(T owner) @@ -124,7 +124,7 @@ namespace Game.Movement // Call for creature group update Creature creature = owner.ToCreature(); if (creature != null) - creature.SignalFormationMovement(_destination, _movementId); + creature.SignalFormationMovement(); } if (owner.MoveSpline.Finalized()) diff --git a/Source/Game/Movement/Generators/RandomMovementGenerator.cs b/Source/Game/Movement/Generators/RandomMovementGenerator.cs index a4f5d0628..c500bf24d 100644 --- a/Source/Game/Movement/Generators/RandomMovementGenerator.cs +++ b/Source/Game/Movement/Generators/RandomMovementGenerator.cs @@ -201,7 +201,7 @@ namespace Game.Movement } // Call for creature group update - owner.SignalFormationMovement(position); + owner.SignalFormationMovement(); } public override void UnitSpeedChanged() { AddFlag(MovementGeneratorFlags.SpeedUpdatePending); } diff --git a/Source/Game/Movement/Generators/WaypointMovement.cs b/Source/Game/Movement/Generators/WaypointMovement.cs index a491db71c..eb3813bd1 100644 --- a/Source/Game/Movement/Generators/WaypointMovement.cs +++ b/Source/Game/Movement/Generators/WaypointMovement.cs @@ -341,7 +341,6 @@ namespace Game.Movement Cypher.Assert(_currentNode < _path.nodes.Count, $"WaypointMovementGenerator.StartMove: tried to reference a node id ({_currentNode}) which is not included in path ({_path.id})"); WaypointNode waypoint = _path.nodes[_currentNode]; - Position formationDest = new(waypoint.x, waypoint.y, waypoint.z, (waypoint.orientation != 0 && waypoint.delay != 0) ? waypoint.orientation : 0.0f); RemoveFlag(MovementGeneratorFlags.Transitory | MovementGeneratorFlags.InformEnabled | MovementGeneratorFlags.TimedPaused); @@ -351,16 +350,7 @@ namespace Game.Movement //! If creature is on transport, we assume waypoints set in DB are already transport offsets if (transportPath) - { init.DisableTransportPathTransformations(); - ITransport trans = owner.GetDirectTransport(); - if (trans != null) - { - float orientation = formationDest.GetOrientation(); - trans.CalculatePassengerPosition(ref formationDest.posX, ref formationDest.posY, ref formationDest.posZ, ref orientation); - formationDest.SetOrientation(orientation); - } - } //! Do not use formationDest here, MoveTo requires transport offsets due to DisableTransportPathTransformations() call //! but formationDest contains global coordinates @@ -389,7 +379,7 @@ namespace Game.Movement init.Launch(); // inform formation - owner.SignalFormationMovement(formationDest, waypoint.id, waypoint.moveType, (waypoint.orientation != 0 && waypoint.delay != 0)); + owner.SignalFormationMovement(); } bool ComputeNextNode() diff --git a/Source/Game/Movement/MotionMaster.cs b/Source/Game/Movement/MotionMaster.cs index e0d6f8c78..eadc8b22f 100644 --- a/Source/Game/Movement/MotionMaster.cs +++ b/Source/Game/Movement/MotionMaster.cs @@ -994,10 +994,10 @@ namespace Game.Movement Add(new RotateMovementGenerator(id, time, direction)); } - public void MoveFormation(uint id, Position destination, WaypointMoveType moveType, bool forceRun = false, bool forceOrientation = false) + public void MoveFormation(Unit leader, float range, float angle, uint point1, uint point2) { - if (_owner.GetTypeId() == TypeId.Unit) - Add(new FormationMovementGenerator(id, destination, moveType, forceRun, forceOrientation)); + if (_owner.GetTypeId() == TypeId.Unit && leader != null) + Add(new FormationMovementGenerator(leader, range, angle, point1, point2), MovementSlot.Default); } public void LaunchMoveSpline(MoveSplineInit init, uint id = 0, MovementGeneratorPriority priority = MovementGeneratorPriority.Normal, MovementGeneratorType type = MovementGeneratorType.Effect) diff --git a/Source/Game/Movement/MoveSpline.cs b/Source/Game/Movement/MoveSpline.cs index d24fe7704..c4b5e7d60 100644 --- a/Source/Game/Movement/MoveSpline.cs +++ b/Source/Game/Movement/MoveSpline.cs @@ -54,6 +54,8 @@ namespace Game.Movement anim_tier = args.animTier; splineIsFacingOnly = args.path.Count == 2 && args.facing.type != MonsterMoveType.Normal && ((args.path[1] - args.path[0]).Length() < 0.1f); + velocity = args.velocity; + // Check if its a stop spline if (args.flags.HasFlag(SplineFlag.Done)) { @@ -141,7 +143,7 @@ namespace Game.Movement time_passed = Duration(); } public Vector4 ComputePosition(int time_point, int point_index) - { + { float u = 1.0f; int seg_time = spline.Length(point_index, point_index + 1); if (seg_time > 0) @@ -255,7 +257,7 @@ namespace Game.Movement { return time_passed > 0; } - + public void Interrupt() { splineflags.SetUnsetFlag(SplineFlag.Done); } public void UpdateState(int difftime) { @@ -342,7 +344,8 @@ namespace Game.Movement public bool IsCyclic() { return splineflags.HasFlag(SplineFlag.Cyclic); } public bool IsFalling() { return splineflags.HasFlag(SplineFlag.Falling); } public bool Initialized() { return !spline.Empty(); } - public Vector3 FinalDestination() { return Initialized() ? spline.GetPoint(spline.Last()) : new Vector3(); } + public Vector3 FinalDestination() { return Initialized() ? spline.GetPoint(spline.Last()) : Vector3.Zero; } + public Vector3 CurrentDestination() { return Initialized() ? spline.GetPoint(point_Idx + 1) : Vector3.Zero; } #region Fields public MoveSplineInitArgs InitArgs; @@ -358,6 +361,7 @@ namespace Game.Movement public int effect_start_time; public int point_Idx; public int point_Idx_offset; + public float velocity; public Optional spell_effect_extra; public Optional anim_tier; #endregion