#include "StarDungeonGenerator.hpp" #include "StarCasting.hpp" #include "StarRandom.hpp" #include "StarLogging.hpp" #include "StarAssets.hpp" #include "StarLexicalCast.hpp" #include "StarJsonExtra.hpp" #include "StarMaterialDatabase.hpp" #include "StarRoot.hpp" #include "StarLiquidsDatabase.hpp" #include "StarDungeonImagePart.hpp" #include "StarDungeonTMXPart.hpp" namespace Star { size_t const DefinitionsCacheSize = 20; namespace Dungeon { EnumMap const DungeonDirectionNames{ {Dungeon::Direction::Left, "left"}, {Dungeon::Direction::Right, "right"}, {Dungeon::Direction::Up, "up"}, {Dungeon::Direction::Down, "down"}, {Dungeon::Direction::Unknown, "unknown"}, {Dungeon::Direction::Any, "any"}, }; Direction flipDirection(Direction direction) { if (direction == Direction::Left) return Direction::Right; if (direction == Direction::Right) return Direction::Left; if (direction == Direction::Up) return Direction::Down; if (direction == Direction::Down) return Direction::Up; if (direction == Direction::Any) return Direction::Any; throw DungeonException("Invalid direction"); } MaterialId biomeMaterialForJson(int variant) { if (variant == 0) return BiomeMaterialId; if (variant == 1) return Biome1MaterialId; if (variant == 2) return Biome2MaterialId; if (variant == 3) return Biome3MaterialId; if (variant == 4) return Biome4MaterialId; starAssert(variant == 5); return Biome5MaterialId; } ConnectorConstPtr chooseOption(List& options, RandomSource& rnd) { float distribution = 0; for (size_t i = 0; i < options.size(); i++) distribution += options[i]->part()->chance(); float pick = rnd.randf() * distribution; for (size_t i = 0; i < options.size(); i++) { pick -= options[i]->part()->chance(); if (pick <= 0) return options.takeAt(i); } // float rounding is always fun return options.takeAt(options.size() - 1); } List Rule::readRules(Json const& rules) { List result; for (auto const& list : rules.iterateArray()) { Maybe rule = Rule::parse(list); if (rule.isValid()) result.push_back(*rule); } return result; } List Brush::readBrushes(Json const& brushes) { List result; for (auto const& list : brushes.iterateArray()) result.push_back(Brush::parse(list)); return result; } Maybe Rule::parse(Json const& rule) { String key = rule.getString(0); if (key == "worldGenMustContainLiquid") return as(make_shared()); if (key == "worldGenMustNotContainLiquid") return as(make_shared()); if (key == "worldGenMustContainSolidForeground") return as(make_shared(TileLayer::Foreground)); if (key == "worldGenMustContainAirForeground") return as(make_shared(TileLayer::Foreground)); if (key == "worldGenMustContainSolidBackground") return as(make_shared(TileLayer::Background)); if (key == "worldGenMustContainAirBackground") return as(make_shared(TileLayer::Background)); if (key == "allowOverdrawing") return as(make_shared()); if (key == "ignorePartMaximumRule") return as(make_shared()); if (key == "maxSpawnCount") return as(make_shared(rule)); if (key == "doNotConnectToPart") return as(make_shared(rule)); if (key == "doNotCombineWith") return as(make_shared(rule)); Logger::error("Unknown dungeon rule: {}", key); return Maybe(); } bool Rule::checkTileCanPlace(Vec2I, DungeonGeneratorWriter*) const { return true; } bool Rule::overdrawable() const { return false; } bool Rule::ignorePartMaximum() const { return false; } bool Rule::allowSpawnCount(int) const { return true; } bool Rule::doesNotConnectToPart(String const&) const { return false; } bool Rule::checkPartCombinationsAllowed(StringMap const&) const { return true; } bool Rule::requiresOpen() const { return false; } bool Rule::requiresSolid() const { return false; } bool Rule::requiresLiquid() const { return false; } BrushConstPtr parseFrontBrush(Json const& brush) { String material; Maybe mod; Maybe hueshift, modhueshift; Maybe colorVariant; if (brush.isType(Json::Type::Object)) { material = brush.getString("material"); mod = brush.optString("mod"); hueshift = brush.optFloat("hueshift"); modhueshift = brush.optFloat("modhueshift"); colorVariant = brush.optFloat("colorVariant"); } else { material = brush.getString(1); if (brush.size() > 2) mod = brush.getString(2); } return make_shared(material, mod, hueshift, modhueshift, colorVariant); } BrushConstPtr parseBackBrush(Json const& brush) { String material; Maybe mod; Maybe hueshift, modhueshift; Maybe colorVariant; if (brush.isType(Json::Type::Object)) { material = brush.getString("material"); mod = brush.optString("mod"); hueshift = brush.optFloat("hueshift"); modhueshift = brush.optFloat("modhueshift"); colorVariant = brush.optFloat("colorVariant"); } else { material = brush.getString(1); if (brush.size() > 2) mod = brush.getString(2); } return make_shared(material, mod, hueshift, modhueshift, colorVariant); } BrushConstPtr parseObjectBrush(Json const& brush) { String object; Star::Direction direction; Json parameters; object = brush.getString(1); JsonObject settings; if (brush.size() > 2) settings = brush.getObject(2); if (settings.contains("direction")) direction = DirectionNames.getLeft(settings.get("direction").toString()); else direction = Star::Direction::Left; if (settings.contains("parameters")) parameters = settings.get("parameters"); return make_shared(object, direction, parameters); } BrushConstPtr parseSurfaceBrush(Json const& brush) { Json settings = Json::ofType(Json::Type::Object); if (brush.size() > 1) settings = brush.get(1); return make_shared(settings.optInt("variant"), settings.optString("mod")); } BrushConstPtr parseSurfaceBackgroundBrush(Json const& brush) { Json settings = Json::ofType(Json::Type::Object); if (brush.size() > 1) settings = brush.get(1); return make_shared(settings.optInt("variant"), settings.optString("mod")); } BrushConstPtr parseWireBrush(Json const& brush) { Json settings = brush.get(1); String group = settings.getString("group"); bool local = settings.getBool("local", true); return make_shared(group, local); } BrushConstPtr parseItemBrush(Json const& brush) { ItemDescriptor item(brush.getString(1), 1); return make_shared(item); } BrushConstPtr Brush::parse(Json const& brush) { String key = brush.getString(0); if (key == "clear") return as(make_shared()); if (key == "front") return parseFrontBrush(brush); if (key == "back") return parseBackBrush(brush); if (key == "object") return parseObjectBrush(brush); if (key == "biomeitems") return as(make_shared()); if (key == "biometree") return as(make_shared()); if (key == "item") return parseItemBrush(brush); if (key == "npc") return as(make_shared(brush.get(1))); if (key == "stagehand") return as(make_shared(brush.get(1))); if (key == "random") return as(make_shared(brush)); if (key == "surface") return parseSurfaceBrush(brush); if (key == "surfacebackground") return parseSurfaceBackgroundBrush(brush); if (key == "liquid") return as(make_shared(brush.getString(1), 1.0f, brush.getBool(2, false))); if (key == "wire") return parseWireBrush(brush); if (key == "playerstart") return as(make_shared()); throw DungeonException::format("Unknown dungeon brush: {}", key); } RandomBrush::RandomBrush(Json const& brush) { JsonArray options = brush.getArray(1); for (auto const& option : options) m_brushes.append(Brush::parse(option)); m_seed = Random::randi64(); } void RandomBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { size_t rnd = (size_t)staticRandomI32(m_seed, position[0], position[1]); m_brushes[rnd % m_brushes.size()]->paint(position, phase, writer); } void ClearBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::ClearPhase) return; // TODO: delete objects too? writer->setLiquid(position, LiquidStore(EmptyLiquidId, 0.0f, 0.0f, false)); writer->setForegroundMaterial(position, EmptyMaterialId, 0, DefaultMaterialColorVariant); writer->setBackgroundMaterial(position, EmptyMaterialId, 0, DefaultMaterialColorVariant); writer->setForegroundMod(position, NoModId, 0); writer->setBackgroundMod(position, NoModId, 0); } FrontBrush::FrontBrush(String const& material, Maybe mod, Maybe hueshift, Maybe modhueshift, Maybe colorVariant) { m_material = material; m_mod = mod; m_materialHue = hueshift.apply(materialHueFromDegrees).value(0); m_modHue = modhueshift.apply(materialHueFromDegrees).value(0); m_materialColorVariant = colorVariant.value(DefaultMaterialColorVariant); } void FrontBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::WallPhase) return; auto materialDatabase = Root::singleton().materialDatabase(); MaterialId material = materialDatabase->materialId(m_material); ModId mod = NoModId; if (m_mod) mod = materialDatabase->modId(*m_mod); if (isSolidColliding(materialDatabase->materialCollisionKind(material))) writer->setLiquid(position, LiquidStore(EmptyLiquidId, 0.0f, 0.0f, false)); writer->setForegroundMaterial(position, material, m_materialHue, m_materialColorVariant); if (isRealMod(mod)) { writer->setForegroundMod(position, mod, m_modHue); } } BackBrush::BackBrush(String const& material, Maybe mod, Maybe hueshift, Maybe modhueshift, Maybe colorVariant) { m_material = material; m_mod = mod; m_materialHue = hueshift.apply(materialHueFromDegrees).value(0); m_modHue = modhueshift.apply(materialHueFromDegrees).value(0); m_materialColorVariant = colorVariant.value(DefaultMaterialColorVariant); } void BackBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::WallPhase) return; auto materialDatabase = Root::singleton().materialDatabase(); MaterialId material = materialDatabase->materialId(m_material); ModId mod = NoModId; if (m_mod) mod = materialDatabase->modId(*m_mod); writer->setBackgroundMaterial(position, material, m_materialHue, m_materialColorVariant); if (isRealMod(mod)) { writer->setBackgroundMod(position, mod, m_modHue); } } ObjectBrush::ObjectBrush(String const& object, Star::Direction direction, Json const& parameters) { m_object = object; m_direction = direction; m_parameters = parameters; } void ObjectBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::ObjectPhase) return; writer->placeObject(position, m_object, m_direction, m_parameters); } VehicleBrush::VehicleBrush(String const& vehicle, Json const& parameters) { m_vehicle = vehicle; m_parameters = parameters; } void VehicleBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::ObjectPhase) return; writer->placeVehicle(Vec2F(position), m_vehicle, m_parameters); } void BiomeItemsBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::BiomeItemsPhase) return; writer->placeSurfaceBiomeItems(position); } void BiomeTreeBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::BiomeTreesPhase) return; writer->placeBiomeTree(position); } ItemBrush::ItemBrush(ItemDescriptor const& item) : m_item(item) {} void ItemBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::ItemPhase) return; writer->addDrop(Vec2F(position), m_item); } NpcBrush::NpcBrush(Json const& brush) { m_npc = brush; auto map = m_npc.toObject(); if (map.value("seed") == Json("stable")) map["seed"] = Random::randu64(); m_npc = map; } void NpcBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::NpcPhase) return; if (m_npc.contains("species")) { // interpret species as a comma separated list of unquoted strings StringList speciesOptions = m_npc.get("species").toString().replace(" ", "").split(","); writer->spawnNpc(Vec2F(position), m_npc.set("species", Random::randFrom(speciesOptions))); } else { writer->spawnNpc(Vec2F(position), m_npc); } } StagehandBrush::StagehandBrush(Json const& definition) { m_definition = definition; } void StagehandBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::NpcPhase) return; writer->spawnStagehand(Vec2F(position), m_definition); } DungeonIdBrush::DungeonIdBrush(DungeonId dungeonId) { m_dungeonId = dungeonId; } void DungeonIdBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::DungeonIdPhase) return; writer->setDungeonId(position, m_dungeonId); } SurfaceBrush::SurfaceBrush(Maybe variant, Maybe mod) { m_variant = variant.value(0); m_mod = mod; } void SurfaceBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase == Phase::WallPhase) { writer->setForegroundMaterial(position, biomeMaterialForJson(m_variant), 0, DefaultMaterialColorVariant); writer->setBackgroundMaterial(position, biomeMaterialForJson(m_variant), 0, DefaultMaterialColorVariant); } if (phase == Phase::ModsPhase) { if (m_mod.isValid()) { auto materialDatabase = Root::singleton().materialDatabase(); writer->setForegroundMod(position, materialDatabase->modId(*m_mod), 0); } else { if (writer->needsForegroundBiomeMod(position)) { writer->setForegroundMod(position, BiomeModId, 0); } } } } SurfaceBackgroundBrush::SurfaceBackgroundBrush(Maybe variant, Maybe mod) { m_variant = variant.value(0); m_mod = mod; } void SurfaceBackgroundBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase == Phase::WallPhase) { writer->setBackgroundMaterial(position, biomeMaterialForJson(m_variant), 0, DefaultMaterialColorVariant); } if (phase == Phase::ModsPhase) { if (m_mod.isValid()) { auto materialDatabase = Root::singleton().materialDatabase(); writer->setBackgroundMod(position, materialDatabase->modId(*m_mod), 0); } else { if (writer->needsBackgroundBiomeMod(position)) { writer->setBackgroundMod(position, BiomeModId, 0); } } } } LiquidBrush::LiquidBrush(String const& liquidName, float quantity, bool source) : m_liquid(liquidName), m_quantity(quantity), m_source(source) {} void LiquidBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { auto liquidsDatabase = Root::singleton().liquidsDatabase(); LiquidId liquidId = liquidsDatabase->liquidId(m_liquid); LiquidStore liquid(liquidId, m_quantity, 1.0f, m_source); if (phase == Phase::WallPhase) { writer->requestLiquid(position, liquid); } } void WireBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase != Phase::WirePhase) return; writer->requestWire(position, m_wireGroup, m_partLocal); } void PlayerStartBrush::paint(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { if (phase == Phase::NpcPhase) writer->setPlayerStart(Vec2F(position)); } InvalidBrush::InvalidBrush(Maybe nameHint) : m_nameHint(nameHint) {} void InvalidBrush::paint(Vec2I, Phase, DungeonGeneratorWriter*) const { if (m_nameHint) Logger::error("Invalid tile '{}'", *m_nameHint); else Logger::error("Invalid tile"); } bool Tile::canPlace(Vec2I position, DungeonGeneratorWriter* writer) const { if (writer->otherDungeonPresent(position)) return false; else if (position[1] < 0) return false; for (size_t i = 0; i < rules.size(); i++) if (!rules[i]->checkTileCanPlace(position, writer)) return false; return true; } void Tile::place(Vec2I position, Phase phase, DungeonGeneratorWriter* writer) const { for (size_t i = 0; i < brushes.size(); i++) { brushes[i]->paint(position, phase, writer); } } bool Tile::usesPlaces() const { if (brushes.size() == 0) return false; for (size_t i = 0; i < rules.size(); i++) if (rules[i]->overdrawable()) return false; return true; } bool Tile::modifiesPlaces() const { return brushes.size() != 0; } bool Tile::collidesWithPlaces() const { return usesPlaces(); } bool Tile::requiresOpen() const { for (size_t i = 0; i < rules.size(); i++) if (rules[i]->requiresOpen()) return true; return false; } bool Tile::requiresSolid() const { for (size_t i = 0; i < rules.size(); i++) if (rules[i]->requiresSolid()) return true; return false; } bool Tile::requiresLiquid() const { for (size_t i = 0; i < rules.size(); i++) if (rules[i]->requiresLiquid()) return true; return false; } PartConstPtr parsePart(DungeonDefinition* dungeon, Json const& definition, Maybe tileset) { String kind = definition.get("def").getString(0); if (kind == "image") { if (tileset.isNothing()) throw DungeonException("Dungeon parts designed in images require the 'tiles' key in the .dungeon file"); return make_shared(dungeon, definition, make_shared(*tileset)); } else if (kind == "tmx") return make_shared(dungeon, definition, make_shared()); throw DungeonException::format("Unknown dungeon part kind: {}", kind); } Part::Part(DungeonDefinition* dungeon, Json const& part, PartReaderPtr reader) { m_dungeon = dungeon; m_name = part.getString("name"); m_rules = Rule::readRules(part.get("rules")); m_chance = part.getFloat("chance", 1); if (m_chance <= 0) m_chance = 0.0001f; m_markDungeonId = part.getBool("markDungeonId", true); m_overrideAllowAlways = part.getBool("overrideAllowAlways", false); m_minimumThreatLevel = part.optFloat("minimumThreatLevel"); m_maximumThreatLevel = part.optFloat("maximumThreatLevel"); m_clearAnchoredObjects = part.getBool("clearAnchoredObjects", true); m_reader = reader; Json const& def = part.get("def"); if (def.get(1).type() == Json::Type::String) { reader->readAsset(AssetPath::relativeTo(dungeon->directory(), def.get(1).toString())); } else { for (auto const& asset : def.get(1).iterateArray()) reader->readAsset(AssetPath::relativeTo(dungeon->directory(), asset.toString())); } m_size = m_reader->size(); scanConnectors(); scanAnchor(); } String const& Part::name() const { return m_name; } Vec2U Part::size() const { return m_size; } Vec2I Part::anchorPoint() const { return m_anchorPoint; } float Part::chance() const { return m_chance; } bool Part::markDungeonId() const { return m_markDungeonId; } Maybe Part::minimumThreatLevel() const { return m_minimumThreatLevel; } Maybe Part::maximumThreatLevel() const { return m_maximumThreatLevel; } bool Part::clearAnchoredObjects() const { return m_clearAnchoredObjects; } int Part::placementLevelConstraint() const { Vec2I air = {0, size().y()}; Vec2I ground = {0, 0}; Vec2I liquid = {0, 0}; m_reader->forEachTile([&ground, &air, &liquid](Vec2I tilePos, Tile const& tile) -> bool { for (auto const& rule : tile.rules) { if (is(rule) && tilePos.y() > ground.y()) { ground = tilePos; } if (is(rule) && tilePos.y() < air.y()) { air = tilePos; } if ((is(rule) || is(rule)) && tilePos.y() > liquid.y()) { liquid = tilePos; } } return false; }); ground[1] = max(ground[1], liquid[1]); if (air.y() < ground.y()) throw DungeonException("Invalid ground vs air contraint. Ground at: " + toString(ground.y()) + " Air at: " + toString(air.y()) + " Pixels: highest ground:" + toString(ground) + " lowest air:" + toString(air)); return air.y(); } bool Part::ignoresPartMaximum() const { for (size_t i = 0; i < m_rules.size(); i++) if (m_rules[i]->ignorePartMaximum()) return true; return false; } bool Part::allowsPlacement(int currentPlacementCount) const { for (size_t i = 0; i < m_rules.size(); i++) if (!m_rules[i]->allowSpawnCount(currentPlacementCount)) return false; return true; } List const& Part::connections() const { return m_connections; } bool Part::doesNotConnectTo(Part* part) const { for (size_t i = 0; i < m_rules.size(); i++) if (m_rules[i]->doesNotConnectToPart(part->name())) return true; for (size_t i = 0; i < part->m_rules.size(); i++) if (part->m_rules[i]->doesNotConnectToPart(m_name)) return true; return false; } bool Part::checkPartCombinationsAllowed(StringMap const& placementCounter) const { for (size_t i = 0; i < m_rules.size(); i++) if (!m_rules[i]->checkPartCombinationsAllowed(placementCounter)) return false; return true; } bool Part::collidesWithPlaces(Vec2I pos, Set& places) const { if (m_overrideAllowAlways) return true; bool result = false; m_reader->forEachTile([&result, pos, &places](Vec2I tilePos, Tile const& tile) -> bool { if (tile.collidesWithPlaces()) if (places.contains(pos + tilePos)) { Logger::debug("Tile collided with place at {}", pos + tilePos); result = true; return true; } return false; }); return result; } bool Part::canPlace(Vec2I pos, DungeonGeneratorWriter* writer) const { if (m_overrideAllowAlways) return true; // Speed up repeated failing calls by first checking the tile that failed // last time (if it did). bool result = true; m_reader->forEachTile([&result, pos, writer](Vec2I tilePos, Tile const& tile) -> bool { Vec2I position = pos + tilePos; if (!tile.canPlace(position, writer)) { result = false; return true; } return false; }); return result; } void Part::place(Vec2I pos, Set const& places, DungeonGeneratorWriter* writer) const { placePhase(pos, Phase::ClearPhase, places, writer); placePhase(pos, Phase::WallPhase, places, writer); placePhase(pos, Phase::ModsPhase, places, writer); placePhase(pos, Phase::ObjectPhase, places, writer); placePhase(pos, Phase::BiomeTreesPhase, places, writer); placePhase(pos, Phase::BiomeItemsPhase, places, writer); placePhase(pos, Phase::WirePhase, places, writer); placePhase(pos, Phase::ItemPhase, places, writer); placePhase(pos, Phase::NpcPhase, places, writer); placePhase(pos, Phase::DungeonIdPhase, places, writer); } void Part::forEachTile(TileCallback const& callback) const { m_reader->forEachTile(callback); } void Part::placePhase(Vec2I pos, Phase phase, Set const& places, DungeonGeneratorWriter* writer) const { m_reader->forEachTile([&places, pos, phase, writer](Vec2I tilePos, Tile const& tile) -> bool { Vec2I position = pos + tilePos; if (tile.collidesWithPlaces() || !places.contains(position)) { try { tile.place(position, phase, writer); } catch (std::exception const&) { Logger::error("Error at map position {}:", tilePos); throw; } } return false; }); } bool Part::tileUsesPlaces(Vec2I pos) const { bool result = false; m_reader->forEachTileAt(pos, [&result](Vec2I, Tile const& tile) -> bool { if (tile.usesPlaces()) { result = true; return true; } return false; }); return result; } Direction Part::pickByEdge(Vec2I position, Vec2U size) const { int dxa = position[0]; int dxb = size[0] - position[0]; int dya = position[1]; int dyb = size[1] - position[1]; int m = min(min(dxa, dxb), min(dya, dyb)); if (dxa == m) return Direction::Left; if (dxb == m) return Direction::Right; if (dya == m) return Direction::Down; if (dyb == m) return Direction::Up; throw DungeonException("Ambiguous direction"); } Direction Part::pickByNeighbours(Vec2I pos) const { int x = pos.x(), y = pos.y(); // if on a border use that, corners use the left/right direction if (x == 0) return Direction::Left; if (x == (int)size().x() - 1) return Direction::Right; if (y == 0) return Direction::Down; if (y == (int)size().y() - 1) return Direction::Up; // scans around the connector, the direction where it finds a solid is where // it assume the // connection comes from if (tileUsesPlaces({x + 1, y}) && !tileUsesPlaces({x - 1, y})) return Direction::Left; if (tileUsesPlaces({x - 1, y}) && !tileUsesPlaces({x + 1, y})) return Direction::Right; if (tileUsesPlaces({x, y + 1}) && !tileUsesPlaces({x, y - 1})) return Direction::Down; if (tileUsesPlaces({x, y - 1}) && !tileUsesPlaces({x, y + 1})) return Direction::Up; return Direction::Unknown; } void Part::scanConnectors() { try { m_reader->forEachTile([this](Vec2I position, Tile const& tile) -> bool { if (tile.connector.isValid()) { auto d = tile.connector->direction; if (d == Direction::Unknown) d = pickByNeighbours(position); if (d == Direction::Unknown) d = pickByEdge(position, m_size); Logger::debug("Found connector on {} at {} group {} direction {}", m_name, position, tile.connector->value, (int)d); m_connections.append(make_shared(this, tile.connector->value, tile.connector->forwardOnly, d, position)); } return false; }); } catch (std::exception& e) { throw DungeonException(strf("Exception {} in connector {}", outputException(e, true), m_name)); } } void Part::scanAnchor() { int cx, cy, cc; cx = cy = cc = 0; int lowestAir = m_size[1]; int highestGound = -1; int highestLiquid = -1; try { m_reader->forEachTile([&](Vec2I pos, Tile const& tile) -> bool { int x = pos.x(), y = pos.y(); if (tile.collidesWithPlaces()) { cx += x; cy += y; cc++; } if (tile.requiresOpen()) { if ((int)y < lowestAir) lowestAir = y; } if (tile.requiresSolid()) { if ((int)y > highestGound) highestGound = y; } if (tile.requiresLiquid()) { if ((int)y > highestLiquid) highestLiquid = y; } return false; }); } catch (std::exception& e) { throw DungeonException(strf("Exception {} in part {}", outputException(e, true), m_name)); } highestGound = max(highestGound, highestLiquid); if (highestGound == -1) highestGound = lowestAir - 1; if (lowestAir == (int)m_size[1]) lowestAir = highestGound + 1; if (cc == 0) { cx = m_size[0] / 2; cy = m_size[1] / 2; } else { cx /= cc; cy /= cc; } if (highestGound != -1) cy = highestGound + 1; m_anchorPoint = {cx, cy}; } bool WorldGenMustContainSolidRule::checkTileCanPlace(Vec2I position, DungeonGeneratorWriter* writer) const { return writer->checkSolid(position, layer); } bool WorldGenMustContainAirRule::checkTileCanPlace(Vec2I position, DungeonGeneratorWriter* writer) const { return writer->checkOpen(position, layer); } bool WorldGenMustContainLiquidRule::checkTileCanPlace(Vec2I position, DungeonGeneratorWriter * writer) const { return writer->checkLiquid(position); } bool WorldGenMustNotContainLiquidRule::checkTileCanPlace(Vec2I position, DungeonGeneratorWriter * writer) const { return !writer->checkLiquid(position); } Connector::Connector(Part* part, String value, bool forwardOnly, Direction direction, Vec2I offset) : m_value(value), m_forwardOnly(forwardOnly), m_direction(direction), m_offset(offset), m_part(part) {} bool Connector::connectsTo(ConnectorConstPtr connector) const { if (m_forwardOnly) return false; if (m_value != connector->m_value) return false; if (m_direction == Direction::Any || connector->m_direction == Direction::Any) return true; if (m_direction != flipDirection(connector->m_direction)) return false; return true; } String Connector::value() const { return m_value; } Vec2I Connector::positionAdjustment() const { if (m_direction == Direction::Any) return Vec2I(0, 0); if (m_direction == Direction::Left) return Vec2I(-1, 0); if (m_direction == Direction::Right) return Vec2I(1, 0); if (m_direction == Direction::Up) return Vec2I(0, 1); starAssert(m_direction == Direction::Down); return Vec2I(0, -1); } Part* Connector::part() const { return m_part; } Vec2I Connector::offset() const { return m_offset; } DungeonGeneratorWriter::DungeonGeneratorWriter(DungeonGeneratorWorldFacadePtr facade, Maybe terrainMarkingSurfaceLevel, Maybe terrainSurfaceSpaceExtends) : m_facade(facade), m_terrainMarkingSurfaceLevel(terrainMarkingSurfaceLevel), m_terrainSurfaceSpaceExtends(terrainSurfaceSpaceExtends) { m_currentBounds.setMin(Vec2I{std::numeric_limits::max(), std::numeric_limits::max()}); m_currentBounds.setMax(Vec2I{std::numeric_limits::min(), std::numeric_limits::min()}); } Vec2I DungeonGeneratorWriter::wrapPosition(Vec2I const& pos) const { return m_facade->getWorldGeometry().xwrap(pos); } void DungeonGeneratorWriter::setMarkDungeonId(Maybe dungeonId) { m_markDungeonId = dungeonId; } void DungeonGeneratorWriter::requestLiquid(Vec2I const& pos, LiquidStore const& liquid) { m_pendingLiquids[pos] = liquid; } void DungeonGeneratorWriter::setLiquid(Vec2I const& pos, LiquidStore const& liquid) { m_liquids[pos] = liquid; markPosition(pos); } void DungeonGeneratorWriter::setForegroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant) { m_foregroundMaterial[position] = {material, hueshift, colorVariant}; markPosition(position); } void DungeonGeneratorWriter::setBackgroundMaterial(Vec2I const& position, MaterialId material, MaterialHue hueshift, MaterialColorVariant colorVariant) { m_backgroundMaterial[position] = {material, hueshift, colorVariant}; markPosition(position); } void DungeonGeneratorWriter::setForegroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift) { m_foregroundMod[position] = {mod, hueshift}; markPosition(position); } void DungeonGeneratorWriter::setBackgroundMod(Vec2I const& position, ModId mod, MaterialHue hueshift) { m_backgroundMod[position] = {mod, hueshift}; markPosition(position); } bool DungeonGeneratorWriter::needsForegroundBiomeMod(Vec2I const& position) { if (!m_foregroundMaterial.contains(position)) return false; if (!isBiomeMaterial(m_foregroundMaterial[position].material)) return false; Vec2I abovePosition(position.x(), position.y() + 1); if (m_foregroundMaterial.contains(abovePosition)) if (m_foregroundMaterial[abovePosition].material != EmptyMaterialId) return false; return true; } bool DungeonGeneratorWriter::needsBackgroundBiomeMod(Vec2I const& position) { if (!m_backgroundMaterial.contains(position)) return false; if (!isBiomeMaterial(m_backgroundMaterial[position].material)) return false; Vec2I abovePosition(position.x(), position.y() + 1); if (m_backgroundMaterial.contains(abovePosition)) if (m_backgroundMaterial[abovePosition].material != EmptyMaterialId) return false; if (m_foregroundMaterial.contains(abovePosition)) if (m_foregroundMaterial[abovePosition].material != EmptyMaterialId) return false; return true; } void DungeonGeneratorWriter::placeObject(Vec2I const& pos, String const& objectType, Star::Direction direction, Json const& parameters) { m_objects[pos] = {objectType, direction, parameters}; markPosition(pos); } void DungeonGeneratorWriter::placeVehicle(Vec2F const& pos, String const& vehicleName, Json const& parameters) { m_vehicles[pos] = make_pair(vehicleName, parameters); markPosition(pos); } void DungeonGeneratorWriter::placeSurfaceBiomeItems(Vec2I const& pos) { m_biomeItems.insert(pos); markPosition(pos); } void DungeonGeneratorWriter::placeBiomeTree(Vec2I const& pos) { m_biomeTrees.insert(pos); markPosition(pos); } void DungeonGeneratorWriter::addDrop(Vec2F const& position, ItemDescriptor const& item) { m_drops[position] = item; markPosition(position); } void DungeonGeneratorWriter::requestWire(Vec2I const& position, String const& wireGroup, bool partLocal) { if (partLocal) m_openLocalWires[wireGroup].add(position); else m_globalWires[wireGroup].add(position); } void DungeonGeneratorWriter::spawnNpc(Vec2F const& position, Json const& definition) { m_npcs[position] = definition; markPosition(position); } void DungeonGeneratorWriter::spawnStagehand(Vec2F const& position, Json const& definition) { m_stagehands[position] = definition; markPosition(position); } void DungeonGeneratorWriter::setPlayerStart(Vec2F const& startPosition) { m_facade->setPlayerStart(startPosition); } bool DungeonGeneratorWriter::checkSolid(Vec2I position, TileLayer layer) { if (m_terrainMarkingSurfaceLevel) return position.y() < *m_terrainMarkingSurfaceLevel; return m_facade->checkSolid(position, layer); } bool DungeonGeneratorWriter::checkOpen(Vec2I position, TileLayer layer) { if (m_terrainMarkingSurfaceLevel) return position.y() >= *m_terrainMarkingSurfaceLevel; return m_facade->checkOpen(position, layer); } bool DungeonGeneratorWriter::checkLiquid(Vec2I const& position) { return m_facade->checkOceanLiquid(position); } bool DungeonGeneratorWriter::otherDungeonPresent(Vec2I position) { return m_facade->getDungeonIdAt(position) != NoDungeonId; } void DungeonGeneratorWriter::setDungeonId(Vec2I const& pos, DungeonId dungeonId) { m_dungeonIds[pos] = dungeonId; } void DungeonGeneratorWriter::markPosition(Vec2F const& pos) { markPosition(Vec2I(pos.floor())); } void DungeonGeneratorWriter::markPosition(Vec2I const& pos) { m_currentBounds.combine(pos); if (m_markDungeonId) m_dungeonIds[pos] = *m_markDungeonId; } void DungeonGeneratorWriter::clearTileEntities(RectI const& bounds, Set const& positions, bool clearAnchoredObjects) { m_facade->clearTileEntities(bounds, positions, clearAnchoredObjects); } void DungeonGeneratorWriter::finishPart() { for (auto& entries : m_openLocalWires) m_localWires.append(entries.second); m_openLocalWires.clear(); if (m_currentBounds.xMin() > m_currentBounds.xMax()) return; m_boundingBoxes.push_back(m_currentBounds); m_currentBounds.setMin(Vec2I{std::numeric_limits::max(), std::numeric_limits::max()}); m_currentBounds.setMax(Vec2I{std::numeric_limits::min(), std::numeric_limits::min()}); } void DungeonGeneratorWriter::flushLiquid() { // For each liquid type, find each contiguous region of liquid, then // pressurize that region based on the highest position in the region Map> unpressurizedLiquids; for (auto& p : m_pendingLiquids) unpressurizedLiquids[p.second.liquid].add(p.first); for (auto& liquidPair : unpressurizedLiquids) { auto& unpressurized = liquidPair.second; while (!unpressurized.empty()) { // Start with the first unpressurized block as the open set. Vec2I firstBlock = unpressurized.takeFirst(); List openSet = {firstBlock}; Set contiguousRegion = {firstBlock}; // For each element in the previous open set, add all connected blocks // in // the unpressurized set to the new open set and to the total contiguous // region, taking them from the unpressurized set. while (!openSet.empty()) { auto oldOpenSet = take(openSet); for (auto const& p : oldOpenSet) { for (auto dir : {Vec2I(1, 0), Vec2I(-1, 0), Vec2I(0, 1), Vec2I(0, -1)}) { Vec2I pos = p + dir; if (unpressurized.remove(pos)) { contiguousRegion.add(pos); openSet.append(pos); } } } } // Once we have found no more blocks in the unpressurized set to add to // the open set, then we have taken a contiguous region out of the // unpressurized set. Pressurize it based on the highest point. int highestPoint = lowest(); for (auto const& p : contiguousRegion) highestPoint = max(highestPoint, p[1]); for (auto const& p : contiguousRegion) m_pendingLiquids[p].pressure = 1.0f + highestPoint - p[1]; } } for (auto& p : m_pendingLiquids) setLiquid(p.first, p.second); m_pendingLiquids.clear(); } void DungeonGeneratorWriter::flush() { auto geometry = m_facade->getWorldGeometry(); auto displace = [&](Vec2I pos) -> Vec2I { return geometry.xwrap(pos); }; auto displaceF = [&](Vec2F pos) -> Vec2F { return geometry.xwrap(pos); }; PolyF::VertexList terrainBlendingVertexes; PolyF::VertexList spaceBlendingVertexes; for (auto bb : m_boundingBoxes) { m_facade->markRegion(bb); if (m_terrainMarkingSurfaceLevel) { // Mark the regions of the dungeon above the dungeon surface as needing // space, and the regions below the surface as needing terrain if (bb.yMin() < *m_terrainMarkingSurfaceLevel) { RectI lower = bb; lower.setYMax(min(lower.yMax(), *m_terrainMarkingSurfaceLevel)); terrainBlendingVertexes.append(Vec2F(lower.xMin(), lower.yMin())); terrainBlendingVertexes.append(Vec2F(lower.xMax(), lower.yMin())); terrainBlendingVertexes.append(Vec2F(lower.xMin(), lower.yMax())); terrainBlendingVertexes.append(Vec2F(lower.xMax(), lower.yMax())); } if (bb.yMax() > *m_terrainMarkingSurfaceLevel) { RectI upper = bb; upper.setYMin(max(upper.yMin(), *m_terrainMarkingSurfaceLevel)); spaceBlendingVertexes.append(Vec2F(upper.xMin(), upper.yMin())); spaceBlendingVertexes.append(Vec2F(upper.xMax(), upper.yMin())); spaceBlendingVertexes.append(Vec2F(upper.xMin(), upper.yMax() + m_terrainSurfaceSpaceExtends.value(0))); spaceBlendingVertexes.append(Vec2F(upper.xMax(), upper.yMax() + m_terrainSurfaceSpaceExtends.value(0))); } } } if (!terrainBlendingVertexes.empty()) m_facade->markTerrain(PolyF::convexHull(terrainBlendingVertexes)); if (!spaceBlendingVertexes.empty()) m_facade->markSpace(PolyF::convexHull(spaceBlendingVertexes)); for (auto iter = m_backgroundMaterial.begin(); iter != m_backgroundMaterial.end(); iter++) m_facade->setBackgroundMaterial(displace(iter->first), iter->second.material, iter->second.hueshift, iter->second.colorVariant); for (auto iter = m_foregroundMaterial.begin(); iter != m_foregroundMaterial.end(); iter++) m_facade->setForegroundMaterial(displace(iter->first), iter->second.material, iter->second.hueshift, iter->second.colorVariant); for (auto iter = m_foregroundMod.begin(); iter != m_foregroundMod.end(); iter++) m_facade->setForegroundMod(displace(iter->first), iter->second.mod, iter->second.hueshift); for (auto iter = m_backgroundMod.begin(); iter != m_backgroundMod.end(); iter++) m_facade->setBackgroundMod(displace(iter->first), iter->second.mod, iter->second.hueshift); List sortedPositions = m_objects.keys(); sortByComputedValue(sortedPositions, [](Vec2I pos) { return pos[1] + pos[0] / 1000.0f; }); for (auto pos : sortedPositions) { auto& object = m_objects[pos]; m_facade->placeObject(displace(pos), object.objectName, object.direction, object.parameters); } for (auto entry : m_vehicles) { String vehicleName; Json parameters; tie(vehicleName, parameters) = entry.second; m_facade->placeVehicle(displaceF(entry.first), vehicleName, parameters); } sortedPositions = List::from(m_biomeTrees); sortByComputedValue(sortedPositions, [](Vec2I pos) { return pos[1] + pos[0] / 1000.0f; }); for (auto pos : sortedPositions) { m_facade->placeBiomeTree(pos); } sortedPositions = List::from(m_biomeItems); sortByComputedValue(sortedPositions, [](Vec2I pos) { return pos[1] + pos[0] / 1000.0f; }); for (auto pos : sortedPositions) { m_facade->placeSurfaceBiomeItems(pos); } for (auto& npc : m_npcs) { m_facade->spawnNpc(displaceF(npc.first), npc.second); } for (auto& stagehand : m_stagehands) { m_facade->spawnStagehand(displaceF(stagehand.first), stagehand.second); } for (auto& wires : m_globalWires) { List wireGroup; for (auto& pos : wires.second) wireGroup.append(displace(pos)); m_facade->connectWireGroup(wireGroup); } for (auto& wires : m_localWires) { List wireGroup; for (auto& pos : wires) wireGroup.append(displace(pos)); m_facade->connectWireGroup(wireGroup); } for (auto iter = m_drops.begin(); iter != m_drops.end(); iter++) m_facade->addDrop(displaceF(iter->first), iter->second); for (auto iter = m_liquids.begin(); iter != m_liquids.end(); iter++) m_facade->setLiquid(displace(iter->first), iter->second); for (auto const& dungeonId : m_dungeonIds) m_facade->setDungeonIdAt(dungeonId.first, dungeonId.second); } List DungeonGeneratorWriter::boundingBoxes() const { return m_boundingBoxes; } void DungeonGeneratorWriter::reset() { m_currentBounds.setMin(Vec2I{std::numeric_limits::max(), std::numeric_limits::max()}); m_currentBounds.setMax(Vec2I{std::numeric_limits::min(), std::numeric_limits::min()}); m_pendingLiquids.clear(); m_foregroundMaterial.clear(); m_backgroundMaterial.clear(); m_foregroundMod.clear(); m_backgroundMod.clear(); m_objects.clear(); m_biomeTrees.clear(); m_biomeItems.clear(); m_drops.clear(); m_npcs.clear(); m_stagehands.clear(); m_liquids.clear(); m_globalWires.clear(); m_localWires.clear(); m_openLocalWires.clear(); m_boundingBoxes.clear(); } } DungeonDefinitions::DungeonDefinitions() : m_paths(), m_cacheMutex(), m_definitionCache(DefinitionsCacheSize) { auto assets = Root::singleton().assets(); for (auto& file : assets->scan(".dungeon")) { Json dungeon = assets->json(file); m_paths.insert(dungeon.get("metadata").getString("name"), file); } } DungeonDefinitionConstPtr DungeonDefinitions::get(String const& name) const { MutexLocker locker(m_cacheMutex); return m_definitionCache.get(name, [this](String const& name) -> DungeonDefinitionPtr { if (auto path = m_paths.maybe(name)) return readDefinition(*path); throw DungeonException::format("Unknown dungeon: '{}'", name); }); } JsonObject DungeonDefinitions::getMetadata(String const& name) const { auto definition = get(name); return definition->metadata(); } DungeonDefinitionPtr DungeonDefinitions::readDefinition(String const& path) { try { auto assets = Root::singleton().assets(); return make_shared(assets->json(path).toObject(), AssetPath::directory(path)); } catch (std::exception const& e) { throw DungeonException::format("Error loading dungeon '{}': {}", path, outputException(e, false)); } } DungeonDefinition::DungeonDefinition(JsonObject const& definition, String const& directory) { m_directory = directory; m_metadata = definition.get("metadata").toObject(); m_name = m_metadata.get("name").toString(); m_displayName = m_metadata.contains("displayName") ? m_metadata.get("displayName").toString() : ""; m_species = m_metadata.get("species").toString(); m_isProtected = m_metadata.contains("protected") ? m_metadata.get("protected").toBool() : false; if (m_metadata.contains("rules")) m_rules = Dungeon::Rule::readRules(m_metadata.get("rules")); m_maxRadius = m_metadata.value("maxRadius", 100).toInt(); m_maxParts = m_metadata.value("maxParts", 100).toInt(); m_extendSurfaceFreeSpace = m_metadata.value("extendSurfaceFreeSpace", 0).toInt(); m_anchors = jsonToStringList(m_metadata.get("anchor")); auto tileset = definition.maybe("tiles").apply([](Json const& tileset) { return make_shared(tileset); }); for (auto const& partsDefMap : definition.get("parts").iterateArray()) { Dungeon::PartConstPtr part = parsePart(this, partsDefMap, tileset); if (m_parts.contains(part->name())) throw DungeonException::format("Duplicate dungeon part name: {}", part->name()); m_parts.insert(part->name(), part); } if (m_metadata.contains("gravity")) m_gravity = m_metadata.get("gravity").toFloat(); if (m_metadata.contains("breathable")) m_breathable = m_metadata.get("breathable").toBool(); } JsonObject DungeonDefinition::metadata() const { return m_metadata; } String DungeonDefinition::directory() const { return m_directory; } String DungeonDefinition::name() const { return m_name; } String DungeonDefinition::displayName() const { return m_displayName; } bool DungeonDefinition::isProtected() const { return m_isProtected; } Maybe DungeonDefinition::gravity() const { return m_gravity; } Maybe DungeonDefinition::breathable() const { return m_breathable; } StringMap const& DungeonDefinition::parts() const { return m_parts; } List const& DungeonDefinition::anchors() const { return m_anchors; } Maybe const& DungeonDefinition::optTileset() const { return m_tileset; } int DungeonDefinition::maxParts() const { return m_maxParts; } int DungeonDefinition::maxRadius() const { return m_maxRadius; } int DungeonDefinition::extendSurfaceFreeSpace() const { return m_extendSurfaceFreeSpace; } DungeonGenerator::DungeonGenerator(String const& dungeonName, uint64_t seed, float threatLevel, Maybe dungeonId) : m_rand(seed), m_threatLevel(threatLevel), m_dungeonId(dungeonId) { m_def = Root::singleton().dungeonDefinitions()->get(dungeonName); } Maybe, Set>> DungeonGenerator::generate(DungeonGeneratorWorldFacadePtr facade, Vec2I position, bool markSurfaceAndTerrain, bool forcePlacement) { try { Dungeon::DungeonGeneratorWriter writer(facade, markSurfaceAndTerrain ? position[1] : Maybe(), m_def->extendSurfaceFreeSpace()); Logger::debug(forcePlacement ? "Forcing generation of dungeon {}" : "Generating dungeon {}", m_def->name()); Dungeon::PartConstPtr anchor = pickAnchor(); if (!anchor) { Logger::error("No valid anchor piece found for dungeon at {}", position); return {}; } auto pos = position + Vec2I(0, -anchor->placementLevelConstraint()); if (forcePlacement || anchor->canPlace(pos, &writer)) { Logger::info("Placing dungeon at {}", position); return buildDungeon(anchor, pos, &writer, forcePlacement); } else { Logger::debug("Failed to place a dungeon at {}", position); return {}; } } catch (std::exception const& e) { throw DungeonException(strf("Error generating dungeon named '{}'", m_def->name()), e); } } pair, Set> DungeonGenerator::buildDungeon(Dungeon::PartConstPtr anchor, Vec2I basePos, Dungeon::DungeonGeneratorWriter* writer, bool forcePlacement) { writer->reset(); Deque> openSet; StringMap placementCounter; Set modifiedTiles; Set preserveTiles; int piecesPlaced = 0; Logger::debug("Placing dungeon entrance at {}", basePos); auto placePart = [&](Dungeon::Part const* part, Vec2I const& placePos) { Set clearTileEntityPositions; part->forEachTile([&](Vec2I tilePos, Dungeon::Tile const& tile) -> bool { if (tile.modifiesPlaces()) clearTileEntityPositions.insert(writer->wrapPosition(placePos + tilePos)); return false; }); auto partBounds = RectI::withSize(placePos, Vec2I(part->size())); writer->clearTileEntities(partBounds, clearTileEntityPositions, part->clearAnchoredObjects()); if (part->markDungeonId()) writer->setMarkDungeonId(m_dungeonId); else writer->setMarkDungeonId(); part->place(placePos, preserveTiles, writer); writer->finishPart(); part->forEachTile([&](Vec2I tilePos, Dungeon::Tile const& tile) -> bool { if (tile.usesPlaces()) preserveTiles.insert(placePos + tilePos); if (tile.modifiesPlaces()) modifiedTiles.insert(placePos + tilePos); return false; }); openSet.append({part, placePos}); placementCounter[part->name()]++; piecesPlaced++; Logger::debug("placed {}", part->name()); }; placePart(anchor.get(), basePos); Vec2I origin = basePos + Vec2I(anchor->size()) / 2; Set closedConnectors; while (openSet.size()) { Dungeon::Part const* parentPart = openSet.first().first; Vec2I parentPos = openSet.first().second; openSet.takeFirst(); Logger::debug("Trying to add part {} at {} connectors: {}", parentPart->name(), parentPos, parentPart->connections().size()); for (size_t i = 0; i < parentPart->connections().size(); i++) { auto connector = parentPart->connections()[i]; Vec2I connectorPos = parentPos + connector->offset(); if (closedConnectors.contains(connectorPos)) continue; List options = findConnectablePart(connector); while (options.size()) { Dungeon::ConnectorConstPtr option = chooseOption(options, m_rand); Logger::debug("Trying part {}", option->part()->name()); Vec2I partPos = connectorPos - option->offset() + option->positionAdjustment(); Vec2I optionPos = connectorPos + option->positionAdjustment(); if (!option->part()->ignoresPartMaximum()) { if (piecesPlaced >= m_def->maxParts()) continue; if ((partPos - origin).magnitude() > m_def->maxRadius()) { Logger::debug("out of range. {} ... {}", partPos, origin); continue; } } if (!option->part()->allowsPlacement(placementCounter[option->part()->name()])) { Logger::debug("part failed in allowsPlacement"); continue; } if (!option->part()->checkPartCombinationsAllowed(placementCounter)) { Logger::debug("part failed in checkPartCombinationsAllowed"); continue; } if (option->part()->collidesWithPlaces(partPos, preserveTiles)) { Logger::debug("part failed in collidesWithPlaces"); continue; } if (option->part()->minimumThreatLevel() && m_threatLevel < *option->part()->minimumThreatLevel()) { Logger::debug("part failed in minimumThreatLevel"); continue; } if (option->part()->maximumThreatLevel() && m_threatLevel > *option->part()->maximumThreatLevel()) { Logger::debug("part failed in maximumThreatLevel"); continue; } if (forcePlacement || option->part()->canPlace(partPos, writer)) { placePart(option->part(), partPos); closedConnectors.add(connectorPos); closedConnectors.add(optionPos); break; } else { Logger::debug("part failed in canPlace"); } } } } Logger::debug("Settling dungeon water."); writer->flushLiquid(); Logger::debug("Flushing dungeon into the worldgen."); writer->flush(); return {writer->boundingBoxes(), modifiedTiles}; } Dungeon::PartConstPtr DungeonGenerator::pickAnchor() { auto validAnchors = m_def->anchors().filtered([this](String const& anchorName) { auto anchorPart = m_def->parts().get(anchorName); return (!anchorPart->minimumThreatLevel() || m_threatLevel >= *anchorPart->minimumThreatLevel()) && (!anchorPart->maximumThreatLevel() || m_threatLevel <= *anchorPart->maximumThreatLevel()); }); if (validAnchors.empty()) return {}; return m_def->parts().get(m_rand.randFrom(validAnchors)); } List DungeonGenerator::findConnectablePart(Dungeon::ConnectorConstPtr connector) const { List result; for (auto const& partPair : m_def->parts()) { if (partPair.second->doesNotConnectTo(connector->part())) continue; for (auto const& connection : partPair.second->connections()) { if (connection->connectsTo(connector)) result.append(connection); } } return result; } DungeonDefinitionConstPtr DungeonGenerator::definition() const { return m_def; } }