瀏覽代碼

Add support for custom icons & descriptions for bonuses

Ivan Savenko 5 月之前
父節點
當前提交
25655184d3

+ 22 - 20
client/windows/CCreatureWindow.cpp

@@ -258,8 +258,8 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
 
 	static const std::array<Point, 2> offset =
 	{
-		Point(6, 4),
-		Point(214, 4)
+		Point(6, 2),
+		Point(214, 2)
 	};
 
 	auto drawBonusSource = [this](int leftRight, Point p, BonusInfo & bi)
@@ -313,8 +313,14 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
 			BonusInfo & bi = parent->activeBonuses[bonusIndex];
 			if (!bi.imagePath.empty())
 				icon[leftRight] = std::make_shared<CPicture>(bi.imagePath, position.x, position.y);
-			name[leftRight] = std::make_shared<CLabel>(position.x + 60, position.y + 2, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.name, 137);
-			description[leftRight] = std::make_shared<CMultiLineLabel>(Rect(position.x + 60, position.y + 20, 137, 26), FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description);
+
+			if (!bi.name.empty())
+			{
+				name[leftRight] = std::make_shared<CLabel>(position.x + 60, position.y + 2, FONT_TINY, ETextAlignment::TOPLEFT, Colors::YELLOW, bi.name, 137);
+				description[leftRight] = std::make_shared<CMultiLineLabel>(Rect(position.x + 60, position.y + 20, 137, 26), FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description);
+			}
+			else
+				description[leftRight] = std::make_shared<CMultiLineLabel>(Rect(position.x + 60, position.y + 2, 137, 50), FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description);
 			drawBonusSource(leftRight, Point(position.x - 1, position.y - 1), bi);
 		}
 	}
@@ -846,12 +852,9 @@ void CStackWindow::init()
 
 void CStackWindow::initBonusesList()
 {
-	auto inputPtr = info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all);
-
-	BonusList output;
-	BonusList input = *inputPtr;
+	BonusList receivedBonuses = *info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all);
 
-	std::sort(input.begin(), input.end(), [this](std::shared_ptr<Bonus> v1, std::shared_ptr<Bonus> & v2){
+	std::sort(receivedBonuses.begin(), receivedBonuses.end(), [this](std::shared_ptr<Bonus> v1, std::shared_ptr<Bonus> & v2){
 		if (v1->source != v2->source)
 		{
 			int priorityV1 = v1->source == BonusSource::CREATURE_ABILITY ? -1 : static_cast<int>(v1->source);
@@ -862,25 +865,24 @@ void CStackWindow::initBonusesList()
 			return  info->stackNode->bonusToString(v1, false) < info->stackNode->bonusToString(v2, false);
 	});
 
-	while(!input.empty())
-	{
-		auto b = input.front();
-		output.push_back(std::make_shared<Bonus>(*b));
-		output.back()->val = input.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); //merge multiple bonuses into one
-		input.remove_if (Selector::typeSubtype(b->type, b->subtype)); //remove used bonuses
-	}
+	BonusList visibleBonuses;
+
+	for (const auto & bonus : info->stackNode->getExportedBonusList())
+		visibleBonuses.push_back(bonus);
+	for (const auto & bonus : info->creature->getExportedBonusList())
+		visibleBonuses.push_back(bonus);
+	for (const auto & bonus : receivedBonuses)
+		if (bonus->sid.as<CreatureID>() != info->stackNode->getId())
+			visibleBonuses.push_back(bonus);
 
 	BonusInfo bonusInfo;
-	for(auto b : output)
+	for(auto b : visibleBonuses)
 	{
 		bonusInfo.name = info->stackNode->bonusToString(b, false);
 		bonusInfo.description = info->stackNode->bonusToString(b, true);
 		bonusInfo.imagePath = info->stackNode->bonusToGraphics(b);
 		bonusInfo.bonusSource = b->source;
 
-		if(b->sid.as<CreatureID>() != info->stackNode->getId() && b->propagator && b->propagator->getPropagatorType() == CBonusSystemNode::HERO) // Shows bonus with "propagator":"HERO" only at creature with bonus
-			continue;
-
 		//if it's possible to give any description or image for this kind of bonus
 		//TODO: figure out why half of bonuses don't have proper description
 		if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty())

+ 7 - 7
config/battlefields.json

@@ -79,14 +79,14 @@
 				"type" : "MORALE",
 				"val" : 1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.123",
+				"description" : "@core.arraytxt.123",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["good"] }]
 			},
 			{
 				"type" : "MORALE",
 				"val" : -1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.124",
+				"description" : "@core.arraytxt.124",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["evil"] }]
 			}
 		]
@@ -99,7 +99,7 @@
 				"type" : "LUCK",
 				"val" : 2,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.83",
+				"description" : "@core.arraytxt.83",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["neutral"] }]
 			}
 		]
@@ -112,14 +112,14 @@
 				"type" : "MORALE",
 				"val" : -1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.126",
+				"description" : "@core.arraytxt.126",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["good"] }]
 			},
 			{
 				"type" : "MORALE",
 				"val" : 1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "core.arraytxt.125",
+				"description" : "@core.arraytxt.125",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["evil"] }]
 			}
 		]
@@ -132,13 +132,13 @@
 				"type" : "NO_MORALE",
 				"val" : 0,
 				"valueType" : "INDEPENDENT_MIN",
-				"description" : "core.arraytxt.112"
+				"description" : "@core.arraytxt.112"
 			},
 			{
 				"type" : "NO_LUCK",
 				"val" : 0,
 				"valueType" : "INDEPENDENT_MIN",
-				"description" : "core.arraytxt.81"
+				"description" : "@core.arraytxt.81"
 			},
 			{
 				"type" : "BLOCK_MAGIC_ABOVE",

+ 3 - 0
config/creatures/dungeon.json

@@ -238,6 +238,7 @@
 			{
 				"type" : "SPELL_AFTER_ATTACK",
 				"subtype" : "spell.stoneGaze",
+				"description" : "Medusa are able to turn anyone who looked at them to stone",
 				"val" : 20,
 				"addInfo" : [0,2]
 			}
@@ -282,6 +283,8 @@
 			{
 				"type" : "SPELL_AFTER_ATTACK",
 				"subtype" : "spell.stoneGaze",
+				"description" : "{Eyes of Petrification}\nMedusa Queens have a 40% chance of turning anyone they look at to stone",
+				"icon" : "zvs/Lib1.res/unused/stone",
 				"val" : 20,
 				"addInfo" : [0,2]
 			}

+ 3 - 0
config/creatures/necropolis.json

@@ -438,6 +438,8 @@
 				"stacking" : "Undead Dragons",
 				"propagator": "BATTLE_WIDE",
 				"propagationUpdater" : "BONUS_OWNER_UPDATER",
+				"icon" : "zvs/Lib1.res/unused/negativemorale"
+				"description" : "{Intimidating Presence}\Bone Dragons reduce morale of all enemy units by 1",
 				"limiters" : [ "OPPOSITE_SIDE" ]
 			},
 			"KING_1" : // Will be affected by Slayer with no expertise
@@ -493,6 +495,7 @@
 				"stacking" : "Undead Dragons",
 				"propagator": "BATTLE_WIDE",
 				"propagationUpdater" : "BONUS_OWNER_UPDATER",
+				"description" : "{Intimidating Presence}\nReduces morale of all enemy units by 1",
 				"limiters" : [ "OPPOSITE_SIDE" ]
 			},
 			"KING_1" : // Will be affected by Slayer with no expertise

+ 6 - 1
lib/CCreatureHandler.cpp

@@ -228,6 +228,11 @@ std::string CCreature::getDescriptionTextID() const
 	return TextIdentifier("creatures", modScope, identifier, "description").get();
 }
 
+std::string CCreature::getBonusTextID(const std::string & bonusID) const
+{
+	return TextIdentifier("creatures", modScope, identifier, "bonus", bonusID).get();
+}
+
 CCreature::CreatureQuantityId CCreature::getQuantityID(const int & quantity)
 {
 	if (quantity<5)
@@ -904,7 +909,7 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c
 		{
 			if (!ability.second.isNull())
 			{
-				auto b = JsonUtils::parseBonus(ability.second);
+				auto b = JsonUtils::parseBonus(ability.second, creature->getBonusTextID(ability.first));
 				b->source = BonusSource::CREATURE_ABILITY;
 				b->sid = BonusSourceID(creature->getId());
 				b->duration = BonusDuration::PERMANENT;

+ 1 - 0
lib/CCreatureHandler.h

@@ -56,6 +56,7 @@ class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode
 public:
 	std::string getDescriptionTranslated() const;
 	std::string getDescriptionTextID() const;
+	std::string getBonusTextID(const std::string & bonusID) const;
 
 	ui32 ammMin; // initial size of stack of these creatures on adventure map (if not set in editor)
 	ui32 ammMax;

+ 9 - 0
lib/CCreatureSet.cpp

@@ -837,11 +837,20 @@ void CStackInstance::setCount(TQuantity newCount)
 
 std::string CStackInstance::bonusToString(const std::shared_ptr<Bonus>& bonus, bool description) const
 {
+	if (!bonus->description.empty())
+	{
+		if (description)
+			return bonus->description.toString();
+		else
+			return {};
+	}
 	return LIBRARY->getBth()->bonusToString(bonus, this, description);
 }
 
 ImagePath CStackInstance::bonusToGraphics(const std::shared_ptr<Bonus> & bonus) const
 {
+	if (!bonus->customIconPath.empty())
+		return bonus->customIconPath;
 	return LIBRARY->getBth()->bonusToGraphics(bonus);
 }
 

+ 5 - 2
lib/CSkillHandler.cpp

@@ -109,8 +109,11 @@ void CSkill::addNewBonus(const std::shared_ptr<Bonus> & b, int level)
 	b->source = BonusSource::SECONDARY_SKILL;
 	b->sid = BonusSourceID(id);
 	b->duration = BonusDuration::PERMANENT;
-	b->description.appendTextID(getNameTextID());
-	b->description.appendRawString(" %+d");
+	if (b->description.empty() && (b->type == BonusType::LUCK || b->type == BonusType::MORALE))
+	{
+		b->description.appendTextID(getNameTextID());
+		b->description.appendRawString(" %+d");
+	}
 	levels[level-1].effects.push_back(b);
 }
 

+ 4 - 0
lib/bonuses/Bonus.h

@@ -15,6 +15,7 @@
 #include "../constants/EntityIdentifiers.h"
 #include "../serializer/Serializeable.h"
 #include "../texts/MetaString.h"
+#include "../filesystem/ResourcePath.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -79,6 +80,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>, public Se
 	TUpdaterPtr updater;
 	TUpdaterPtr propagationUpdater;
 
+	ImagePath customIconPath;
 	MetaString description;
 
 	Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID);
@@ -95,6 +97,8 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>, public Se
 		h & val;
 		h & sid;
 		h & description;
+		if (h.hasFeature(Handler::Version::CUSTOM_BONUS_ICONS))
+			h & customIconPath;
 		h & additionalInfo;
 		h & turnsRemain;
 		h & valType;

+ 0 - 2
lib/bonuses/BonusList.h

@@ -80,8 +80,6 @@ public:
 		std::copy(newList.begin(), newList.end(), bonuses.begin());
 	}
 
-	template <class InputIterator>
-	void insert(const int position, InputIterator first, InputIterator last);
 	void insert(TInternalContainer::iterator position, TInternalContainer::size_type n, const std::shared_ptr<Bonus> & x);
 
 	template <typename Handler>

+ 5 - 2
lib/entities/artifact/CArtifact.cpp

@@ -332,8 +332,11 @@ void CArtifact::addNewBonus(const std::shared_ptr<Bonus>& b)
 {
 	b->source = BonusSource::ARTIFACT;
 	b->duration = BonusDuration::PERMANENT;
-	b->description.appendTextID(getNameTextID());
-	b->description.appendRawString(" %+d");
+	if (b->description.empty() && (b->type == BonusType::LUCK || b->type == BonusType::MORALE))
+	{
+		b->description.appendTextID(getNameTextID());
+		b->description.appendRawString(" %+d");
+	}
 	CBonusSystemNode::addNewBonus(b);
 }
 

+ 16 - 5
lib/json/JsonBonus.cpp

@@ -615,10 +615,10 @@ std::shared_ptr<ILimiter> JsonUtils::parseLimiter(const JsonNode & limiter)
 	return nullptr;
 }
 
-std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability)
+std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability, const TextIdentifier & descriptionID)
 {
 	auto b = std::make_shared<Bonus>();
-	if (!parseBonus(ability, b.get()))
+	if (!parseBonus(ability, b.get(), descriptionID))
 	{
 		// caller code can not handle this case and presumes that returned bonus is always valid
 		logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toString());
@@ -628,7 +628,7 @@ std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability)
 	return b;
 }
 
-bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
+bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b, const TextIdentifier & descriptionID)
 {
 	const JsonNode * value = nullptr;
 
@@ -671,12 +671,23 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 
 	if(!ability["description"].isNull())
 	{
-		if (ability["description"].isString())
-			b->description.appendTextID(ability["description"].String());
+		if (ability["description"].isString() && !ability["description"].String().empty())
+		{
+			if (ability["description"].String()[0] == '@')
+				b->description.appendTextID(ability["description"].String());
+			else if (!descriptionID.get().empty())
+			{
+				LIBRARY->generaltexth->registerString(ability.getModScope(), descriptionID, ability["description"]);
+				b->description.appendTextID(descriptionID.get());
+			}
+		}
 		if (ability["description"].isNumber())
 			b->description.appendTextID("core.arraytxt." + std::to_string(ability["description"].Integer()));
 	}
 
+	if(!ability["icon"].isNull())
+		b->customIconPath = ImagePath::fromJson(ability["icon"]);
+
 	value = &ability["effectRange"];
 	if (!value->isNull())
 		b->effectRange = static_cast<BonusLimitEffect>(parseByMapN(bonusLimitEffect, value, "effect range "));

+ 3 - 3
lib/json/JsonBonus.h

@@ -11,6 +11,7 @@
 
 #include "JsonNode.h"
 #include "../GameConstants.h"
+#include "../texts/TextIdentifier.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -18,12 +19,11 @@ struct Bonus;
 class ILimiter;
 class CSelector;
 class CAddInfo;
-
 namespace JsonUtils
 {
 	std::shared_ptr<Bonus> parseBonus(const JsonVector & ability_vec);
-	std::shared_ptr<Bonus> parseBonus(const JsonNode & ability);
-	bool parseBonus(const JsonNode & ability, Bonus * placement);
+	std::shared_ptr<Bonus> parseBonus(const JsonNode & ability, const TextIdentifier & descriptionID = "");
+	bool parseBonus(const JsonNode & ability, Bonus * placement, const TextIdentifier & descriptionID = "");
 	std::shared_ptr<ILimiter> parseLimiter(const JsonNode & limiter);
 	CSelector parseSelector(const JsonNode &ability);
 	void resolveAddInfo(CAddInfo & var, const JsonNode & node);

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -42,8 +42,9 @@ enum class ESerializationVersion : int32_t
 	REWARDABLE_EXTENSIONS, // new functionality for rewardable objects
 	FLAGGABLE_BONUS_SYSTEM_NODE, // flaggable objects now contain bonus system node
 	RANDOMIZATION_REWORK, // random rolls logic has been moved to server
+	CUSTOM_BONUS_ICONS, // support for custom icons in bonuses
 
-	CURRENT = RANDOMIZATION_REWORK,
+	CURRENT = CUSTOM_BONUS_ICONS,
 };
 
 static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!");