DungeonGenerator.cs 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535
  1. using DunGen.Collision;
  2. using DunGen.Generation;
  3. using DunGen.Graph;
  4. using DunGen.LockAndKey;
  5. using DunGen.Pooling;
  6. using System;
  7. using System.Collections;
  8. using System.Collections.Generic;
  9. using System.Diagnostics;
  10. using System.Linq;
  11. using UnityEngine;
  12. using UnityEngine.Serialization;
  13. using Debug = UnityEngine.Debug;
  14. namespace DunGen
  15. {
  16. public delegate void TileInjectionDelegate(RandomStream randomStream, ref List<InjectedTile> tilesToInject);
  17. public delegate void GenerationFailureReportProduced(DungeonGenerator generator, GenerationFailureReport report);
  18. public enum AxisDirection
  19. {
  20. [InspectorName("+X")]
  21. PosX,
  22. [InspectorName("-X")]
  23. NegX,
  24. [InspectorName("+Y")]
  25. PosY,
  26. [InspectorName("-Y")]
  27. NegY,
  28. [InspectorName("+Z")]
  29. PosZ,
  30. [InspectorName("-Z")]
  31. NegZ,
  32. }
  33. public enum TriggerPlacementMode
  34. {
  35. [InspectorName("None")]
  36. None,
  37. [InspectorName("3D")]
  38. ThreeDimensional,
  39. [InspectorName("2D")]
  40. TwoDimensional,
  41. }
  42. [Serializable]
  43. public class DungeonGenerator : ISerializationCallbackReceiver
  44. {
  45. public const int CurrentFileVersion = 3;
  46. #region Legacy Properties
  47. // Legacy properties only exist to avoid breaking existing projects
  48. // Converting old data structures over to the new ones
  49. [SerializeField]
  50. [FormerlySerializedAs("AllowImmediateRepeats")]
  51. private bool allowImmediateRepeats = false;
  52. [Obsolete("Use the 'CollisionSettings' property instead")]
  53. public float OverlapThreshold = 0.01f;
  54. [Obsolete("Use the 'CollisionSettings' property instead")]
  55. public float Padding = 0f;
  56. [Obsolete("Use the 'CollisionSettings' property instead")]
  57. public bool DisallowOverhangs = false;
  58. [Obsolete("Use the 'CollisionSettings' property instead")]
  59. public bool AvoidCollisionsWithOtherDungeons = true;
  60. [Obsolete("Use the 'CollisionSettings' property instead")]
  61. public readonly List<Bounds> AdditionalCollisionBounds = new List<Bounds>();
  62. [Obsolete("Use the 'CollisionSettings' property instead")]
  63. public AdditionalCollisionsPredicate AdditionalCollisionsPredicate { get; set; }
  64. [Obsolete("Use the 'TriggerPlacement' enum property instead")]
  65. public bool PlaceTileTriggers = true;
  66. #endregion
  67. #region Helper Struct
  68. struct PropProcessingData
  69. {
  70. public RandomProp PropComponent;
  71. public int HierarchyDepth;
  72. public Tile OwningTile;
  73. }
  74. #endregion
  75. public int Seed;
  76. public bool ShouldRandomizeSeed = true;
  77. public RandomStream RandomStream { get; protected set; }
  78. public int MaxAttemptCount = 20;
  79. public bool UseMaximumPairingAttempts = false;
  80. public int MaxPairingAttempts = 5;
  81. public AxisDirection UpDirection = AxisDirection.PosY;
  82. [FormerlySerializedAs("OverrideAllowImmediateRepeats")]
  83. public bool OverrideRepeatMode = false;
  84. public TileRepeatMode RepeatMode = TileRepeatMode.Allow;
  85. public bool OverrideAllowTileRotation = false;
  86. public bool AllowTileRotation = false;
  87. public bool DebugRender = false;
  88. public float LengthMultiplier = 1.0f;
  89. public TriggerPlacementMode TriggerPlacement = TriggerPlacementMode.ThreeDimensional;
  90. public int TileTriggerLayer = 2;
  91. public bool GenerateAsynchronously = false;
  92. public float MaxAsyncFrameMilliseconds = 10;
  93. public float PauseBetweenRooms = 0;
  94. public bool RestrictDungeonToBounds = false;
  95. public Bounds TilePlacementBounds = new Bounds(Vector3.zero, Vector3.one * 10f);
  96. public DungeonCollisionSettings CollisionSettings = new DungeonCollisionSettings();
  97. public Vector3 UpVector
  98. {
  99. get
  100. {
  101. return UpDirection switch
  102. {
  103. AxisDirection.PosX => new Vector3(+1, 0, 0),
  104. AxisDirection.NegX => new Vector3(-1, 0, 0),
  105. AxisDirection.PosY => new Vector3(0, +1, 0),
  106. AxisDirection.NegY => new Vector3(0, -1, 0),
  107. AxisDirection.PosZ => new Vector3(0, 0, +1),
  108. AxisDirection.NegZ => new Vector3(0, 0, -1),
  109. _ => throw new NotImplementedException("AxisDirection '" + UpDirection + "' not implemented"),
  110. };
  111. }
  112. }
  113. public event GenerationStatusDelegate OnGenerationStatusChanged;
  114. public event DungeonGenerationDelegate OnGenerationStarted;
  115. public event DungeonGenerationDelegate OnGenerationComplete;
  116. public static event GenerationStatusDelegate OnAnyDungeonGenerationStatusChanged;
  117. public static event DungeonGenerationDelegate OnAnyDungeonGenerationStarted;
  118. public static event DungeonGenerationDelegate OnAnyDungeonGenerationComplete;
  119. public event TileInjectionDelegate TileInjectionMethods;
  120. public event Action Cleared;
  121. public event Action Retrying;
  122. public static event GenerationFailureReportProduced OnGenerationFailureReportProduced;
  123. public GameObject Root;
  124. public DungeonFlow DungeonFlow;
  125. public GenerationStatus Status { get; private set; }
  126. public GenerationStats GenerationStats { get; private set; }
  127. public int ChosenSeed { get; protected set; }
  128. public Dungeon CurrentDungeon { get; private set; }
  129. public bool IsGenerating { get; private set; }
  130. public bool IsAnalysis { get; set; }
  131. public bool AllowTilePooling { get; set; }
  132. /// <summary>
  133. /// Settings for generating the new dungeon as an attachment to a previous dungeon
  134. /// </summary>
  135. public DungeonAttachmentSettings AttachmentSettings { get; set; }
  136. public DungeonCollisionManager CollisionManager { get; private set; }
  137. protected int retryCount;
  138. protected DungeonProxy proxyDungeon;
  139. protected readonly List<TilePlacementResult> tilePlacementResults = new List<TilePlacementResult>();
  140. protected readonly List<GameObject> useableTiles = new List<GameObject>();
  141. protected int targetLength;
  142. protected List<InjectedTile> tilesPendingInjection;
  143. protected List<DungeonGeneratorPostProcessStep> postProcessSteps = new List<DungeonGeneratorPostProcessStep>();
  144. [SerializeField]
  145. private int fileVersion;
  146. private int nextNodeIndex;
  147. private DungeonArchetype currentArchetype;
  148. private GraphLine previousLineSegment;
  149. private readonly Dictionary<GameObject, TileProxy> preProcessData = new Dictionary<GameObject, TileProxy>();
  150. private readonly Dictionary<TileProxy, InjectedTile> injectedTiles = new Dictionary<TileProxy, InjectedTile>();
  151. private readonly Stopwatch yieldTimer = new Stopwatch();
  152. private readonly BucketedObjectPool<TileProxy, TileProxy> tileProxyPool;
  153. private readonly TileInstanceSource tileInstanceSource;
  154. public DungeonGenerator()
  155. {
  156. AllowTilePooling = true;
  157. GenerationStats = new GenerationStats();
  158. CollisionManager = new DungeonCollisionManager();
  159. tileProxyPool = new BucketedObjectPool<TileProxy, TileProxy>(
  160. objectFactory: template => new TileProxy(template),
  161. takeAction: x =>
  162. {
  163. x.Placement.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
  164. }
  165. );
  166. tileInstanceSource = new TileInstanceSource();
  167. tileInstanceSource.TileInstanceSpawned += (tilePrefab, tileInstance, fromPool) =>
  168. {
  169. GenerationStats.TileAdded(tilePrefab, fromPool);
  170. };
  171. }
  172. public DungeonGenerator(GameObject root)
  173. : this()
  174. {
  175. Root = root;
  176. }
  177. public void Generate()
  178. {
  179. if (IsGenerating)
  180. return;
  181. OnGenerationStarted?.Invoke(this);
  182. OnAnyDungeonGenerationStarted?.Invoke(this);
  183. // Detach the previous dungeon if we're generating the new one as an attachment
  184. // We need to do this to avoid overwriting the previous dungeon
  185. if (AttachmentSettings != null && CurrentDungeon != null)
  186. DetachDungeon();
  187. if(CollisionSettings == null)
  188. CollisionSettings = new DungeonCollisionSettings();
  189. if(CollisionManager == null)
  190. CollisionManager = new DungeonCollisionManager();
  191. CollisionManager.Settings = CollisionSettings;
  192. DoorwayPairFinder.SortCustomConnectionRules();
  193. IsAnalysis = false;
  194. IsGenerating = true;
  195. Wait(OuterGenerate());
  196. }
  197. public void Cancel()
  198. {
  199. if (!IsGenerating)
  200. return;
  201. Clear(true);
  202. IsGenerating = false;
  203. }
  204. public Dungeon DetachDungeon()
  205. {
  206. if (CurrentDungeon == null)
  207. return null;
  208. Dungeon dungeon = CurrentDungeon;
  209. CurrentDungeon = null;
  210. Root = null;
  211. Clear(true);
  212. // If the dungeon is empty, we should just destroy it
  213. if (dungeon.transform.childCount == 0)
  214. UnityEngine.Object.DestroyImmediate(dungeon.gameObject);
  215. return dungeon;
  216. }
  217. protected virtual IEnumerator OuterGenerate()
  218. {
  219. Clear(false);
  220. yieldTimer.Restart();
  221. Status = GenerationStatus.NotStarted;
  222. #if UNITY_EDITOR
  223. // Validate the dungeon archetype if we're running in the editor
  224. DungeonArchetypeValidator validator = new DungeonArchetypeValidator(DungeonFlow);
  225. if (!validator.IsValid())
  226. {
  227. ChangeStatus(GenerationStatus.Failed);
  228. IsGenerating = false;
  229. yield break;
  230. }
  231. #endif
  232. ChosenSeed = (ShouldRandomizeSeed) ? new RandomStream().Next() : Seed;
  233. RandomStream = new RandomStream(ChosenSeed);
  234. if (Root == null)
  235. Root = new GameObject(Constants.DefaultDungeonRootName);
  236. bool enableTilePooling = AllowTilePooling && DunGenSettings.Instance.EnableTilePooling;
  237. tileInstanceSource.Initialise(enableTilePooling, Root);
  238. yield return Wait(InnerGenerate(false));
  239. IsGenerating = false;
  240. }
  241. private Coroutine Wait(IEnumerator routine)
  242. {
  243. if (GenerateAsynchronously)
  244. return CoroutineHelper.Start(routine);
  245. else
  246. {
  247. while (routine.MoveNext()) { }
  248. return null;
  249. }
  250. }
  251. public void RandomizeSeed()
  252. {
  253. Seed = new RandomStream().Next();
  254. }
  255. protected virtual IEnumerator InnerGenerate(bool isRetry)
  256. {
  257. if (isRetry)
  258. {
  259. ChosenSeed = RandomStream.Next();
  260. RandomStream = new RandomStream(ChosenSeed);
  261. if (retryCount >= MaxAttemptCount && Application.isEditor)
  262. {
  263. Debug.LogError(TilePlacementResult.ProduceReport(tilePlacementResults, MaxAttemptCount));
  264. OnGenerationFailureReportProduced?.Invoke(this, new GenerationFailureReport(MaxAttemptCount, tilePlacementResults));
  265. ChangeStatus(GenerationStatus.Failed);
  266. yield break;
  267. }
  268. retryCount++;
  269. GenerationStats.IncrementRetryCount();
  270. Retrying?.Invoke();
  271. }
  272. else
  273. {
  274. retryCount = 0;
  275. GenerationStats.Clear();
  276. }
  277. CurrentDungeon = Root.GetComponent<Dungeon>();
  278. if (CurrentDungeon == null)
  279. CurrentDungeon = Root.AddComponent<Dungeon>();
  280. CollisionManager.Initialize(this);
  281. CurrentDungeon.DebugRender = DebugRender;
  282. CurrentDungeon.PreGenerateDungeon(this);
  283. Clear(false);
  284. targetLength = Mathf.RoundToInt(DungeonFlow.Length.GetRandom(RandomStream) * LengthMultiplier);
  285. targetLength = Mathf.Max(targetLength, 2);
  286. // PauseBetweenRooms is for debug purposes only, so we should disable it in regular builds to improve performance
  287. #if !UNITY_EDITOR
  288. PauseBetweenRooms = 0.0f;
  289. #endif
  290. Transform debugVisualsRoot = (PauseBetweenRooms > 0f) ? Root.transform : null;
  291. proxyDungeon = new DungeonProxy(debugVisualsRoot);
  292. // Tile Injection
  293. GenerationStats.BeginTime(GenerationStatus.TileInjection);
  294. ChangeStatus(GenerationStatus.TileInjection);
  295. if (tilesPendingInjection == null)
  296. tilesPendingInjection = new List<InjectedTile>();
  297. else
  298. tilesPendingInjection.Clear();
  299. injectedTiles.Clear();
  300. GatherTilesToInject();
  301. // Pre-Processing
  302. GenerationStats.BeginTime(GenerationStatus.PreProcessing);
  303. PreProcess();
  304. // Main Path Generation
  305. GenerationStats.BeginTime(GenerationStatus.MainPath);
  306. yield return Wait(GenerateMainPath());
  307. // We may have had to retry when generating the main path, if so, the status will be either Complete or Failed and we should exit here
  308. if (Status == GenerationStatus.Complete || Status == GenerationStatus.Failed)
  309. yield break;
  310. // Branch Paths Generation
  311. GenerationStats.BeginTime(GenerationStatus.Branching);
  312. yield return Wait(GenerateBranchPaths());
  313. // If there are any required tiles missing from the tile injection stage, the generation process should fail
  314. foreach (var tileInjection in tilesPendingInjection)
  315. if (tileInjection.IsRequired)
  316. {
  317. yield return Wait(InnerGenerate(true));
  318. yield break;
  319. }
  320. // We may have missed some required injected tiles and have had to retry, if so, the status will be either Complete or Failed and we should exit here
  321. if (Status == GenerationStatus.Complete || Status == GenerationStatus.Failed)
  322. yield break;
  323. GenerationStats.BeginTime(GenerationStatus.BranchPruning);
  324. ChangeStatus(GenerationStatus.BranchPruning);
  325. // Prune branches if we have any tags set up
  326. if (DungeonFlow.BranchPruneTags.Count > 0)
  327. PruneBranches();
  328. // Instantiate Tiles
  329. GenerationStats.BeginTime(GenerationStatus.InstantiatingTiles);
  330. ChangeStatus(GenerationStatus.InstantiatingTiles);
  331. proxyDungeon.ConnectOverlappingDoorways(DungeonFlow.DoorwayConnectionChance, DungeonFlow, RandomStream);
  332. yield return Wait(CurrentDungeon.FromProxy(proxyDungeon, this, tileInstanceSource, () => ShouldSkipFrame(false)));
  333. // Post-Processing
  334. yield return Wait(PostProcess());
  335. // Waiting one frame so objects are in their expected state
  336. yield return null;
  337. // Inform objects in the dungeon that generation is complete
  338. foreach (var callbackReceiver in CurrentDungeon.gameObject.GetComponentsInChildren<IDungeonCompleteReceiver>(false))
  339. callbackReceiver.OnDungeonComplete(CurrentDungeon);
  340. ChangeStatus(GenerationStatus.Complete);
  341. bool charactersShouldRecheckTile = true;
  342. #if UNITY_EDITOR
  343. charactersShouldRecheckTile = UnityEditor.EditorApplication.isPlaying;
  344. #endif
  345. // Let DungenCharacters know that they should re-check the Tile they're in
  346. if (charactersShouldRecheckTile)
  347. {
  348. foreach (var character in UnityUtil.FindObjectsByType<DungenCharacter>())
  349. character.ForceRecheckTile();
  350. }
  351. }
  352. private void PruneBranches()
  353. {
  354. var branchTips = new Stack<TileProxy>();
  355. foreach (var tile in proxyDungeon.BranchPathTiles)
  356. {
  357. var connectedTiles = tile.UsedDoorways.Select(d => d.ConnectedDoorway.TileProxy);
  358. // If we're not connected to another tile with a higher branch depth, this is a branch tip
  359. if (!connectedTiles.Any(t => t.Placement.BranchDepth > tile.Placement.BranchDepth))
  360. branchTips.Push(tile);
  361. }
  362. while (branchTips.Count > 0)
  363. {
  364. var tile = branchTips.Pop();
  365. bool isRequiredTile = tile.Placement.InjectionData != null && tile.Placement.InjectionData.IsRequired;
  366. bool shouldPruneTile = !isRequiredTile && DungeonFlow.ShouldPruneTileWithTags(tile.PrefabTile.Tags);
  367. if (shouldPruneTile)
  368. {
  369. // Find that tile that came before this one
  370. var precedingTileConnection = tile.UsedDoorways
  371. .Select(d => d.ConnectedDoorway)
  372. .Where(d => d.TileProxy.Placement.IsOnMainPath || d.TileProxy.Placement.BranchDepth < tile.Placement.BranchDepth)
  373. .Select(d => new ProxyDoorwayConnection(d, d.ConnectedDoorway))
  374. .First();
  375. // Remove tile and connection
  376. proxyDungeon.RemoveTile(tile);
  377. CollisionManager.RemoveTile(tile);
  378. proxyDungeon.RemoveConnection(precedingTileConnection);
  379. GenerationStats.PrunedBranchTileCount++;
  380. var precedingTile = precedingTileConnection.A.TileProxy;
  381. // The preceding tile is the new tip of this branch
  382. if (!precedingTile.Placement.IsOnMainPath)
  383. branchTips.Push(precedingTile);
  384. }
  385. }
  386. }
  387. public virtual void Clear(bool stopCoroutines)
  388. {
  389. if (stopCoroutines)
  390. CoroutineHelper.StopAll();
  391. if (proxyDungeon != null)
  392. proxyDungeon.ClearDebugVisuals();
  393. proxyDungeon = null;
  394. if (CurrentDungeon != null)
  395. CurrentDungeon.Clear(tileInstanceSource.DespawnTile);
  396. useableTiles.Clear();
  397. preProcessData.Clear();
  398. previousLineSegment = null;
  399. tilePlacementResults.Clear();
  400. Cleared?.Invoke();
  401. }
  402. private void ChangeStatus(GenerationStatus status)
  403. {
  404. var previousStatus = Status;
  405. Status = status;
  406. if (status == GenerationStatus.Complete || status == GenerationStatus.Failed)
  407. IsGenerating = false;
  408. if (status == GenerationStatus.Failed)
  409. Clear(true);
  410. if (previousStatus != status)
  411. {
  412. OnGenerationStatusChanged?.Invoke(this, status);
  413. OnAnyDungeonGenerationStatusChanged?.Invoke(this, status);
  414. if (status == GenerationStatus.Complete)
  415. {
  416. OnGenerationComplete?.Invoke(this);
  417. OnAnyDungeonGenerationComplete?.Invoke(this);
  418. }
  419. }
  420. }
  421. protected virtual void PreProcess()
  422. {
  423. if (preProcessData.Count > 0)
  424. return;
  425. ChangeStatus(GenerationStatus.PreProcessing);
  426. var usedTileSets = DungeonFlow.GetUsedTileSets().Concat(tilesPendingInjection.Select(x => x.TileSet)).Distinct();
  427. foreach (var tileSet in usedTileSets)
  428. foreach (var tile in tileSet.TileWeights.Weights)
  429. {
  430. if (tile.Value != null)
  431. {
  432. useableTiles.Add(tile.Value);
  433. tile.TileSet = tileSet;
  434. }
  435. }
  436. }
  437. protected virtual void GatherTilesToInject()
  438. {
  439. var injectionRandomStream = new RandomStream(ChosenSeed);
  440. // Gather from DungeonFlow
  441. foreach (var rule in DungeonFlow.TileInjectionRules)
  442. {
  443. // Ignore invalid rules
  444. if (rule.TileSet == null || (!rule.CanAppearOnMainPath && !rule.CanAppearOnBranchPath))
  445. continue;
  446. bool isOnMainPath = (!rule.CanAppearOnBranchPath) ? true : (!rule.CanAppearOnMainPath) ? false : injectionRandomStream.NextDouble() > 0.5;
  447. var injectedTile = new InjectedTile(rule, isOnMainPath, injectionRandomStream);
  448. tilesPendingInjection.Add(injectedTile);
  449. }
  450. // Gather from external delegates
  451. TileInjectionMethods?.Invoke(injectionRandomStream, ref tilesPendingInjection);
  452. }
  453. protected virtual IEnumerator GenerateMainPath()
  454. {
  455. ChangeStatus(GenerationStatus.MainPath);
  456. nextNodeIndex = 0;
  457. var handledNodes = new List<GraphNode>(DungeonFlow.Nodes.Count);
  458. bool isDone = false;
  459. int i = 0;
  460. // Keep track of these now, we'll need them later when we know the actual length of the dungeon
  461. var placementSlots = new List<TilePlacementParameters>(targetLength);
  462. var slotTileSets = new List<List<TileSet>>();
  463. // We can't rigidly stick to the target length since we need at least one room for each node and that might be more than targetLength
  464. while (!isDone)
  465. {
  466. float depth = Mathf.Clamp(i / (float)(targetLength - 1), 0, 1);
  467. GraphLine lineSegment = DungeonFlow.GetLineAtDepth(depth);
  468. // This should never happen
  469. if (lineSegment == null)
  470. {
  471. yield return Wait(InnerGenerate(true));
  472. yield break;
  473. }
  474. // We're on a new line segment, change the current archetype
  475. if (lineSegment != previousLineSegment)
  476. {
  477. currentArchetype = lineSegment.GetRandomArchetype(RandomStream, placementSlots.Select(x => x.Archetype));
  478. previousLineSegment = lineSegment;
  479. }
  480. List<TileSet> useableTileSets = null;
  481. GraphNode nextNode = null;
  482. var orderedNodes = DungeonFlow.Nodes.OrderBy(x => x.Position).ToArray();
  483. // Determine which node comes next
  484. foreach (var node in orderedNodes)
  485. {
  486. if (depth >= node.Position && !handledNodes.Contains(node))
  487. {
  488. nextNode = node;
  489. handledNodes.Add(node);
  490. break;
  491. }
  492. }
  493. var placementParams = new TilePlacementParameters();
  494. placementSlots.Add(placementParams);
  495. // Assign the TileSets to use based on whether we're on a node or a line segment
  496. if (nextNode != null)
  497. {
  498. useableTileSets = nextNode.TileSets;
  499. nextNodeIndex = (nextNodeIndex >= orderedNodes.Length - 1) ? -1 : nextNodeIndex + 1;
  500. placementParams.Node = nextNode;
  501. if (nextNode == orderedNodes[orderedNodes.Length - 1])
  502. isDone = true;
  503. }
  504. else
  505. {
  506. useableTileSets = currentArchetype.TileSets;
  507. placementParams.Archetype = currentArchetype;
  508. placementParams.Line = lineSegment;
  509. }
  510. slotTileSets.Add(useableTileSets);
  511. i++;
  512. }
  513. int tileRetryCount = 0;
  514. int totalForLoopRetryCount = 0;
  515. for (int j = 0; j < placementSlots.Count; j++)
  516. {
  517. var attachTo = (j == 0) ? null : proxyDungeon.MainPathTiles[proxyDungeon.MainPathTiles.Count - 1];
  518. var tile = AddTile(attachTo, slotTileSets[j], j / (float)(placementSlots.Count - 1), placementSlots[j]);
  519. // if no tile could be generated delete last successful tile and retry from previous index
  520. // else return false
  521. if (j > 5 && tile == null && tileRetryCount < 5 && totalForLoopRetryCount < 20)
  522. {
  523. TileProxy previousTile = proxyDungeon.MainPathTiles[j - 1];
  524. // If the tile we're removing was placed by tile injection, be sure to place the injected tile back on the pending list
  525. InjectedTile previousInjectedTile;
  526. if (injectedTiles.TryGetValue(previousTile, out previousInjectedTile))
  527. {
  528. tilesPendingInjection.Add(previousInjectedTile);
  529. injectedTiles.Remove(previousTile);
  530. }
  531. proxyDungeon.RemoveLastConnection();
  532. proxyDungeon.RemoveTile(previousTile);
  533. CollisionManager.RemoveTile(previousTile);
  534. j -= 2; // -2 because loop adds 1
  535. tileRetryCount++;
  536. totalForLoopRetryCount++;
  537. }
  538. else if (tile == null)
  539. {
  540. yield return Wait(InnerGenerate(true));
  541. yield break;
  542. }
  543. else
  544. {
  545. tile.Placement.PlacementParameters = placementSlots[j];
  546. tileRetryCount = 0;
  547. // Wait for a frame to allow for animated loading screens, etc
  548. if (ShouldSkipFrame(true))
  549. yield return GetRoomPause();
  550. }
  551. }
  552. yield break; // Required for generation to run synchronously
  553. }
  554. private bool ShouldSkipFrame(bool isRoomPlacement)
  555. {
  556. if (!GenerateAsynchronously)
  557. return false;
  558. if (isRoomPlacement && PauseBetweenRooms > 0)
  559. return true;
  560. else
  561. {
  562. bool frameWasTooLong = MaxAsyncFrameMilliseconds <= 0 ||
  563. yieldTimer.Elapsed.TotalMilliseconds >= MaxAsyncFrameMilliseconds;
  564. if (frameWasTooLong)
  565. {
  566. yieldTimer.Restart();
  567. return true;
  568. }
  569. else
  570. return false;
  571. }
  572. }
  573. private YieldInstruction GetRoomPause()
  574. {
  575. if (PauseBetweenRooms > 0)
  576. return new WaitForSeconds(PauseBetweenRooms);
  577. else
  578. return null;
  579. }
  580. protected virtual IEnumerator GenerateBranchPaths()
  581. {
  582. ChangeStatus(GenerationStatus.Branching);
  583. int[] mainPathBranches = new int[proxyDungeon.MainPathTiles.Count];
  584. BranchCountHelper.ComputeBranchCounts(DungeonFlow, RandomStream, proxyDungeon, ref mainPathBranches);
  585. int branchId = 0;
  586. for (int b = 0; b < mainPathBranches.Length; b++)
  587. {
  588. var tile = proxyDungeon.MainPathTiles[b];
  589. int branchCount = mainPathBranches[b];
  590. // This tile was created from a graph node, there should be no branching
  591. if (tile.Placement.Archetype == null)
  592. continue;
  593. if (branchCount == 0)
  594. continue;
  595. for (int i = 0; i < branchCount; i++)
  596. {
  597. TileProxy previousTile = tile;
  598. int branchDepth = tile.Placement.Archetype.BranchingDepth.GetRandom(RandomStream);
  599. for (int j = 0; j < branchDepth; j++)
  600. {
  601. List<TileSet> useableTileSets;
  602. // Branch start tiles
  603. if (j == 0 && tile.Placement.Archetype.GetHasValidBranchStartTiles())
  604. {
  605. if (tile.Placement.Archetype.BranchStartType == BranchCapType.InsteadOf)
  606. useableTileSets = tile.Placement.Archetype.BranchStartTileSets;
  607. else
  608. useableTileSets = tile.Placement.Archetype.TileSets.Concat(tile.Placement.Archetype.BranchStartTileSets).ToList();
  609. }
  610. // Branch cap tiles
  611. else if (j == (branchDepth - 1) && tile.Placement.Archetype.GetHasValidBranchCapTiles())
  612. {
  613. if (tile.Placement.Archetype.BranchCapType == BranchCapType.InsteadOf)
  614. useableTileSets = tile.Placement.Archetype.BranchCapTileSets;
  615. else
  616. useableTileSets = tile.Placement.Archetype.TileSets.Concat(tile.Placement.Archetype.BranchCapTileSets).ToList();
  617. }
  618. // Other tiles
  619. else
  620. useableTileSets = tile.Placement.Archetype.TileSets;
  621. float normalizedDepth = (branchDepth <= 1) ? 1 : j / (float)(branchDepth - 1);
  622. var newTile = AddTile(previousTile, useableTileSets, normalizedDepth, tile.Placement.PlacementParameters);
  623. if (newTile == null)
  624. break;
  625. newTile.Placement.BranchDepth = j;
  626. newTile.Placement.NormalizedBranchDepth = normalizedDepth;
  627. newTile.Placement.BranchId = branchId;
  628. newTile.Placement.PlacementParameters = previousTile.Placement.PlacementParameters;
  629. previousTile = newTile;
  630. // Wait for a frame to allow for animated loading screens, etc
  631. if (ShouldSkipFrame(true))
  632. yield return GetRoomPause();
  633. }
  634. branchId++;
  635. }
  636. }
  637. yield break;
  638. }
  639. protected virtual TileProxy AddTile(TileProxy attachTo, IEnumerable<TileSet> useableTileSets, float normalizedDepth, TilePlacementParameters placementParams)
  640. {
  641. bool isOnMainPath = (Status == GenerationStatus.MainPath);
  642. bool isFirstTile = attachTo == null;
  643. // If we're attaching to an existing dungeon, generate a dummy attachment point
  644. if(isFirstTile && AttachmentSettings != null)
  645. {
  646. var attachmentProxy = AttachmentSettings.GenerateAttachmentProxy(UpVector, RandomStream);
  647. attachTo = attachmentProxy;
  648. }
  649. // Check list of tiles to inject
  650. InjectedTile chosenInjectedTile = null;
  651. int injectedTileIndexToRemove = -1;
  652. bool isPlacingSpecificRoom = isOnMainPath && (placementParams.Archetype == null);
  653. if (tilesPendingInjection != null && !isPlacingSpecificRoom)
  654. {
  655. float pathDepth = (isOnMainPath) ? normalizedDepth : attachTo.Placement.PathDepth / (targetLength - 1f);
  656. float branchDepth = (isOnMainPath) ? 0 : normalizedDepth;
  657. for (int i = 0; i < tilesPendingInjection.Count; i++)
  658. {
  659. var injectedTile = tilesPendingInjection[i];
  660. if (injectedTile.ShouldInjectTileAtPoint(isOnMainPath, pathDepth, branchDepth))
  661. {
  662. chosenInjectedTile = injectedTile;
  663. injectedTileIndexToRemove = i;
  664. break;
  665. }
  666. }
  667. }
  668. // Select appropriate tile weights
  669. IEnumerable<GameObjectChance> chanceEntries;
  670. if (chosenInjectedTile != null)
  671. chanceEntries = new List<GameObjectChance>(chosenInjectedTile.TileSet.TileWeights.Weights);
  672. else
  673. chanceEntries = useableTileSets.SelectMany(x => x.TileWeights.Weights);
  674. // Leave the decision to allow rotation up to the new tile by default
  675. bool? allowRotation = null;
  676. // Apply constraint overrides
  677. if (OverrideAllowTileRotation)
  678. allowRotation = AllowTileRotation;
  679. DoorwayPairFinder doorwayPairFinder = new DoorwayPairFinder()
  680. {
  681. DungeonFlow = DungeonFlow,
  682. RandomStream = RandomStream,
  683. PlacementParameters = placementParams,
  684. GetTileTemplateDelegate = GetTileTemplate,
  685. IsOnMainPath = isOnMainPath,
  686. NormalizedDepth = normalizedDepth,
  687. PreviousTile = attachTo,
  688. UpVector = UpVector,
  689. AllowRotation = allowRotation,
  690. TileWeights = new List<GameObjectChance>(chanceEntries),
  691. DungeonProxy = proxyDungeon,
  692. IsTileAllowedPredicate = (TileProxy previousTile, TileProxy potentialNextTile, ref float weight) =>
  693. {
  694. bool isImmediateRepeat = previousTile != null && (potentialNextTile.Prefab == previousTile.Prefab);
  695. var repeatMode = TileRepeatMode.Allow;
  696. if (OverrideRepeatMode)
  697. repeatMode = RepeatMode;
  698. else if (potentialNextTile != null)
  699. repeatMode = potentialNextTile.PrefabTile.RepeatMode;
  700. bool allowTile = true;
  701. switch (repeatMode)
  702. {
  703. case TileRepeatMode.Allow:
  704. allowTile = true;
  705. break;
  706. case TileRepeatMode.DisallowImmediate:
  707. allowTile = !isImmediateRepeat;
  708. break;
  709. case TileRepeatMode.Disallow:
  710. allowTile = !proxyDungeon.AllTiles.Where(t => t.Prefab == potentialNextTile.Prefab).Any();
  711. break;
  712. default:
  713. throw new NotImplementedException("TileRepeatMode " + repeatMode + " is not implemented");
  714. }
  715. return allowTile;
  716. },
  717. };
  718. int? maxPairingAttempts = (UseMaximumPairingAttempts) ? (int?)MaxPairingAttempts : null;
  719. Queue<DoorwayPair> pairsToTest = doorwayPairFinder.GetDoorwayPairs(maxPairingAttempts);
  720. TilePlacementResult lastTileResult = null;
  721. TileProxy createdTile = null;
  722. if (pairsToTest.Count == 0)
  723. tilePlacementResults.Add(new NoMatchingDoorwayPlacementResult(attachTo));
  724. while (pairsToTest.Count > 0)
  725. {
  726. var pair = pairsToTest.Dequeue();
  727. lastTileResult = TryPlaceTile(pair, placementParams, out createdTile);
  728. if (lastTileResult is SuccessPlacementResult)
  729. break;
  730. else
  731. tilePlacementResults.Add(lastTileResult);
  732. }
  733. // Successfully placed the tile
  734. if (lastTileResult is SuccessPlacementResult)
  735. {
  736. // We've successfully injected the tile, so we can remove it from the pending list now
  737. if (chosenInjectedTile != null)
  738. {
  739. createdTile.Placement.InjectionData = chosenInjectedTile;
  740. injectedTiles[createdTile] = chosenInjectedTile;
  741. tilesPendingInjection.RemoveAt(injectedTileIndexToRemove);
  742. if (isOnMainPath)
  743. targetLength++;
  744. }
  745. return createdTile;
  746. }
  747. else
  748. return null;
  749. }
  750. protected TilePlacementResult TryPlaceTile(DoorwayPair pair, TilePlacementParameters placementParameters, out TileProxy tile)
  751. {
  752. tile = null;
  753. var toTemplate = pair.NextTemplate;
  754. var fromDoorway = pair.PreviousDoorway;
  755. if (toTemplate == null)
  756. return new NullTemplatePlacementResult();
  757. int toDoorwayIndex = pair.NextTemplate.Doorways.IndexOf(pair.NextDoorway);
  758. tile = tileProxyPool.TakeObject(toTemplate);
  759. tile.Placement.IsOnMainPath = Status == GenerationStatus.MainPath;
  760. tile.Placement.PlacementParameters = placementParameters;
  761. tile.Placement.TileSet = pair.NextTileSet;
  762. if (fromDoorway != null)
  763. {
  764. // Move the proxy object into position
  765. var toProxyDoor = tile.Doorways[toDoorwayIndex];
  766. tile.PositionBySocket(toProxyDoor, fromDoorway);
  767. Bounds proxyBounds = tile.Placement.Bounds;
  768. // Check if the new tile is outside of the valid bounds
  769. if (RestrictDungeonToBounds && !TilePlacementBounds.Contains(proxyBounds))
  770. {
  771. tileProxyPool.ReturnObject(tile);
  772. return new OutOfBoundsPlacementResult(toTemplate);
  773. }
  774. // Check if the new tile is colliding with any other
  775. bool isColliding = CollisionManager.IsCollidingWithAnyTile(UpDirection, tile, fromDoorway.TileProxy);
  776. if (isColliding)
  777. {
  778. tileProxyPool.ReturnObject(tile);
  779. return new TileIsCollidingPlacementResult(toTemplate);
  780. }
  781. }
  782. if (tile.Placement.IsOnMainPath)
  783. {
  784. if (pair.PreviousTile != null)
  785. tile.Placement.PathDepth = pair.PreviousTile.Placement.PathDepth + 1;
  786. }
  787. else
  788. {
  789. tile.Placement.PathDepth = pair.PreviousTile.Placement.PathDepth;
  790. tile.Placement.BranchDepth = (pair.PreviousTile.Placement.IsOnMainPath) ? 0 : pair.PreviousTile.Placement.BranchDepth + 1;
  791. }
  792. var toDoorway = tile.Doorways[toDoorwayIndex];
  793. if (fromDoorway != null)
  794. proxyDungeon.MakeConnection(fromDoorway, toDoorway);
  795. proxyDungeon.AddTile(tile);
  796. CollisionManager.AddTile(tile);
  797. return new SuccessPlacementResult();
  798. }
  799. protected TileProxy GetTileTemplate(GameObject prefab)
  800. {
  801. // No proxy has been loaded yet, we should create one
  802. if (!preProcessData.TryGetValue(prefab, out var template))
  803. {
  804. template = new TileProxy(prefab);
  805. preProcessData.Add(prefab, template);
  806. }
  807. return template;
  808. }
  809. protected TileProxy PickRandomTemplate(DoorwaySocket socketGroupFilter)
  810. {
  811. // Pick a random tile
  812. var tile = useableTiles[RandomStream.Next(0, useableTiles.Count)];
  813. var template = GetTileTemplate(tile);
  814. // If there's a socket group filter and the chosen Tile doesn't have a socket of this type, try again
  815. if (socketGroupFilter != null && !template.UnusedDoorways.Where(d => d.Socket == socketGroupFilter).Any())
  816. return PickRandomTemplate(socketGroupFilter);
  817. return template;
  818. }
  819. protected int NormalizedDepthToIndex(float normalizedDepth)
  820. {
  821. return Mathf.RoundToInt(normalizedDepth * (targetLength - 1));
  822. }
  823. protected float IndexToNormalizedDepth(int index)
  824. {
  825. return index / (float)targetLength;
  826. }
  827. /// <summary>
  828. /// Registers a post-process step with the generator which allows for a callback function to be invoked during the PostProcess step
  829. /// </summary>
  830. /// <param name="postProcessCallback">The callback to invoke</param>
  831. /// <param name="priority">The priority which determines the order in which post-process steps are invoked (highest to lowest).</param>
  832. /// <param name="phase">Which phase to run the post-process step. Used to determine whether the step should run before or after DunGen's built-in post-processing</param>
  833. public void RegisterPostProcessStep(Action<DungeonGenerator> postProcessCallback, int priority = 0, PostProcessPhase phase = PostProcessPhase.AfterBuiltIn)
  834. {
  835. postProcessSteps.Add(new DungeonGeneratorPostProcessStep(postProcessCallback, priority, phase));
  836. }
  837. /// <summary>
  838. /// Unregisters an existing post-process step registered using RegisterPostProcessStep()
  839. /// </summary>
  840. /// <param name="postProcessCallback">The callback to remove</param>
  841. public void UnregisterPostProcessStep(Action<DungeonGenerator> postProcessCallback)
  842. {
  843. for (int i = 0; i < postProcessSteps.Count; i++)
  844. if (postProcessSteps[i].PostProcessCallback == postProcessCallback)
  845. postProcessSteps.RemoveAt(i);
  846. }
  847. protected virtual IEnumerator PostProcess()
  848. {
  849. GenerationStats.BeginTime(GenerationStatus.PostProcessing);
  850. ChangeStatus(GenerationStatus.PostProcessing);
  851. int length = proxyDungeon.MainPathTiles.Count;
  852. // Calculate maximum branch depth
  853. int maxBranchDepth = 0;
  854. if (proxyDungeon.BranchPathTiles.Count > 0)
  855. {
  856. foreach(var branchTile in proxyDungeon.BranchPathTiles)
  857. {
  858. int branchDepth = branchTile.Placement.BranchDepth;
  859. if (branchDepth > maxBranchDepth)
  860. maxBranchDepth = branchDepth;
  861. }
  862. }
  863. // Waiting one frame so objects are in their expected state
  864. yield return null;
  865. // Order post-process steps by priority
  866. postProcessSteps.Sort((a, b) =>
  867. {
  868. return b.Priority.CompareTo(a.Priority);
  869. });
  870. // Apply any post-process to be run BEFORE built-in post-processing is run
  871. foreach (var step in postProcessSteps)
  872. {
  873. if (ShouldSkipFrame(false))
  874. yield return null;
  875. if (step.Phase == PostProcessPhase.BeforeBuiltIn)
  876. step.PostProcessCallback(this);
  877. }
  878. // Waiting one frame so objects are in their expected state
  879. yield return null;
  880. foreach (var tile in CurrentDungeon.AllTiles)
  881. {
  882. if (ShouldSkipFrame(false))
  883. yield return null;
  884. tile.Placement.NormalizedPathDepth = tile.Placement.PathDepth / (float)(length - 1);
  885. }
  886. CurrentDungeon.PostGenerateDungeon(this);
  887. // Process random props
  888. ProcessLocalProps();
  889. ProcessGlobalProps();
  890. if (DungeonFlow.KeyManager != null)
  891. PlaceLocksAndKeys();
  892. GenerationStats.SetRoomStatistics(CurrentDungeon.MainPathTiles.Count, CurrentDungeon.BranchPathTiles.Count, maxBranchDepth);
  893. preProcessData.Clear();
  894. // Waiting one frame so objects are in their expected state
  895. yield return null;
  896. // Apply any post-process to be run AFTER built-in post-processing is run
  897. foreach (var step in postProcessSteps)
  898. {
  899. if (ShouldSkipFrame(false))
  900. yield return null;
  901. if (step.Phase == PostProcessPhase.AfterBuiltIn)
  902. step.PostProcessCallback(this);
  903. }
  904. // Finalise
  905. GenerationStats.EndTime();
  906. // Activate all door gameobjects that were added to doorways
  907. foreach (var door in CurrentDungeon.Doors)
  908. if (door != null)
  909. door.SetActive(true);
  910. }
  911. protected virtual void ProcessLocalProps()
  912. {
  913. void GetHierarchyDepth(Transform transform, ref int depth)
  914. {
  915. if (transform.parent != null)
  916. {
  917. depth++;
  918. GetHierarchyDepth(transform.parent, ref depth);
  919. }
  920. }
  921. var props = Root.GetComponentsInChildren<RandomProp>();
  922. var propData = new List<PropProcessingData>();
  923. foreach (var prop in props)
  924. {
  925. int depth = 0;
  926. GetHierarchyDepth(prop.transform, ref depth);
  927. propData.Add(new PropProcessingData()
  928. {
  929. PropComponent = prop,
  930. HierarchyDepth = depth,
  931. OwningTile = prop.GetComponentInParent<Tile>()
  932. });
  933. }
  934. // Order by hierarchy depth to ensure a parent prop group is processed before its children
  935. propData = propData.OrderBy(x => x.HierarchyDepth).ToList();
  936. var spawnedObjects = new List<GameObject>();
  937. for (int i = 0; i < propData.Count; i++)
  938. {
  939. var data = propData[i];
  940. if (data.PropComponent == null)
  941. continue;
  942. spawnedObjects.Clear();
  943. data.PropComponent.Process(RandomStream, data.OwningTile, ref spawnedObjects);
  944. // Gather any spawned sub-props and insert them into the list to be processed too
  945. var spawnedSubProps = spawnedObjects.SelectMany(x => x.GetComponentsInChildren<RandomProp>()).Distinct();
  946. foreach (var subProp in spawnedSubProps)
  947. {
  948. propData.Insert(i + 1, new PropProcessingData()
  949. {
  950. PropComponent = subProp,
  951. HierarchyDepth = data.HierarchyDepth + 1,
  952. OwningTile = data.OwningTile
  953. });
  954. }
  955. }
  956. }
  957. protected virtual void ProcessGlobalProps()
  958. {
  959. Dictionary<int, GameObjectChanceTable> globalPropWeights = new Dictionary<int, GameObjectChanceTable>();
  960. foreach (var tile in CurrentDungeon.AllTiles)
  961. {
  962. foreach (var prop in tile.GetComponentsInChildren<GlobalProp>(true))
  963. {
  964. GameObjectChanceTable table = null;
  965. if (!globalPropWeights.TryGetValue(prop.PropGroupID, out table))
  966. {
  967. table = new GameObjectChanceTable();
  968. globalPropWeights[prop.PropGroupID] = table;
  969. }
  970. float weight = (tile.Placement.IsOnMainPath) ? prop.MainPathWeight : prop.BranchPathWeight;
  971. weight *= prop.DepthWeightScale.Evaluate(tile.Placement.NormalizedDepth);
  972. table.Weights.Add(new GameObjectChance(prop.gameObject, weight, 0, null));
  973. }
  974. }
  975. foreach (var chanceTable in globalPropWeights.Values)
  976. foreach (var weight in chanceTable.Weights)
  977. weight.Value.SetActive(false);
  978. List<int> processedPropGroups = new List<int>(globalPropWeights.Count);
  979. foreach (var pair in globalPropWeights)
  980. {
  981. if (processedPropGroups.Contains(pair.Key))
  982. {
  983. Debug.LogWarning("Dungeon Flow contains multiple entries for the global prop group ID: " + pair.Key + ". Only the first entry will be used.");
  984. continue;
  985. }
  986. var prop = DungeonFlow.GlobalProps.Where(x => x.ID == pair.Key).FirstOrDefault();
  987. if (prop == null)
  988. continue;
  989. var weights = pair.Value.Clone();
  990. int propCount = prop.Count.GetRandom(RandomStream);
  991. propCount = Mathf.Clamp(propCount, 0, weights.Weights.Count);
  992. for (int i = 0; i < propCount; i++)
  993. {
  994. var chosenEntry = weights.GetRandom(RandomStream,
  995. isOnMainPath: true,
  996. normalizedDepth: 0,
  997. previouslyChosen: null,
  998. allowImmediateRepeats: true,
  999. removeFromTable: true);
  1000. if (chosenEntry != null && chosenEntry.Value != null)
  1001. chosenEntry.Value.SetActive(true);
  1002. }
  1003. processedPropGroups.Add(pair.Key);
  1004. }
  1005. }
  1006. protected virtual void PlaceLocksAndKeys()
  1007. {
  1008. var nodes = CurrentDungeon.ConnectionGraph.Nodes.Select(x => x.Tile.Placement.GraphNode).Where(x => { return x != null; }).Distinct().ToArray();
  1009. var lines = CurrentDungeon.ConnectionGraph.Nodes.Select(x => x.Tile.Placement.GraphLine).Where(x => { return x != null; }).Distinct().ToArray();
  1010. Dictionary<Doorway, Key> lockedDoorways = new Dictionary<Doorway, Key>();
  1011. // Lock doorways on nodes
  1012. foreach (var node in nodes)
  1013. {
  1014. foreach (var l in node.Locks)
  1015. {
  1016. var tile = CurrentDungeon.AllTiles.Where(x => { return x.Placement.GraphNode == node; }).FirstOrDefault();
  1017. var connections = CurrentDungeon.ConnectionGraph.Nodes.Where(x => { return x.Tile == tile; }).FirstOrDefault().Connections;
  1018. Doorway entrance = null;
  1019. Doorway exit = null;
  1020. foreach (var conn in connections)
  1021. {
  1022. if (conn.DoorwayA.Tile == tile)
  1023. exit = conn.DoorwayA;
  1024. else if (conn.DoorwayB.Tile == tile)
  1025. entrance = conn.DoorwayB;
  1026. }
  1027. var key = node.Graph.KeyManager.GetKeyByID(l.ID);
  1028. if (entrance != null && (node.LockPlacement & NodeLockPlacement.Entrance) == NodeLockPlacement.Entrance)
  1029. lockedDoorways.Add(entrance, key);
  1030. if (exit != null && (node.LockPlacement & NodeLockPlacement.Exit) == NodeLockPlacement.Exit)
  1031. lockedDoorways.Add(exit, key);
  1032. }
  1033. }
  1034. // Lock doorways on lines
  1035. foreach (var line in lines)
  1036. {
  1037. var doorways = CurrentDungeon.ConnectionGraph.Connections.Where(x =>
  1038. {
  1039. var tileSet = x.DoorwayA.Tile.Placement.TileSet;
  1040. if (tileSet == null)
  1041. return false;
  1042. bool isDoorwayAlreadyLocked = lockedDoorways.ContainsKey(x.DoorwayA) || lockedDoorways.ContainsKey(x.DoorwayB);
  1043. bool doorwayHasLockPrefabs = tileSet.LockPrefabs.Count > 0;
  1044. return x.DoorwayA.Tile.Placement.GraphLine == line &&
  1045. x.DoorwayB.Tile.Placement.GraphLine == line &&
  1046. !isDoorwayAlreadyLocked &&
  1047. doorwayHasLockPrefabs;
  1048. }).Select(x => x.DoorwayA).ToList();
  1049. if (doorways.Count == 0)
  1050. continue;
  1051. foreach (var l in line.Locks)
  1052. {
  1053. int lockCount = l.Range.GetRandom(RandomStream);
  1054. lockCount = Mathf.Clamp(lockCount, 0, doorways.Count);
  1055. for (int i = 0; i < lockCount; i++)
  1056. {
  1057. if (doorways.Count == 0)
  1058. break;
  1059. var doorway = doorways[RandomStream.Next(0, doorways.Count)];
  1060. doorways.Remove(doorway);
  1061. if (lockedDoorways.ContainsKey(doorway))
  1062. continue;
  1063. var key = line.Graph.KeyManager.GetKeyByID(l.ID);
  1064. lockedDoorways.Add(doorway, key);
  1065. }
  1066. }
  1067. }
  1068. // Lock doorways on injected tiles
  1069. foreach (var tile in CurrentDungeon.AllTiles)
  1070. {
  1071. if (tile.Placement.InjectionData != null && tile.Placement.InjectionData.IsLocked)
  1072. {
  1073. var validLockedDoorways = new List<Doorway>();
  1074. foreach (var doorway in tile.UsedDoorways)
  1075. {
  1076. bool isDoorwayAlreadyLocked = lockedDoorways.ContainsKey(doorway) || lockedDoorways.ContainsKey(doorway.ConnectedDoorway);
  1077. bool doorwayHasLockPrefabs = tile.Placement.TileSet.LockPrefabs.Count > 0;
  1078. bool isEntranceDoorway = tile.GetEntranceDoorway() == doorway;
  1079. if (!isDoorwayAlreadyLocked &&
  1080. doorwayHasLockPrefabs &&
  1081. isEntranceDoorway)
  1082. {
  1083. validLockedDoorways.Add(doorway);
  1084. }
  1085. }
  1086. if (validLockedDoorways.Any())
  1087. {
  1088. var doorway = validLockedDoorways.First();
  1089. var key = DungeonFlow.KeyManager.GetKeyByID(tile.Placement.InjectionData.LockID);
  1090. lockedDoorways.Add(doorway, key);
  1091. }
  1092. }
  1093. }
  1094. var locksToRemove = new List<Doorway>();
  1095. var usedSpawnComponents = new List<IKeySpawner>();
  1096. foreach (var pair in lockedDoorways)
  1097. {
  1098. var doorway = pair.Key;
  1099. var key = pair.Value;
  1100. var possibleSpawnTiles = new List<Tile>();
  1101. foreach (var t in CurrentDungeon.AllTiles)
  1102. {
  1103. if (t.Placement.NormalizedPathDepth >= doorway.Tile.Placement.NormalizedPathDepth)
  1104. continue;
  1105. bool canPlaceKey = false;
  1106. if (t.Placement.GraphNode != null && t.Placement.GraphNode.Keys.Where(x => { return x.ID == key.ID; }).Count() > 0)
  1107. canPlaceKey = true;
  1108. else if (t.Placement.GraphLine != null && t.Placement.GraphLine.Keys.Where(x => { return x.ID == key.ID; }).Count() > 0)
  1109. canPlaceKey = true;
  1110. if (!canPlaceKey)
  1111. continue;
  1112. possibleSpawnTiles.Add(t);
  1113. }
  1114. var possibleSpawnComponents = possibleSpawnTiles
  1115. .SelectMany(x => x.GetComponentsInChildren<Component>()
  1116. .OfType<IKeySpawner>())
  1117. .Except(usedSpawnComponents)
  1118. .Where(x => x.CanSpawnKey(DungeonFlow.KeyManager, key))
  1119. .ToArray();
  1120. GameObject lockedDoorPrefab = null;
  1121. if(possibleSpawnComponents.Any())
  1122. lockedDoorPrefab = TryGetRandomLockedDoorPrefab(doorway, key, DungeonFlow.KeyManager);
  1123. if (!possibleSpawnComponents.Any() || lockedDoorPrefab == null)
  1124. locksToRemove.Add(doorway);
  1125. else
  1126. {
  1127. doorway.LockID = key.ID;
  1128. var keySpawnParameters = new KeySpawnParameters(key, DungeonFlow.KeyManager, this);
  1129. int keysToSpawn = key.KeysPerLock.GetRandom(RandomStream);
  1130. keysToSpawn = Math.Min(keysToSpawn, possibleSpawnComponents.Length);
  1131. for (int i = 0; i < keysToSpawn; i++)
  1132. {
  1133. int chosenSpawnerIndex = RandomStream.Next(0, possibleSpawnComponents.Length);
  1134. var keySpawner = possibleSpawnComponents[chosenSpawnerIndex];
  1135. keySpawnParameters.OutputSpawnedKeys.Clear();
  1136. keySpawner.SpawnKey(keySpawnParameters);
  1137. foreach(var receiver in keySpawnParameters.OutputSpawnedKeys)
  1138. receiver.OnKeyAssigned(key, DungeonFlow.KeyManager);
  1139. usedSpawnComponents.Add(keySpawner);
  1140. }
  1141. LockDoorway(doorway, lockedDoorPrefab, key, DungeonFlow.KeyManager);
  1142. }
  1143. }
  1144. foreach (var doorway in locksToRemove)
  1145. {
  1146. doorway.LockID = null;
  1147. lockedDoorways.Remove(doorway);
  1148. }
  1149. }
  1150. protected virtual GameObject TryGetRandomLockedDoorPrefab(Doorway doorway, Key key, KeyManager keyManager)
  1151. {
  1152. var placement = doorway.Tile.Placement;
  1153. var prefabs = doorway.Tile.Placement.TileSet.LockPrefabs.Where(x =>
  1154. {
  1155. if (x == null || x.LockPrefabs == null)
  1156. return false;
  1157. if (!x.LockPrefabs.HasAnyValidEntries(placement.IsOnMainPath, placement.NormalizedDepth, null, true))
  1158. return false;
  1159. var lockSocket = x.Socket;
  1160. if (lockSocket == null)
  1161. return true;
  1162. else
  1163. return DoorwaySocket.CanSocketsConnect(lockSocket, doorway.Socket);
  1164. }).Select(x => x.LockPrefabs).ToArray();
  1165. if (prefabs.Length == 0)
  1166. return null;
  1167. var chosenEntry = prefabs[RandomStream.Next(0, prefabs.Length)].GetRandom(RandomStream, placement.IsOnMainPath, placement.NormalizedDepth, null, true);
  1168. return chosenEntry.Value;
  1169. }
  1170. protected virtual void LockDoorway(Doorway doorway, GameObject doorPrefab, Key key, KeyManager keyManager)
  1171. {
  1172. GameObject doorObj = GameObject.Instantiate(doorPrefab, doorway.transform);
  1173. DungeonUtil.AddAndSetupDoorComponent(CurrentDungeon, doorObj, doorway);
  1174. // Remove any existing door prefab that may have been placed as we'll be replacing it with a locked door
  1175. doorway.RemoveUsedPrefab();
  1176. // Set this locked door as the current door prefab
  1177. doorway.SetUsedPrefab(doorObj);
  1178. doorway.ConnectedDoorway.SetUsedPrefab(doorObj);
  1179. foreach (var keylock in doorObj.GetComponentsInChildren<Component>().OfType<IKeyLock>())
  1180. keylock.OnKeyAssigned(key, keyManager);
  1181. }
  1182. #region ISerializationCallbackReceiver Implementation
  1183. public void OnBeforeSerialize()
  1184. {
  1185. fileVersion = CurrentFileVersion;
  1186. }
  1187. public void OnAfterDeserialize()
  1188. {
  1189. #pragma warning disable CS0618 // Type or member is obsolete
  1190. // Upgrade to new repeat mode
  1191. if (fileVersion < 1)
  1192. RepeatMode = (allowImmediateRepeats) ? TileRepeatMode.Allow : TileRepeatMode.DisallowImmediate;
  1193. // Moved collision properties to their own settings class
  1194. if (fileVersion < 2)
  1195. {
  1196. if(CollisionSettings == null)
  1197. CollisionSettings = new DungeonCollisionSettings();
  1198. CollisionSettings.DisallowOverhangs = DisallowOverhangs;
  1199. CollisionSettings.OverlapThreshold = OverlapThreshold;
  1200. CollisionSettings.Padding = Padding;
  1201. CollisionSettings.AvoidCollisionsWithOtherDungeons = AvoidCollisionsWithOtherDungeons;
  1202. }
  1203. if (fileVersion < 3)
  1204. TriggerPlacement = PlaceTileTriggers ? TriggerPlacementMode.ThreeDimensional : TriggerPlacementMode.None;
  1205. #pragma warning restore CS0618 // Type or member is obsolete
  1206. }
  1207. #endregion
  1208. }
  1209. }