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