001package squidpony.squidgrid.mapping;
002
003import squidpony.ArrayTools;
004import squidpony.squidai.DijkstraMap;
005import squidpony.squidgrid.Measurement;
006import squidpony.squidgrid.mapping.styled.DungeonBoneGen;
007import squidpony.squidgrid.mapping.styled.TilesetType;
008import squidpony.squidmath.*;
009
010import java.util.*;
011
012/**
013 * A good way to create a more-complete dungeon, layering different effects and modifications on top of a dungeon
014 * produced by DungeonBoneGen or another dungeon without such effects. Unlike DungeonGenerator, this class uses
015 * environment information for the dungeons it is given (or quickly generates such information if using DungeonBoneGen),
016 * and uses that information to only place effects like grass or water where you specify, like "only in caves", or
017 * "doors should never be in caves". Ensures only connected regions of the map are used by filling unreachable areas
018 * with walls, and can find far-apart staircase positions if generate() is used or can keep existing staircases in a map
019 * if generateRespectingStairs() is used.
020 * <br>
021 * The main technique for using this is simple: Construct a DungeonGenerator, usually with the desired width and height,
022 * then call any feature adding methods that you want in the dungeon, like addWater(), addTraps, addGrass(), or
023 * addDoors(). All of these methods except addDoors() take an int argument that corresponds to a constant in this class,
024 * CAVE, CORRIDOR, or ROOM, or ALL, and they will only cause the requested feature to show up in that environment. Some
025 * of these take different parameters, like addDoors() which needs to know if it should check openings that are two
026 * cells wide to add a door and a wall to, or whether it should only add doors to single-cell openings. In the case of
027 * addDoors(), it doesn't take an environment argument since doors almost always are between environments (rooms and
028 * corridors), so placing them only within one or the other doesn't make sense. This class, unlike the normal
029 * DungeonGenerator, also has an addLake() method, which, like addDoors(), doesn't take an environment parameter. It can
030 * be used to turn a large section of what would otherwise be walls into a lake (of some character for deep lake cells
031 * and some character for shallow lake cells), and corridors that cross the lake become bridges, shown as ':'. It should
032 * be noted that because the lake fills walls, it doesn't change the connectivity of the map unless you can cross the
033 * lake. There's also addMaze(), which does change the connectivity by replacing sections of impassable walls with
034 * twisty, maze-like passages.
035 * <br>
036 * Once you've added any features to the generator's effects list, call generate() to get a char[][] with the
037 * desired dungeon map, using a fixed repertoire of chars to represent the different features, with the exception of the
038 * customization that can be requested from addLake(). If you use the libGDX text-based display module, you can change
039 * what chars are shown by using addSwap() in TextCellFactory. After calling generate(), you can safely get the values
040 * from the stairsUp and stairsDown fields, which are Coords that should be a long distance from each other but
041 * connected in the dungeon. You may want to change those to staircase characters, but there's no requirement to do
042 * anything with them. It's recommended that you keep the resulting char[][] maps in some collection that can be saved,
043 * since SectionDungeonGenerator only stores a temporary copy of the most recently-generated map. The DungeonUtility
044 * field of this class, utility, is a convenient way of accessing the non-static methods in that class, such as
045 * randomFloor(), without needing to create another DungeonUtility (this class creates one, so you don't have to).
046 * Similarly, the Placement field of this class, placement, can be used to find parts of a dungeon that fit certain
047 * qualities for the placement of items, terrain features, or NPCs.
048 * <br>
049 * Example map with a custom-representation lake: https://gist.github.com/tommyettinger/0055075f9de59c452d25
050 * @see DungeonUtility this class exposes a DungeonUtility member; DungeonUtility also has many useful static methods
051 * @see DungeonGenerator for a slightly simpler alternative that does not recognize different sections of dungeon
052 *
053 * @author Eben Howard - http://squidpony.com - howard@squidpony.com
054 * @author Tommy Ettinger - https://github.com/tommyettinger
055 */
056public class SectionDungeonGenerator implements IDungeonGenerator{
057
058    /**
059     * The effects that can be applied to this dungeon. More may be added in future releases.
060     */
061    public enum FillEffect
062    {
063        /**
064         * Water, represented by '~'
065         */
066        WATER,
067        /**
068         * Traps, represented by '^'
069         */
070        TRAPS,
071        /**
072         * Grass, represented by '"'
073         */
074        GRASS,
075        /**
076         * Boulders strewn about open areas, represented by '#' and treated as walls
077         */
078        BOULDERS,
079        /**
080         * Islands of ground, '.', surrounded by shallow water, ',', to place in water at evenly spaced points
081         */
082        ISLANDS
083    }
084
085    /**
086     * Constant for features being added to all environment types.
087     */
088    public static final int ALL = 0,
089    /**
090     * Constant for features being added only to rooms.
091     */
092    ROOM = 1,
093    /**
094     * Constant for features being added only to corridors.
095     */
096    CORRIDOR = 2,
097    /**
098     * Constant for features being added only to caves.
099     */
100    CAVE = 3;
101
102    /**
103     * The effects that will be applied when generate is called. Strongly prefer using addWater, addDoors, addTraps,
104     * and addGrass.
105     */
106    public EnumMap<FillEffect, Integer> roomFX, corridorFX, caveFX;
107
108    /**
109     * Percentage of viable positions to fill with doors, represented by '+' for east-to-west connections or '/' for
110     * north-to-south ones; this number will be negative if filling two-cell wide positions but will be made positive
111     * when needed.
112     */
113    public int doorFX;
114    /**
115     * The char to use for deep lake cells.
116     */
117    public char deepLakeGlyph = '~';
118    /**
119     * The char to use for shallow lake cells.
120     */
121    public char shallowLakeGlyph = ',';
122    /**
123     * The approximate percentage of non-room, non-cave, non-edge-of-map wall cells to try to fill with lake. Corridors
124     * that are covered by a lake will become bridges, the glyph ':'.
125     */
126    public int lakeFX;
127    /**
128     * The approximate percentage of non-room, non-cave, non-edge-of-map wall cells to try to fill with maze. Corridors
129     * that are covered by a maze will become part of its layout.
130     */
131    public int mazeFX;
132    public DungeonUtility utility;
133    protected int height, width;
134    public Coord stairsUp, stairsDown;
135    public StatefulRNG rng;
136    protected long rebuildSeed;
137    protected boolean seedFixed;
138    protected int environmentType = 1;
139
140    protected char[][] dungeon;
141    /**
142     * Potentially important if you need to identify specific rooms, corridors, or cave areas in a map.
143     */
144    public RoomFinder finder;
145    /**
146     * Configured by this class after you call generate(), this Placement can be used to locate areas of the dungeon
147     * that fit certain properties, like "out of sight from a door" or "a large flat section of wall that could be used
148     * to place a straight-line object." You can use this as-needed; it does only a small amount of work at the start,
149     * and does the calculations for what areas have certain properties on request.
150     */
151    public Placement placement;
152
153    /**
154     * Get the most recently generated char[][] dungeon out of this class. The
155     * dungeon may be null if generate() or setDungeon() have not been called.
156     * @return a char[][] dungeon, or null.
157     */
158    public char[][] getDungeon() {
159        return dungeon;
160    }
161    /**
162     * Get the most recently generated char[][] dungeon out of this class without any chars other than '#' or '.', for
163     * walls and floors respectively. The dungeon may be null if generate() or setDungeon() have not been called.
164     * @return a char[][] dungeon with only '#' for walls and '.' for floors, or null.
165     */
166    public char[][] getBareDungeon() {
167        return DungeonUtility.simplifyDungeon(dungeon);
168    }
169
170    /**
171     * Change the underlying char[][]; only affects the toString method, and of course getDungeon.
172     * @param dungeon a char[][], probably produced by an earlier call to this class and then modified.
173     */
174    public void setDungeon(char[][] dungeon) {
175        this.dungeon = dungeon;
176        if(dungeon == null)
177        {
178            width = 0;
179            height = 0;
180            return;
181        }
182        width = dungeon.length;
183        if(width > 0)
184            height = dungeon[0].length;
185    }
186
187    /**
188     * Height of the dungeon in cells.
189     * @return Height of the dungeon in cells.
190     */
191    public int getHeight() {
192        return height;
193    }
194
195    /**
196     * Width of the dungeon in cells.
197     * @return Width of the dungeon in cells.
198     */
199    public int getWidth() {
200        return width;
201    }
202
203
204    /**
205     * Make a SectionDungeonGenerator with a LightRNG using a random seed, height 40, and width 40.
206     */
207    public SectionDungeonGenerator()
208    {
209        rng = new StatefulRNG();
210        utility = new DungeonUtility(rng);
211        rebuildSeed = rng.getState();
212        height = 40;
213        width = 40;
214        roomFX = new EnumMap<>(FillEffect.class);
215        corridorFX = new EnumMap<>(FillEffect.class);
216        caveFX = new EnumMap<>(FillEffect.class);
217    }
218
219    /**
220     * Make a SectionDungeonGenerator with the given height and width; the RNG used for generating a dungeon and
221     * adding features will be a LightRNG using a random seed. If width or height is greater than 256, then this will
222     * expand the Coord pool from its 256x256 default so it stores a reference to each Coord that might be used in the
223     * creation of the dungeon (if width and height are 300 and 300, the Coord pool will be 300x300; if width and height
224     * are 500 and 100, the Coord pool will be 500x256 because it won't shrink below the default size of 256x256).
225     * @param width The width of the dungeon in cells
226     * @param height The height of the dungeon in cells
227     */
228    public SectionDungeonGenerator(int width, int height)
229    {
230        this(width, height, new StatefulRNG());
231    }
232
233    /**
234     * Make a SectionDungeonGenerator with the given height, width, and RNG. Use this if you want to seed the RNG. If
235     * width or height is greater than 256, then this will expand the Coord pool from its 256x256 default so it stores a
236     * reference to each Coord that might be used in the creation of the dungeon (if width and height are 300 and 300,
237     * the Coord pool will be 300x300; if width and height are 500 and 100, the Coord pool will be 500x256 because it
238     * won't shrink below the default size of 256x256).
239     * @param width The width of the dungeon in cells
240     * @param height The height of the dungeon in cells
241     * @param rng The RNG to use for all purposes in this class; if it is a StatefulRNG, then it will be used as-is,
242     *            but if it is not a StatefulRNG, a new StatefulRNG will be used, randomly seeded by this parameter
243     */
244    public SectionDungeonGenerator(int width, int height, IRNG rng)
245    {
246        Coord.expandPoolTo(width, height);
247        this.rng = (rng instanceof StatefulRNG) ? (StatefulRNG) rng : new StatefulRNG(rng.nextLong());
248        utility = new DungeonUtility(this.rng);
249        rebuildSeed = this.rng.getState();
250        this.height = height;
251        this.width = width;
252        roomFX = new EnumMap<>(FillEffect.class);
253        corridorFX = new EnumMap<>(FillEffect.class);
254        caveFX = new EnumMap<>(FillEffect.class);
255    }
256
257    /**
258     * Copies all fields from copying and makes a new DungeonGenerator.
259     * @param copying the DungeonGenerator to copy
260     */
261    public SectionDungeonGenerator(SectionDungeonGenerator copying)
262    {
263        rng = new StatefulRNG(copying.rng.getState());
264        utility = new DungeonUtility(rng);
265        rebuildSeed = rng.getState();
266        height = copying.height;
267        width = copying.width;
268        Coord.expandPoolTo(width, height);
269        roomFX = new EnumMap<>(copying.roomFX);
270        corridorFX = new EnumMap<>(copying.corridorFX);
271        caveFX = new EnumMap<>(copying.caveFX);
272        doorFX = copying.doorFX;
273        lakeFX = copying.lakeFX;
274        deepLakeGlyph = copying.deepLakeGlyph;
275        shallowLakeGlyph = copying.shallowLakeGlyph;
276        dungeon = copying.dungeon;
277    }
278
279    /**
280     * Turns the majority of the given percentage of floor cells into water cells, represented by '~'. Water will be
281     * clustered into a random number of pools, with more appearing if needed to fill the percentage.
282     * Each pool will have randomized volume that should fill or get very close to filling the requested
283     * percentage, unless the pools encounter too much tight space. If this DungeonGenerator previously had addWater
284     * called, the latest call will take precedence. No islands will be placed with this variant, but the edge of the
285     * water will be shallow, represented by ','.
286     * @param env the environment to apply this to; uses MixedGenerator's constants, or 0 for "all environments"
287     * @param percentage the percentage of floor cells to fill with water
288     * @return this DungeonGenerator; can be chained
289     */
290    public SectionDungeonGenerator addWater(int env, int percentage)
291    {
292        if(percentage < 0) percentage = 0;
293        if(percentage > 100) percentage = 100;
294        switch (env)
295        {
296            case ROOM:
297                roomFX.remove(FillEffect.WATER);
298                roomFX.put(FillEffect.WATER, percentage);
299                break;
300            case CORRIDOR:
301                corridorFX.remove(FillEffect.WATER);
302                corridorFX.put(FillEffect.WATER, percentage);
303                break;
304            case CAVE:
305                caveFX.remove(FillEffect.WATER);
306                caveFX.put(FillEffect.WATER, percentage);
307                break;
308            default:
309                if(roomFX.containsKey(FillEffect.WATER))
310                    roomFX.put(FillEffect.WATER, Math.min(100, roomFX.get(FillEffect.WATER) + percentage));
311                else
312                    roomFX.put(FillEffect.WATER, percentage);
313                if(corridorFX.containsKey(FillEffect.WATER))
314                    corridorFX.put(FillEffect.WATER, Math.min(100, corridorFX.get(FillEffect.WATER) + percentage));
315                else
316                    corridorFX.put(FillEffect.WATER, percentage);
317                if(caveFX.containsKey(FillEffect.WATER))
318                    caveFX.put(FillEffect.WATER, Math.min(100, caveFX.get(FillEffect.WATER) + percentage));
319                else
320                    caveFX.put(FillEffect.WATER, percentage);
321        }
322        return this;
323    }
324    /**
325     * Turns the majority of the given percentage of floor cells into water cells, represented by '~'. Water will be
326     * clustered into a random number of pools, with more appearing if needed to fill the percentage. Each pool will
327     * have randomized volume that should fill or get very close to filling the requested percentage,
328     * unless the pools encounter too much tight space. If this DungeonGenerator previously had addWater called, the
329     * latest call will take precedence. If islandSpacing is greater than 1, then this will place islands of floor, '.',
330     * surrounded by shallow water, ',', at about the specified distance with Euclidean measurement.
331     * @param env the environment to apply this to; uses MixedGenerator's constants, or 0 for "all environments"
332     * @param percentage the percentage of floor cells to fill with water
333     * @param islandSpacing if greater than 1, islands will be placed randomly this many cells apart.
334     * @return this DungeonGenerator; can be chained
335     */
336    public SectionDungeonGenerator addWater(int env, int percentage, int islandSpacing)
337    {
338        addWater(env, percentage);
339
340        if(percentage < 0) percentage = 0;
341        if(percentage > 100) percentage = 100;
342        switch (env) {
343            case ROOM:
344                roomFX.remove(FillEffect.ISLANDS);
345                if(islandSpacing > 1)
346                    roomFX.put(FillEffect.ISLANDS, percentage);
347                break;
348            case CORRIDOR:
349                corridorFX.remove(FillEffect.ISLANDS);
350                if(islandSpacing > 1)
351                    corridorFX.put(FillEffect.ISLANDS, percentage);
352                break;
353            case CAVE:
354                caveFX.remove(FillEffect.ISLANDS);
355                if(islandSpacing > 1)
356                    caveFX.put(FillEffect.ISLANDS, percentage);
357                break;
358            default:
359                roomFX.remove(FillEffect.ISLANDS);
360                if(islandSpacing > 1)
361                    roomFX.put(FillEffect.ISLANDS, percentage);
362                corridorFX.remove(FillEffect.ISLANDS);
363                if(islandSpacing > 1)
364                    corridorFX.put(FillEffect.ISLANDS, percentage);
365                caveFX.remove(FillEffect.ISLANDS);
366                if(islandSpacing > 1)
367                    caveFX.put(FillEffect.ISLANDS, percentage);
368        }
369
370        return this;
371    }
372
373    /**
374     * Turns the majority of the given percentage of floor cells into grass cells, represented by '"'. Grass will be
375     * clustered into a random number of patches, with more appearing if needed to fill the percentage. Each area will
376     * have randomized volume that should fill or get very close to filling (two thirds of) the requested percentage,
377     * unless the patches encounter too much tight space. If this DungeonGenerator previously had addGrass called, the
378     * latest call will take precedence.
379     * @param env the environment to apply this to; uses MixedGenerator's constants, or 0 for "all environments"
380     * @param percentage the percentage of floor cells to fill with grass; this can vary quite a lot. It may be
381     *                   difficult to fill very high (over 66%) percentages of map with grass, though you can do this by
382     *                   giving a percentage of between 100 and 150.
383     * @return this DungeonGenerator; can be chained
384     */
385    public SectionDungeonGenerator addGrass(int env, int percentage)
386    {
387        if(percentage < 0) percentage = 0;
388        if(percentage > 100) percentage = 100;
389        switch (env)
390        {
391            case ROOM:
392                roomFX.remove(FillEffect.GRASS);
393                roomFX.put(FillEffect.GRASS, percentage);
394                break;
395            case CORRIDOR:
396                corridorFX.remove(FillEffect.GRASS);
397                corridorFX.put(FillEffect.GRASS, percentage);
398                break;
399            case CAVE:
400                caveFX.remove(FillEffect.GRASS);
401                caveFX.put(FillEffect.GRASS, percentage);
402                break;
403            default:
404                if(roomFX.containsKey(FillEffect.GRASS))
405                    roomFX.put(FillEffect.GRASS, Math.min(100, roomFX.get(FillEffect.GRASS) + percentage));
406                else
407                    roomFX.put(FillEffect.GRASS, percentage);
408                if(corridorFX.containsKey(FillEffect.GRASS))
409                    corridorFX.put(FillEffect.GRASS, Math.min(100, corridorFX.get(FillEffect.GRASS) + percentage));
410                else
411                    corridorFX.put(FillEffect.GRASS, percentage);
412                if(caveFX.containsKey(FillEffect.GRASS))
413                    caveFX.put(FillEffect.GRASS, Math.min(100, caveFX.get(FillEffect.GRASS) + percentage));
414                else
415                    caveFX.put(FillEffect.GRASS, percentage);
416        }
417        return this;
418    }
419    /**
420     * Turns the given percentage of floor cells not already adjacent to walls into wall cells, represented by '#'.
421     * If this DungeonGenerator previously had addBoulders called, the latest call will take precedence.
422     * @param env the environment to apply this to; uses MixedGenerator's constants, or 0 for "all environments"
423     * @param percentage the percentage of floor cells not adjacent to walls to fill with boulders.
424     * @return this DungeonGenerator; can be chained
425     */
426    public SectionDungeonGenerator addBoulders(int env, int percentage)
427    {
428        if(percentage < 0) percentage = 0;
429        if(percentage > 100) percentage = 100;
430        switch (env)
431        {
432            case ROOM:
433                roomFX.remove(FillEffect.BOULDERS);
434                roomFX.put(FillEffect.BOULDERS, percentage);
435                break;
436            case CORRIDOR:
437                corridorFX.remove(FillEffect.BOULDERS);
438                corridorFX.put(FillEffect.BOULDERS, percentage);
439                break;
440            case CAVE:
441                caveFX.remove(FillEffect.BOULDERS);
442                caveFX.put(FillEffect.BOULDERS, percentage);
443                break;
444            default:
445                if(roomFX.containsKey(FillEffect.BOULDERS))
446                    roomFX.put(FillEffect.BOULDERS, Math.min(100, roomFX.get(FillEffect.BOULDERS) + percentage));
447                else
448                    roomFX.put(FillEffect.BOULDERS, percentage);
449                if(corridorFX.containsKey(FillEffect.BOULDERS))
450                    corridorFX.put(FillEffect.BOULDERS, Math.min(100, corridorFX.get(FillEffect.BOULDERS) + percentage));
451                else
452                    corridorFX.put(FillEffect.BOULDERS, percentage);
453                if(caveFX.containsKey(FillEffect.BOULDERS))
454                    caveFX.put(FillEffect.BOULDERS, Math.min(100, caveFX.get(FillEffect.BOULDERS) + percentage));
455                else
456                    caveFX.put(FillEffect.BOULDERS, percentage);
457        }
458        return this;
459    }
460    /**
461     * Turns the given percentage of viable doorways into doors, represented by '+' for doors that allow travel along
462     * the x-axis and '/' for doors that allow travel along the y-axis. If doubleDoors is true,
463     * 2-cell-wide openings will be considered viable doorways and will fill one cell with a wall, the other a door.
464     * If this DungeonGenerator previously had addDoors called, the latest call will take precedence.
465     * @param percentage the percentage of valid openings to corridors to fill with doors; should be between 10 and
466     *                   20 if you want doors to appear more than a few times, but not fill every possible opening.
467     * @param doubleDoors true if you want two-cell-wide openings to receive a door and a wall; false if only
468     *                    one-cell-wide openings should receive doors. Usually, this should be true.
469     * @return this DungeonGenerator; can be chained
470     */
471    public SectionDungeonGenerator addDoors(int percentage, boolean doubleDoors)
472    {
473        if(percentage < 0) percentage = 0;
474        if(percentage > 100) percentage = 100;
475        if(doubleDoors) percentage *= -1;
476        doorFX = percentage;
477        return this;
478    }
479
480    /**
481     * Instructs the generator to add a winding section of corridors into a large area that can be filled without
482     * overwriting rooms, caves, or the edge of the map; wall cells will become either '#' or '.' and corridors will be
483     * overwritten. If the percentage is too high (40% is probably too high to adequately fill), this will fill less than
484     * the requested percentage rather than fill multiple mazes.
485     * @param percentage The percentage of non-room, non-cave, non-edge-of-map wall cells to try to fill with maze.
486     * @return this for chaining
487     */
488    public SectionDungeonGenerator addMaze(int percentage)
489    {
490
491        if(percentage < 0) percentage = 0;
492        if(percentage > 100) percentage = 100;
493        mazeFX = percentage;
494        return this;
495    }
496
497    /**
498     * Instructs the generator to add a lake (here, of water) into a large area that can be filled without overwriting
499     * rooms, caves, or the edge of the map; wall cells will become the deep lake glyph (here, '~'), unless they are
500     * close to an existing room or cave, in which case they become the shallow lake glyph (here, ','), and corridors
501     * that are "covered" by a lake will become bridges, the glyph ':'. If the percentage is too high (40% is probably
502     * too high to adequately fill), this will fill less than the requested percentage rather than fill multiple lakes.
503     * @param percentage The percentage of non-room, non-cave, non-edge-of-map wall cells to try to fill with lake.
504     * @return this for chaining
505     */
506    public SectionDungeonGenerator addLake(int percentage)
507    {
508        return addLake(percentage, '~', ',');
509    }
510
511    /**
512     * Instructs the generator to add a lake into a large area that can be filled without overwriting rooms, caves, or
513     * the edge of the map; wall cells will become the char deepLake, unless they are close to an existing room or cave,
514     * in which case they become the char shallowLake, and corridors that are "covered" by a lake will become bridges,
515     * the glyph ':'. If the percentage is too high (40% is probably too high to adequately fill), this will fill less
516     * than the requested percentage rather than fill multiple lakes.
517     * @param percentage The percentage of non-room, non-cave, non-edge-of-map wall cells to try to fill with lake.
518     * @param deepLake the char to use for deep lake cells, such as '~'
519     * @param shallowLake the char to use for shallow lake cells, such as ','
520     * @return this for chaining
521     */
522    public SectionDungeonGenerator addLake(int percentage, char deepLake, char shallowLake)
523    {
524        if(percentage < 0) percentage = 0;
525        if(percentage > 100) percentage = 100;
526        lakeFX = percentage;
527        deepLakeGlyph = deepLake;
528        shallowLakeGlyph = shallowLake;
529        return this;
530    }
531
532    /**
533     * Turns the given percentage of open area floor cells into trap cells, represented by '^'. Corridors that have no
534     * possible way to move around a trap will not receive traps, ever. If this DungeonGenerator previously had
535     * addTraps called, the latest call will take precedence.
536     * @param env the environment to apply this to; uses MixedGenerator's constants, or 0 for "all environments"
537     * @param percentage the percentage of valid cells to fill with traps; should be no higher than 5 unless
538     *                   the dungeon floor is meant to be a kill screen or minefield.
539     * @return this DungeonGenerator; can be chained
540     */
541    public SectionDungeonGenerator addTraps(int env, int percentage)
542    {
543        if(percentage < 0) percentage = 0;
544        if(percentage > 100) percentage = 100;
545        switch (env)
546        {
547            case ROOM:
548                roomFX.remove(FillEffect.TRAPS);
549                roomFX.put(FillEffect.TRAPS, percentage);
550                break;
551            case CORRIDOR:
552                corridorFX.remove(FillEffect.TRAPS);
553                corridorFX.put(FillEffect.TRAPS, percentage);
554                break;
555            case CAVE:
556                caveFX.remove(FillEffect.TRAPS);
557                caveFX.put(FillEffect.TRAPS, percentage);
558                break;
559            default:
560                if(roomFX.containsKey(FillEffect.TRAPS))
561                    roomFX.put(FillEffect.TRAPS, Math.min(100, roomFX.get(FillEffect.TRAPS) + percentage));
562                else
563                    roomFX.put(FillEffect.TRAPS, percentage);
564                if(corridorFX.containsKey(FillEffect.TRAPS))
565                    corridorFX.put(FillEffect.TRAPS, Math.min(100, corridorFX.get(FillEffect.TRAPS) + percentage));
566                else
567                    corridorFX.put(FillEffect.TRAPS, percentage);
568                if(caveFX.containsKey(FillEffect.TRAPS))
569                    caveFX.put(FillEffect.TRAPS, Math.min(100, caveFX.get(FillEffect.TRAPS) + percentage));
570                else
571                    caveFX.put(FillEffect.TRAPS, percentage);
572        }
573        return this;
574    }
575
576    /**
577     * Removes any door, water, or trap insertion effects that this DungeonGenerator would put in future dungeons.
578     * @return this DungeonGenerator, with all effects removed. Can be chained.
579     */
580    public SectionDungeonGenerator clearEffects()
581    {
582        roomFX.clear();
583        corridorFX.clear();
584        caveFX.clear();
585        lakeFX = 0;
586        mazeFX = 0;
587        doorFX = 0;
588        return this;
589    }
590
591    protected OrderedSet<Coord> removeAdjacent(OrderedSet<Coord> coll, Coord pt)
592    {
593        for(Coord temp : new Coord[]{Coord.get(pt.x + 1, pt.y), Coord.get(pt.x - 1, pt.y),
594                Coord.get(pt.x, pt.y + 1), Coord.get(pt.x, pt.y - 1)})
595        {
596            coll.remove(temp);
597        }
598
599        return coll;
600    }
601    protected OrderedSet<Coord> removeAdjacent(OrderedSet<Coord> coll, Coord pt1, Coord pt2)
602    {
603
604        for(Coord temp : new Coord[]{Coord.get(pt1.x + 1, pt1.y), Coord.get(pt1.x - 1, pt1.y),
605                Coord.get(pt1.x, pt1.y + 1), Coord.get(pt1.x, pt1.y - 1),
606                Coord.get(pt2.x + 1, pt2.y), Coord.get(pt2.x - 1, pt2.y),
607                Coord.get(pt2.x, pt2.y + 1), Coord.get(pt2.x, pt2.y - 1),})
608        {
609            if(!(temp.x == pt1.x && temp.y == pt1.y) && !(temp.x == pt2.x && temp.y == pt2.y))
610                coll.remove(temp);
611        }
612
613        return coll;
614    }
615    protected OrderedSet<Coord> removeNearby(OrderedSet<Coord> coll, char[][] disallowed)
616    {
617        if(coll == null || disallowed == null || disallowed.length == 0 || disallowed[0].length == 0)
618            return new OrderedSet<>();
619        OrderedSet<Coord> next = new OrderedSet<>(coll.size());
620        int width = disallowed.length, height = disallowed[0].length;
621        COORD_WISE:
622        for(Coord c : coll)
623        {
624            for (int x = Math.max(0, c.x - 1); x <= Math.min(width - 1, c.x + 1); x++) {
625
626                for (int y = Math.max(0, c.y - 1); y <= Math.min(height - 1, c.y + 1); y++) {
627                    if(disallowed[x][y] != '#')
628                        continue COORD_WISE;
629                }
630            }
631            next.add(c);
632        }
633
634        return next;
635    }
636
637
638    protected OrderedSet<Coord> viableDoorways(boolean doubleDoors, char[][] map, char[][] allCaves,
639                                                  char[][] allCorridors)
640    {
641        OrderedSet<Coord> doors = new OrderedSet<>();
642        OrderedSet<Coord> blocked = new OrderedSet<>(4);
643        DijkstraMap dm = new DijkstraMap(map, Measurement.EUCLIDEAN);
644        for(int x = 1; x < map.length - 1; x++) {
645            for (int y = 1; y < map[x].length - 1; y++) {
646                if(map[x][y] == '#' || allCorridors[x][y] != '#')
647                    continue;
648                if (doubleDoors) {
649                    if (x >= map.length - 2 || y >= map[x].length - 2)
650                        continue;
651                    else {
652                        if (map[x+1][y] != '#' &&
653                                map[x + 2][y] == '#' && map[x - 1][y] == '#'
654                                && map[x][y + 1] != '#' && map[x][y - 1] != '#'
655                                && map[x+1][y + 1] != '#' && map[x+1][y - 1] != '#') {
656                            if (map[x + 2][y + 1] != '#' || map[x - 1][y + 1] != '#' || map[x + 2][y - 1] != '#' || map[x - 1][y - 1] != '#') {
657                                dm.resetMap();
658                                dm.clearGoals();
659                                dm.setGoal(x, y+1);
660                                blocked.clear();
661                                blocked.add(Coord.get(x, y));
662                                blocked.add(Coord.get(x + 1, y));
663                                dm.partialScan(null, 16, blocked);
664                                if(dm.gradientMap[x][y-1] < DijkstraMap.FLOOR)
665                                    continue;
666                                doors.add(Coord.get(x, y));
667                                doors.add(Coord.get(x + 1, y));
668                                doors = removeAdjacent(doors, Coord.get(x, y), Coord.get(x + 1, y));
669                                continue;
670                            }
671                        } else if (map[x][y+1] != '#' &&
672                                map[x][y + 2] == '#' && map[x][y - 1] == '#'
673                                && map[x + 1][y] != '#' && map[x - 1][y] != '#'
674                                && map[x + 1][y+1] != '#' && map[x - 1][y+1] != '#') {
675                            if (map[x + 1][y + 2] != '#' || map[x + 1][y - 1] != '#' || map[x - 1][y + 2] != '#' || map[x - 1][y - 1] != '#') {
676                                dm.resetMap();
677                                dm.clearGoals();
678                                dm.setGoal(x+1, y);
679                                blocked.clear();
680                                blocked.add(Coord.get(x, y));
681                                blocked.add(Coord.get(x, y+1));
682                                dm.partialScan(null, 16, blocked);
683                                if(dm.gradientMap[x-1][y] < DijkstraMap.FLOOR)
684                                    continue;
685                                doors.add(Coord.get(x, y));
686                                doors.add(Coord.get(x, y+1));
687                                doors = removeAdjacent(doors, Coord.get(x, y), Coord.get(x, y + 1));
688                                continue;
689                            }
690                        }
691                    }
692                }
693                if (map[x + 1][y] == '#' && map[x - 1][y] == '#' && map[x][y + 1] != '#' && map[x][y - 1] != '#') {
694                    if (map[x + 1][y + 1] != '#' || map[x - 1][y + 1] != '#' || map[x + 1][y - 1] != '#' || map[x - 1][y - 1] != '#') {
695                        dm.resetMap();
696                        dm.clearGoals();
697                        dm.setGoal(x, y+1);
698                        blocked.clear();
699                        blocked.add(Coord.get(x, y));
700                        dm.partialScan(null, 16, blocked);
701                        if(dm.gradientMap[x][y-1] < DijkstraMap.FLOOR)
702                            continue;
703                        doors.add(Coord.get(x, y));
704                        doors = removeAdjacent(doors, Coord.get(x, y));
705                    }
706                } else if (map[x][y + 1] == '#' && map[x][y - 1] == '#' && map[x + 1][y] != '#' && map[x - 1][y] != '#') {
707                    if (map[x + 1][y + 1] != '#' || map[x + 1][y - 1] != '#' || map[x - 1][y + 1] != '#' || map[x - 1][y - 1] != '#') {
708                        dm.resetMap();
709                        dm.clearGoals();
710                        dm.setGoal(x+1, y);
711                        blocked.clear();
712                        blocked.add(Coord.get(x, y));
713                        dm.partialScan(null, 16, blocked);
714                        if(dm.gradientMap[x-1][y] < DijkstraMap.FLOOR)
715                            continue;
716                        doors.add(Coord.get(x, y));
717                        doors = removeAdjacent(doors, Coord.get(x, y));
718                    }
719                }
720
721            }
722        }
723
724        return removeNearby(doors, allCaves);
725    }
726
727    /**
728     * Generate a char[][] dungeon using TilesetType.DEFAULT_DUNGEON; this produces a dungeon appropriate for a level
729     * of ruins or a partially constructed dungeon. This uses '#' for walls, '.' for floors, '~' for deep water, ',' for
730     * shallow water, '^' for traps, '+' for doors that provide horizontal passage, and '/' for doors that provide
731     * vertical passage. Use the addDoors, addWater, addGrass, and addTraps methods of this class to request these in
732     * the generated map.
733     * Also sets the fields stairsUp and stairsDown to two randomly chosen, distant, connected, walkable cells.
734     * @return a char[][] dungeon
735     */
736    public char[][] generate() {
737        return generate(TilesetType.DEFAULT_DUNGEON);
738    }
739
740    /**
741     * Generate a char[][] dungeon given a TilesetType; the comments in that class provide some opinions on what
742     * each TilesetType value could be used for in a game. This uses '#' for walls, '.' for floors, '~' for deep water,
743     * ',' for shallow water, '^' for traps, '+' for doors that provide horizontal passage, and '/' for doors that
744     * provide vertical passage. Use the addDoors, addWater, addGrass, and addTraps methods of this class to request
745     * these in the generated map.
746     * Also sets the fields stairsUp and stairsDown to two randomly chosen, distant, connected, walkable cells.
747     * @see TilesetType
748     * @param kind a TilesetType enum value, such as TilesetType.DEFAULT_DUNGEON
749     * @return a char[][] dungeon
750     */
751    public char[][] generate(TilesetType kind)
752    {
753        rebuildSeed = rng.getState();
754        environmentType = kind.environment();
755        DungeonBoneGen gen = new DungeonBoneGen(rng);
756        char[][] map = DungeonUtility.wallWrap(gen.generate(kind, width, height));
757
758        seedFixed = false;
759        DijkstraMap dijkstra = new DijkstraMap(map);
760        int frustrated = 0;
761        do {
762            dijkstra.clearGoals();
763            stairsUp = utility.randomFloor(map);
764            dijkstra.setGoal(stairsUp);
765            dijkstra.scan(null, null);
766            frustrated++;
767        }while (dijkstra.getMappedCount() < width + height && frustrated < 15);
768        double maxDijkstra = 0.0;
769        for (int i = 0; i < width; i++) {
770            for (int j = 0; j < height; j++) {
771                if(dijkstra.gradientMap[i][j] >= DijkstraMap.FLOOR) {
772                    map[i][j] = '#';
773                }
774                else if(dijkstra.gradientMap[i][j] > maxDijkstra) {
775                    maxDijkstra = dijkstra.gradientMap[i][j];
776                }
777            }
778        }
779        stairsDown = new GreasedRegion(dijkstra.gradientMap, maxDijkstra * 0.7,
780                DijkstraMap.FLOOR).singleRandom(rng);
781        finder = new RoomFinder(map, environmentType);
782        return innerGenerate();
783    }
784
785    /**
786     * Generate a char[][] dungeon with extra features given a baseDungeon that has already been generated and an
787     * environment as an int[][], which can often be obtained from MixedGenerator or classes that use it, like
788     * SerpentMapGenerator, with their getEnvironment method.
789     * Typically, you want to call generate with a TilesetType or no argument for the easiest generation; this method
790     * is meant for adding features like water and doors to existing maps while avoiding placing incongruous features in
791     * areas where they don't fit, like a door in a cave or moss in a room.
792     * This uses '#' for walls, '.' for floors, '~' for deep water, ',' for shallow water, '^' for traps, '+' for doors
793     * that provide horizontal passage, and '/' for doors that provide vertical passage.
794     * Use the addDoors, addWater, addGrass, and addTraps methods of this class to request these in the generated map.
795     * Also sets the fields stairsUp and stairsDown to two randomly chosen, distant, connected, walkable cells.
796     * <br>
797     * Special behavior here: If tab characters are present in the 2D char array, they will be replaced with '.' in the
798     * final dungeon, but will also be tried first as valid staircase locations (with a high distance possible to travel
799     * away from the starting staircase). If no tab characters are present this will search for '.' floors to place
800     * stairs on, as normal. This tab-first behavior is useful in conjunction with some methods that establish a good
801     * path in an existing dungeon; an example is {@code DungeonUtility.ensurePath(dungeon, rng, '\t', '#');} then
802     * passing dungeon (which that code modifies) in as baseDungeon to this method. Because tabs will always be replaced
803     * by floors ('.'), this considers any tabs that overlap with what the environment considers a wall (cave wall, room
804     * wall, corridor wall, or untouched) to really refer to a corridor floor, but doesn't reconsider tabs that overlap
805     * with floors already (it keeps the state of actual room, cave, and corridor floors). This is useful so you only
806     * have to call ensurePath or a similar method on the 2D char array and can leave the 2D int array alone.
807     * @param baseDungeon a pre-made dungeon consisting of '#' for walls and '.' for floors; may be modified in-place
808     * @param environment stores whether a cell is room, corridor, or cave; getEnvironment() typically gives this
809     * @return a char[][] dungeon
810     */
811    public char[][] generate(char[][] baseDungeon, int[][] environment)
812    {
813        if(!seedFixed)
814        {
815            rebuildSeed = rng.getState();
816        }
817        seedFixed = false;
818        char[][] map = DungeonUtility.wallWrap(baseDungeon);
819        width = map.length;
820        height = map[0].length;
821        int[][] env2 = new int[width][height];
822        for (int x = 0; x < width; x++) {
823            System.arraycopy(environment[x], 0, env2[x], 0, height);
824        }
825
826        DijkstraMap dijkstra = new DijkstraMap(map);
827        int frustrated = 0;
828        do {
829            dijkstra.clearGoals();
830            stairsUp = utility.randomMatchingTile(map, '\t');
831            if(stairsUp == null) {
832                stairsUp = utility.randomFloor(map);
833                if (stairsUp == null) {
834                    frustrated++;
835                    continue;
836                }
837            }
838            dijkstra.setGoal(stairsUp);
839            dijkstra.scan(null, null);
840            frustrated++;
841        }while (dijkstra.getMappedCount() < width + height && frustrated < 8);
842        if(frustrated >= 8)
843        {
844            return generate();
845        }
846        double maxDijkstra = 0.0;
847        for (int i = 0; i < width; i++) {
848            for (int j = 0; j < height; j++) {
849                if(dijkstra.gradientMap[i][j] >= DijkstraMap.FLOOR) {
850                    map[i][j] = '#';
851                    env2[i][j] = DungeonUtility.UNTOUCHED;
852                }
853                else if(dijkstra.gradientMap[i][j] > maxDijkstra) {
854                    maxDijkstra = dijkstra.gradientMap[i][j];
855                }
856                if (map[i][j] == '\t') {
857                    map[i][j] = '.';
858                    if((env2[i][j] & 1) == 0) // environment is a wall here
859                        env2[i][j] = DungeonUtility.CORRIDOR_FLOOR;
860                }
861            }
862        }
863        if(maxDijkstra < 16)
864            return generate(baseDungeon, environment);
865        stairsDown = new GreasedRegion(dijkstra.gradientMap, maxDijkstra * 0.7,
866                DijkstraMap.FLOOR).singleRandom(rng);
867        finder = new RoomFinder(map, env2);
868        return innerGenerate();
869    }
870
871    /**
872     * Generate a char[][] dungeon with extra features given a baseDungeon that has already been generated, with
873     * staircases represented by greater than and less than signs, and an environment as an int[][], which can often be
874     * obtained from MixedGenerator or classes that use it, like SerpentMapGenerator, with their getEnvironment method.
875     * Typically, you want to call generate with a TilesetType or no argument for the easiest generation; this method
876     * is meant for adding features like water and doors to existing maps while avoiding placing incongruous features in
877     * areas where they don't fit, like a door in a cave or moss in a room.
878     * This uses '#' for walls, '.' for floors, '~' for deep water, ',' for shallow water, '^' for traps, '+' for doors
879     * that provide horizontal passage, and '/' for doors that provide vertical passage.
880     * Use the addDoors, addWater, addGrass, and addTraps methods of this class to request these in the generated map.
881     * Also sets the fields stairsUp and stairsDown to null, and expects stairs to be already handled.
882     * @param baseDungeon a pre-made dungeon consisting of '#' for walls and '.' for floors, with stairs already in;
883     *                    may be modified in-place
884     * @param environment stores whether a cell is room, corridor, or cave; getEnvironment() typically gives this
885     * @return a char[][] dungeon
886     */
887    public char[][] generateRespectingStairs(char[][] baseDungeon, int[][] environment) {
888        if(!seedFixed)
889        {
890            rebuildSeed = rng.getState();
891        }
892        seedFixed = false;
893        char[][] map = DungeonUtility.wallWrap(baseDungeon);
894        int[][] env2 = new int[width][height];
895        for (int x = 0; x < width; x++) {
896            System.arraycopy(environment[x], 0, env2[x], 0, height);
897        }
898        DijkstraMap dijkstra = new DijkstraMap(map);
899        stairsUp = null;
900        stairsDown = null;
901
902        dijkstra.clearGoals();
903        ArrayList<Coord> stairs = DungeonUtility.allMatching(map, '<', '>');
904        for (int j = 0; j < stairs.size(); j++) {
905            dijkstra.setGoal(stairs.get(j));
906        }
907        dijkstra.scan(null, null);
908        for (int i = 0; i < width; i++) {
909            for (int j = 0; j < height; j++) {
910                if (dijkstra.gradientMap[i][j] >= DijkstraMap.FLOOR) {
911                    map[i][j] = '#';
912                    env2[i][j] = DungeonUtility.UNTOUCHED;
913                }
914            }
915        }
916        finder = new RoomFinder(map, env2);
917        return innerGenerate();
918    }
919
920
921
922    protected char[][] innerGenerate() {
923        dungeon = ArrayTools.fill('#', width, height);
924        ArrayList<char[][]> rm = finder.findRooms(),
925                cr = finder.findCorridors(),
926                cv = finder.findCaves();
927        char[][] roomMap = innerGenerate(RoomFinder.merge(rm, width, height), roomFX),
928                allCorridors = RoomFinder.merge(cr, width, height),
929                corridorMap = innerGenerate(allCorridors, corridorFX),
930                allCaves = RoomFinder.merge(cv, width, height),
931                caveMap = innerGenerate(allCaves, caveFX),
932                doorMap;
933        char[][][] lakesAndMazes = makeLake(rm, cv);
934        for (int y = 0; y < height; y++) {
935            for (int x = 0; x < width; x++) {
936                if (corridorMap[x][y] != '#' && lakesAndMazes[0][x][y] != '#')
937                    dungeon[x][y] = ':';
938                else if (roomMap[x][y] != '#')
939                    dungeon[x][y] = roomMap[x][y];
940                else if (lakesAndMazes[1][x][y] != '#') {
941                    dungeon[x][y] = lakesAndMazes[1][x][y];
942                    finder.environment[x][y] = DungeonUtility.CORRIDOR_FLOOR;
943                } else if (corridorMap[x][y] != '#')
944                    dungeon[x][y] = corridorMap[x][y];
945                else if (caveMap[x][y] != '#')
946                    dungeon[x][y] = caveMap[x][y];
947                else if (lakesAndMazes[0][x][y] != '#') {
948                    dungeon[x][y] = lakesAndMazes[0][x][y];
949                    finder.environment[x][y] = DungeonUtility.CAVE_FLOOR;
950                }
951            }
952        }
953        finder = new RoomFinder(dungeon, finder.environment);
954        rm = finder.findRooms();
955        cr = finder.findCorridors();
956        cv = finder.findCaves();
957        cv.add(lakesAndMazes[0]);
958        allCaves = RoomFinder.merge(cv, width, height);
959        doorMap = makeDoors(rm, cr, allCaves, allCorridors);
960        for (int y = 0; y < height; y++) {
961            for (int x = 0; x < width; x++) {
962                if (doorMap[x][y] == '+' || doorMap[x][y] == '/')
963                    dungeon[x][y] = doorMap[x][y];
964                else if (doorMap[x][y] == '*')
965                    dungeon[x][y] = '#';
966
967            }
968        }
969        placement = new Placement(finder);
970        return dungeon;
971
972    }
973    protected char[][] makeDoors(ArrayList<char[][]> rooms, ArrayList<char[][]> corridors, char[][] allCaves,
974                               char[][] allCorridors)
975    {
976        char[][] map = new char[width][height];
977        for (int x = 0; x < width; x++) {
978            Arrays.fill(map[x], '#');
979        }
980        if(doorFX == 0 || (rooms.isEmpty() && corridors.isEmpty()))
981            return map;
982        boolean doubleDoors = false;
983        int doorFill = doorFX;
984        if(doorFill < 0)
985        {
986            doubleDoors = true;
987            doorFill *= -1;
988        }
989        ArrayList<char[][]> fused = new ArrayList<>(rooms.size() + corridors.size());
990        fused.addAll(rooms);
991        fused.addAll(corridors);
992
993        map = RoomFinder.merge(fused, width, height);
994
995        OrderedSet<Coord> doorways = viableDoorways(doubleDoors, map, allCaves, allCorridors);
996
997
998        int total = doorways.size() * doorFill / 100;
999
1000        BigLoop:
1001        for(int i = 0; i < total; i++) {
1002            Coord entry = rng.getRandomElement(doorways);
1003            if (map[entry.x][entry.y] == '<' || map[entry.x][entry.y] == '>')
1004                continue;
1005            if (map[entry.x - 1][entry.y] != '#' && map[entry.x + 1][entry.y] != '#' &&
1006                    map[entry.x - 1][entry.y] != '*' && map[entry.x + 1][entry.y] != '*') {
1007                map[entry.x][entry.y] = '+';
1008            } else {
1009                map[entry.x][entry.y] = '/';
1010            }
1011            Coord[] adj = new Coord[]{Coord.get(entry.x + 1, entry.y), Coord.get(entry.x - 1, entry.y),
1012                    Coord.get(entry.x, entry.y + 1), Coord.get(entry.x, entry.y - 1)};
1013            for (Coord near : adj) {
1014                if (doorways.contains(near)) {
1015                    map[near.x][near.y] = '*';
1016                    doorways.remove(near);
1017                    doorways.remove(entry);
1018                    i++;
1019                    continue BigLoop;
1020                }
1021            }
1022            doorways.remove(entry);
1023        }
1024        return map;
1025
1026    }
1027    protected char[][][] makeLake(ArrayList<char[][]> rooms, ArrayList<char[][]> caves)
1028    {
1029        char[][][] maps = new char[2][width][height];
1030        char[][] fusedMap;
1031        for (int x = 0; x < width; x++) {
1032            Arrays.fill(maps[0][x], '#');
1033            Arrays.fill(maps[1][x], '#');
1034        }
1035        if((lakeFX == 0 && mazeFX == 0) || (rooms.isEmpty() && caves.isEmpty()))
1036            return maps;
1037        int lakeFill = lakeFX, mazeFill = mazeFX;
1038        if(lakeFX + mazeFX > 100)
1039        {
1040            lakeFill -= (lakeFX + mazeFX - 100) / 2;
1041            mazeFill -= (lakeFX + mazeFX - 99) / 2;
1042        }
1043
1044        ArrayList<char[][]> fused = new ArrayList<>(rooms.size() + caves.size());
1045        fused.addAll(rooms);
1046        fused.addAll(caves);
1047
1048        fusedMap = RoomFinder.merge(fused, width, height);
1049        GreasedRegion limit = new GreasedRegion(width, height).insertRectangle(1, 1, width - 2, height - 2),
1050                potential = new GreasedRegion(fusedMap, '#').and(limit),
1051                flooded, chosen, tmp = new GreasedRegion(width, height);
1052        int ctr = potential.size(), potentialMazeSize = ctr * mazeFill / 100, potentialLakeSize = ctr * lakeFill / 100;
1053        ArrayList<GreasedRegion> viable;
1054        int minSize;
1055        Coord center;
1056        boolean[][] deep;
1057        if(potentialMazeSize > 0) {
1058            viable = potential.split();
1059            if (viable.isEmpty())
1060                return maps;
1061
1062            chosen = viable.get(0);
1063            minSize = chosen.size();
1064            for (GreasedRegion sa : viable) {
1065                int sz = sa.size();
1066                if (sz > minSize) {
1067                    chosen = sa;
1068                    minSize = sz;
1069                }
1070            }
1071            PacMazeGenerator pac = new PacMazeGenerator(width - width % 3, height - height % 3, rng);
1072            char[][] pacMap = ArrayTools.insert(pac.generate(), ArrayTools.fill('#', width, height), 1, 1);
1073            center = chosen.singleRandom(rng);
1074            flooded = new GreasedRegion(center, width, height).spill(chosen, potentialMazeSize, rng).and(limit);
1075            GreasedRegion pacEnv = new GreasedRegion(pacMap, '.').and(flooded).removeIsolated();
1076            deep = pacEnv.decode();
1077
1078            for (int x = 1; x < width - 1; x++) {
1079                for (int y = 1; y < height - 1; y++) {
1080                    if (deep[x][y])
1081                        maps[1][x][y] = pacMap[x][y];
1082                }
1083            }
1084            finder.corridors.put(pacEnv, new ArrayList<GreasedRegion>());
1085            finder.allCorridors.or(pacEnv);
1086            finder.allFloors.or(pacEnv);
1087            potential.andNot(flooded);
1088        }
1089        if(potentialLakeSize > 0) {
1090            viable = potential.split();
1091            if (viable.isEmpty())
1092                return maps;
1093            chosen = viable.get(0);
1094            minSize = chosen.size();
1095            for (GreasedRegion sa : viable) {
1096                int sz = sa.size();
1097                if (sz > minSize) {
1098                    chosen = sa;
1099                    minSize = sz;
1100                }
1101            }
1102            center = chosen.singleRandom(rng);
1103            flooded = new GreasedRegion(center, width, height).spill(chosen, potentialLakeSize, rng).and(limit);
1104
1105            deep = flooded.decode();
1106            flooded.flood(new GreasedRegion(fusedMap, '.').fringe8way(3), 3).and(limit);
1107
1108            boolean[][] shallow = flooded.decode();
1109
1110            for (int x = 0; x < width; x++) {
1111                for (int y = 0; y < height; y++) {
1112                    if (deep[x][y])
1113                        maps[0][x][y] = deepLakeGlyph;
1114                    else if (shallow[x][y])
1115                        maps[0][x][y] = shallowLakeGlyph;
1116                }
1117            }
1118            ArrayList<GreasedRegion> change = new ArrayList<>();
1119            for (GreasedRegion room : finder.rooms.keySet()) {
1120                if(flooded.intersects(tmp.remake(room).expand8way()))
1121                    change.add(room);
1122            }
1123            for (GreasedRegion region : change) {
1124                finder.caves.put(region, finder.rooms.remove(region));
1125                finder.allRooms.andNot(region);
1126                finder.allCaves.or(region);
1127            }
1128        }
1129        return maps;
1130    }
1131
1132    protected char[][] innerGenerate(char[][] map, EnumMap<FillEffect, Integer> fx)
1133    {
1134        OrderedSet<Coord> hazards = new OrderedSet<>();
1135        int floorCount = DungeonUtility.countCells(map, '.'),
1136                doorFill = 0,
1137                waterFill = 0,
1138                grassFill = 0,
1139                trapFill = 0,
1140                boulderFill = 0,
1141                islandSpacing = 0;
1142
1143        if(fx.containsKey(FillEffect.GRASS)) {
1144            grassFill = fx.get(FillEffect.GRASS);
1145        }
1146        if(fx.containsKey(FillEffect.WATER)) {
1147            waterFill = fx.get(FillEffect.WATER);
1148        }
1149        if(fx.containsKey(FillEffect.BOULDERS)) {
1150            boulderFill = fx.get(FillEffect.BOULDERS) * floorCount / 100;
1151        }
1152        if(fx.containsKey(FillEffect.ISLANDS)) {
1153            islandSpacing = fx.get(FillEffect.ISLANDS);
1154        }
1155        if(fx.containsKey(FillEffect.TRAPS)) {
1156            trapFill = fx.get(FillEffect.TRAPS);
1157        }
1158        if (boulderFill > 0.0) {
1159            /*
1160            short[] floor = pack(map, '.');
1161            short[] viable = retract(floor, 1, width, height, true);
1162            ArrayList<Coord> boulders = randomPortion(viable, boulderFill, rng);
1163            for (Coord boulder : boulders) {
1164                map[boulder.x][boulder.y] = '#';
1165            }
1166            */
1167            Coord[] boulders = new GreasedRegion(map, '.').retract8way(1).randomPortion(rng, boulderFill);
1168            Coord t;
1169            for (int i = 0; i < boulders.length; i++) {
1170                t = boulders[i];
1171                map[t.x][t.y] = '#';
1172            }
1173        }
1174
1175
1176        if(trapFill > 0) {
1177            for (int x = 1; x < map.length - 1; x++) {
1178                for (int y = 1; y < map[x].length - 1; y++) {
1179                    if (map[x][y] == '.') {
1180                        int ctr = 0;
1181                        if (map[x + 1][y] != '#') ++ctr;
1182                        if (map[x - 1][y] != '#') ++ctr;
1183                        if (map[x][y + 1] != '#') ++ctr;
1184                        if (map[x][y - 1] != '#') ++ctr;
1185                        if (map[x + 1][y + 1] != '#') ++ctr;
1186                        if (map[x - 1][y + 1] != '#') ++ctr;
1187                        if (map[x + 1][y - 1] != '#') ++ctr;
1188                        if (map[x - 1][y - 1] != '#') ++ctr;
1189                        if (ctr >= 5) hazards.add(Coord.get(x, y));
1190                    }
1191                }
1192            }
1193        }
1194        GreasedRegion floors = new GreasedRegion(map, '.'), working = new GreasedRegion(width, height);
1195        floorCount = floors.size();
1196        float waterRate = waterFill / 100.0f, grassRate = grassFill / 100.0f;
1197        if(waterRate + grassRate > 1.0f)
1198        {
1199            waterRate /= (waterFill + grassFill) / 100.0f;
1200            grassRate /= (waterFill + grassFill) / 100.0f;
1201        }
1202        int targetWater = Math.round(floorCount * waterRate),
1203                targetGrass = Math.round(floorCount * grassRate),
1204                sizeWaterPools = targetWater / rng.between(3, 6),
1205                sizeGrassPools = targetGrass / rng.between(2, 5);
1206
1207        Coord[] scatter;
1208        int remainingWater = targetWater, remainingGrass = targetGrass;
1209        if(targetWater > 0) {
1210            scatter = floors.quasiRandomSeparated(1.0 / 7.0);
1211            rng.shuffleInPlace(scatter);
1212            GreasedRegion allWater = new GreasedRegion(width, height);
1213            for (int i = 0; i < scatter.length; i++) {
1214                if (remainingWater > 5)
1215                {
1216                    if(!floors.contains(scatter[i]))
1217                        continue;
1218                    working.empty().insert(scatter[i]).spill(floors, rng.between(4, Math.min(remainingWater, sizeWaterPools)), rng);
1219
1220                    floors.andNot(working);
1221                    remainingWater -= working.size();
1222                    allWater.addAll(working);
1223                } else
1224                    break;
1225            }
1226
1227            for (Coord pt : allWater) {
1228                hazards.remove(pt);
1229                //obstacles.add(pt);
1230                if (map[pt.x][pt.y] != '<' && map[pt.x][pt.y] != '>')
1231                    map[pt.x][pt.y] = '~';
1232            }
1233            for (Coord pt : allWater) {
1234                if (map[pt.x][pt.y] != '<' && map[pt.x][pt.y] != '>' &&
1235                        (map[pt.x - 1][pt.y] == '.' || map[pt.x + 1][pt.y] == '.' ||
1236                                map[pt.x][pt.y - 1] == '.' || map[pt.x][pt.y + 1] == '.'))
1237                    map[pt.x][pt.y] = ',';
1238            }
1239        }
1240
1241        if(targetGrass > 0) {
1242            scatter = floors.quasiRandomSeparated(1.03/6.7);
1243            rng.shuffleInPlace(scatter);
1244            for (int i = 0; i < scatter.length; i++) {
1245                if (remainingGrass > 5) //remainingGrass >= targetGrass * 0.02 &&
1246                {
1247                    working.empty().insert(scatter[i]).spill(floors, rng.between(4, Math.min(remainingGrass, sizeGrassPools)), rng);
1248                    if (working.isEmpty())
1249                        continue;
1250                    floors.andNot(working);
1251                    remainingGrass -= working.size();
1252                    map = working.inverseMask(map, '"');
1253                } else
1254                    break;
1255            }
1256        }
1257
1258        if(islandSpacing > 1 && targetWater > 0) {
1259            OrderedSet<Coord> islands = PoissonDisk.sampleMap(map, 1f * islandSpacing, rng, '#', '.', '"', '+', '/', '^', '<', '>');
1260            for (Coord c : islands) {
1261                map[c.x][c.y] = '.';
1262                if (map[c.x - 1][c.y] != '#' && map[c.x - 1][c.y] != '<' && map[c.x - 1][c.y] != '>')
1263                    map[c.x - 1][c.y] = ',';
1264                if (map[c.x + 1][c.y] != '#' && map[c.x + 1][c.y] != '<' && map[c.x + 1][c.y] != '>')
1265                    map[c.x + 1][c.y] = ',';
1266                if (map[c.x][c.y - 1] != '#' && map[c.x][c.y - 1] != '<' && map[c.x][c.y - 1] != '>')
1267                    map[c.x][c.y - 1] = ',';
1268                if (map[c.x][c.y + 1] != '#' && map[c.x][c.y + 1] != '<' && map[c.x][c.y + 1] != '>')
1269                    map[c.x][c.y + 1] = ',';
1270            }
1271        }
1272
1273        if(trapFill > 0)
1274        {
1275            int total = hazards.size() * trapFill / 100;
1276
1277            for(int i = 0; i < total; i++)
1278            {
1279                Coord entry = rng.getRandomElement(hazards);
1280                if(map[entry.x][entry.y] == '<' || map[entry.x][entry.y] == '>')
1281                    continue;
1282                map[entry.x][entry.y] = '^';
1283                hazards.remove(entry);
1284            }
1285        }
1286
1287        return map;
1288
1289    }
1290
1291    /**
1292     * Gets the seed that can be used to rebuild an identical dungeon to the latest one generated (or the seed that
1293     * will be used to generate the first dungeon if none has been made yet). You can pass the long this returns to
1294     * the setState() method on this class' rng field, which assuming all other calls to generate a dungeon are
1295     * identical, will ensure generate() or generateRespectingStairs() will produce the same dungeon output as the
1296     * dungeon originally generated with the seed this returned.
1297     * <br>
1298     * You can also call getState() on the rng field yourself immediately before generating a dungeon, but this method
1299     * handles some complexities of when the state is actually used to generate a dungeon; since StatefulRNG objects can
1300     * be shared between different classes that use random numbers, the state could change between when you call
1301     * getState() and when this class generates a dungeon. Using getRebuildSeed() eliminates that confusion.
1302     * @return a seed as a long that can be passed to setState() on this class' rng field to recreate a dungeon
1303     */
1304    public long getRebuildSeed() {
1305        return rebuildSeed;
1306    }
1307
1308    /**
1309     * Provides a string representation of the latest generated dungeon.
1310     *
1311     * @return a printable string version of the latest generated dungeon.
1312     */
1313    @Override
1314        public String toString() {
1315        char[][] trans = new char[height][width];
1316        for (int x = 0; x < width; x++) {
1317            for (int y = 0; y < height; y++) {
1318                trans[y][x] = dungeon[x][y];
1319            }
1320        }
1321        StringBuilder sb = new StringBuilder();
1322        for (int row = 0; row < height; row++) {
1323            sb.append(trans[row]);
1324            sb.append('\n');
1325        }
1326        return sb.toString();
1327    }
1328
1329}