2023-04-28 00:47:29 +03:00
|
|
|
/*
|
2023-04-30 16:52:48 +03:00
|
|
|
* Limiters.cpp, part of VCMI engine
|
2023-04-28 00:47:29 +03:00
|
|
|
*
|
|
|
|
|
* 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"
|
2023-04-30 16:52:48 +03:00
|
|
|
#include "Limiters.h"
|
2025-03-28 23:06:03 +08:00
|
|
|
#include "Updaters.h"
|
2023-04-28 00:47:29 +03:00
|
|
|
|
2025-06-09 11:40:21 +03:00
|
|
|
#include "../CBonusTypeHandler.h"
|
2025-02-14 16:23:37 +00:00
|
|
|
#include "../GameLibrary.h"
|
2024-07-21 10:49:40 +00:00
|
|
|
#include "../entities/faction/CTownHandler.h"
|
2023-04-28 00:47:29 +03:00
|
|
|
#include "../CCreatureHandler.h"
|
|
|
|
|
#include "../CStack.h"
|
|
|
|
|
#include "../TerrainHandler.h"
|
2023-08-20 00:22:31 +03:00
|
|
|
#include "../constants/StringConstants.h"
|
2023-04-28 00:47:29 +03:00
|
|
|
#include "../battle/BattleInfo.h"
|
2024-02-11 23:09:01 +02:00
|
|
|
#include "../json/JsonUtils.h"
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
VCMI_LIB_NAMESPACE_BEGIN
|
|
|
|
|
|
|
|
|
|
const std::map<std::string, TLimiterPtr> bonusLimiterMap =
|
|
|
|
|
{
|
2023-05-01 01:20:01 +03:00
|
|
|
{"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)},
|
2023-04-28 00:47:29 +03:00
|
|
|
{"CREATURE_NATIVE_TERRAIN", std::make_shared<CreatureTerrainLimiter>()},
|
|
|
|
|
{"CREATURES_ONLY", std::make_shared<CreatureLevelLimiter>()},
|
|
|
|
|
{"OPPOSITE_SIDE", std::make_shared<OppositeSideLimiter>()},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const CStack * retrieveStackBattle(const CBonusSystemNode * node)
|
|
|
|
|
{
|
|
|
|
|
switch(node->getNodeType())
|
|
|
|
|
{
|
2025-06-25 17:34:20 +03:00
|
|
|
case BonusNodeType::STACK_BATTLE:
|
2023-04-28 00:47:29 +03:00
|
|
|
return dynamic_cast<const CStack *>(node);
|
|
|
|
|
default:
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const CStackInstance * retrieveStackInstance(const CBonusSystemNode * node)
|
|
|
|
|
{
|
|
|
|
|
switch(node->getNodeType())
|
|
|
|
|
{
|
2025-06-25 17:34:20 +03:00
|
|
|
case BonusNodeType::STACK_INSTANCE:
|
2023-04-28 00:47:29 +03:00
|
|
|
return (dynamic_cast<const CStackInstance *>(node));
|
2025-06-25 17:34:20 +03:00
|
|
|
case BonusNodeType::STACK_BATTLE:
|
2023-04-28 00:47:29 +03:00
|
|
|
return (dynamic_cast<const CStack *>(node))->base;
|
|
|
|
|
default:
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const CCreature * retrieveCreature(const CBonusSystemNode *node)
|
|
|
|
|
{
|
|
|
|
|
switch(node->getNodeType())
|
|
|
|
|
{
|
2025-06-25 17:34:20 +03:00
|
|
|
case BonusNodeType::CREATURE:
|
2023-04-28 00:47:29 +03:00
|
|
|
return (dynamic_cast<const CCreature *>(node));
|
2025-06-25 17:34:20 +03:00
|
|
|
case BonusNodeType::STACK_BATTLE:
|
2023-04-28 00:47:29 +03:00
|
|
|
return (dynamic_cast<const CStack *>(node))->unitType();
|
|
|
|
|
default:
|
|
|
|
|
const CStackInstance * csi = retrieveStackInstance(node);
|
|
|
|
|
if(csi)
|
2024-10-12 16:02:35 +00:00
|
|
|
return csi->getCreature();
|
2023-04-28 00:47:29 +03:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-24 01:23:07 +02:00
|
|
|
ILimiter::EDecision ILimiter::limit(const BonusLimitationContext &context) const
|
2023-04-28 00:47:29 +03:00
|
|
|
{
|
|
|
|
|
return ILimiter::EDecision::ACCEPT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string ILimiter::toString() const
|
|
|
|
|
{
|
|
|
|
|
return typeid(*this).name();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JsonNode ILimiter::toJsonNode() const
|
|
|
|
|
{
|
2024-02-13 13:18:10 +02:00
|
|
|
JsonNode root;
|
2023-04-28 00:47:29 +03:00
|
|
|
root["type"].String() = toString();
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ILimiter::EDecision CCreatureTypeLimiter::limit(const BonusLimitationContext &context) const
|
|
|
|
|
{
|
|
|
|
|
const CCreature *c = retrieveCreature(&context.node);
|
|
|
|
|
if(!c)
|
2025-04-13 23:31:37 +03:00
|
|
|
return ILimiter::EDecision::NOT_APPLICABLE;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
2025-05-10 19:09:08 +03:00
|
|
|
auto accept = c->getId() == creatureID || (includeUpgrades && creatureID.toCreature()->isMyDirectOrIndirectUpgrade(c));
|
2023-04-28 00:47:29 +03:00
|
|
|
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)
|
2024-10-12 18:19:58 +00:00
|
|
|
: creatureID(creature_.getId()), includeUpgrades(IncludeUpgrades)
|
2023-04-28 00:47:29 +03:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CCreatureTypeLimiter::setCreature(const CreatureID & id)
|
|
|
|
|
{
|
2024-10-12 18:19:58 +00:00
|
|
|
creatureID = id;
|
2023-04-28 00:47:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string CCreatureTypeLimiter::toString() const
|
|
|
|
|
{
|
|
|
|
|
boost::format fmt("CCreatureTypeLimiter(creature=%s, includeUpgrades=%s)");
|
2025-02-14 16:23:37 +00:00
|
|
|
fmt % creatureID.toEntity(LIBRARY)->getJsonKey() % (includeUpgrades ? "true" : "false");
|
2023-04-28 00:47:29 +03:00
|
|
|
return fmt.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JsonNode CCreatureTypeLimiter::toJsonNode() const
|
|
|
|
|
{
|
2024-02-13 13:18:10 +02:00
|
|
|
JsonNode root;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
root["type"].String() = "CREATURE_TYPE_LIMITER";
|
2025-02-14 16:23:37 +00:00
|
|
|
root["parameters"].Vector().emplace_back(creatureID.toEntity(LIBRARY)->getJsonKey());
|
2024-02-13 13:18:10 +02:00
|
|
|
root["parameters"].Vector().emplace_back(includeUpgrades);
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 01:20:01 +03:00
|
|
|
HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus )
|
2023-10-05 16:13:52 +03:00
|
|
|
: type(bonus), isSubtypeRelevant(false), isSourceRelevant(false), isSourceIDRelevant(false)
|
2023-04-28 00:47:29 +03:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-21 14:50:42 +03:00
|
|
|
HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus, BonusSubtypeID _subtype )
|
2023-04-28 00:47:29 +03:00
|
|
|
: type(bonus), subtype(_subtype), isSubtypeRelevant(true), isSourceRelevant(false), isSourceIDRelevant(false)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-01 01:20:01 +03:00
|
|
|
HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, BonusSource src)
|
2023-04-28 00:47:29 +03:00
|
|
|
: type(bonus), source(src), isSubtypeRelevant(false), isSourceRelevant(true), isSourceIDRelevant(false)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-21 14:50:42 +03:00
|
|
|
HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, BonusSubtypeID _subtype, BonusSource src)
|
2023-04-28 00:47:29 +03:00
|
|
|
: type(bonus), subtype(_subtype), isSubtypeRelevant(true), source(src), isSourceRelevant(true), isSourceIDRelevant(false)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ILimiter::EDecision HasAnotherBonusLimiter::limit(const BonusLimitationContext &context) const
|
|
|
|
|
{
|
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-11 15:04:30 +03:00
|
|
|
boost::container::static_vector<CSelector, 4> selectorSegments;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
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-11 15:04:30 +03:00
|
|
|
if (type != BonusType::NONE)
|
|
|
|
|
selectorSegments.push_back(Selector::type()(type));
|
2023-04-28 00:47:29 +03:00
|
|
|
if(isSubtypeRelevant)
|
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-11 15:04:30 +03:00
|
|
|
selectorSegments.push_back(Selector::subtype()(subtype));
|
2023-04-28 00:47:29 +03:00
|
|
|
if(isSourceRelevant && isSourceIDRelevant)
|
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-11 15:04:30 +03:00
|
|
|
selectorSegments.push_back(Selector::source(source, sid));
|
2023-04-28 00:47:29 +03:00
|
|
|
else if (isSourceRelevant)
|
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-11 15:04:30 +03:00
|
|
|
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]);
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
//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
|
|
|
|
|
{
|
2025-06-09 11:40:21 +03:00
|
|
|
std::string typeName = LIBRARY->bth->bonusToString(type);
|
2023-04-28 00:47:29 +03:00
|
|
|
if(isSubtypeRelevant)
|
|
|
|
|
{
|
2023-10-05 16:13:52 +03:00
|
|
|
boost::format fmt("HasAnotherBonusLimiter(type=%s, subtype=%s)");
|
|
|
|
|
fmt % typeName % subtype.toString();
|
2023-04-28 00:47:29 +03:00
|
|
|
return fmt.str();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
boost::format fmt("HasAnotherBonusLimiter(type=%s)");
|
|
|
|
|
fmt % typeName;
|
|
|
|
|
return fmt.str();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JsonNode HasAnotherBonusLimiter::toJsonNode() const
|
|
|
|
|
{
|
2024-02-13 13:18:10 +02:00
|
|
|
JsonNode root;
|
2025-06-09 11:40:21 +03:00
|
|
|
std::string typeName = LIBRARY->bth->bonusToString(type);
|
2023-04-28 00:47:29 +03:00
|
|
|
auto sourceTypeName = vstd::findKey(bonusSourceMap, source);
|
|
|
|
|
|
|
|
|
|
root["type"].String() = "HAS_ANOTHER_BONUS_LIMITER";
|
2024-02-13 13:18:10 +02:00
|
|
|
root["parameters"].Vector().emplace_back(typeName);
|
2023-04-28 00:47:29 +03:00
|
|
|
if(isSubtypeRelevant)
|
2024-02-13 13:18:10 +02:00
|
|
|
root["parameters"].Vector().emplace_back(subtype.toString());
|
2023-04-28 00:47:29 +03:00
|
|
|
if(isSourceRelevant)
|
2024-02-13 13:18:10 +02:00
|
|
|
root["parameters"].Vector().emplace_back(sourceTypeName);
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ILimiter::EDecision UnitOnHexLimiter::limit(const BonusLimitationContext &context) const
|
|
|
|
|
{
|
|
|
|
|
const auto * stack = retrieveStackBattle(&context.node);
|
|
|
|
|
if(!stack)
|
2025-04-13 23:31:37 +03:00
|
|
|
return ILimiter::EDecision::NOT_APPLICABLE;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
auto accept = false;
|
2024-12-05 22:01:13 +01:00
|
|
|
|
2025-01-13 14:12:00 +01:00
|
|
|
for (const auto & hex : stack->getHexes())
|
2024-11-12 07:11:18 +01:00
|
|
|
accept |= applicableHexes.contains(hex);
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
return accept ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-28 20:50:26 +02:00
|
|
|
UnitOnHexLimiter::UnitOnHexLimiter(const BattleHexArray & applicableHexes):
|
2023-04-28 00:47:29 +03:00
|
|
|
applicableHexes(applicableHexes)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JsonNode UnitOnHexLimiter::toJsonNode() const
|
|
|
|
|
{
|
2024-02-13 13:18:10 +02:00
|
|
|
JsonNode root;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
root["type"].String() = "UNIT_ON_HEXES";
|
2025-01-13 14:12:00 +01:00
|
|
|
for(const auto & hex : applicableHexes)
|
2025-01-06 23:05:45 +01:00
|
|
|
root["parameters"].Vector().emplace_back(hex.toInt());
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CreatureTerrainLimiter::CreatureTerrainLimiter()
|
|
|
|
|
: terrainType(ETerrainId::NATIVE_TERRAIN)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CreatureTerrainLimiter::CreatureTerrainLimiter(TerrainId terrain):
|
|
|
|
|
terrainType(terrain)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ILimiter::EDecision CreatureTerrainLimiter::limit(const BonusLimitationContext &context) const
|
|
|
|
|
{
|
2025-06-25 17:34:20 +03:00
|
|
|
if (context.node.getNodeType() != BonusNodeType::STACK_BATTLE && context.node.getNodeType() != BonusNodeType::STACK_INSTANCE)
|
2025-04-13 23:31:37 +03:00
|
|
|
return ILimiter::EDecision::NOT_APPLICABLE;
|
2025-04-13 22:23:12 +03:00
|
|
|
|
|
|
|
|
if (terrainType == ETerrainId::NATIVE_TERRAIN)
|
2023-04-28 00:47:29 +03:00
|
|
|
{
|
2025-04-13 22:23:12 +03:00
|
|
|
auto selector = Selector::type()(BonusType::TERRAIN_NATIVE);
|
2023-04-28 00:47:29 +03:00
|
|
|
|
2025-04-13 22:23:12 +03:00
|
|
|
if(context.alreadyAccepted.getFirst(selector))
|
2023-04-28 00:47:29 +03:00
|
|
|
return ILimiter::EDecision::ACCEPT;
|
|
|
|
|
|
2025-04-13 22:23:12 +03:00
|
|
|
if(context.stillUndecided.getFirst(selector))
|
|
|
|
|
return ILimiter::EDecision::NOT_SURE;
|
|
|
|
|
|
|
|
|
|
// TODO: CStack and CStackInstance need some common base type that represents any stack
|
|
|
|
|
// Closest existing class is ACreature, however it is also used as base for CCreature, which is not a stack
|
2025-06-25 17:34:20 +03:00
|
|
|
if (context.node.getNodeType() == BonusNodeType::STACK_BATTLE)
|
2025-04-13 22:23:12 +03:00
|
|
|
{
|
|
|
|
|
const auto * unit = dynamic_cast<const CStack *>(&context.node);
|
|
|
|
|
auto unitNativeTerrain = unit->getFactionID().toEntity(LIBRARY)->getNativeTerrain();
|
|
|
|
|
if (unit->getCurrentTerrain() == unitNativeTerrain)
|
|
|
|
|
return ILimiter::EDecision::ACCEPT;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
const auto * unit = dynamic_cast<const CStackInstance *>(&context.node);
|
|
|
|
|
auto unitNativeTerrain = unit->getFactionID().toEntity(LIBRARY)->getNativeTerrain();
|
|
|
|
|
if (unit->getCurrentTerrain() == unitNativeTerrain)
|
|
|
|
|
return ILimiter::EDecision::ACCEPT;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-06-25 17:34:20 +03:00
|
|
|
if (context.node.getNodeType() == BonusNodeType::STACK_BATTLE)
|
2025-04-13 22:23:12 +03:00
|
|
|
{
|
|
|
|
|
const auto * unit = dynamic_cast<const CStack *>(&context.node);
|
|
|
|
|
if (unit->getCurrentTerrain() == terrainType)
|
|
|
|
|
return ILimiter::EDecision::ACCEPT;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
const auto * unit = dynamic_cast<const CStackInstance*>(&context.node);
|
|
|
|
|
if (unit->getCurrentTerrain() == terrainType)
|
|
|
|
|
return ILimiter::EDecision::ACCEPT;
|
|
|
|
|
}
|
2023-04-28 00:47:29 +03:00
|
|
|
}
|
|
|
|
|
return ILimiter::EDecision::DISCARD;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string CreatureTerrainLimiter::toString() const
|
|
|
|
|
{
|
|
|
|
|
boost::format fmt("CreatureTerrainLimiter(terrainType=%s)");
|
2025-02-14 16:23:37 +00:00
|
|
|
auto terrainName = LIBRARY->terrainTypeHandler->getById(terrainType)->getJsonKey();
|
2023-04-28 00:47:29 +03:00
|
|
|
fmt % (terrainType == ETerrainId::NATIVE_TERRAIN ? "native" : terrainName);
|
|
|
|
|
return fmt.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JsonNode CreatureTerrainLimiter::toJsonNode() const
|
|
|
|
|
{
|
2024-02-13 13:18:10 +02:00
|
|
|
JsonNode root;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
root["type"].String() = "CREATURE_TERRAIN_LIMITER";
|
2025-02-14 16:23:37 +00:00
|
|
|
auto terrainName = LIBRARY->terrainTypeHandler->getById(terrainType)->getJsonKey();
|
2024-02-13 13:18:10 +02:00
|
|
|
root["parameters"].Vector().emplace_back(terrainName);
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
2024-10-05 19:37:52 +00:00
|
|
|
return bearer->getFactionID() == faction ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
switch(context.b.source)
|
|
|
|
|
{
|
2023-05-01 01:20:01 +03:00
|
|
|
case BonusSource::CREATURE_ABILITY:
|
2024-10-05 19:37:52 +00:00
|
|
|
return bearer->getFactionID() == context.b.sid.as<CreatureID>().toCreature()->getFactionID() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
2023-05-01 01:20:01 +03:00
|
|
|
case BonusSource::TOWN_STRUCTURE:
|
2024-10-05 19:37:52 +00:00
|
|
|
return bearer->getFactionID() == context.b.sid.as<BuildingTypeUniqueID>().getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
//TODO: other sources of bonuses
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-13 23:31:37 +03:00
|
|
|
return ILimiter::EDecision::NOT_APPLICABLE; //Discard by default
|
2023-04-28 00:47:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string FactionLimiter::toString() const
|
|
|
|
|
{
|
|
|
|
|
boost::format fmt("FactionLimiter(faction=%s)");
|
2025-02-14 16:23:37 +00:00
|
|
|
fmt % LIBRARY->factions()->getById(faction)->getJsonKey();
|
2023-04-28 00:47:29 +03:00
|
|
|
return fmt.str();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JsonNode FactionLimiter::toJsonNode() const
|
|
|
|
|
{
|
2024-02-13 13:18:10 +02:00
|
|
|
JsonNode root;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
root["type"].String() = "FACTION_LIMITER";
|
2025-02-14 16:23:37 +00:00
|
|
|
root["parameters"].Vector().emplace_back(LIBRARY->factions()->getById(faction)->getJsonKey());
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2025-04-13 23:31:37 +03:00
|
|
|
if (!c)
|
|
|
|
|
return ILimiter::EDecision::NOT_APPLICABLE;
|
|
|
|
|
|
|
|
|
|
auto accept = c->getLevel() < maxLevel && c->getLevel() >= minLevel;
|
2023-04-28 00:47:29 +03:00
|
|
|
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
|
|
|
|
|
{
|
2024-02-13 13:18:10 +02:00
|
|
|
JsonNode root;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
root["type"].String() = "CREATURE_LEVEL_LIMITER";
|
2024-02-13 13:18:10 +02:00
|
|
|
root["parameters"].Vector().emplace_back(minLevel);
|
|
|
|
|
root["parameters"].Vector().emplace_back(maxLevel);
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2025-04-13 23:31:37 +03:00
|
|
|
|
|
|
|
|
return ILimiter::EDecision::DISCARD;
|
2023-04-28 00:47:29 +03:00
|
|
|
}
|
|
|
|
|
|
2025-04-13 23:31:37 +03:00
|
|
|
return ILimiter::EDecision::NOT_APPLICABLE;
|
2023-04-28 00:47:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
{
|
2024-02-13 13:18:10 +02:00
|
|
|
JsonNode root;
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
root["type"].String() = "CREATURE_ALIGNMENT_LIMITER";
|
2024-02-13 13:18:10 +02:00
|
|
|
root["parameters"].Vector().emplace_back(GameConstants::ALIGNMENT_NAMES[vstd::to_underlying(alignment)]);
|
2023-04-28 00:47:29 +03:00
|
|
|
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
2025-06-25 17:34:20 +03:00
|
|
|
if (csi->getNodeType() == BonusNodeType::COMMANDER) //no stack exp bonuses for commander creatures
|
2023-04-28 00:47:29 +03:00
|
|
|
return ILimiter::EDecision::DISCARD;
|
|
|
|
|
if (csi->getExpRank() > minRank && csi->getExpRank() < maxRank)
|
|
|
|
|
return ILimiter::EDecision::ACCEPT;
|
2025-04-13 23:31:37 +03:00
|
|
|
|
|
|
|
|
return ILimiter::EDecision::DISCARD;
|
2023-04-28 00:47:29 +03:00
|
|
|
}
|
2025-04-13 23:31:37 +03:00
|
|
|
return ILimiter::EDecision::NOT_APPLICABLE;
|
2023-04-28 00:47:29 +03:00
|
|
|
}
|
|
|
|
|
|
2025-06-03 19:23:40 +03:00
|
|
|
OppositeSideLimiter::OppositeSideLimiter()
|
2023-04-28 00:47:29 +03:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ILimiter::EDecision OppositeSideLimiter::limit(const BonusLimitationContext & context) const
|
|
|
|
|
{
|
2025-06-03 19:23:40 +03:00
|
|
|
PlayerColor contextOwner = context.node.getOwner();
|
|
|
|
|
PlayerColor bonusOwner = context.b.bonusOwner;
|
2025-05-13 00:58:10 +08:00
|
|
|
if (contextOwner == PlayerColor::UNFLAGGABLE)
|
|
|
|
|
contextOwner = PlayerColor::NEUTRAL;
|
2025-06-03 19:23:40 +03:00
|
|
|
auto decision = (bonusOwner == contextOwner || bonusOwner == PlayerColor::CANNOT_DETERMINE) ? ILimiter::EDecision::DISCARD : ILimiter::EDecision::ACCEPT;
|
2023-04-28 00:47:29 +03:00
|
|
|
return decision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
{
|
2024-02-13 13:18:10 +02:00
|
|
|
JsonNode result;
|
|
|
|
|
result.Vector().emplace_back(getAggregator());
|
2023-04-28 00:47:29 +03:00
|
|
|
for(const auto & l : limiters)
|
|
|
|
|
result.Vector().push_back(l->toJsonNode());
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2025-04-13 23:31:37 +03:00
|
|
|
if(result == ILimiter::EDecision::DISCARD || result == ILimiter::EDecision::NOT_APPLICABLE)
|
2023-04-28 00:47:29 +03:00
|
|
|
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);
|
2025-04-13 23:31:37 +03:00
|
|
|
if(result == ILimiter::EDecision::NOT_APPLICABLE)
|
|
|
|
|
return ILimiter::EDecision::NOT_APPLICABLE;
|
2023-04-28 00:47:29 +03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-20 16:29:14 +02:00
|
|
|
HasChargesLimiter::HasChargesLimiter(const uint16_t cost)
|
|
|
|
|
: chargeCost(cost)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ILimiter::EDecision HasChargesLimiter::limit(const BonusLimitationContext & context) const
|
|
|
|
|
{
|
|
|
|
|
for(const auto & bonus : context.stillUndecided)
|
|
|
|
|
{
|
2025-06-24 01:23:07 +02:00
|
|
|
if(bonus->type == BonusType::ARTIFACT_CHARGE && bonus->sid == context.b.sid)
|
2025-05-20 16:29:14 +02:00
|
|
|
return ILimiter::EDecision::NOT_SURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for(const auto & bonus : context.alreadyAccepted)
|
|
|
|
|
{
|
2025-06-24 01:23:07 +02:00
|
|
|
if(bonus->type == BonusType::ARTIFACT_CHARGE && bonus->sid == context.b.sid)
|
2025-05-20 16:29:14 +02:00
|
|
|
return bonus->val >= chargeCost ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
|
|
|
|
|
}
|
|
|
|
|
return ILimiter::EDecision::DISCARD;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-28 00:47:29 +03:00
|
|
|
VCMI_LIB_NAMESPACE_END
|