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 = static const std::array<Point, 2> offset =
{ {
Point(6, 4), Point(6, 2),
Point(214, 4) Point(214, 2)
}; };
auto drawBonusSource = [this](int leftRight, Point p, BonusInfo & bi) 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]; BonusInfo & bi = parent->activeBonuses[bonusIndex];
if (!bi.imagePath.empty()) if (!bi.imagePath.empty())
icon[leftRight] = std::make_shared<CPicture>(bi.imagePath, position.x, position.y); 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); drawBonusSource(leftRight, Point(position.x - 1, position.y - 1), bi);
} }
} }
@ -846,12 +852,9 @@ void CStackWindow::init()
void CStackWindow::initBonusesList() 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; std::sort(receivedBonuses.begin(), receivedBonuses.end(), [this](std::shared_ptr<Bonus> v1, std::shared_ptr<Bonus> & v2){
BonusList input = *inputPtr;
std::sort(input.begin(), input.end(), [this](std::shared_ptr<Bonus> v1, std::shared_ptr<Bonus> & v2){
if (v1->source != v2->source) if (v1->source != v2->source)
{ {
int priorityV1 = v1->source == BonusSource::CREATURE_ABILITY ? -1 : static_cast<int>(v1->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); return info->stackNode->bonusToString(v1, false) < info->stackNode->bonusToString(v2, false);
}); });
while(!input.empty()) BonusList visibleBonuses;
{
auto b = input.front(); for (const auto & bonus : info->stackNode->getExportedBonusList())
output.push_back(std::make_shared<Bonus>(*b)); visibleBonuses.push_back(bonus);
output.back()->val = input.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); //merge multiple bonuses into one for (const auto & bonus : info->creature->getExportedBonusList())
input.remove_if (Selector::typeSubtype(b->type, b->subtype)); //remove used bonuses visibleBonuses.push_back(bonus);
} for (const auto & bonus : receivedBonuses)
if (bonus->sid.as<CreatureID>() != info->stackNode->getId())
visibleBonuses.push_back(bonus);
BonusInfo bonusInfo; BonusInfo bonusInfo;
for(auto b : output) for(auto b : visibleBonuses)
{ {
bonusInfo.name = info->stackNode->bonusToString(b, false); bonusInfo.name = info->stackNode->bonusToString(b, false);
bonusInfo.description = info->stackNode->bonusToString(b, true); bonusInfo.description = info->stackNode->bonusToString(b, true);
bonusInfo.imagePath = info->stackNode->bonusToGraphics(b); bonusInfo.imagePath = info->stackNode->bonusToGraphics(b);
bonusInfo.bonusSource = b->source; 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 //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 //TODO: figure out why half of bonuses don't have proper description
if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty()) if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty())

View File

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

View File

@ -238,6 +238,7 @@
{ {
"type" : "SPELL_AFTER_ATTACK", "type" : "SPELL_AFTER_ATTACK",
"subtype" : "spell.stoneGaze", "subtype" : "spell.stoneGaze",
"description" : "Medusa are able to turn anyone who looked at them to stone",
"val" : 20, "val" : 20,
"addInfo" : [0,2] "addInfo" : [0,2]
} }
@ -282,6 +283,8 @@
{ {
"type" : "SPELL_AFTER_ATTACK", "type" : "SPELL_AFTER_ATTACK",
"subtype" : "spell.stoneGaze", "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, "val" : 20,
"addInfo" : [0,2] "addInfo" : [0,2]
} }

View File

@ -438,6 +438,8 @@
"stacking" : "Undead Dragons", "stacking" : "Undead Dragons",
"propagator": "BATTLE_WIDE", "propagator": "BATTLE_WIDE",
"propagationUpdater" : "BONUS_OWNER_UPDATER", "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" ] "limiters" : [ "OPPOSITE_SIDE" ]
}, },
"KING_1" : // Will be affected by Slayer with no expertise "KING_1" : // Will be affected by Slayer with no expertise
@ -493,6 +495,7 @@
"stacking" : "Undead Dragons", "stacking" : "Undead Dragons",
"propagator": "BATTLE_WIDE", "propagator": "BATTLE_WIDE",
"propagationUpdater" : "BONUS_OWNER_UPDATER", "propagationUpdater" : "BONUS_OWNER_UPDATER",
"description" : "{Intimidating Presence}\nReduces morale of all enemy units by 1",
"limiters" : [ "OPPOSITE_SIDE" ] "limiters" : [ "OPPOSITE_SIDE" ]
}, },
"KING_1" : // Will be affected by Slayer with no expertise "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(); 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) CCreature::CreatureQuantityId CCreature::getQuantityID(const int & quantity)
{ {
if (quantity<5) if (quantity<5)
@ -904,7 +909,7 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c
{ {
if (!ability.second.isNull()) 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->source = BonusSource::CREATURE_ABILITY;
b->sid = BonusSourceID(creature->getId()); b->sid = BonusSourceID(creature->getId());
b->duration = BonusDuration::PERMANENT; b->duration = BonusDuration::PERMANENT;

View File

@ -56,6 +56,7 @@ class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode
public: public:
std::string getDescriptionTranslated() const; std::string getDescriptionTranslated() const;
std::string getDescriptionTextID() 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 ammMin; // initial size of stack of these creatures on adventure map (if not set in editor)
ui32 ammMax; 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 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); return LIBRARY->getBth()->bonusToString(bonus, this, description);
} }
ImagePath CStackInstance::bonusToGraphics(const std::shared_ptr<Bonus> & bonus) const ImagePath CStackInstance::bonusToGraphics(const std::shared_ptr<Bonus> & bonus) const
{ {
if (!bonus->customIconPath.empty())
return bonus->customIconPath;
return LIBRARY->getBth()->bonusToGraphics(bonus); 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->source = BonusSource::SECONDARY_SKILL;
b->sid = BonusSourceID(id); b->sid = BonusSourceID(id);
b->duration = BonusDuration::PERMANENT; b->duration = BonusDuration::PERMANENT;
b->description.appendTextID(getNameTextID()); if (b->description.empty() && (b->type == BonusType::LUCK || b->type == BonusType::MORALE))
b->description.appendRawString(" %+d"); {
b->description.appendTextID(getNameTextID());
b->description.appendRawString(" %+d");
}
levels[level-1].effects.push_back(b); levels[level-1].effects.push_back(b);
} }

View File

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

View File

@ -80,8 +80,6 @@ public:
std::copy(newList.begin(), newList.end(), bonuses.begin()); 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); void insert(TInternalContainer::iterator position, TInternalContainer::size_type n, const std::shared_ptr<Bonus> & x);
template <typename Handler> template <typename Handler>

View File

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

View File

@ -615,10 +615,10 @@ std::shared_ptr<ILimiter> JsonUtils::parseLimiter(const JsonNode & limiter)
return nullptr; 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>(); 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 // 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()); 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; 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; const JsonNode * value = nullptr;
@ -671,12 +671,23 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
if(!ability["description"].isNull()) if(!ability["description"].isNull())
{ {
if (ability["description"].isString()) if (ability["description"].isString() && !ability["description"].String().empty())
b->description.appendTextID(ability["description"].String()); {
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()) if (ability["description"].isNumber())
b->description.appendTextID("core.arraytxt." + std::to_string(ability["description"].Integer())); b->description.appendTextID("core.arraytxt." + std::to_string(ability["description"].Integer()));
} }
if(!ability["icon"].isNull())
b->customIconPath = ImagePath::fromJson(ability["icon"]);
value = &ability["effectRange"]; value = &ability["effectRange"];
if (!value->isNull()) if (!value->isNull())
b->effectRange = static_cast<BonusLimitEffect>(parseByMapN(bonusLimitEffect, value, "effect range ")); b->effectRange = static_cast<BonusLimitEffect>(parseByMapN(bonusLimitEffect, value, "effect range "));

View File

@ -11,6 +11,7 @@
#include "JsonNode.h" #include "JsonNode.h"
#include "../GameConstants.h" #include "../GameConstants.h"
#include "../texts/TextIdentifier.h"
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN
@ -18,12 +19,11 @@ struct Bonus;
class ILimiter; class ILimiter;
class CSelector; class CSelector;
class CAddInfo; class CAddInfo;
namespace JsonUtils namespace JsonUtils
{ {
std::shared_ptr<Bonus> parseBonus(const JsonVector & ability_vec); std::shared_ptr<Bonus> parseBonus(const JsonVector & ability_vec);
std::shared_ptr<Bonus> parseBonus(const JsonNode & ability); std::shared_ptr<Bonus> parseBonus(const JsonNode & ability, const TextIdentifier & descriptionID = "");
bool parseBonus(const JsonNode & ability, Bonus * placement); bool parseBonus(const JsonNode & ability, Bonus * placement, const TextIdentifier & descriptionID = "");
std::shared_ptr<ILimiter> parseLimiter(const JsonNode & limiter); std::shared_ptr<ILimiter> parseLimiter(const JsonNode & limiter);
CSelector parseSelector(const JsonNode &ability); CSelector parseSelector(const JsonNode &ability);
void resolveAddInfo(CAddInfo & var, const JsonNode & node); 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 REWARDABLE_EXTENSIONS, // new functionality for rewardable objects
FLAGGABLE_BONUS_SYSTEM_NODE, // flaggable objects now contain bonus system node FLAGGABLE_BONUS_SYSTEM_NODE, // flaggable objects now contain bonus system node
RANDOMIZATION_REWORK, // random rolls logic has been moved to server 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!"); static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!");