public bool Equals(MapCoordinates other) { return(Row.Equals(other.Row) && Column.Equals(other.Column)); }
//////////////////////////////////////////////////////////////////////////////////////////////////// /// <summary> Computes the FOV. </summary> /// /// <remarks> Darrell, 8/28/2016. </remarks> /// /// <param name="octant"> The octant to be scanned. </param> /// <param name="origin"> The spot from which the FOV is computed. </param> /// <param name="rangeLimit"> The range limit. </param> /// <param name="x"> The x coordinate. </param> /// <param name="top"> The top slope. </param> /// <param name="bottom"> The bottom slope. </param> //////////////////////////////////////////////////////////////////////////////////////////////////// private void Compute(int octant, MapCoordinates origin, int rangeLimit, int x, Slope top, Slope bottom) { // throughout this function there are references to various parts of tiles. a tile's coordinates refer to its // center, and the following diagram shows the parts of the tile and the vectors from the origin that pass through // those parts. given a part of a tile with vector u, a vector v passes above it if v > u and below it if v < u // g center: y / x // a------b a top left: (y*2+1) / (x*2-1) i inner top left: (y*4+1) / (x*4-1) // | /\ | b top right: (y*2+1) / (x*2+1) j inner top right: (y*4+1) / (x*4+1) // |i/__\j| c bottom left: (y*2-1) / (x*2-1) k inner bottom left: (y*4-1) / (x*4-1) //e|/| |\|f d bottom right: (y*2-1) / (x*2+1) m inner bottom right: (y*4-1) / (x*4+1) // |\|__|/| e middle left: (y*2) / (x*2-1) // |k\ /m| f middle right: (y*2) / (x*2+1) a-d are the corners of the tile // | \/ | g top center: (y*2+1) / (x*2) e-h are the corners of the inner (wall) diamond // c------d h bottom center: (y*2-1) / (x*2) i-m are the corners of the inner square (1/2 tile width) // h for (; x <= rangeLimit; x++) { // compute the Y coordinates of the top and bottom of the sector. we maintain that top > bottom int topY; // if top == ?/1 then it must be 1/1 because 0/1 < top <= 1/1. this is special-cased because top // starts at 1/1 and remains 1/1 as long as it doesn't hit anything, so it's a common case if (top.X == 1) { topY = x; } else // top < 1 { // Get the tile that the top vector enters from the left. Since our coordinates refer to the center of the // tile, this is (x-0.5)*top+0.5, which can be computed as (x-0.5)*top+0.5 = (2(x+0.5)*top+1)/2 = // ((2x+1)*top+1)/2. Since top == a/b, this is ((2x+1)*a+b)/2b. If it enters a tile at one of the left // corners, it will round up, so it'll enter from the bottom-left and never the top-left // // The Y coordinate of the tile entered from the left // Now it's possible that the vector passes from the left side of the tile up into the tile above before // exiting from the right side of this column so we may need to increment topY topY = ((x * 2 - 1) * top.Y + top.X) / (top.X * 2); // If the tile blocks light (i.e. is a wall)... if (BlocksLight(x, topY, octant, origin)) { // If the tile entered from the left blocks light, whether it passes into the tile above depends on the shape // of the wall tile as well as the angle of the vector. If the tile has does not have a beveled top-left // corner, then it is blocked. The corner is beveled if the tiles above and to the left are not walls. We can // ignore the tile to the left because if it was a wall tile, the top vector must have entered this tile from // the bottom-left corner, in which case it can't possibly enter the tile above. // // Otherwise, with a beveled top-left corner, the slope of the vector must be greater than or equal to the // slope of the vector to the top center of the tile (x*2, topY*2+1) in order for it to miss the wall and // pass into the tile above if (top.GreaterOrEqual(topY * 2 + 1, x * 2) && !BlocksLight(x, topY + 1, octant, origin)) { topY++; } } else { // Since this tile doesn't block light, there's nothing to stop it from passing into the tile above, and it // does so if the vector is greater than the vector for the bottom-right corner of the tile above. However, // there is one additional consideration - later code in this method assumes that if a tile blocks light then // it must be visible, so if the tile above blocks light we have to make sure the light actually impacts the // wall shape. There are three cases: // // 1) the tile above is clear, in which case the vector must be above the bottom-right corner of the tile above // 2) the tile above blocks light and does not have a beveled bottom-right corner, in which case the // vector must be above the bottom-right corner // 3) the tile above blocks light and does have a beveled bottom-right corner, in which case // the vector must be above the bottom center of the tile above (i.e. the corner of the beveled edge). // // Now it's possible to merge 1 and 2 into a single check, and we get the following: if the tile above and to // the right is a wall, then the vector must be above the bottom-right corner. Otherwise, the vector must be // above the bottom center. this works because if the tile above and to the right is a wall, then there are // two cases: // // 1) the tile above is also a wall, in which case we must check against the bottom-right corner, // 2) the tile above is not a wall, in which case the vector passes into it if it's above the bottom-right corner. // // So either way we use the bottom-right corner in that case. Now, if the tile above and to the right // is not a wall, then we again have two cases: // // 1) the tile above is a wall with a beveled edge, in which case we must check against the bottom center // 2) the tile above is not a wall, in which case it will only be visible if light passes through the inner // square, and the inner square is guaranteed to be no larger than a wall diamond, so if it wouldn't pass // through a wall diamond then it can't be visible, so there's no point in incrementing topY even if light // passes through the corner of the tile above. so we might as well use the bottom center for both cases. // center var ax = x * 2; // use bottom-right if the tile above and right is a wall if (BlocksLight(x + 1, topY + 1, octant, origin)) { ax++; } if (top.Greater(topY * 2 + 1, ax)) { topY++; } } } // if bottom == 0/?, then it's hitting the tile at Y=0 dead center. this is special-cased because // bottom.Y starts at zero and remains zero as long as it doesn't hit anything, so it's common int bottomY; if (bottom.Y == 0) { bottomY = 0; } else { // The tile that the bottom vector enters from the left // code below assumes that if a tile is a wall then it's visible, so if the tile contains a wall we have to // ensure that the bottom vector actually hits the wall shape. It misses the wall shape if the top-left corner // is beveled and bottom >= (bottomY*2+1)/(x*2). Finally, the top-left corner is beveled if the tiles to the // left and above are clear. we can assume the tile to the left is clear because otherwise the bottom vector // would be greater, so we only have to check above bottomY = ((x * 2 - 1) * bottom.Y + bottom.X) / (bottom.X * 2); if (bottom.GreaterOrEqual(bottomY * 2 + 1, x * 2) && BlocksLight(x, bottomY, octant, origin) && !BlocksLight(x, bottomY + 1, octant, origin)) { bottomY++; } } // Go through the tiles in the column now that we know which ones could possibly be visible var wasOpaque = -1; // 0:false, 1:true, -1:not applicable var rangeLimitSq = rangeLimit * rangeLimit; for (var y = topY; y >= bottomY; y--) // use a signed comparison because y can wrap around when decremented { if (rangeLimit < 0 || GetDistanceSq(x, y) <= rangeLimitSq) // skip the tile if it's out of visual range { var isOpaque = BlocksLight(x, y, octant, origin); // every tile where topY > y > bottomY is guaranteed to be visible. also, the code that initializes topY and // bottomY guarantees that if the tile is opaque then it's visible. so we only have to do extra work for the // case where the tile is clear and y == topY or y == bottomY. if y == topY then we have to make sure that // the top vector is above the bottom-right corner of the inner square. if y == bottomY then we have to make // sure that the bottom vector is below the top-left corner of the inner square var isVisible = isOpaque || ((y != topY || top.Greater(y * 4 - 1, x * 4 + 1)) && (y != bottomY || bottom.Less(y * 4 + 1, x * 4 - 1))); // NOTE: if you want the algorithm to be either fully or mostly symmetrical, replace the line above with the // following line (and uncomment the Slope.LessOrEqual method). the line ensures that a clear tile is visible // only if there's an unobstructed line to its center. if you want it to be fully symmetrical, also remove // the "isOpaque ||" part and see NOTE comments further down // bool isVisible = isOpaque || ((y != topY || top.GreaterOrEqual(y, x)) && (y != bottomY || bottom.LessOrEqual(y, x))); if (isVisible) { SetVisible(x, y, octant, origin); } // if we found a transition from clear to opaque or vice versa, adjust the top and bottom vectors // but don't bother adjusting them if this is the last column anyway if (x != rangeLimit) { if (isOpaque) { // If we found a transition from clear to opaque, this sector is done in this column, // so adjust the bottom vector upward and continue processing it in the next column. // If the opaque tile has a beveled top-left corner, move the bottom vector up to the top center. // Otherwise, move it up to the top left. The corner is beveled if the tiles above and to the left are // clear. we can assume the tile to the left is clear because otherwise the vector would be higher, so // we only have to check the tile above. if (wasOpaque == 0) { // top center by default int nx = x * 2, ny = y * 2 + 1; // NOTE: If you're using full symmetry and want more expansive walls (recommended), comment out the next if statement if (BlocksLight(x, y + 1, octant, origin)) { // top left if the corner is not beveled nx--; } // We have to maintain the invariant that top > bottom, so the new sector // created by adjusting the bottom is only valid if that's the case // if we're at the bottom of the column, then just adjust the current sector rather than recursing // since there's no chance that this sector can be split in two by a later transition back to clear if (top.Greater(ny, nx)) { // don't recurse unless necessary if (y == bottomY) { bottom = new Slope(ny, nx); break; } Compute(octant, origin, rangeLimit, x + 1, top, new Slope(ny, nx)); } // the new bottom is greater than or equal to the top, so the new sector is empty and we'll ignore // it. if we're at the bottom of the column, we'd normally adjust the current sector rather than else { // recursing, so that invalidates the current sector and we're done if (y == bottomY) { return; } } } wasOpaque = 1; } else { // If we found a transition from opaque to clear, adjust the top vector downwards if (wasOpaque > 0) { // If the opaque tile has a beveled bottom-right corner, move the top vector down to the bottom center - // otherwise, move it down to the bottom right. The corner is beveled if the tiles below and to the right // are clear. We know the tile below is clear because that's the current tile, so just check to the right // The bottom of the opaque tile (oy*2-1) equals the top of this tile (y*2+1) int nx = x * 2, ny = y * 2 + 1; // NOTE: if you're using full symmetry and want more expansive walls (recommended), comment out the next line // Check the right of the opaque tile (y+1), not this one if (BlocksLight(x + 1, y + 1, octant, origin)) { nx++; } // We have to maintain the invariant that top > bottom. if not, the sector is empty and we're done if (bottom.GreaterOrEqual(ny, nx)) { return; } top = new Slope(ny, nx); } wasOpaque = 0; } } } } // if the column didn't end in a clear tile, then there's no reason to continue processing the current sector // because that means either 1) wasOpaque == -1, implying that the sector is empty or at its range limit, or 2) // wasOpaque == 1, implying that we found a transition from clear to opaque and we recursed and we never found // a transition back to clear, so there's nothing else for us to do that the recursive method hasn't already. (if // we didn't recurse (because y == bottomY), it would have executed a break, leaving wasOpaque equal to 0.) if (wasOpaque != 0) { break; } } }
//////////////////////////////////////////////////////////////////////////////////////////////////// /// <summary> Constructor from a 2D character layout. </summary> /// /// <remarks> Darrellp, 9/27/2011. </remarks> /// /// <param name="layout"> The layout. </param> /// <param name="location"> The location. </param> /// <param name="exits"> The exits. </param> //////////////////////////////////////////////////////////////////////////////////////////////////// internal GenericRoom(char[][] layout, MapCoordinates location, List <GenericRoom> exits) { Setup(layout, location, exits); }
//////////////////////////////////////////////////////////////////////////////////////////////////// /// <summary> Return true if a location is part of this room's floor. </summary> /// /// <remarks> Darrellp, 9/29/2011. </remarks> /// /// <param name="location"> The location in global coordinates to be tested. </param> /// /// <returns> true if location is part of our room, false if not. </returns> //////////////////////////////////////////////////////////////////////////////////////////////////// public bool ContainsPosition(MapCoordinates location) { return(location.Column >= Left && location.Column <= Right && location.Row >= Top && location.Row <= Bottom && this[location] == '.'); }
//////////////////////////////////////////////////////////////////////////////////////////////////// /// <summary> Sets a room at a grid location. </summary> /// /// <remarks> Darrellp, 9/23/2011. </remarks> /// /// <param name="gridLocation"> The grid location. </param> /// <param name="room"> The room to be placed there.. </param> //////////////////////////////////////////////////////////////////////////////////////////////////// private void SetRoomAt(MapCoordinates gridLocation, RectangularRoom room) { _rooms[gridLocation.Column][gridLocation.Row] = room; }
//////////////////////////////////////////////////////////////////////////////////////////////////// /// <summary> Retrieve the room at a grid location. </summary> /// /// <remarks> Darrellp, 9/23/2011. </remarks> /// /// <param name="gridLocation"> The grid location. </param> /// /// <returns> The room occupying that grid location. </returns> //////////////////////////////////////////////////////////////////////////////////////////////////// private RectangularRoom RoomAt(MapCoordinates gridLocation) { return(!WithinGrid(gridLocation) ? null : _rooms[gridLocation.Column][gridLocation.Row]); }
//////////////////////////////////////////////////////////////////////////////////////////////////// /// <summary> Locates the rooms on the map. </summary> /// /// <remarks> Darrellp, 9/19/2011. </remarks> /// /// <param name="map"> The map to be excavated. </param> //////////////////////////////////////////////////////////////////////////////////////////////////// private void LocateRooms(IRoomsMap map) { // Max number of cells we can fit with at least base cell size int gridWidth = map.Width / _baseCellWidth; int gridHeight = map.Height / _baseCellHeight; // Size of cells we can manage total int baseRoomWidth = map.Width / gridWidth; int baseRoomHeight = map.Height / gridHeight; // Remainders for Bresenham algorithm int widthRemainder = map.Width % baseRoomWidth; int heightRemainder = map.Height % baseRoomHeight; // Tally for Bresenham int widthTally = gridWidth / 2; int heightTally = gridHeight / 2; // First column is on the left int mapColumn = 0; // Array of rooms in the grid _rooms = new RectangularRoom[gridWidth][]; // For each grid column for (int gridColumn = 0; gridColumn < gridWidth; gridColumn++) { // Reset the map row to 0 int mapRow = 0; // Determine the current map column int currentWidth = baseRoomWidth; widthTally += widthRemainder; // Do we need to bump width ala Bresenham? if (widthTally >= gridWidth) { // Bump width currentWidth++; widthTally -= gridWidth; } // Create the row of rooms for this column _rooms[gridColumn] = new RectangularRoom[gridHeight]; // For each row of the grid for (int gridRow = 0; gridRow < gridHeight; gridRow++) { // Determine the current map row int currentHeight = baseRoomHeight; heightTally += heightRemainder; // Do we need to bump height ala Bresenham? if (heightTally >= gridHeight) { // Bump height currentHeight++; heightTally -= gridHeight; } // Create a room in the grid cell MapCoordinates gridLocation = new MapCoordinates(gridColumn, gridRow); MapCoordinates cellLocation = new MapCoordinates(mapColumn, mapRow); RectangularRoom room = CreateRoomInCell(gridLocation, cellLocation, currentWidth, currentHeight); // Place it in the grid _rooms[gridColumn][gridRow] = room; // Advance the map row mapRow += currentHeight; } // Advance the map column mapColumn += currentWidth; } }
//////////////////////////////////////////////////////////////////////////////////////////////////// /// <summary> Merge two rooms. </summary> /// /// <remarks> Named as though dir was vertical. Darrellp, 9/22/2011. </remarks> /// /// <param name="topRoomsGridLocation"> The location of the top room in the grid. </param> /// <param name="dir"> The direction of the merge. </param> //////////////////////////////////////////////////////////////////////////////////////////////////// private void MergeTwoRooms(MapCoordinates topRoomsGridLocation, Dir dir) { // Get grid coordinates of the other room MapCoordinates bottomRoomsGridLocation = topRoomsGridLocation.NextLarger(dir); Dir otherDir = MapCoordinates.OtherDirection(dir); // Retrieve both rooms RectangularRoom topRoom = RoomAt(topRoomsGridLocation); RectangularRoom bottomRoom = RoomAt(bottomRoomsGridLocation); // Are the rooms unmergable? if (!CheckMergeForOverlap(topRoom, bottomRoom, otherDir)) { // Return and don't merge them - use normal corridors return; } // Remove their generic counterparts _mapRoomToGenericRooms.Remove(topRoom); _mapRoomToGenericRooms.Remove(bottomRoom); // Get their current coordinates, etc. int topRoomsBottom = topRoom.LargeCoord(dir); int topRoomsTop = topRoom.SmallCoord(dir); int bottomRoomsTop = bottomRoom.SmallCoord(dir); int bottomRoomsHeight = bottomRoom.Size(dir); int topRoomsWidth = topRoom.Size(otherDir); int bottomRoomsWidth = bottomRoom.Size(otherDir); // Pick a random spot between the rooms to merge them // This will be the new inside coord of the small coordinate room int mergeRow = _rnd.Next(topRoomsBottom, bottomRoomsTop); // Determine all the new coordinates int topRoomsNewHeight = mergeRow - topRoomsTop + 1; int bottomRoomsNewHeight = bottomRoomsTop - mergeRow + bottomRoomsHeight - 1; MapCoordinates bottomRoomsLocation = bottomRoom.Location; bottomRoomsLocation[dir] = mergeRow + 1; // Create our new expanded rooms RectangularRoom roomTopNew = RectangularRoom.CreateUndirectional( topRoom.Location, topRoomsNewHeight, topRoomsWidth, topRoomsGridLocation[dir], topRoomsGridLocation[otherDir], dir); RectangularRoom roomBottomNew = RectangularRoom.CreateUndirectional( bottomRoomsLocation, bottomRoomsNewHeight, bottomRoomsWidth, bottomRoomsGridLocation[dir], bottomRoomsGridLocation[otherDir], dir); // Install the new rooms SetRoomAt(topRoomsGridLocation, roomTopNew); SetRoomAt(bottomRoomsGridLocation, roomBottomNew); // Create the new generic rooms // We don't create the single merged generic room until we excavate because we don't know // the layout until then. _mapRoomToGenericRooms[roomTopNew] = roomTopNew.ToGeneric(); _mapRoomToGenericRooms[roomBottomNew] = roomBottomNew.ToGeneric(); // Mark this in our merges structure _merges.Connect(topRoomsGridLocation, bottomRoomsGridLocation); }
//////////////////////////////////////////////////////////////////////////////////////////////////// /// <summary> Excavate a merge between two rooms. </summary> /// /// <remarks> /// Names are named as though dir was vertical and dirOther horizontal. Darrellp, 9/22/2011. /// </remarks> /// /// <param name="map"> The map. </param> /// <param name="topRoom"> The top room. </param> /// <param name="bottomRoom"> The bottom room. </param> /// <param name="dir"> The dir. </param> //////////////////////////////////////////////////////////////////////////////////////////////////// private void ExcavateMerge(IRoomsMap map, RectangularRoom topRoom, RectangularRoom bottomRoom, Dir dir) { // Get the opposite direction Dir dirOther = MapCoordinates.OtherDirection(dir); // Are the rooms unmergable? if (!CheckOverlap(topRoom, bottomRoom, dirOther)) { // Should have caught this in MergeTwoRooms - throw exception throw new RogueException("Non-overlapping rooms made it to ExcavateMerge"); } // Get the appropriate coordinates int topRoomsLeft = topRoom.Location[dirOther]; int topRoomsRight = topRoomsLeft + topRoom.Size(dirOther) - 1; int bottomRoomsLeft = bottomRoom.Location[dirOther]; int bottomRoomsRight = bottomRoomsLeft + bottomRoom.Size(dirOther) - 1; // Get the high and low points of the overlap int overlapLeft = Math.Max(topRoomsLeft, bottomRoomsLeft) + 1; int overlapRight = Math.Min(topRoomsRight, bottomRoomsRight) - 1; // Create our new merged generic room GenericRoom groomTop = _mapRoomToGenericRooms[topRoom]; GenericRoom groomBottom = _mapRoomToGenericRooms[bottomRoom]; groomTop.CombineWith(groomBottom); // For each column in the grid foreach (RectangularRoom[] roomColumn in _rooms) { // For each row in the grid for (int iRow = 0; iRow < _rooms[0].Length; iRow++) { // Get the rect room at that spot RectangularRoom room = roomColumn[iRow]; // Is it mapped to our defunct bottom room? if (_mapRoomToGenericRooms[room] == groomBottom) { // Map it to our shiny new top room _mapRoomToGenericRooms[room] = groomTop; } } } // Get the location we're going to start the clearing at int topRoomsBottom = topRoom.Location[dir] + topRoom.Size(dir) - 1; MapCoordinates currentLocation = MapCoordinates.CreateUndirectional(topRoomsBottom, overlapLeft, dir); char floorChar = TerrainFactory.TerrainToChar(TerrainType.Floor); // For each spot along the overlap for (int iCol = overlapLeft; iCol <= overlapRight; iCol++) { // Clear out the two walls of the abutting rooms currentLocation[dirOther] = iCol; map[currentLocation].Terrain = TerrainType.Floor; groomTop[currentLocation] = floorChar; currentLocation[dir] = topRoomsBottom + 1; map[currentLocation].Terrain = TerrainType.Floor; groomTop[currentLocation] = floorChar; currentLocation[dir] = topRoomsBottom; } Debug.Assert(groomBottom == groomTop || !_mapRoomToGenericRooms.ContainsValue(groomBottom)); }