/** * Generate a char[][] dungeon with extra features given a baseDungeon that has already been * generated, and that already has staircases represented by greater than and less than signs. * Typically, you want to call generate with a TilesetType or no argument for the easiest * generation; this method is meant for adding features like water and doors to existing simple * maps. This uses '#' for walls, '.' for floors, '~' for deep water, ',' for shallow water, '^' * for traps, '+' for doors that provide horizontal passage, and '/' for doors that provide * vertical passage. Use the addDoors, addWater, addGrass, and addTraps methods of this class to * request these in the generated map. Also sets the fields stairsUp and stairsDown to null, and * expects stairs to be already handled. * * @param baseDungeon a pre-made dungeon consisting of '#' for walls and '.' for floors, with * stairs already in; may be modified in-place * @return a char[][] dungeon */ public char[][] generateRespectingStairs(char[][] baseDungeon) { if (!seedFixed) { rebuildSeed = rng.getState(); } seedFixed = false; char[][] map = DungeonUtility.wallWrap(baseDungeon); DijkstraMap dijkstra = new DijkstraMap(map); stairsUp = null; stairsDown = null; dijkstra.clearGoals(); ArrayList<Coord> stairs = DungeonUtility.allMatching(map, '<', '>'); for (int j = 0; j < stairs.size(); j++) { dijkstra.setGoal(stairs.get(j)); } dijkstra.scan(null); for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { if (dijkstra.gradientMap[i][j] >= DijkstraMap.FLOOR) { map[i][j] = '#'; } } } return innerGenerate(map); }
/** Make a DungeonGenerator with a LightRNG using a random seed, height 40, and width 40. */ public DungeonGenerator() { rng = new StatefulRNG(); gen = new DungeonBoneGen(rng); utility = new DungeonUtility(rng); rebuildSeed = rng.getState(); height = 40; width = 40; fx = new EnumMap<>(FillEffect.class); }
/** * Copies all fields from copying and makes a new DungeonGenerator. * * @param copying the DungeonGenerator to copy */ public DungeonGenerator(DungeonGenerator copying) { rng = new StatefulRNG(copying.rng.getState()); gen = new DungeonBoneGen(rng); utility = new DungeonUtility(rng); rebuildSeed = rng.getState(); height = copying.height; width = copying.width; fx = new EnumMap<>(copying.fx); dungeon = copying.dungeon; }
/** * Generate a char[][] dungeon with extra features given a baseDungeon that has already been * generated. Typically, you want to call generate with a TilesetType or no argument for the * easiest generation; this method is meant for adding features like water and doors to existing * simple maps. This uses '#' for walls, '.' for floors, '~' for deep water, ',' for shallow * water, '^' for traps, '+' for doors that provide horizontal passage, and '/' for doors that * provide vertical passage. Use the addDoors, addWater, addGrass, and addTraps methods of this * class to request these in the generated map. Also sets the fields stairsUp and stairsDown to * two randomly chosen, distant, connected, walkable cells. <br> * Special behavior here: If tab characters are present in the 2D char array, they will be * replaced with '.' in the final dungeon, but will also be tried first as valid staircase * locations (with a high distance possible to travel away from the starting staircase). If no tab * characters are present this will search for '.' floors to place stairs on, as normal. This * tab-first behavior is useful in conjunction with some methods that establish a good path in an * existing dungeon; an example is {@code DungeonUtility.ensurePath(dungeon, rng, '\t', '#');} * then passing dungeon (which that code modifies) in as baseDungeon to this method. * * @param baseDungeon a pre-made dungeon consisting of '#' for walls and '.' for floors; may be * modified in-place * @return a char[][] dungeon */ public char[][] generate(char[][] baseDungeon) { if (!seedFixed) { rebuildSeed = rng.getState(); } seedFixed = false; char[][] map = DungeonUtility.wallWrap(baseDungeon); width = map.length; height = map[0].length; DijkstraMap dijkstra = new DijkstraMap(map); int frustrated = 0; do { dijkstra.clearGoals(); stairsUp = utility.randomMatchingTile(map, '\t'); if (stairsUp == null) { stairsUp = utility.randomFloor(map); if (stairsUp == null) { frustrated++; continue; } } dijkstra.setGoal(stairsUp); dijkstra.scan(null); frustrated++; } while (dijkstra.getMappedCount() < width + height && frustrated < 8); if (frustrated >= 8) { return generate(); } double maxDijkstra = 0.0; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { if (dijkstra.gradientMap[i][j] >= DijkstraMap.FLOOR) { map[i][j] = '#'; } else if (dijkstra.gradientMap[i][j] > maxDijkstra) { maxDijkstra = dijkstra.gradientMap[i][j]; } if (map[i][j] == '\t') { map[i][j] = '.'; } } } stairsDown = new GreasedRegion(dijkstra.gradientMap, maxDijkstra * 0.7, DijkstraMap.FLOOR) .singleRandom(rng); return innerGenerate(map); }
private char[][] innerGenerate(char[][] map) { OrderedSet<Coord> doorways; OrderedSet<Coord> hazards = new OrderedSet<>(); Coord temp; boolean doubleDoors = false; int floorCount = DungeonUtility.countCells(map, '.'), doorFill = 0, waterFill = 0, grassFill = 0, trapFill = 0, boulderFill = 0, islandSpacing = 0; if (fx.containsKey(FillEffect.DOORS)) { doorFill = fx.get(FillEffect.DOORS); if (doorFill < 0) { doubleDoors = true; doorFill *= -1; } } if (fx.containsKey(FillEffect.GRASS)) { grassFill = fx.get(FillEffect.GRASS); } if (fx.containsKey(FillEffect.WATER)) { waterFill = fx.get(FillEffect.WATER); } if (fx.containsKey(FillEffect.BOULDERS)) { boulderFill = fx.get(FillEffect.BOULDERS) * floorCount / 100; } if (fx.containsKey(FillEffect.ISLANDS)) { islandSpacing = fx.get(FillEffect.ISLANDS); } if (fx.containsKey(FillEffect.TRAPS)) { trapFill = fx.get(FillEffect.TRAPS); } doorways = viableDoorways(doubleDoors, map); OrderedSet<Coord> obstacles = new OrderedSet<>(doorways.size() * doorFill / 100); if (doorFill > 0) { int total = doorways.size() * doorFill / 100; BigLoop: for (int i = 0; i < total; i++) { Coord entry = rng.getRandomElement(doorways); if (map[entry.x][entry.y] == '<' || map[entry.x][entry.y] == '>') continue; if (map[entry.x - 1][entry.y] != '#' && map[entry.x + 1][entry.y] != '#') { map[entry.x][entry.y] = '+'; } else { map[entry.x][entry.y] = '/'; } obstacles.add(entry); Coord[] adj = new Coord[] { Coord.get(entry.x + 1, entry.y), Coord.get(entry.x - 1, entry.y), Coord.get(entry.x, entry.y + 1), Coord.get(entry.x, entry.y - 1) }; for (Coord near : adj) { if (doorways.contains(near)) { map[near.x][near.y] = '#'; obstacles.add(near); doorways.remove(near); i++; doorways.remove(entry); continue BigLoop; } } doorways.remove(entry); } } if (boulderFill > 0.0) { /* short[] floor = pack(map, '.'); short[] viable = retract(floor, 1, width, height, true); ArrayList<Coord> boulders = randomPortion(viable, boulderFill, rng); for (Coord boulder : boulders) { map[boulder.x][boulder.y] = '#'; } */ Coord[] boulders = new GreasedRegion(map, '.').retract8way(1).randomPortion(rng, boulderFill); Coord t; for (int i = 0; i < boulders.length; i++) { t = boulders[i]; map[t.x][t.y] = '#'; } } if (trapFill > 0) { for (int x = 1; x < map.length - 1; x++) { for (int y = 1; y < map[x].length - 1; y++) { temp = Coord.get(x, y); if (map[x][y] == '.' && !obstacles.contains(temp)) { int ctr = 0; if (map[x + 1][y] != '#') ++ctr; if (map[x - 1][y] != '#') ++ctr; if (map[x][y + 1] != '#') ++ctr; if (map[x][y - 1] != '#') ++ctr; if (map[x + 1][y + 1] != '#') ++ctr; if (map[x - 1][y + 1] != '#') ++ctr; if (map[x + 1][y - 1] != '#') ++ctr; if (map[x - 1][y - 1] != '#') ++ctr; if (ctr >= 5) hazards.add(Coord.get(x, y)); } } } } short[] floors = pack(map, '.'), working; floorCount = count(floors); float waterRate = waterFill / 100.0f, grassRate = grassFill / 100.0f; if (waterRate + grassRate > 1.0f) { waterRate /= (waterFill + grassFill) / 100.0f; grassRate /= (waterFill + grassFill) / 100.0f; } int targetWater = Math.round(floorCount * waterRate), targetGrass = Math.round(floorCount * grassRate), sizeWaterPools = targetWater / rng.between(3, 6), sizeGrassPools = targetGrass / rng.between(2, 5); Coord[] scatter; int remainingWater = targetWater, remainingGrass = targetGrass; if (targetWater > 0) { scatter = fractionPacked(floors, 7); scatter = rng.shuffle(scatter, new Coord[scatter.length]); ArrayList<Coord> allWater = new ArrayList<>(targetWater); for (int i = 0; i < scatter.length; i++) { if (remainingWater > 5) // remainingWater >= targetWater * 0.02 && { if (!queryPacked(floors, scatter[i].x, scatter[i].y)) continue; working = spill( floors, packOne(scatter[i]), rng.between(4, Math.min(remainingWater, sizeWaterPools)), rng); floors = differencePacked(floors, working); Coord[] pts = allPacked(working); remainingWater -= pts.length; Collections.addAll(allWater, pts); } else break; } for (Coord pt : allWater) { hazards.remove(pt); // obstacles.add(pt); if (map[pt.x][pt.y] != '<' && map[pt.x][pt.y] != '>') map[pt.x][pt.y] = '~'; } for (Coord pt : allWater) { if (map[pt.x][pt.y] != '<' && map[pt.x][pt.y] != '>' && (map[pt.x - 1][pt.y] == '.' || map[pt.x + 1][pt.y] == '.' || map[pt.x][pt.y - 1] == '.' || map[pt.x][pt.y + 1] == '.')) map[pt.x][pt.y] = ','; } } if (targetGrass > 0) { scatter = fractionPacked(floors, 7); scatter = rng.shuffle(scatter, new Coord[scatter.length]); Coord p; for (int i = 0; i < scatter.length; i++) { if (remainingGrass > 5) // remainingGrass >= targetGrass * 0.02 && { working = spill( floors, packOne(scatter[i]), rng.between(4, Math.min(remainingGrass, sizeGrassPools)), rng); if (working.length == 0) continue; floors = differencePacked(floors, working); Coord[] pts = allPacked(working); remainingGrass -= pts.length; for (int c = 0; c < pts.length; c++) { p = pts[c]; map[p.x][p.y] = '"'; } } else break; } } if (islandSpacing > 1 && targetWater > 0) { ArrayList<Coord> islands = PoissonDisk.sampleMap( map, 1f * islandSpacing, rng, '#', '.', '"', '+', '/', '^', '<', '>'); for (Coord c : islands) { map[c.x][c.y] = '.'; if (map[c.x - 1][c.y] != '#' && map[c.x - 1][c.y] != '<' && map[c.x - 1][c.y] != '>') map[c.x - 1][c.y] = ','; if (map[c.x + 1][c.y] != '#' && map[c.x + 1][c.y] != '<' && map[c.x + 1][c.y] != '>') map[c.x + 1][c.y] = ','; if (map[c.x][c.y - 1] != '#' && map[c.x][c.y - 1] != '<' && map[c.x][c.y - 1] != '>') map[c.x][c.y - 1] = ','; if (map[c.x][c.y + 1] != '#' && map[c.x][c.y + 1] != '<' && map[c.x][c.y + 1] != '>') map[c.x][c.y + 1] = ','; } } if (trapFill > 0) { int total = hazards.size() * trapFill / 100; for (int i = 0; i < total; i++) { Coord entry = rng.getRandomElement(hazards); if (map[entry.x][entry.y] == '<' || map[entry.x][entry.y] == '<') continue; map[entry.x][entry.y] = '^'; hazards.remove(entry); } } dungeon = map; return map; }
/** * Generate a char[][] dungeon given a TilesetType; the comments in that class provide some * opinions on what each TilesetType value could be used for in a game. This uses '#' for walls, * '.' for floors, '~' for deep water, ',' for shallow water, '^' for traps, '+' for doors that * provide horizontal passage, and '/' for doors that provide vertical passage. Use the addDoors, * addWater, addGrass, and addTraps methods of this class to request these in the generated map. * Also sets the fields stairsUp and stairsDown to two randomly chosen, distant, connected, * walkable cells. * * @see squidpony.squidgrid.mapping.styled.TilesetType * @param kind a TilesetType enum value, such as TilesetType.DEFAULT_DUNGEON * @return a char[][] dungeon */ public char[][] generate(TilesetType kind) { seedFixed = true; rebuildSeed = rng.getState(); return generate(gen.generate(kind, width, height)); }