Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FleeGoal #636

Merged
merged 6 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Core/ClassConfig/ClassConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 1 addition & 2 deletions Core/GOAP/GoapAgentState.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

namespace Core.GOAP;
namespace Core.GOAP;

public sealed class GoapAgentState
{
Expand Down
5 changes: 5 additions & 0 deletions Core/Goals/CombatGoal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.Extensions.Logging;

using System;
using System.Collections.Generic;
using System.Numerics;

namespace Core.Goals;
Expand Down Expand Up @@ -162,6 +163,10 @@ public override void Update()
stopMoving.Stop();
FindNewTarget();
}
else
{
input.PressClearTarget();
}
}
}

Expand Down
120 changes: 120 additions & 0 deletions Core/Goals/FleeGoal.cs
Original file line number Diff line number Diff line change
@@ -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<CombatGoal> 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<CombatGoal> 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<Vector3> pooler = ArrayPool<Vector3>.Shared;
Vector3[] array = pooler.Rent(count);
var span = array.AsSpan();

safeSpotCollector.MapLocations.CopyTo(array, 0);

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()
{
safeSpotCollector.Reduce(playerReader.MapPosNoZ);

navigation.Stop();
navigation.StopMovement();
}

public override void Update()
{
if (bits.Target())
{
input.PressClearTarget();
}

wait.Update();
navigation.Update();
}
}
8 changes: 2 additions & 6 deletions Core/GoalsComponent/Navigation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}

Expand Down
69 changes: 69 additions & 0 deletions Core/GoalsComponent/SafeSpotCollector.cs
Original file line number Diff line number Diff line change
@@ -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<Vector3> 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();
}
}
}
}
10 changes: 10 additions & 0 deletions Core/GoalsFactory/GoalFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>();

Expand Down Expand Up @@ -136,6 +137,7 @@ public static IServiceProvider Create(
services.AddScoped<GoapGoal, WalkToCorpseGoal>();
services.AddScoped<GoapGoal, PullTargetGoal>();
services.AddScoped<GoapGoal, ApproachTargetGoal>();
AddFleeGoal(services, classConfig);
services.AddScoped<GoapGoal, CombatGoal>();

if (classConfig.WrongZone.ZoneId > 0)
Expand Down Expand Up @@ -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<GoapGoal, FleeGoal>();
}

private static string RelativeFilePath(DataConfig dataConfig, string path)
{
return !path.Contains(dataConfig.Path)
Expand Down
7 changes: 5 additions & 2 deletions Core/Path/Simplify/PathSimplify.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;

Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | `{}` |
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down