DungeonFlow.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. using DunGen.Tags;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using UnityEngine;
  6. using UnityEngine.Serialization;
  7. namespace DunGen.Graph
  8. {
  9. /// <summary>
  10. /// A graph representing the flow of a dungeon
  11. /// </summary>
  12. [Serializable]
  13. [CreateAssetMenu(fileName = "New Dungeon", menuName = "DunGen/Dungeon Flow", order = 700)]
  14. public class DungeonFlow : ScriptableObject, ISerializationCallbackReceiver
  15. {
  16. public const int FileVersion = 1;
  17. #region Nested Types
  18. [Serializable]
  19. public sealed class GlobalPropSettings
  20. {
  21. public int ID;
  22. public IntRange Count;
  23. public GlobalPropSettings()
  24. {
  25. ID = 0;
  26. Count = new IntRange(0, 1);
  27. }
  28. public GlobalPropSettings(int id, IntRange count)
  29. {
  30. ID = id;
  31. Count = count;
  32. }
  33. }
  34. /// <summary>
  35. /// Defines how tile connection rules are applied.
  36. /// </summary>
  37. public enum TagConnectionMode
  38. {
  39. /// <summary>
  40. /// Tiles will only connect if they have tags that appear in the TileConnectionTags list
  41. /// </summary>
  42. Accept,
  43. /// <summary>
  44. /// Tiles will always connect unless they have tags that appear in the TileConnectionTags list
  45. /// </summary>
  46. Reject,
  47. }
  48. /// <summary>
  49. /// Defines how tags are used to determine which branch tip tiles to prune
  50. /// </summary>
  51. public enum BranchPruneMode
  52. {
  53. /// <summary>
  54. /// Removes tiles at the end of a branch when they have any of the specified tags
  55. /// </summary>
  56. AnyTagPresent,
  57. /// <summary>
  58. /// Removes tiles at the end of a branch when they don't have any of the specified tags
  59. /// </summary>
  60. AllTagsMissing,
  61. }
  62. #endregion
  63. #region Legacy Properties
  64. [SerializeField]
  65. [FormerlySerializedAs("GlobalPropGroupIDs")]
  66. private List<int> globalPropGroupID_obsolete = new List<int>();
  67. [SerializeField]
  68. [FormerlySerializedAs("GlobalPropRanges")]
  69. private List<IntRange> globalPropRanges_obsolete = new List<IntRange>();
  70. #endregion
  71. /// <summary>
  72. /// The minimum and maximum length of the dungeon
  73. /// </summary>
  74. public IntRange Length = new IntRange(5, 10);
  75. /// <summary>
  76. /// Determines how the number of branches from the main path is calculated
  77. /// </summary>
  78. public BranchMode BranchMode = BranchMode.Local;
  79. /// <summary>
  80. /// The number of branches to appear across the entire dungeon
  81. /// Only used if <see cref="BranchMode"/> is set to <see cref="BranchMode.Global"/>
  82. /// </summary>
  83. public IntRange BranchCount = new IntRange(1, 5);
  84. /// <summary>
  85. /// Information about which (and how many) global props should appear throughout the dungeon
  86. /// </summary>
  87. public List<GlobalPropSettings> GlobalProps = new List<GlobalPropSettings>();
  88. /// <summary>
  89. /// The asset that handles all of the keys that this dungeon needs to know about
  90. /// </summary>
  91. public KeyManager KeyManager = null;
  92. /// <summary>
  93. /// The percentage chance of two unconnected but overlapping doorways being connected (0-1)
  94. /// </summary>
  95. [Range(0f, 1f)]
  96. public float DoorwayConnectionChance = 0f;
  97. /// <summary>
  98. /// If true, only doorways belonging to tiles on the same section of the dungeon can be connected
  99. /// This will prevent some unexpected shortcuts from opening up through the dungeon
  100. /// </summary>
  101. public bool RestrictConnectionToSameSection = false;
  102. /// <summary>
  103. /// Simple rules for injecting special tiles into the dungeon generation process
  104. /// </summary>
  105. public List<TileInjectionRule> TileInjectionRules = new List<TileInjectionRule>();
  106. /// <summary>
  107. /// Defined how tile connection rules are applied see <see cref="TagConnectionMode"/>
  108. /// </summary>
  109. public TagConnectionMode TileTagConnectionMode;
  110. /// <summary>
  111. /// A list of tag pairs that define how tiles are allowed to connect. If empty, all
  112. /// tiles can connect to oneanother, otherwise the tiles must have a pair of
  113. /// matching tags from this list (omni-directional)
  114. /// </summary>
  115. public List<TagPair> TileConnectionTags = new List<TagPair>();
  116. /// <summary>
  117. /// Determines how tags are used to prune tiles at the end of branches
  118. /// </summary>
  119. public BranchPruneMode BranchTagPruneMode = BranchPruneMode.AllTagsMissing;
  120. /// <summary>
  121. /// A list of tags used to decide if a tile at the end of a branch should be deleted
  122. /// </summary>
  123. public List<Tag> BranchPruneTags = new List<Tag>();
  124. /// <summary>
  125. /// Global settings for straightening the dungeon path. Can be overridden in Archetypes and on Nodes in the flow graph
  126. /// </summary>
  127. public PathStraighteningSettings GlobalStraighteningSettings = new PathStraighteningSettings();
  128. public List<GraphNode> Nodes = new List<GraphNode>();
  129. public List<GraphLine> Lines = new List<GraphLine>();
  130. [SerializeField]
  131. private int currentFileVersion;
  132. /// <summary>
  133. /// Creates the default graph
  134. /// </summary>
  135. public void Reset()
  136. {
  137. var emptyTileSet = new TileSet[0];
  138. var emptyArchetype = new DungeonArchetype[0];
  139. var builder = new DungeonFlowBuilder(this)
  140. .AddNode(emptyTileSet, "Start")
  141. .AddLine(emptyArchetype, 1.0f)
  142. .AddNode(emptyTileSet, "Goal");
  143. builder.Complete();
  144. }
  145. public GraphLine GetLineAtDepth(float normalizedDepth)
  146. {
  147. normalizedDepth = Mathf.Clamp(normalizedDepth, 0, 1);
  148. if (normalizedDepth == 0)
  149. return Lines[0];
  150. else if (normalizedDepth == 1)
  151. return Lines[Lines.Count - 1];
  152. foreach (var line in Lines)
  153. if (normalizedDepth >= line.Position && normalizedDepth < line.Position + line.Length)
  154. return line;
  155. Debug.LogError("GetLineAtDepth was unable to find a line at depth " + normalizedDepth + ". This shouldn't happen.");
  156. return null;
  157. }
  158. public DungeonArchetype[] GetUsedArchetypes()
  159. {
  160. return Lines.SelectMany(x => x.DungeonArchetypes).ToArray();
  161. }
  162. public TileSet[] GetUsedTileSets()
  163. {
  164. List<TileSet> tileSets = new List<TileSet>();
  165. foreach (var node in Nodes)
  166. tileSets.AddRange(node.TileSets);
  167. foreach(var line in Lines)
  168. foreach (var archetype in line.DungeonArchetypes)
  169. {
  170. tileSets.AddRange(archetype.TileSets);
  171. tileSets.AddRange(archetype.BranchCapTileSets);
  172. }
  173. return tileSets.ToArray();
  174. }
  175. public bool ShouldPruneTileWithTags(TagContainer tileTags)
  176. {
  177. switch (BranchTagPruneMode)
  178. {
  179. case BranchPruneMode.AnyTagPresent:
  180. return tileTags.HasAnyTag(BranchPruneTags.ToArray());
  181. case BranchPruneMode.AllTagsMissing:
  182. return !tileTags.HasAnyTag(BranchPruneTags.ToArray());
  183. default:
  184. throw new NotImplementedException(string.Format("BranchPruneMode {0} is not implemented", BranchTagPruneMode));
  185. }
  186. }
  187. public void OnBeforeSerialize()
  188. {
  189. currentFileVersion = FileVersion;
  190. }
  191. public void OnAfterDeserialize()
  192. {
  193. // Convert to new format for Global Props
  194. if(currentFileVersion < 1)
  195. {
  196. for (int i = 0; i < globalPropGroupID_obsolete.Count; i++)
  197. {
  198. int id = globalPropGroupID_obsolete[i];
  199. var count = globalPropRanges_obsolete[i];
  200. GlobalProps.Add(new GlobalPropSettings(id, count));
  201. }
  202. globalPropGroupID_obsolete.Clear();
  203. globalPropRanges_obsolete.Clear();
  204. }
  205. }
  206. /// <summary>
  207. /// Checks the connection rules (if any) to see if two tiles are allowed
  208. /// to connect by checking their tags
  209. /// </summary>
  210. /// <param name="tileA">The first tile</param>
  211. /// <param name="tileB">The second tile</param>
  212. /// <returns>True if the tiles are allowed to connect</returns>
  213. public bool CanTilesConnect(Tile tileA, Tile tileB)
  214. {
  215. if (TileConnectionTags.Count == 0)
  216. return true;
  217. switch (TileTagConnectionMode)
  218. {
  219. case TagConnectionMode.Accept:
  220. return HasMatchingTagPair(tileA, tileB);
  221. case TagConnectionMode.Reject:
  222. return !HasMatchingTagPair(tileA, tileB);
  223. default:
  224. throw new NotImplementedException(string.Format("{0}.{1} is not implemented", typeof(TagConnectionMode).Name, TileTagConnectionMode));
  225. }
  226. }
  227. public bool CanDoorwaysConnect(ProposedConnection connection)
  228. {
  229. foreach (var rule in DoorwayPairFinder.CustomConnectionRules)
  230. {
  231. TileConnectionRule.ConnectionResult result = TileConnectionRule.ConnectionResult.Passthrough;
  232. if (rule.ConnectionDelegate != null)
  233. result = rule.ConnectionDelegate(connection);
  234. #pragma warning disable CS0618 // For now, we allow rules that haven't been updated to the new delegate format
  235. else if (rule.Delegate != null)
  236. result = rule.Delegate(connection.PreviousTile.PrefabTile, connection.NextTile.PrefabTile, connection.PreviousDoorway.DoorwayComponent, connection.NextDoorway.DoorwayComponent);
  237. #pragma warning restore
  238. if (result == TileConnectionRule.ConnectionResult.Passthrough)
  239. continue;
  240. else
  241. return result == TileConnectionRule.ConnectionResult.Allow;
  242. }
  243. // No custom rules handled this connection, use default behaviour
  244. return DoorwaySocket.CanSocketsConnect(connection.PreviousDoorway.DoorwayComponent.Socket, connection.NextDoorway.DoorwayComponent.Socket) && CanTilesConnect(connection.PreviousTile.PrefabTile, connection.NextTile.PrefabTile);
  245. }
  246. private bool HasMatchingTagPair(Tile tileA, Tile tileB)
  247. {
  248. foreach(var tagPair in TileConnectionTags)
  249. {
  250. if ((tileA.Tags.HasTag(tagPair.TagA) && tileB.Tags.HasTag(tagPair.TagB)) ||
  251. (tileB.Tags.HasTag(tagPair.TagA) && tileA.Tags.HasTag(tagPair.TagB)))
  252. return true;
  253. }
  254. return false;
  255. }
  256. }
  257. }