1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-07-03 00:46:55 +02:00

Add support for custom icons & descriptions for bonuses

This commit is contained in:
Ivan Savenko
2025-05-19 23:30:41 +03:00
parent b806775f3a
commit 25655184d3
14 changed files with 86 additions and 43 deletions

View File

@ -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);
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 receivedBonuses = *info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all);
BonusList output;
BonusList input = *inputPtr;
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())

View File

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

View File

@ -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]
}

View File

@ -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

View File

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

View File

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

View File

@ -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);
}

View File

@ -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;
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);
}

View File

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

View File

@ -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>

View File

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

View File

@ -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())
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 "));

View File

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

View File

@ -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!");