001package squidpony.squidgrid.mapping; 002 003import squidpony.ArrayTools; 004import squidpony.annotation.Beta; 005import squidpony.squidmath.Coord; 006import squidpony.squidmath.GreasedRegion; 007import squidpony.squidmath.NumberTools; 008 009import java.io.Serializable; 010import java.util.ArrayList; 011 012/** 013 * A subsection of a (typically modern-day or sci-fi) area map that can be placed by ModularMapGenerator. 014 * <br> 015 * This is marked Beta because both MapModule and {@link ModularMapGenerator} need improvement to be actually usable, 016 * but it might be a while before there's a clear pathway towards how they can be improved. 017 * <br> 018 * Created by Tommy Ettinger on 4/4/2016. 019 */ 020@Beta 021public class MapModule implements Comparable<MapModule>, Serializable { 022 private static final long serialVersionUID = -1273406898212937188L; 023 024 /** 025 * The contents of this section of map. 026 */ 027 public char[][] map; 028 /** 029 * The room/cave/corridor/wall status for each cell of this section of map. 030 */ 031 public int[][] environment; 032 /** 033 * Stores Coords just outside the contents of the MapModule, where doors are allowed to connect into this. 034 * Uses Coord positions that are relative to this MapModule's map field, not whatever this is being placed into. 035 */ 036 public GreasedRegion validDoors; 037 /** 038 * The minimum point on the bounding rectangle of the room, including walls. 039 */ 040 public Coord min; 041 /** 042 * The maximum point on the bounding rectangle of the room, including walls. 043 */ 044 public Coord max; 045 046 public ArrayList<Coord> leftDoors, rightDoors, topDoors, bottomDoors; 047 048 public int category; 049 050 private static final char[] 051 validPacking = new char[]{'.', ',', '"', '^', '<', '>'}, 052 doors = new char[]{'+', '/'}; 053 public MapModule() 054 { 055 this(DungeonUtility.wallWrap(ArrayTools.fill('.', 8, 8))); 056 } 057 058 /** 059 * Constructs a MapModule given only a 2D char array as the contents of this section of map. The actual MapModule 060 * will use doors in the 2D char array as '+' or '/' if present. Otherwise, the valid locations for doors will be 061 * any outer wall adjacent to a floor ('.'), shallow water (','), grass ('"'), trap ('^'), or staircase (less than 062 * or greater than signs). The max and min Coords of the bounding rectangle, including one layer of outer walls, 063 * will also be calculated. The map you pass to this does need to have outer walls present in it already. 064 * @param map the 2D char array that contains the contents of this section of map 065 */ 066 public MapModule(char[][] map) 067 { 068 if(map == null || map.length <= 0) 069 throw new UnsupportedOperationException("Given map cannot be empty in MapModule"); 070 this.map = ArrayTools.copy(map); 071 environment = ArrayTools.fill(DungeonUtility.ROOM_FLOOR, this.map.length, this.map[0].length); 072 for (int x = 0; x < map.length; x++) { 073 for (int y = 0; y < map[0].length; y++) { 074 if(this.map[x][y] == '#') 075 environment[x][y] = DungeonUtility.ROOM_WALL; 076 } 077 } 078 GreasedRegion pack = new GreasedRegion(this.map, validPacking).fringe(); 079// short[] pk = CoordPacker.fringe( 080// CoordPacker.pack(this.map, validPacking), 081// 1, this.map.length, this.map[0].length, false, true); 082// Coord[] tmp = CoordPacker.bounds(pk); 083 min = Coord.get(pack.xBound(true), pack.yBound(true)); 084 max = Coord.get(pack.xBound(false), pack.yBound(false)); 085 category = categorize(Math.max(max.x, max.y)); 086 validDoors = new GreasedRegion(this.map, doors); 087 if(validDoors.size() < 2) 088 { 089 validDoors.remake(pack).quasiRandomRegion(0.2); 090 } 091 initSides(); 092 } 093 094 /** 095 * Constructs a MapModule from the given arguments without modifying them, copying map without changing its size, 096 * copying validDoors, and using the same min and max (which are immutable, so they can be reused). 097 * @param map the 2D char array that contains the contents of this section of map; will be copied exactly 098 * @param validDoors a Coord array that stores viable locations to place doors in map; will be cloned 099 * @param min the minimum Coord of this MapModule's bounding rectangle 100 * @param max the maximum Coord of this MapModule's bounding rectangle 101 */ 102 public MapModule(char[][] map, GreasedRegion validDoors, Coord min, Coord max) 103 { 104 this.map = ArrayTools.copy(map); 105 environment = ArrayTools.fill(DungeonUtility.ROOM_FLOOR, this.map.length, this.map[0].length); 106 for (int x = 0; x < map.length; x++) { 107 for (int y = 0; y < map[0].length; y++) { 108 if(this.map[x][y] == '#') 109 environment[x][y] = DungeonUtility.ROOM_WALL; 110 } 111 } 112 this.min = min; 113 this.max = max; 114 category = categorize(Math.max(max.x, max.y)); 115 116 this.validDoors = new GreasedRegion(map, doors); 117 if(this.validDoors.isEmpty()) this.validDoors.remake(validDoors); 118 119 initSides(); 120 } 121 122 /** 123 * Copies another MapModule and uses it to construct a new one. 124 * @param other an already-constructed MapModule that this will copy 125 */ 126 public MapModule(MapModule other) 127 { 128 this(other.map, other.validDoors, other.min, other.max); 129 } 130 131 /** 132 * Rotates a copy of this MapModule by the given number of 90-degree turns. Describing the turns as clockwise or 133 * counter-clockwise depends on whether the y-axis "points up" or "points down." If higher values for y are toward the 134 * bottom of the screen (the default for when 2D arrays are printed), a turn of 1 is clockwise 90 degrees, but if the 135 * opposite is true and higher y is toward the top, then a turn of 1 is counter-clockwise 90 degrees. 136 * @param turns the number of 90 degree turns to adjust this by 137 * @return a new MapModule (copied from this one) that has been rotated by the given amount 138 */ 139 public MapModule rotate(int turns) 140 { 141 turns &= 3; 142 char[][] map2; 143 Coord min2, max2; 144 GreasedRegion doors2; 145 int xSize = map.length - 1, ySize = map[0].length - 1; 146 switch (turns) 147 { 148 case 1: 149 map2 = new char[map[0].length][map.length]; 150 for (int i = 0; i < map.length; i++) { 151 for (int j = 0; j < map[0].length; j++) { 152 map2[ySize - j][i] = map[i][j]; 153 } 154 } 155 doors2 = new GreasedRegion(validDoors.height, validDoors.width); 156 for (int i = 0; i < validDoors.size(); i++) { 157 Coord n = validDoors.nth(i); 158 doors2.insert(ySize - n.y, n.x); 159 } 160 min2 = Coord.get(ySize - max.y, min.x); 161 max2 = Coord.get(ySize - min.y, max.x); 162 return new MapModule(map2, doors2, min2, max2); 163 case 2: 164 map2 = new char[map.length][map[0].length]; 165 for (int i = 0; i < map.length; i++) { 166 for (int j = 0; j < map[0].length; j++) { 167 map2[xSize - i][ySize - j] = map[i][j]; 168 } 169 } 170 doors2 = new GreasedRegion(validDoors.width, validDoors.height); 171 for (int i = 0; i < validDoors.size(); i++) { 172 Coord n = validDoors.nth(i); 173 doors2.insert(xSize - n.x, ySize - n.y); 174 } 175 min2 = Coord.get(xSize - max.x, ySize - max.y); 176 max2 = Coord.get(xSize - min.x, ySize - min.y); 177 return new MapModule(map2, doors2, min2, max2); 178 case 3: 179 map2 = new char[map[0].length][map.length]; 180 for (int i = 0; i < map.length; i++) { 181 for (int j = 0; j < map[0].length; j++) { 182 map2[j][xSize - i] = map[i][j]; 183 } 184 } 185 doors2 = new GreasedRegion(validDoors.height, validDoors.width); 186 for (int i = 0; i < validDoors.size(); i++) { 187 Coord n = validDoors.nth(i); 188 doors2.insert(n.y, xSize - n.x); 189 } 190 min2 = Coord.get(min.y, xSize - max.x); 191 max2 = Coord.get(max.y, xSize - min.x); 192 return new MapModule(map2, doors2, min2, max2); 193 default: 194 return new MapModule(map, validDoors, min, max); 195 } 196 } 197 198 public MapModule flip(boolean flipLeftRight, boolean flipUpDown) 199 { 200 if(!flipLeftRight && !flipUpDown) 201 return new MapModule(map, validDoors, min, max); 202 char[][] map2 = new char[map.length][map[0].length]; 203 GreasedRegion doors2 = new GreasedRegion(map.length, map[0].length); 204 Coord min2, max2; 205 int xSize = map.length - 1, ySize = map[0].length - 1; 206 if(flipLeftRight && flipUpDown) 207 { 208 for (int i = 0; i < map.length; i++) { 209 for (int j = 0; j < map[0].length; j++) { 210 map2[xSize - i][ySize - j] = map[i][j]; 211 } 212 } 213 for (int i = 0; i < validDoors.size(); i++) { 214 Coord n = validDoors.nth(i); 215 doors2.insert(xSize - n.x, ySize - n.y); 216 } 217 min2 = Coord.get(xSize - max.x, ySize - max.y); 218 max2 = Coord.get(xSize - min.x, xSize - min.y); 219 } 220 else if(flipLeftRight) 221 { 222 for (int i = 0; i < map.length; i++) { 223 System.arraycopy(map[i], 0, map2[xSize - i], 0, map[0].length); 224 } 225 for (int i = 0; i < validDoors.size(); i++) { 226 Coord n = validDoors.nth(i); 227 doors2.insert(xSize - n.x, n.y); 228 } 229 min2 = Coord.get(xSize - max.x, min.y); 230 max2 = Coord.get(xSize - min.x, max.y); 231 } 232 else 233 { 234 for (int i = 0; i < map.length; i++) { 235 for (int j = 0; j < map[0].length; j++) { 236 map2[i][ySize - j] = map[i][j]; 237 } 238 } 239 for (int i = 0; i < validDoors.size(); i++) { 240 Coord n = validDoors.nth(i); 241 doors2.insert(n.x, ySize - n.y); 242 } 243 min2 = Coord.get(min.x, ySize - max.y); 244 max2 = Coord.get(max.x, xSize - min.y); 245 } 246 return new MapModule(map2, doors2, min2, max2); 247 } 248 249 private static int categorize(int n) 250 { 251 int highest = Integer.highestOneBit(n); 252 return Math.max(4, (highest == NumberTools.lowestOneBit(n)) ? highest : highest << 1); 253 } 254 private void initSides() 255 { 256 leftDoors = new ArrayList<>(8); 257 rightDoors = new ArrayList<>(8); 258 topDoors = new ArrayList<>(8); 259 bottomDoors = new ArrayList<>(8); 260 for(Coord dr : validDoors) 261 { 262 if(dr.x * max.y < dr.y * max.x && dr.y * max.x < (max.x - dr.x) * max.y) 263 leftDoors.add(dr); 264 else if(dr.x * max.y > dr.y * max.x && dr.y * max.x > (max.x - dr.x) * max.y) 265 rightDoors.add(dr); 266 else if(dr.x * max.y > dr.y * max.x && dr.y * max.x < (max.x - dr.x) * max.y) 267 topDoors.add(dr); 268 else if(dr.x * max.y < dr.y * max.x && dr.y * max.x > (max.x - dr.x) * max.y) 269 bottomDoors.add(dr); 270 } 271 } 272 273 @Override 274 public int compareTo(MapModule o) { 275 if(o == null) return 1; 276 return category - o.category; 277 } 278}