소스 검색

multilevel support

Laserlicht 2 달 전
부모
커밋
ecfe09f6b1

+ 1 - 1
client/adventureMap/AdventureMapShortcuts.cpp

@@ -154,7 +154,7 @@ void AdventureMapShortcuts::worldViewScale4x()
 void AdventureMapShortcuts::switchMapLevel()
 {
 	int maxLevels = GAME->interface()->cb->getMapSize().z;
-	if (maxLevels < 2)
+	if (maxLevels < 2) // TODO: multilevel support
 		return;
 
 	owner.hotkeySwitchMapLevel();

+ 1 - 1
client/adventureMap/AdventureMapWidget.cpp

@@ -410,7 +410,7 @@ void AdventureMapWidget::updateActiveStateChildden(CIntObject * widget)
 			if (container->disableCondition == "heroSleeping")
 				container->setEnabled(shortcuts->optionHeroSleeping());
 
-			if (container->disableCondition == "mapLayerSurface")
+			if (container->disableCondition == "mapLayerSurface") // TODO: multilevel support
 				container->setEnabled(shortcuts->optionMapLevelSurface());
 
 			if (container->disableCondition == "mapLayerUnderground")

+ 6 - 6
client/lobby/RandomMapTab.cpp

@@ -54,15 +54,15 @@ RandomMapTab::RandomMapTab():
 		mapGenOptions->setWidth(mapSizeVal[btnId]);
 		mapGenOptions->setHeight(mapSizeVal[btnId]);
 		if(mapGenOptions->getMapTemplate())
-			if(!mapGenOptions->getMapTemplate()->matchesSize(int3{mapGenOptions->getWidth(), mapGenOptions->getHeight(), 1 + mapGenOptions->getHasTwoLevels()}))
+			if(!mapGenOptions->getMapTemplate()->matchesSize(int3{mapGenOptions->getWidth(), mapGenOptions->getHeight(), mapGenOptions->getLevels()}))
 				setTemplate(nullptr);
 		updateMapInfoByHost();
 	});
 	addCallback("toggleTwoLevels", [&](bool on)
 	{
-		mapGenOptions->setHasTwoLevels(on);
+		mapGenOptions->setLevels(on ? 2 : 1); // TODO: multilevel support
 		if(mapGenOptions->getMapTemplate())
-			if(!mapGenOptions->getMapTemplate()->matchesSize(int3{mapGenOptions->getWidth(), mapGenOptions->getHeight(), 1 + mapGenOptions->getHasTwoLevels()}))
+			if(!mapGenOptions->getMapTemplate()->matchesSize(int3{mapGenOptions->getWidth(), mapGenOptions->getHeight(), mapGenOptions->getLevels()}))
 				setTemplate(nullptr);
 		updateMapInfoByHost();
 	});
@@ -202,7 +202,7 @@ void RandomMapTab::updateMapInfoByHost()
 	mapInfo->mapHeader->difficulty = EMapDifficulty::NORMAL;
 	mapInfo->mapHeader->height = mapGenOptions->getHeight();
 	mapInfo->mapHeader->width = mapGenOptions->getWidth();
-	mapInfo->mapHeader->twoLevel = mapGenOptions->getHasTwoLevels();
+	mapInfo->mapHeader->mapLevels = mapGenOptions->getLevels();
 
 	// Generate player information
 	int playersToGen = mapGenOptions->getMaxPlayersCount();
@@ -321,7 +321,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
 			if(auto button = std::dynamic_pointer_cast<CToggleButton>(toggle.second))
 			{
 				const auto & mapSizes = getPossibleMapSizes();
-				int3 size( mapSizes[toggle.first], mapSizes[toggle.first], 1 + mapGenOptions->getHasTwoLevels());
+				int3 size( mapSizes[toggle.first], mapSizes[toggle.first], mapGenOptions->getLevels());
 
 				bool sizeAllowed = !mapGenOptions->getMapTemplate() || mapGenOptions->getMapTemplate()->matchesSize(size);
 				button->block(!sizeAllowed);
@@ -335,7 +335,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
 
 		bool undergoundAllowed = !mapGenOptions->getMapTemplate() || mapGenOptions->getMapTemplate()->matchesSize(size);
 
-		w->setSelected(opts->getHasTwoLevels());
+		w->setSelected(opts->getLevels() == 2); // TODO: multilevel support
 		w->block(!undergoundAllowed);
 	}
 	if(auto w = widget<CToggleGroup>("groupMaxPlayers"))

+ 2 - 1
client/windows/CMapOverview.cpp

@@ -121,7 +121,7 @@ std::vector<std::shared_ptr<CanvasImage>> CMapOverviewWidget::createMinimaps(std
 {
 	std::vector<std::shared_ptr<CanvasImage>> ret;
 
-	for(int i = 0; i < (map->twoLevel ? 2 : 1); i++)
+	for(int i = 0; i < map->levels(); i++)
 		ret.push_back(createMinimapForLayer(map, i));
 
 	return ret;
@@ -129,6 +129,7 @@ std::vector<std::shared_ptr<CanvasImage>> CMapOverviewWidget::createMinimaps(std
 
 std::shared_ptr<CPicture> CMapOverviewWidget::buildDrawMinimap(const JsonNode & config) const
 {
+	// TODO: multilevel support
 	logGlobal->debug("Building widget drawMinimap");
 
 	auto rect = readRect(config["rect"]);

+ 2 - 2
client/windows/InfoWindows.cpp

@@ -351,7 +351,7 @@ MinimapWithIcons::MinimapWithIcons(const Point & position)
 	Rect borderSurface(10, 40, 147, 147);
 	Rect borderUnderground(166, 40, 147, 147);
 
-	bool singleLevelMap = GAME->interface()->cb->getMapSize().z == 1;
+	bool singleLevelMap = GAME->interface()->cb->getMapSize().z == 1; // TODO: multilevel support
 
 	if (singleLevelMap)
 	{
@@ -375,7 +375,7 @@ void MinimapWithIcons::addIcon(const int3 & coordinates, const ImagePath & image
 
 	Rect areaSurface(11, 41, 144, 144);
 	Rect areaUnderground(167, 41, 144, 144);
-	bool singleLevelMap = GAME->interface()->cb->getMapSize().z == 1;
+	bool singleLevelMap = GAME->interface()->cb->getMapSize().z == 1; // TODO: multilevel support
 	if (singleLevelMap)
 		areaSurface.x += 78;
 

+ 1 - 1
lib/callback/MapInfoCallback.cpp

@@ -97,7 +97,7 @@ bool MapInfoCallback::isAllowed(SecondarySkill id) const
 
 int3 MapInfoCallback::getMapSize() const
 {
-	return int3(getMapConstPtr()->width, getMapConstPtr()->height, getMapConstPtr()->twoLevel ? 2 : 1);
+	return int3(getMapConstPtr()->width, getMapConstPtr()->height, getMapConstPtr()->levels());
 }
 
 void MapInfoCallback::getAllowedSpells(std::vector<SpellID> & out, std::optional<ui16> level)

+ 1 - 1
lib/gameState/GameStatistics.cpp

@@ -325,7 +325,7 @@ float Statistic::getMapExploredRatio(const CGameState * gs, PlayerColor player)
 	float visible = 0.0;
 	float numTiles = 0.0;
 
-	for(int layer = 0; layer < (gs->getMap().twoLevel ? 2 : 1); layer++)
+	for(int layer = 0; layer < gs->getMap().levels(); layer++)
 		for(int y = 0; y < gs->getMap().height; ++y)
 			for(int x = 0; x < gs->getMap().width; ++x)
 			{

+ 1 - 2
lib/mapping/CMap.cpp

@@ -239,8 +239,7 @@ void CMap::showObject(CGObjectInstance * obj)
 
 void CMap::calculateGuardingGreaturePositions()
 {
-	int levels = twoLevel ? 2 : 1;
-	for(int z = 0; z < levels; z++)
+	for(int z = 0; z < levels(); z++)
 	{
 		for(int x = 0; x < width; x++)
 		{

+ 1 - 1
lib/mapping/CMap.h

@@ -370,7 +370,7 @@ inline bool CMap::isInTheMap(const int3 & pos) const
 	return
 		static_cast<uint32_t>(pos.x) < static_cast<uint32_t>(width) &&
 		static_cast<uint32_t>(pos.y) < static_cast<uint32_t>(height) &&
-		static_cast<uint32_t>(pos.z) <= (twoLevel ? 1 : 0);
+		static_cast<uint32_t>(pos.z) <= mapLevels - 1;
 }
 
 inline TerrainTile & CMap::getTile(const int3 & tile)

+ 2 - 2
lib/mapping/CMapHeader.cpp

@@ -122,7 +122,7 @@ CMapHeader::CMapHeader()
 	: version(EMapFormat::VCMI)
 	, height(72)
 	, width(72)
-	, twoLevel(true)
+	, mapLevels(2)
 	, difficulty(EMapDifficulty::NORMAL)
 	, levelLimit(0)
 	, victoryIconIndex(0)
@@ -139,7 +139,7 @@ CMapHeader::~CMapHeader() = default;
 
 ui8 CMapHeader::levels() const
 {
-	return (twoLevel ? 2 : 1);
+	return mapLevels;
 }
 
 void CMapHeader::registerMapStrings()

+ 18 - 2
lib/mapping/CMapHeader.h

@@ -246,7 +246,7 @@ public:
 
 	si32 height; /// The default value is 72.
 	si32 width; /// The default value is 72.
-	bool twoLevel; /// The default value is true.
+	si32 mapLevels; /// The default value is 2.
 	MetaString name;
 	MetaString description;
 	EMapDifficulty difficulty;
@@ -295,7 +295,23 @@ public:
 		h & creationDateTime;
 		h & width;
 		h & height;
-		h & twoLevel;
+		if (h.version >= Handler::Version::MORE_MAP_LAYERS)
+			h & mapLevels;
+		else
+		{
+			if (h.saving)
+			{
+				bool hasTwoLevels = mapLevels == 2;
+				h & hasTwoLevels;
+			}
+			else
+			{
+				bool hasTwoLevels;
+				h & hasTwoLevels;
+				mapLevels = hasTwoLevels ? 2 : 1;
+			}
+		}
+
 		h & difficulty;
 
 		h & levelLimit;

+ 4 - 7
lib/mapping/CMapOperation.cpp

@@ -563,14 +563,11 @@ CDrawTerrainOperation::ValidationResult::ValidationResult(bool result, std::stri
 
 CClearTerrainOperation::CClearTerrainOperation(CMap* map, vstd::RNG* gen) : CComposedOperation(map)
 {
-	CTerrainSelection terrainSel(map);
-	terrainSel.selectRange(MapRect(int3(0, 0, 0), map->width, map->height));
-	addOperation(std::make_unique<CDrawTerrainOperation>(map, terrainSel, ETerrainId::WATER, 0, gen));
-	if(map->twoLevel)
+	for (int i = 0; i < map->mapLevels; i++)
 	{
-		terrainSel.clearSelection();
-		terrainSel.selectRange(MapRect(int3(0, 0, 1), map->width, map->height));
-		addOperation(std::make_unique<CDrawTerrainOperation>(map, terrainSel, ETerrainId::ROCK, 0, gen));
+		CTerrainSelection terrainSel(map);
+		terrainSel.selectRange(MapRect(int3(0, 0, i), map->width, map->height));
+		addOperation(std::make_unique<CDrawTerrainOperation>(map, terrainSel, i == 1 ? ETerrainId::ROCK : ETerrainId::WATER, 0, gen));
 	}
 }
 

+ 1 - 1
lib/mapping/MapFormatH3M.cpp

@@ -185,7 +185,7 @@ void CMapLoaderH3M::readHeader()
 	// Read map name, description, dimensions,...
 	mapHeader->areAnyPlayers = reader->readBool();
 	mapHeader->height = mapHeader->width = reader->readInt32();
-	mapHeader->twoLevel = reader->readBool();
+	mapHeader->mapLevels = reader->readBool() ? 2 : 1;
 	mapHeader->name.appendTextID(readLocalizedString("header.name"));
 	mapHeader->description.appendTextID(readLocalizedString("header.description"));
 	mapHeader->author.appendRawString("");

+ 31 - 29
lib/mapping/MapFormatJson.cpp

@@ -240,7 +240,16 @@ const int CMapFormatJson::VERSION_MINOR = 0;
 
 const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json";
 const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json";
-const std::string CMapFormatJson::TERRAIN_FILE_NAMES[2] = {"surface_terrain.json", "underground_terrain.json"};
+
+std::string getTerrainFilename(int i)
+{
+	if(i == 0)
+		return "surface_terrain.json";
+	else if(i == 1)
+		return "underground_terrain.json";
+	else
+		return "level-" + std::to_string(i + 1) + "_terrain.json";
+}
 
 CMapFormatJson::CMapFormatJson():
 	fileVersionMajor(0), fileVersionMinor(0),
@@ -844,7 +853,6 @@ void CMapLoaderJson::readHeader(const bool complete)
 	//loading mods
 	mapHeader->mods = ModVerificationInfo::jsonDeserializeList(header["mods"]);
 
-	//todo: multilevel map load support
 	{
 		auto levels = handler.enterStruct("mapLevels");
 		{
@@ -852,10 +860,7 @@ void CMapLoaderJson::readHeader(const bool complete)
 			handler.serializeInt("height", mapHeader->height);
 			handler.serializeInt("width", mapHeader->width);
 		}
-		{
-			auto underground = handler.enterStruct("underground");
-			mapHeader->twoLevel = !underground->getCurrent().isNull();
-		}
+		mapHeader->mapLevels = levels->getCurrent().Struct().size();
 	}
 
 	serializeHeader(handler);
@@ -993,14 +998,10 @@ void CMapLoaderJson::readTerrainLevel(const JsonNode & src, const int index)
 
 void CMapLoaderJson::readTerrain()
 {
+	for(int i = 0; i < map->mapLevels; i++)
 	{
-		const JsonNode surface = getFromArchive(TERRAIN_FILE_NAMES[0]);
-		readTerrainLevel(surface, 0);
-	}
-	if(map->twoLevel)
-	{
-		const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]);
-		readTerrainLevel(underground, 1);
+		const JsonNode node = getFromArchive(getTerrainFilename(i));
+		readTerrainLevel(node, i);
 	}
 }
 
@@ -1198,17 +1199,22 @@ void CMapSaverJson::writeHeader()
 	//write mods
 	header["mods"] = ModVerificationInfo::jsonSerializeList(mapHeader->mods);
 
-	//todo: multilevel map save support
-	JsonNode & levels = header["mapLevels"];
-	levels["surface"]["height"].Float() = mapHeader->height;
-	levels["surface"]["width"].Float() = mapHeader->width;
-	levels["surface"]["index"].Float() = 0;
+	auto getName = [](int level){
+		if(level == 0)
+			return std::string("surface");
+		else if(level == 1)
+			return std::string("underground");
+		else
+			return "level-" + std::to_string(level + 1);
+	};
 
-	if(mapHeader->twoLevel)
+	JsonNode & levels = header["mapLevels"];
+	for(int i = 0; i < map->mapLevels; i++)
 	{
-		levels["underground"]["height"].Float() = mapHeader->height;
-		levels["underground"]["width"].Float() = mapHeader->width;
-		levels["underground"]["index"].Float() = 1;
+		auto name = getName(i);
+		levels[name]["height"].Float() = mapHeader->height;
+		levels[name]["width"].Float() = mapHeader->width;
+		levels[name]["index"].Float() = i;
 	}
 
 	serializeHeader(handler);
@@ -1266,15 +1272,11 @@ JsonNode CMapSaverJson::writeTerrainLevel(const int index)
 void CMapSaverJson::writeTerrain()
 {
 	logGlobal->trace("Saving terrain");
-	//todo: multilevel map save support
-
-	JsonNode surface = writeTerrainLevel(0);
-	addToArchive(surface, TERRAIN_FILE_NAMES[0]);
 
-	if(map->twoLevel)
+	for(int i = 0; i < map->mapLevels; i++)
 	{
-		JsonNode underground = writeTerrainLevel(1);
-		addToArchive(underground, TERRAIN_FILE_NAMES[1]);
+		JsonNode node = writeTerrainLevel(i);
+		addToArchive(node, getTerrainFilename(i));
 	}
 }
 

+ 15 - 10
lib/rmg/CMapGenOptions.cpp

@@ -24,7 +24,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 CMapGenOptions::CMapGenOptions()
-	: width(CMapHeader::MAP_SIZE_MIDDLE), height(CMapHeader::MAP_SIZE_MIDDLE), hasTwoLevels(true),
+	: width(CMapHeader::MAP_SIZE_MIDDLE), height(CMapHeader::MAP_SIZE_MIDDLE), levels(2),
 	humanOrCpuPlayerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE),
 	waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr),
 	customizedPlayers(false)
@@ -54,14 +54,14 @@ void CMapGenOptions::setHeight(si32 value)
 	height = value;
 }
 
-bool CMapGenOptions::getHasTwoLevels() const
+int CMapGenOptions::getLevels() const
 {
-	return hasTwoLevels;
+	return levels;
 }
 
-void CMapGenOptions::setHasTwoLevels(bool value)
+void CMapGenOptions::setLevels(int value)
 {
-	hasTwoLevels = value;
+	levels = value;
 }
 
 si8 CMapGenOptions::getHumanOrCpuPlayerCount() const
@@ -425,12 +425,12 @@ void CMapGenOptions::setMapTemplate(const CRmgTemplate * value)
 	//validate & adapt options according to template
 	if(mapTemplate)
 	{
-		if(!mapTemplate->matchesSize(int3(getWidth(), getHeight(), 1 + getHasTwoLevels())))
+		if(!mapTemplate->matchesSize(int3(getWidth(), getHeight(), getLevels())))
 		{
 			auto sizes = mapTemplate->getMapSizes();
 			setWidth(sizes.first.x);
 			setHeight(sizes.first.y);
-			setHasTwoLevels(sizes.first.z - 1);
+			setLevels(sizes.first.z);
 		}
 
 		si8 maxPlayerCount = getMaxPlayersCount(false);
@@ -488,7 +488,7 @@ void CMapGenOptions::setPlayerTeam(const PlayerColor & color, const TeamID & tea
 
 void CMapGenOptions::finalize(vstd::RNG & rand)
 {
-	logGlobal->info("RMG map: %dx%d, %s underground", getWidth(), getHeight(), getHasTwoLevels() ? "WITH" : "NO");
+	logGlobal->info("RMG map: %dx%d, %s underground", getWidth(), getHeight(), getLevels() >= 2 ? "WITH" : "NO");
 	logGlobal->info("RMG settings: players %d, teams %d, computer players %d, computer teams %d, water %d, monsters %d",
 		static_cast<int>(getHumanOrCpuPlayerCount()), static_cast<int>(getTeamCount()), static_cast<int>(getCompOnlyPlayerCount()),
 		static_cast<int>(getCompOnlyTeamCount()), static_cast<int>(getWaterContent()), static_cast<int>(getMonsterStrength()));
@@ -700,7 +700,7 @@ bool CMapGenOptions::arePlayersCustomized() const
 
 std::vector<const CRmgTemplate *> CMapGenOptions::getPossibleTemplates() const
 {
-	int3 tplSize(width, height, (hasTwoLevels ? 2 : 1));
+	int3 tplSize(width, height, levels);
 	auto humanPlayers = countHumanPlayers();
 
 	auto templates = LIBRARY->tplh->getTemplates();
@@ -825,7 +825,12 @@ void CMapGenOptions::serializeJson(JsonSerializeFormat & handler)
 {
 	handler.serializeInt("width", width);
 	handler.serializeInt("height", height);
-	handler.serializeBool("haswoLevels", hasTwoLevels);
+	bool hasTwoLevelsKey = !handler.getCurrent()["haswoLevels"].isNull();
+	bool hasTwoLevels = levels == 2;
+	if(handler.saving || !hasTwoLevelsKey)
+		handler.serializeInt("levels", levels);
+	else
+		handler.serializeBool("haswoLevels", hasTwoLevels);
 	handler.serializeInt("humanOrCpuPlayerCount", humanOrCpuPlayerCount);
 	handler.serializeInt("teamCount", teamCount);
 	handler.serializeInt("compOnlyPlayerCount", compOnlyPlayerCount);

+ 19 - 4
lib/rmg/CMapGenOptions.h

@@ -90,8 +90,8 @@ public:
 	si32 getHeight() const;
 	void setHeight(si32 value);
 
-	bool getHasTwoLevels() const;
-	void setHasTwoLevels(bool value);
+	int getLevels() const;
+	void setLevels(int value);
 
 	/// The count of all (human or computer) players ranging from 1 to PlayerColor::PLAYER_LIMIT or RANDOM_SIZE for random. If you call
 	/// this method, all player settings are reset to default settings.
@@ -170,7 +170,7 @@ private:
 
 	si32 width;
 	si32 height;
-	bool hasTwoLevels;
+	si32 levels;
 	si8 humanOrCpuPlayerCount;
 	si8 teamCount;
 	si8 compOnlyPlayerCount;
@@ -190,7 +190,22 @@ public:
 	{
 		h & width;
 		h & height;
-		h & hasTwoLevels;
+		if (h.version >= Handler::Version::MORE_MAP_LAYERS)
+			h & levels;
+		else
+		{
+			if (h.saving)
+			{
+				bool hasTwoLevels = levels == 2;
+				h & hasTwoLevels;
+			}
+			else
+			{
+				bool hasTwoLevels;
+				h & hasTwoLevels;
+				levels = hasTwoLevels ? 2 : 1;
+			}
+		}
 		h & humanOrCpuPlayerCount;
 		h & teamCount;
 		h & compOnlyPlayerCount;

+ 1 - 1
lib/rmg/CMapGenerator.cpp

@@ -458,7 +458,7 @@ void CMapGenerator::addHeaderInfo()
 	m.version = EMapFormat::VCMI;
 	m.width = mapGenOptions.getWidth();
 	m.height = mapGenOptions.getHeight();
-	m.twoLevel = mapGenOptions.getHasTwoLevels();
+	m.mapLevels = mapGenOptions.getLevels();
 	m.name.appendLocalString(EMetaText::GENERAL_TXT, 740);
 	m.description = getMapDescription();
 	m.difficulty = EMapDifficulty::NORMAL;

+ 34 - 21
lib/rmg/CZonePlacer.cpp

@@ -329,7 +329,7 @@ void CZonePlacer::placeZones(vstd::RNG * rand)
 	{
 		return pr.second->getType() == ETemplateZoneType::WATER;
 	});
-	bool underground = map.getMapGenOptions().getHasTwoLevels();
+	bool mapLevels = map.getMapGenOptions().getLevels();
 
 	findPathsBetweenZones();
 	placeOnGrid(rand);
@@ -347,7 +347,7 @@ void CZonePlacer::placeZones(vstd::RNG * rand)
 	RandomGeneratorUtil::randomShuffle(zonesVector, *rand);
 
 	//0. set zone sizes and surface / underground level
-	prepareZones(zones, zonesVector, underground, rand);
+	prepareZones(zones, zonesVector, mapLevels, rand);
 
 	std::map<std::shared_ptr<Zone>, float3> bestSolution;
 
@@ -441,21 +441,44 @@ void CZonePlacer::placeZones(vstd::RNG * rand)
 	}
 }
 
-void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const bool underground, vstd::RNG * rand)
+void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const int mapLevels, vstd::RNG * rand)
 {
-	std::vector<float> totalSize = { 0, 0 }; //make sure that sum of zone sizes on surface and uderground match size of the map
+	std::map<int, float> totalSize; //make sure that sum of zone sizes on surface and uderground match size of the map
 
-	int zonesOnLevel[2] = { 0, 0 };
+	std::map<int, int> zonesOnLevel;
+	for (int i = 0; i < mapLevels; i++)
+		zonesOnLevel[i] = 0;
 
 	//even distribution for surface / underground zones. Surface zones always have priority.
 
 	TZoneVector zonesToPlace;
 	std::map<TRmgTemplateZoneId, int> levels;
 
+	auto addZoneEqually = [&](auto & zone, bool ignoreUnderground = false) {
+		int chosenLevel = -1;
+		int minCount = std::numeric_limits<int>::max();
+
+		for (const auto& [level, count] : zonesOnLevel) {
+			if (ignoreUnderground && level == 1)
+				continue;
+
+			if (count < minCount ||
+				(count == minCount && level == 0) ||
+				(count == minCount && chosenLevel != 0 && level < chosenLevel))
+			{
+				chosenLevel = level;
+				minCount = count;
+			}
+		}
+
+		levels[zone.first] = chosenLevel;
+		zonesOnLevel[chosenLevel]++;
+	};
+
 	//first pass - determine fixed surface for zones
 	for(const auto & zone : zonesVector)
 	{
-		if (!underground) //this step is ignored
+		if (mapLevels == 1) //this step is ignored
 			zonesToPlace.push_back(zone);
 		else //place players depending on their factions
 		{
@@ -495,8 +518,7 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const
 						else
 						{
 							//surface
-							zonesOnLevel[0]++;
-							levels[zone.first] = 0;
+							addZoneEqually(zone, true);
 						}
 					}
 				}
@@ -509,17 +531,8 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const
 	}
 	for(const auto & zone : zonesToPlace)
 	{
-		if (underground) //only then consider underground zones
-		{
-			int level = 0;
-			if (zonesOnLevel[1] < zonesOnLevel[0]) //only if there are less underground zones
-				level = 1;
-			else
-				level = 0;
-
-			levels[zone.first] = level;
-			zonesOnLevel[level]++;
-		}
+		if (mapLevels > 1) //only then consider underground zones
+			addZoneEqually(zone);
 		else
 			levels[zone.first] = 0;
 	}
@@ -541,8 +554,8 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const
 	prescaler = sqrt((WH)/(sum(n^2)*pi))
 	*/
 
-	std::vector<float> prescaler = { 0, 0 };
-	for (int i = 0; i < 2; i++)
+	std::map<int, float> prescaler;
+	for (int i = 0; i < mapLevels; i++)
 		prescaler[i] = std::sqrt((width * height) / (totalSize[i] * PI_CONSTANT));
 	mapSize = static_cast<float>(sqrt(width * height));
 	for(const auto & zone : zones)

+ 1 - 1
lib/rmg/CZonePlacer.h

@@ -50,7 +50,7 @@ public:
 	const TDistanceMap & getDistanceMap();
 	
 private:
-	void prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const bool underground, vstd::RNG * rand);
+	void prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const int mapLevels, vstd::RNG * rand);
 	void attractConnectedZones(TZoneMap & zones, TForceVector & forces, TDistanceVector & distances) const;
 	void separateOverlappingZones(TZoneMap &zones, TForceVector &forces, TDistanceVector &overlaps);
 	void moveOneZone(TZoneMap & zones, TForceVector & totalForces, TDistanceVector & distances, TDistanceVector & overlaps);

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -48,8 +48,9 @@ enum class ESerializationVersion : int32_t
 	UNIVERSITY_CONFIG, // town university is configurable
 	CAMPAIGN_BONUSES, // new format for scenario bonuses in campaigns
 	BONUS_HIDDEN, // hidden bonus
+	MORE_MAP_LAYERS, // more map layers
 
-	CURRENT = BONUS_HIDDEN,
+	CURRENT = MORE_MAP_LAYERS,
 };
 
 static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!");

+ 2 - 2
mapeditor/mainwindow.cpp

@@ -387,7 +387,7 @@ void MainWindow::initializeMap(bool isNew)
 	ui->actionMapSettings->setEnabled(true);
 	ui->actionPlayers_settings->setEnabled(true);
 	ui->actionTranslations->setEnabled(true);
-	ui->actionLevel->setEnabled(controller.map()->twoLevel);
+	ui->actionLevel->setEnabled(controller.map()->mapLevels == 2); // TODO: multilevel support
 	
 	//set minimal players count
 	if(isNew)
@@ -968,7 +968,7 @@ void MainWindow::loadObjectsTree()
 
 void MainWindow::on_actionLevel_triggered()
 {
-	if(controller.map() && controller.map()->twoLevel)
+	if(controller.map() && controller.map()->mapLevels == 2)  // TODO: multilevel support
 	{
 		mapLevel = mapLevel ? 0 : 1;
 		ui->mapView->setScene(controller.scene(mapLevel));

+ 1 - 1
mapeditor/mapcontroller.cpp

@@ -260,7 +260,7 @@ void MapController::sceneForceUpdate()
 {
 	_scenes[0]->updateViews();
 	_miniscenes[0]->updateViews();
-	if(_map->twoLevel)
+	if(_map->mapLevels == 2)  // TODO: multilevel support
 	{
 		_scenes[1]->updateViews();
 		_miniscenes[1]->updateViews();

+ 1 - 1
mapeditor/maphandler.cpp

@@ -256,7 +256,7 @@ void MapHandler::initObjectRects()
 	if(!map)
 		return;
 	
-	tileObjects.resize(map->width * map->height * (map->twoLevel ? 2 : 1));
+	tileObjects.resize(map->width * map->height * map->mapLevels);
 	
 	//initializing objects / rects
 	for(const auto & elem : map->objects)

+ 2 - 2
mapeditor/scenelayer.cpp

@@ -93,7 +93,7 @@ void PassabilityLayer::update()
 	pixmap.reset(new QPixmap(map->width * 32, map->height * 32));
 	pixmap->fill(Qt::transparent);
 	
-	if(scene->level == 0 || map->twoLevel)
+	if(scene->level == 0 || map->mapLevels == 2) // TODO: multilevel support
 	{
 		QPainter painter(pixmap.get());
 		for(int j = 0; j < map->height; ++j)
@@ -121,7 +121,7 @@ void ObjectPickerLayer::highlight(std::function<bool(const CGObjectInstance *)>
 	if(!map)
 		return;
 	
-	if(scene->level == 0 || map->twoLevel)
+	if(scene->level == 0 || map->mapLevels == 2) // TODO: multilevel support
 	{
 		for(int j = 0; j < map->height; ++j)
 		{

+ 4 - 4
mapeditor/windownewmap.cpp

@@ -78,7 +78,7 @@ WindowNewMap::WindowNewMap(QWidget *parent) :
 		mapGenOptions.setWidth(width ? width : 1);
 		mapGenOptions.setHeight(height ? height : 1);
 		bool twoLevel = ui->twoLevelCheck->isChecked();
-		mapGenOptions.setHasTwoLevels(twoLevel);
+		mapGenOptions.setLevels(twoLevel ? 2 : 1);  // TODO: multilevel support
 
 		updateTemplateList();
 	}
@@ -123,7 +123,7 @@ bool WindowNewMap::loadUserSettings()
 			}
 		}
 
-		ui->twoLevelCheck->setChecked(mapGenOptions.getHasTwoLevels());
+		ui->twoLevelCheck->setChecked(mapGenOptions.getLevels() == 2); // TODO: multilevel support
 
 		ui->humanCombo->setCurrentIndex(mapGenOptions.getHumanOrCpuPlayerCount());
 		ui->cpuCombo->setCurrentIndex(mapGenOptions.getCompOnlyPlayerCount());
@@ -213,7 +213,7 @@ std::unique_ptr<CMap> generateEmptyMap(CMapGenOptions & options)
 	map->creationDateTime = std::time(nullptr);
 	map->width = options.getWidth();
 	map->height = options.getHeight();
-	map->twoLevel = options.getHasTwoLevels();
+	map->mapLevels = options.getLevels();
 	
 	map->initTerrain();
 	map->getEditManager()->clearTerrain(&CRandomGenerator::getDefault());
@@ -331,7 +331,7 @@ void WindowNewMap::on_sizeCombo_activated(int index)
 void WindowNewMap::on_twoLevelCheck_stateChanged(int arg1)
 {
 	bool twoLevel = ui->twoLevelCheck->isChecked();
-	mapGenOptions.setHasTwoLevels(twoLevel);
+	mapGenOptions.setLevels(twoLevel ? 2 : 1); // TODO: multilevel support
 	updateTemplateList();
 }
 

+ 1 - 1
test/map/CMapFormatTest.cpp

@@ -52,7 +52,7 @@ TEST(MapFormat, DISABLED_Random)
 
 	opt.setHeight(CMapHeader::MAP_SIZE_MIDDLE);
 	opt.setWidth(CMapHeader::MAP_SIZE_MIDDLE);
-	opt.setHasTwoLevels(true);
+	opt.setLevels(2);
 	opt.setHumanOrCpuPlayerCount(4);
 
 	opt.setPlayerTypeForStandardPlayer(PlayerColor(0), EPlayerType::HUMAN);

+ 1 - 1
test/map/MapComparer.cpp

@@ -151,7 +151,7 @@ void MapComparer::compareHeader()
 	//map size parameters are vital for further checks
 	VCMI_REQUIRE_FIELD_EQUAL_P(height);
 	VCMI_REQUIRE_FIELD_EQUAL_P(width);
-	VCMI_REQUIRE_FIELD_EQUAL_P(twoLevel);
+	VCMI_REQUIRE_FIELD_EQUAL_P(mapLevels);
 
 	VCMI_CHECK_FIELD_EQUAL_P(name);
 	VCMI_CHECK_FIELD_EQUAL_P(description);