Browse Source

Merge pull request #2240 from krs0/feature/show_max_range_limit_for_ranged_units

Show max range limit for ranged units
Ivan Savenko 2 years ago
parent
commit
b93b8f45db

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

@@ -32,15 +32,3 @@
 	]
 }
 
-
-
-
-
-
-
-
-
-
-
-
-

+ 34 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json

@@ -0,0 +1,34 @@
+{
+	"basepath" : "battle/rangeHighlights/red/",
+	"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
+	]
+}
+

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


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


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


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


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


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


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


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


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

@@ -109,8 +109,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.rangeLimitHighlightOnHover.hover": "Show range limits for shooters",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Show range limits for shooters on Hover}\n\nShow shooter's range limits 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",

+ 2 - 2
Mods/vcmi/config/vcmi/ukrainian.json

@@ -92,8 +92,8 @@
 	"vcmi.battleOptions.animationsSpeed6.help": "Встановити миттєву швидкість анімації",
 	"vcmi.battleOptions.movementHighlightOnHover.hover": "Підсвічувати зону руху істоти",
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{Підсвічувати зону руху істоти}\n\nПідсвічувати можливу зону руху істоти при наведенні курсора миші на неї",
-	"vcmi.battleOptions.rangedFullDamageLimitHighlightOnHover.hover": "Межа повного шкоди пострілом",
-	"vcmi.battleOptions.rangedFullDamageLimitHighlightOnHover.help": "{Межа повного шкоди пострілом}\n\nПідсвічувати зону у якій створіння може завдавати максимальної шкоди пострілом при наведенні на неї курсору миші.",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Межа повного шкоди пострілом",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Межа повного шкоди пострілом}\n\nПідсвічувати зону у якій створіння може завдавати максимальної шкоди пострілом при наведенні на неї курсору миші.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускати вступну музику",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.",
 	

+ 106 - 71
client/battle/BattleFieldController.cpp

@@ -128,11 +128,16 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	attackCursors = std::make_shared<CAnimation>("CRCOMBAT");
 	attackCursors->preload();
 
-	rangedFullDamageLimitImages = std::make_unique<CAnimation>("battle/rangeHighlights/rangeHighlightsGreen.json");
+	initializeHexEdgeMaskToFrameIndex();
+
+	rangedFullDamageLimitImages = std::make_shared<CAnimation>("battle/rangeHighlights/rangeHighlightsGreen.json");
 	rangedFullDamageLimitImages->preload();
 
-	initializeHexEdgeMaskToFrameIndex();
-	flipRangedFullDamageLimitImagesIntoPositions();
+	shootingRangeLimitImages = std::make_shared<CAnimation>("battle/rangeHighlights/rangeHighlightsRed.json");
+	shootingRangeLimitImages->preload();
+
+	flipRangeLimitImagesIntoPositions(rangedFullDamageLimitImages);
+	flipRangeLimitImagesIntoPositions(shootingRangeLimitImages);
 
 	if(!owner.siegeController)
 	{
@@ -441,65 +446,62 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget(
 	return {};
 }
 
-std::vector<BattleHex> BattleFieldController::getRangedFullDamageHexes()
-{
-	std::vector<BattleHex> rangedFullDamageHexes; // used for return
+// Range limit highlight helpers
 
-	// 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;
+std::vector<BattleHex> BattleFieldController::getRangeHexes(BattleHex sourceHex, uint8_t distance)
+{
+	std::vector<BattleHex> rangeHexes;
 
-	auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance();
+	if (!settings["battle"]["rangeLimitHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
+		return rangeHexes;
 
-	// get only battlefield hexes that are in full range damage distance
-	std::set<BattleHex> fullRangeLimit; 
+	// get only battlefield hexes that are within the given distance
 	for(auto i = 0; i < GameConstants::BFIELD_SIZE; i++)
 	{
 		BattleHex hex(i);
-		if(hex.isAvailable() && BattleHex::getDistance(hoveredHex, hex) <= rangedFullDamageDistance)
-			rangedFullDamageHexes.push_back(hex);
+		if(hex.isAvailable() && BattleHex::getDistance(sourceHex, hex) <= distance)
+			rangeHexes.push_back(hex);
 	}
 
-	return rangedFullDamageHexes;
+	return rangeHexes;
 }
 
-std::vector<BattleHex> BattleFieldController::getRangedFullDamageLimitHexes(std::vector<BattleHex> rangedFullDamageHexes)
+std::vector<BattleHex> BattleFieldController::getRangeLimitHexes(BattleHex hoveredHex, std::vector<BattleHex> rangeHexes, uint8_t distanceToLimit)
 {
-	std::vector<BattleHex> rangedFullDamageLimitHexes; // used for return
+	std::vector<BattleHex> rangeLimitHexes;
 
-	// if not a hovered arcer unit -> return
-	auto hoveredHex = getHoveredHex();
-	const CStack * hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+	// from range hexes get only the ones at the limit
+	for(auto & hex : rangeHexes)
+	{
+		if(BattleHex::getDistance(hoveredHex, hex) == distanceToLimit)
+			rangeLimitHexes.push_back(hex);
+	}
 
-	if(!(hoveredStack && hoveredStack->isShooter()))
-		return rangedFullDamageLimitHexes;
+	return rangeLimitHexes;
+}
 
-	auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance();
+bool BattleFieldController::IsHexInRangeLimit(BattleHex hex, std::vector<BattleHex> & rangeLimitHexes, int * hexIndexInRangeLimit)
+{
+	bool  hexInRangeLimit = false;
 
-	// from ranged full damage hexes get only the ones at the limit
-	for(auto & hex : rangedFullDamageHexes)
+	if(!rangeLimitHexes.empty())
 	{
-		if(BattleHex::getDistance(hoveredHex, hex) == rangedFullDamageDistance)
-			rangedFullDamageLimitHexes.push_back(hex);
+		auto pos = std::find(rangeLimitHexes.begin(), rangeLimitHexes.end(), hex);
+		*hexIndexInRangeLimit = std::distance(rangeLimitHexes.begin(), pos);
+		hexInRangeLimit = pos != rangeLimitHexes.end();
 	}
 
-	return rangedFullDamageLimitHexes;
+	return hexInRangeLimit;
 }
 
-std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> rangedFullDamageHexes, std::vector<BattleHex> rangedFullDamageLimitHexes)
+std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> wholeRangeHexes, std::vector<BattleHex> rangeLimitHexes)
 {
 	std::vector<std::vector<BattleHex::EDir>> output;
 
-	if(rangedFullDamageHexes.empty())
+	if(wholeRangeHexes.empty())
 		return output;
 
-	for(auto & hex : rangedFullDamageLimitHexes)
+	for(auto & hex : rangeLimitHexes)
 	{
 		// get all neighbours and their directions
 		
@@ -507,15 +509,15 @@ std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeigh
 
 		std::vector<BattleHex::EDir> outsideNeighbourDirections;
 
-		// for each neighbour add to output only the valid ones and only that are not found in rangedFullDamageHexes
+		// for each neighbour add to output only the valid ones and only that are not found in range Hexes
 		for(auto direction = 0; direction < 6; direction++)
 		{
 			if(!neighbouringTiles[direction].isAvailable())
 				continue;
 
-			auto it = std::find(rangedFullDamageHexes.begin(), rangedFullDamageHexes.end(), neighbouringTiles[direction]);
+			auto it = std::find(wholeRangeHexes.begin(), wholeRangeHexes.end(), neighbouringTiles[direction]);
 
-			if(it == rangedFullDamageHexes.end())
+			if(it == wholeRangeHexes.end())
 				outsideNeighbourDirections.push_back(BattleHex::EDir(direction)); // push direction
 		}
 
@@ -525,14 +527,14 @@ std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeigh
 	return output;
 }
 
-std::vector<std::shared_ptr<IImage>> BattleFieldController::calculateRangedFullDamageHighlightImages(std::vector<std::vector<BattleHex::EDir>> rangedFullDamageLimitHexesNeighbourDirections)
+std::vector<std::shared_ptr<IImage>> BattleFieldController::calculateRangeLimitHighlightImages(std::vector<std::vector<BattleHex::EDir>> hexesNeighbourDirections, std::shared_ptr<CAnimation> limitImages)
 {
 	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())
+	if(hexesNeighbourDirections.empty())
 		return output;
 
-	for(auto & directions : rangedFullDamageLimitHexesNeighbourDirections)
+	for(auto & directions : hexesNeighbourDirections)
 	{
 		std::bitset<6> mask;
 		
@@ -541,46 +543,69 @@ std::vector<std::shared_ptr<IImage>> BattleFieldController::calculateRangedFullD
 			mask.set(direction);
 
 		uint8_t imageKey = static_cast<uint8_t>(mask.to_ulong());
-		output.push_back(rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[imageKey]));
+		output.push_back(limitImages->getImage(hexEdgeMaskToFrameIndex[imageKey]));
 	}
 
 	return output;
 }
 
-void BattleFieldController::flipRangedFullDamageLimitImagesIntoPositions()
+void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr<CAnimation> rangeLimitImages, std::vector<BattleHex> & rangeLimitHexes, std::vector<std::shared_ptr<IImage>> & rangeLimitHexesHighligts)
+{
+		std::vector<BattleHex> rangeHexes = getRangeHexes(hoveredHex, distance);
+		rangeLimitHexes = getRangeLimitHexes(hoveredHex, rangeHexes, distance);
+		std::vector<std::vector<BattleHex::EDir>> rangeLimitNeighbourDirections = getOutsideNeighbourDirectionsForLimitHexes(rangeHexes, rangeLimitHexes);
+		rangeLimitHexesHighligts = calculateRangeLimitHighlightImages(rangeLimitNeighbourDirections, rangeLimitImages);
+}
+
+void BattleFieldController::flipRangeLimitImagesIntoPositions(std::shared_ptr<CAnimation> images)
 {
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRight])->verticalFlip();
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::right])->verticalFlip();
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRight])->doubleFlip();
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeft])->horizontalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRight])->verticalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::right])->verticalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRight])->doubleFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeft])->horizontalFlip();
 
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottom])->horizontalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottom])->horizontalFlip();
 
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner])->verticalFlip();
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner])->doubleFlip();
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner])->horizontalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner])->verticalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner])->doubleFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner])->horizontalFlip();
 
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::rightHalf])->verticalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::rightHalf])->verticalFlip();
 
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightCorner])->verticalFlip();
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner])->doubleFlip();
-	rangedFullDamageLimitImages->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner])->horizontalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightCorner])->verticalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner])->doubleFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner])->horizontalFlip();
 }
 
 void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 {
+	std::vector<BattleHex> rangedFullDamageLimitHexes;
+	std::vector<BattleHex> shootingRangeLimitHexes;
+
+	std::vector<std::shared_ptr<IImage>> rangedFullDamageLimitHexesHighligts;
+	std::vector<std::shared_ptr<IImage>> shootingRangeLimitHexesHighligts;
+
 	std::set<BattleHex> hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack();
 	std::set<BattleHex> hoveredSpellHexes = getHighlightedHexesForSpellRange();
 	std::set<BattleHex> hoveredMoveHexes  = getHighlightedHexesForMovementTarget();
 
-	if(getHoveredHex() == BattleHex::INVALID)
+	BattleHex hoveredHex = getHoveredHex();
+	if(hoveredHex == 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);
+	const CStack * hoveredStack = getHoveredStack();
+
+	// skip range limit calculations if unit hovered is not a shooter
+	if(hoveredStack && hoveredStack->isShooter())
+	{
+		// calculate array with highlight images for ranged full damage limit
+		auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance();
+		calculateRangeLimitAndHighlightImages(rangedFullDamageDistance, rangedFullDamageLimitImages, rangedFullDamageLimitHexes, rangedFullDamageLimitHexesHighligts);
+
+		// calculate array with highlight images for shooting range limit
+		auto shootingRangeDistance = hoveredStack->getShootingRangeDistance();
+		calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts);
+	}
 
 	auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes;
 
@@ -590,14 +615,12 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 		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();
-		}
+		bool hexInRangedFullDamageLimit = IsHexInRangeLimit(hex, rangedFullDamageLimitHexes, &hexIndexInRangedFullDamageLimit);
+
+		// calculate if hex is Shooting Range Limit and its position in highlight array
+		int hexIndexInShootingRangeLimit = 0;
+		bool hexInShootingRangeLimit = IsHexInRangeLimit(hex, shootingRangeLimitHexes, &hexIndexInShootingRangeLimit);
 
 		if(stackMovement && mouse) // area where hovered stackMovement can move shown with highlight. Because also affected by mouse cursor, shade as well
 		{
@@ -612,10 +635,14 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 		{
 			showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false);
 		}
-		if(isRangedFullDamageLimit)
+		if(hexInRangedFullDamageLimit)
 		{
 			showHighlightedHex(canvas, rangedFullDamageLimitHexesHighligts[hexIndexInRangedFullDamageLimit], hex, false);
 		}
+		if(hexInShootingRangeLimit)
+		{
+			showHighlightedHex(canvas, shootingRangeLimitHexesHighligts[hexIndexInShootingRangeLimit], hex, false);
+		}
 	}
 }
 
@@ -643,6 +670,14 @@ BattleHex BattleFieldController::getHoveredHex()
 	return hoveredHex;
 }
 
+const CStack* BattleFieldController::getHoveredStack()
+{
+	auto hoveredHex = getHoveredHex();
+	const CStack* hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+
+	return hoveredStack;
+}
+
 BattleHex BattleFieldController::getHexAtPosition(Point hoverPos)
 {
 	if (owner.attackingHero)

+ 20 - 8
client/battle/BattleFieldController.h

@@ -33,7 +33,8 @@ 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> rangedFullDamageLimitImages;
+	std::shared_ptr<CAnimation> shootingRangeLimitImages;
 
 	std::shared_ptr<CAnimation> attackCursors;
 
@@ -59,22 +60,30 @@ 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();
+	// Range limit highlight helpers
 
-	/// get only hexes at the limit of a ranged unit's full damage range
-	std::vector<BattleHex> getRangedFullDamageLimitHexes(std::vector<BattleHex> rangedFullDamageHexes);
+	/// get all hexes within a certain distance of given hex
+	std::vector<BattleHex> getRangeHexes(BattleHex sourceHex, uint8_t distance);
+
+	/// get only hexes at the limit of a range
+	std::vector<BattleHex> getRangeLimitHexes(BattleHex hoveredHex, std::vector<BattleHex> hexRange, uint8_t distanceToLimit);
+
+	/// calculate if a hex is in range limit and return its index in range
+	bool IsHexInRangeLimit(BattleHex hex, std::vector<BattleHex> & rangeLimitHexes, int * hexIndexInRangeLimit);
 
 	/// 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);
+	std::vector<std::vector<BattleHex::EDir>> getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> rangeHexes, std::vector<BattleHex> rangeLimitHexes);
 
 	/// 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);
+	std::vector<std::shared_ptr<IImage>> calculateRangeLimitHighlightImages(std::vector<std::vector<BattleHex::EDir>> hexesNeighbourDirections, std::shared_ptr<CAnimation> limitImages);
+
+	/// calculates all hexes for a range limit and what images to be shown as highlight for each of the hexes
+	void calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr<CAnimation> rangeLimitImages, std::vector<BattleHex> & rangeLimitHexes, std::vector<std::shared_ptr<IImage>> & rangeLimitHexesHighligts);
 
 	/// to reduce the number of source images used, some images will be used as flipped versions of preloaded ones
-	void flipRangedFullDamageLimitImagesIntoPositions();
+	void flipRangeLimitImagesIntoPositions(std::shared_ptr<CAnimation> images);
 
 	void showBackground(Canvas & canvas);
 	void showBackgroundImage(Canvas & canvas);
@@ -118,6 +127,9 @@ public:
 	/// Returns ID of currently hovered hex or BattleHex::INVALID if none
 	BattleHex getHoveredHex();
 
+	/// Returns the currently hovered stack
+	const CStack* getHoveredStack();
+
 	/// returns true if selected tile can be attacked in melee by current stack
 	bool isTileAttackable(const BattleHex & number) const;
 

+ 6 - 6
client/windows/settings/BattleOptionsTab.cpp

@@ -36,9 +36,9 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner)
 	{
 		movementHighlightOnHoverChangedCallback(value, owner);
 	});
-	addCallback("rangedFullDamageLimitHighlightOnHoverChanged", [this, owner](bool value)
+	addCallback("rangeLimitHighlightOnHoverChanged", [this, owner](bool value)
 	{
-		rangedFullDamageLimitHighlightOnHoverChangedCallback(value, owner);
+		rangeLimitHighlightOnHoverChangedCallback(value, owner);
 	});
 	addCallback("mouseShadowChanged", [this](bool value)
 	{
@@ -77,8 +77,8 @@ 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> rangeLimitHighlightOnHoverCheckbox = widget<CToggleButton>("rangeLimitHighlightOnHoverCheckbox");
+	rangeLimitHighlightOnHoverCheckbox->setSelected(settings["battle"]["rangeLimitHighlightOnHover"].Bool());
 
 	std::shared_ptr<CToggleButton> mouseShadowCheckbox = widget<CToggleButton>("mouseShadowCheckbox");
 	mouseShadowCheckbox->setSelected(settings["battle"]["mouseShadow"].Bool());
@@ -156,9 +156,9 @@ void BattleOptionsTab::movementHighlightOnHoverChangedCallback(bool value, Battl
 		parentBattleInterface->redrawBattlefield();
 }
 
-void BattleOptionsTab::rangedFullDamageLimitHighlightOnHoverChangedCallback(bool value, BattleInterface * parentBattleInterface)
+void BattleOptionsTab::rangeLimitHighlightOnHoverChangedCallback(bool value, BattleInterface * parentBattleInterface)
 {
-	Settings stackRange = settings.write["battle"]["rangedFullDamageLimitHighlightOnHover"];
+	Settings stackRange = settings.write["battle"]["rangeLimitHighlightOnHover"];
 	stackRange->Bool() = value;
 	if(parentBattleInterface)
 		parentBattleInterface->redrawBattlefield();

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

@@ -25,7 +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 rangeLimitHighlightOnHoverChangedCallback(bool value, BattleInterface * parentBattleInterface);
 	void mouseShadowChangedCallback(bool value);
 	void animationSpeedChangedCallback(int value);
 	void showQueueChangedCallback(bool value, BattleInterface * parentBattleInterface);

+ 2 - 2
config/schemas/settings.json

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

+ 4 - 4
config/widgets/settings/battleOptionsTab.json

@@ -108,7 +108,7 @@
 					"text": "vcmi.battleOptions.movementHighlightOnHover.hover",
 				},
 				{
-					"text": "vcmi.battleOptions.rangedFullDamageLimitHighlightOnHover.hover",
+					"text": "vcmi.battleOptions.rangeLimitHighlightOnHover.hover",
 				},
 				{
 					"text": "core.genrltxt.406",
@@ -140,9 +140,9 @@
 					"callback": "movementHighlightOnHoverChanged"
 				},
 				{
-					"name": "rangedFullDamageLimitHighlightOnHoverCheckbox",
-					"help": "vcmi.battleOptions.rangedFullDamageLimitHighlightOnHover",
-					"callback": "rangedFullDamageLimitHighlightOnHoverChanged"
+					"name": "rangeLimitHighlightOnHoverCheckbox",
+					"help": "vcmi.battleOptions.rangeLimitHighlightOnHover",
+					"callback": "rangeLimitHighlightOnHoverChanged"
 				},
 				{
 					"name": "mouseShadowCheckbox",

+ 2 - 1
lib/GameConstants.h

@@ -56,7 +56,8 @@ namespace GameConstants
 	constexpr int HERO_GOLD_COST = 2500;
 	constexpr int SPELLBOOK_GOLD_COST = 500;
 	constexpr int SKILL_GOLD_COST = 2000;
-	constexpr int BATTLE_PENALTY_DISTANCE = 10; //if the distance is > than this, then shooting stack has distance penalty
+	constexpr int BATTLE_SHOOTING_PENALTY_DISTANCE = 10; //if the distance is > than this, then shooting stack has distance penalty
+	constexpr int BATTLE_SHOOTING_RANGE_DISTANCE = std::numeric_limits<uint8_t>::max(); // used when shooting stack has no shooting range limit
 	constexpr int ARMY_SIZE = 7;
 	constexpr int SKILL_PER_HERO = 8;
 	constexpr ui32 HERO_HIGH_LEVEL = 10; // affects primary skill upgrade order

+ 2 - 2
lib/battle/CBattleInfoCallback.cpp

@@ -1456,7 +1456,7 @@ bool CBattleInfoCallback::battleHasDistancePenalty(const IBonusBearer * shooter,
 	if(const auto * target = battleGetUnitByPos(destHex, true))
 	{
 		//If any hex of target creature is within range, there is no penalty
-		int range = GameConstants::BATTLE_PENALTY_DISTANCE;
+		int range = GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE;
 
 		auto bonus = shooter->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE));
 		if(bonus != nullptr && bonus->additionalInfo != CAddInfo::NONE)
@@ -1467,7 +1467,7 @@ bool CBattleInfoCallback::battleHasDistancePenalty(const IBonusBearer * shooter,
 	}
 	else
 	{
-		if(BattleHex::getDistance(shooterPosition, destHex) <= GameConstants::BATTLE_PENALTY_DISTANCE)
+		if(BattleHex::getDistance(shooterPosition, destHex) <= GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE)
 			return false;
 	}
 

+ 19 - 1
lib/battle/CUnitState.cpp

@@ -596,7 +596,7 @@ uint8_t CUnitState::getRangedFullDamageDistance() const
 	if(!isShooter())
 		return 0;
 
-	uint8_t rangedFullDamageDistance = GameConstants::BATTLE_PENALTY_DISTANCE;
+	uint8_t rangedFullDamageDistance = GameConstants::BATTLE_SHOOTING_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)))
@@ -609,6 +609,24 @@ uint8_t CUnitState::getRangedFullDamageDistance() const
 	return rangedFullDamageDistance;
 }
 
+uint8_t CUnitState::getShootingRangeDistance() const
+{
+	if(!isShooter())
+		return 0;
+
+	uint8_t shootingRangeDistance = GameConstants::BATTLE_SHOOTING_RANGE_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)
+			shootingRangeDistance = bonus->val;
+	}
+
+	return shootingRangeDistance;
+}
+
 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

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