From caec1db5884ff55d5d78abb3a6ec2a3f26615a7c Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 11 Nov 2025 11:24:22 -0500 Subject: [PATCH] Split random map generator into multiple implementation files (#10354) --- VisualStudio/fheroes2/sources.props | 4 + src/fheroes2/maps/map_random_generator.cpp | 909 +++--------------- .../maps/map_random_generator_helper.cpp | 569 +++++++++++ .../maps/map_random_generator_helper.h | 73 ++ .../maps/map_random_generator_info.cpp | 184 ++++ src/fheroes2/maps/map_random_generator_info.h | 208 ++++ 6 files changed, 1153 insertions(+), 794 deletions(-) create mode 100644 src/fheroes2/maps/map_random_generator_helper.cpp create mode 100644 src/fheroes2/maps/map_random_generator_helper.h create mode 100644 src/fheroes2/maps/map_random_generator_info.cpp create mode 100644 src/fheroes2/maps/map_random_generator_info.h diff --git a/VisualStudio/fheroes2/sources.props b/VisualStudio/fheroes2/sources.props index 67e12b86d..e3dc02dfc 100644 --- a/VisualStudio/fheroes2/sources.props +++ b/VisualStudio/fheroes2/sources.props @@ -209,6 +209,8 @@ + + @@ -416,6 +418,8 @@ + + diff --git a/src/fheroes2/maps/map_random_generator.cpp b/src/fheroes2/maps/map_random_generator.cpp index d0c75f63b..d5de43a6d 100644 --- a/src/fheroes2/maps/map_random_generator.cpp +++ b/src/fheroes2/maps/map_random_generator.cpp @@ -36,260 +36,39 @@ #include #include "color.h" -#include "direction.h" #include "editor_interface.h" #include "ground.h" #include "logging.h" #include "map_format_helper.h" #include "map_format_info.h" #include "map_object_info.h" -#include "maps.h" +#include "map_random_generator_helper.h" +#include "map_random_generator_info.h" #include "maps_tiles.h" -#include "maps_tiles_helper.h" #include "math_base.h" -#include "mp2.h" -#include "race.h" #include "rand.h" #include "resource.h" #include "route.h" #include "skill.h" -#include "ui_map_object.h" #include "world.h" -#include "world_object_uid.h" #include "world_pathfinding.h" namespace { - const int neutralColorIndex{ Color::GetIndex( PlayerColor::UNUSED ) }; constexpr int32_t smallestStartingRegionSize{ 200 }; constexpr int32_t emptySpacePercentage{ 40 }; - constexpr int randomCastleIndex{ 12 }; - constexpr int randomTownIndex{ 13 }; constexpr int randomMonsterIndex{ 68 }; - constexpr int maxPlacementAttempts{ 30 }; const std::vector playerStartingTerrain = { Maps::Ground::GRASS, Maps::Ground::DIRT, Maps::Ground::SNOW, Maps::Ground::LAVA, Maps::Ground::WASTELAND }; const std::vector neutralTerrain = { Maps::Ground::GRASS, Maps::Ground::DIRT, Maps::Ground::SNOW, Maps::Ground::LAVA, Maps::Ground::WASTELAND, Maps::Ground::BEACH, Maps::Ground::SWAMP, Maps::Ground::DESERT }; - constexpr uint8_t directionCount{ 8 }; - const std::array directionOffsets{ { { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 }, { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 } } }; - constexpr std::array secondaryResources = { Resource::CRYSTAL, Resource::SULFUR, Resource::GEMS, Resource::MERCURY }; - - // TODO: replace list of object indicies with a struct for a better variety - const std::map> obstaclesPerGround = { - { Maps::Ground::DESERT, { 24, 25, 26, 27, 28, 29 } }, { Maps::Ground::SNOW, { 30, 31, 32, 33, 34, 35 } }, { Maps::Ground::SWAMP, { 18, 19, 20, 21, 22, 23 } }, - { Maps::Ground::WASTELAND, { 12, 13, 14, 15, 16, 17 } }, { Maps::Ground::BEACH, { 24, 25, 26, 27, 28, 29 } }, { Maps::Ground::LAVA, { 6, 7, 8, 9, 10, 11 } }, - { Maps::Ground::DIRT, { 18, 19, 20, 21, 22, 23 } }, { Maps::Ground::GRASS, { 0, 1, 2, 3, 4, 5 } }, { Maps::Ground::WATER, {} }, - }; - - enum class NodeType : uint8_t - { - OPEN, - BORDER, - ACTION, - OBSTACLE, - CONNECTOR, - PATH - }; - - struct Node final - { - int index{ -1 }; - uint32_t region{ 0 }; - NodeType type{ NodeType::OPEN }; - - Node() = default; - - explicit Node( const int index_ ) - : index( index_ ) - { - // Do nothing - } - }; - - class NodeCache final - { - public: - NodeCache( const int32_t width, const int32_t height ) - : _mapSize( width ) - , _outOfBounds( -1 ) - , _data( static_cast( width ) * height ) - { - assert( width > 0 && height > 0 ); - assert( width == height ); - - _outOfBounds.type = NodeType::BORDER; - - for ( size_t i = 0; i < _data.size(); ++i ) { - _data[i].index = static_cast( i ); - } - } - - Node & getNode( const fheroes2::Point position ) - { - if ( position.x < 0 || position.x >= _mapSize || position.y < 0 || position.y >= _mapSize ) { - // We shouldn't try to get a tile with an invalid index. - // TODO: here we must add an assertion and make sure that we never reach this place. - return _outOfBounds; - } - - return _data[position.y * _mapSize + position.x]; - } - - const Node & getNode( const fheroes2::Point position ) const - { - if ( position.x < 0 || position.x >= _mapSize || position.y < 0 || position.y >= _mapSize ) { - // We shouldn't try to get a tile with an invalid index. - // However, since we are accessing const value it is fine to get an invalid item. - return _outOfBounds; - } - - return _data[position.y * _mapSize + position.x]; - } - - Node & getNode( const int32_t index ) - { - if ( index < 0 || index >= _mapSize * _mapSize ) { - // We shouldn't try to get a tile with an invalid index. - assert( 0 ); - return _outOfBounds; - } - - return _data[index]; - } - - private: - const int32_t _mapSize{ 0 }; - Node _outOfBounds; - std::vector _data; - }; - - struct Region final - { - uint32_t id{ 0 }; - int32_t centerIndex{ -1 }; - std::set neighbours; - std::vector> nodes; - std::map connections; - size_t sizeLimit{ 0 }; - size_t lastProcessedNode{ 0 }; - int colorIndex{ neutralColorIndex }; - int groundType{ Maps::Ground::GRASS }; - - Region() = default; - - Region( const uint32_t regionIndex, Node & centerNode, const int playerColor, const int ground, const size_t expectedSize ) - : id( regionIndex ) - , centerIndex( centerNode.index ) - , sizeLimit( expectedSize ) - , colorIndex( playerColor ) - , groundType( ground ) - { - assert( expectedSize > 0 ); - - centerNode.region = regionIndex; - nodes.reserve( expectedSize ); - nodes.emplace_back( centerNode ); - } - }; - - struct ObjectPlacement final - { - fheroes2::Point offset; - Maps::ObjectGroup groupType{ Maps::ObjectGroup::NONE }; - int32_t objectIndex{ 0 }; - }; - - struct ObjectSet final - { - std::vector obstacles; - std::vector valuables; - std::vector monsters; - std::vector entranceCheck; - }; - - const std::vector prefabObjectSets - = { { ObjectSet{ - { ObjectPlacement{ { 0, -1 }, Maps::ObjectGroup::LANDSCAPE_TREES, 3 }, ObjectPlacement{ { 3, -1 }, Maps::ObjectGroup::LANDSCAPE_TREES, 2 }, - ObjectPlacement{ { 3, 1 }, Maps::ObjectGroup::LANDSCAPE_TREES, 3 } }, // obstacles - { ObjectPlacement{ { 1, -1 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, ObjectPlacement{ { 2, -1 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, - ObjectPlacement{ { 1, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, - ObjectPlacement{ { 2, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 } }, // valuables - { ObjectPlacement{ { 0, 0 }, Maps::ObjectGroup::MONSTERS, randomMonsterIndex } }, // monsters - { { -1, 0 }, { -1, 1 }, { 0, 1 } } // entranceCheck - }, - ObjectSet{ - { ObjectPlacement{ { -3, 0 }, Maps::ObjectGroup::LANDSCAPE_TREES, 3 }, ObjectPlacement{ { -3, 1 }, Maps::ObjectGroup::LANDSCAPE_TREES, 0 }, - ObjectPlacement{ { 0, 2 }, Maps::ObjectGroup::LANDSCAPE_TREES, 3 } }, // obstacles - { ObjectPlacement{ { -2, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, ObjectPlacement{ { -1, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, - ObjectPlacement{ { -2, 1 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, - ObjectPlacement{ { -1, 1 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 } }, // valuables - { ObjectPlacement{ { 0, 0 }, Maps::ObjectGroup::MONSTERS, randomMonsterIndex } }, // monsters - { { 0, -1 }, { 1, -1 }, { 1, 0 } } // entranceCheck - }, - ObjectSet{ - { ObjectPlacement{ { 3, -1 }, Maps::ObjectGroup::LANDSCAPE_TREES, 0 }, ObjectPlacement{ { 1, 2 }, Maps::ObjectGroup::LANDSCAPE_TREES, 5 }, - ObjectPlacement{ { 3, 2 }, Maps::ObjectGroup::LANDSCAPE_TREES, 3 } }, // obstacles - { ObjectPlacement{ { 1, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, ObjectPlacement{ { 2, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, - ObjectPlacement{ { 1, 1 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, - ObjectPlacement{ { 2, 1 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 } }, // valuables - { ObjectPlacement{ { 0, 0 }, Maps::ObjectGroup::MONSTERS, randomMonsterIndex } }, // monsters - { { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, -1 } } // entranceCheck - }, - ObjectSet{ - { ObjectPlacement{ { -3, -1 }, Maps::ObjectGroup::LANDSCAPE_TREES, 1 }, ObjectPlacement{ { -1, 2 }, Maps::ObjectGroup::LANDSCAPE_TREES, 4 }, - ObjectPlacement{ { -3, 2 }, Maps::ObjectGroup::LANDSCAPE_TREES, 2 } }, // obstacles - { ObjectPlacement{ { -1, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, ObjectPlacement{ { -2, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, - ObjectPlacement{ { -1, 1 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, - ObjectPlacement{ { -2, 1 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 } }, // valuables - { ObjectPlacement{ { 0, 0 }, Maps::ObjectGroup::MONSTERS, randomMonsterIndex } }, // monsters - { { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, -1 } } // entranceCheck - }, - ObjectSet{ - { ObjectPlacement{ { -3, 0 }, Maps::ObjectGroup::LANDSCAPE_TREES, 3 }, ObjectPlacement{ { -1, -1 }, Maps::ObjectGroup::LANDSCAPE_TREES, 5 }, - ObjectPlacement{ { -3, 1 }, Maps::ObjectGroup::LANDSCAPE_TREES, 2 } }, // obstacles - { ObjectPlacement{ { -2, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 }, - ObjectPlacement{ { -1, 0 }, Maps::ObjectGroup::ADVENTURE_TREASURES, 9 } }, // valuables - { ObjectPlacement{ { 0, 0 }, Maps::ObjectGroup::MONSTERS, randomMonsterIndex } }, // monsters - { { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, -1 }, { 0, 1 } } // entranceCheck - } } }; - - struct RegionalObjects final - { - uint8_t castleCount{ 0 }; - uint8_t mineCount{ 0 }; - uint8_t objectCount{ 0 }; - uint8_t powerUpsCount{ 0 }; - uint8_t treasureCount{ 0 }; - }; - - constexpr std::array( Maps::Random_Generator::ResourceDensity::ITEM_COUNT )> regionObjectSetup = { { + constexpr std::array( Maps::Random_Generator::ResourceDensity::ITEM_COUNT )> regionObjectSetup = { { { 1, 2, 1, 1, 2 }, // ResourceDensity::SCARCE { 1, 6, 2, 1, 3 }, // ResourceDensity::NORMAL { 1, 7, 2, 2, 5 } // ResourceDensity::ABUNDANT } }; - struct MapEconomy final - { - std::map minesCount{ { Resource::WOOD, 0 }, { Resource::ORE, 0 }, { Resource::CRYSTAL, 0 }, { Resource::SULFUR, 0 }, - { Resource::GEMS, 0 }, { Resource::MERCURY, 0 }, { Resource::GOLD, 0 } }; - - void increaseMineCount( const int resource ) - { - const auto it = minesCount.find( resource ); - assert( it != minesCount.end() ); - ++it->second; - } - - int pickNextMineResource() - { - const auto it = std::min_element( secondaryResources.begin(), secondaryResources.end(), - [this]( const auto & a, const auto & b ) { return minesCount.at( a ) < minesCount.at( b ); } ); - assert( it != secondaryResources.end() ); - - return *it; - } - }; + const std::vector randomMonsterSet{ { { 0, 0 }, Maps::ObjectGroup::MONSTERS, randomMonsterIndex } }; int32_t calculateRegionSizeLimit( const Maps::Random_Generator::Configuration & config, const int32_t width, const int32_t height ) { @@ -327,524 +106,96 @@ namespace return groundTiles / canFit; } - - struct PlacementTile final - { - int index{ 0 }; - float distance{ 0 }; - int ring{ 0 }; - }; - - std::vector> findOpenTilesSortedJittered( const Region & region, int mapWidth, Rand::PCG32 & randomGenerator ) - { - if ( region.centerIndex < 0 || region.nodes.empty() || mapWidth <= 0 ) { - return {}; - } - - std::vector> buckets; - - const int centerX = region.centerIndex % mapWidth; - const int centerY = region.centerIndex / mapWidth; - - for ( const Node & node : region.nodes ) { - if ( node.type != NodeType::OPEN || node.index < 0 ) { - continue; - } - - const int dx = ( node.index % mapWidth ) - centerX; - const int dy = ( node.index / mapWidth ) - centerY; - - const float distance = std::sqrt( static_cast( dx * dx + dy * dy ) ); - const int noise = static_cast( Rand::GetWithGen( 0, 2, randomGenerator ) ); - const int ring = static_cast( std::floor( distance ) ) + noise; - if ( static_cast( ring ) >= buckets.size() ) { - buckets.resize( ring + 1 ); - } - - buckets[ring].push_back( { node.index, distance, ring } ); - } - - for ( auto & bucket : buckets ) { - if ( bucket.empty() ) { - continue; - } - - Rand::ShuffleWithGen( bucket, randomGenerator ); - } - return buckets; - } - - constexpr int32_t treeTypeFromGroundType( const int groundType ) - { - switch ( groundType ) { - case Maps::Ground::WATER: - assert( 0 ); - return 0; - case Maps::Ground::GRASS: - return 0; - case Maps::Ground::SNOW: - return 30; - case Maps::Ground::SWAMP: - return 0; - case Maps::Ground::LAVA: - return 6; - case Maps::Ground::DESERT: - return 24; - case Maps::Ground::DIRT: - return 18; - case Maps::Ground::WASTELAND: - return 12; - case Maps::Ground::BEACH: - return 24; - default: - // Have you added a new ground? Add the logic above! - assert( 0 ); - break; - } - return 0; - } - - constexpr int32_t mountainTypeFromGroundType( const int groundType ) - { - switch ( groundType ) { - case Maps::Ground::WATER: - assert( 0 ); - return 0; - case Maps::Ground::GRASS: - return 6; - case Maps::Ground::SNOW: - return 12; - case Maps::Ground::SWAMP: - return 18; - case Maps::Ground::LAVA: - return 24; - case Maps::Ground::DESERT: - return 30; - case Maps::Ground::DIRT: - return 36; - case Maps::Ground::WASTELAND: - return 44; - case Maps::Ground::BEACH: - return 0; - default: - // Have you added a new ground? Add the logic above! - assert( 0 ); - break; - } - return 0; - } - - bool checkForRegionConnectors( NodeCache & data, std::vector & mapRegions, Region & region, Node & node ) - { - if ( node.type != NodeType::BORDER ) { - return false; - } - - const fheroes2::Point position = Maps::GetPoint( node.index ); - - int distinctNeighbours = 0; - uint32_t seen = node.region; - for ( uint8_t direction = 0; direction < directionCount; ++direction ) { - const Node & newTile = data.getNode( position + directionOffsets[direction] ); - - if ( newTile.index == -1 || newTile.region == region.id ) { - continue; - } - - if ( seen != newTile.region ) { - seen = newTile.region; - if ( ++distinctNeighbours > 1 ) { - return false; - } - } - } - - for ( uint8_t direction = 0; direction < 4; ++direction ) { - Node & adjacent = data.getNode( position + directionOffsets[direction] ); - - if ( adjacent.index == -1 || adjacent.region == region.id || mapRegions[adjacent.region].groundType == Maps::Ground::WATER ) { - continue; - } - - Node & twoAway = data.getNode( position + directionOffsets[direction] + directionOffsets[direction] ); - if ( twoAway.index == -1 || twoAway.type != NodeType::OPEN ) { - continue; - } - - if ( region.connections.find( adjacent.region ) == region.connections.end() ) { - DEBUG_LOG( DBG_DEVEL, DBG_TRACE, "Found a connection between " << region.id << " and " << adjacent.region << ", via " << node.index ) - region.connections.emplace( adjacent.region, node.index ); - mapRegions[adjacent.region].connections.emplace( region.id, node.index ); - node.type = NodeType::CONNECTOR; - adjacent.type = NodeType::PATH; - twoAway.type = NodeType::PATH; - - Node & stepBack = data.getNode( position - directionOffsets[direction] ); - if ( stepBack.index != -1 ) { - stepBack.type = NodeType::PATH; - } - - break; - } - } - - return node.type == NodeType::CONNECTOR; - } - - void checkAdjacentTiles( NodeCache & rawData, Region & region, Rand::PCG32 & randomGenerator ) - { - Node & previousNode = region.nodes[region.lastProcessedNode]; - const int nodeIndex = previousNode.index; - - for ( uint8_t direction = 0; direction < directionCount; ++direction ) { - if ( region.nodes.size() > region.sizeLimit ) { - previousNode.type = NodeType::BORDER; - break; - } - - // Check diagonal direction only 50% of the time to get more circular distribution. - // It gives randomness for uneven edges. - if ( direction > 3 && Rand::GetWithGen( 0, 1, randomGenerator ) ) { - continue; - } - - // TODO: use node index and pre-calculate offsets in advance. - // This will speed up the below calculations. - const fheroes2::Point newPosition = Maps::GetPoint( nodeIndex ) + directionOffsets[direction]; - if ( !Maps::isValidAbsPoint( newPosition.x, newPosition.y ) ) { - continue; - } - - Node & newTile = rawData.getNode( newPosition ); - if ( newTile.region == 0 && newTile.type == NodeType::OPEN ) { - newTile.region = region.id; - region.nodes.push_back( newTile ); - } - else if ( newTile.region != region.id ) { - previousNode.type = NodeType::BORDER; - region.neighbours.insert( newTile.region ); - } - } - } - - void regionExpansion( NodeCache & rawData, Region & region, Rand::PCG32 & randomGenerator ) - { - // Process only "open" nodes that exist at the start of the loop and ignore what's added. - const size_t nodesEnd = region.nodes.size(); - - while ( region.lastProcessedNode < nodesEnd ) { - checkAdjacentTiles( rawData, region, randomGenerator ); - ++region.lastProcessedNode; - } - } - - template - bool iterateOverObjectParts( const Maps::ObjectInfo & info, const F & lambda ) - { - for ( const auto & objectPart : info.groundLevelParts ) { - if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { - // Shadow and terrain layer parts are ignored. - continue; - } - - lambda( objectPart ); - } - for ( const auto & objectPart : info.topLevelParts ) { - lambda( objectPart ); - } - return true; - } - - bool canFitObject( const NodeCache & data, const Maps::ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool isAction, const bool skipBorders ) - { - bool invalid = false; - fheroes2::Rect objectRect; - - iterateOverObjectParts( info, [&data, &mainTilePos, &objectRect, skipBorders, &invalid]( const auto & partInfo ) { - const Node & node = data.getNode( mainTilePos + partInfo.tileOffset ); - - if ( node.index == -1 || node.region == 0 ) { - invalid = true; - return; - } - if ( node.type != NodeType::OPEN && ( skipBorders || node.type != NodeType::BORDER ) ) { - invalid = true; - return; - } - - objectRect.x = std::min( objectRect.x, partInfo.tileOffset.x ); - objectRect.width = std::max( objectRect.width, partInfo.tileOffset.x ); - } ); - - if ( invalid ) { - return false; - } - - if ( isAction ) { - for ( int x = objectRect.x - 1; x <= objectRect.width + 1; ++x ) { - const Node & pathNode = data.getNode( mainTilePos + fheroes2::Point{ x, 1 } ); - if ( pathNode.index == -1 || pathNode.type == NodeType::OBSTACLE ) { - return false; - } - if ( pathNode.type == NodeType::BORDER ) { - return false; - } - } - } - - return true; - } - - bool canFitObjectSet( const NodeCache & data, const ObjectSet & set, const fheroes2::Point & mainTilePos ) - { - for ( const fheroes2::Point offset : set.entranceCheck ) { - const Node & node = data.getNode( mainTilePos + offset ); - if ( node.index == -1 || node.type != NodeType::OPEN ) { - return false; - } - } - - for ( const auto * placements : { &set.obstacles, &set.valuables, &set.monsters } ) { - for ( const auto & placement : *placements ) { - const auto & info = Maps::getObjectInfo( placement.groupType, placement.objectIndex ); - const bool isAction = MP2::isInGameActionObject( info.objectType ); - const fheroes2::Point position = mainTilePos + placement.offset; - bool invalid = false; - - iterateOverObjectParts( info, [&data, &position, isAction, &invalid]( const auto & partInfo ) { - const Node & node = data.getNode( position + partInfo.tileOffset ); - - if ( node.index == -1 || node.region == 0 ) { - invalid = true; - return; - } - if ( node.type != NodeType::OPEN && ( isAction || node.type != NodeType::BORDER ) ) { - invalid = true; - return; - } - } ); - - if ( invalid ) { - return false; - } - } - } - - return true; - } - - void markObjectPlacement( NodeCache & data, const Maps::ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool isAction ) - { - fheroes2::Rect objectRect; - - iterateOverObjectParts( info, [&data, &mainTilePos, &objectRect]( const auto & partInfo ) { - Node & node = data.getNode( mainTilePos + partInfo.tileOffset ); - objectRect.x = std::min( objectRect.x, partInfo.tileOffset.x ); - objectRect.width = std::max( objectRect.width, partInfo.tileOffset.x ); - - node.type = NodeType::OBSTACLE; - } ); - - if ( !isAction ) { - return; - } - - for ( int x = objectRect.x - 1; x <= objectRect.width + 1; ++x ) { - data.getNode( mainTilePos + fheroes2::Point{ x, objectRect.height + 1 } ).type = NodeType::PATH; - } - - // Mark extra nodes as path to avoid objects clumping together - data.getNode( mainTilePos + fheroes2::Point{ objectRect.x - 1, 0 } ).type = NodeType::PATH; - data.getNode( mainTilePos + fheroes2::Point{ objectRect.width + 1, 0 } ).type = NodeType::PATH; - - data.getNode( mainTilePos ).type = NodeType::ACTION; - } - - bool putObjectOnMap( Maps::Map_Format::MapFormat & mapFormat, Maps::Tile & tile, const Maps::ObjectGroup groupType, const int32_t objectIndex ) - { - const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex ); - if ( objectInfo.empty() ) { - assert( 0 ); - return false; - } - - const MP2::MapObjectType mp2Type = objectInfo.objectType; - // Maps::setObjectOnTile isn't idempotent, check if object was already placed - if ( MP2::isInGameActionObject( mp2Type ) && tile.getMainObjectType() == mp2Type ) { - return false; - } - - // Do not update passabilities after every object placement. - if ( !Maps::setObjectOnTile( tile, objectInfo, false ) ) { - return false; - } - - Maps::addObjectToMap( mapFormat, tile.GetIndex(), groupType, static_cast( objectIndex ) ); - - return true; - } - - bool actionObjectPlacer( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, Maps::Tile & tile, const Maps::ObjectGroup groupType, const int32_t type ) - { - const fheroes2::Point tilePos = tile.GetCenter(); - const auto & objectInfo = Maps::getObjectInfo( groupType, type ); - if ( canFitObject( data, objectInfo, tilePos, true, true ) && putObjectOnMap( mapFormat, tile, groupType, type ) ) { - markObjectPlacement( data, objectInfo, tilePos, true ); - return true; - } - - return false; - } - - fheroes2::Point adjustRegionToFitCastle( const Maps::Map_Format::MapFormat & mapFormat, Region & region ) - { - const fheroes2::Point startingLocation = Maps::GetPoint( region.centerIndex ); - const int32_t castleX = std::min( std::max( startingLocation.x, 4 ), mapFormat.width - 4 ); - const int32_t castleY = std::min( std::max( startingLocation.y, 4 ), mapFormat.width - 4 ); - region.centerIndex = Maps::GetIndexFromAbsPoint( castleX, castleY + 2 ); - return { castleX, castleY }; - } - - bool placeCastle( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, const Region & region, const fheroes2::Point tilePos, const bool isCastle ) - { - auto & tile = world.getTile( tilePos.x, tilePos.y ); - if ( tile.isWater() ) { - return false; - } - - const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); - const int32_t castleObjectId = isCastle ? randomCastleIndex : randomTownIndex; - - const auto & basementInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); - const auto & castleInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, castleObjectId ); - - if ( canFitObject( data, basementInfo, tilePos, false, true ) && canFitObject( data, castleInfo, tilePos, true, true ) ) { - if ( !putObjectOnMap( mapFormat, tile, Maps::ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ) ) { - return false; - } - - // Since the whole object consists of multiple "objects" we have to put the same ID for all of them. - // Every time an object is being placed on a map the counter is going to be increased by 1. - // Therefore, we set the counter by 1 less for each object to match object UID for all of them. - assert( Maps::getLastObjectUID() > 0 ); - const uint32_t objectId = Maps::getLastObjectUID() - 1; - - Maps::setLastObjectUID( objectId ); - - if ( !putObjectOnMap( mapFormat, tile, Maps::ObjectGroup::KINGDOM_TOWNS, castleObjectId ) ) { - return false; - } - - const int32_t bottomIndex = Maps::GetDirectionIndex( tile.GetIndex(), Direction::BOTTOM ); - - if ( Maps::isValidAbsIndex( bottomIndex ) && Maps::doesContainRoads( mapFormat.tiles[bottomIndex] ) ) { - // Update road if there is one in front of the town/castle entrance. - Maps::updateRoadSpriteOnTile( mapFormat, bottomIndex, false ); - } - - // By default use random (default) army for the neutral race town/castle. - const PlayerColor color = Color::IndexToColor( region.colorIndex ); - if ( color == PlayerColor::NONE ) { - Maps::setDefaultCastleDefenderArmy( mapFormat.castleMetadata[Maps::getLastObjectUID()] ); - } - - // Add flags. - assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); - Maps::setLastObjectUID( objectId ); - - if ( !putObjectOnMap( mapFormat, world.getTile( tile.GetIndex() - 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, Color::GetIndex( color ) * 2 ) ) { - return false; - } - - Maps::setLastObjectUID( objectId ); - - if ( !putObjectOnMap( mapFormat, world.getTile( tile.GetIndex() + 1 ), Maps::ObjectGroup::LANDSCAPE_FLAGS, Color::GetIndex( color ) * 2 + 1 ) ) { - return false; - } - - const Maps::ObjectInfo & townObjectInfo = Maps::getObjectInfo( Maps::ObjectGroup::KINGDOM_TOWNS, castleObjectId ); - const uint8_t race = Race::IndexToRace( static_cast( townObjectInfo.metadata[0] ) ); - - world.addCastle( tile.GetIndex(), race, color ); - - markObjectPlacement( data, basementInfo, tilePos, false ); - markObjectPlacement( data, castleInfo, tilePos, true ); - - // Force roads coming from the castle - const int32_t nextIndex = Maps::GetDirectionIndex( bottomIndex, Direction::BOTTOM ); - if ( Maps::isValidAbsIndex( nextIndex ) ) { - Maps::updateRoadOnTile( mapFormat, bottomIndex, true ); - Maps::updateRoadOnTile( mapFormat, nextIndex, true ); - } - - return true; - } - - return false; - } - - bool placeMine( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & node, const int resource ) - { - Maps::Tile & mineTile = world.getTile( node.index ); - const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); - return actionObjectPlacer( mapFormat, data, mineTile, Maps::ObjectGroup::ADVENTURE_MINES, mineType ); - } - - int32_t localizeObjectToTerrain( const Maps::ObjectGroup groupType, const int32_t objectIndex, const int groundType ) - { - if ( groupType == Maps::ObjectGroup::LANDSCAPE_TREES ) { - assert( objectIndex < 6 ); - return treeTypeFromGroundType( groundType ) + objectIndex; - } - - if ( groupType == Maps::ObjectGroup::LANDSCAPE_MOUNTAINS ) { - assert( objectIndex < 8 ); - return mountainTypeFromGroundType( groundType ) + objectIndex; - } - return objectIndex; - } - - bool placeObstacle( Maps::Map_Format::MapFormat & mapFormat, const NodeCache & data, const Node & node, Rand::PCG32 & randomGenerator ) - { - Maps::Tile & tile = world.getTile( node.index ); - const auto it = obstaclesPerGround.find( tile.GetGround() ); - if ( it == obstaclesPerGround.end() || it->second.empty() ) { - return false; - } - - std::vector obstacleList = it->second; - Rand::ShuffleWithGen( obstacleList, randomGenerator ); - - const fheroes2::Point tilePos = tile.GetCenter(); - for ( const auto & obstacleId : obstacleList ) { - const auto & objectInfo = Maps::getObjectInfo( Maps::ObjectGroup::LANDSCAPE_TREES, obstacleId ); - if ( canFitObject( data, objectInfo, tilePos, false, false ) && putObjectOnMap( mapFormat, tile, Maps::ObjectGroup::LANDSCAPE_TREES, obstacleId ) ) { - return true; - } - } - return false; - } - - bool placeSimpleObject( Maps::Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & centerNode, const ObjectPlacement & placement ) - { - const fheroes2::Point position = Maps::GetPoint( centerNode.index ) + placement.offset; - if ( !Maps::isValidAbsPoint( position.x, position.y ) ) { - return false; - } - - Maps::Tile & tile = world.getTile( position.x, position.y ); - const int32_t objectIndex = localizeObjectToTerrain( placement.groupType, placement.objectIndex, tile.GetGround() ); - const auto & objectInfo = Maps::getObjectInfo( placement.groupType, objectIndex ); - if ( putObjectOnMap( mapFormat, tile, placement.groupType, objectIndex ) ) { - markObjectPlacement( data, objectInfo, position, false ); - return true; - } - return false; - } } namespace Maps::Random_Generator { + const std::vector prefabObjectSets{ ObjectSet{ // Obstacles. + { { { 0, -1 }, ObjectGroup::LANDSCAPE_TREES, 3 }, + { { 3, -1 }, ObjectGroup::LANDSCAPE_TREES, 2 }, + { { 3, 1 }, ObjectGroup::LANDSCAPE_TREES, 3 } }, + // Valuables. + { { { 1, -1 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { 2, -1 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { 1, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { 2, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 } }, + // Monsters. + randomMonsterSet, + // Entrance check. + { { -1, 0 }, { -1, 1 }, { 0, 1 } } }, + ObjectSet{ // Obstacles. + { { { -3, 0 }, ObjectGroup::LANDSCAPE_TREES, 3 }, + { { -3, 1 }, ObjectGroup::LANDSCAPE_TREES, 0 }, + { { 0, 2 }, ObjectGroup::LANDSCAPE_TREES, 3 } }, + // Valuables. + { { { -2, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { -1, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { -2, 1 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { -1, 1 }, ObjectGroup::ADVENTURE_TREASURES, 9 } }, + // Monsters. + randomMonsterSet, + // Entrance check. + { { 0, -1 }, { 1, -1 }, { 1, 0 } } }, + ObjectSet{ // Obstacles. + { { { 3, -1 }, ObjectGroup::LANDSCAPE_TREES, 0 }, + { { 1, 2 }, ObjectGroup::LANDSCAPE_TREES, 5 }, + { { 3, 2 }, ObjectGroup::LANDSCAPE_TREES, 3 } }, + // Valuables. + { { { 1, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { 2, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { 1, 1 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { 2, 1 }, ObjectGroup::ADVENTURE_TREASURES, 9 } }, + // Monsters. + randomMonsterSet, + // Entrance check. + { { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, -1 } } }, + ObjectSet{ // Obstacles. + { { { -3, -1 }, ObjectGroup::LANDSCAPE_TREES, 1 }, + { { -1, 2 }, ObjectGroup::LANDSCAPE_TREES, 4 }, + { { -3, 2 }, ObjectGroup::LANDSCAPE_TREES, 2 } }, + // Valuables. + { { { -1, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { -2, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { -1, 1 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { -2, 1 }, ObjectGroup::ADVENTURE_TREASURES, 9 } }, + // Monsters. + randomMonsterSet, + // Entrance check. + { { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, -1 } } }, + ObjectSet{ // Obstacles. + { { { -3, 0 }, ObjectGroup::LANDSCAPE_TREES, 3 }, + { { -1, -1 }, ObjectGroup::LANDSCAPE_TREES, 5 }, + { { -3, 1 }, ObjectGroup::LANDSCAPE_TREES, 2 } }, + // Valuables. + { { { -2, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, { { -1, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 } }, + // Monsters. + randomMonsterSet, + // Entrance check. + { { 1, -1 }, { 1, 0 }, { 1, 1 }, { 0, -1 }, { 0, 1 } } } }; + + const std::vector powerupObjectSets{ + ObjectSet{ // Obstacles. + { { { 0, -1 }, ObjectGroup::LANDSCAPE_TREES, 3 }, { { 3, -1 }, ObjectGroup::LANDSCAPE_TREES, 2 }, { { 3, 1 }, ObjectGroup::LANDSCAPE_TREES, 3 } }, + // Valuables. + { { { 1, -1 }, ObjectGroup::ADVENTURE_POWER_UPS, 10 }, + { { 1, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { 2, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 } }, + // Monsters. + randomMonsterSet, + // Entrance check. + { { -1, 0 }, { -1, 1 }, { 0, 1 } } }, + ObjectSet{ // Obstacles. + { { { 0, -1 }, ObjectGroup::LANDSCAPE_TREES, 3 }, { { 3, -1 }, ObjectGroup::LANDSCAPE_TREES, 2 }, { { 3, 1 }, ObjectGroup::LANDSCAPE_TREES, 3 } }, + // Valuables. + { { { 1, -1 }, ObjectGroup::ADVENTURE_POWER_UPS, 12 }, + { { 1, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 }, + { { 2, 0 }, ObjectGroup::ADVENTURE_TREASURES, 9 } }, + // Monsters. + randomMonsterSet, + // Entrance check. + { { -1, 0 }, { -1, 1 }, { 0, 1 } } }, + }; + bool generateMap( Map_Format::MapFormat & mapFormat, const Configuration & config, const int32_t width, const int32_t height ) { // Make sure that we are generating a valid map. @@ -933,9 +284,7 @@ namespace Maps::Random_Generator stillRoomToExpand = false; // Skip the first region which is the border region. for ( size_t regionID = 1; regionID < mapRegions.size(); ++regionID ) { - Region & region = mapRegions[regionID]; - regionExpansion( data, region, randomGenerator ); - if ( region.lastProcessedNode != region.nodes.size() ) { + if ( mapRegions[regionID].regionExpansion( data, randomGenerator ) ) { stillRoomToExpand = true; } } @@ -982,7 +331,7 @@ namespace Maps::Random_Generator } if ( region.colorIndex != neutralColorIndex ) { - const fheroes2::Point castlePos = adjustRegionToFitCastle( mapFormat, region ); + const fheroes2::Point castlePos = region.adjustRegionToFitCastle( mapFormat ); if ( !placeCastle( mapFormat, data, region, castlePos, true ) ) { // Return early if we can't place a starting player castle. DEBUG_LOG( DBG_DEVEL, DBG_WARN, "Not able to place a starting player castle on tile " << castlePos.x << ", " << castlePos.y ) @@ -992,20 +341,20 @@ namespace Maps::Random_Generator } else if ( static_cast( region.nodes.size() ) > regionSizeLimit ) { // Place non-mandatory castles in bigger neutral regions. - const bool useNeutralCastles = config.resourceDensity == Maps::Random_Generator::ResourceDensity::ABUNDANT; - const fheroes2::Point castlePos = adjustRegionToFitCastle( mapFormat, region ); + const bool useNeutralCastles = ( config.resourceDensity == ResourceDensity::ABUNDANT ); + const fheroes2::Point castlePos = region.adjustRegionToFitCastle( mapFormat ); placeCastle( mapFormat, data, region, castlePos, useNeutralCastles ); } - const std::vector> tileRings = findOpenTilesSortedJittered( region, width, randomGenerator ); + const std::vector> tileRings = findOpenTilesSortedJittered( region, width, randomGenerator ); - const auto tryToPlaceMine = [&]( const std::vector & ring, const int resource ) { - for ( const PlacementTile & tile : ring ) { - const auto & node = data.getNode( tile.index ); + const auto tryToPlaceMine = [&]( const std::vector & ring, const int resource ) { + for ( const int32_t tileIndex : ring ) { + const auto & node = data.getNode( tileIndex ); if ( placeMine( mapFormat, data, node, resource ) ) { mapEconomy.increaseMineCount( resource ); - actionLocations.insert( tile.index ); - return tile.index; + actionLocations.insert( tileIndex ); + return tileIndex; } } return -1; @@ -1028,7 +377,7 @@ namespace Maps::Random_Generator for ( size_t ringIndex = tileRings.size() - 3; ringIndex > 0; --ringIndex ) { const int mineIndex = tryToPlaceMine( tileRings[ringIndex], resource ); if ( mineIndex != -1 ) { - putObjectOnMap( mapFormat, world.getTile( mineIndex + mapFormat.width ), Maps::ObjectGroup::MONSTERS, randomMonsterIndex ); + putObjectOnMap( mapFormat, world.getTile( mineIndex + mapFormat.width ), ObjectGroup::MONSTERS, randomMonsterIndex ); break; } } @@ -1041,7 +390,7 @@ namespace Maps::Random_Generator continue; } for ( Node & node : region.nodes ) { - checkForRegionConnectors( data, mapRegions, region, node ); + region.checkNodeForConnections( data, mapRegions, node ); } } @@ -1053,7 +402,7 @@ namespace Maps::Random_Generator } } - // Step 7. Set up pathfinder to generate road based paths. + // Step 7. Set up pathfinder and generate road network. AIWorldPathfinder pathfinder; pathfinder.reset(); const PlayerColor testPlayer = PlayerColor::BLUE; @@ -1077,45 +426,24 @@ namespace Maps::Random_Generator } } - // TODO: place objects while avoiding the borders. - - // TODO: place treasure objects. + // Step 8: Place powerups and treasure clusters while avoiding the paths. + const auto & regionConfiguration = regionObjectSetup[static_cast( config.resourceDensity )]; for ( const Region & region : mapRegions ) { - int treasureObjectsPlaced = 0; + placeObjectSet( mapFormat, data, region, powerupObjectSets, regionConfiguration.powerUpsCount, randomGenerator ); + placeObjectSet( mapFormat, data, region, prefabObjectSets, regionConfiguration.treasureCount, randomGenerator ); + } - for ( int attempt = 0; attempt < maxPlacementAttempts; ++attempt ) { - if ( treasureObjectsPlaced == regionObjectSetup[static_cast( config.resourceDensity )].treasureCount ) { - break; - } + // TODO: Step 9: Detect and fill empty areas with decorative/flavour objects. - const Node & node = Rand::GetWithGen( region.nodes, randomGenerator ); - - std::vector shuffledObjectSets = prefabObjectSets; - Rand::ShuffleWithGen( shuffledObjectSets, randomGenerator ); - for ( const auto & prefab : shuffledObjectSets ) { - if ( !canFitObjectSet( data, prefab, Maps::GetPoint( node.index ) ) ) { - continue; - } - - for ( const auto & obstacle : prefab.obstacles ) { - placeSimpleObject( mapFormat, data, node, obstacle ); - } - for ( const auto & treasure : prefab.valuables ) { - placeSimpleObject( mapFormat, data, node, treasure ); - } - for ( const auto & monster : prefab.monsters ) { - const fheroes2::Point position = Maps::GetPoint( node.index ) + monster.offset; - putObjectOnMap( mapFormat, world.getTile( position.x, position.y ), Maps::ObjectGroup::MONSTERS, monster.objectIndex ); - } - ++treasureObjectsPlaced; - break; - } + // Step 10: Place missing monsters. + for ( const Region & region : mapRegions ) { + for ( const auto & [regionId, tileIndex] : region.connections ) { + putObjectOnMap( mapFormat, world.getTile( tileIndex ), ObjectGroup::MONSTERS, randomMonsterIndex ); } } - // TODO: fill big empty islands with obstacles - - for ( const int start : startingLocations ) { + // Step 11: Validate that map is playable. + for ( const int32_t start : startingLocations ) { pathfinder.reEvaluateIfNeeded( start, testPlayer, 999999.9, Skill::Level::EXPERT ); for ( const int action : actionLocations ) { if ( pathfinder.getDistance( action ) == 0 ) { @@ -1125,13 +453,6 @@ namespace Maps::Random_Generator } } - // Step 11: place monsters. - for ( const Region & region : mapRegions ) { - for ( const auto & [regionId, tileIndex] : region.connections ) { - putObjectOnMap( mapFormat, world.getTile( tileIndex ), Maps::ObjectGroup::MONSTERS, randomMonsterIndex ); - } - } - // Visual debug for ( const Region & region : mapRegions ) { for ( const Node & node : region.nodes ) { diff --git a/src/fheroes2/maps/map_random_generator_helper.cpp b/src/fheroes2/maps/map_random_generator_helper.cpp new file mode 100644 index 000000000..da6186d2d --- /dev/null +++ b/src/fheroes2/maps/map_random_generator_helper.cpp @@ -0,0 +1,569 @@ +/*************************************************************************** + * fheroes2: https://github.com/ihhub/fheroes2 * + * Copyright (C) 2025 * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include "map_random_generator_helper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "color.h" +#include "direction.h" +#include "ground.h" +#include "map_format_helper.h" +#include "map_format_info.h" +#include "map_object_info.h" +#include "map_random_generator_info.h" +#include "maps.h" +#include "maps_tiles.h" +#include "maps_tiles_helper.h" +#include "mp2.h" +#include "race.h" +#include "rand.h" +#include "ui_map_object.h" +#include "world.h" +#include "world_object_uid.h" + +namespace +{ + constexpr int randomCastleIndex{ 12 }; + constexpr int randomTownIndex{ 13 }; + constexpr int maxPlacementAttempts{ 30 }; + constexpr std::array, 4> randomMonsterThresholds = { { { 67, 0 }, { 68, 1250 }, { 69, 3250 }, { 70, 8250 } } }; + + const std::map> obstaclesPerGround = { + { Maps::Ground::DESERT, { 24, 25, 26, 27, 28, 29 } }, { Maps::Ground::SNOW, { 30, 31, 32, 33, 34, 35 } }, { Maps::Ground::SWAMP, { 18, 19, 20, 21, 22, 23 } }, + { Maps::Ground::WASTELAND, { 12, 13, 14, 15, 16, 17 } }, { Maps::Ground::BEACH, { 24, 25, 26, 27, 28, 29 } }, { Maps::Ground::LAVA, { 6, 7, 8, 9, 10, 11 } }, + { Maps::Ground::DIRT, { 18, 19, 20, 21, 22, 23 } }, { Maps::Ground::GRASS, { 0, 1, 2, 3, 4, 5 } }, { Maps::Ground::WATER, {} }, + }; + + // Evaluating all treasure and power-ups using gold equivalent + // Benchmark is 1500 gold = 1000 experience; 1 primary attribute point worth 2000 gold + // Artifacts: treasure is 1 attribute, minor is 2, major is 4 with a bonus for rarity + const std::map objectGoldValue = { + { MP2::OBJ_AIR_ALTAR, 4000 }, + { MP2::OBJ_ARCHER_HOUSE, 2000 }, + { MP2::OBJ_CAMPFIRE, 1000 }, + { MP2::OBJ_CAVE, 1500 }, + { MP2::OBJ_DESERT_TENT, 3000 }, + { MP2::OBJ_DWARF_COTTAGE, 2000 }, + { MP2::OBJ_EARTH_ALTAR, 4000 }, + { MP2::OBJ_EXCAVATION, 1500 }, + { MP2::OBJ_FIRE_ALTAR, 4000 }, + { MP2::OBJ_FORT, 2000 }, + { MP2::OBJ_GAZEBO, 1500 }, + { MP2::OBJ_GENIE_LAMP, 6000 }, + { MP2::OBJ_GOBLIN_HUT, 1500 }, + { MP2::OBJ_HALFLING_HOLE, 1500 }, + { MP2::OBJ_MERCENARY_CAMP, 2000 }, + { MP2::OBJ_PEASANT_HUT, 250 }, + { MP2::OBJ_RANDOM_ARTIFACT_MAJOR, 10000 }, + { MP2::OBJ_RANDOM_ARTIFACT_MINOR, 4000 }, + { MP2::OBJ_RANDOM_ARTIFACT_TREASURE, 2000 }, + { MP2::OBJ_RANDOM_RESOURCE, 750 }, + { MP2::OBJ_RESOURCE, 750 }, + { MP2::OBJ_RUINS, 4000 }, + { MP2::OBJ_SHRINE_FIRST_CIRCLE, 1000 }, + { MP2::OBJ_SHRINE_SECOND_CIRCLE, 2000 }, + { MP2::OBJ_SHRINE_THIRD_CIRCLE, 3000 }, + { MP2::OBJ_STANDING_STONES, 2000 }, + { MP2::OBJ_TREASURE_CHEST, 1500 }, + { MP2::OBJ_TREE_CITY, 1500 }, + { MP2::OBJ_TREE_HOUSE, 1500 }, + { MP2::OBJ_TREE_OF_KNOWLEDGE, 3000 }, + { MP2::OBJ_WAGON_CAMP, 2000 }, + { MP2::OBJ_WATCH_TOWER, 2000 }, + { MP2::OBJ_WATER_ALTAR, 4000 }, + { MP2::OBJ_WITCH_DOCTORS_HUT, 2000 }, + { MP2::OBJ_XANADU, 8000 }, + }; + + constexpr int32_t treeTypeFromGroundType( const int groundType ) + { + switch ( groundType ) { + case Maps::Ground::WATER: + assert( 0 ); + return 0; + case Maps::Ground::GRASS: + return 0; + case Maps::Ground::SNOW: + return 30; + case Maps::Ground::SWAMP: + return 0; + case Maps::Ground::LAVA: + return 6; + case Maps::Ground::DESERT: + return 24; + case Maps::Ground::DIRT: + return 18; + case Maps::Ground::WASTELAND: + return 12; + case Maps::Ground::BEACH: + return 24; + default: + // Have you added a new ground? Add the logic above! + assert( 0 ); + break; + } + return 0; + } + + constexpr int32_t mountainTypeFromGroundType( const int groundType ) + { + switch ( groundType ) { + case Maps::Ground::WATER: + assert( 0 ); + return 0; + case Maps::Ground::GRASS: + return 6; + case Maps::Ground::SNOW: + return 12; + case Maps::Ground::SWAMP: + return 18; + case Maps::Ground::LAVA: + return 24; + case Maps::Ground::DESERT: + return 30; + case Maps::Ground::DIRT: + return 36; + case Maps::Ground::WASTELAND: + return 44; + case Maps::Ground::BEACH: + return 0; + default: + // Have you added a new ground? Add the logic above! + assert( 0 ); + break; + } + return 0; + } + + bool iterateOverObjectParts( const Maps::ObjectInfo & info, const std::function & lambda ) + { + for ( const auto & objectPart : info.groundLevelParts ) { + if ( objectPart.layerType == Maps::SHADOW_LAYER || objectPart.layerType == Maps::TERRAIN_LAYER ) { + // Shadow and terrain layer parts are ignored. + continue; + } + + lambda( objectPart ); + } + for ( const auto & objectPart : info.topLevelParts ) { + lambda( objectPart ); + } + return true; + } +} + +namespace Maps::Random_Generator +{ + int32_t getObjectGoldValue( const Maps::ObjectGroup group, const int32_t objectIndex ) + { + const auto & objectInfo = Maps::getObjectInfo( group, objectIndex ); + + const auto it = objectGoldValue.find( objectInfo.objectType ); + if ( it == objectGoldValue.end() ) { + // No valuation for the object? Add it! + assert( 0 ); + return 0; + } + + return it->second; + } + + int32_t pickMonsterByValue( const int32_t protectedObjectValue ) + { + assert( protectedObjectValue >= 0 ); + + int32_t bestMonster = 0; + for ( const auto & [monsterIndex, threshold] : randomMonsterThresholds ) { + if ( protectedObjectValue >= threshold ) { + bestMonster = monsterIndex; + } + } + return bestMonster; + } + + int32_t selectTerrainVariantForObject( const ObjectGroup groupType, const int32_t objectIndex, const int groundType ) + { + if ( groupType == ObjectGroup::LANDSCAPE_TREES ) { + assert( objectIndex < 6 ); + return treeTypeFromGroundType( groundType ) + objectIndex; + } + + if ( groupType == ObjectGroup::LANDSCAPE_MOUNTAINS ) { + assert( objectIndex < 8 ); + return mountainTypeFromGroundType( groundType ) + objectIndex; + } + return objectIndex; + } + + std::vector> findOpenTilesSortedJittered( const Region & region, int32_t mapWidth, Rand::PCG32 & randomGenerator ) + { + if ( region.centerIndex < 0 || region.nodes.empty() || mapWidth <= 0 ) { + return {}; + } + + std::vector> buckets; + + const int centerX = region.centerIndex % mapWidth; + const int centerY = region.centerIndex / mapWidth; + + for ( const Node & node : region.nodes ) { + if ( node.type != NodeType::OPEN || node.index < 0 ) { + continue; + } + + const int dx = ( node.index % mapWidth ) - centerX; + const int dy = ( node.index / mapWidth ) - centerY; + + const double distance = std::sqrt( static_cast( dx * dx + dy * dy ) ); + const uint32_t noise = static_cast( Rand::GetWithGen( 0, 2, randomGenerator ) ); + const size_t ring = static_cast( std::floor( distance ) + noise ); + if ( ring >= buckets.size() ) { + buckets.resize( ring + 1 ); + } + + buckets[ring].push_back( node.index ); + } + + for ( auto & bucket : buckets ) { + if ( bucket.empty() ) { + continue; + } + + Rand::ShuffleWithGen( bucket, randomGenerator ); + } + return buckets; + } + + bool canFitObject( const NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool skipBorders ) + { + bool invalid = false; + fheroes2::Rect objectRect; + + auto updateObjectArea = [&data, &mainTilePos, &objectRect, skipBorders, &invalid]( const auto & partInfo ) { + const Node & node = data.getNode( mainTilePos + partInfo.tileOffset ); + + if ( node.index == -1 || node.region == 0 ) { + invalid = true; + return; + } + if ( node.type != NodeType::OPEN && ( skipBorders || node.type != NodeType::BORDER ) ) { + invalid = true; + return; + } + + objectRect.x = std::min( objectRect.x, partInfo.tileOffset.x ); + objectRect.width = std::max( objectRect.width, partInfo.tileOffset.x ); + }; + + iterateOverObjectParts( info, updateObjectArea ); + + if ( invalid ) { + return false; + } + + if ( MP2::isOffGameActionObject( info.objectType ) ) { + for ( int x = objectRect.x - 1; x <= objectRect.width + 1; ++x ) { + const Node & pathNode = data.getNode( mainTilePos + fheroes2::Point{ x, 1 } ); + if ( pathNode.index == -1 || pathNode.type == NodeType::OBSTACLE ) { + return false; + } + if ( pathNode.type == NodeType::BORDER ) { + return false; + } + } + } + + return true; + } + + bool canFitObjectSet( const NodeCache & data, const ObjectSet & set, const fheroes2::Point & mainTilePos ) + { + for ( const fheroes2::Point offset : set.entranceCheck ) { + const Node & node = data.getNode( mainTilePos + offset ); + if ( node.index == -1 || node.type != NodeType::OPEN ) { + return false; + } + } + + for ( const auto * placements : { &set.obstacles, &set.valuables, &set.monsters } ) { + for ( const auto & placement : *placements ) { + const auto & info = Maps::getObjectInfo( placement.groupType, placement.objectIndex ); + const bool isAction = MP2::isInGameActionObject( info.objectType ); + const fheroes2::Point position = mainTilePos + placement.offset; + bool invalid = false; + + auto isValidObjectPart = [&data, &position, isAction, &invalid]( const auto & partInfo ) { + const Node & node = data.getNode( position + partInfo.tileOffset ); + + if ( node.index == -1 || node.region == 0 ) { + invalid = true; + return; + } + if ( node.type != NodeType::OPEN && ( isAction || node.type != NodeType::BORDER ) ) { + invalid = true; + return; + } + }; + + iterateOverObjectParts( info, isValidObjectPart ); + + if ( invalid ) { + return false; + } + } + } + + return true; + } + + void markObjectPlacement( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos ) + { + fheroes2::Rect objectRect; + + auto updateObjectArea = [&data, &mainTilePos, &objectRect]( const auto & partInfo ) { + Node & node = data.getNode( mainTilePos + partInfo.tileOffset ); + objectRect.x = std::min( objectRect.x, partInfo.tileOffset.x ); + objectRect.width = std::max( objectRect.width, partInfo.tileOffset.x ); + + node.type = NodeType::OBSTACLE; + }; + + iterateOverObjectParts( info, updateObjectArea ); + + if ( !MP2::isOffGameActionObject( info.objectType ) ) { + return; + } + + for ( int x = objectRect.x - 1; x <= objectRect.width + 1; ++x ) { + data.getNode( mainTilePos + fheroes2::Point{ x, objectRect.height + 1 } ).type = NodeType::PATH; + } + + // Mark extra nodes as path to avoid objects clumping together + data.getNode( mainTilePos + fheroes2::Point{ objectRect.x - 1, 0 } ).type = NodeType::PATH; + data.getNode( mainTilePos + fheroes2::Point{ objectRect.width + 1, 0 } ).type = NodeType::PATH; + + data.getNode( mainTilePos ).type = NodeType::ACTION; + } + + bool putObjectOnMap( Map_Format::MapFormat & mapFormat, Tile & tile, const ObjectGroup groupType, const int32_t objectIndex ) + { + const auto & objectInfo = Maps::getObjectInfo( groupType, objectIndex ); + if ( objectInfo.empty() ) { + assert( 0 ); + return false; + } + + // Maps::setObjectOnTile isn't idempotent, check if object was already placed + if ( MP2::isInGameActionObject( objectInfo.objectType ) && tile.getMainObjectType() == objectInfo.objectType ) { + return false; + } + + // Do not update passabilities after every object placement. + if ( !Maps::setObjectOnTile( tile, objectInfo, false ) ) { + return false; + } + + Maps::addObjectToMap( mapFormat, tile.GetIndex(), groupType, static_cast( objectIndex ) ); + + return true; + } + + bool placeActionObject( Map_Format::MapFormat & mapFormat, NodeCache & data, Tile & tile, const ObjectGroup groupType, const int32_t type ) + { + const fheroes2::Point tilePos = tile.GetCenter(); + const auto & objectInfo = Maps::getObjectInfo( groupType, type ); + if ( canFitObject( data, objectInfo, tilePos, true ) && putObjectOnMap( mapFormat, tile, groupType, type ) ) { + markObjectPlacement( data, objectInfo, tilePos ); + return true; + } + + return false; + } + + bool placeCastle( Map_Format::MapFormat & mapFormat, NodeCache & data, const Region & region, const fheroes2::Point tilePos, const bool isCastle ) + { + auto & tile = world.getTile( tilePos.x, tilePos.y ); + if ( tile.isWater() ) { + return false; + } + + const int32_t basementId = fheroes2::getTownBasementId( tile.GetGround() ); + const int32_t castleObjectId = isCastle ? randomCastleIndex : randomTownIndex; + + const auto & basementInfo = Maps::getObjectInfo( ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ); + const auto & castleInfo = Maps::getObjectInfo( ObjectGroup::KINGDOM_TOWNS, castleObjectId ); + + if ( !canFitObject( data, basementInfo, tilePos, true ) || !canFitObject( data, castleInfo, tilePos, true ) ) { + return false; + } + + if ( !putObjectOnMap( mapFormat, tile, ObjectGroup::LANDSCAPE_TOWN_BASEMENTS, basementId ) ) { + return false; + } + + // Since the whole object consists of multiple "objects" we have to put the same ID for all of them. + // Every time an object is being placed on a map the counter is going to be increased by 1. + // Therefore, we set the counter by 1 less for each object to match object UID for all of them. + assert( Maps::getLastObjectUID() > 0 ); + const uint32_t objectId = Maps::getLastObjectUID() - 1; + + Maps::setLastObjectUID( objectId ); + + if ( !putObjectOnMap( mapFormat, tile, ObjectGroup::KINGDOM_TOWNS, castleObjectId ) ) { + return false; + } + + const int32_t bottomIndex = Maps::GetDirectionIndex( tile.GetIndex(), Direction::BOTTOM ); + + if ( Maps::isValidAbsIndex( bottomIndex ) && Maps::doesContainRoads( mapFormat.tiles[bottomIndex] ) ) { + // Update road if there is one in front of the town/castle entrance. + Maps::updateRoadSpriteOnTile( mapFormat, bottomIndex, false ); + } + + // By default use random (default) army for the neutral race town/castle. + const PlayerColor color = Color::IndexToColor( region.colorIndex ); + if ( color == PlayerColor::NONE ) { + Maps::setDefaultCastleDefenderArmy( mapFormat.castleMetadata[Maps::getLastObjectUID()] ); + } + + // Add flags. + assert( tile.GetIndex() > 0 && tile.GetIndex() < world.w() * world.h() - 1 ); + Maps::setLastObjectUID( objectId ); + + if ( !putObjectOnMap( mapFormat, world.getTile( tile.GetIndex() - 1 ), ObjectGroup::LANDSCAPE_FLAGS, Color::GetIndex( color ) * 2 ) ) { + return false; + } + + Maps::setLastObjectUID( objectId ); + + if ( !putObjectOnMap( mapFormat, world.getTile( tile.GetIndex() + 1 ), ObjectGroup::LANDSCAPE_FLAGS, Color::GetIndex( color ) * 2 + 1 ) ) { + return false; + } + + const ObjectInfo & townObjectInfo = Maps::getObjectInfo( ObjectGroup::KINGDOM_TOWNS, castleObjectId ); + const uint8_t race = Race::IndexToRace( static_cast( townObjectInfo.metadata[0] ) ); + + world.addCastle( tile.GetIndex(), race, color ); + + markObjectPlacement( data, basementInfo, tilePos ); + markObjectPlacement( data, castleInfo, tilePos ); + + // Force roads coming from the castle + const int32_t nextIndex = Maps::GetDirectionIndex( bottomIndex, Direction::BOTTOM ); + if ( Maps::isValidAbsIndex( nextIndex ) ) { + Maps::updateRoadOnTile( mapFormat, bottomIndex, true ); + Maps::updateRoadOnTile( mapFormat, nextIndex, true ); + } + + return true; + } + + bool placeMine( Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & node, const int resource ) + { + Tile & mineTile = world.getTile( node.index ); + const int32_t mineType = fheroes2::getMineObjectInfoId( resource, mineTile.GetGround() ); + return placeActionObject( mapFormat, data, mineTile, ObjectGroup::ADVENTURE_MINES, mineType ); + } + + bool placeObstacle( Map_Format::MapFormat & mapFormat, const NodeCache & data, const Node & node, Rand::PCG32 & randomGenerator ) + { + Tile & tile = world.getTile( node.index ); + const auto it = obstaclesPerGround.find( tile.GetGround() ); + if ( it == obstaclesPerGround.end() || it->second.empty() ) { + return false; + } + + std::vector obstacleList = it->second; + Rand::ShuffleWithGen( obstacleList, randomGenerator ); + + const fheroes2::Point tilePos = tile.GetCenter(); + for ( const auto & obstacleId : obstacleList ) { + const auto & objectInfo = Maps::getObjectInfo( ObjectGroup::LANDSCAPE_TREES, obstacleId ); + if ( canFitObject( data, objectInfo, tilePos, false ) && putObjectOnMap( mapFormat, tile, ObjectGroup::LANDSCAPE_TREES, obstacleId ) ) { + return true; + } + } + return false; + } + + bool placeSimpleObject( Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & centerNode, const ObjectPlacement & placement ) + { + const fheroes2::Point position = Maps::GetPoint( centerNode.index ) + placement.offset; + if ( !Maps::isValidAbsPoint( position.x, position.y ) ) { + return false; + } + + Tile & tile = world.getTile( position.x, position.y ); + const int32_t objectIndex = selectTerrainVariantForObject( placement.groupType, placement.objectIndex, tile.GetGround() ); + const auto & objectInfo = Maps::getObjectInfo( placement.groupType, objectIndex ); + if ( putObjectOnMap( mapFormat, tile, placement.groupType, objectIndex ) ) { + markObjectPlacement( data, objectInfo, position ); + return true; + } + return false; + } + + void placeObjectSet( Map_Format::MapFormat & mapFormat, NodeCache & data, const Region & region, std::vector objects, const uint8_t expectedCount, + Rand::PCG32 & randomGenerator ) + { + int objectsPlaced = 0; + for ( int attempt = 0; attempt < maxPlacementAttempts; ++attempt ) { + if ( objectsPlaced == expectedCount ) { + break; + } + + const Node & node = Rand::GetWithGen( region.nodes, randomGenerator ); + + Rand::ShuffleWithGen( objects, randomGenerator ); + for ( const auto & prefab : objects ) { + if ( !canFitObjectSet( data, prefab, Maps::GetPoint( node.index ) ) ) { + continue; + } + + for ( const auto & obstacle : prefab.obstacles ) { + placeSimpleObject( mapFormat, data, node, obstacle ); + } + + int32_t groupValue = 0; + for ( const auto & treasure : prefab.valuables ) { + placeSimpleObject( mapFormat, data, node, treasure ); + groupValue += getObjectGoldValue( treasure.groupType, treasure.objectIndex ); + } + + const int32_t monsterIndex = pickMonsterByValue( groupValue ); + for ( const auto & monster : prefab.monsters ) { + const fheroes2::Point position = Maps::GetPoint( node.index ) + monster.offset; + putObjectOnMap( mapFormat, world.getTile( position.x, position.y ), ObjectGroup::MONSTERS, monsterIndex ); + } + ++objectsPlaced; + break; + } + } + } +} diff --git a/src/fheroes2/maps/map_random_generator_helper.h b/src/fheroes2/maps/map_random_generator_helper.h new file mode 100644 index 000000000..6fa06a705 --- /dev/null +++ b/src/fheroes2/maps/map_random_generator_helper.h @@ -0,0 +1,73 @@ +/*************************************************************************** + * fheroes2: https://github.com/ihhub/fheroes2 * + * Copyright (C) 2025 * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#pragma once + +#include +#include + +#include "math_base.h" + +namespace Maps +{ + class Tile; + struct ObjectInfo; + enum class ObjectGroup : uint8_t; +} + +namespace Maps::Random_Generator +{ + class NodeCache; + struct Node; + struct ObjectPlacement; + struct ObjectSet; + struct Region; +} + +namespace Maps::Map_Format +{ + struct MapFormat; +} + +namespace Rand +{ + class PCG32; +} + +namespace Maps::Random_Generator +{ + int32_t getObjectGoldValue( const ObjectGroup group, const int32_t objectIndex ); + int32_t pickMonsterByValue( const int32_t protectedObjectValue ); + int32_t selectTerrainVariantForObject( const ObjectGroup groupType, const int32_t objectIndex, const int groundType ); + std::vector> findOpenTilesSortedJittered( const Region & region, int32_t mapWidth, Rand::PCG32 & randomGenerator ); + + bool canFitObject( const NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos, const bool skipBorders ); + bool canFitObjectSet( const NodeCache & data, const ObjectSet & set, const fheroes2::Point & mainTilePos ); + void markObjectPlacement( NodeCache & data, const ObjectInfo & info, const fheroes2::Point & mainTilePos ); + + bool putObjectOnMap( Map_Format::MapFormat & mapFormat, Tile & tile, const ObjectGroup groupType, const int32_t objectIndex ); + bool placeActionObject( Map_Format::MapFormat & mapFormat, NodeCache & data, Tile & tile, const ObjectGroup groupType, const int32_t type ); + bool placeCastle( Map_Format::MapFormat & mapFormat, NodeCache & data, const Region & region, const fheroes2::Point tilePos, const bool isCastle ); + bool placeMine( Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & node, const int resource ); + bool placeObstacle( Map_Format::MapFormat & mapFormat, const NodeCache & data, const Node & node, Rand::PCG32 & randomGenerator ); + bool placeSimpleObject( Map_Format::MapFormat & mapFormat, NodeCache & data, const Node & centerNode, const ObjectPlacement & placement ); + void placeObjectSet( Map_Format::MapFormat & mapFormat, NodeCache & data, const Region & region, std::vector objects, const uint8_t expectedCount, + Rand::PCG32 & randomGenerator ); +} diff --git a/src/fheroes2/maps/map_random_generator_info.cpp b/src/fheroes2/maps/map_random_generator_info.cpp new file mode 100644 index 000000000..9f69c06a4 --- /dev/null +++ b/src/fheroes2/maps/map_random_generator_info.cpp @@ -0,0 +1,184 @@ +/*************************************************************************** + * fheroes2: https://github.com/ihhub/fheroes2 * + * Copyright (C) 2025 * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#include "map_random_generator_info.h" + +#include +#include +#include +#include + +#include "logging.h" +#include "map_format_info.h" +#include "maps.h" +#include "rand.h" + +namespace +{ + constexpr uint8_t directionCount{ 8 }; + const std::array directionOffsets{ { { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 }, { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 } } }; +} + +namespace Maps::Random_Generator +{ + NodeCache::NodeCache( const int32_t width, const int32_t height ) + : _mapSize( width ) + , _data( static_cast( width ) * height ) + { + assert( width > 0 && height > 0 ); + assert( width == height ); + + for ( size_t i = 0; i < _data.size(); ++i ) { + _data[i].index = static_cast( i ); + } + } + + void MapEconomy::increaseMineCount( const int resourceType ) + { + const auto it = _minesCount.find( resourceType ); + assert( it != _minesCount.end() ); + ++it->second; + } + + int MapEconomy::pickNextMineResource() + { + const auto it = std::min_element( secondaryResources.begin(), secondaryResources.end(), + [this]( const auto & a, const auto & b ) { return _minesCount.at( a ) < _minesCount.at( b ); } ); + assert( it != secondaryResources.end() ); + + return *it; + } + + void Region::checkAdjacentTiles( NodeCache & rawData, Rand::PCG32 & randomGenerator ) + { + Node & previousNode = nodes[lastProcessedNode]; + const int nodeIndex = previousNode.index; + + for ( uint8_t direction = 0; direction < directionCount; ++direction ) { + if ( nodes.size() > sizeLimit ) { + previousNode.type = NodeType::BORDER; + break; + } + + // Check diagonal direction only 50% of the time to get more circular distribution. + // It gives randomness for uneven edges. + if ( direction > 3 && Rand::GetWithGen( 0, 1, randomGenerator ) ) { + continue; + } + + // TODO: use node index and pre-calculate offsets in advance. + // This will speed up the below calculations. + const fheroes2::Point newPosition = Maps::GetPoint( nodeIndex ) + directionOffsets[direction]; + if ( !Maps::isValidAbsPoint( newPosition.x, newPosition.y ) ) { + continue; + } + + Node & newTile = rawData.getNode( newPosition ); + if ( newTile.region == 0 && newTile.type == NodeType::OPEN ) { + newTile.region = id; + nodes.emplace_back( newTile ); + } + else if ( newTile.region != id ) { + previousNode.type = NodeType::BORDER; + neighbours.insert( newTile.region ); + } + } + } + + bool Region::regionExpansion( NodeCache & rawData, Rand::PCG32 & randomGenerator ) + { + // Process only "open" nodes that exist at the start of the loop and ignore what's added. + const size_t nodesEnd = nodes.size(); + + while ( lastProcessedNode < nodesEnd ) { + checkAdjacentTiles( rawData, randomGenerator ); + ++lastProcessedNode; + } + return lastProcessedNode != nodes.size(); + } + + bool Region::checkNodeForConnections( NodeCache & data, std::vector & mapRegions, Node & node ) + { + if ( node.type != NodeType::BORDER ) { + return false; + } + + const fheroes2::Point position = Maps::GetPoint( node.index ); + + int distinctNeighbours = 0; + uint32_t seen = node.region; + for ( uint8_t direction = 0; direction < directionCount; ++direction ) { + const Node & newTile = data.getNode( position + directionOffsets[direction] ); + + if ( newTile.index == -1 || newTile.region == id ) { + continue; + } + + if ( seen != newTile.region ) { + seen = newTile.region; + + ++distinctNeighbours; + if ( distinctNeighbours > 1 ) { + return false; + } + } + } + + for ( uint8_t direction = 0; direction < 4; ++direction ) { + Node & adjacent = data.getNode( position + directionOffsets[direction] ); + + if ( adjacent.index == -1 || adjacent.region == id || mapRegions[adjacent.region].groundType == Ground::WATER ) { + continue; + } + + Node & twoAway = data.getNode( position + directionOffsets[direction] + directionOffsets[direction] ); + if ( twoAway.index == -1 || twoAway.type != NodeType::OPEN ) { + continue; + } + + if ( connections.find( adjacent.region ) == connections.end() ) { + DEBUG_LOG( DBG_DEVEL, DBG_TRACE, "Found a connection between " << id << " and " << adjacent.region << ", via " << node.index ) + connections.emplace( adjacent.region, node.index ); + mapRegions[adjacent.region].connections.emplace( id, node.index ); + node.type = NodeType::CONNECTOR; + adjacent.type = NodeType::PATH; + twoAway.type = NodeType::PATH; + + Node & stepBack = data.getNode( position - directionOffsets[direction] ); + if ( stepBack.index != -1 ) { + stepBack.type = NodeType::PATH; + } + + break; + } + } + + return node.type == NodeType::CONNECTOR; + } + + fheroes2::Point Region::adjustRegionToFitCastle( const Map_Format::MapFormat & mapFormat ) + { + const fheroes2::Point startingLocation = Maps::GetPoint( centerIndex ); + const int32_t castleX = std::min( std::max( startingLocation.x, 4 ), mapFormat.width - 4 ); + const int32_t castleY = std::min( std::max( startingLocation.y, 4 ), mapFormat.width - 4 ); + centerIndex = Maps::GetIndexFromAbsPoint( castleX, castleY + 2 ); + return { castleX, castleY }; + } +} diff --git a/src/fheroes2/maps/map_random_generator_info.h b/src/fheroes2/maps/map_random_generator_info.h new file mode 100644 index 000000000..b45260944 --- /dev/null +++ b/src/fheroes2/maps/map_random_generator_info.h @@ -0,0 +1,208 @@ +/*************************************************************************** + * fheroes2: https://github.com/ihhub/fheroes2 * + * Copyright (C) 2025 * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + ***************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "color.h" +#include "ground.h" +#include "map_object_info.h" +#include "math_base.h" +#include "resource.h" + +namespace Maps::Map_Format +{ + struct MapFormat; +} + +namespace Rand +{ + class PCG32; +} + +namespace Maps::Random_Generator +{ + inline int neutralColorIndex{ Color::GetIndex( PlayerColor::UNUSED ) }; + constexpr std::array secondaryResources = { Resource::CRYSTAL, Resource::SULFUR, Resource::GEMS, Resource::MERCURY }; + + enum class NodeType : uint8_t + { + OPEN, + BORDER, + ACTION, + OBSTACLE, + CONNECTOR, + PATH + }; + + struct Node final + { + int index{ -1 }; + uint32_t region{ 0 }; + NodeType type{ NodeType::OPEN }; + + Node() = default; + + explicit Node( const int index_ ) + : index( index_ ) + { + // Do nothing + } + + explicit Node( const int index_, const uint32_t region_, const NodeType type_ ) + : index( index_ ) + , region( region_ ) + , type( type_ ) + { + // Do nothing + } + }; + + class NodeCache final + { + public: + NodeCache( const int32_t width, const int32_t height ); + + Node & getNode( const fheroes2::Point position ) + { + if ( position.x < 0 || position.x >= _mapSize || position.y < 0 || position.y >= _mapSize ) { + // We shouldn't try to get a tile with an invalid index. + // TODO: here we must add an assertion and make sure that we never reach this place. + assert( _outOfBounds.type == NodeType::BORDER ); + assert( _outOfBounds.index == -1 ); + + return _outOfBounds; + } + + return _data[position.y * _mapSize + position.x]; + } + + const Node & getNode( const fheroes2::Point position ) const + { + if ( position.x < 0 || position.x >= _mapSize || position.y < 0 || position.y >= _mapSize ) { + // We shouldn't try to get a tile with an invalid index. + // However, since we are accessing const value it is fine to get an invalid item. + assert( _outOfBounds.type == NodeType::BORDER ); + assert( _outOfBounds.index == -1 ); + + return _outOfBounds; + } + + return _data[position.y * _mapSize + position.x]; + } + + Node & getNode( const int32_t index ) + { + if ( index < 0 || index >= _mapSize * _mapSize ) { + // We shouldn't try to get a tile with an invalid index. + assert( 0 ); + + assert( _outOfBounds.type == NodeType::BORDER ); + assert( _outOfBounds.index == -1 ); + + return _outOfBounds; + } + + return _data[index]; + } + + private: + const int32_t _mapSize{ 0 }; + Node _outOfBounds{ -1, 0, NodeType::BORDER }; + std::vector _data; + }; + + struct Region final + { + uint32_t id{ 0 }; + int32_t centerIndex{ -1 }; + std::set neighbours; + std::vector> nodes; + std::map connections; + size_t sizeLimit{ 0 }; + size_t lastProcessedNode{ 0 }; + int colorIndex{ neutralColorIndex }; + int groundType{ Ground::GRASS }; + + Region() = default; + + Region( const uint32_t regionIndex, Node & centerNode, const int playerColor, const int ground, const size_t expectedSize ) + : id( regionIndex ) + , centerIndex( centerNode.index ) + , sizeLimit( expectedSize ) + , colorIndex( playerColor ) + , groundType( ground ) + { + assert( expectedSize > 0 ); + + centerNode.region = regionIndex; + nodes.reserve( expectedSize ); + nodes.emplace_back( centerNode ); + } + + void checkAdjacentTiles( NodeCache & rawData, Rand::PCG32 & randomGenerator ); + bool regionExpansion( NodeCache & rawData, Rand::PCG32 & randomGenerator ); + bool checkNodeForConnections( NodeCache & data, std::vector & mapRegions, Node & node ); + fheroes2::Point adjustRegionToFitCastle( const Map_Format::MapFormat & mapFormat ); + }; + + class MapEconomy final + { + public: + void increaseMineCount( const int resourceType ); + int pickNextMineResource(); + + private: + std::map _minesCount{ { Resource::WOOD, 0 }, { Resource::ORE, 0 }, { Resource::CRYSTAL, 0 }, { Resource::SULFUR, 0 }, + { Resource::GEMS, 0 }, { Resource::MERCURY, 0 }, { Resource::GOLD, 0 } }; + }; + + struct RegionalObjects final + { + uint8_t castleCount{ 0 }; + uint8_t mineCount{ 0 }; + uint8_t objectCount{ 0 }; + uint8_t powerUpsCount{ 0 }; + uint8_t treasureCount{ 0 }; + }; + + struct ObjectPlacement final + { + fheroes2::Point offset; + ObjectGroup groupType{ ObjectGroup::NONE }; + int32_t objectIndex{ 0 }; + }; + + struct ObjectSet final + { + std::vector obstacles; + std::vector valuables; + std::vector monsters; + std::vector entranceCheck; + }; +}