diff --git a/Core/ClassConfig/ClassConfiguration.cs b/Core/ClassConfig/ClassConfiguration.cs index c6eaab7ce..77641ce3a 100644 --- a/Core/ClassConfig/ClassConfiguration.cs +++ b/Core/ClassConfig/ClassConfiguration.cs @@ -69,6 +69,7 @@ public sealed partial class ClassConfiguration public Dictionary IntVariables { get; } = new(); public KeyActions Pull { get; } = new(); + public KeyActions Flee { get; } = new(); public KeyActions Combat { get; } = new(); public KeyActions Adhoc { get; } = new(); public KeyActions Parallel { get; } = new(); diff --git a/Core/GOAP/GoapAgentState.cs b/Core/GOAP/GoapAgentState.cs index 386f5d59e..5d304769c 100644 --- a/Core/GOAP/GoapAgentState.cs +++ b/Core/GOAP/GoapAgentState.cs @@ -1,5 +1,4 @@ - -namespace Core.GOAP; +namespace Core.GOAP; public sealed class GoapAgentState { diff --git a/Core/Goals/CombatGoal.cs b/Core/Goals/CombatGoal.cs index b4b4e1ae0..073f0f26a 100644 --- a/Core/Goals/CombatGoal.cs +++ b/Core/Goals/CombatGoal.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Numerics; namespace Core.Goals; @@ -162,6 +163,10 @@ public override void Update() stopMoving.Stop(); FindNewTarget(); } + else + { + input.PressClearTarget(); + } } } diff --git a/Core/Goals/FleeGoal.cs b/Core/Goals/FleeGoal.cs new file mode 100644 index 000000000..19930e309 --- /dev/null +++ b/Core/Goals/FleeGoal.cs @@ -0,0 +1,120 @@ +using Core.GOAP; + +using Microsoft.Extensions.Logging; + +using SharedLib.Extensions; + +using System; +using System.Buffers; +using System.Numerics; + +namespace Core.Goals; + +public sealed class FleeGoal : GoapGoal, IRouteProvider +{ + public override float Cost => 3.1f; + + private readonly ILogger logger; + private readonly ConfigurableInput input; + private readonly ClassConfiguration classConfig; + private readonly Wait wait; + private readonly PlayerReader playerReader; + private readonly Navigation navigation; + private readonly AddonBits bits; + + private readonly SafeSpotCollector safeSpotCollector; + + private Vector3[] MapPoints = []; + + public FleeGoal(ILogger logger, ConfigurableInput input, + Wait wait, PlayerReader playerReader, AddonBits bits, + ClassConfiguration classConfiguration, Navigation playerNavigation, + ClassConfiguration classConfig, + SafeSpotCollector safeSpotCollector) + : base(nameof(FleeGoal)) + { + this.logger = logger; + this.input = input; + + this.wait = wait; + this.playerReader = playerReader; + this.navigation = playerNavigation; + this.bits = bits; + + this.classConfig = classConfig; + + AddPrecondition(GoapKey.incombat, true); + + Keys = classConfiguration.Flee.Sequence; + + this.safeSpotCollector = safeSpotCollector; + } + + #region IRouteProvider + + public DateTime LastActive => navigation.LastActive; + + public Vector3[] MapRoute() => MapPoints; + + public Vector3[] PathingRoute() + { + return navigation.TotalRoute; + } + + public bool HasNext() + { + return navigation.HasNext(); + } + + public Vector3 NextMapPoint() + { + return navigation.NextMapPoint(); + } + + #endregion + + public override bool CanRun() + { + return + safeSpotCollector.MapLocations.Count > 0 && + Keys.Length > 0 && Keys[0].CanRun(); + } + + public override void OnEnter() + { + int count = safeSpotCollector.MapLocations.Count; + + ArrayPool pooler = ArrayPool.Shared; + Vector3[] array = pooler.Rent(count); + var span = array.AsSpan(); + + safeSpotCollector.MapLocations.CopyTo(array, 0); + + Span simplified = PathSimplify.Simplify(array.AsSpan()[..count], PathSimplify.HALF, true); + MapPoints = simplified.ToArray(); + + navigation.SetWayPoints(simplified); + navigation.ResetStuckParameters(); + + pooler.Return(array); + } + + public override void OnExit() + { + safeSpotCollector.Reduce(playerReader.MapPosNoZ); + + navigation.Stop(); + navigation.StopMovement(); + } + + public override void Update() + { + if (bits.Target()) + { + input.PressClearTarget(); + } + + wait.Update(); + navigation.Update(); + } +} diff --git a/Core/GoalsComponent/Navigation.cs b/Core/GoalsComponent/Navigation.cs index 403093705..9cceaa052 100644 --- a/Core/GoalsComponent/Navigation.cs +++ b/Core/GoalsComponent/Navigation.cs @@ -419,14 +419,10 @@ private float ReachedDistance(float minDistance) private void ReduceByDistance(Vector3 playerW, float minDistance) { - float worldDistance = playerW.WorldDistanceXYTo(routeToNextWaypoint.Peek()); - while (worldDistance < ReachedDistance(minDistance) && routeToNextWaypoint.Count > 0) + while (routeToNextWaypoint.Count > 0 && + playerW.WorldDistanceXYTo(routeToNextWaypoint.Peek()) < ReachedDistance(minDistance)) { routeToNextWaypoint.Pop(); - if (routeToNextWaypoint.Count > 0) - { - worldDistance = playerW.WorldDistanceXYTo(routeToNextWaypoint.Peek()); - } } } diff --git a/Core/GoalsComponent/SafeSpotCollector.cs b/Core/GoalsComponent/SafeSpotCollector.cs new file mode 100644 index 000000000..2f47a346e --- /dev/null +++ b/Core/GoalsComponent/SafeSpotCollector.cs @@ -0,0 +1,69 @@ +using SharedLib.Extensions; + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Threading; + +namespace Core.Goals; + +public sealed class SafeSpotCollector : IDisposable +{ + private readonly PlayerReader playerReader; + private readonly AddonBits bits; + + private readonly Timer timer; + + public Stack MapLocations { get; } = new(); + + public SafeSpotCollector( + PlayerReader playerReader, + AddonBits bits) + { + this.playerReader = playerReader; + this.bits = bits; + + timer = new(Update, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); + } + + public void Dispose() + { + timer.Dispose(); + } + + public void Update(object? obj) + { + if (bits.Combat()) + return; + + if (MapLocations.TryPeek(out Vector3 lastMapPos) && + lastMapPos == playerReader.MapPosNoZ) + return; + + MapLocations.Push(playerReader.MapPosNoZ); + } + + public void Reduce(Vector3 mapPosition) + { + lock (MapLocations) + { + Vector3 closestM = default; + float distanceM = float.MaxValue; + + foreach (Vector3 p in MapLocations) + { + float d = mapPosition.MapDistanceXYTo(p); + if (d < distanceM) + { + closestM = p; + distanceM = d; + } + } + + while (MapLocations.TryPeek(out var p) && p != closestM) + { + MapLocations.Pop(); + } + } + } +} diff --git a/Core/GoalsFactory/GoalFactory.cs b/Core/GoalsFactory/GoalFactory.cs index e1e55be92..a5da1286e 100644 --- a/Core/GoalsFactory/GoalFactory.cs +++ b/Core/GoalsFactory/GoalFactory.cs @@ -57,6 +57,7 @@ public static IServiceProvider Create( services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); var playerReader = sp.GetRequiredService(); @@ -136,6 +137,7 @@ public static IServiceProvider Create( services.AddScoped(); services.AddScoped(); services.AddScoped(); + AddFleeGoal(services, classConfig); services.AddScoped(); if (classConfig.WrongZone.ZoneId > 0) @@ -291,6 +293,14 @@ public static void ResolveFollowRouteGoal(IServiceCollection services, } } + public static void AddFleeGoal(IServiceCollection services, ClassConfiguration classConfig) + { + if (classConfig.Flee.Sequence.Length == 0) + return; + + services.AddScoped(); + } + private static string RelativeFilePath(DataConfig dataConfig, string path) { return !path.Contains(dataConfig.Path) diff --git a/Core/Path/Simplify/PathSimplify.cs b/Core/Path/Simplify/PathSimplify.cs index 83f175bbf..a196a915e 100644 --- a/Core/Path/Simplify/PathSimplify.cs +++ b/Core/Path/Simplify/PathSimplify.cs @@ -7,6 +7,9 @@ namespace Core; public static class PathSimplify { + public const float DEFAULT = 0.3f; + public const float HALF = 0.15f; + // square distance from a Vector3 to a segment private static float GetSquareSegmentDistance(in Vector3 p, in Vector3 p1, in Vector3 p2) { @@ -129,10 +132,10 @@ private static Span DouglasPeucker(Span points, float sqTolera /// Tolerance tolerance in the same measurement as the Vector3 coordinates /// Enable highest quality for using Douglas-Peucker, set false for Radial-Distance algorithm /// Simplified list of Vector3 - public static Span Simplify(Span points, float tolerance = 0.3f, bool highestQuality = false) + public static Span Simplify(Span points, float tolerance = DEFAULT, bool highestQuality = false) { if (points.Length == 0) - return Array.Empty(); + return []; float sqTolerance = tolerance * tolerance; diff --git a/README.md b/README.md index 3bcb887bc..24997abdf 100644 --- a/README.md +++ b/README.md @@ -482,6 +482,7 @@ Your class file probably exists and just needs to be edited to set the pathing f | `"IntVariables"` | List of user defined `integer` variables | true | `[]` | | --- | --- | --- | --- | | `"Pull"` | [KeyActions](#keyactions) to execute upon [Pull Goal](#pull-goal) | true | `{}` | +| `"Flee"` | [KeyActions](#keyactions) to execute upon [Flee Goal](#flee-goal). Currently only the first one is considered for the custom logic. | true | `{}` | | `"Combat"` | [KeyActions](#keyactions) to execute upon [Combat Goal](#combat-goal) | **false** | `{}` | | `"AssistFocus"` | [KeyActions](#keyactions) to execute upon [Assist Focus Goal](#assist-focus-goal) | **false** | `{}` | | `"Adhoc"` | [KeyActions](#keyactions) to execute upon [Adhoc Goals](#adhoc-goals) | true | `{}` | @@ -900,6 +901,33 @@ e.g. of a Balance Druid }, ``` +### Flee Goal + +Its an opt-in goal. + +Can define custom rules when the character should try to run away from an encounter which seems to be impossible to survive. + +The goal will be executed while the player is in combat and the first KeyAction custom [Requirement(s)](#requirement) are met. + +While the goal is active +* the player going to retrace the past locations which were deemed to be safe. +* Clears the current target if have any. + +The path will be simplifed to ensure straight line of movement. + +To opt-in the goal execution you have to define the following the [Class Configuration](#12-class-configuration) + +```json +"Flee": { + "Sequence": [ + { + "Name": "Flee", + "Requirement": "MobCount > 1 && Health% < 50" + } + ] +}, +``` + ### Combat Goal The `Sequence` of [KeyAction(s)](#keyaction) that are used when in combat and trying to kill a mob. @@ -1931,6 +1959,7 @@ e.g. Rogue_20.json Every [KeyAction](#keyaction) has individual Interrupt(s) condition(s) which are [Requirement(s)](#requirement) to stop execution before fully finishing it. As of now every [Goal groups](#goal-groups) has a default Interrupt. +* [Flee Goal](#flee-goal) based [KeyAction(s)](#keyaction) interrupted once the player left combat. * [Combat Goal](#combat-goal) based [KeyAction(s)](#keyaction) interrupted once the target dies and the player loses the target. * [Parallel Goal](#parallel-goals) based [KeyAction(s)](#keyaction) has **No** interrupt conditions. * [Adhoc Goals](#adhoc-goals) based [KeyAction(s)](#keyaction) depends on `KeyAction.InCombat` flag.