1
0

DungeonSystem.Helpers.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. using Content.Shared.NPC;
  2. using Robust.Shared.Collections;
  3. using Robust.Shared.Utility;
  4. namespace Content.Server.Procedural;
  5. public sealed partial class DungeonSystem
  6. {
  7. public List<(Vector2i Start, Vector2i End)> MinimumSpanningTree(List<Vector2i> tiles, System.Random random)
  8. {
  9. // Generate connections between all rooms.
  10. var connections = new Dictionary<Vector2i, List<(Vector2i Tile, float Distance)>>(tiles.Count);
  11. foreach (var entrance in tiles)
  12. {
  13. var edgeConns = new List<(Vector2i Tile, float Distance)>(tiles.Count - 1);
  14. foreach (var other in tiles)
  15. {
  16. if (entrance == other)
  17. continue;
  18. edgeConns.Add((other, (other - entrance).Length));
  19. }
  20. // Sort these as they will be iterated many times.
  21. edgeConns.Sort((x, y) => x.Distance.CompareTo(y.Distance));
  22. connections.Add(entrance, edgeConns);
  23. }
  24. var seedIndex = random.Next(tiles.Count);
  25. var remaining = new ValueList<Vector2i>(tiles);
  26. remaining.RemoveAt(seedIndex);
  27. var edges = new List<(Vector2i Start, Vector2i End)>();
  28. var seedEntrance = tiles[seedIndex];
  29. var forest = new ValueList<Vector2i>(tiles.Count) { seedEntrance };
  30. while (remaining.Count > 0)
  31. {
  32. // Get cheapest edge
  33. var cheapestDistance = float.MaxValue;
  34. var cheapest = (Vector2i.Zero, Vector2i.Zero);
  35. foreach (var node in forest)
  36. {
  37. foreach (var conn in connections[node])
  38. {
  39. // Existing tile, skip
  40. if (forest.Contains(conn.Tile))
  41. continue;
  42. // Not the cheapest
  43. if (cheapestDistance < conn.Distance)
  44. continue;
  45. cheapestDistance = conn.Distance;
  46. cheapest = (node, conn.Tile);
  47. // List is pre-sorted so we can just breakout easily.
  48. break;
  49. }
  50. }
  51. DebugTools.Assert(cheapestDistance < float.MaxValue);
  52. // Add to tree
  53. edges.Add(cheapest);
  54. forest.Add(cheapest.Item2);
  55. remaining.Remove(cheapest.Item2);
  56. }
  57. return edges;
  58. }
  59. /// <summary>
  60. /// Primarily for dungeon usage.
  61. /// </summary>
  62. public void GetCorridorNodes(HashSet<Vector2i> corridorTiles,
  63. List<(Vector2i Start, Vector2i End)> edges,
  64. int pathLimit,
  65. HashSet<Vector2i>? forbiddenTiles = null,
  66. Func<Vector2i, float>? tileCallback = null)
  67. {
  68. // Pathfind each entrance
  69. var frontier = new PriorityQueue<Vector2i, float>();
  70. var cameFrom = new Dictionary<Vector2i, Vector2i>();
  71. var directions = new Dictionary<Vector2i, Direction>();
  72. var costSoFar = new Dictionary<Vector2i, float>();
  73. forbiddenTiles ??= new HashSet<Vector2i>();
  74. foreach (var (start, end) in edges)
  75. {
  76. frontier.Clear();
  77. cameFrom.Clear();
  78. costSoFar.Clear();
  79. directions.Clear();
  80. directions[start] = Direction.Invalid;
  81. frontier.Enqueue(start, 0f);
  82. costSoFar[start] = 0f;
  83. var found = false;
  84. var count = 0;
  85. while (frontier.Count > 0 && count < pathLimit)
  86. {
  87. count++;
  88. var node = frontier.Dequeue();
  89. if (node == end)
  90. {
  91. found = true;
  92. break;
  93. }
  94. var lastDirection = directions[node];
  95. // Foreach neighbor etc etc
  96. for (var x = -1; x <= 1; x++)
  97. {
  98. for (var y = -1; y <= 1; y++)
  99. {
  100. // Cardinals only.
  101. if (x != 0 && y != 0)
  102. continue;
  103. var neighbor = new Vector2i(node.X + x, node.Y + y);
  104. // FORBIDDEN
  105. if (neighbor != end &&
  106. forbiddenTiles.Contains(neighbor))
  107. {
  108. continue;
  109. }
  110. var tileCost = SharedPathfindingSystem.ManhattanDistance(node, neighbor);
  111. // Weight towards existing corridors ig
  112. if (corridorTiles.Contains(neighbor))
  113. {
  114. tileCost *= 0.10f;
  115. }
  116. var costMod = tileCallback?.Invoke(neighbor);
  117. costMod ??= 1f;
  118. tileCost *= costMod.Value;
  119. var direction = (neighbor - node).GetCardinalDir();
  120. directions[neighbor] = direction;
  121. // If direction is different then penalise it.
  122. if (direction != lastDirection)
  123. {
  124. tileCost *= 3f;
  125. }
  126. // f = g + h
  127. // gScore is distance to the start node
  128. // hScore is distance to the end node
  129. var gScore = costSoFar[node] + tileCost;
  130. if (costSoFar.TryGetValue(neighbor, out var nextValue) && gScore >= nextValue)
  131. {
  132. continue;
  133. }
  134. cameFrom[neighbor] = node;
  135. costSoFar[neighbor] = gScore;
  136. // Make it greedy so multiply h-score to punish further nodes.
  137. // This is necessary as we might have the deterredTiles multiplying towards the end
  138. // so just finish it.
  139. var hScore = SharedPathfindingSystem.ManhattanDistance(end, neighbor) * (1.0f - 1.0f / 1000.0f);
  140. var fScore = gScore + hScore;
  141. frontier.Enqueue(neighbor, fScore);
  142. }
  143. }
  144. }
  145. // Rebuild path if it's valid.
  146. if (found)
  147. {
  148. var node = end;
  149. while (true)
  150. {
  151. node = cameFrom[node];
  152. // Don't want start or end nodes included.
  153. if (node == start)
  154. break;
  155. corridorTiles.Add(node);
  156. }
  157. }
  158. }
  159. }
  160. }