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}