VGRoid support (#27659)

* Dungeon spawn support for grid spawns

* Recursive dungeons working

* Mask approach working

* zack

* More work

* Fix recursive dungeons

* Heap of work

* weh

* the cud

* rar

* Job

* weh

* weh

* weh

* Master merges

* orch

* weh

* vgroid most of the work

* Tweaks

* Tweaks

* weh

* do do do do do do

* Basic layout

* Ore spawning working

* Big breaking changes

* Mob gen working

* weh

* Finalising

* emo

* More finalising

* reverty

* Reduce distance
This commit is contained in:
metalgearsloth
2024-07-03 22:23:11 +10:00
committed by GitHub
parent 1faa1b5df6
commit a2f99cc69e
103 changed files with 4928 additions and 2627 deletions

View File

@@ -0,0 +1,123 @@
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
/*
* Handle BFS searches from Start->End. Doesn't consider NPC pathfinding.
*/
/// <summary>
/// Pathfinding args for a 1-many path.
/// </summary>
public record struct BreadthPathArgs()
{
public Vector2i Start;
public List<Vector2i> Ends;
public bool Diagonals = false;
public Func<Vector2i, float>? TileCost;
public int Limit = 10000;
}
/// <summary>
/// Gets a BFS path from start to any end. Can also supply an optional tile-cost for tiles.
/// </summary>
public SimplePathResult GetBreadthPath(BreadthPathArgs args)
{
var cameFrom = new Dictionary<Vector2i, Vector2i>();
var costSoFar = new Dictionary<Vector2i, float>();
var frontier = new PriorityQueue<Vector2i, float>();
costSoFar[args.Start] = 0f;
frontier.Enqueue(args.Start, 0f);
var count = 0;
while (frontier.TryDequeue(out var node, out _) && count < args.Limit)
{
count++;
if (args.Ends.Contains(node))
{
// Found target
var path = ReconstructPath(node, cameFrom);
return new SimplePathResult()
{
CameFrom = cameFrom,
Path = path,
};
}
var gCost = costSoFar[node];
if (args.Diagonals)
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
var neighbor = node + new Vector2i(x, y);
var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
if (neighborCost.Equals(0f))
{
continue;
}
// f = g + h
// gScore is distance to the start node
// hScore is distance to the end node
var gScore = gCost + neighborCost;
// Slower to get here so just ignore it.
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
{
continue;
}
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
// pFactor is tie-breaker where the fscore is otherwise equal.
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
// There's other ways to do it but future consideration
// The closer the fScore is to the actual distance then the better the pathfinder will be
// (i.e. somewhere between 1 and infinite)
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
frontier.Enqueue(neighbor, gScore);
}
}
}
else
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
continue;
var neighbor = node + new Vector2i(x, y);
var neighborCost = ManhattanDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
if (neighborCost.Equals(0f))
continue;
var gScore = gCost + neighborCost;
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
continue;
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
frontier.Enqueue(neighbor, gScore);
}
}
}
}
return SimplePathResult.NoPath;
}
}

View File

@@ -0,0 +1,74 @@
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
public void GridCast(Vector2i start, Vector2i end, Vector2iCallback callback)
{
// https://gist.github.com/Pyr3z/46884d67641094d6cf353358566db566
// declare all locals at the top so it's obvious how big the footprint is
int dx, dy, xinc, yinc, side, i, error;
// starting cell is always returned
if (!callback(start))
return;
xinc = (end.X < start.X) ? -1 : 1;
yinc = (end.Y < start.Y) ? -1 : 1;
dx = xinc * (end.X - start.X);
dy = yinc * (end.Y - start.Y);
var ax = start.X;
var ay = start.Y;
if (dx == dy) // Handle perfect diagonals
{
// I include this "optimization" for more aesthetic reasons, actually.
// While Bresenham's Line can handle perfect diagonals just fine, it adds
// additional cells to the line that make it not a perfect diagonal
// anymore. So, while this branch is ~twice as fast as the next branch,
// the real reason it is here is for style.
// Also, there *is* the reason of performance. If used for cell-based
// raycasts, for example, then perfect diagonals will check half as many
// cells.
while (dx --> 0)
{
ax += xinc;
ay += yinc;
if (!callback(new Vector2i(ax, ay)))
return;
}
return;
}
// Handle all other lines
side = -1 * ((dx == 0 ? yinc : xinc) - 1);
i = dx + dy;
error = dx - dy;
dx *= 2;
dy *= 2;
while (i --> 0)
{
if (error > 0 || error == side)
{
ax += xinc;
error -= dy;
}
else
{
ay += yinc;
error += dx;
}
if (!callback(new Vector2i(ax, ay)))
return;
}
}
public delegate bool Vector2iCallback(Vector2i index);
}

View File

@@ -0,0 +1,154 @@
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
/// <summary>
/// Pathfinding args for a 1-1 path.
/// </summary>
public record struct SimplePathArgs()
{
public Vector2i Start;
public Vector2i End;
public bool Diagonals = false;
public int Limit = 10000;
/// <summary>
/// Custom tile-costs if applicable.
/// </summary>
public Func<Vector2i, float>? TileCost;
}
public record struct SimplePathResult
{
public static SimplePathResult NoPath = new();
public List<Vector2i> Path;
public Dictionary<Vector2i, Vector2i> CameFrom;
}
/// <summary>
/// Gets simple A* path from start to end. Can also supply an optional tile-cost for tiles.
/// </summary>
public SimplePathResult GetPath(SimplePathArgs args)
{
var cameFrom = new Dictionary<Vector2i, Vector2i>();
var costSoFar = new Dictionary<Vector2i, float>();
var frontier = new PriorityQueue<Vector2i, float>();
costSoFar[args.Start] = 0f;
frontier.Enqueue(args.Start, 0f);
var count = 0;
while (frontier.TryDequeue(out var node, out _) && count < args.Limit)
{
count++;
if (node == args.End)
{
// Found target
var path = ReconstructPath(args.End, cameFrom);
return new SimplePathResult()
{
CameFrom = cameFrom,
Path = path,
};
}
var gCost = costSoFar[node];
if (args.Diagonals)
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
var neighbor = node + new Vector2i(x, y);
var neighborCost = OctileDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
if (neighborCost.Equals(0f))
{
continue;
}
// f = g + h
// gScore is distance to the start node
// hScore is distance to the end node
var gScore = gCost + neighborCost;
// Slower to get here so just ignore it.
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
{
continue;
}
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
// pFactor is tie-breaker where the fscore is otherwise equal.
// See http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html#breaking-ties
// There's other ways to do it but future consideration
// The closer the fScore is to the actual distance then the better the pathfinder will be
// (i.e. somewhere between 1 and infinite)
// Can use hierarchical pathfinder or whatever to improve the heuristic but this is fine for now.
var hScore = OctileDistance(args.End, neighbor) * (1.0f + 1.0f / 1000.0f);
var fScore = gScore + hScore;
frontier.Enqueue(neighbor, fScore);
}
}
}
else
{
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (x != 0 && y != 0)
continue;
var neighbor = node + new Vector2i(x, y);
var neighborCost = ManhattanDistance(node, neighbor) * args.TileCost?.Invoke(neighbor) ?? 1f;
if (neighborCost.Equals(0f))
continue;
var gScore = gCost + neighborCost;
if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
continue;
cameFrom[neighbor] = node;
costSoFar[neighbor] = gScore;
// Still use octile even for manhattan distance.
var hScore = OctileDistance(args.End, neighbor) * 1.001f;
var fScore = gScore + hScore;
frontier.Enqueue(neighbor, fScore);
}
}
}
}
return SimplePathResult.NoPath;
}
private List<Vector2i> ReconstructPath(Vector2i end, Dictionary<Vector2i, Vector2i> cameFrom)
{
var path = new List<Vector2i>()
{
end,
};
var node = end;
while (cameFrom.TryGetValue(node, out var source))
{
path.Add(source);
node = source;
}
path.Reverse();
return path;
}
}

View File

@@ -0,0 +1,180 @@
using Robust.Shared.Collections;
using Robust.Shared.Random;
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
public record struct SimplifyPathArgs
{
public Vector2i Start;
public Vector2i End;
public List<Vector2i> Path;
}
public record struct SplinePathResult()
{
public static SplinePathResult NoPath = new();
public List<Vector2i> Points = new();
public List<Vector2i> Path = new();
public Dictionary<Vector2i, Vector2i> CameFrom;
}
public record struct SplinePathArgs(SimplePathArgs Args)
{
public SimplePathArgs Args = Args;
public float MaxRatio = 0.25f;
/// <summary>
/// Minimum distance between subdivisions.
/// </summary>
public int Distance = 20;
}
/// <summary>
/// Gets a spline path from start to end.
/// </summary>
public SplinePathResult GetSplinePath(SplinePathArgs args, Random random)
{
var start = args.Args.Start;
var end = args.Args.End;
var path = new List<Vector2i>();
var pairs = new ValueList<(Vector2i Start, Vector2i End)> { (start, end) };
var subdivided = true;
// Sub-divide recursively
while (subdivided)
{
// Sometimes we might inadvertantly get 2 nodes too close together so better to just check each one as it comes up instead.
var i = 0;
subdivided = false;
while (i < pairs.Count)
{
var pointA = pairs[i].Start;
var pointB = pairs[i].End;
var vector = pointB - pointA;
var halfway = vector / 2f;
// Finding the point
var adj = halfway.Length();
// Should we even subdivide.
if (adj <= args.Distance)
{
// Just check the next entry no double skip.
i++;
continue;
}
subdivided = true;
var opposite = args.MaxRatio * adj;
var hypotenuse = MathF.Sqrt(MathF.Pow(adj, 2) + MathF.Pow(opposite, 2));
// Okay so essentially we have 2 points and no poly
// We add 2 other points to form a diamond and want some point halfway between randomly offset.
var angle = new Angle(MathF.Atan(opposite / adj));
var pointAPerp = pointA + angle.RotateVec(halfway).Normalized() * hypotenuse;
var pointBPerp = pointA + (-angle).RotateVec(halfway).Normalized() * hypotenuse;
var perpLine = pointBPerp - pointAPerp;
var perpHalfway = perpLine.Length() / 2f;
var splinePoint = (pointAPerp + perpLine.Normalized() * random.NextFloat(-args.MaxRatio, args.MaxRatio) * perpHalfway).Floored();
// We essentially take (A, B) and turn it into (A, C) & (C, B)
pairs[i] = (pointA, splinePoint);
pairs.Insert(i + 1, (splinePoint, pointB));
i+= 2;
}
}
var spline = new ValueList<Vector2i>(pairs.Count - 1)
{
start
};
foreach (var pair in pairs)
{
spline.Add(pair.End);
}
// Now we need to pathfind between each node on the spline.
// TODO: Add rotation version or straight-line version for pathfinder config
// Move the worm pathfinder to here I think.
var cameFrom = new Dictionary<Vector2i, Vector2i>();
// TODO: Need to get rid of the branch bullshit.
var points = new List<Vector2i>();
for (var i = 0; i < spline.Count - 1; i++)
{
var point = spline[i];
var target = spline[i + 1];
points.Add(point);
var aStarArgs = args.Args with { Start = point, End = target };
var aStarResult = GetPath(aStarArgs);
if (aStarResult == SimplePathResult.NoPath)
return SplinePathResult.NoPath;
path.AddRange(aStarResult.Path[0..]);
foreach (var a in aStarResult.CameFrom)
{
cameFrom[a.Key] = a.Value;
}
}
points.Add(spline[^1]);
var simple = SimplifyPath(new SimplifyPathArgs()
{
Start = args.Args.Start,
End = args.Args.End,
Path = path,
});
return new SplinePathResult()
{
Path = simple,
CameFrom = cameFrom,
Points = points,
};
}
/// <summary>
/// Does a simpler pathfinder over the nodes to prune unnecessary branches.
/// </summary>
public List<Vector2i> SimplifyPath(SimplifyPathArgs args)
{
var nodes = new HashSet<Vector2i>(args.Path);
var result = GetBreadthPath(new BreadthPathArgs()
{
Start = args.Start,
Ends = new List<Vector2i>()
{
args.End,
},
TileCost = node =>
{
if (!nodes.Contains(node))
return 0f;
return 1f;
}
});
return result.Path;
}
}

View File

@@ -0,0 +1,89 @@
using System.Numerics;
using Robust.Shared.Random;
namespace Content.Server.NPC.Pathfinding;
public sealed partial class PathfindingSystem
{
/// <summary>
/// Widens the path by the specified amount.
/// </summary>
public HashSet<Vector2i> GetWiden(WidenArgs args, Random random)
{
var tiles = new HashSet<Vector2i>(args.Path.Count * 2);
var variance = (args.MaxWiden - args.MinWiden) / 2f + args.MinWiden;
var counter = 0;
foreach (var tile in args.Path)
{
counter++;
if (counter != args.TileSkip)
continue;
counter = 0;
var center = new Vector2(tile.X + 0.5f, tile.Y + 0.5f);
if (args.Square)
{
for (var x = -variance; x <= variance; x++)
{
for (var y = -variance; y <= variance; y++)
{
var neighbor = center + new Vector2(x, y);
tiles.Add(neighbor.Floored());
}
}
}
else
{
for (var x = -variance; x <= variance; x++)
{
for (var y = -variance; y <= variance; y++)
{
var offset = new Vector2(x, y);
if (offset.Length() > variance)
continue;
var neighbor = center + offset;
tiles.Add(neighbor.Floored());
}
}
}
variance += random.NextFloat(-args.Variance * args.TileSkip, args.Variance * args.TileSkip);
variance = Math.Clamp(variance, args.MinWiden, args.MaxWiden);
}
return tiles;
}
public record struct WidenArgs()
{
public bool Square = false;
/// <summary>
/// How many tiles to skip between iterations., 1-in-n
/// </summary>
public int TileSkip = 3;
/// <summary>
/// Maximum amount to vary per tile.
/// </summary>
public float Variance = 0.25f;
/// <summary>
/// Minimum width.
/// </summary>
public float MinWiden = 2f;
public float MaxWiden = 7f;
public List<Vector2i> Path;
}
}

View File

@@ -142,6 +142,13 @@ public sealed partial class NPCSteeringSystem
// Grab the target position, either the next path node or our end goal..
var targetCoordinates = GetTargetCoordinates(steering);
if (!targetCoordinates.IsValid(EntityManager))
{
steering.Status = SteeringStatus.NoPath;
return false;
}
var needsPath = false;
// If the next node is invalid then get new ones
@@ -243,6 +250,14 @@ public sealed partial class NPCSteeringSystem
// Alright just adjust slightly and grab the next node so we don't stop moving for a tick.
// TODO: If it's the last node just grab the target instead.
targetCoordinates = GetTargetCoordinates(steering);
if (!targetCoordinates.IsValid(EntityManager))
{
SetDirection(mover, steering, Vector2.Zero);
steering.Status = SteeringStatus.NoPath;
return false;
}
targetMap = targetCoordinates.ToMap(EntityManager, _transform);
// Can't make it again.