Browse Source

Merge remote-tracking branch 'origin/develop' into water_prison_tavern

Tomasz Zieliński 2 years ago
parent
commit
f1356dd5bf
56 changed files with 881 additions and 410 deletions
  1. BIN
      Mods/vcmi/Sprites/battle/rangeHighlights/green/empty.png
  2. BIN
      Mods/vcmi/Sprites/battle/rangeHighlights/green/fullHex.png
  3. BIN
      Mods/vcmi/Sprites/battle/rangeHighlights/green/left.png
  4. BIN
      Mods/vcmi/Sprites/battle/rangeHighlights/green/leftHalf.png
  5. BIN
      Mods/vcmi/Sprites/battle/rangeHighlights/green/top.png
  6. BIN
      Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeft.png
  7. BIN
      Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftCorner.png
  8. BIN
      Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png
  9. 46 0
      Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json
  10. 2 0
      Mods/vcmi/config/vcmi/english.json
  11. 10 18
      Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON
  12. 230 0
      client/battle/BattleFieldController.cpp
  13. 18 0
      client/battle/BattleFieldController.h
  14. 5 1
      client/battle/BattleObstacleController.cpp
  15. 2 2
      client/battle/BattleRenderer.h
  16. 1 1
      client/battle/BattleSiegeController.cpp
  17. 41 63
      client/render/CDefFile.cpp
  18. 1 0
      client/render/IImage.h
  19. 6 0
      client/renderSDL/SDLImage.cpp
  20. 1 0
      client/renderSDL/SDLImage.h
  21. 2 2
      client/widgets/CArtifactsOfHeroBase.cpp
  22. 15 0
      client/windows/settings/BattleOptionsTab.cpp
  23. 1 0
      client/windows/settings/BattleOptionsTab.h
  24. 1 0
      config/objects/generic.json
  25. 28 0
      config/objects/moddables.json
  26. 2 0
      config/objects/rewardableBonusing.json
  27. 36 169
      config/obstacles.json
  28. 12 4
      config/schemas/obstacle.json
  29. 5 1
      config/schemas/settings.json
  30. 13 5
      config/widgets/settings/battleOptionsTab.json
  31. 4 4
      launcher/settingsView/csettingsview_moc.cpp
  32. 4 4
      launcher/settingsView/csettingsview_moc.h
  33. 12 1
      lib/CArtHandler.cpp
  34. 4 3
      lib/CArtHandler.h
  35. 3 2
      lib/NetPacksLib.cpp
  36. 1 0
      lib/ObstacleHandler.cpp
  37. 5 7
      lib/ObstacleHandler.h
  38. 18 0
      lib/battle/CUnitState.cpp
  39. 1 0
      lib/battle/CUnitState.h
  40. 0 1
      lib/mapObjects/CGHeroInstance.cpp
  41. 18 4
      lib/rmg/RmgArea.cpp
  42. 1 1
      lib/rmg/RmgArea.h
  43. 26 3
      lib/rmg/RmgObject.cpp
  44. 4 0
      lib/rmg/RmgObject.h
  45. 28 2
      lib/rmg/Zone.cpp
  46. 58 3
      lib/rmg/modificators/ObjectManager.cpp
  47. 58 10
      lib/rmg/modificators/ObstaclePlacer.cpp
  48. 4 1
      lib/rmg/modificators/RiverPlacer.cpp
  49. 9 0
      lib/rmg/modificators/RoadPlacer.cpp
  50. 1 0
      lib/rmg/modificators/RoadPlacer.h
  51. 0 2
      lib/rmg/modificators/RockFiller.cpp
  52. 11 1
      lib/rmg/modificators/RockPlacer.cpp
  53. 7 4
      lib/rmg/modificators/TownPlacer.cpp
  54. 120 88
      lib/rmg/modificators/TreasurePlacer.cpp
  55. 1 1
      lib/rmg/modificators/TreasurePlacer.h
  56. 5 2
      lib/rmg/modificators/WaterProxy.cpp

BIN
Mods/vcmi/Sprites/battle/rangeHighlights/green/empty.png


BIN
Mods/vcmi/Sprites/battle/rangeHighlights/green/fullHex.png


BIN
Mods/vcmi/Sprites/battle/rangeHighlights/green/left.png


BIN
Mods/vcmi/Sprites/battle/rangeHighlights/green/leftHalf.png


BIN
Mods/vcmi/Sprites/battle/rangeHighlights/green/top.png


BIN
Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeft.png


BIN
Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftCorner.png


BIN
Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png


+ 46 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json

@@ -0,0 +1,46 @@
+{
+	"basepath" : "battle/rangeHighlights/green/",
+	"images" :
+	[
+		{ "frame" : 00, "file" : "empty.png"},						// 000001 -> 00 empty frame
+			
+		// load single edges
+		{ "frame" : 01, "file" : "topLeft.png"},                    //000001 -> 01 topLeft
+		{ "frame" : 02, "file" : "topLeft.png"},                    //000010 -> 02 topRight
+		{ "frame" : 03, "file" : "left.png"},                       //000100 -> 04 right
+		{ "frame" : 04, "file" : "topLeft.png"},                    //001000 -> 08 bottomRight
+		{ "frame" : 05, "file" : "topLeft.png"},                    //010000 -> 16 bottomLeft
+		{ "frame" : 06, "file" : "left.png"},                       //100000 -> 32 left
+		
+		// load double edges
+		{ "frame" : 07, "file" : "top.png"},                        //000011 -> 03 top
+		{ "frame" : 08, "file" : "top.png"},                        //011000 -> 24 bottom
+		{ "frame" : 09, "file" : "topLeftHalfCorner.png"},          //000110 -> 06 topRightHalfCorner
+		{ "frame" : 10, "file" : "topLeftHalfCorner.png"},          //001100 -> 12 bottomRightHalfCorner
+		{ "frame" : 11, "file" : "topLeftHalfCorner.png"},          //110000 -> 48 bottomLeftHalfCorner
+		{ "frame" : 12, "file" : "topLeftHalfCorner.png"},          //100001 -> 33 topLeftHalfCorner
+		
+		// load halves
+		{ "frame" : 13, "file" : "leftHalf.png"},                   //001110 -> 14 rightHalf
+		{ "frame" : 14, "file" : "leftHalf.png"},                   //110001 -> 49 leftHalf
+		
+		// load corners
+		{ "frame" : 15, "file" : "topLeftCorner.png"},              //000111 -> 07 topRightCorner
+		{ "frame" : 16, "file" : "topLeftCorner.png"},              //011100 -> 28 bottomRightCorner
+		{ "frame" : 17, "file" : "topLeftCorner.png"},              //111000 -> 56 bottomLeftCorner
+		{ "frame" : 18, "file" : "topLeftCorner.png"}               //100011 -> 35 topLeftCorner
+	]
+}
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 2 - 0
Mods/vcmi/config/vcmi/english.json

@@ -104,6 +104,8 @@
 	"vcmi.battleOptions.animationsSpeed6.help": "Set animation speed to instantaneous",
 	"vcmi.battleOptions.movementHighlightOnHover.hover": "Movement Highlight on Hover",
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{Movement Highlight on Hover}\n\nHighlight unit's movement range when you hover over it.",
+	"vcmi.battleOptions.rangedFullDamageLimitHighlightOnHover.hover": "Ranged Full Damage Limit Highlight",
+	"vcmi.battleOptions.rangedFullDamageLimitHighlightOnHover.help": "{Ranged Full Damage Limit on Hover}\n\nHighlight ranged unit's full damage range limit when you hover over it.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle",
 	"vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to start battle immediately",

+ 10 - 18
Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON

@@ -18,8 +18,7 @@
 				"treasure" :
 				[
 					{ "min" : 300, "max" : 3000, "density" : 12 },
-					{ "min" : 5000, "max" : 9000, "density" : 6 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
+					{ "min" : 5000, "max" : 9000, "density" : 6 }
 				]
 			},
 			"2" :
@@ -34,8 +33,8 @@
 				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
 				"treasure" :
 				[
-					{ "min" : 5000, "max" : 7000, "density" : 30 },
 					{ "min" : 10000, "max" : 15000, "density" : 1 },
+					{ "min" : 5000, "max" : 7000, "density" : 30 },
 					{ "min" : 300, "max" : 3000, "density" : 5 }
 				]
 			},
@@ -51,8 +50,8 @@
 				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 },
 				"treasure" :
 				[
-					{ "min" : 12000, "max" : 16000, "density" : 5 },
 					{ "min" : 20000, "max" : 21000, "density" : 6 },
+					{ "min" : 12000, "max" : 16000, "density" : 5 },
 					{ "min" : 300, "max" : 3000, "density" : 5 }
 				]
 			},
@@ -69,8 +68,7 @@
 				"treasure" :
 				[
 					{ "min" : 25000, "max" : 30000, "density" : 10 },
-					{ "min" : 300, "max" : 3000, "density" : 10 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
+					{ "min" : 300, "max" : 3000, "density" : 10 }
 				]
 			},
 			"5" :
@@ -85,9 +83,7 @@
 				"mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 4 },
 				"treasure" :
 				[
-					{ "min" : 30000, "max" : 90000, "density" : 25 },
-					{ "min" : 0, "max" : 0, "density" : 1 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
+					{ "min" : 30000, "max" : 90000, "density" : 25 }
 				]
 			},
 			"6" :
@@ -167,8 +163,7 @@
 				"treasure" :
 				[
 					{ "min" : 300, "max" : 3000, "density" : 12 },
-					{ "min" : 5000, "max" : 9000, "density" : 6 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
+					{ "min" : 5000, "max" : 9000, "density" : 6 }
 				]
 			},
 			"2" :
@@ -183,8 +178,8 @@
 				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
 				"treasure" :
 				[
-					{ "min" : 5000, "max" : 7000, "density" : 10 },
 					{ "min" : 10000, "max" : 15000, "density" : 1 },
+					{ "min" : 5000, "max" : 7000, "density" : 10 },
 					{ "min" : 300, "max" : 3000, "density" : 5 }
 				]
 			},
@@ -200,8 +195,8 @@
 				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 },
 				"treasure" :
 				[
-					{ "min" : 12000, "max" : 16000, "density" : 5 },
 					{ "min" : 20000, "max" : 21000, "density" : 6 },
+					{ "min" : 12000, "max" : 16000, "density" : 5 },
 					{ "min" : 300, "max" : 3000, "density" : 5 }
 				]
 			},
@@ -218,8 +213,7 @@
 				"treasure" :
 				[
 					{ "min" : 25000, "max" : 30000, "density" : 10 },
-					{ "min" : 300, "max" : 3000, "density" : 10 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
+					{ "min" : 300, "max" : 3000, "density" : 10 }
 				]
 			},
 			"5" :
@@ -233,9 +227,7 @@
 				"mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 10 },
 				"treasure" :
 				[
-					{ "min" : 30000, "max" : 90000, "density" : 25 },
-					{ "min" : 0, "max" : 0, "density" : 1 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
+					{ "min" : 30000, "max" : 90000, "density" : 25 }
 				]
 			},
 			"6" :

+ 230 - 0
client/battle/BattleFieldController.cpp

@@ -29,6 +29,7 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../adventureMap/CInGameConsole.h"
+#include "../client/render/CAnimation.h"
 
 #include "../../CCallback.h"
 #include "../../lib/BattleFieldHandler.h"
@@ -36,6 +37,83 @@
 #include "../../lib/CStack.h"
 #include "../../lib/spells/ISpellMechanics.h"
 
+namespace HexMasks
+{
+	// mask definitions that has set to 1 the edges present in the hex edges highlight image
+	/*
+	    /\
+	   0  1
+	  /    \
+	 |      |
+	 5      2
+	 |      |
+	  \    /
+	   4  3
+	    \/
+	*/
+	enum HexEdgeMasks {
+		empty                 = 0b000000, // empty used when wanting to keep indexes the same but no highlight should be displayed
+		topLeft               = 0b000001,
+		topRight              = 0b000010,
+		right                 = 0b000100,
+		bottomRight           = 0b001000,
+		bottomLeft            = 0b010000,
+		left                  = 0b100000,
+						  
+		top                   = 0b000011,
+		bottom                = 0b011000,
+		topRightHalfCorner    = 0b000110,
+		bottomRightHalfCorner = 0b001100,
+		bottomLeftHalfCorner  = 0b110000,
+		topLeftHalfCorner     = 0b100001,
+
+		rightTopAndBottom     = 0b001010, // special case, right half can be drawn instead of only top and bottom
+		leftTopAndBottom      = 0b010001, // special case, left half can be drawn instead of only top and bottom
+						  
+		rightHalf             = 0b001110,
+		leftHalf              = 0b110001,
+						  
+		topRightCorner        = 0b000111,
+		bottomRightCorner     = 0b011100,
+		bottomLeftCorner      = 0b111000,
+		topLeftCorner         = 0b100011
+	};
+}
+
+std::map<int, int> hexEdgeMaskToFrameIndex;
+
+// Maps HexEdgesMask to "Frame" indexes for range highligt images
+void initializeHexEdgeMaskToFrameIndex()
+{
+	hexEdgeMaskToFrameIndex[HexMasks::empty] = 0;
+
+    hexEdgeMaskToFrameIndex[HexMasks::topLeft] = 1;
+    hexEdgeMaskToFrameIndex[HexMasks::topRight] = 2;
+    hexEdgeMaskToFrameIndex[HexMasks::right] = 3;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomRight] = 4;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomLeft] = 5;
+    hexEdgeMaskToFrameIndex[HexMasks::left] = 6;
+
+    hexEdgeMaskToFrameIndex[HexMasks::top] = 7;
+    hexEdgeMaskToFrameIndex[HexMasks::bottom] = 8;
+
+    hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner] = 9;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner] = 10;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner] = 11;
+    hexEdgeMaskToFrameIndex[HexMasks::topLeftHalfCorner] = 12;
+
+    hexEdgeMaskToFrameIndex[HexMasks::rightTopAndBottom] = 13;
+    hexEdgeMaskToFrameIndex[HexMasks::leftTopAndBottom] = 14;
+	
+    hexEdgeMaskToFrameIndex[HexMasks::rightHalf] = 13;
+    hexEdgeMaskToFrameIndex[HexMasks::leftHalf] = 14;
+
+    hexEdgeMaskToFrameIndex[HexMasks::topRightCorner] = 15;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner] = 16;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner] = 17;
+    hexEdgeMaskToFrameIndex[HexMasks::topLeftCorner] = 18;
+}
+
 BattleFieldController::BattleFieldController(BattleInterface & owner):
 	owner(owner)
 {
@@ -50,6 +128,12 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	attackCursors = std::make_shared<CAnimation>("CRCOMBAT");
 	attackCursors->preload();
 
+	rangedFullDamageLimitImages = std::make_unique<CAnimation>("battle/rangeHighlights/rangeHighlightsGreen.json");
+	rangedFullDamageLimitImages->preload();
+
+	initializeHexEdgeMaskToFrameIndex();
+	flipRangedFullDamageLimitImagesIntoPositions();
+
 	if(!owner.siegeController)
 	{
 		auto bfieldType = owner.curInt->cb->battleGetBattlefieldType();
@@ -361,6 +445,132 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget(
 	return {};
 }
 
+std::vector<BattleHex> BattleFieldController::getRangedFullDamageHexes()
+{
+	std::vector<BattleHex> rangedFullDamageHexes; // used for return
+
+	// if not a hovered arcer unit -> return
+	auto hoveredHex = getHoveredHex();
+	const CStack * hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+
+	if (!settings["battle"]["rangedFullDamageLimitHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
+		return rangedFullDamageHexes;
+
+	if(!(hoveredStack && hoveredStack->isShooter()))
+		return rangedFullDamageHexes;
+
+	auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance();
+
+	// get only battlefield hexes that are in full range damage distance
+	std::set<BattleHex> fullRangeLimit; 
+	for(auto i = 0; i < GameConstants::BFIELD_SIZE; i++)
+	{
+		BattleHex hex(i);
+		if(hex.isAvailable() && BattleHex::getDistance(hoveredHex, hex) <= rangedFullDamageDistance)
+			rangedFullDamageHexes.push_back(hex);
+	}
+
+	return rangedFullDamageHexes;
+}
+
+std::vector<BattleHex> BattleFieldController::getRangedFullDamageLimitHexes(std::vector<BattleHex> rangedFullDamageHexes)
+{
+	std::vector<BattleHex> rangedFullDamageLimitHexes; // used for return
+
+	// if not a hovered arcer unit -> return
+	auto hoveredHex = getHoveredHex();
+	const CStack * hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+
+	if(!(hoveredStack && hoveredStack->isShooter()))
+		return rangedFullDamageLimitHexes;
+
+	auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance();
+
+	// from ranged full damage hexes get only the ones at the limit
+	for(auto & hex : rangedFullDamageHexes)
+	{
+		if(BattleHex::getDistance(hoveredHex, hex) == rangedFullDamageDistance)
+			rangedFullDamageLimitHexes.push_back(hex);
+	}
+
+	return rangedFullDamageLimitHexes;
+}
+
+std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> rangedFullDamageHexes, std::vector<BattleHex> rangedFullDamageLimitHexes)
+{
+	std::vector<std::vector<BattleHex::EDir>> output;
+
+	if(rangedFullDamageHexes.empty())
+		return output;
+
+	for(auto & hex : rangedFullDamageLimitHexes)
+	{
+		// get all neighbours and their directions
+		
+		auto neighbouringTiles = hex.allNeighbouringTiles();
+
+		std::vector<BattleHex::EDir> outsideNeighbourDirections;
+
+		// for each neighbour add to output only the valid ones and only that are not found in rangedFullDamageHexes
+		for(auto direction = 0; direction < 6; direction++)
+		{
+			if(!neighbouringTiles[direction].isAvailable())
+				continue;
+
+			auto it = std::find(rangedFullDamageHexes.begin(), rangedFullDamageHexes.end(), neighbouringTiles[direction]);
+
+			if(it == rangedFullDamageHexes.end())
+				outsideNeighbourDirections.push_back(BattleHex::EDir(direction)); // push direction
+		}
+
+		output.push_back(outsideNeighbourDirections);
+	}
+
+	return output;
+}
+
+std::vector<std::shared_ptr<IImage>> BattleFieldController::calculateRangedFullDamageHighlightImages(std::vector<std::vector<BattleHex::EDir>> rangedFullDamageLimitHexesNeighbourDirections)
+{
+	std::vector<std::shared_ptr<IImage>> output; // if no image is to be shown an empty image is still added to help with traverssing the range
+
+	if(rangedFullDamageLimitHexesNeighbourDirections.empty())
+		return output;
+
+	for(auto & directions : rangedFullDamageLimitHexesNeighbourDirections)
+	{
+		std::bitset<6> mask;
+		
+		// convert directions to mask
+		for(auto direction : directions)
+			mask.set(direction);
+
+		uint8_t imageKey = static_cast<uint8_t>(mask.to_ulong());
+		output.push_back(rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[imageKey]));
+	}
+
+	return output;
+}
+
+void BattleFieldController::flipRangedFullDamageLimitImagesIntoPositions()
+{
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRight])->verticalFlip();
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::right])->verticalFlip();
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRight])->doubleFlip();
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeft])->horizontalFlip();
+
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottom])->horizontalFlip();
+
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner])->verticalFlip();
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner])->doubleFlip();
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner])->horizontalFlip();
+
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::rightHalf])->verticalFlip();
+
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightCorner])->verticalFlip();
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner])->doubleFlip();
+	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner])->horizontalFlip();
+}
+
 void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 {
 	std::set<BattleHex> hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack();
@@ -370,6 +580,12 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 	if(getHoveredHex() == BattleHex::INVALID)
 		return;
 
+	// calculate array with highlight images for ranged full damage limit
+	std::vector<BattleHex> rangedFullDamageHexes = getRangedFullDamageHexes();
+	std::vector<BattleHex> rangedFullDamageLimitHexes = getRangedFullDamageLimitHexes(rangedFullDamageHexes);
+	std::vector<std::vector<BattleHex::EDir>> rangedFullDamageLimitHexesNeighbourDirections = getOutsideNeighbourDirectionsForLimitHexes(rangedFullDamageHexes, rangedFullDamageLimitHexes);
+	std::vector<std::shared_ptr<IImage>> rangedFullDamageLimitHexesHighligts = calculateRangedFullDamageHighlightImages(rangedFullDamageLimitHexesNeighbourDirections);
+
 	auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes;
 
 	for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex)
@@ -377,6 +593,16 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 		bool stackMovement = hoveredStackMovementRangeHexes.count(hex);
 		bool mouse = hoveredMouseHexes.count(hex);
 
+		// calculate if hex is Ranged Full Damage Limit and its position in highlight array
+		bool isRangedFullDamageLimit = false;
+		int hexIndexInRangedFullDamageLimit = 0;
+		if(!rangedFullDamageLimitHexes.empty())
+		{
+			auto pos = std::find(rangedFullDamageLimitHexes.begin(), rangedFullDamageLimitHexes.end(), hex);
+			hexIndexInRangedFullDamageLimit = std::distance(rangedFullDamageLimitHexes.begin(), pos);
+			isRangedFullDamageLimit = pos != rangedFullDamageLimitHexes.end();
+		}
+
 		if(stackMovement && mouse) // area where hovered stackMovement can move shown with highlight. Because also affected by mouse cursor, shade as well
 		{
 			showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false);
@@ -390,6 +616,10 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 		{
 			showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false);
 		}
+		if(isRangedFullDamageLimit)
+		{
+			showHighlightedHex(canvas, rangedFullDamageLimitHexesHighligts[hexIndexInRangedFullDamageLimit], hex, false);
+		}
 	}
 }
 

+ 18 - 0
client/battle/BattleFieldController.h

@@ -33,6 +33,7 @@ class BattleFieldController : public CIntObject
 	std::shared_ptr<IImage> cellUnitMovementHighlight;
 	std::shared_ptr<IImage> cellUnitMaxMovementHighlight;
 	std::shared_ptr<IImage> cellShade;
+	std::unique_ptr<CAnimation> rangedFullDamageLimitImages;
 
 	std::shared_ptr<CAnimation> attackCursors;
 
@@ -58,6 +59,23 @@ class BattleFieldController : public CIntObject
 	std::set<BattleHex> getHighlightedHexesForSpellRange();
 	std::set<BattleHex> getHighlightedHexesForMovementTarget();
 
+	/// get all hexes where a ranged unit can do full damage
+	std::vector<BattleHex> getRangedFullDamageHexes();
+
+	/// get only hexes at the limit of a ranged unit's full damage range
+	std::vector<BattleHex> getRangedFullDamageLimitHexes(std::vector<BattleHex> rangedFullDamageHexes);
+
+	/// get an array that has for each hex in range, an aray with all directions where an ouside neighbour hex exists
+	std::vector<std::vector<BattleHex::EDir>> getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> rangedFullDamageHexes, std::vector<BattleHex> rangedFullDamageLimitHexes);
+
+	/// calculates what image to use as range limit, depending on the direction of neighbors
+	/// a mask is used internally to mark the directions of all neighbours
+	/// based on this mask the corresponding image is selected
+	std::vector<std::shared_ptr<IImage>> calculateRangedFullDamageHighlightImages(std::vector<std::vector<BattleHex::EDir>> fullRangeLimitHexesNeighbourDirections);
+
+	/// to reduce the number of source images used, some images will be used as flipped versions of preloaded ones
+	void flipRangedFullDamageLimitImagesIntoPositions();
+
 	void showBackground(Canvas & canvas);
 	void showBackgroundImage(Canvas & canvas);
 	void showBackgroundImageWithHexes(Canvas & canvas);

+ 5 - 1
client/battle/BattleObstacleController.cpp

@@ -148,7 +148,11 @@ void BattleObstacleController::collectRenderableObjects(BattleRenderer & rendere
 		if (obstacle->obstacleType == CObstacleInstance::MOAT)
 			continue;
 
-		renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){
+		bool isForeground = obstacle->obstacleType == CObstacleInstance::USUAL && obstacle->getInfo().isForegroundObstacle;
+
+		auto layer = isForeground ? EBattleFieldLayer::OBSTACLES_FG : EBattleFieldLayer::OBSTACLES_BG;
+
+		renderer.insert(layer, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){
 			auto img = getObstacleImage(*obstacle);
 			if(img)
 			{

+ 2 - 2
client/battle/BattleRenderer.h

@@ -16,12 +16,12 @@ class BattleInterface;
 
 enum class EBattleFieldLayer {
 					   // confirmed ordering requirements:
-	OBSTACLES     = 0,
+	OBSTACLES_BG  = 0,
 	CORPSES       = 0,
 	WALLS         = 1,
 	HEROES        = 2,
 	STACKS        = 2, // after corpses, obstacles, walls
-	BATTLEMENTS   = 3, // after stacks
+	OBSTACLES_FG  = 3, // after stacks
 	STACK_AMOUNTS = 3, // after stacks, obstacles, corpses
 	EFFECTS       = 4, // after obstacles, battlements
 };

+ 1 - 1
client/battle/BattleSiegeController.cpp

@@ -307,7 +307,7 @@ void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer)
 			renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
 				owner.stacksController->showStack(canvas, getTurretStack(wallPiece));
 			});
-			renderer.insert( EBattleFieldLayer::BATTLEMENTS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
+			renderer.insert( EBattleFieldLayer::OBSTACLES_FG, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
 				showWallPiece(canvas, wallPiece);
 			});
 		}

+ 41 - 63
client/render/CDefFile.cpp

@@ -82,9 +82,19 @@ static CFileCache animationCache;
  *  DefFile, class used for def loading                                  *
  *************************************************************************/
 
-bool operator== (const SDL_Color & lhs, const SDL_Color & rhs)
+static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs)
 {
-	return (lhs.a == rhs.a) && (lhs.b == rhs.b) &&(lhs.g == rhs.g) &&(lhs.r == rhs.r);
+	// it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow
+	// exact logic is not clear and requires extensive testing with image editing
+	// potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component
+	static const int threshold = 8;
+
+	int diffR = static_cast<int>(lhs.r) - rhs.r;
+	int diffG = static_cast<int>(lhs.g) - rhs.g;
+	int diffB = static_cast<int>(lhs.b) - rhs.b;
+	int diffA = static_cast<int>(lhs.a) - rhs.a;
+
+	return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold;
 }
 
 CDefFile::CDefFile(std::string Name):
@@ -92,23 +102,34 @@ CDefFile::CDefFile(std::string Name):
 	palette(nullptr)
 {
 	//First 8 colors in def palette used for transparency
-	static SDL_Color H3Palette[8] =
-	{
-		{   0,   0,   0,   0},// transparency                  ( used in most images )
-		{   0,   0,   0,  64},// shadow border                 ( used in battle, adventure map def's )
-		{   0,   0,   0,  64},// shadow border                 ( used in fog-of-war def's )
-		{   0,   0,   0, 128},// shadow body                   ( used in fog-of-war def's )
-		{   0,   0,   0, 128},// shadow body                   ( used in battle, adventure map def's )
-		{   0,   0,   0,   0},// selection                     ( used in battle def's )
-		{   0,   0,   0, 128},// shadow body   below selection ( used in battle def's )
-		{   0,   0,   0,  64} // shadow border below selection ( used in battle def's )
+	static const SDL_Color sourcePalette[8] = {
+		{0,   255, 255, SDL_ALPHA_OPAQUE},
+		{255, 150, 255, SDL_ALPHA_OPAQUE},
+		{255, 100, 255, SDL_ALPHA_OPAQUE},
+		{255, 50,  255, SDL_ALPHA_OPAQUE},
+		{255, 0,   255, SDL_ALPHA_OPAQUE},
+		{255, 255, 0,   SDL_ALPHA_OPAQUE},
+		{180, 0,   255, SDL_ALPHA_OPAQUE},
+		{0,   255, 0,   SDL_ALPHA_OPAQUE}
+	};
+
+	static const SDL_Color targetPalette[8] = {
+		{0, 0, 0, 0  }, // transparency                  ( used in most images )
+		{0, 0, 0, 64 }, // shadow border                 ( used in battle, adventure map def's )
+		{0, 0, 0, 64 }, // shadow border                 ( used in fog-of-war def's )
+		{0, 0, 0, 128}, // shadow body                   ( used in fog-of-war def's )
+		{0, 0, 0, 128}, // shadow body                   ( used in battle, adventure map def's )
+		{0, 0, 0, 0  }, // selection / owner flag        ( used in battle, adventure map def's )
+		{0, 0, 0, 128}, // shadow body   below selection ( used in battle def's )
+		{0, 0, 0, 64 }  // shadow border below selection ( used in battle def's )
 	};
+
 	data = animationCache.getCachedFile(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION));
 
 	palette = std::unique_ptr<SDL_Color[]>(new SDL_Color[256]);
 	int it = 0;
 
-	ui32 type = read_le_u32(data.get() + it);
+	//ui32 type = read_le_u32(data.get() + it);
 	it+=4;
 	//int width  = read_le_u32(data + it); it+=4;//not used
 	//int height = read_le_u32(data + it); it+=4;
@@ -124,59 +145,16 @@ CDefFile::CDefFile(std::string Name):
 		palette[i].a = SDL_ALPHA_OPAQUE;
 	}
 
-	switch(static_cast<DefType>(type))
+	// first color seems to be used unconditionally as 100% transparency
+	palette[0] = targetPalette[0];
+
+	// rest of special colors are used only if their RGB values are close to H3
+	for (uint32_t i = 1; i < 8; ++i)
 	{
-	case DefType::SPELL:
-		palette[0] = H3Palette[0];
-		break;
-	case DefType::SPRITE:
-	case DefType::SPRITE_FRAME:
-		for(ui32 i= 0; i<8; i++)
-			palette[i] = H3Palette[i];
-		break;
-	case DefType::CREATURE:
-		palette[0] = H3Palette[0];
-		palette[1] = H3Palette[1];
-		palette[4] = H3Palette[4];
-		palette[5] = H3Palette[5];
-		palette[6] = H3Palette[6];
-		palette[7] = H3Palette[7];
-		break;
-	case DefType::MAP_HERO:
-		palette[0] = H3Palette[0];
-		palette[1] = H3Palette[1];
-		palette[4] = H3Palette[4];
-		//5 = owner flag, handled separately
-		break;
-	case DefType::MAP:
-	case DefType::TERRAIN:
-		palette[0] = H3Palette[0];
-		palette[1] = H3Palette[1];
-		palette[2] = H3Palette[2];
-		palette[3] = H3Palette[3];
-		palette[4] = H3Palette[4];
-		break;
-	case DefType::CURSOR:
-		palette[0] = H3Palette[0];
-		break;
-	case DefType::INTERFACE:
-		palette[0] = H3Palette[0];
-		palette[1] = H3Palette[1];
-		palette[4] = H3Palette[4];
-		//player colors handled separately
-		//TODO: disallow colorizing other def types
-		break;
-	case DefType::BATTLE_HERO:
-		palette[0] = H3Palette[0];
-		palette[1] = H3Palette[1];
-		palette[4] = H3Palette[4];
-		break;
-	default:
-		logAnim->error("Unknown def type %d in %s", type, Name);
-		break;
+		if (colorsSimilar(sourcePalette[i], palette[i]))
+			palette[i] = targetPalette[i];
 	}
 
-
 	for (ui32 i=0; i<totalBlocks; i++)
 	{
 		size_t blockID = read_le_u32(data.get() + it);

+ 1 - 0
client/render/IImage.h

@@ -78,6 +78,7 @@ public:
 
 	virtual void horizontalFlip() = 0;
 	virtual void verticalFlip() = 0;
+	virtual void doubleFlip() = 0;
 
 	IImage();
 	virtual ~IImage();

+ 6 - 0
client/renderSDL/SDLImage.cpp

@@ -281,6 +281,12 @@ void SDLImage::verticalFlip()
 	surf = flipped;
 }
 
+void SDLImage::doubleFlip()
+{
+	horizontalFlip();
+	verticalFlip();
+}
+
 // Keep the original palette, in order to do color switching operation
 void SDLImage::savePalette()
 {

+ 1 - 0
client/renderSDL/SDLImage.h

@@ -64,6 +64,7 @@ public:
 
 	void horizontalFlip() override;
 	void verticalFlip() override;
+	void doubleFlip() override;
 
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;

+ 2 - 2
client/widgets/CArtifactsOfHeroBase.cpp

@@ -225,8 +225,8 @@ void CArtifactsOfHeroBase::updateWornSlots()
 
 void CArtifactsOfHeroBase::updateBackpackSlots()
 {
-	for(auto artPlace : backpack)
-		updateSlot(artPlace->slot);
+	if(curHero->artifactsInBackpack.size() <= backpack.size() && backpackPos != 0)
+		backpackPos = 0;
 	scrollBackpackForArtSet(0, *curHero);
 }
 

+ 15 - 0
client/windows/settings/BattleOptionsTab.cpp

@@ -39,6 +39,10 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner)
 	{
 		movementHighlightOnHoverChangedCallback(value, owner);
 	});
+	addCallback("rangedFullDamageLimitHighlightOnHoverChanged", [this, owner](bool value)
+	{
+		rangedFullDamageLimitHighlightOnHoverChangedCallback(value, owner);
+	});
 	addCallback("mouseShadowChanged", [this](bool value)
 	{
 		mouseShadowChangedCallback(value);
@@ -76,6 +80,9 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner)
 	std::shared_ptr<CToggleButton> movementHighlightOnHoverCheckbox = widget<CToggleButton>("movementHighlightOnHoverCheckbox");
 	movementHighlightOnHoverCheckbox->setSelected(settings["battle"]["movementHighlightOnHover"].Bool());
 
+	std::shared_ptr<CToggleButton> rangedFullDamageLimitHighlightOnHoverCheckbox = widget<CToggleButton>("rangedFullDamageLimitHighlightOnHoverCheckbox");
+	rangedFullDamageLimitHighlightOnHoverCheckbox->setSelected(settings["battle"]["rangedFullDamageLimitHighlightOnHover"].Bool());
+
 	std::shared_ptr<CToggleButton> mouseShadowCheckbox = widget<CToggleButton>("mouseShadowCheckbox");
 	mouseShadowCheckbox->setSelected(settings["battle"]["mouseShadow"].Bool());
 
@@ -152,6 +159,14 @@ void BattleOptionsTab::movementHighlightOnHoverChangedCallback(bool value, Battl
 		parentBattleInterface->redrawBattlefield();
 }
 
+void BattleOptionsTab::rangedFullDamageLimitHighlightOnHoverChangedCallback(bool value, BattleInterface * parentBattleInterface)
+{
+	Settings stackRange = settings.write["battle"]["rangedFullDamageLimitHighlightOnHover"];
+	stackRange->Bool() = value;
+	if(parentBattleInterface)
+		parentBattleInterface->redrawBattlefield();
+}
+
 void BattleOptionsTab::mouseShadowChangedCallback(bool value)
 {
 	Settings shadow = settings.write["battle"]["mouseShadow"];

+ 1 - 0
client/windows/settings/BattleOptionsTab.h

@@ -25,6 +25,7 @@ private:
 	void viewGridChangedCallback(bool value, BattleInterface * parentBattleInterface);
 	void movementShadowChangedCallback(bool value, BattleInterface * parentBattleInterface);
 	void movementHighlightOnHoverChangedCallback(bool value, BattleInterface * parentBattleInterface);
+	void rangedFullDamageLimitHighlightOnHoverChangedCallback(bool value, BattleInterface * parentBattleInterface);
 	void mouseShadowChangedCallback(bool value);
 	void animationSpeedChangedCallback(int value);
 	void showQueueChangedCallback(bool value, BattleInterface * parentBattleInterface);

+ 1 - 0
config/objects/generic.json

@@ -511,6 +511,7 @@
 				"index" : 0,
 				"aiValue" : 100,
 				"rmg" : {
+					"zoneLimit"	: 1,
 					"mapLimit"	: 32,
 					"value"		: 100,
 					"rarity"	: 20

+ 28 - 0
config/objects/moddables.json

@@ -50,6 +50,34 @@
 		}
 	},
 
+	"randomResource":
+	{
+		"index" :76,
+		"handler": "resource",
+		"base" : {
+			"base" : {
+				"visitableFrom" : [ "+++", "+-+", "+++" ],
+				"mask" : [ "VA" ]
+			}
+		},
+		"types" : {
+			"randomResource" : {
+				"index" : 0,
+				"rmg" : {
+					"value"		: 1500,
+					"rarity"	: 2000
+				},
+				"templates" :
+				{
+					"res" :
+					{
+						"animation" : "AVTrndm0.def"
+					}
+				}
+			}
+		}
+	},
+
 	// subtype: resource ID
 	"resource" : {
 		"index" :79,

+ 2 - 0
config/objects/rewardableBonusing.json

@@ -15,6 +15,7 @@
 				"index" : 0,
 				"aiValue" : 100,
 				"rmg" : {
+					"zoneLimit"	: 1,
 					"value"		: 100,
 					"rarity"	: 100
 				},
@@ -253,6 +254,7 @@
 				"index" : 0,
 				"aiValue" : 100,
 				"rmg" : {
+					"zoneLimit"	: 1,
 					"value"		: 100,
 					"rarity"	: 20
 				},

File diff suppressed because it is too large
+ 36 - 169
config/obstacles.json


+ 12 - 4
config/schemas/obstacle.json

@@ -3,7 +3,15 @@
 	"$schema" : "http://json-schema.org/draft-04/schema",
 	"title" : "VCMI obstacle format",
 	"description" : "Format used to define new obstacles in VCMI",
-	"required" : [ "animation" ],
+	"required" : [ "animation", "width", "height", "blockedTiles" ],
+	"anyOf" : [
+		{
+			"required" : [ "allowedTerrains" ]
+		},
+		{
+			"required" : [ "specialBattlefields" ]
+		}
+	],
 	"additionalProperties" : false,
 	"properties" : {
 		"allowedTerrains" : {
@@ -41,9 +49,9 @@
 				{ "format" : "imageFile" }
 			]
 		},
-		"unknown" : {
-			"type" : "number",
-			"description" : "Unknown field"
+		"foreground" : {
+			"type" : "boolean",
+			"description" : "If set to true, obstacle will appear in front of units or other battlefield objects"
 		}
 	}
 }

+ 5 - 1
config/schemas/settings.json

@@ -286,7 +286,7 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize" ],
+			"required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangedFullDamageLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize" ],
 			"properties" : {
 				"speedFactor" : {
 					"type" : "number",
@@ -308,6 +308,10 @@
 					"type" : "boolean",
 					"default" : true
 				},
+				"rangedFullDamageLimitHighlightOnHover" : {
+					"type" : "boolean",
+					"default" : false
+				},
 				"showQueue" : {
 					"type" : "boolean",
 					"default" : true

+ 13 - 5
config/widgets/settings/battleOptionsTab.json

@@ -7,13 +7,13 @@
 			"name": "lineCreatureInfo",
 			"type": "texture",
 			"image": "settingsWindow/lineHorizontal",
-			"rect": { "x" : 5, "y" : 229, "w": 365, "h": 3}
+			"rect": { "x" : 5, "y" : 289, "w": 365, "h": 3}
 		},
 		{
 			"name": "lineAnimationSpeed",
 			"type": "texture",
 			"image": "settingsWindow/lineHorizontal",
-			"rect": { "x" : 5, "y" : 319, "w": 365, "h": 3}
+			"rect": { "x" : 5, "y" : 349, "w": 365, "h": 3}
 		},
 		{
 			"type" : "labelTitle",
@@ -23,7 +23,7 @@
 		{
 			"type" : "labelTitle",
 			"text": "core.genrltxt.397", // Creature info
-			"position": {"x": 10, "y": 235}
+			"position": {"x": 10, "y": 265}
 		},
 /////////////////////////////////////// Right section - Auto-combat settings (NOT IMPLEMENTED)
 		{
@@ -69,7 +69,7 @@
 			"name": "creatureInfoLabels",
 			"type" : "verticalLayout",
 			"customType" : "labelDescription",
-			"position": {"x": 45, "y": 265},
+			"position": {"x": 45, "y": 295},
 			"items":
 			[
 				{
@@ -84,7 +84,7 @@
 			"name": "creatureInfoCheckboxes",
 			"type" : "verticalLayout",
 			"customType" : "checkboxFake",
-			"position": {"x": 10, "y": 263},
+			"position": {"x": 10, "y": 293},
 			"items":
 			[
 				{},
@@ -107,6 +107,9 @@
 				{
 					"text": "vcmi.battleOptions.movementHighlightOnHover.hover",
 				},
+				{
+					"text": "vcmi.battleOptions.rangedFullDamageLimitHighlightOnHover.hover",
+				},
 				{
 					"text": "core.genrltxt.406",
 				},
@@ -136,6 +139,11 @@
 					"help": "vcmi.battleOptions.movementHighlightOnHover",
 					"callback": "movementHighlightOnHoverChanged"
 				},
+				{
+					"name": "rangedFullDamageLimitHighlightOnHoverCheckbox",
+					"help": "vcmi.battleOptions.rangedFullDamageLimitHighlightOnHover",
+					"callback": "rangedFullDamageLimitHighlightOnHoverChanged"
+				},
 				{
 					"name": "mouseShadowCheckbox",
 					"help": "core.help.429",

+ 4 - 4
launcher/settingsView/csettingsview_moc.cpp

@@ -225,25 +225,25 @@ void CSettingsView::on_comboBoxDisplayIndex_currentIndexChanged(int index)
 	fillValidResolutionsForScreen(index);
 }
 
-void CSettingsView::on_comboBoxPlayerAI_currentIndexChanged(const QString & arg1)
+void CSettingsView::on_comboBoxPlayerAI_currentTextChanged(const QString & arg1)
 {
 	Settings node = settings.write["server"]["playerAI"];
 	node->String() = arg1.toUtf8().data();
 }
 
-void CSettingsView::on_comboBoxFriendlyAI_currentIndexChanged(const QString & arg1)
+void CSettingsView::on_comboBoxFriendlyAI_currentTextChanged(const QString & arg1)
 {
 	Settings node = settings.write["server"]["friendlyAI"];
 	node->String() = arg1.toUtf8().data();
 }
 
-void CSettingsView::on_comboBoxNeutralAI_currentIndexChanged(const QString & arg1)
+void CSettingsView::on_comboBoxNeutralAI_currentTextChanged(const QString & arg1)
 {
 	Settings node = settings.write["server"]["neutralAI"];
 	node->String() = arg1.toUtf8().data();
 }
 
-void CSettingsView::on_comboBoxEnemyAI_currentIndexChanged(const QString & arg1)
+void CSettingsView::on_comboBoxEnemyAI_currentTextChanged(const QString & arg1)
 {
 	Settings node = settings.write["server"]["enemyAI"];
 	node->String() = arg1.toUtf8().data();

+ 4 - 4
launcher/settingsView/csettingsview_moc.h

@@ -35,10 +35,10 @@ public slots:
 private slots:
 	void on_comboBoxResolution_currentTextChanged(const QString & arg1);
 	void on_comboBoxFullScreen_currentIndexChanged(int index);
-	void on_comboBoxPlayerAI_currentIndexChanged(const QString & arg1);
-	void on_comboBoxFriendlyAI_currentIndexChanged(const QString & arg1);
-	void on_comboBoxNeutralAI_currentIndexChanged(const QString & arg1);
-	void on_comboBoxEnemyAI_currentIndexChanged(const QString & arg1);
+	void on_comboBoxPlayerAI_currentTextChanged(const QString & arg1);
+	void on_comboBoxFriendlyAI_currentTextChanged(const QString & arg1);
+	void on_comboBoxNeutralAI_currentTextChanged(const QString & arg1);
+	void on_comboBoxEnemyAI_currentTextChanged(const QString & arg1);
 	void on_spinBoxNetworkPort_valueChanged(int arg1);
 	void on_plainTextEditRepos_textChanged();
 	void on_openTempDir_clicked();

+ 12 - 1
lib/CArtHandler.cpp

@@ -924,6 +924,16 @@ void CCombinedArtifactInstance::addAsConstituent(CArtifactInstance * art, const
 	attachTo(*art);
 }
 
+void CCombinedArtifactInstance::removeFrom(ArtifactLocation al)
+{
+	CArtifactInstance::removeFrom(al);
+	for(auto & part : constituentsInfo)
+	{
+		if(part.slot != ArtifactPosition::PRE_FIRST)
+			part.slot = ArtifactPosition::PRE_FIRST;
+	}
+}
+
 void CCombinedArtifactInstance::deserializationFix()
 {
 	for(ConstituentInfo &ci : constituentsInfo)
@@ -1092,7 +1102,8 @@ void CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art)
 		const CArtifactInstance * mainPart = nullptr;
 		auto & parts = dynamic_cast<CCombinedArtifactInstance*>(art)->constituentsInfo;
 		for(const auto & part : parts)
-			if(vstd::contains(part.art->artType->possibleSlots.at(bearerType()), slot))
+			if(vstd::contains(part.art->artType->possibleSlots.at(bearerType()), slot)
+				&& (part.slot == ArtifactPosition::PRE_FIRST))
 			{
 				mainPart = part.art;
 				break;

+ 4 - 3
lib/CArtHandler.h

@@ -162,9 +162,9 @@ public:
 	/// of itself, additionally truth is returned for constituents of combined arts
 	virtual bool isPart(const CArtifactInstance *supposedPart) const;
 
-	void putAt(ArtifactLocation al);
-	void removeFrom(ArtifactLocation al);
-	void move(const ArtifactLocation & src,const ArtifactLocation & dst);
+	virtual void putAt(ArtifactLocation al);
+	virtual void removeFrom(ArtifactLocation al);
+	virtual void move(const ArtifactLocation & src, const ArtifactLocation & dst);
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
@@ -198,6 +198,7 @@ public:
 	bool isPart(const CArtifactInstance *supposedPart) const override;
 	void createConstituents();
 	void addAsConstituent(CArtifactInstance * art, const ArtifactPosition & slot);
+	void removeFrom(ArtifactLocation al) override;
 
 	CCombinedArtifactInstance() = default;
 

+ 3 - 2
lib/NetPacksLib.cpp

@@ -1970,13 +1970,14 @@ void AssembledArtifact::applyGs(CGameState *gs)
 			if(!vstd::contains(combinedArt->artType->possibleSlots[artSet->bearerType()], al.slot)
 				&& vstd::contains(combinedArt->artType->possibleSlots[artSet->bearerType()], pos))
 				al.slot = pos;
+			if(al.slot == pos)
+				pos = ArtifactPosition::PRE_FIRST;
 		}
 		else
 		{
 			al.slot = std::min(al.slot, pos);
-		}
-		if(al.slot == pos)
 			pos = ArtifactPosition::PRE_FIRST;
+		}
 		combinedArt->addAsConstituent(constituentInstance, pos);
 	}
 

+ 1 - 0
lib/ObstacleHandler.cpp

@@ -103,6 +103,7 @@ ObstacleInfo * ObstacleHandler::loadFromJson(const std::string & scope, const Js
 		info->allowedSpecialBfields.emplace_back(t.String());
 	info->blockedTiles = json["blockedTiles"].convertTo<std::vector<si16>>();
 	info->isAbsoluteObstacle = json["absolute"].Bool();
+	info->isForegroundObstacle = json["foreground"].Bool();
 
 	objects.emplace_back(info);
 

+ 5 - 7
lib/ObstacleHandler.h

@@ -20,11 +20,11 @@ VCMI_LIB_NAMESPACE_BEGIN
 class DLL_LINKAGE ObstacleInfo : public EntityT<Obstacle>
 {
 public:
-	ObstacleInfo(): obstacle(-1), width(0), height(0), isAbsoluteObstacle(false), iconIndex(0)
+	ObstacleInfo(): obstacle(-1), width(0), height(0), isAbsoluteObstacle(false), iconIndex(0), isForegroundObstacle(false)
 	{}
 	
 	ObstacleInfo(Obstacle obstacle, std::string identifier)
-	: obstacle(obstacle), identifier(identifier), iconIndex(obstacle.getNum()), width(0), height(0), isAbsoluteObstacle(false)
+	: obstacle(obstacle), identifier(identifier), iconIndex(obstacle.getNum()), width(0), height(0), isAbsoluteObstacle(false), isForegroundObstacle(false)
 	{
 	}
 	
@@ -35,10 +35,8 @@ public:
 	std::vector<TerrainId> allowedTerrains;
 	std::vector<std::string> allowedSpecialBfields;
 	
-	//TODO: here is extra field to implement it's logic in the future but save backward compatibility
-	int obstacleType = -1;
-	
-	ui8 isAbsoluteObstacle; //there may only one such obstacle in battle and its position is always the same
+	bool isAbsoluteObstacle; //there may only one such obstacle in battle and its position is always the same
+	bool isForegroundObstacle;
 	si32 width, height; //how much space to the right and up is needed to place obstacle (affects only placement algorithm)
 	std::vector<si16> blockedTiles; //offsets relative to obstacle position (that is its left bottom corner)
 	
@@ -57,7 +55,6 @@ public:
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & obstacle;
-		h & obstacleType;
 		h & iconIndex;
 		h & identifier;
 		h & animation;
@@ -66,6 +63,7 @@ public:
 		h & allowedTerrains;
 		h & allowedSpecialBfields;
 		h & isAbsoluteObstacle;
+		h & isForegroundObstacle;
 		h & width;
 		h & height;
 		h & blockedTiles;

+ 18 - 0
lib/battle/CUnitState.cpp

@@ -591,6 +591,24 @@ int32_t CUnitState::getInitiative(int turn) const
 	return valOfBonuses(Selector::type()(BonusType::STACKS_SPEED).And(Selector::turns(turn)));
 }
 
+uint8_t CUnitState::getRangedFullDamageDistance() const
+{
+	if(!isShooter())
+		return 0;
+
+	uint8_t rangedFullDamageDistance = GameConstants::BATTLE_PENALTY_DISTANCE;
+
+	// overwrite full ranged damage distance with the value set in Additional info field of LIMITED_SHOOTING_RANGE bonus
+	if(this->hasBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)))
+	{
+		auto bonus = this->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE));
+		if(bonus != nullptr && bonus->additionalInfo != CAddInfo::NONE)
+			rangedFullDamageDistance = bonus->additionalInfo[0];
+	}
+
+	return rangedFullDamageDistance;
+}
+
 bool CUnitState::canMove(int turn) const
 {
 	return alive() && !hasBonus(Selector::type()(BonusType::NOT_ACTIVE).And(Selector::turns(turn))); //eg. Ammo Cart or blinded creature

+ 1 - 0
lib/battle/CUnitState.h

@@ -221,6 +221,7 @@ public:
 	BattleHex getPosition() const override;
 	void setPosition(BattleHex hex) override;
 	int32_t getInitiative(int turn = 0) const override;
+	uint8_t getRangedFullDamageDistance() const;
 
 	bool canMove(int turn = 0) const override;
 	bool defended(int turn = 0) const override;

+ 0 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -1040,7 +1040,6 @@ std::string CGHeroInstance::getBiographyTextID() const
 
 void CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance *art)
 {
-	assert(!getArt(pos));
 	assert(art->artType->canBePutAt(this, pos));
 
 	CArtifactSet::putArtifact(pos, art);

+ 18 - 4
lib/rmg/RmgArea.cpp

@@ -64,21 +64,35 @@ void Area::invalidate()
 	dBorderOutsideCache.clear();
 }
 
-bool Area::connected() const
+bool Area::connected(bool noDiagonals) const
 {
 	std::list<int3> queue({*dTiles.begin()});
 	Tileset connected = dTiles; //use invalidated cache - ok
+
 	while(!queue.empty())
 	{
 		auto t = queue.front();
 		connected.erase(t);
 		queue.pop_front();
 		
-		for(auto & i : int3::getDirs())
+		if (noDiagonals)
+		{
+			for (auto& i : dirs4)
+			{
+				if (connected.count(t + i))
+				{
+					queue.push_back(t + i);
+				}
+			}
+		}
+		else
 		{
-			if(connected.count(t + i))
+			for (auto& i : int3::getDirs())
 			{
-				queue.push_back(t + i);
+				if (connected.count(t + i))
+				{
+					queue.push_back(t + i);
+				}
 			}
 		}
 	}

+ 1 - 1
lib/rmg/RmgArea.h

@@ -44,7 +44,7 @@ namespace rmg
 
 		Area getSubarea(const std::function<bool(const int3 &)> & filter) const;
 
-		bool connected() const; //is connected
+		bool connected(bool noDiagonals = false) const; //is connected
 		bool empty() const;
 		bool contains(const int3 & tile) const;
 		bool contains(const std::vector<int3> & tiles) const;

+ 26 - 3
lib/rmg/RmgObject.cpp

@@ -161,17 +161,21 @@ const CGObjectInstance & Object::Instance::object() const
 	return dObject;
 }
 
-Object::Object(CGObjectInstance & object, const int3 & position)
+Object::Object(CGObjectInstance & object, const int3 & position):
+	guarded(false)
 {
 	addInstance(object, position);
 }
 
-Object::Object(CGObjectInstance & object)
+Object::Object(CGObjectInstance & object):
+	guarded(false)
 {
 	addInstance(object);
 }
 
-Object::Object(const Object & object): dStrength(object.dStrength)
+Object::Object(const Object & object):
+	dStrength(object.dStrength),
+	guarded(false)
 {
 	for(const auto & i : object.dInstances)
 		addInstance(const_cast<CGObjectInstance &>(i.object()), i.getPosition());
@@ -197,7 +201,9 @@ std::list<const Object::Instance*> Object::instances() const
 void Object::addInstance(Instance & object)
 {
 	//assert(object.dParent == *this);
+	setGuardedIfMonster(object);
 	dInstances.push_back(object);
+
 	dFullAreaCache.clear();
 	dAccessibleAreaCache.clear();
 	dAccessibleAreaFullCache.clear();
@@ -206,6 +212,8 @@ void Object::addInstance(Instance & object)
 Object::Instance & Object::addInstance(CGObjectInstance & object)
 {
 	dInstances.emplace_back(*this, object);
+	setGuardedIfMonster(dInstances.back());
+
 	dFullAreaCache.clear();
 	dAccessibleAreaCache.clear();
 	dAccessibleAreaFullCache.clear();
@@ -215,6 +223,8 @@ Object::Instance & Object::addInstance(CGObjectInstance & object)
 Object::Instance & Object::addInstance(CGObjectInstance & object, const int3 & position)
 {
 	dInstances.emplace_back(*this, object, position);
+	setGuardedIfMonster(dInstances.back());
+
 	dFullAreaCache.clear();
 	dAccessibleAreaCache.clear();
 	dAccessibleAreaFullCache.clear();
@@ -302,6 +312,19 @@ const int3 Object::getVisibleTop() const
 	return topTile;
 }
 
+bool rmg::Object::isGuarded() const
+{
+	return guarded;
+}
+
+void rmg::Object::setGuardedIfMonster(const Instance& object)
+{
+	if (object.object().ID == Obj::MONSTER)
+	{
+		guarded = true;
+	}
+}
+
 void Object::Instance::finalize(RmgMap & map)
 {
 	if(!map.isOnMap(getPosition(true)))

+ 4 - 0
lib/rmg/RmgObject.h

@@ -77,6 +77,9 @@ public:
 	
 	const Area & getArea() const;  //lazy cache invalidation
 	const int3 getVisibleTop() const;
+
+	bool isGuarded() const;
+	void setGuardedIfMonster(const Instance & object);
 	
 	void finalize(RmgMap & map);
 	void clear();
@@ -87,6 +90,7 @@ private:
 	mutable Area dAccessibleAreaCache, dAccessibleAreaFullCache;
 	int3 dPosition;
 	ui32 dStrength;
+	bool guarded;
 };
 }
 

+ 28 - 2
lib/rmg/Zone.cpp

@@ -203,8 +203,34 @@ void Zone::fractalize()
 	rmg::Area possibleTiles(dAreaPossible);
 	rmg::Area tilesToIgnore; //will be erased in this iteration
 
-	const float minDistance = 10 * 10; //squared
-	float blockDistance = minDistance * 0.25f;
+	//Squared
+	float minDistance = 10 * 10;
+	float spanFactor = (pos.z ? 0.25 : 0.5f); //Narrower passages in the Underground
+
+	int treasureValue = 0;
+	int treasureDensity = 0;
+	for (auto t : treasureInfo)
+	{
+		treasureValue += ((t.min + t.max) / 2) * t.density / 1000.f; //Thousands
+		treasureDensity += t.density;
+	}
+
+	if (treasureValue > 200)
+	{
+		//Less obstacles - max span is 1 (no obstacles)
+		spanFactor = 1.0f - ((std::max(0, (1000 - treasureValue)) / (1000.f - 200)) * (1 - spanFactor));
+	}
+	else if (treasureValue < 100)
+	{
+		//Dense obstacles
+		spanFactor *= (treasureValue / 100.f);
+		vstd::amax(spanFactor, 0.2f);
+	}
+	if (treasureDensity <= 10)
+	{
+		vstd::amin(spanFactor, 0.25f); //Add extra obstacles to fill up space
+	}
+	float blockDistance = minDistance * spanFactor; //More obstacles in the Underground
 	
 	if(type != ETemplateZoneType::JUNCTION)
 	{

+ 58 - 3
lib/rmg/modificators/ObjectManager.cpp

@@ -166,7 +166,6 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object
 		}
 	}
 	
-	//FIXME: Race condition for tiles? For Area?
 	if(result.valid())
 		obj.setPosition(result);
 	return result;
@@ -183,8 +182,15 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object
 
 		for(const auto & t : obj.getArea().getTilesVector())
 		{
-			if(map.getTileInfo(t).getNearestObjectDistance() < min_dist)
+			auto localDist = map.getTileInfo(t).getNearestObjectDistance();
+			if (localDist < min_dist)
+			{
 				return -1.f;
+			}
+			else
+			{
+				vstd::amin(dist, localDist); //Evaluate object tile which will be closest to another object
+			}
 		}
 		
 		return dist;
@@ -206,6 +212,55 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 				return -1.f;
 		}
 		
+		rmg::Area perimeter;
+		rmg::Area areaToBlock;
+		if (obj.isGuarded())
+		{
+			auto guardedArea = obj.instances().back()->getAccessibleArea();
+			guardedArea.add(obj.instances().back()->getVisitablePosition());
+			areaToBlock = obj.getAccessibleArea(true);
+			areaToBlock.subtract(guardedArea);
+		
+			if (!areaToBlock.empty())
+			{
+				perimeter = areaToBlock;
+				perimeter.unite(areaToBlock.getBorderOutside());
+				//We could have added border around guard
+				perimeter.subtract(guardedArea);
+			}
+		}
+		else
+		{
+			perimeter = obj.getArea();
+			perimeter.subtract(obj.getAccessibleArea());
+			if (!perimeter.empty())
+			{
+				perimeter.unite(perimeter.getBorderOutside());
+				perimeter.subtract(obj.getAccessibleArea());
+			}
+		}
+		//Check if perimeter of the object intersects with more than one blocked areas
+
+		auto tiles = perimeter.getTiles();
+		vstd::erase_if(tiles, [this](const int3& tile) -> bool
+		{
+			//Out-of-map area also is an obstacle
+			if (!map.isOnMap(tile))
+				return false;
+			return !(map.isBlocked(tile) || map.isUsed(tile));
+		});
+
+		if (!tiles.empty())
+		{
+			rmg::Area border(tiles);
+			border.subtract(areaToBlock);
+			if (!border.connected())
+			{
+				//We don't want to connect two blocked areas to create impassable obstacle
+				return -1.f;
+			}
+		}
+		
 		return dist;
 	}, isGuarded, onlyStraight, optimizer);
 }
@@ -230,7 +285,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 			accessibleArea.intersect(guardedArea);
 			accessibleArea.add(obj.instances().back()->getPosition(true));
 		}
-		
+
 		auto path = zone.searchPath(accessibleArea, onlyStraight, [&obj, isGuarded](const int3 & t)
 		{
 			if(isGuarded)

+ 58 - 10
lib/rmg/modificators/ObstaclePlacer.cpp

@@ -12,7 +12,7 @@
 #include "ObstaclePlacer.h"
 #include "ObjectManager.h"
 #include "TreasurePlacer.h"
-#include "RockPlacer.h"
+#include "RockFiller.h"
 #include "WaterRoutes.h"
 #include "WaterProxy.h"
 #include "RoadPlacer.h"
@@ -35,14 +35,56 @@ void ObstaclePlacer::process()
 
 	collectPossibleObstacles(zone.getTerrainType());
 	
-	blockedArea = zone.area().getSubarea([this](const int3 & t)
 	{
-		return map.shouldBeBlocked(t);
-	});
-	blockedArea.subtract(zone.areaUsed());
-	zone.areaPossible().subtract(blockedArea);
+		Zone::Lock lock(zone.areaMutex);
+		blockedArea = zone.area().getSubarea([this](const int3& t)
+			{
+				return map.shouldBeBlocked(t);
+			});
+		blockedArea.subtract(zone.areaUsed());
+		zone.areaPossible().subtract(blockedArea);
 
-	prohibitedArea = zone.freePaths() + zone.areaUsed() + manager->getVisitableArea();
+		prohibitedArea = zone.freePaths() + zone.areaUsed() + manager->getVisitableArea();
+
+		//Progressively block tiles, but make sure they don't seal any gap between blocks
+		rmg::Area toBlock;
+		do
+		{
+			toBlock.clear();
+			for (const auto& tile : zone.areaPossible().getTiles())
+			{
+				rmg::Area neighbors;
+				rmg::Area t;
+				t.add(tile);
+
+				for (const auto& n : t.getBorderOutside())
+				{
+					//Area outside the map is also impassable
+					if (!map.isOnMap(n) || map.shouldBeBlocked(n))
+					{
+						neighbors.add(n);
+					}
+				}
+				if (neighbors.empty())
+				{
+					continue;
+				}
+				//Will only be added if it doesn't connect two disjointed blocks
+				if (neighbors.connected(true)) //Do not block diagonal pass
+				{
+					toBlock.add(tile);
+				}
+			}
+			zone.areaPossible().subtract(toBlock);
+			for (const auto& tile : toBlock.getTiles())
+			{
+				map.setOccupied(tile, ETileType::BLOCKED);
+			}
+
+		} while (!toBlock.empty());
+
+		prohibitedArea.unite(zone.areaPossible());
+	}
 
 	auto objs = createObstacles(zone.getRand());
 	mapProxy->insertObjects(objs);
@@ -52,10 +94,16 @@ void ObstaclePlacer::init()
 {
 	DEPENDENCY(ObjectManager);
 	DEPENDENCY(TreasurePlacer);
-	DEPENDENCY(WaterRoutes);
-	DEPENDENCY(WaterProxy);
 	DEPENDENCY(RoadPlacer);
-	DEPENDENCY_ALL(RockPlacer);
+	if (zone.isUnderground())
+	{
+		DEPENDENCY(RockFiller);
+	}
+	else
+	{
+		DEPENDENCY(WaterRoutes);
+		DEPENDENCY(WaterProxy);
+	}
 }
 
 bool ObstaclePlacer::isInTheMap(const int3& tile)

+ 4 - 1
lib/rmg/modificators/RiverPlacer.cpp

@@ -82,7 +82,10 @@ void RiverPlacer::process()
 
 void RiverPlacer::init()
 {
-	DEPENDENCY_ALL(WaterProxy);
+	if (!zone.isUnderground())
+	{
+		DEPENDENCY_ALL(WaterProxy);
+	}
 	DEPENDENCY(ObjectManager);
 	DEPENDENCY(ObstaclePlacer);
 }

+ 9 - 0
lib/rmg/modificators/RoadPlacer.cpp

@@ -12,6 +12,7 @@
 #include "RoadPlacer.h"
 #include "ObjectManager.h"
 #include "ObstaclePlacer.h"
+#include "RockFiller.h"
 #include "../Functions.h"
 #include "../CMapGenerator.h"
 #include "../threadpool/MapProxy.h"
@@ -28,6 +29,14 @@ void RoadPlacer::process()
 	connectRoads();
 }
 
+void RoadPlacer::init()
+{
+	if (zone.isUnderground())
+	{
+		DEPENDENCY_ALL(RockFiller);
+	}
+}
+
 rmg::Area & RoadPlacer::areaForRoads()
 {
 	return areaRoads;

+ 1 - 0
lib/rmg/modificators/RoadPlacer.h

@@ -19,6 +19,7 @@ public:
 	MODIFICATOR(RoadPlacer);
 	
 	void process() override;
+	void init() override;
 	char dump(const int3 &) override;
 	
 	void addRoadNode(const int3 & node);

+ 0 - 2
lib/rmg/modificators/RockFiller.cpp

@@ -13,7 +13,6 @@
 #include "RockPlacer.h"
 #include "TreasurePlacer.h"
 #include "ObjectManager.h"
-#include "RoadPlacer.h"
 #include "RiverPlacer.h"
 #include "../RmgMap.h"
 #include "../CMapGenerator.h"
@@ -63,7 +62,6 @@ void RockFiller::processMap()
 void RockFiller::init()
 {
 	DEPENDENCY_ALL(RockPlacer);
-	POSTFUNCTION_ALL(RoadPlacer);
 }
 
 char RockFiller::dump(const int3 & t)

+ 11 - 1
lib/rmg/modificators/RockPlacer.cpp

@@ -67,7 +67,17 @@ void RockPlacer::postProcess()
 
 void RockPlacer::init()
 {
-	DEPENDENCY_ALL(TreasurePlacer);
+	for (const auto& zone : map.getZones())
+	{
+		if (zone.second->isUnderground())
+		{
+			auto * tp = zone.second->getModificator<TreasurePlacer>();
+			if (tp)
+			{
+				dependency(tp);
+			}
+		}
+	}
 }
 
 char RockPlacer::dump(const int3 & t)

+ 7 - 4
lib/rmg/modificators/TownPlacer.cpp

@@ -163,11 +163,14 @@ void TownPlacer::cleanupBoundaries(const rmg::Object & rmgObject)
 	Zone::Lock lock(zone.areaMutex);
 	for(const auto & t : rmgObject.getArea().getBorderOutside())
 	{
-		if(map.isOnMap(t))
+		if (t.y > rmgObject.getVisitablePosition().y) //Line below the town
 		{
-			map.setOccupied(t, ETileType::FREE);
-			zone.areaPossible().erase(t);
-			zone.freePaths().add(t);
+			if (map.isOnMap(t))
+			{
+				map.setOccupied(t, ETileType::FREE);
+				zone.areaPossible().erase(t);
+				zone.freePaths().add(t);
+			}
 		}
 	}
 }

+ 120 - 88
lib/rmg/modificators/TreasurePlacer.cpp

@@ -568,7 +568,7 @@ std::vector<ObjectInfo*> TreasurePlacer::prepareTreasurePile(const CTreasureInfo
 	bool hasLargeObject = false;
 	while(currentValue <= static_cast<int>(desiredValue) - 100) //no objects with value below 100 are available
 	{
-		auto * oi = getRandomObject(desiredValue, currentValue, maxValue, !hasLargeObject);
+		auto * oi = getRandomObject(desiredValue, currentValue, !hasLargeObject);
 		if(!oi) //fail
 			break;
 		
@@ -665,13 +665,13 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 	return rmgObject;
 }
 
-ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValue, ui32 maxValue, bool allowLargeObjects)
+ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValue, bool allowLargeObjects)
 {
 	std::vector<std::pair<ui32, ObjectInfo*>> thresholds; //handle complex object via pointer
 	ui32 total = 0;
 	
 	//calculate actual treasure value range based on remaining value
-	ui32 maxVal = maxValue - currentValue;
+	ui32 maxVal = desiredValue - currentValue;
 	ui32 minValue = static_cast<ui32>(0.25f * (desiredValue - currentValue));
 	
 	for(ObjectInfo & oi : possibleObjects) //copy constructor turned out to be costly
@@ -707,133 +707,165 @@ ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValu
 	}
 }
 
-void TreasurePlacer::createTreasures(ObjectManager & manager)
+void TreasurePlacer::createTreasures(ObjectManager& manager)
 {
 	const int maxAttempts = 2;
-	
+
 	int mapMonsterStrength = map.getMapGenOptions().getMonsterStrength();
-	int monsterStrength = (zone.monsterStrength == EMonsterStrength::ZONE_NONE ? 0 : zone.monsterStrength  + mapMonsterStrength - 1); //array index from 0 to 4; pick any correct value for ZONE_NONE, minGuardedValue won't be used in this case anyway
+	int monsterStrength = (zone.monsterStrength == EMonsterStrength::ZONE_NONE ? 0 : zone.monsterStrength + mapMonsterStrength - 1); //array index from 0 to 4; pick any correct value for ZONE_NONE, minGuardedValue won't be used in this case anyway
 	static int minGuardedValues[] = { 6500, 4167, 3000, 1833, 1333 };
 	minGuardedValue = minGuardedValues[monsterStrength];
-	
-	auto valueComparator = [](const CTreasureInfo & lhs, const CTreasureInfo & rhs) -> bool
+
+	auto valueComparator = [](const CTreasureInfo& lhs, const CTreasureInfo& rhs) -> bool
 	{
 		return lhs.max > rhs.max;
 	};
-	
-	auto restoreZoneLimits = [](const std::vector<ObjectInfo*> & treasurePile)
+
+	auto restoreZoneLimits = [](const std::vector<ObjectInfo*>& treasurePile)
 	{
-		for(auto * oi : treasurePile)
+		for (auto* oi : treasurePile)
 		{
 			oi->maxPerZone++;
 		}
 	};
-	
+
 	//place biggest treasures first at large distance, place smaller ones inbetween
 	auto treasureInfo = zone.getTreasureInfo();
 	boost::sort(treasureInfo, valueComparator);
-	
+
 	//sort treasures by ascending value so we can stop checking treasures with too high value
 	boost::sort(possibleObjects, [](const ObjectInfo& oi1, const ObjectInfo& oi2) -> bool
 	{
 		return oi1.value < oi2.value;
 	});
-	
+
+	size_t size = 0;
+	{
+		Zone::Lock lock(zone.areaMutex);
+		size = zone.getArea().getTiles().size();
+	}
+
 	int totalDensity = 0;
-	for (auto t : treasureInfo)
+
+	for (auto t  = treasureInfo.begin(); t != treasureInfo.end(); t++)
 	{
+		std::vector<rmg::Object> treasures;
+
 		//discard objects with too high value to be ever placed
 		vstd::erase_if(possibleObjects, [t](const ObjectInfo& oi) -> bool
 		{
-			return oi.value > t.max;
+			return oi.value > t->max;
 		});
-		
-		totalDensity += t.density;
-		
-		//treasure density is inversely proportional to zone size but must be scaled back to map size
-		//also, normalize it to zone count - higher count means relatively smaller zones
+
+		totalDensity += t->density;
+
+		size_t count = size * t->density / 500;
+
+		//Assure space for lesser treasures, if there are any left
+		if (t != (treasureInfo.end() - 1))
+		{
+			const int averageValue = (t->min + t->max) / 2;
+			if (averageValue > 10000)
+			{
+				//Will surely be guarded => larger piles => less space inbetween
+				vstd::amin(count, size * (10.f / 500) / (std::sqrt((float)averageValue / 10000)));
+			}
+		}
 		
 		//this is squared distance for optimization purposes
-		const float minDistance = std::max<float>((125.f / totalDensity), 2.0f);
-		//distance lower than 2 causes objects to overlap and crash
-		
-		for(int attempt = 0; attempt <= maxAttempts;)
+		const float minDistance = std::max<float>((125.f / totalDensity), 1.0f);
+
+		size_t emergencyLoopFinish = 0;
+		while(treasures.size() < count && emergencyLoopFinish < count)
 		{
-			auto treasurePileInfos = prepareTreasurePile(t);
-			if(treasurePileInfos.empty())
+			auto treasurePileInfos = prepareTreasurePile(*t);
+			if (treasurePileInfos.empty())
 			{
-				++attempt;
+				emergencyLoopFinish++; //Exit potentially infinite loop for bad settings
 				continue;
 			}
-			
-			int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo * oi){return v + oi->value;});
-			
-			auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts);
-			if(rmgObject.instances().empty()) //handle incorrect placement
+
+			int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; });
+
+			for (ui32 attempt = 0; attempt <= 2; attempt++)
 			{
-				restoreZoneLimits(treasurePileInfos);
-				continue;
+				auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts);
+
+				if (rmgObject.instances().empty()) //handle incorrect placement
+				{
+					restoreZoneLimits(treasurePileInfos);
+					continue;
+				}
+
+				//guard treasure pile
+				bool guarded = isGuardNeededForTreasure(value);
+				if (guarded)
+					guarded = manager.addGuard(rmgObject, value);
+
+				treasures.push_back(rmgObject);
+				break;
 			}
-			
-			//guard treasure pile
-			bool guarded = isGuardNeededForTreasure(value);
-			if(guarded)
-				guarded = manager.addGuard(rmgObject, value);
-			
-			Zone::Lock lock(zone.areaMutex); //We are going to subtract this area
-			//TODO: Don't place 
-			auto possibleArea = zone.areaPossible();
-			
-			auto path = rmg::Path::invalid();
-			if(guarded)
+		}
+
+		for (auto& rmgObject : treasures)
+		{
+			const bool guarded = rmgObject.isGuarded();
+
+			for (int attempt = 0; attempt <= maxAttempts;)
 			{
-				path = manager.placeAndConnectObject(possibleArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3 & tile)
+				auto path = rmg::Path::invalid();
+
+				Zone::Lock lock(zone.areaMutex); //We are going to subtract this area
+				auto possibleArea = zone.areaPossible();
+
+				if (guarded)
 				{
-					auto ti = map.getTileInfo(tile);
-					if(ti.getNearestObjectDistance() < minDistance)
-						return -1.f;
+					path = manager.placeAndConnectObject(possibleArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3& tile)
+						{
+							auto ti = map.getTileInfo(tile);
+							if (ti.getNearestObjectDistance() < minDistance)
+								return -1.f;
+
+							for (const auto& t : rmgObject.getArea().getTilesVector())
+							{
+								if (map.getTileInfo(t).getNearestObjectDistance() < minDistance)
+									return -1.f;
+							}
+
+							auto guardedArea = rmgObject.instances().back()->getAccessibleArea();
+							auto areaToBlock = rmgObject.getAccessibleArea(true);
+							areaToBlock.subtract(guardedArea);
+							if (areaToBlock.overlap(zone.freePaths()) || areaToBlock.overlap(manager.getVisitableArea()))
+								return -1.f;
+
+							return ti.getNearestObjectDistance();
+						}, guarded, false, ObjectManager::OptimizeType::DISTANCE);
+				}
+				else
+				{
+					path = manager.placeAndConnectObject(possibleArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE);
+				}
 
-					for(const auto & t : rmgObject.getArea().getTilesVector())
+				if (path.valid())
+				{
+					//debug purposes
+					treasureArea.unite(rmgObject.getArea());
+					if (guarded)
 					{
-						if(map.getTileInfo(t).getNearestObjectDistance() < minDistance)
-							return -1.f;
+						guards.unite(rmgObject.instances().back()->getBlockedArea());
+						auto guardedArea = rmgObject.instances().back()->getAccessibleArea();
+						auto areaToBlock = rmgObject.getAccessibleArea(true);
+						areaToBlock.subtract(guardedArea);
+						treasureBlockArea.unite(areaToBlock);
 					}
-					
-					auto guardedArea = rmgObject.instances().back()->getAccessibleArea();
-					auto areaToBlock = rmgObject.getAccessibleArea(true);
-					areaToBlock.subtract(guardedArea);
-					if(areaToBlock.overlap(zone.freePaths()) || areaToBlock.overlap(manager.getVisitableArea()))
-						return -1.f;
-					
-					return ti.getNearestObjectDistance();
-				}, guarded, false, ObjectManager::OptimizeType::DISTANCE);
-			}
-			else
-			{
-				path = manager.placeAndConnectObject(possibleArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE);
-			}
-			
-			if(path.valid())
-			{
-				//debug purposes
-				treasureArea.unite(rmgObject.getArea());
-				if(guarded)
+					zone.connectPath(path);
+					manager.placeObject(rmgObject, guarded, true);
+					break;
+				}
+				else
 				{
-					guards.unite(rmgObject.instances().back()->getBlockedArea());
-					auto guardedArea = rmgObject.instances().back()->getAccessibleArea();
-					auto areaToBlock = rmgObject.getAccessibleArea(true);
-					areaToBlock.subtract(guardedArea);
-					treasureBlockArea.unite(areaToBlock);
+					++attempt;
 				}
-				zone.connectPath(path);
-				manager.placeObject(rmgObject, guarded, true);
-				attempt = 0;
-			}
-			else
-			{
-				restoreZoneLimits(treasurePileInfos);
-				rmgObject.clear();
-				++attempt;
 			}
 		}
 	}

+ 1 - 1
lib/rmg/modificators/TreasurePlacer.h

@@ -54,7 +54,7 @@ public:
 protected:
 	bool isGuardNeededForTreasure(int value);
 	
-	ObjectInfo * getRandomObject(ui32 desiredValue, ui32 currentValue, ui32 maxValue, bool allowLargeObjects);
+	ObjectInfo * getRandomObject(ui32 desiredValue, ui32 currentValue, bool allowLargeObjects);
 	std::vector<ObjectInfo*> prepareTreasurePile(const CTreasureInfo & treasureInfo);
 	rmg::Object constructTreasurePile(const std::vector<ObjectInfo*> & treasureInfos, bool densePlacement = false);
 

+ 5 - 2
lib/rmg/modificators/WaterProxy.cpp

@@ -83,8 +83,11 @@ void WaterProxy::init()
 {
 	for(auto & z : map.getZones())
 	{
-		dependency(z.second->getModificator<TownPlacer>());
-		dependency(z.second->getModificator<WaterAdopter>());
+		if (!zone.isUnderground())
+		{
+			dependency(z.second->getModificator<TownPlacer>());
+			dependency(z.second->getModificator<WaterAdopter>());
+		}
 		postfunction(z.second->getModificator<ConnectionsPlacer>());
 		postfunction(z.second->getModificator<ObjectManager>());
 	}

Some files were not shown because too many files changed in this diff