// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.Offline; using MUnique.OpenMU.GameLogic.MuHelper; using MUnique.OpenMU.Pathfinding; /// /// Handles movement logic including walking, regrouping, and return-to-origin. /// public sealed class MovementHandler { private const byte RegroupDistanceThreshold = 1; private readonly OfflinePlayer _player; private readonly IMuHelperSettings? _config; private DateTime? _outOfRangeSince; /// /// Initializes a new instance of the class. /// /// The offline player. /// The MU Helper configuration. public MovementHandler(OfflinePlayer player, IMuHelperSettings? config) { this._player = player; this._config = config; } /// /// Gets the position to hunt around. Dynamic so bots can roam between hunting grounds. /// private Point OriginPosition => this._player.HuntingOrigin; /// /// Gets the hunting range in tiles. /// private byte HuntingRange => CombatHandler.CalculateHuntingRange(this._config); /// /// Returns the character to the original position if configured and distance/time thresholds are met. /// /// True, if the loop can continue; False, if a regrouping walk was initiated. public async ValueTask RegroupAsync() { if (this._config is null || !this._config.ReturnToOriginalPosition) { return true; } if (this.ShouldRegroup(out var distance)) { await this.WalkToAsync(this.OriginPosition).ConfigureAwait(false); this._outOfRangeSince = null; return false; } if (distance <= RegroupDistanceThreshold) { this._outOfRangeSince = null; } return true; } /// /// Moves the player closer to a target within the specified range. /// /// The target to move closer to. /// The range to stop within. /// True, if a walk towards the target was started; false, if no path exists or walking is not possible. public async ValueTask MoveCloserToTargetAsync(IAttackable target, byte range) { if (this._player.CurrentMap is { } map && target.IsInRange(this.OriginPosition, this.HuntingRange)) { var walkTarget = GetApproachPoint(map, this._player.Position, target.Position, range); return await this.WalkToAsync(walkTarget).ConfigureAwait(false); } return false; } /// /// Picks the point to walk to when closing in on a target: straight along the line towards it, /// stopping at attack range. The previous behavior re-randomized a point around the target every /// tick, which made the character zig-zag visibly towards its prey and re-path constantly. /// Falls back to a random point near the target when the straight-line point is not walkable. /// private static Point GetApproachPoint(GameMap map, Point from, Point to, byte stopRange) { var dx = to.X - from.X; var dy = to.Y - from.Y; var distance = Math.Max(Math.Abs(dx), Math.Abs(dy)); if (distance <= stopRange) { return from; } var factor = (double)(distance - stopRange) / distance; var x = (byte)Math.Clamp(from.X + (int)Math.Round(dx * factor), 0, 255); var y = (byte)Math.Clamp(from.Y + (int)Math.Round(dy * factor), 0, 255); if (map.Terrain.WalkMap[x, y] && !map.Terrain.SafezoneMap[x, y]) { return new Point(x, y); } return map.Terrain.GetRandomCoordinate(to, stopRange); } /// /// Walks the player to the specified target position. /// /// The target position to walk to. /// True if the walk was successful; otherwise, false. private async ValueTask WalkToAsync(Point target) { if (this._player.IsWalking || this._player.CurrentMap is not { } map) { return false; } var pathFinder = await this._player.GameContext.PathFinderPool.GetAsync().ConfigureAwait(false); try { pathFinder.ResetPathFinder(); var path = pathFinder.FindPath(this._player.Position, target, map.Terrain.AIgrid, false); if (path is null || path.Count == 0) { return false; } var stepsCount = Math.Min(path.Count, 16); var steps = new WalkingStep[stepsCount]; for (int i = 0; i < stepsCount; i++) { var node = path[i]; var prevPos = i == 0 ? this._player.Position : steps[i - 1].To; steps[i] = new WalkingStep(prevPos, node.Point, prevPos.GetDirectionTo(node.Point)); } await this._player.WalkToAsync(target, steps).ConfigureAwait(false); return true; } finally { this._player.GameContext.PathFinderPool.Return(pathFinder); } } private bool ShouldRegroup(out double distance) { distance = this._player.GetDistanceTo(this.OriginPosition); if (distance <= RegroupDistanceThreshold) { return false; } this._outOfRangeSince ??= DateTime.UtcNow; var secondsAway = (DateTime.UtcNow - this._outOfRangeSince.Value).TotalSeconds; return secondsAway >= this._config!.MaxSecondsAway || distance > this.HuntingRange; } }