Просмотр исходного кода

Merge pull request #2156 from krs0/feature/Highlight_For_Ranged_Full_Damage_Limit

Highlight Ranged Full Damage Limit
Ivan Savenko 2 лет назад
Родитель
Сommit
3d5ea269ca

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",

+ 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);

+ 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;

+ 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);

+ 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",

+ 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;