1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-10-08 23:22:25 +02:00
Files
vcmi/lib/bonuses/Limiters.cpp
Ivan Savenko 896a7ec88e Better support for Adela specialty (+new modding functionality for it)
Fixes Adela specialty that was apparently broken back in #1518 and
replaced with logic that was clearly not tested - it was neither
functional, nor it was following H3 behavior.

- `HAS_ANOTHER_BONUS_LIMITER` now accepts `null` in place of bonus type,
for cases when limiting is needed by bonus source or bonus subtype. This
allows Adela Bless specialty to always work, irregardless of which
bonuses are provided by Bless.
- Implemented `DIVIDE_STACK_LEVEL` updater that functions same as
`TIMES_STACK_LEVEL`, but it divides bonus value, instead of multiplying
it (to make Adela specialty weaker for high-tier units, as in H3)
- Implemented `TIMES_HERO_LEVEL_DIVIDE_STACK_LEVEL` updater that
combines two existing updaters, to implement `val * heroLevel /
unitLevel` formula needed for Adela specialty
- Removed deprecated `ARMY_MOVEMENT` updater. Its functionality has
already been removed in 1.6.X releases, and it was remaining only as a
placeholder
- Updated modding documentation to account for these changes & to remove
some TODO's

Fixed regression from #777 that could led to either duplicated bonuses
or to multiple application of updaters. It introduced double-recursion -
node parents were gathered recursively, and then bonuses were also
collected recursively within each parent. This created situation where
updater could be applied different number of times. For example, hero
bonus that is propagated to unit in combat could be selected directly,
or via hero->combat unit chain, or via hero->garrison unit->combat unit
chains, leading to different calls to updaters if updater handles
garrison unit node type
2025-04-13 16:24:51 +03:00

603 lines
16 KiB
C++

/*
* Limiters.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "Limiters.h"
#include "Updaters.h"
#include "../GameLibrary.h"
#include "../entities/faction/CFaction.h"
#include "../entities/faction/CTownHandler.h"
#include "../spells/CSpellHandler.h"
#include "../CCreatureHandler.h"
#include "../CCreatureSet.h"
#include "../texts/CGeneralTextHandler.h"
#include "../CSkillHandler.h"
#include "../CStack.h"
#include "../CArtHandler.h"
#include "../TerrainHandler.h"
#include "../constants/StringConstants.h"
#include "../battle/BattleInfo.h"
#include "../json/JsonUtils.h"
VCMI_LIB_NAMESPACE_BEGIN
const std::map<std::string, TLimiterPtr> bonusLimiterMap =
{
{"SHOOTER_ONLY", std::make_shared<HasAnotherBonusLimiter>(BonusType::SHOOTER)},
{"DRAGON_NATURE", std::make_shared<HasAnotherBonusLimiter>(BonusType::DRAGON_NATURE)},
{"IS_UNDEAD", std::make_shared<HasAnotherBonusLimiter>(BonusType::UNDEAD)},
{"CREATURE_NATIVE_TERRAIN", std::make_shared<CreatureTerrainLimiter>()},
{"CREATURE_FACTION", std::make_shared<AllOfLimiter>(std::initializer_list<TLimiterPtr>{std::make_shared<CreatureLevelLimiter>(), std::make_shared<FactionLimiter>()})},
{"SAME_FACTION", std::make_shared<FactionLimiter>()},
{"CREATURES_ONLY", std::make_shared<CreatureLevelLimiter>()},
{"OPPOSITE_SIDE", std::make_shared<OppositeSideLimiter>()},
};
static const CStack * retrieveStackBattle(const CBonusSystemNode * node)
{
switch(node->getNodeType())
{
case CBonusSystemNode::STACK_BATTLE:
return dynamic_cast<const CStack *>(node);
default:
return nullptr;
}
}
static const CStackInstance * retrieveStackInstance(const CBonusSystemNode * node)
{
switch(node->getNodeType())
{
case CBonusSystemNode::STACK_INSTANCE:
return (dynamic_cast<const CStackInstance *>(node));
case CBonusSystemNode::STACK_BATTLE:
return (dynamic_cast<const CStack *>(node))->base;
default:
return nullptr;
}
}
static const CCreature * retrieveCreature(const CBonusSystemNode *node)
{
switch(node->getNodeType())
{
case CBonusSystemNode::CREATURE:
return (dynamic_cast<const CCreature *>(node));
case CBonusSystemNode::STACK_BATTLE:
return (dynamic_cast<const CStack *>(node))->unitType();
default:
const CStackInstance * csi = retrieveStackInstance(node);
if(csi)
return csi->getCreature();
return nullptr;
}
}
ILimiter::EDecision ILimiter::limit(const BonusLimitationContext &context) const /*return true to drop the bonus */
{
return ILimiter::EDecision::ACCEPT;
}
std::string ILimiter::toString() const
{
return typeid(*this).name();
}
JsonNode ILimiter::toJsonNode() const
{
JsonNode root;
root["type"].String() = toString();
return root;
}
void ILimiter::acceptUpdater(IUpdater& visitor) {}
ILimiter::EDecision CCreatureTypeLimiter::limit(const BonusLimitationContext &context) const
{
const CCreature *c = retrieveCreature(&context.node);
if(!c)
return ILimiter::EDecision::DISCARD;
auto accept = c->getId() == creatureID || (includeUpgrades && creatureID.toCreature()->isMyUpgrade(c));
return accept ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
//drop bonus if it's not our creature and (we don`t check upgrades or its not our upgrade)
}
CCreatureTypeLimiter::CCreatureTypeLimiter(const CCreature & creature_, bool IncludeUpgrades)
: creatureID(creature_.getId()), includeUpgrades(IncludeUpgrades)
{
}
void CCreatureTypeLimiter::setCreature(const CreatureID & id)
{
creatureID = id;
}
std::string CCreatureTypeLimiter::toString() const
{
boost::format fmt("CCreatureTypeLimiter(creature=%s, includeUpgrades=%s)");
fmt % creatureID.toEntity(LIBRARY)->getJsonKey() % (includeUpgrades ? "true" : "false");
return fmt.str();
}
JsonNode CCreatureTypeLimiter::toJsonNode() const
{
JsonNode root;
root["type"].String() = "CREATURE_TYPE_LIMITER";
root["parameters"].Vector().emplace_back(creatureID.toEntity(LIBRARY)->getJsonKey());
root["parameters"].Vector().emplace_back(includeUpgrades);
return root;
}
void CCreatureTypeLimiter::acceptUpdater(IUpdater & visitor)
{
visitor.visitLimiter(*this);
}
HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus )
: type(bonus), isSubtypeRelevant(false), isSourceRelevant(false), isSourceIDRelevant(false)
{
}
HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus, BonusSubtypeID _subtype )
: type(bonus), subtype(_subtype), isSubtypeRelevant(true), isSourceRelevant(false), isSourceIDRelevant(false)
{
}
HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, BonusSource src)
: type(bonus), source(src), isSubtypeRelevant(false), isSourceRelevant(true), isSourceIDRelevant(false)
{
}
HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, BonusSubtypeID _subtype, BonusSource src)
: type(bonus), subtype(_subtype), isSubtypeRelevant(true), source(src), isSourceRelevant(true), isSourceIDRelevant(false)
{
}
ILimiter::EDecision HasAnotherBonusLimiter::limit(const BonusLimitationContext &context) const
{
boost::container::static_vector<CSelector, 4> selectorSegments;
if (type != BonusType::NONE)
selectorSegments.push_back(Selector::type()(type));
if(isSubtypeRelevant)
selectorSegments.push_back(Selector::subtype()(subtype));
if(isSourceRelevant && isSourceIDRelevant)
selectorSegments.push_back(Selector::source(source, sid));
else if (isSourceRelevant)
selectorSegments.push_back(Selector::sourceTypeSel(source));
auto mySelector = selectorSegments.empty() ? Selector::none : selectorSegments[0];
for (size_t i = 1; i <selectorSegments.size(); ++i)
mySelector = mySelector.And(selectorSegments[i]);
//if we have a bonus of required type accepted, limiter should accept also this bonus
if(context.alreadyAccepted.getFirst(mySelector))
return ILimiter::EDecision::ACCEPT;
//if there are no matching bonuses pending, we can (and must) reject right away
if(!context.stillUndecided.getFirst(mySelector))
return ILimiter::EDecision::DISCARD;
//do not accept for now but it may change if more bonuses gets included
return ILimiter::EDecision::NOT_SURE;
}
std::string HasAnotherBonusLimiter::toString() const
{
std::string typeName = vstd::findKey(bonusNameMap, type);
if(isSubtypeRelevant)
{
boost::format fmt("HasAnotherBonusLimiter(type=%s, subtype=%s)");
fmt % typeName % subtype.toString();
return fmt.str();
}
else
{
boost::format fmt("HasAnotherBonusLimiter(type=%s)");
fmt % typeName;
return fmt.str();
}
}
JsonNode HasAnotherBonusLimiter::toJsonNode() const
{
JsonNode root;
std::string typeName = vstd::findKey(bonusNameMap, type);
auto sourceTypeName = vstd::findKey(bonusSourceMap, source);
root["type"].String() = "HAS_ANOTHER_BONUS_LIMITER";
root["parameters"].Vector().emplace_back(typeName);
if(isSubtypeRelevant)
root["parameters"].Vector().emplace_back(subtype.toString());
if(isSourceRelevant)
root["parameters"].Vector().emplace_back(sourceTypeName);
return root;
}
void HasAnotherBonusLimiter::acceptUpdater(IUpdater & visitor)
{
visitor.visitLimiter(*this);
}
ILimiter::EDecision UnitOnHexLimiter::limit(const BonusLimitationContext &context) const
{
const auto * stack = retrieveStackBattle(&context.node);
if(!stack)
return ILimiter::EDecision::DISCARD;
auto accept = false;
for (const auto & hex : stack->getHexes())
accept |= applicableHexes.contains(hex);
return accept ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
}
UnitOnHexLimiter::UnitOnHexLimiter(const BattleHexArray & applicableHexes):
applicableHexes(applicableHexes)
{
}
JsonNode UnitOnHexLimiter::toJsonNode() const
{
JsonNode root;
root["type"].String() = "UNIT_ON_HEXES";
for(const auto & hex : applicableHexes)
root["parameters"].Vector().emplace_back(hex.toInt());
return root;
}
void UnitOnHexLimiter::acceptUpdater(IUpdater& visitor)
{
visitor.visitLimiter(*this);
}
CreatureTerrainLimiter::CreatureTerrainLimiter()
: terrainType(ETerrainId::NATIVE_TERRAIN)
{
}
CreatureTerrainLimiter::CreatureTerrainLimiter(TerrainId terrain):
terrainType(terrain)
{
}
ILimiter::EDecision CreatureTerrainLimiter::limit(const BonusLimitationContext &context) const
{
const CStack *stack = retrieveStackBattle(&context.node);
if(stack)
{
if (terrainType == ETerrainId::NATIVE_TERRAIN && stack->isOnNativeTerrain())//terrainType not specified = native
return ILimiter::EDecision::ACCEPT;
if(terrainType != ETerrainId::NATIVE_TERRAIN && stack->isOnTerrain(terrainType))
return ILimiter::EDecision::ACCEPT;
}
return ILimiter::EDecision::DISCARD;
//TODO neutral creatues
}
std::string CreatureTerrainLimiter::toString() const
{
boost::format fmt("CreatureTerrainLimiter(terrainType=%s)");
auto terrainName = LIBRARY->terrainTypeHandler->getById(terrainType)->getJsonKey();
fmt % (terrainType == ETerrainId::NATIVE_TERRAIN ? "native" : terrainName);
return fmt.str();
}
JsonNode CreatureTerrainLimiter::toJsonNode() const
{
JsonNode root;
root["type"].String() = "CREATURE_TERRAIN_LIMITER";
auto terrainName = LIBRARY->terrainTypeHandler->getById(terrainType)->getJsonKey();
root["parameters"].Vector().emplace_back(terrainName);
return root;
}
void CreatureTerrainLimiter::acceptUpdater(IUpdater & visitor)
{
visitor.visitLimiter(*this);
}
FactionLimiter::FactionLimiter(FactionID creatureFaction)
: faction(creatureFaction)
{
}
ILimiter::EDecision FactionLimiter::limit(const BonusLimitationContext &context) const
{
const auto * bearer = dynamic_cast<const INativeTerrainProvider*>(&context.node);
if(bearer)
{
if(faction != FactionID::DEFAULT)
return bearer->getFactionID() == faction ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
switch(context.b.source)
{
case BonusSource::CREATURE_ABILITY:
return bearer->getFactionID() == context.b.sid.as<CreatureID>().toCreature()->getFactionID() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
case BonusSource::TOWN_STRUCTURE:
return bearer->getFactionID() == context.b.sid.as<BuildingTypeUniqueID>().getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
//TODO: other sources of bonuses
}
}
return ILimiter::EDecision::DISCARD; //Discard by default
}
std::string FactionLimiter::toString() const
{
boost::format fmt("FactionLimiter(faction=%s)");
fmt % LIBRARY->factions()->getById(faction)->getJsonKey();
return fmt.str();
}
JsonNode FactionLimiter::toJsonNode() const
{
JsonNode root;
root["type"].String() = "FACTION_LIMITER";
root["parameters"].Vector().emplace_back(LIBRARY->factions()->getById(faction)->getJsonKey());
return root;
}
void FactionLimiter::acceptUpdater(IUpdater & visitor)
{
visitor.visitLimiter(*this);
}
CreatureLevelLimiter::CreatureLevelLimiter(uint32_t minLevel, uint32_t maxLevel) :
minLevel(minLevel),
maxLevel(maxLevel)
{
}
ILimiter::EDecision CreatureLevelLimiter::limit(const BonusLimitationContext &context) const
{
const auto *c = retrieveCreature(&context.node);
auto accept = c && (c->getLevel() < maxLevel && c->getLevel() >= minLevel);
return accept ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; //drop bonus for non-creatures or non-native residents
}
std::string CreatureLevelLimiter::toString() const
{
boost::format fmt("CreatureLevelLimiter(minLevel=%d,maxLevel=%d)");
fmt % minLevel % maxLevel;
return fmt.str();
}
JsonNode CreatureLevelLimiter::toJsonNode() const
{
JsonNode root;
root["type"].String() = "CREATURE_LEVEL_LIMITER";
root["parameters"].Vector().emplace_back(minLevel);
root["parameters"].Vector().emplace_back(maxLevel);
return root;
}
void CreatureLevelLimiter::acceptUpdater(IUpdater& visitor)
{
visitor.visitLimiter(*this);
}
CreatureAlignmentLimiter::CreatureAlignmentLimiter(EAlignment Alignment)
: alignment(Alignment)
{
}
ILimiter::EDecision CreatureAlignmentLimiter::limit(const BonusLimitationContext &context) const
{
const auto * c = retrieveCreature(&context.node);
if(c) {
if(alignment == EAlignment::GOOD && c->isGood())
return ILimiter::EDecision::ACCEPT;
if(alignment == EAlignment::EVIL && c->isEvil())
return ILimiter::EDecision::ACCEPT;
if(alignment == EAlignment::NEUTRAL && !c->isEvil() && !c->isGood())
return ILimiter::EDecision::ACCEPT;
}
return ILimiter::EDecision::DISCARD;
}
std::string CreatureAlignmentLimiter::toString() const
{
boost::format fmt("CreatureAlignmentLimiter(alignment=%s)");
fmt % GameConstants::ALIGNMENT_NAMES[vstd::to_underlying(alignment)];
return fmt.str();
}
JsonNode CreatureAlignmentLimiter::toJsonNode() const
{
JsonNode root;
root["type"].String() = "CREATURE_ALIGNMENT_LIMITER";
root["parameters"].Vector().emplace_back(GameConstants::ALIGNMENT_NAMES[vstd::to_underlying(alignment)]);
return root;
}
void CreatureAlignmentLimiter::acceptUpdater(IUpdater & visitor)
{
visitor.visitLimiter(*this);
}
RankRangeLimiter::RankRangeLimiter(ui8 Min, ui8 Max)
:minRank(Min), maxRank(Max)
{
}
RankRangeLimiter::RankRangeLimiter()
{
minRank = maxRank = -1;
}
ILimiter::EDecision RankRangeLimiter::limit(const BonusLimitationContext &context) const
{
const CStackInstance * csi = retrieveStackInstance(&context.node);
if(csi)
{
if (csi->getNodeType() == CBonusSystemNode::COMMANDER) //no stack exp bonuses for commander creatures
return ILimiter::EDecision::DISCARD;
if (csi->getExpRank() > minRank && csi->getExpRank() < maxRank)
return ILimiter::EDecision::ACCEPT;
}
return ILimiter::EDecision::DISCARD;
}
void RankRangeLimiter::acceptUpdater(IUpdater & visitor)
{
visitor.visitLimiter(*this);
}
OppositeSideLimiter::OppositeSideLimiter(PlayerColor Owner):
owner(std::move(Owner))
{
}
ILimiter::EDecision OppositeSideLimiter::limit(const BonusLimitationContext & context) const
{
auto contextOwner = context.node.getOwner();
auto decision = (owner == contextOwner || owner == PlayerColor::CANNOT_DETERMINE) ? ILimiter::EDecision::DISCARD : ILimiter::EDecision::ACCEPT;
return decision;
}
void OppositeSideLimiter::acceptUpdater(IUpdater & visitor)
{
visitor.visitLimiter(*this);
}
// Aggregate/Boolean Limiters
AggregateLimiter::AggregateLimiter(std::vector<TLimiterPtr> limiters):
limiters(std::move(limiters))
{
}
void AggregateLimiter::add(const TLimiterPtr & limiter)
{
if(limiter)
limiters.push_back(limiter);
}
JsonNode AggregateLimiter::toJsonNode() const
{
JsonNode result;
result.Vector().emplace_back(getAggregator());
for(const auto & l : limiters)
result.Vector().push_back(l->toJsonNode());
return result;
}
void AggregateLimiter::acceptUpdater(IUpdater & visitor)
{
visitor.visitLimiter(*this);
}
const std::string AllOfLimiter::aggregator = "allOf";
const std::string & AllOfLimiter::getAggregator() const
{
return aggregator;
}
AllOfLimiter::AllOfLimiter(std::vector<TLimiterPtr> limiters):
AggregateLimiter(limiters)
{
}
ILimiter::EDecision AllOfLimiter::limit(const BonusLimitationContext & context) const
{
bool wasntSure = false;
for(const auto & limiter : limiters)
{
auto result = limiter->limit(context);
if(result == ILimiter::EDecision::DISCARD)
return result;
if(result == ILimiter::EDecision::NOT_SURE)
wasntSure = true;
}
return wasntSure ? ILimiter::EDecision::NOT_SURE : ILimiter::EDecision::ACCEPT;
}
const std::string AnyOfLimiter::aggregator = "anyOf";
const std::string & AnyOfLimiter::getAggregator() const
{
return aggregator;
}
AnyOfLimiter::AnyOfLimiter(std::vector<TLimiterPtr> limiters):
AggregateLimiter(limiters)
{
}
ILimiter::EDecision AnyOfLimiter::limit(const BonusLimitationContext & context) const
{
bool wasntSure = false;
for(const auto & limiter : limiters)
{
auto result = limiter->limit(context);
if(result == ILimiter::EDecision::ACCEPT)
return result;
if(result == ILimiter::EDecision::NOT_SURE)
wasntSure = true;
}
return wasntSure ? ILimiter::EDecision::NOT_SURE : ILimiter::EDecision::DISCARD;
}
const std::string NoneOfLimiter::aggregator = "noneOf";
const std::string & NoneOfLimiter::getAggregator() const
{
return aggregator;
}
NoneOfLimiter::NoneOfLimiter(std::vector<TLimiterPtr> limiters):
AggregateLimiter(limiters)
{
}
ILimiter::EDecision NoneOfLimiter::limit(const BonusLimitationContext & context) const
{
bool wasntSure = false;
for(const auto & limiter : limiters)
{
auto result = limiter->limit(context);
if(result == ILimiter::EDecision::ACCEPT)
return ILimiter::EDecision::DISCARD;
if(result == ILimiter::EDecision::NOT_SURE)
wasntSure = true;
}
return wasntSure ? ILimiter::EDecision::NOT_SURE : ILimiter::EDecision::ACCEPT;
}
VCMI_LIB_NAMESPACE_END