using DunGen.Tags; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Serialization; namespace DunGen.Graph { /// /// A graph representing the flow of a dungeon /// [Serializable] [CreateAssetMenu(fileName = "New Dungeon", menuName = "DunGen/Dungeon Flow", order = 700)] public class DungeonFlow : ScriptableObject, ISerializationCallbackReceiver { public const int FileVersion = 1; #region Nested Types [Serializable] public sealed class GlobalPropSettings { public int ID; public IntRange Count; public GlobalPropSettings() { ID = 0; Count = new IntRange(0, 1); } public GlobalPropSettings(int id, IntRange count) { ID = id; Count = count; } } /// /// Defines how tile connection rules are applied. /// public enum TagConnectionMode { /// /// Tiles will only connect if they have tags that appear in the TileConnectionTags list /// Accept, /// /// Tiles will always connect unless they have tags that appear in the TileConnectionTags list /// Reject, } /// /// Defines how tags are used to determine which branch tip tiles to prune /// public enum BranchPruneMode { /// /// Removes tiles at the end of a branch when they have any of the specified tags /// AnyTagPresent, /// /// Removes tiles at the end of a branch when they don't have any of the specified tags /// AllTagsMissing, } #endregion #region Legacy Properties [SerializeField] [FormerlySerializedAs("GlobalPropGroupIDs")] private List globalPropGroupID_obsolete = new List(); [SerializeField] [FormerlySerializedAs("GlobalPropRanges")] private List globalPropRanges_obsolete = new List(); #endregion /// /// The minimum and maximum length of the dungeon /// public IntRange Length = new IntRange(5, 10); /// /// Determines how the number of branches from the main path is calculated /// public BranchMode BranchMode = BranchMode.Local; /// /// The number of branches to appear across the entire dungeon /// Only used if is set to /// public IntRange BranchCount = new IntRange(1, 5); /// /// Information about which (and how many) global props should appear throughout the dungeon /// public List GlobalProps = new List(); /// /// The asset that handles all of the keys that this dungeon needs to know about /// public KeyManager KeyManager = null; /// /// The percentage chance of two unconnected but overlapping doorways being connected (0-1) /// [Range(0f, 1f)] public float DoorwayConnectionChance = 0f; /// /// If true, only doorways belonging to tiles on the same section of the dungeon can be connected /// This will prevent some unexpected shortcuts from opening up through the dungeon /// public bool RestrictConnectionToSameSection = false; /// /// Simple rules for injecting special tiles into the dungeon generation process /// public List TileInjectionRules = new List(); /// /// Defined how tile connection rules are applied see /// public TagConnectionMode TileTagConnectionMode; /// /// A list of tag pairs that define how tiles are allowed to connect. If empty, all /// tiles can connect to oneanother, otherwise the tiles must have a pair of /// matching tags from this list (omni-directional) /// public List TileConnectionTags = new List(); /// /// Determines how tags are used to prune tiles at the end of branches /// public BranchPruneMode BranchTagPruneMode = BranchPruneMode.AllTagsMissing; /// /// A list of tags used to decide if a tile at the end of a branch should be deleted /// public List BranchPruneTags = new List(); /// /// Global settings for straightening the dungeon path. Can be overridden in Archetypes and on Nodes in the flow graph /// public PathStraighteningSettings GlobalStraighteningSettings = new PathStraighteningSettings(); public List Nodes = new List(); public List Lines = new List(); [SerializeField] private int currentFileVersion; /// /// Creates the default graph /// public void Reset() { var emptyTileSet = new TileSet[0]; var emptyArchetype = new DungeonArchetype[0]; var builder = new DungeonFlowBuilder(this) .AddNode(emptyTileSet, "Start") .AddLine(emptyArchetype, 1.0f) .AddNode(emptyTileSet, "Goal"); builder.Complete(); } public GraphLine GetLineAtDepth(float normalizedDepth) { normalizedDepth = Mathf.Clamp(normalizedDepth, 0, 1); if (normalizedDepth == 0) return Lines[0]; else if (normalizedDepth == 1) return Lines[Lines.Count - 1]; foreach (var line in Lines) if (normalizedDepth >= line.Position && normalizedDepth < line.Position + line.Length) return line; Debug.LogError("GetLineAtDepth was unable to find a line at depth " + normalizedDepth + ". This shouldn't happen."); return null; } public DungeonArchetype[] GetUsedArchetypes() { return Lines.SelectMany(x => x.DungeonArchetypes).ToArray(); } public TileSet[] GetUsedTileSets() { List tileSets = new List(); foreach (var node in Nodes) tileSets.AddRange(node.TileSets); foreach(var line in Lines) foreach (var archetype in line.DungeonArchetypes) { tileSets.AddRange(archetype.TileSets); tileSets.AddRange(archetype.BranchCapTileSets); } return tileSets.ToArray(); } public bool ShouldPruneTileWithTags(TagContainer tileTags) { switch (BranchTagPruneMode) { case BranchPruneMode.AnyTagPresent: return tileTags.HasAnyTag(BranchPruneTags.ToArray()); case BranchPruneMode.AllTagsMissing: return !tileTags.HasAnyTag(BranchPruneTags.ToArray()); default: throw new NotImplementedException(string.Format("BranchPruneMode {0} is not implemented", BranchTagPruneMode)); } } public void OnBeforeSerialize() { currentFileVersion = FileVersion; } public void OnAfterDeserialize() { // Convert to new format for Global Props if(currentFileVersion < 1) { for (int i = 0; i < globalPropGroupID_obsolete.Count; i++) { int id = globalPropGroupID_obsolete[i]; var count = globalPropRanges_obsolete[i]; GlobalProps.Add(new GlobalPropSettings(id, count)); } globalPropGroupID_obsolete.Clear(); globalPropRanges_obsolete.Clear(); } } /// /// Checks the connection rules (if any) to see if two tiles are allowed /// to connect by checking their tags /// /// The first tile /// The second tile /// True if the tiles are allowed to connect public bool CanTilesConnect(Tile tileA, Tile tileB) { if (TileConnectionTags.Count == 0) return true; switch (TileTagConnectionMode) { case TagConnectionMode.Accept: return HasMatchingTagPair(tileA, tileB); case TagConnectionMode.Reject: return !HasMatchingTagPair(tileA, tileB); default: throw new NotImplementedException(string.Format("{0}.{1} is not implemented", typeof(TagConnectionMode).Name, TileTagConnectionMode)); } } public bool CanDoorwaysConnect(ProposedConnection connection) { foreach (var rule in DoorwayPairFinder.CustomConnectionRules) { TileConnectionRule.ConnectionResult result = TileConnectionRule.ConnectionResult.Passthrough; if (rule.ConnectionDelegate != null) result = rule.ConnectionDelegate(connection); #pragma warning disable CS0618 // For now, we allow rules that haven't been updated to the new delegate format else if (rule.Delegate != null) result = rule.Delegate(connection.PreviousTile.PrefabTile, connection.NextTile.PrefabTile, connection.PreviousDoorway.DoorwayComponent, connection.NextDoorway.DoorwayComponent); #pragma warning restore if (result == TileConnectionRule.ConnectionResult.Passthrough) continue; else return result == TileConnectionRule.ConnectionResult.Allow; } // No custom rules handled this connection, use default behaviour return DoorwaySocket.CanSocketsConnect(connection.PreviousDoorway.DoorwayComponent.Socket, connection.NextDoorway.DoorwayComponent.Socket) && CanTilesConnect(connection.PreviousTile.PrefabTile, connection.NextTile.PrefabTile); } private bool HasMatchingTagPair(Tile tileA, Tile tileB) { foreach(var tagPair in TileConnectionTags) { if ((tileA.Tags.HasTag(tagPair.TagA) && tileB.Tags.HasTag(tagPair.TagB)) || (tileB.Tags.HasTag(tagPair.TagA) && tileA.Tags.HasTag(tagPair.TagB))) return true; } return false; } } }