package squidpony.squidgrid.mapping; import squidpony.ArrayTools; import squidpony.squidgrid.Direction; import squidpony.squidgrid.mapping.locks.Edge; import squidpony.squidgrid.mapping.locks.IRoomLayout; import squidpony.squidgrid.mapping.locks.Room; import squidpony.squidgrid.mapping.locks.util.Rect2I; import squidpony.squidmath.Coord; import squidpony.squidmath.IntVLA; import squidpony.squidmath.PoissonDisk; import squidpony.squidmath.RNG; import java.util.*; /** * A dungeon generator that can use a mix of techniques to have part-cave, part-room dungeons. Not entirely intended for * normal use outside of this library, though it can be very useful when you want to make a dungeon match a specific * path and existing generators that use MixedGenerator aren't sufficient. You may want to use a simpler generator based * on this, like SerpentMapGenerator, which generates a long, winding path that loops around on itself. This supports * the getEnvironment() method, which can be used in conjunction with RoomFinder to find where separate room, corridor, * and cave areas have been placed. * <br> * Based on Michael Patraw's excellent Drunkard's Walk dungeon generator. * http://mpatraw.github.io/libdrunkard/ * @see squidpony.squidgrid.mapping.SerpentMapGenerator a normal use for MixedGenerator that makes winding dungeons * @see squidpony.squidgrid.mapping.SerpentDeepMapGenerator uses MixedGenerator as it makes a multi-level dungeon * Created by Tommy Ettinger on 10/22/2015. */ public class MixedGenerator { public enum CarverType { CAVE, BOX, ROUND, BOX_WALLED, ROUND_WALLED } /** * Constant for environment tiles that are not near a cave, room, or corridor. Value is 0. */ public static final int UNTOUCHED = 0; /** * Constant for environment tiles that are floors for a room. Value is 1. */ public static final int ROOM_FLOOR = 1; /** * Constant for environment tiles that are walls near a room. Value is 2. */ public static final int ROOM_WALL = 2; /** * Constant for environment tiles that are floors for a cave. Value is 3. */ public static final int CAVE_FLOOR = 3; /** * Constant for environment tiles that are walls near a cave. Value is 4. */ public static final int CAVE_WALL = 4; /** * Constant for environment tiles that are floors for a corridor. Value is 5. */ public static final int CORRIDOR_FLOOR = 5; /** * Constant for environment tiles that are walls near a corridor. Value is 6. */ public static final int CORRIDOR_WALL = 6; protected EnumMap<CarverType, Integer> carvers; protected int width, height; protected float roomWidth, roomHeight; public RNG rng; protected char[][] dungeon; protected boolean generated = false; protected int[][] environment; protected boolean[][] marked, walled, fixedRooms; protected IntVLA points; protected int totalPoints; /** * Internal use. * @param width dungeon width in cells * @param height dungeon height in cells * @param rng rng to use * @return evenly spaced Coord points in a list made by PoissonDisk, trimmed down so they aren't all used * @see PoissonDisk used to make the list */ protected static List<Coord> basicPoints(int width, int height, RNG rng) { return PoissonDisk.sampleRectangle(Coord.get(2, 2), Coord.get(width - 3, height - 3), 8.5f * (width + height) / 120f, width, height, 35, rng); } /** * This prepares a map generator that will generate a map with the given width and height, using the given RNG. * This version of the constructor uses Poisson Disk sampling to generate the points it will draw caves and * corridors between, ensuring a minimum distance between points, but it does not ensure that paths between points * will avoid overlapping with rooms or other paths. You call the different carver-adding methods to affect what the * dungeon will look like, putCaveCarvers(), putBoxRoomCarvers(), and putRoundRoomCarvers(), defaulting to only * caves if none are called. You call generate() after adding carvers, which returns a char[][] for a map. * @param width the width of the final map in cells * @param height the height of the final map in cells * @param rng an RNG object to use for random choices; this make a lot of random choices. * @see PoissonDisk used to ensure spacing for the points. */ public MixedGenerator(int width, int height, RNG rng) { this(width, height, rng, basicPoints(width, height, rng)); } /** * This prepares a map generator that will generate a map with the given width and height, using the given RNG. * This version of the constructor uses a List of Coord points from some other source to determine the path to add * rooms or caves to and then connect. You call the different carver-adding methods to affect what the * dungeon will look like, putCaveCarvers(), putBoxRoomCarvers(), and putRoundRoomCarvers(), defaulting to only * caves if none are called. You call generate() after adding carvers, which returns a char[][] for a map. * @param width the width of the final map in cells * @param height the height of the final map in cells * @param rng an RNG object to use for random choices; this make a lot of random choices. * @param sequence a List of Coord to connect in order; index 0 is the start, index size() - 1 is the end. * @see SerpentMapGenerator a class that uses this technique */ public MixedGenerator(int width, int height, RNG rng, List<Coord> sequence) { this.width = width; this.height = height; this.roomWidth = width / 64.0f; this.roomHeight = height / 64.0f; if(width <= 2 || height <= 2) throw new IllegalStateException("width and height must be greater than 2"); this.rng = rng; dungeon = new char[width][height]; environment = new int[width][height]; marked = new boolean[width][height]; walled = new boolean[width][height]; fixedRooms = new boolean[width][height]; Arrays.fill(dungeon[0], '#'); Arrays.fill(environment[0], UNTOUCHED); for (int i = 1; i < width; i++) { System.arraycopy(dungeon[0], 0, dungeon[i], 0, height); System.arraycopy(environment[0], 0, environment[i], 0, height); } totalPoints = sequence.size() - 1; points = new IntVLA(totalPoints); for (int i = 0; i < totalPoints; i++) { Coord c1 = sequence.get(i), c2 = sequence.get(i + 1); points.add(((c1.x & 0xff) << 24) | ((c1.y & 0xff) << 16) | ((c2.x & 0xff) << 8) | (c2.y & 0xff)); } carvers = new EnumMap<>(CarverType.class); } /** * This prepares a map generator that will generate a map with the given width and height, using the given RNG. * This version of the constructor uses a Map with Coord keys and Coord array values to determine a * branching path for the dungeon to take; each key will connect once to each of the Coords in its value, and you * usually don't want to connect in both directions. You call the different carver-adding methods to affect what the * dungeon will look like, putCaveCarvers(), putBoxRoomCarvers(), and putRoundRoomCarvers(), defaulting to only * caves if none are called. You call generate() after adding carvers, which returns a char[][] for a map. * @param width the width of the final map in cells * @param height the height of the final map in cells * @param rng an RNG object to use for random choices; this make a lot of random choices. * @param connections a Map of Coord keys to arrays of Coord to connect to next; shouldn't connect both ways * @see SerpentMapGenerator a class that uses this technique */ public MixedGenerator(int width, int height, RNG rng, Map<Coord, List<Coord>> connections) { this(width, height, rng, connections, 0.8f); } /** * This prepares a map generator that will generate a map with the given width and height, using the given RNG. * This version of the constructor uses a Map with Coord keys and Coord array values to determine a * branching path for the dungeon to take; each key will connect once to each of the Coords in its value, and you * usually don't want to connect in both directions. You call the different carver-adding methods to affect what the * dungeon will look like, putCaveCarvers(), putBoxRoomCarvers(), and putRoundRoomCarvers(), defaulting to only * caves if none are called. You call generate() after adding carvers, which returns a char[][] for a map. * @param width the width of the final map in cells * @param height the height of the final map in cells * @param rng an RNG object to use for random choices; this make a lot of random choices. * @param connections a Map of Coord keys to arrays of Coord to connect to next; shouldn't connect both ways * @param roomSizeMultiplier a float multiplier that will be applied to each room's width and height * @see SerpentMapGenerator a class that uses this technique */ public MixedGenerator(int width, int height, RNG rng, Map<Coord, List<Coord>> connections, float roomSizeMultiplier) { this.width = width; this.height = height; roomWidth = (width / 64.0f) * roomSizeMultiplier; roomHeight = (height / 64.0f) * roomSizeMultiplier; if(width <= 2 || height <= 2) throw new IllegalStateException("width and height must be greater than 2"); this.rng = rng; dungeon = new char[width][height]; environment = new int[width][height]; marked = new boolean[width][height]; walled = new boolean[width][height]; fixedRooms = new boolean[width][height]; Arrays.fill(dungeon[0], '#'); Arrays.fill(environment[0], UNTOUCHED); for (int i = 1; i < width; i++) { System.arraycopy(dungeon[0], 0, dungeon[i], 0, height); System.arraycopy(environment[0], 0, environment[i], 0, height); } totalPoints = 0; for(List<Coord> vals : connections.values()) { totalPoints += vals.size(); } points = new IntVLA(totalPoints); for (Map.Entry<Coord, List<Coord>> kv : connections.entrySet()) { Coord c1 = kv.getKey(); for (Coord c2 : kv.getValue()) { points.add(((c1.x & 0xff) << 24) | ((c1.y & 0xff) << 16) | ((c2.x & 0xff) << 8) | (c2.y & 0xff)); } } carvers = new EnumMap<>(CarverType.class); } /** * This prepares a map generator that will generate a map with the given width and height, using the given RNG. * This version of the constructor uses an {@link squidpony.squidgrid.mapping.locks.IRoomLayout} to set up rooms, * almost always produced by {@link squidpony.squidgrid.mapping.locks.generators.LayoutGenerator}. This method does * alter the individual Room objects inside layout, making the center of each room match where that center is placed * in the dungeon this generates. You call the different carver-adding methods to affect what the dungeon will look * like, i.e. {@link #putCaveCarvers(int)}, {@link #putBoxRoomCarvers(int)} , {@link #putRoundRoomCarvers(int)}, * {@link #putWalledBoxRoomCarvers(int)}, and {@link #putWalledRoundRoomCarvers(int)}, defaulting to only caves if * none are called (using rooms is recommended for this constructor). You call generate() after adding carvers, * which returns a char[][] for a map and sets the environment to be fetched with {@link #getEnvironment()}, which * is usually needed for {@link SectionDungeonGenerator} to correctly place doors and various other features. * @param width the width of the final map in cells * @param height the height of the final map in cells * @param rng an RNG object to use for random choices; this make a lot of random choices. * @param layout an IRoomLayout that will almost always be produced by LayoutGenerator; the rooms will be altered * @param roomSizeMultiplier a float multiplier that will be applied to each room's width and height * @see SerpentMapGenerator a class that uses this technique */ public MixedGenerator(int width, int height, RNG rng, IRoomLayout layout, float roomSizeMultiplier) { this.width = width; this.height = height; Rect2I bounds = layout.getExtentBounds(); int offX = bounds.getBottomLeft().x, offY = bounds.getBottomLeft().y; float rw = (width) / (bounds.width+1f), rh = (height) / (bounds.height+1f); this.roomWidth = roomSizeMultiplier * rw * 0.125f; this.roomHeight = roomSizeMultiplier * rh * 0.125f; if(width <= 2 || height <= 2) throw new IllegalStateException("width and height must be greater than 2"); this.rng = rng; dungeon = new char[width][height]; environment = new int[width][height]; marked = new boolean[width][height]; walled = new boolean[width][height]; fixedRooms = new boolean[width][height]; ArrayTools.fill(dungeon, '#'); ArrayTools.fill(environment, UNTOUCHED); totalPoints = layout.roomCount(); points = new IntVLA(totalPoints); Coord c2; Set<Room> rooms = layout.getRooms(), removing = new HashSet<>(rooms); Room t; for (Room room : rooms) { Coord c1 = room.getCenter(); if (!bounds.contains(c1)) { removing.remove(room); } else { room.setCenter(Coord.get( (int) ((c1.x - offX + 0.75f) * (rw)) & 0xff, (int) ((c1.y - offY + 0.75f) * (rh)) & 0xff)); } } for (Room room : rooms) { Coord c1 = room.getCenter(); for (Edge e : room.getEdges()) { if (removing.contains(t = layout.get(e.getTargetRoomId()))) { c2 = t.getCenter(); points.add((c1.x << 24) | (c1.y << 16) | (c2.x << 8) | c2.y); } } removing.remove(room); } totalPoints = points.size; carvers = new EnumMap<>(CarverType.class); } /** * Changes the number of "carvers" that will create caves from one room to the next. If count is 0 or less, no caves * will be made. If count is at least 1, caves are possible, and higher numbers relative to the other carvers make * caves more likely. Carvers are shuffled when used, then repeat if exhausted during generation. Since typically * about 30-40 rooms are carved, large totals for carver count aren't really needed; aiming for a total of 10 * between the count of putCaveCarvers(), putBoxRoomCarvers(), putRoundRoomCarvers(), putWalledBoxRoomCarvers(), and * putWalledRoundRoomCarvers() is reasonable. * @param count the number of carvers making caves between rooms; only matters in relation to other carvers */ public void putCaveCarvers(int count) { carvers.put(CarverType.CAVE, count); } /** * Changes the number of "carvers" that will create right-angle corridors from one room to the next, create rooms * with a random size in a box shape at the start and end, and a small room at the corner if there is one. If count * is 0 or less, no box-shaped rooms will be made. If count is at least 1, box-shaped rooms are possible, and higher * numbers relative to the other carvers make box-shaped rooms more likely. Carvers are shuffled when used, then * repeat if exhausted during generation. Since typically about 30-40 rooms are carved, large totals for carver * count aren't really needed; aiming for a total of 10 between the count of putCaveCarvers(), putBoxRoomCarvers(), * putRoundRoomCarvers(), putWalledBoxRoomCarvers(), and putWalledRoundRoomCarvers() is reasonable. * @param count the number of carvers making box-shaped rooms and corridors between them; only matters in relation * to other carvers */ public void putBoxRoomCarvers(int count) { carvers.put(CarverType.BOX, count); } /** * Changes the number of "carvers" that will create right-angle corridors from one room to the next, create rooms * with a random size in a circle shape at the start and end, and a small circular room at the corner if there is * one. If count is 0 or less, no circular rooms will be made. If count is at least 1, circular rooms are possible, * and higher numbers relative to the other carvers make circular rooms more likely. Carvers are shuffled when used, * then repeat if exhausted during generation. Since typically about 30-40 rooms are carved, large totals for carver * count aren't really needed; aiming for a total of 10 between the count of putCaveCarvers(), putBoxRoomCarvers(), * putRoundRoomCarvers(), putWalledBoxRoomCarvers(), and putWalledRoundRoomCarvers() is reasonable. * @param count the number of carvers making circular rooms and corridors between them; only matters in relation * to other carvers */ public void putRoundRoomCarvers(int count) { carvers.put(CarverType.ROUND, count); } /** * Changes the number of "carvers" that will create right-angle corridors from one room to the next, create rooms * with a random size in a box shape at the start and end, and a small room at the corner if there is one, enforcing * the presence of walls around the rooms even if another room is already there or would be placed there. Corridors * can always pass through enforced walls, but caves will open at most one cell in the wall. If count * is 0 or less, no box-shaped rooms will be made. If count is at least 1, box-shaped rooms are possible, and higher * numbers relative to the other carvers make box-shaped rooms more likely. Carvers are shuffled when used, then * repeat if exhausted during generation. Since typically about 30-40 rooms are carved, large totals for carver * count aren't really needed; aiming for a total of 10 between the count of putCaveCarvers(), putBoxRoomCarvers(), * putRoundRoomCarvers(), putWalledBoxRoomCarvers(), and putWalledRoundRoomCarvers() is reasonable. * @param count the number of carvers making box-shaped rooms and corridors between them; only matters in relation * to other carvers */ public void putWalledBoxRoomCarvers(int count) { carvers.put(CarverType.BOX_WALLED, count); } /** * Changes the number of "carvers" that will create right-angle corridors from one room to the next, create rooms * with a random size in a circle shape at the start and end, and a small circular room at the corner if there is * one, enforcing the presence of walls around the rooms even if another room is already there or would be placed * there. Corridors can always pass through enforced walls, but caves will open at most one cell in the wall. If * count is 0 or less, no circular rooms will be made. If count is at least 1, circular rooms are possible, * and higher numbers relative to the other carvers make circular rooms more likely. Carvers are shuffled when used, * then repeat if exhausted during generation. Since typically about 30-40 rooms are carved, large totals for carver * count aren't really needed; aiming for a total of 10 between the count of putCaveCarvers(), putBoxRoomCarvers(), * putRoundRoomCarvers(), putWalledBoxRoomCarvers(), and putWalledRoundRoomCarvers() is reasonable. * @param count the number of carvers making circular rooms and corridors between them; only matters in relation * to other carvers */ public void putWalledRoundRoomCarvers(int count) { carvers.put(CarverType.ROUND_WALLED, count); } /** * Uses the added carvers (or just makes caves if none were added) to carve from point to point in sequence, if it * was provided by the constructor, or evenly-spaced randomized points if it was not. This will never carve out * cells on the very edge of the map. Uses the numbers of the various kinds of carver that were added relative to * each other to determine how frequently to use a given carver type. * @return a char[][] where '#' is a wall and '.' is a floor or corridor; x first y second */ public char[][] generate() { CarverType[] carvings = carvers.keySet().toArray(new CarverType[carvers.size()]); int[] carvingsCounters = new int[carvings.length]; int totalLength = 0; for (int i = 0; i < carvings.length; i++) { carvingsCounters[i] = carvers.get(carvings[i]); totalLength += carvingsCounters[i]; } CarverType[] allCarvings = new CarverType[totalLength]; for (int i = 0, c = 0; i < carvings.length; i++) { for (int j = 0; j < carvingsCounters[i]; j++) { allCarvings[c++] = carvings[i]; } } if(allCarvings.length == 0) { allCarvings = new CarverType[]{CarverType.CAVE}; totalLength = 1; } else allCarvings = rng.shuffle(allCarvings, new CarverType[allCarvings.length]); for (int p = 0, c = 0; p < totalPoints; p++, c = (c+1) % totalLength) { int pair = points.get(p); Coord start = Coord.get(pair >>> 24 & 0xff, pair >>> 16 & 0xff), end = Coord.get(pair >>> 8 & 0xff, pair & 0xff); CarverType ct = allCarvings[c]; Direction dir; switch (ct) { case CAVE: markPiercing(end); markEnvironmentCave(end.x, end.y); store(); double weight = 0.75; do { Coord cent = markPlusCave(start); if(cent != null) { markPiercingCave(cent); markPiercingCave(cent.translate(1, 0)); markPiercingCave(cent.translate(-1, 0)); markPiercingCave(cent.translate(0, 1)); markPiercingCave(cent.translate(0, -1)); weight = 0.95; } dir = stepWobbly(start, end, weight); start = start.translate(dir); }while (dir != Direction.NONE); break; case BOX: markRectangle(end, rng.between(1, 5), rng.between(1, 5)); markRectangle(start, rng.between(1, 4), rng.between(1, 4)); store(); dir = Direction.getDirection(end.x - start.x, end.y - start.y); if(dir.isDiagonal()) dir = rng.nextBoolean() ? Direction.getCardinalDirection(dir.deltaX, 0) : Direction.getCardinalDirection(0, -dir.deltaY); while (start.x != end.x && start.y != end.y) { markPiercing(start); markEnvironmentCorridor(start.x, start.y); start = start.translate(dir); } markRectangle(start, 1, 1); dir = Direction.getCardinalDirection(end.x - start.x, -(end.y - start.y)); while (!(start.x == end.x && start.y == end.y)) { markPiercing(start); markEnvironmentCorridor(start.x, start.y); start = start.translate(dir); } break; case BOX_WALLED: markRectangleWalled(end, rng.between(1, 5), rng.between(1, 5)); markRectangleWalled(start, rng.between(1, 4), rng.between(1, 4)); store(); dir = Direction.getDirection(end.x - start.x, end.y - start.y); if(dir.isDiagonal()) dir = rng.nextBoolean() ? Direction.getCardinalDirection(dir.deltaX, 0) : Direction.getCardinalDirection(0, -dir.deltaY); while (start.x != end.x && start.y != end.y) { markPiercing(start); markEnvironmentCorridor(start.x, start.y); start = start.translate(dir); } markRectangleWalled(start, 1, 1); dir = Direction.getCardinalDirection(end.x - start.x, -(end.y - start.y)); while (!(start.x == end.x && start.y == end.y)) { markPiercing(start); markEnvironmentCorridor(start.x, start.y); start = start.translate(dir); } break; case ROUND: markCircle(end, rng.between(2, 6)); markCircle(start, rng.between(2, 6)); store(); dir = Direction.getDirection(end.x - start.x, end.y - start.y); if(dir.isDiagonal()) dir = rng.nextBoolean() ? Direction.getCardinalDirection(dir.deltaX, 0) : Direction.getCardinalDirection(0, -dir.deltaY); while (start.x != end.x && start.y != end.y) { markPiercing(start); markEnvironmentCorridor(start.x, start.y); start = start.translate(dir); } markCircle(start, 2); dir = Direction.getCardinalDirection(end.x - start.x, -(end.y - start.y)); while (!(start.x == end.x && start.y == end.y)) { markPiercing(start); markEnvironmentCorridor(start.x, start.y); start = start.translate(dir); } break; case ROUND_WALLED: markCircleWalled(end, rng.between(2, 6)); markCircleWalled(start, rng.between(2, 6)); store(); dir = Direction.getDirection(end.x - start.x, end.y - start.y); if(dir.isDiagonal()) dir = rng.nextBoolean() ? Direction.getCardinalDirection(dir.deltaX, 0) : Direction.getCardinalDirection(0, -dir.deltaY); while (start.x != end.x && start.y != end.y) { markPiercing(start); markEnvironmentCorridor(start.x, start.y); start = start.translate(dir); } markCircleWalled(start, 2); dir = Direction.getCardinalDirection(end.x - start.x, -(end.y - start.y)); while (!(start.x == end.x && start.y == end.y)) { markPiercing(start); markEnvironmentCorridor(start.x, start.y); start = start.translate(dir); } break; } store(); } for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if(fixedRooms[x][y]) markPiercingRoom(x, y); } } store(); markEnvironmentWalls(); generated = true; return dungeon; } public int[][] getEnvironment() { return environment; } public boolean hasGenerated() { return generated; } public boolean[][] getFixedRooms() { return fixedRooms; } public void setFixedRooms(boolean[][] fixedRooms) { this.fixedRooms = fixedRooms; } /** * Internal use. Takes cells that have been previously marked and permanently stores them as floors in the dungeon. */ protected void store() { for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { if(marked[i][j]) { dungeon[i][j] = '.'; marked[i][j] = false; } } } } /** * Internal use. Finds all floor cells by environment and marks untouched adjacent (8-way) cells as walls, using the * appropriate type for the nearby floor. */ protected void markEnvironmentWalls() { for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { if(environment[i][j] == UNTOUCHED) { boolean allWalls = true; //lowest precedence, also checks for any floors for (int x = Math.max(0, i - 1); x <= Math.min(width - 1, i + 1); x++) { for (int y = Math.max(0, j - 1); y <= Math.min(height - 1, j + 1); y++) { if (environment[x][y] == CORRIDOR_FLOOR) { markEnvironment(i, j, CORRIDOR_WALL); } if(dungeon[x][y] == '.') allWalls = false; } } //if there are no floors we don't need to check twice again. if(allWalls) continue; //more precedence for (int x = Math.max(0, i - 1); x <= Math.min(width - 1, i + 1); x++) { for (int y = Math.max(0, j - 1); y <= Math.min(height - 1, j + 1); y++) { if (environment[x][y] == CAVE_FLOOR) { markEnvironment(i, j, CAVE_WALL); } } } //highest precedence for (int x = Math.max(0, i - 1); x <= Math.min(width - 1, i + 1); x++) { for (int y = Math.max(0, j - 1); y <= Math.min(height - 1, j + 1); y++) { if (environment[x][y] == ROOM_FLOOR) { markEnvironment(i, j, ROOM_WALL); } } } } } } } /** * Internal use. Marks a point to be made into floor. * @param x x position to mark * @param y y position to mark * @return false if everything is normal, true if and only if this failed to mark because the position is walled */ protected boolean mark(int x, int y) { if (x > 0 && x < width - 1 && y > 0 && y < height - 1 && !walled[x][y]) { marked[x][y] = true; return false; } else return x > 0 && x < width - 1 && y > 0 && y < height - 1 && walled[x][y]; } /** * Internal use. Marks a point to be made into floor. * @param x x position to mark * @param y y position to mark */ protected void markPiercing(int x, int y) { if (x > 0 && x < width - 1 && y > 0 && y < height - 1) { marked[x][y] = true; } } /** * Internal use. Marks a point's environment type as the appropriate kind of environment. * @param x x position to mark * @param y y position to mark * @param kind an int that should be one of the constants in MixedGenerator for environment types. */ protected void markEnvironment(int x, int y, int kind) { environment[x][y] = kind; } /** * Internal use. Marks a point's environment type as a corridor floor. * @param x x position to mark * @param y y position to mark */ protected void markEnvironmentCorridor(int x, int y) { if (x > 0 && x < width - 1 && y > 0 && y < height - 1 && environment[x][y] != ROOM_FLOOR && environment[x][y] != CAVE_FLOOR) { markEnvironment(x, y, CORRIDOR_FLOOR); } } /** * Internal use. Marks a point's environment type as a room floor. * @param x x position to mark * @param y y position to mark */ protected void markEnvironmentRoom(int x, int y) { if (x > 0 && x < width - 1 && y > 0 && y < height - 1) { markEnvironment(x, y, ROOM_FLOOR); } } /** * Internal use. Marks a point's environment type as a cave floor. * @param x x position to mark * @param y y position to mark */ protected void markEnvironmentCave(int x, int y) { if (x > 0 && x < width - 1 && y > 0 && y < height - 1 && environment[x][y] != ROOM_FLOOR) { markEnvironment(x, y, CAVE_FLOOR); } } /** * Internal use. Marks a point to be made into floor. * @param x x position to mark * @param y y position to mark */ protected void wallOff(int x, int y) { if (x > 0 && x < width - 1 && y > 0 && y < height - 1) { walled[x][y] = true; } } /** * Internal use. Marks a point to be made into floor. * @param pos position to mark * @return false if everything is normal, true if and only if this failed to mark because the position is walled */ protected boolean mark(Coord pos) { return mark(pos.x, pos.y); } /** * Internal use. Marks a point to be made into floor, piercing walls. * @param pos position to mark */ protected void markPiercing(Coord pos) { markPiercing(pos.x, pos.y); } /** * Internal use. Marks a point to be made into floor, piercing walls, and also marks the point as a cave floor. * @param pos position to mark */ protected void markPiercingCave(Coord pos) { markPiercing(pos.x, pos.y); markEnvironmentCave(pos.x, pos.y); } /** * Internal use. Marks a point to be made into floor, piercing walls, and also marks the point as a room floor. * @param x x coordinate of position to mark * @param y y coordinate of position to mark */ protected void markPiercingRoom(int x, int y) { markPiercing(x, y); markEnvironmentCave(x, y); } /** * Internal use. Marks a point and the four cells orthogonally adjacent to it. * @param pos center position to mark * @return null if the center of the plus shape wasn't blocked by wall, otherwise the Coord of the center */ private Coord markPlus(Coord pos) { Coord block = null; if (mark(pos.x, pos.y)) block = pos; mark(pos.x + 1, pos.y); mark(pos.x - 1, pos.y); mark(pos.x, pos.y + 1); mark(pos.x, pos.y - 1); return block; } /** * Internal use. Marks a point and the four cells orthogonally adjacent to it, and also marks any cells that weren't * blocked as cave floors. * @param pos center position to mark * @return null if the center of the plus shape wasn't blocked by wall, otherwise the Coord of the center */ private Coord markPlusCave(Coord pos) { Coord block = null; if (mark(pos.x, pos.y)) block = pos; else markEnvironmentCave(pos.x, pos.y); if(!mark(pos.x + 1, pos.y)) markEnvironmentCave(pos.x + 1, pos.y); if(!mark(pos.x - 1, pos.y)) markEnvironmentCave(pos.x - 1, pos.y); if(!mark(pos.x, pos.y + 1)) markEnvironmentCave(pos.x, pos.y + 1); if(!mark(pos.x, pos.y - 1)) markEnvironmentCave(pos.x, pos.y - 1); return block; } /** * Internal use. Marks a rectangle of points centered on pos, extending halfWidth in both x directions and * halfHeight in both vertical directions. Marks all cells in the rectangle as room floors. * @param pos center position to mark * @param halfWidth the distance from the center to extend horizontally * @param halfHeight the distance from the center to extend vertically * @return null if no points in the rectangle were blocked by walls, otherwise a Coord blocked by a wall */ private Coord markRectangle(Coord pos, int halfWidth, int halfHeight) { halfWidth = Math.max(1, Math.round(halfWidth * roomWidth)); halfHeight = Math.max(1, Math.round(halfHeight * roomHeight)); Coord block = null; for (int i = pos.x - halfWidth; i <= pos.x + halfWidth; i++) { for (int j = pos.y - halfHeight; j <= pos.y + halfHeight; j++) { if(mark(i, j)) block = Coord.get(i, j); else markEnvironmentRoom(i, j); } } return block; } /** * Internal use. Marks a rectangle of points centered on pos, extending halfWidth in both x directions and * halfHeight in both vertical directions. Also considers the area just beyond each wall, but not corners, to be * a blocking wall that can only be passed by corridors and small cave openings. Marks all cells in the rectangle as * room floors. * @param pos center position to mark * @param halfWidth the distance from the center to extend horizontally * @param halfHeight the distance from the center to extend vertically * @return null if no points in the rectangle were blocked by walls, otherwise a Coord blocked by a wall */ private Coord markRectangleWalled(Coord pos, int halfWidth, int halfHeight) { halfWidth = Math.max(1, Math.round(halfWidth * roomWidth)); halfHeight = Math.max(1, Math.round(halfHeight * roomHeight)); Coord block = null; for (int i = pos.x - halfWidth; i <= pos.x + halfWidth; i++) { for (int j = pos.y - halfHeight; j <= pos.y + halfHeight; j++) { if(mark(i, j)) block = Coord.get(i, j); else markEnvironmentRoom(i, j); } } for (int i = Math.max(0, pos.x - halfWidth - 1); i <= Math.min(width - 1, pos.x + halfWidth + 1); i++) { for (int j = Math.max(0, pos.y - halfHeight - 1); j <= Math.min(height - 1, pos.y + halfHeight + 1); j++) { wallOff(i, j); } } return block; } /** * Internal use. Marks a circle of points centered on pos, extending out to radius in Euclidean measurement. Marks * all cells in the circle as room floors. * @param pos center position to mark * @param radius radius to extend in all directions from center * @return null if no points in the circle were blocked by walls, otherwise a Coord blocked by a wall */ private Coord markCircle(Coord pos, int radius) { Coord block = null; int high; radius = Math.max(1, Math.round(radius * Math.min(roomWidth, roomHeight))); for (int dx = -radius; dx <= radius; ++dx) { high = (int)Math.floor(Math.sqrt(radius * radius - dx * dx)); for (int dy = -high; dy <= high; ++dy) { if(mark(pos.x + dx, pos.y + dy)) block = pos.translate(dx, dy); else markEnvironmentRoom(pos.x + dx, pos.y + dy); } } return block; } /** * Internal use. Marks a circle of points centered on pos, extending out to radius in Euclidean measurement. * Also considers the area just beyond each wall, but not corners, to be a blocking wall that can only be passed by * corridors and small cave openings. Marks all cells in the circle as room floors. * @param pos center position to mark * @param radius radius to extend in all directions from center * @return null if no points in the circle were blocked by walls, otherwise a Coord blocked by a wall */ private Coord markCircleWalled(Coord pos, int radius) { Coord block = null; int high; radius = Math.max(1, Math.round(radius * Math.min(roomWidth, roomHeight))); for (int dx = -radius; dx <= radius; ++dx) { high = (int)Math.floor(Math.sqrt(radius * radius - dx * dx)); for (int dy = -high; dy <= high; ++dy) { if(mark(pos.x + dx, pos.y + dy)) block = pos.translate(dx, dy); else markEnvironmentRoom(pos.x + dx, pos.y + dy); } } for (int dx = -radius; dx <= radius; ++dx) { high = (int)Math.floor(Math.sqrt(radius * radius - dx * dx)); int dx2 = Math.max(1, Math.min(pos.x + dx, width - 2)); for (int dy = -high; dy <= high; ++dy) { int dy2 = Math.max(1, Math.min(pos.y + dy, height - 2)); wallOff(dx2, dy2-1); wallOff(dx2+1, dy2-1); wallOff(dx2-1, dy2-1); wallOff(dx2, dy2); wallOff(dx2+1, dy2); wallOff(dx2-1, dy2); wallOff(dx2, dy2+1); wallOff(dx2+1, dy2+1); wallOff(dx2-1, dy2+1); } } return block; } /** * Internal use. Drunkard's walk algorithm, single step. Based on Michael Patraw's C code, used for cave carving. * http://mpatraw.github.io/libdrunkard/ * @param current the current point * @param target the point to wobble towards * @param weight between 0.5 and 1.0, usually. 0.6 makes very random caves, 0.9 is almost a straight line. * @return a Direction, either UP, DOWN, LEFT, or RIGHT if we should move, or NONE if we have reached our target */ private Direction stepWobbly(Coord current, Coord target, double weight) { int dx = target.x - current.x; int dy = target.y - current.y; if (dx > 1) dx = 1; if (dx < -1) dx = -1; if (dy > 1) dy = 1; if (dy < -1) dy = -1; double r = rng.nextDouble(); Direction dir; if (dx == 0 && dy == 0) { return Direction.NONE; } else if (dx == 0 || dy == 0) { int dx2 = (dx == 0) ? dx : dy, dy2 = (dx == 0) ? dy : dx; if (r >= (weight * 0.5)) { r -= weight * 0.5; if (r < weight * (1.0 / 6) + (1 - weight) * (1.0 / 3)) { dx2 = -1; dy2 = 0; } else if (r < weight * (2.0 / 6) + (1 - weight) * (2.0 / 3)) { dx2 = 1; dy2 = 0; } else { dx2 = 0; dy2 *= -1; } } dir = Direction.getCardinalDirection(dx2, -dy2); } else { if (r < weight * 0.5) { dy = 0; } else if (r < weight) { dx = 0; } else if (r < weight + (1 - weight) * 0.5) { dx *= -1; dy = 0; } else { dx = 0; dy *= -1; } dir = Direction.getCardinalDirection(dx, -dy); } if(current.x + dir.deltaX <= 0 || current.x + dir.deltaX >= width - 1) { if (current.y < target.y) dir = Direction.DOWN; else if (current.y > target.y) dir = Direction.UP; } else if(current.y + dir.deltaY <= 0 || current.y + dir.deltaY >= height - 1) { if (current.x < target.x) dir = Direction.RIGHT; else if (current.x > target.x) dir = Direction.LEFT; } return dir; } }