From eda5c4579be0ce5bd6c88b03ec58cfda9fca3bfa Mon Sep 17 00:00:00 2001
From: Aneesh Dogra <lionaneesh@users.noreply.github.com>
Date: Thu, 12 Dec 2024 00:09:53 +0100
Subject: [PATCH 1/5] Add FleeGoal

A goal which can be configured to run away to a possible safepoint if X mobs attack.
---
 Core/GOAP/GoapAgentState.cs      |   5 +-
 Core/Goals/CombatGoal.cs         |  36 +++++++-
 Core/Goals/FleeGoal.cs           | 147 +++++++++++++++++++++++++++++++
 Core/GoalsFactory/GoalFactory.cs |   1 +
 4 files changed, 187 insertions(+), 2 deletions(-)
 create mode 100644 Core/Goals/FleeGoal.cs

diff --git a/Core/GOAP/GoapAgentState.cs b/Core/GOAP/GoapAgentState.cs
index 386f5d59e..4f6024149 100644
--- a/Core/GOAP/GoapAgentState.cs
+++ b/Core/GOAP/GoapAgentState.cs
@@ -1,4 +1,6 @@
-
+using System.Collections.Generic;
+using System.Numerics;
+
 namespace Core.GOAP;
 
 public sealed class GoapAgentState
@@ -9,4 +11,5 @@ public sealed class GoapAgentState
     public int ConsumableCorpseCount { get; set; }
     public int LastCombatKillCount { get; set; }
     public bool Gathering { get; set; }
+    public LinkedList<Vector3> safeLocations = new LinkedList<Vector3>();
 }
diff --git a/Core/Goals/CombatGoal.cs b/Core/Goals/CombatGoal.cs
index b4b4e1ae0..9d7eed602 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;
@@ -21,6 +22,7 @@ public sealed class CombatGoal : GoapGoal, IGoapEventListener
     private readonly CastingHandler castingHandler;
     private readonly IMountHandler mountHandler;
     private readonly CombatLog combatLog;
+    private readonly GoapAgentState goapAgentState;
 
     private float lastDirection;
     private float lastMinDistance;
@@ -29,7 +31,7 @@ public sealed class CombatGoal : GoapGoal, IGoapEventListener
     public CombatGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
         Wait wait, PlayerReader playerReader, StopMoving stopMoving, AddonBits bits,
         ClassConfiguration classConfiguration, ClassConfiguration classConfig,
-        CastingHandler castingHandler, CombatLog combatLog,
+        CastingHandler castingHandler, CombatLog combatLog, GoapAgentState state,
         IMountHandler mountHandler)
         : base(nameof(CombatGoal))
     {
@@ -46,6 +48,8 @@ public CombatGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
         this.mountHandler = mountHandler;
         this.classConfig = classConfig;
 
+        this.goapAgentState = state;
+
         AddPrecondition(GoapKey.incombat, true);
         AddPrecondition(GoapKey.hastarget, true);
         AddPrecondition(GoapKey.targetisalive, true);
@@ -162,6 +166,36 @@ public override void Update()
                 stopMoving.Stop();
                 FindNewTarget();
             }
+            else
+            {
+                // dont save too close points
+                logger.LogInformation("Target(s) Dead -- trying saving safe pos " + playerReader.MapPosNoZ.ToString());
+                bool foundClosePoint = false;
+                if (this.goapAgentState.safeLocations == null)
+                {
+                    return;
+                }
+                for (LinkedListNode<Vector3> point = this.goapAgentState.safeLocations.Last; point != null; point = point.Previous)
+                {
+                    Vector2 p1 = new Vector2(point.Value.X, point.Value.Y);
+                    Vector2 p2 = new Vector2(playerReader.MapPos.X, playerReader.MapPos.Y);
+                    if (Vector2.Distance(p1, p2) <= 0.1)
+                    {
+                        foundClosePoint = true;
+                        break;
+                    }
+                }
+                if (!foundClosePoint)
+                {
+                    Console.WriteLine("Saving safepos: " + playerReader.MapPosNoZ.ToString());
+                    this.goapAgentState.safeLocations.AddLast(playerReader.MapPosNoZ);
+                    if (this.goapAgentState.safeLocations.Count > 100)
+                    {
+                        this.goapAgentState.safeLocations.RemoveFirst();
+                    }
+                }
+                input.PressClearTarget();
+            }
         }
     }
 
diff --git a/Core/Goals/FleeGoal.cs b/Core/Goals/FleeGoal.cs
new file mode 100644
index 000000000..b437792db
--- /dev/null
+++ b/Core/Goals/FleeGoal.cs
@@ -0,0 +1,147 @@
+using Core.GOAP;
+
+using Microsoft.Extensions.Logging;
+
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace Core.Goals;
+
+public sealed class FleeGoal : GoapGoal
+{
+    public override float Cost => 4f;
+    private readonly ILogger<CombatGoal> logger;
+    private readonly ConfigurableInput input;
+    private readonly ClassConfiguration classConfig;
+    private readonly Wait wait;
+    private readonly PlayerReader playerReader;
+    private readonly Navigation playerNavigation;
+    private readonly AddonBits bits;
+    private readonly StopMoving stopMoving;
+    private readonly CastingHandler castingHandler;
+    private readonly IMountHandler mountHandler;
+    private readonly CombatLog combatLog;
+    private readonly GoapAgentState goapAgentState;
+    public int MOB_COUNT = 1;
+    public bool runningAway;
+
+    public FleeGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
+        Wait wait, PlayerReader playerReader, StopMoving stopMoving, AddonBits bits,
+        ClassConfiguration classConfiguration, Navigation playerNavigation, GoapAgentState state,
+        ClassConfiguration classConfig, CastingHandler castingHandler, CombatLog combatLog,
+        IMountHandler mountHandler)
+        : base(nameof(CombatGoal))
+    {
+        this.logger = logger;
+        this.input = input;
+
+        this.runningAway = false;
+        this.wait = wait;
+        this.playerReader = playerReader;
+        this.playerNavigation = playerNavigation;
+        this.bits = bits;
+        this.combatLog = combatLog;
+
+        this.stopMoving = stopMoving;
+        this.castingHandler = castingHandler;
+        this.mountHandler = mountHandler;
+        this.classConfig = classConfig;
+        this.goapAgentState = state;
+
+        AddPrecondition(GoapKey.incombat, true);
+        //AddPrecondition(GoapKey.hastarget, true);
+        //AddPrecondition(GoapKey.targetisalive, true);
+        //AddPrecondition(GoapKey.targethostile, true);
+        //AddPrecondition(GoapKey.targettargetsus, true);
+        //AddPrecondition(GoapKey.incombatrange, true);
+
+        //AddEffect(GoapKey.producedcorpse, true);
+        //AddEffect(GoapKey.targetisalive, false);
+        //AddEffect(GoapKey.hastarget, false);
+
+        Keys = classConfiguration.Combat.Sequence;
+    }
+
+    private void ResetCooldowns()
+    {
+        ReadOnlySpan<KeyAction> span = Keys;
+        for (int i = 0; i < span.Length; i++)
+        {
+            KeyAction keyAction = span[i];
+            if (keyAction.ResetOnNewTarget)
+            {
+                keyAction.ResetCooldown();
+                keyAction.ResetCharges();
+            }
+        }
+    }
+
+    public override void OnEnter()
+    {
+        if (mountHandler.IsMounted())
+        {
+            mountHandler.Dismount();
+        }
+
+        this.runningAway = false;
+        playerNavigation.Stop();
+        playerNavigation.SetWayPoints(stackalloc Vector3[] { });
+        playerNavigation.ResetStuckParameters();
+    }
+
+    public override void OnExit()
+    {
+        if (combatLog.DamageTakenCount() > 0 && !bits.Target())
+        {
+            stopMoving.Stop();
+        }
+        // clearing
+        this.runningAway = false;
+        playerNavigation.Stop();
+        playerNavigation.SetWayPoints(stackalloc Vector3[] { });
+        playerNavigation.ResetStuckParameters();
+    }
+
+    public override void Update()
+    {
+        wait.Update();
+        if (this.goapAgentState.safeLocations.Count >= MOB_COUNT && this.runningAway == false)
+        {
+
+            bool foundPoint = false;
+            logger.LogInformation("Flee Goal Activated. Current Pos: " + playerReader.MapPos.ToString() + ",Safe Spots: " + goapAgentState.safeLocations.Count);
+            if (goapAgentState.safeLocations == null)
+            {
+                return;
+            }
+            for (LinkedListNode<Vector3> point = goapAgentState.safeLocations.Last; point != null; point = point.Previous)
+            {
+                Vector2 p1 = new Vector2(point.Value.X, point.Value.Y);
+                Vector2 p2 = new Vector2(playerReader.MapPos.X, playerReader.MapPos.Y);
+                if (Vector2.Distance(p1, p2) >= 2.2)
+                {
+                    // select the point far enough to lose the current mobs.
+                    input.PressClearTarget();
+                    playerNavigation.Stop();
+                    playerNavigation.ResetStuckParameters();
+                    playerNavigation.SetWayPoints(stackalloc Vector3[] { (Vector3)(point.Value) });
+                    playerNavigation.Update();
+                    Console.WriteLine("Found point " + point.Value.ToString());
+                    foundPoint = true;
+                    this.runningAway = true;
+                    break;
+                }
+            }
+            if (foundPoint)
+            {
+                logger.LogInformation("Running away to the last safe point!");
+                return;
+            }
+            else
+            {
+                logger.LogInformation("Cant run away, figting!");
+            }
+        }
+    }
+}
diff --git a/Core/GoalsFactory/GoalFactory.cs b/Core/GoalsFactory/GoalFactory.cs
index e1e55be92..0ff42a867 100644
--- a/Core/GoalsFactory/GoalFactory.cs
+++ b/Core/GoalsFactory/GoalFactory.cs
@@ -83,6 +83,7 @@ public static IServiceProvider Create(
         {
             services.AddScoped<GoapGoal, WalkToCorpseGoal>();
             services.AddScoped<GoapGoal, CombatGoal>();
+            services.AddScoped<GoapGoal, FleeGoal>();
             services.AddScoped<GoapGoal, ApproachTargetGoal>();
             services.AddScoped<GoapGoal, WaitForGatheringGoal>();
             ResolveFollowRouteGoal(services, classConfig);

From ca04ca32310eecf027ed24fa8e204b378b44159e Mon Sep 17 00:00:00 2001
From: Xian55 <367101+Xian55@users.noreply.github.com>
Date: Thu, 12 Dec 2024 03:03:22 +0100
Subject: [PATCH 2/5] Refactor: FleeGoal: uses a slightly different
 implementation to better fit with the current project setup.

---
 Core/GOAP/GoapAgentState.cs              |   3 +-
 Core/Goals/CombatGoal.cs                 |  31 +----
 Core/Goals/FleeGoal.cs                   | 152 +++++++++--------------
 Core/GoalsComponent/SafeSpotCollector.cs |  44 +++++++
 Core/GoalsFactory/GoalFactory.cs         |   2 +
 5 files changed, 111 insertions(+), 121 deletions(-)
 create mode 100644 Core/GoalsComponent/SafeSpotCollector.cs

diff --git a/Core/GOAP/GoapAgentState.cs b/Core/GOAP/GoapAgentState.cs
index 4f6024149..f54c86ddb 100644
--- a/Core/GOAP/GoapAgentState.cs
+++ b/Core/GOAP/GoapAgentState.cs
@@ -11,5 +11,6 @@ public sealed class GoapAgentState
     public int ConsumableCorpseCount { get; set; }
     public int LastCombatKillCount { get; set; }
     public bool Gathering { get; set; }
-    public LinkedList<Vector3> safeLocations = new LinkedList<Vector3>();
+
+    public Stack<Vector3> SafeLocations { get; } = new();
 }
diff --git a/Core/Goals/CombatGoal.cs b/Core/Goals/CombatGoal.cs
index 9d7eed602..073f0f26a 100644
--- a/Core/Goals/CombatGoal.cs
+++ b/Core/Goals/CombatGoal.cs
@@ -22,7 +22,6 @@ public sealed class CombatGoal : GoapGoal, IGoapEventListener
     private readonly CastingHandler castingHandler;
     private readonly IMountHandler mountHandler;
     private readonly CombatLog combatLog;
-    private readonly GoapAgentState goapAgentState;
 
     private float lastDirection;
     private float lastMinDistance;
@@ -31,7 +30,7 @@ public sealed class CombatGoal : GoapGoal, IGoapEventListener
     public CombatGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
         Wait wait, PlayerReader playerReader, StopMoving stopMoving, AddonBits bits,
         ClassConfiguration classConfiguration, ClassConfiguration classConfig,
-        CastingHandler castingHandler, CombatLog combatLog, GoapAgentState state,
+        CastingHandler castingHandler, CombatLog combatLog,
         IMountHandler mountHandler)
         : base(nameof(CombatGoal))
     {
@@ -48,8 +47,6 @@ public CombatGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
         this.mountHandler = mountHandler;
         this.classConfig = classConfig;
 
-        this.goapAgentState = state;
-
         AddPrecondition(GoapKey.incombat, true);
         AddPrecondition(GoapKey.hastarget, true);
         AddPrecondition(GoapKey.targetisalive, true);
@@ -168,32 +165,6 @@ public override void Update()
             }
             else
             {
-                // dont save too close points
-                logger.LogInformation("Target(s) Dead -- trying saving safe pos " + playerReader.MapPosNoZ.ToString());
-                bool foundClosePoint = false;
-                if (this.goapAgentState.safeLocations == null)
-                {
-                    return;
-                }
-                for (LinkedListNode<Vector3> point = this.goapAgentState.safeLocations.Last; point != null; point = point.Previous)
-                {
-                    Vector2 p1 = new Vector2(point.Value.X, point.Value.Y);
-                    Vector2 p2 = new Vector2(playerReader.MapPos.X, playerReader.MapPos.Y);
-                    if (Vector2.Distance(p1, p2) <= 0.1)
-                    {
-                        foundClosePoint = true;
-                        break;
-                    }
-                }
-                if (!foundClosePoint)
-                {
-                    Console.WriteLine("Saving safepos: " + playerReader.MapPosNoZ.ToString());
-                    this.goapAgentState.safeLocations.AddLast(playerReader.MapPosNoZ);
-                    if (this.goapAgentState.safeLocations.Count > 100)
-                    {
-                        this.goapAgentState.safeLocations.RemoveFirst();
-                    }
-                }
                 input.PressClearTarget();
             }
         }
diff --git a/Core/Goals/FleeGoal.cs b/Core/Goals/FleeGoal.cs
index b437792db..06787544d 100644
--- a/Core/Goals/FleeGoal.cs
+++ b/Core/Goals/FleeGoal.cs
@@ -3,145 +3,117 @@
 using Microsoft.Extensions.Logging;
 
 using System;
-using System.Collections.Generic;
+using System.Buffers;
 using System.Numerics;
 
 namespace Core.Goals;
 
-public sealed class FleeGoal : GoapGoal
+public sealed class FleeGoal : GoapGoal, IRouteProvider
 {
-    public override float Cost => 4f;
+    public override float Cost => 3.1f;
+
     private readonly ILogger<CombatGoal> logger;
     private readonly ConfigurableInput input;
     private readonly ClassConfiguration classConfig;
     private readonly Wait wait;
     private readonly PlayerReader playerReader;
-    private readonly Navigation playerNavigation;
+    private readonly Navigation navigation;
     private readonly AddonBits bits;
-    private readonly StopMoving stopMoving;
-    private readonly CastingHandler castingHandler;
-    private readonly IMountHandler mountHandler;
     private readonly CombatLog combatLog;
     private readonly GoapAgentState goapAgentState;
+
+    private readonly SafeSpotCollector safeSpotCollector;
+
+    private Vector3[] MapPoints = [];
+
     public int MOB_COUNT = 1;
-    public bool runningAway;
 
     public FleeGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
-        Wait wait, PlayerReader playerReader, StopMoving stopMoving, AddonBits bits,
+        Wait wait, PlayerReader playerReader, AddonBits bits,
         ClassConfiguration classConfiguration, Navigation playerNavigation, GoapAgentState state,
-        ClassConfiguration classConfig, CastingHandler castingHandler, CombatLog combatLog,
-        IMountHandler mountHandler)
-        : base(nameof(CombatGoal))
+        ClassConfiguration classConfig, CombatLog combatLog,
+        SafeSpotCollector safeSpotCollector)
+        : base(nameof(FleeGoal))
     {
         this.logger = logger;
         this.input = input;
 
-        this.runningAway = false;
         this.wait = wait;
         this.playerReader = playerReader;
-        this.playerNavigation = playerNavigation;
+        this.navigation = playerNavigation;
         this.bits = bits;
         this.combatLog = combatLog;
 
-        this.stopMoving = stopMoving;
-        this.castingHandler = castingHandler;
-        this.mountHandler = mountHandler;
         this.classConfig = classConfig;
         this.goapAgentState = state;
 
         AddPrecondition(GoapKey.incombat, true);
-        //AddPrecondition(GoapKey.hastarget, true);
-        //AddPrecondition(GoapKey.targetisalive, true);
-        //AddPrecondition(GoapKey.targethostile, true);
-        //AddPrecondition(GoapKey.targettargetsus, true);
-        //AddPrecondition(GoapKey.incombatrange, true);
-
-        //AddEffect(GoapKey.producedcorpse, true);
-        //AddEffect(GoapKey.targetisalive, false);
-        //AddEffect(GoapKey.hastarget, false);
 
         Keys = classConfiguration.Combat.Sequence;
+
+        // this will ensure that the component is created
+        this.safeSpotCollector = safeSpotCollector;
+    }
+
+    #region IRouteProvider
+
+    public DateTime LastActive => navigation.LastActive;
+
+    public Vector3[] MapRoute() => MapPoints;
+
+    public Vector3[] PathingRoute()
+    {
+        return navigation.TotalRoute;
     }
 
-    private void ResetCooldowns()
+    public bool HasNext()
     {
-        ReadOnlySpan<KeyAction> span = Keys;
-        for (int i = 0; i < span.Length; i++)
-        {
-            KeyAction keyAction = span[i];
-            if (keyAction.ResetOnNewTarget)
-            {
-                keyAction.ResetCooldown();
-                keyAction.ResetCharges();
-            }
-        }
+        return navigation.HasNext();
+    }
+
+    public Vector3 NextMapPoint()
+    {
+        return navigation.NextMapPoint();
+    }
+
+    #endregion
+
+    public override bool CanRun()
+    {
+        return
+            goapAgentState.SafeLocations.Count > 0 &&
+            combatLog.DamageTakenCount() > MOB_COUNT;
     }
 
     public override void OnEnter()
     {
-        if (mountHandler.IsMounted())
-        {
-            mountHandler.Dismount();
-        }
+        // TODO: might have to do some pre processing like
+        // straightening the path
+        var count = goapAgentState.SafeLocations.Count;
+        MapPoints = new Vector3[count];
+
+        goapAgentState.SafeLocations.CopyTo(MapPoints, 0);
 
-        this.runningAway = false;
-        playerNavigation.Stop();
-        playerNavigation.SetWayPoints(stackalloc Vector3[] { });
-        playerNavigation.ResetStuckParameters();
+        navigation.SetWayPoints(MapPoints.AsSpan(0, count));
+        navigation.ResetStuckParameters();
     }
 
     public override void OnExit()
     {
-        if (combatLog.DamageTakenCount() > 0 && !bits.Target())
-        {
-            stopMoving.Stop();
-        }
-        // clearing
-        this.runningAway = false;
-        playerNavigation.Stop();
-        playerNavigation.SetWayPoints(stackalloc Vector3[] { });
-        playerNavigation.ResetStuckParameters();
+        goapAgentState.SafeLocations.Clear();
+
+        navigation.Stop();
+        navigation.StopMovement();
     }
 
     public override void Update()
     {
-        wait.Update();
-        if (this.goapAgentState.safeLocations.Count >= MOB_COUNT && this.runningAway == false)
+        if (bits.Target())
         {
-
-            bool foundPoint = false;
-            logger.LogInformation("Flee Goal Activated. Current Pos: " + playerReader.MapPos.ToString() + ",Safe Spots: " + goapAgentState.safeLocations.Count);
-            if (goapAgentState.safeLocations == null)
-            {
-                return;
-            }
-            for (LinkedListNode<Vector3> point = goapAgentState.safeLocations.Last; point != null; point = point.Previous)
-            {
-                Vector2 p1 = new Vector2(point.Value.X, point.Value.Y);
-                Vector2 p2 = new Vector2(playerReader.MapPos.X, playerReader.MapPos.Y);
-                if (Vector2.Distance(p1, p2) >= 2.2)
-                {
-                    // select the point far enough to lose the current mobs.
-                    input.PressClearTarget();
-                    playerNavigation.Stop();
-                    playerNavigation.ResetStuckParameters();
-                    playerNavigation.SetWayPoints(stackalloc Vector3[] { (Vector3)(point.Value) });
-                    playerNavigation.Update();
-                    Console.WriteLine("Found point " + point.Value.ToString());
-                    foundPoint = true;
-                    this.runningAway = true;
-                    break;
-                }
-            }
-            if (foundPoint)
-            {
-                logger.LogInformation("Running away to the last safe point!");
-                return;
-            }
-            else
-            {
-                logger.LogInformation("Cant run away, figting!");
-            }
+            input.PressClearTarget();
         }
+
+        wait.Update();
+        navigation.Update();
     }
 }
diff --git a/Core/GoalsComponent/SafeSpotCollector.cs b/Core/GoalsComponent/SafeSpotCollector.cs
new file mode 100644
index 000000000..d5c7cbc5a
--- /dev/null
+++ b/Core/GoalsComponent/SafeSpotCollector.cs
@@ -0,0 +1,44 @@
+using Core.GOAP;
+
+using System;
+using System.Threading;
+
+namespace Core.Goals;
+
+public sealed class SafeSpotCollector : IDisposable
+{
+    private readonly PlayerReader playerReader;
+    private readonly GoapAgentState state;
+    private readonly AddonBits bits;
+
+    private readonly Timer timer;
+
+    public SafeSpotCollector(
+        PlayerReader playerReader,
+        GoapAgentState state,
+        AddonBits bits)
+    {
+        this.playerReader = playerReader;
+        this.state = state;
+        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 (state.SafeLocations.TryPeek(out var lastPos) &&
+            lastPos == playerReader.MapPosNoZ)
+            return;
+
+        state.SafeLocations.Push(playerReader.MapPosNoZ);
+    }
+}
diff --git a/Core/GoalsFactory/GoalFactory.cs b/Core/GoalsFactory/GoalFactory.cs
index 0ff42a867..fa6fbd1e7 100644
--- a/Core/GoalsFactory/GoalFactory.cs
+++ b/Core/GoalsFactory/GoalFactory.cs
@@ -57,6 +57,7 @@ public static IServiceProvider Create(
         services.AddScoped<CastingHandler>();
         services.AddScoped<StuckDetector>();
         services.AddScoped<CombatUtil>();
+        services.AddScoped<SafeSpotCollector>();
 
         var playerReader = sp.GetRequiredService<PlayerReader>();
 
@@ -137,6 +138,7 @@ public static IServiceProvider Create(
             services.AddScoped<GoapGoal, WalkToCorpseGoal>();
             services.AddScoped<GoapGoal, PullTargetGoal>();
             services.AddScoped<GoapGoal, ApproachTargetGoal>();
+            services.AddScoped<GoapGoal, FleeGoal>();
             services.AddScoped<GoapGoal, CombatGoal>();
 
             if (classConfig.WrongZone.ZoneId > 0)

From f063ae9643af8b35226b795daa5120f8ac3dedc2 Mon Sep 17 00:00:00 2001
From: Xian55 <367101+Xian55@users.noreply.github.com>
Date: Thu, 12 Dec 2024 03:08:27 +0100
Subject: [PATCH 3/5] Added more notes

---
 Core/Goals/FleeGoal.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/Core/Goals/FleeGoal.cs b/Core/Goals/FleeGoal.cs
index 06787544d..990113e1b 100644
--- a/Core/Goals/FleeGoal.cs
+++ b/Core/Goals/FleeGoal.cs
@@ -100,6 +100,7 @@ public override void OnEnter()
 
     public override void OnExit()
     {
+        // TODO: there might be better options here to dont clear all of them
         goapAgentState.SafeLocations.Clear();
 
         navigation.Stop();

From 46a178d40ecdf6a9e3b78a3ea745ce4bc194fd68 Mon Sep 17 00:00:00 2001
From: Xian55 <367101+Xian55@users.noreply.github.com>
Date: Thu, 12 Dec 2024 03:27:08 +0100
Subject: [PATCH 4/5] Refactor: GoapAgentState: no longer needed

Refactor: SafeSpotCollector: stores the spots.

More todos
---
 Core/GOAP/GoapAgentState.cs              |  7 +------
 Core/Goals/FleeGoal.cs                   | 15 +++++++--------
 Core/GoalsComponent/SafeSpotCollector.cs | 12 +++++++-----
 3 files changed, 15 insertions(+), 19 deletions(-)

diff --git a/Core/GOAP/GoapAgentState.cs b/Core/GOAP/GoapAgentState.cs
index f54c86ddb..5d304769c 100644
--- a/Core/GOAP/GoapAgentState.cs
+++ b/Core/GOAP/GoapAgentState.cs
@@ -1,7 +1,4 @@
-using System.Collections.Generic;
-using System.Numerics;
-
-namespace Core.GOAP;
+namespace Core.GOAP;
 
 public sealed class GoapAgentState
 {
@@ -11,6 +8,4 @@ public sealed class GoapAgentState
     public int ConsumableCorpseCount { get; set; }
     public int LastCombatKillCount { get; set; }
     public bool Gathering { get; set; }
-
-    public Stack<Vector3> SafeLocations { get; } = new();
 }
diff --git a/Core/Goals/FleeGoal.cs b/Core/Goals/FleeGoal.cs
index 990113e1b..8d0742151 100644
--- a/Core/Goals/FleeGoal.cs
+++ b/Core/Goals/FleeGoal.cs
@@ -20,7 +20,6 @@ public sealed class FleeGoal : GoapGoal, IRouteProvider
     private readonly Navigation navigation;
     private readonly AddonBits bits;
     private readonly CombatLog combatLog;
-    private readonly GoapAgentState goapAgentState;
 
     private readonly SafeSpotCollector safeSpotCollector;
 
@@ -30,7 +29,7 @@ public sealed class FleeGoal : GoapGoal, IRouteProvider
 
     public FleeGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
         Wait wait, PlayerReader playerReader, AddonBits bits,
-        ClassConfiguration classConfiguration, Navigation playerNavigation, GoapAgentState state,
+        ClassConfiguration classConfiguration, Navigation playerNavigation,
         ClassConfiguration classConfig, CombatLog combatLog,
         SafeSpotCollector safeSpotCollector)
         : base(nameof(FleeGoal))
@@ -45,7 +44,6 @@ public FleeGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
         this.combatLog = combatLog;
 
         this.classConfig = classConfig;
-        this.goapAgentState = state;
 
         AddPrecondition(GoapKey.incombat, true);
 
@@ -81,18 +79,19 @@ public Vector3 NextMapPoint()
     public override bool CanRun()
     {
         return
-            goapAgentState.SafeLocations.Count > 0 &&
+            safeSpotCollector.Locations.Count > 0 &&
             combatLog.DamageTakenCount() > MOB_COUNT;
     }
 
     public override void OnEnter()
     {
         // TODO: might have to do some pre processing like
-        // straightening the path
-        var count = goapAgentState.SafeLocations.Count;
+        // straightening the path like
+        // PathSimplify.SimplifyPath(MapPoints);
+        var count = safeSpotCollector.Locations.Count;
         MapPoints = new Vector3[count];
 
-        goapAgentState.SafeLocations.CopyTo(MapPoints, 0);
+        safeSpotCollector.Locations.CopyTo(MapPoints, 0);
 
         navigation.SetWayPoints(MapPoints.AsSpan(0, count));
         navigation.ResetStuckParameters();
@@ -101,7 +100,7 @@ public override void OnEnter()
     public override void OnExit()
     {
         // TODO: there might be better options here to dont clear all of them
-        goapAgentState.SafeLocations.Clear();
+        safeSpotCollector.Locations.Clear();
 
         navigation.Stop();
         navigation.StopMovement();
diff --git a/Core/GoalsComponent/SafeSpotCollector.cs b/Core/GoalsComponent/SafeSpotCollector.cs
index d5c7cbc5a..0bf0d6498 100644
--- a/Core/GoalsComponent/SafeSpotCollector.cs
+++ b/Core/GoalsComponent/SafeSpotCollector.cs
@@ -1,6 +1,8 @@
 using Core.GOAP;
 
 using System;
+using System.Collections.Generic;
+using System.Numerics;
 using System.Threading;
 
 namespace Core.Goals;
@@ -8,18 +10,17 @@ namespace Core.Goals;
 public sealed class SafeSpotCollector : IDisposable
 {
     private readonly PlayerReader playerReader;
-    private readonly GoapAgentState state;
     private readonly AddonBits bits;
 
     private readonly Timer timer;
 
+    public Stack<Vector3> Locations { get; } = new();
+
     public SafeSpotCollector(
         PlayerReader playerReader,
-        GoapAgentState state,
         AddonBits bits)
     {
         this.playerReader = playerReader;
-        this.state = state;
         this.bits = bits;
 
         timer = new(Update, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
@@ -35,10 +36,11 @@ public void Update(object? obj)
         if (bits.Combat())
             return;
 
-        if (state.SafeLocations.TryPeek(out var lastPos) &&
+        if (Locations.TryPeek(out var lastPos) &&
             lastPos == playerReader.MapPosNoZ)
             return;
 
-        state.SafeLocations.Push(playerReader.MapPosNoZ);
+        // TODO: might be a good idea to check distance to last location
+        Locations.Push(playerReader.MapPosNoZ);
     }
 }

From f1682ef22657b6c73a98d2de81dadcd30785d9c8 Mon Sep 17 00:00:00 2001
From: Xian55 <367101+Xian55@users.noreply.github.com>
Date: Sun, 15 Dec 2024 14:31:48 +0100
Subject: [PATCH 5/5] Core: FleeGoal: Added a way to custom define when the
 goal should be opt-in.

Core: Refactor a little bit.
---
 Core/ClassConfig/ClassConfiguration.cs   |  1 +
 Core/Goals/FleeGoal.cs                   | 37 ++++++++++++------------
 Core/GoalsComponent/Navigation.cs        |  8 ++---
 Core/GoalsComponent/SafeSpotCollector.cs | 35 ++++++++++++++++++----
 Core/GoalsFactory/GoalFactory.cs         | 11 +++++--
 Core/Path/Simplify/PathSimplify.cs       |  7 +++--
 README.md                                | 29 +++++++++++++++++++
 7 files changed, 94 insertions(+), 34 deletions(-)

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<string, int> 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/Goals/FleeGoal.cs b/Core/Goals/FleeGoal.cs
index 8d0742151..19930e309 100644
--- a/Core/Goals/FleeGoal.cs
+++ b/Core/Goals/FleeGoal.cs
@@ -2,6 +2,8 @@
 
 using Microsoft.Extensions.Logging;
 
+using SharedLib.Extensions;
+
 using System;
 using System.Buffers;
 using System.Numerics;
@@ -19,18 +21,15 @@ public sealed class FleeGoal : GoapGoal, IRouteProvider
     private readonly PlayerReader playerReader;
     private readonly Navigation navigation;
     private readonly AddonBits bits;
-    private readonly CombatLog combatLog;
 
     private readonly SafeSpotCollector safeSpotCollector;
 
     private Vector3[] MapPoints = [];
 
-    public int MOB_COUNT = 1;
-
     public FleeGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
         Wait wait, PlayerReader playerReader, AddonBits bits,
         ClassConfiguration classConfiguration, Navigation playerNavigation,
-        ClassConfiguration classConfig, CombatLog combatLog,
+        ClassConfiguration classConfig,
         SafeSpotCollector safeSpotCollector)
         : base(nameof(FleeGoal))
     {
@@ -41,15 +40,13 @@ public FleeGoal(ILogger<CombatGoal> logger, ConfigurableInput input,
         this.playerReader = playerReader;
         this.navigation = playerNavigation;
         this.bits = bits;
-        this.combatLog = combatLog;
 
         this.classConfig = classConfig;
 
         AddPrecondition(GoapKey.incombat, true);
 
-        Keys = classConfiguration.Combat.Sequence;
+        Keys = classConfiguration.Flee.Sequence;
 
-        // this will ensure that the component is created
         this.safeSpotCollector = safeSpotCollector;
     }
 
@@ -79,28 +76,32 @@ public Vector3 NextMapPoint()
     public override bool CanRun()
     {
         return
-            safeSpotCollector.Locations.Count > 0 &&
-            combatLog.DamageTakenCount() > MOB_COUNT;
+            safeSpotCollector.MapLocations.Count > 0 &&
+            Keys.Length > 0 && Keys[0].CanRun();
     }
 
     public override void OnEnter()
     {
-        // TODO: might have to do some pre processing like
-        // straightening the path like
-        // PathSimplify.SimplifyPath(MapPoints);
-        var count = safeSpotCollector.Locations.Count;
-        MapPoints = new Vector3[count];
+        int count = safeSpotCollector.MapLocations.Count;
+
+        ArrayPool<Vector3> pooler = ArrayPool<Vector3>.Shared;
+        Vector3[] array = pooler.Rent(count);
+        var span = array.AsSpan();
 
-        safeSpotCollector.Locations.CopyTo(MapPoints, 0);
+        safeSpotCollector.MapLocations.CopyTo(array, 0);
 
-        navigation.SetWayPoints(MapPoints.AsSpan(0, count));
+        Span<Vector3> simplified = PathSimplify.Simplify(array.AsSpan()[..count], PathSimplify.HALF, true);
+        MapPoints = simplified.ToArray();
+
+        navigation.SetWayPoints(simplified);
         navigation.ResetStuckParameters();
+
+        pooler.Return(array);
     }
 
     public override void OnExit()
     {
-        // TODO: there might be better options here to dont clear all of them
-        safeSpotCollector.Locations.Clear();
+        safeSpotCollector.Reduce(playerReader.MapPosNoZ);
 
         navigation.Stop();
         navigation.StopMovement();
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
index 0bf0d6498..2f47a346e 100644
--- a/Core/GoalsComponent/SafeSpotCollector.cs
+++ b/Core/GoalsComponent/SafeSpotCollector.cs
@@ -1,4 +1,4 @@
-using Core.GOAP;
+using SharedLib.Extensions;
 
 using System;
 using System.Collections.Generic;
@@ -14,7 +14,7 @@ public sealed class SafeSpotCollector : IDisposable
 
     private readonly Timer timer;
 
-    public Stack<Vector3> Locations { get; } = new();
+    public Stack<Vector3> MapLocations { get; } = new();
 
     public SafeSpotCollector(
         PlayerReader playerReader,
@@ -36,11 +36,34 @@ public void Update(object? obj)
         if (bits.Combat())
             return;
 
-        if (Locations.TryPeek(out var lastPos) &&
-            lastPos == playerReader.MapPosNoZ)
+        if (MapLocations.TryPeek(out Vector3 lastMapPos) &&
+            lastMapPos == playerReader.MapPosNoZ)
             return;
 
-        // TODO: might be a good idea to check distance to last location
-        Locations.Push(playerReader.MapPosNoZ);
+        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 fa6fbd1e7..a5da1286e 100644
--- a/Core/GoalsFactory/GoalFactory.cs
+++ b/Core/GoalsFactory/GoalFactory.cs
@@ -84,7 +84,6 @@ public static IServiceProvider Create(
         {
             services.AddScoped<GoapGoal, WalkToCorpseGoal>();
             services.AddScoped<GoapGoal, CombatGoal>();
-            services.AddScoped<GoapGoal, FleeGoal>();
             services.AddScoped<GoapGoal, ApproachTargetGoal>();
             services.AddScoped<GoapGoal, WaitForGatheringGoal>();
             ResolveFollowRouteGoal(services, classConfig);
@@ -138,7 +137,7 @@ public static IServiceProvider Create(
             services.AddScoped<GoapGoal, WalkToCorpseGoal>();
             services.AddScoped<GoapGoal, PullTargetGoal>();
             services.AddScoped<GoapGoal, ApproachTargetGoal>();
-            services.AddScoped<GoapGoal, FleeGoal>();
+            AddFleeGoal(services, classConfig);
             services.AddScoped<GoapGoal, CombatGoal>();
 
             if (classConfig.WrongZone.ZoneId > 0)
@@ -294,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<GoapGoal, FleeGoal>();
+    }
+
     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<Vector3> DouglasPeucker(Span<Vector3> points, float sqTolera
     /// <param name="tolerance">Tolerance tolerance in the same measurement as the Vector3 coordinates</param>
     /// <param name="highestQuality">Enable highest quality for using Douglas-Peucker, set false for Radial-Distance algorithm</param>
     /// <returns>Simplified list of Vector3</returns>
-    public static Span<Vector3> Simplify(Span<Vector3> points, float tolerance = 0.3f, bool highestQuality = false)
+    public static Span<Vector3> Simplify(Span<Vector3> points, float tolerance = DEFAULT, bool highestQuality = false)
     {
         if (points.Length == 0)
-            return Array.Empty<Vector3>();
+            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.