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}