Browse Source

[HotA] Add DAMAGE_RECEIVED_CAP bonus (#6436)

Andrej Dudenhefner 4 days ago
parent
commit
9056050374

+ 1 - 0
Mods/vcmi/Content/config/english.json

@@ -1113,6 +1113,7 @@
 	"core.bonus.UNDEAD.description" : "{Undead}\nCreature is Undead and is immune to effects that only affect living",
 	"core.bonus.UNLIMITED_RETALIATIONS.description" : "{Unlimited retaliations}\nThis unit can retaliate against an unlimited number of attacks",
 	"core.bonus.WIDE_BREATH.description" : "{Wide breath}\nThis unit attacks all units around its target",
+	"core.bonus.DAMAGE_RECEIVED_CAP.description" : "{Damage limit (${val}%) }\nNo single attack can deal more damage than ${val}% of max health",
 
 	"spell.core.castleMoat.name" : "Moat",
 	"spell.core.castleMoatTrigger.name" : "Moat",

+ 18 - 4
client/windows/CCreatureWindow.cpp

@@ -837,6 +837,7 @@ void CStackWindow::init()
 
 	if(!info->stackNode)
 	{
+		//does not contain any propagated bonuses
 		fakeNode = std::make_unique<CStackInstance>(nullptr, info->creature->getId(), 1, true);
 		info->stackNode = fakeNode.get();
 	}
@@ -856,14 +857,26 @@ void CStackWindow::init()
 
 void CStackWindow::initBonusesList()
 {
-	BonusList receivedBonuses = *info->stackNode->getBonuses(CSelector(Bonus::Permanent));
+	const IBonusBearer * bonusSource = (info->stack && !info->stack->base) 
+    ? static_cast<const IBonusBearer*>(info->stack)  // Use CStack for war machines
+    : static_cast<const IBonusBearer*>(info->stackNode);  // Use CStackInstance for regular units
+
+	auto bonusToString = [bonusSource](const std::shared_ptr<Bonus> & bonus) -> std::string
+	{
+		if(!bonus->description.empty())
+			return bonus->description.toString();
+		else
+			return LIBRARY->getBth()->bonusToString(bonus, bonusSource);
+	};
+
+	BonusList receivedBonuses = *bonusSource->getBonuses(CSelector(Bonus::Permanent));
 	BonusList abilities = info->creature->getExportedBonusList();
 
 	// remove all bonuses that are not propagated away
 	// such bonuses should be present in received bonuses, and if not - this means that they are behind inactive limiter, such as inactive stack experience bonuses
 	abilities.remove_if([](const Bonus* b){ return b->propagator == nullptr;});
 
-	const auto & bonusSortingPredicate = [this](const std::shared_ptr<Bonus> & v1, const std::shared_ptr<Bonus> & v2){
+	const auto & bonusSortingPredicate = [bonusToString](const std::shared_ptr<Bonus> & v1, const std::shared_ptr<Bonus> & v2){
 		if (v1->source != v2->source)
 		{
 			int priorityV1 = v1->source == BonusSource::CREATURE_ABILITY ? -1 : static_cast<int>(v1->source);
@@ -871,7 +884,7 @@ void CStackWindow::initBonusesList()
 			return priorityV1 < priorityV2;
 		}
 		else
-			return  info->stackNode->bonusToString(v1) < info->stackNode->bonusToString(v2);
+			return bonusToString(v1) < bonusToString(v2);
 	};
 
 	receivedBonuses.remove_if([](const Bonus* b)
@@ -939,7 +952,8 @@ void CStackWindow::initBonusesList()
 	BonusInfo bonusInfo;
 	for(auto b : visibleBonuses)
 	{
-		bonusInfo.description = info->stackNode->bonusToString(b);
+		bonusInfo.description = bonusToString(b);
+		//FIXME: we possibly use fakeNode, which does not have the correct bonus value
 		bonusInfo.imagePath = info->stackNode->bonusToGraphics(b);
 		bonusInfo.bonusSource = b->source;
 

+ 5 - 1
config/bonuses.json

@@ -205,7 +205,11 @@
 			"bonusSubtype.damageTypeMelee" : null,
 		}
 	},
-	
+
+	"DAMAGE_RECEIVED_CAP":
+	{
+	},
+
 	"GENERATE_RESOURCE":
 	{
 		"blockDescriptionPropagation": true

+ 6 - 0
docs/modders/Bonus/Bonus_Types.md

@@ -648,6 +648,12 @@ Affected units will receive reduced damage from attacks by other units
   - damageTypeRanged: only ranged damage will be reduced
   - damageTypeAll: all damage will be reduced
 
+### DAMAGE_RECEIVED_CAP
+
+Limits maximal damage received by affected units based on max hp (HotA war machines)
+
+- val: maximal damage limit, percentage of max hp
+
 ### PERCENTAGE_DAMAGE_BOOST
 
 Affected units will deal increased damage when attacking other units

+ 15 - 2
lib/battle/DamageCalculator.cpp

@@ -541,6 +541,18 @@ int DamageCalculator::battleBonusValue(const IBonusBearer * bearer, const CSelec
 	return bearer->getBonuses(selector)->valOfBonuses(noLimit.Or(limitMatches));
 };
 
+int64_t DamageCalculator::getDamageCap() const
+{
+	const std::string cachingStrDamageCap = "type_DAMAGE_RECEIVED_CAP";
+	static const auto selectorDamageCap = Selector::type()(BonusType::DAMAGE_RECEIVED_CAP);
+
+	int damageCapPercentage = info.defender->valOfBonuses(selectorDamageCap, cachingStrDamageCap);
+	if (damageCapPercentage <= 0)
+		return std::numeric_limits<int64_t>::max();
+
+	return info.defender->getMaxHealth() * damageCapPercentage / 100;
+}
+
 DamageEstimation DamageCalculator::calculateDmgRange() const
 {
 	DamageRange damageBase = getBaseDamageStack();
@@ -566,12 +578,13 @@ DamageEstimation DamageCalculator::calculateDmgRange() const
 	double resultingFactor = attackFactorTotal * defenseFactorTotal;
 
 	int64_t avail = info.defender->getAvailableHealth();
+	int64_t cap = getDamageCap();
 
 	auto dmin = std::max<int64_t>(1.0, std::floor(damageBase.min * resultingFactor));
 	auto dmax = std::max<int64_t>(1.0, std::floor(damageBase.max * resultingFactor));
 
-	dmin = std::min(dmin, avail);
-	dmax = std::min(dmax, avail);
+	dmin = std::min({dmin, avail, cap});
+	dmax = std::min({dmax, avail, cap});
 
 	DamageRange damageDealt{ dmin, dmax };
 

+ 1 - 0
lib/battle/DamageCalculator.h

@@ -66,6 +66,7 @@ class DLL_LINKAGE DamageCalculator
 	double getDefensePetrificationFactor() const;
 	double getDefenseMagicFactor() const;
 	double getDefenseMindFactor() const;
+	int64_t getDamageCap() const;
 
 	std::vector<double> getAttackFactors() const;
 	std::vector<double> getDefenseFactors() const;

+ 1 - 0
lib/bonuses/BonusEnum.h

@@ -195,6 +195,7 @@ class JsonNode;
 	BONUS_NAME(COMBAT_MANA_BONUS) /* Additional mana per combat */ \
 	BONUS_NAME(SPECIFIC_SPELL_RANGE) /* value used for allowed spell range, subtype - spell id */\
 	BONUS_NAME(HATES_TRAIT) /* affected unit deals additional damage to units with specific bonus. subtype - bonus, val - damage bonus percent */ \
+	BONUS_NAME(DAMAGE_RECEIVED_CAP) /* limits the damage dealt to affected unit */ \
 	/* end of list */