diff --git a/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonGenerator.cs b/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonGenerator.cs index 21927d0..7a064f7 100644 --- a/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonGenerator.cs +++ b/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonGenerator.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Text; using Unity.VisualScripting; -using UnityEngine; using Random = System.Random; namespace DungeonGenerator @@ -43,10 +43,39 @@ namespace DungeonGenerator AddNormalRoomsAroundMonsterRooms(dungeonMap); AddNormalRoomsAroundBossRoom(dungeonMap); + AddConnectionRooms(dungeonMap); return dungeonMap; } + private void AddConnectionRooms(DungeonMap dungeon) + { + // For each room adjacent to a monster and boos room connect it with the closest thing (entrance room, or monster adjacent room) + // unless there is something already going there. + List rootRooms = new List(dungeon.GetMonsterRooms()); + rootRooms.Add(dungeon.GetBossRoom()); + + // Create list of connection rooms + Dictionary nodeRoomAndSideItIsOn = new Dictionary(); + + List nodeRoomsWithEntrances = new List(dungeon.GetNodeRooms()); + nodeRoomsWithEntrances.AddRange(dungeon.GetEntranceRooms()); + + // For each connection room that is not an entrance room, + // find the next closest uninterrupted connection room that is not on the same root room. + + foreach (var roomSideWithRoom in nodeRoomAndSideItIsOn) + { + Room closestSeenRoom = FindClosestSeenConnection(roomSideWithRoom.Value, + roomSideWithRoom.Key, + nodeRoomsWithEntrances, + rootRooms); + //Place rooms along the line until they are connected + dungeon.AddRooms(PlaceRoomsOrganicallyTowardRoom(roomSideWithRoom.Value, closestSeenRoom, RoomType.Normal, + dungeon.GetOccupiedPoints())); + } + } + private void AddNormalRoomsAroundBossRoom(DungeonMap dungeon) { AddNormalRoomsAroundRoom(dungeon.GetBossRoom(), dungeon); @@ -64,46 +93,10 @@ namespace DungeonGenerator { Random random = new Random(); - var roomPoints = room.GetPointsInRoom(); - int minX = roomPoints.Min(p => p.X); - int maxX = roomPoints.Max(p => p.X); - int minY = roomPoints.Min(p => p.Y); - int maxY = roomPoints.Max(p => p.Y); - - List topUnoccupiedPoints = new List(); - List bottomUnoccupiedPoints = new List(); - List leftUnoccupiedPoints = new List(); - List rightUnoccupiedPoints = new List(); - - for (int x = minX - SIDE_LENGTH_OF_NORMAL; x <= maxX + SIDE_LENGTH_OF_NORMAL; x++) - { - Point point = new Point(x, minY - SIDE_LENGTH_OF_NORMAL); - if (dungeon.GetUnoccupiedPoints().Contains(point)) - { - topUnoccupiedPoints.Add(point); - } - - point = new Point(x, maxY + SIDE_LENGTH_OF_NORMAL); - if (dungeon.GetUnoccupiedPoints().Contains(point)) - { - bottomUnoccupiedPoints.Add(point); - } - } - - for (int y = minY - SIDE_LENGTH_OF_NORMAL; y <= maxY + SIDE_LENGTH_OF_NORMAL; y++) - { - Point point = new Point(minX - SIDE_LENGTH_OF_NORMAL, y); - if (dungeon.GetUnoccupiedPoints().Contains(point)) - { - leftUnoccupiedPoints.Add(point); - } - - point = new Point(maxX + SIDE_LENGTH_OF_NORMAL, y); - if (dungeon.GetUnoccupiedPoints().Contains(point)) - { - rightUnoccupiedPoints.Add(point); - } - } + List topUnoccupiedPoints = new List(GetUnoccupiedPointsOnSide(room, RoomSide.Top, dungeon.GetUnoccupiedPoints())); + List bottomUnoccupiedPoints = new List(GetUnoccupiedPointsOnSide(room, RoomSide.Bottom, dungeon.GetUnoccupiedPoints())); + List leftUnoccupiedPoints = new List(GetUnoccupiedPointsOnSide(room, RoomSide.Left, dungeon.GetUnoccupiedPoints())); + List rightUnoccupiedPoints = new List(GetUnoccupiedPointsOnSide(room, RoomSide.Right, dungeon.GetUnoccupiedPoints())); int minNecessaryAvailablePoints = (SIDE_LENGTH_OF_MONSTER + SIDE_LENGTH_OF_NORMAL) / 2; @@ -119,16 +112,15 @@ namespace DungeonGenerator // Randomly shuffle and take the required number of sides foreach (RoomSide side in availableSides.OrderBy(_ => random.Next()).Take(numRoomsToAdd)) { - // Select appropriate unoccupied points based on the side List unoccupiedPointsOnSide = side switch { RoomSide.Top => topUnoccupiedPoints, RoomSide.Bottom => bottomUnoccupiedPoints, RoomSide.Left => leftUnoccupiedPoints, RoomSide.Right => rightUnoccupiedPoints, - _ => new List() + _ => throw new ArgumentOutOfRangeException(nameof(side), $"Unexpected RoomSide value: {side}") }; - + // Create room and add it if valid Room newRoom = CreateAdjacentRoom(RoomType.Normal, unoccupiedPointsOnSide, side, dungeon.GetOccupiedPoints()); if (newRoom != null) @@ -139,6 +131,65 @@ namespace DungeonGenerator } } + private List GetUnoccupiedPointsOnSide(Room room, RoomSide side, HashSet unoccupiedPoints) + { + List unoccupiedPointsOnSide = new List(); + var roomPoints = room.GetPointsInRoom(); + int minX = roomPoints.Min(p => p.X); + int maxX = roomPoints.Max(p => p.X); + int minY = roomPoints.Min(p => p.Y); + int maxY = roomPoints.Max(p => p.Y); + + switch (side) + { + case RoomSide.Top: + for (int x = minX - SIDE_LENGTH_OF_NORMAL; x <= maxX + SIDE_LENGTH_OF_NORMAL; x++) + { + Point point = new Point(x, minY - SIDE_LENGTH_OF_NORMAL); + if (unoccupiedPoints.Contains(point)) + { + unoccupiedPointsOnSide.Add(point); + } + } + return unoccupiedPointsOnSide; + case RoomSide.Bottom: + for (int x = minX - SIDE_LENGTH_OF_NORMAL; x <= maxX + SIDE_LENGTH_OF_NORMAL; x++) + { + Point point = new Point(x, maxY + SIDE_LENGTH_OF_NORMAL); + if (unoccupiedPoints.Contains(point)) + { + unoccupiedPointsOnSide.Add(point); + } + } + + return unoccupiedPointsOnSide; + case RoomSide.Left: + for (int y = minY - SIDE_LENGTH_OF_NORMAL; y <= maxY + SIDE_LENGTH_OF_NORMAL; y++) + { + Point point = new Point(minX - SIDE_LENGTH_OF_NORMAL, y); + if (unoccupiedPoints.Contains(point)) + { + unoccupiedPointsOnSide.Add(point); + } + } + + return unoccupiedPointsOnSide; + case RoomSide.Right: + for (int y = minY - SIDE_LENGTH_OF_NORMAL; y <= maxY + SIDE_LENGTH_OF_NORMAL; y++) + { + Point point = new Point(maxX + SIDE_LENGTH_OF_NORMAL, y); + if (unoccupiedPoints.Contains(point)) + { + unoccupiedPointsOnSide.Add(point); + } + } + + return unoccupiedPointsOnSide; + default: + throw new ArgumentException("Side not recognized"); + } + } + private Room CreateAdjacentRoom(RoomType type, List unoccupiedPointsOnSide, RoomSide side, HashSet occupiedPoints) { int sizeOfNewRoom = GetRoomSizeByType(type); @@ -394,5 +445,127 @@ namespace DungeonGenerator int midpoint = length / 2; return midpoint - (numberOfRooms * sizeOfRoom / 2); } - } -} \ No newline at end of file + + private Room FindClosestSeenConnection(Room nodeRoom, RoomSide attachedSide, List possibleConnections, + List obstructions) + { + const float minClosestLength = 0; + Vector2 lookDirection = attachedSide switch + { + RoomSide.Top => new Vector2(0, -1), // Expanding upward + RoomSide.Bottom => new Vector2(0, 1), // Expanding downward + RoomSide.Left => new Vector2(-1, 0), // Expanding left + RoomSide.Right => new Vector2(1, 0), // Expanding right + _ => Vector2.Zero // Default (should not happen) + }; + + Point nodeCenter = nodeRoom.GetCenterOfRoom(); + float visionConeAngle = MathF.PI / 4; // 45 degree cone in each side + + Room closestSeenRoom = null; + float closestDistance = float.MaxValue; + + foreach (Room otherRoom in possibleConnections) + { + if (otherRoom == nodeRoom) continue; + + Point otherCenter = otherRoom.GetCenterOfRoom(); + Vector2 toOther = new Vector2(otherCenter.X - nodeCenter.X, otherCenter.Y - nodeCenter.Y); + float distance = toOther.Length(); + + if (distance <= minClosestLength || distance >= closestDistance) continue; + + // Normalize direction vectors + toOther = Vector2.Normalize(toOther); + Vector2 lookNormalized = Vector2.Normalize(lookDirection); + + float angle = MathF.Acos(Vector2.Dot(lookNormalized, toOther)); + + if (angle <= visionConeAngle) + { + if (!IsObstructed(nodeCenter, otherCenter, obstructions)) + { + closestSeenRoom = otherRoom; + closestDistance = distance; + } + } + } + + return closestSeenRoom; // Could be null if no valid room is found + } + + private bool IsObstructed(Point start, Point end, List obstructions) + { + List linePoints = GetPointsOnLine(start, end); + foreach (Point point in linePoints) + { + if (obstructions.Any(room => room.ContainsPoint(point))) + { + return true; + } + } + + return false; + } + + // Bresenham’s Line Algorithm to get points between two points + private List GetPointsOnLine(Point p0, Point p1) + { + List points = new List(); + int dx = Math.Abs(p1.X - p0.X), sx = p0.X < p1.X ? 1 : -1; + int dy = -Math.Abs(p1.Y - p0.Y), sy = p0.Y < p1.Y ? 1 : -1; + int err = dx + dy, e2; + + int x = p0.X, y = p0.Y; + while (true) + { + points.Add(new Point(x, y)); + if (x == p1.X && y == p1.Y) break; + e2 = 2 * err; + if (e2 >= dy) { err += dy; x += sx; } + if (e2 <= dx) { err += dx; y += sy; } + } + return points; + } + + private List PlaceRoomsOrganicallyTowardRoom(Room startingRoom, Room targetRoom, RoomType type, + HashSet occupiedPoints) + { + List placedRooms = new List(); + Room currentRoom = startingRoom; + bool sideToggle = true; // Start with left placement first + + List path = GetPointsOnLine(startingRoom.GetCenterOfRoom(), targetRoom.GetCenterOfRoom()); + + foreach (Point p in path) + { + List availableSides = sideToggle + ? new List { RoomSide.Left, RoomSide.Top } + : new List { RoomSide.Right, RoomSide.Bottom }; + + sideToggle = !sideToggle; // Alternate side for next placement. + + foreach (RoomSide side in availableSides) + { + List unoccupiedPointsOnSide = GetUnoccupiedPointsOnSide(currentRoom, side, occupiedPoints); + Room newRoom = CreateAdjacentRoom(RoomType.Normal, unoccupiedPointsOnSide, side, occupiedPoints); + + if (newRoom != null) + { + placedRooms.Add(newRoom); + occupiedPoints.AddRange(newRoom.GetPointsInRoom()); + currentRoom = newRoom; + break; // Move to the next room + } + } + + if (currentRoom.GetCenterOfRoom().ManhattanDistance(targetRoom.GetCenterOfRoom()) <= + GetRoomSizeByType(type)) + break; + } + + return placedRooms; + } + + } // Class +} // Namespace \ No newline at end of file diff --git a/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonMap.cs b/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonMap.cs index 1288d67..def1186 100644 --- a/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonMap.cs +++ b/PuzzleGameProject/Assets/Scripts/DungeonGenerator/DungeonMap.cs @@ -165,5 +165,31 @@ namespace DungeonGenerator _occupiedPoints.ExceptWith(points); _unoccupiedPoints.AddRange(points); } + + // Node rooms are the rooms placed around each monster room and the boss room. + public List GetNodeRooms() + { + List nodeRooms = new List(); + nodeRooms.AddRange(_monsterRooms.SelectMany(room => room.GetAdjacentRooms())); + nodeRooms.AddRange(_bossRoom.GetAdjacentRooms()); + return nodeRooms; + } + + public Dictionary GetNodesWithSides() + { + Dictionary nodesWithSides = new Dictionary(); + foreach (Room room in _monsterRooms) + { + foreach (var kvp in room.GetAdjacentRoomsDict()) + { + foreach (Room adjacentRoom in kvp.Value) + { + nodesWithSides[kvp.Key] = room; + } + } + } + + return nodesWithSides; + } } } \ No newline at end of file diff --git a/PuzzleGameProject/Assets/Scripts/DungeonGenerator/Room.cs b/PuzzleGameProject/Assets/Scripts/DungeonGenerator/Room.cs index b988324..1cbdc1f 100644 --- a/PuzzleGameProject/Assets/Scripts/DungeonGenerator/Room.cs +++ b/PuzzleGameProject/Assets/Scripts/DungeonGenerator/Room.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using Unity.VisualScripting; @@ -51,6 +52,11 @@ namespace DungeonGenerator return points; } + public bool ContainsPoint(Point point) + { + return GetPointsInRoom().Any(p => p.Equals(point)); + } + public int GetDistanceToRoom(Room other) { Point centerOfRoom = GetCenterOfRoom(); @@ -78,6 +84,16 @@ namespace DungeonGenerator } } + public IEnumerable GetAdjacentRooms() + { + return _adjacentRooms.Values.SelectMany(roomList => roomList); + } + + public Dictionary> GetAdjacentRoomsDict() + { + return new Dictionary>(_adjacentRooms); + } + // Helper method to get the opposite side private RoomSide GetOppositeSide(RoomSide side) => side switch {