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}