1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-07-15 01:24:45 +02:00

Configurable icons for bonuses

It is now possible for mods (e.g. vcmi extras) to provide custom icons
for bonuses subtypes or for custom bonuses values without requiring
hardcoded check in vcmi.

All existing hardcoded checks have been removed.

Bonuses config json from mods is now actually loaded.
This commit is contained in:
Ivan Savenko
2025-02-11 22:08:45 +00:00
parent 8f074490a7
commit 07a46ed03b
7 changed files with 58 additions and 462 deletions

View File

@ -310,6 +310,7 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
if(parent->activeBonuses.size() > bonusIndex)
{
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);

View File

@ -1,54 +1,26 @@
//TODO: selector-based config
// school immunities
// LEVEL_SPELL_IMMUNITY
{
"ADDITIONAL_ATTACK":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_DOUBLE"
}
},
"ADDITIONAL_RETALIATION":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_RETAIL1"
}
},
"ATTACKS_ALL_ADJACENT":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_ROUND"
}
},
"BLOCKS_RANGED_RETALIATION":
{
"graphics":
{
"icon": "zvs/Lib1.res/RANGEDBLOCK"
}
},
"BLOCKS_RETALIATION":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_RETAIL"
}
},
"CATAPULT":
{
"graphics":
{
"icon": "zvs/Lib1.res/Catapult"
}
},
"CATAPULT_EXTRA_SHOTS":
@ -58,26 +30,14 @@
"CHANGES_SPELL_COST_FOR_ALLY":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_MANA"
}
},
"CHANGES_SPELL_COST_FOR_ENEMY":
{
"graphics":
{
"icon": "zvs/Lib1.res/MagicDamper"
}
},
"CHARGE_IMMUNITY":
{
"graphics":
{
"icon": "zvs/Lib1.res/ChargeImmune"
}
},
"DARKNESS":
@ -87,42 +47,22 @@
"DEATH_STARE":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_DEATH"
}
},
"DEFENSIVE_STANCE":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_DEFBON"
}
},
"DESTRUCTION":
{
"graphics":
{
"icon": "zvs/Lib1.res/DESTROYER"
}
},
"DOUBLE_DAMAGE_CHANCE":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_DBLOW"
}
},
"DRAGON_NATURE":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_DRAGON"
}
},
"DISGUISED":
@ -132,148 +72,74 @@
"ENCHANTER":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_CAST1"
}
},
"ENCHANTED":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_BLESS"
}
},
"ENEMY_ATTACK_REDUCTION":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_RATT"
}
},
"ENEMY_DEFENCE_REDUCTION":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_RDEF"
}
},
"FIRE_SHIELD":
{
"graphics":
{
"icon": "zvs/Lib1.res/FireShield"
}
},
"FIRST_STRIKE":
{
"graphics":
{
"icon": "zvs/Lib1.res/FIRSTSTRIKE"
}
},
"FEAR":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_FEAR"
}
},
"FEARLESS":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_FEARL"
}
},
"FEROCITY":
{
"graphics":
{
"icon": "zvs/Lib1.res/Ferocity"
}
},
"FLYING":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_FLY"
}
},
"FREE_SHOOTING":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_SHOOTA"
}
},
"GARGOYLE":
{
"graphics":
{
"icon": "zvs/Lib1.res/NonLiving" // Just use the NonLiving icon for now
}
},
"GENERAL_DAMAGE_REDUCTION":
{
"graphics":
{
"icon": "zvs/Lib1.res/DamageReductionMelee"
}
},
"HATE":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_HATE"
}
},
"HEALER":
{
"graphics":
{
"icon": "zvs/Lib1.res/Healer"
}
},
"HP_REGENERATION":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_TROLL"
}
},
"JOUSTING":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_CHAMP"
}
},
"KING":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_KING3"
}
},
"LEARN_BATTLE_SPELL_CHANCE":
@ -288,66 +154,34 @@
"LEVEL_SPELL_IMMUNITY":
{
"graphics":
{
"icon": ""
}
},
"LIFE_DRAIN":
{
"graphics":
{
"icon": "zvs/Lib1.res/DrainLife"
}
},
"LIMITED_SHOOTING_RANGE":
{
"graphics":
{
"icon": "zvs/Lib1.res/LIM_SHOOT"
}
},
"MANA_CHANNELING":
{
"graphics":
{
"icon": "zvs/Lib1.res/ManaChannel"
}
},
"MANA_DRAIN":
{
"graphics":
{
"icon": "zvs/Lib1.res/ManaDrain"
}
},
"MAGIC_MIRROR":
{
"graphics":
{
"icon": "zvs/Lib1.res/MagicMirror"
}
},
"MAGIC_RESISTANCE":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_DWARF"
}
},
"MIND_IMMUNITY":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_MIND"
}
},
"NONE":
@ -357,34 +191,18 @@
"NO_DISTANCE_PENALTY":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_DIST"
}
},
"NO_MELEE_PENALTY":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_MELEE"
}
},
"NO_MORALE":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_MORAL"
}
},
"NO_WALL_PENALTY":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_OBST"
}
},
"NO_TERRAIN_PENALTY":
@ -394,34 +212,18 @@
"NON_LIVING":
{
"graphics":
{
"icon": "zvs/Lib1.res/NonLiving"
}
},
"MECHANICAL":
{
"graphics":
{
"icon": "zvs/Lib1.res/Mechanical"
}
},
"OPENING_BATTLE_SPELL":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_SPDFIRE"
}
},
"RANDOM_SPELLCASTER":
{
"graphics":
{
"icon": "zvs/Lib1.res/RandomBoost"
}
},
"PERCENTAGE_DAMAGE_BOOST":
@ -431,114 +233,58 @@
"RANGED_RETALIATION":
{
"graphics":
{
"icon": "zvs/Lib1.res/RANGEDCOUNTER"
}
},
"RECEPTIVE":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_NOFRIM"
}
},
"REBIRTH":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_REBIRTH"
}
},
"RETURN_AFTER_STRIKE":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_HARPY"
}
},
"REVENGE":
{
"graphics":
{
"icon": "zvs/Lib1.res/Revenge"
}
},
"SHOOTER":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_SHOOT"
}
},
"SHOOTS_ALL_ADJACENT":
{
"graphics":
{
"icon": "zvs/Lib1.res/AREASHOT"
}
},
"SOUL_STEAL":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_SUMMON2"
}
},
"SPELLCASTER":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_CASTER"
}
},
"SPELL_AFTER_ATTACK":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_CAST"
}
},
"SPELL_BEFORE_ATTACK":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_CAST2"
}
},
"SPELL_DAMAGE_REDUCTION":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_GOLEM"
}
},
"SPELL_IMMUNITY":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_SPDISB" //todo: configurable use from spell handler
}
},
"SPELL_LIKE_ATTACK":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_SPDFIRE"
}
},
"SPELL_SCHOOL_IMMUNITY":
@ -547,66 +293,34 @@
"SPELL_RESISTANCE_AURA":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_UNIC"
}
},
"SUMMON_GUARDIANS":
{
"graphics":
{
"icon": "zvs/Lib1.res/SUMMONGUARDS"
}
},
"TWO_HEX_ATTACK_BREATH":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_BREATH"
}
},
"PRISM_HEX_ATTACK_BREATH":
{
"graphics":
{
"icon": "zvs/Lib1.res/PrismBreath"
}
},
"THREE_HEADED_ATTACK":
{
"graphics":
{
"icon": "zvs/Lib1.res/ThreeHeaded"
}
},
"TRANSMUTATION":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_SGTYPE"
}
},
"UNDEAD":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_UNDEAD"
}
},
"UNLIMITED_RETALIATIONS":
{
"graphics":
{
"icon": "zvs/Lib1.res/E_RETAIL1"
}
},
"VISIONS":
@ -616,28 +330,14 @@
"WIDE_BREATH":
{
"graphics":
{
"icon": "zvs/Lib1.res/MEGABREATH"
}
},
"DISINTEGRATE":
{
"graphics":
{
"icon": "zvs/Lib1.res/DISINTEGRATE"
}
},
"INVINCIBLE":
{
"graphics":
{
"icon": "zvs/Lib1.res/INVINCIBLE"
}
}
}

View File

@ -19,6 +19,7 @@
#include "GameConstants.h"
#include "GameLibrary.h"
#include "modding/ModScope.h"
#include "modding/IdentifierStorage.h"
#include "spells/CSpellHandler.h"
#include "texts/CGeneralTextHandler.h"
#include "json/JsonUtils.h"
@ -57,14 +58,9 @@ CBonusTypeHandler::CBonusTypeHandler()
BONUS_LIST;
#undef BONUS_NAME
load();
}
CBonusTypeHandler::~CBonusTypeHandler()
{
//dtor
}
CBonusTypeHandler::~CBonusTypeHandler() = default;
std::string CBonusTypeHandler::bonusToString(const std::shared_ptr<Bonus> & bonus, const IBonusBearer * bearer, bool description) const
{
@ -97,152 +93,43 @@ std::string CBonusTypeHandler::bonusToString(const std::shared_ptr<Bonus> & bonu
}
ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr<Bonus> & bonus) const
{
std::string fileName;
bool fullPath = false;
switch(bonus->type)
{
case BonusType::SPELL_IMMUNITY:
{
fullPath = true;
if (bonus->subtype.as<SpellID>().hasValue())
{
const CSpell * sp = bonus->subtype.as<SpellID>().toSpell();
fileName = sp->getIconImmune();
}
break;
}
case BonusType::SPELL_DAMAGE_REDUCTION: //Spell damage reduction for all schools
{
if (bonus->subtype.as<SpellSchool>() == SpellSchool::ANY)
fileName = "E_GOLEM.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::AIR)
fileName = "E_LIGHT.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::FIRE)
fileName = "E_FIRE.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::WATER)
fileName = "E_COLD.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::EARTH)
fileName = "E_SPEATH1.bmp"; //No separate icon for earth damage
break;
}
case BonusType::SPELL_SCHOOL_IMMUNITY: //for all school
{
if (bonus->subtype.as<SpellSchool>() == SpellSchool::AIR)
fileName = "E_SPAIR.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::FIRE)
fileName = "E_SPFIRE.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::WATER)
fileName = "E_SPWATER.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::EARTH)
fileName = "E_SPEATH.bmp";
break;
}
case BonusType::NEGATIVE_EFFECTS_IMMUNITY:
{
if (bonus->subtype.as<SpellSchool>() == SpellSchool::AIR)
fileName = "E_SPAIR1.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::FIRE)
fileName = "E_SPFIRE1.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::WATER)
fileName = "E_SPWATER1.bmp";
if (bonus->subtype.as<SpellSchool>() == SpellSchool::EARTH)
fileName = "E_SPEATH1.bmp";
break;
}
case BonusType::LEVEL_SPELL_IMMUNITY:
{
if(vstd::iswithin(bonus->val, 1, 5))
{
fileName = "E_SPLVL" + std::to_string(bonus->val) + ".bmp";
}
break;
}
case BonusType::KING:
{
if(vstd::iswithin(bonus->val, 0, 3))
{
fileName = "E_KING" + std::to_string(std::max(1, bonus->val)) + ".bmp";
}
break;
}
case BonusType::GENERAL_DAMAGE_REDUCTION:
{
if (bonus->subtype == BonusCustomSubtype::damageTypeMelee)
fileName = "DamageReductionMelee.bmp";
if (bonus->subtype == BonusCustomSubtype::damageTypeRanged)
fileName = "DamageReductionRanged.bmp";
if (bonus->subtype == BonusCustomSubtype::damageTypeAll)
fileName = "DamageReductionAll.bmp";
break;
}
default:
{
const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)];
fileName = bt.icon;
fullPath = true;
}
break;
if (bt.subtypeIcons.count(bonus->subtype.getNum()))
return bt.subtypeIcons.at(bonus->subtype.getNum());
if (bt.valueIcons.count(bonus->val))
return bt.valueIcons.at(bonus->val);
return bt.icon;
}
if(!fileName.empty() && !fullPath)
fileName = "zvs/Lib1.res/" + fileName;
return ImagePath::builtinTODO(fileName);
std::vector<JsonNode> CBonusTypeHandler::loadLegacyData()
{
return {};
}
void CBonusTypeHandler::load()
void CBonusTypeHandler::loadObject(std::string scope, std::string name, const JsonNode & data)
{
JsonNode gameConf(JsonPath::builtin("config/gameConfig.json"));
gameConf.setModScope(ModScope::scopeBuiltin());
JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"]));
config.setModScope("vcmi");
load(config);
}
void CBonusTypeHandler::load(const JsonNode & config)
{
for(const auto & node : config.Struct())
{
auto it = bonusNameMap.find(node.first);
auto it = bonusNameMap.find(name);
if(it == bonusNameMap.end())
{
//TODO: new bonus
// CBonusType bt;
// loadItem(node.second, bt);
//
// auto new_id = bonusTypes.size();
//
// bonusTypes.push_back(bt);
logBonus->warn("Unrecognized bonus name! (%s)", node.first);
logBonus->warn("Unrecognized bonus name! (%s)", name);
}
else
{
CBonusType & bt = bonusTypes[vstd::to_underlying(it->second)];
loadItem(node.second, bt, node.first);
logBonus->trace("Loaded bonus type %s", node.first);
loadItem(data, bt, name);
logBonus->trace("Loaded bonus type %s", name);
}
}
void CBonusTypeHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index)
{
assert(0);
}
void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const
@ -259,7 +146,23 @@ void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, con
const JsonNode & graphics = source["graphics"];
if(!graphics.isNull())
dest.icon = graphics["icon"].String();
dest.icon = ImagePath::fromJson(graphics["icon"]);
for (const auto & additionalIcon : graphics["subtypeIcons"].Struct())
{
auto path = ImagePath::fromJson(additionalIcon.second);
VLC->identifiers()->requestIdentifier(additionalIcon.second.getModScope(), additionalIcon.first, [&dest, path](int32_t index)
{
dest.subtypeIcons[index] = path;
});
}
for (const auto & additionalIcon : graphics["valueIcons"].Struct())
{
auto path = ImagePath::fromJson(additionalIcon.second);
int value = std::stoi(additionalIcon.first);
dest.valueIcons[value] = path;
}
}
VCMI_LIB_NAMESPACE_END

View File

@ -27,18 +27,12 @@ public:
std::string getNameTextID() const;
std::string getDescriptionTextID() const;
template <typename Handler> void serialize(Handler & h)
{
h & icon;
h & identifier;
h & hidden;
}
private:
friend class CBonusTypeHandler;
std::string icon;
ImagePath icon;
std::map<int, ImagePath> subtypeIcons;
std::map<int, ImagePath> valueIcons;
std::string identifier;
bool hidden;
@ -53,16 +47,11 @@ public:
std::string bonusToString(const std::shared_ptr<Bonus> & bonus, const IBonusBearer * bearer, bool description) const override;
ImagePath bonusToGraphics(const std::shared_ptr<Bonus> & bonus) const override;
template <typename Handler> void serialize(Handler & h)
{
//for now always use up to date configuration
//once modded bonus type will be implemented, serialize only them
std::vector<CBonusType> ignore;
h & ignore;
}
std::vector<JsonNode> loadLegacyData() override;
void loadObject(std::string scope, std::string name, const JsonNode & data) override;
void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override;
private:
void load();
void load(const JsonNode & config);
void loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const;
std::vector<CBonusType> bonusTypes; //index = BonusType

View File

@ -51,7 +51,6 @@ namespace scripting
/// Loads and constructs several handlers
class DLL_LINKAGE GameLibrary final : public Services
{
std::shared_ptr<CBonusTypeHandler> bth;
std::shared_ptr<CContentHandler> getContent() const;
void setContent(std::shared_ptr<CContentHandler> content);
@ -78,6 +77,7 @@ public:
const CIdentifierStorage * identifiers() const;
std::shared_ptr<CArtHandler> arth;
std::shared_ptr<CBonusTypeHandler> bth;
std::shared_ptr<CHeroHandler> heroh;
std::shared_ptr<CHeroClassHandler> heroclassesh;
std::shared_ptr<CCreatureHandler> creh;

View File

@ -10,6 +10,7 @@
#pragma once
#include "filesystem/ResourcePath.h"
#include "IHandlerBase.h"
VCMI_LIB_NAMESPACE_BEGIN
@ -18,7 +19,7 @@ struct Bonus;
///High level interface for BonusTypeHandler
class DLL_LINKAGE IBonusTypeHandler
class DLL_LINKAGE IBonusTypeHandler : public IHandlerBase
{
public:
virtual ~IBonusTypeHandler() = default;

View File

@ -23,6 +23,7 @@
#include "../entities/hero/CHeroClassHandler.h"
#include "../entities/hero/CHeroHandler.h"
#include "../texts/CGeneralTextHandler.h"
#include "../CBonusTypeHandler.h"
#include "../CSkillHandler.h"
#include "../CStopWatch.h"
#include "../IGameSettings.h"
@ -241,6 +242,7 @@ void CContentHandler::init()
{
handlers.insert(std::make_pair("heroClasses", ContentTypeHandler(LIBRARY->heroclassesh.get(), "heroClass")));
handlers.insert(std::make_pair("artifacts", ContentTypeHandler(LIBRARY->arth.get(), "artifact")));
handlers.insert(std::make_pair("bonuses", ContentTypeHandler(LIBRARY->bth.get(), "bonus")));
handlers.insert(std::make_pair("creatures", ContentTypeHandler(LIBRARY->creh.get(), "creature")));
handlers.insert(std::make_pair("factions", ContentTypeHandler(LIBRARY->townh.get(), "faction")));
handlers.insert(std::make_pair("objects", ContentTypeHandler(LIBRARY->objtypeh.get(), "object")));