1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-09-16 09:26:28 +02:00

HAS_CHARGES_LIMITER

This commit is contained in:
SoundSSGood
2025-05-20 16:29:14 +02:00
parent b6f1eac19b
commit 6752ab3a75
20 changed files with 136 additions and 78 deletions

View File

@@ -128,6 +128,21 @@ Parameters:
For reference on tiles indexes see image below:
### HAS_CHARGES_LIMITER
Currently works only with spells. Sets the cost of use in charges
Parameters:
- use cost (charges)
```json
"limiters" : [ {
"type" : "HAS_CHARGES_LIMITER",
"parameters" : [2]
} ]
```
![Battlefield Hexes Layout](../../images/Battle_Field_Hexes.svg)
## Aggregate Limiters

View File

@@ -30,7 +30,7 @@ class CSelector;
class IGameInfoCallback;
using BonusSubtypeID = VariantIdentifier<BonusCustomSubtype, SpellID, CreatureID, PrimarySkill, TerrainId, GameResID, SpellSchool>;
using BonusSourceID = VariantIdentifier<BonusCustomSource, SpellID, CreatureID, ArtifactID, CampaignScenarioID, SecondarySkill, HeroTypeID, Obj, ObjectInstanceID, BuildingTypeUniqueID, BattleField>;
using BonusSourceID = VariantIdentifier<BonusCustomSource, SpellID, CreatureID, ArtifactID, ArtifactInstanceID, CampaignScenarioID, SecondarySkill, HeroTypeID, Obj, ObjectInstanceID, BuildingTypeUniqueID, BattleField>;
using TBonusListPtr = std::shared_ptr<BonusList>;
using TConstBonusListPtr = std::shared_ptr<const BonusList>;
using TLimiterPtr = std::shared_ptr<const ILimiter>;

View File

@@ -120,6 +120,7 @@ public:
virtual std::string bonusToString(const std::shared_ptr<Bonus>& bonus) const {return "";}; //description or bonus name
virtual std::string nodeName() const;
bool isHypothetic() const { return isHypotheticNode; }
void setHypothetic(const bool hypothetic);
BonusList & getExportedBonusList();
const BonusList & getExportedBonusList() const;

View File

@@ -591,4 +591,31 @@ ILimiter::EDecision NoneOfLimiter::limit(const BonusLimitationContext & context)
return wasntSure ? ILimiter::EDecision::NOT_SURE : ILimiter::EDecision::ACCEPT;
}
HasChargesLimiter::HasChargesLimiter(const uint16_t cost)
: chargeCost(cost)
{
}
HasChargesLimiter::HasChargesLimiter(const HasChargesLimiter & inst, const BonusSourceID & id)
: HasChargesLimiter(inst)
{
chargesSourceId = id;
}
ILimiter::EDecision HasChargesLimiter::limit(const BonusLimitationContext & context) const
{
for(const auto & bonus : context.stillUndecided)
{
if(bonus->type == BonusType::ARTIFACT_CHARGE && bonus->sid == chargesSourceId)
return ILimiter::EDecision::NOT_SURE;
}
for(const auto & bonus : context.alreadyAccepted)
{
if(bonus->type == BonusType::ARTIFACT_CHARGE && bonus->sid == chargesSourceId)
return bonus->val >= chargeCost ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
}
return ILimiter::EDecision::DISCARD;
}
VCMI_LIB_NAMESPACE_END

View File

@@ -276,4 +276,22 @@ public:
}
};
class DLL_LINKAGE HasChargesLimiter : public ILimiter // works with bonuses that consume charges
{
public:
uint16_t chargeCost;
BonusSourceID chargesSourceId;
HasChargesLimiter(const uint16_t cost = 1);
HasChargesLimiter(const HasChargesLimiter & inst, const BonusSourceID & id);
EDecision limit(const BonusLimitationContext & context) const override;
template <typename Handler> void serialize(Handler &h)
{
h & static_cast<ILimiter&>(*this);
h & chargeCost;
h & chargesSourceId;
}
};
VCMI_LIB_NAMESPACE_END

View File

@@ -134,6 +134,11 @@ BuildingTypeUniqueID::BuildingTypeUniqueID(FactionID factionID, BuildingID build
assert(buildingID.getNum() < 0x10000);
}
std::string ArtifactInstanceID::encode(const si32 index)
{
return "";
}
BuildingID BuildingTypeUniqueID::getBuilding() const
{
return BuildingID(getNum() % 0x10000);

View File

@@ -51,6 +51,8 @@ class ArtifactInstanceID : public StaticIdentifier<ArtifactInstanceID>
{
public:
using StaticIdentifier<ArtifactInstanceID>::StaticIdentifier;
DLL_LINKAGE static std::string encode(const si32 index);
};
class QueryID : public StaticIdentifier<QueryID>

View File

@@ -254,8 +254,6 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
else
art->setDefaultStartCharges(charges);
}
if(art->getDischargeCondition() == DischargeArtifactCondition::SPELLCAST && art->getBonusesOfType(BonusType::SPELL)->size() == 0)
logMod->warn("Warning! %s condition of discharge is \"SPELLCAST\", but there is not a single spell.", art->getNameTranslated());
}
return art;

View File

@@ -14,6 +14,7 @@
#include "ArtifactUtils.h"
#include "CArtifactFittingSet.h"
#include "../../bonuses/Limiters.h"
#include "../../texts/CGeneralTextHandler.h"
#include "../../GameLibrary.h"
@@ -292,6 +293,21 @@ bool CChargedArtifact::getRemoveOnDepletion() const
return removeOnDepletion;
}
std::optional<uint16_t> CChargedArtifact::getChargeCost(const SpellID & id) const
{
auto art = static_cast<const CArtifact*>(this);
for(const auto & bonus : art->instanceBonuses)
{
if(bonus->type == BonusType::SPELL && bonus->subtype.as<SpellID>() == id)
{
if(const auto chargesLimiter = std::static_pointer_cast<const HasChargesLimiter>(bonus->limiter))
return chargesLimiter->chargeCost;
}
}
return std::nullopt;
}
CArtifact::CArtifact()
: CBonusSystemNode(BonusNodeType::ARTIFACT),
iconIndex(ArtifactID::NONE),

View File

@@ -83,6 +83,7 @@ public:
uint16_t getDefaultStartCharges() const;
DischargeArtifactCondition getDischargeCondition() const;
bool getRemoveOnDepletion() const;
std::optional<uint16_t> getChargeCost(const SpellID & id) const;
};
// Container for artifacts. Not for instances.

View File

@@ -120,44 +120,6 @@ void CGrowingArtifactInstance::growingUp()
artInst->addNewBonus(std::make_shared<Bonus>(*bonus.second));
}
}
if(artType->isCharged())
artInst->onChargesChanged();
}
}
void CChargedArtifactInstance::onChargesChanged()
{
auto artInst = static_cast<CArtifactInstance*>(this);
const auto artType = artInst->getType();
if(!artType->isCharged())
return;
const auto bonusSelector = artType->getDischargeCondition() == DischargeArtifactCondition::SPELLCAST ?
Selector::type()(BonusType::SPELL) : Selector::all;
auto instBonuses = artInst->getAllBonuses(bonusSelector, nullptr);
if(artInst->getCharges() == 0)
{
for(const auto & bonus : *instBonuses)
if(bonus->type != BonusType::ARTIFACT_GROWING && bonus->type != BonusType::ARTIFACT_CHARGE)
artInst->removeBonus(bonus);
}
else
{
for(const auto & refBonus : *artType->getAllBonuses(bonusSelector, nullptr))
{
if(const auto bonusFound = std::find_if(instBonuses->begin(), instBonuses->end(),
[refBonus](const auto & instBonus)
{
return refBonus->type == instBonus->type;
}); bonusFound == instBonuses->end())
{
artInst->accumulateBonus(refBonus);
}
}
}
}
@@ -171,7 +133,6 @@ void CChargedArtifactInstance::discharge(const uint16_t charges)
chargedBonus->front()->val -= charges;
else
chargedBonus->front()->val = 0;
onChargesChanged();
}
}
@@ -184,7 +145,6 @@ void CChargedArtifactInstance::addCharges(const uint16_t charges)
const auto chargedBonus = artInst->getBonusesOfType(BonusType::ARTIFACT_CHARGE);
assert(!chargedBonus->empty());
chargedBonus->front()->val += charges;
onChargesChanged();
}
}
@@ -199,21 +159,7 @@ void CArtifactInstance::init()
{
const auto art = artTypeID.toArtifact();
assert(art);
if(art->isCharged())
{
// Charged artifacts contain all bonuses inside instance bonus node
if(art->getDischargeCondition() == DischargeArtifactCondition::SPELLCAST)
{
for(const auto & bonus : *art->getAllBonuses(Selector::all, nullptr))
if(bonus->type != BonusType::SPELL)
accumulateBonus(bonus);
}
}
else
{
attachToSource(*art);
}
attachToSource(*art);
}
CArtifactInstance::CArtifactInstance(IGameInfoCallback *cb, const CArtifact * art)

View File

@@ -80,7 +80,6 @@ class DLL_LINKAGE CChargedArtifactInstance
protected:
CChargedArtifactInstance() = default;
public:
void onChargesChanged();
void discharge(const uint16_t charges);
void addCharges(const uint16_t charges);
uint16_t getCharges() const;
@@ -122,7 +121,6 @@ public:
if(!h.saving && h.loadingGamestate)
{
init();
onChargesChanged();
}
}
};

View File

@@ -946,6 +946,9 @@ void GameStatePackVisitor::visitDischargeArtifact(DischargeArtifact & pack)
ePack.posPack.push_back(pack.artLoc.value().slot);
ePack.visit(*this);
}
// Workaround to inform hero bonus node about changes. Obviously this has to be done somehow differently.
if(pack.artLoc.has_value())
gs.getHero(pack.artLoc.value().artHolder)->nodeHasChanged();
}
void GameStatePackVisitor::visitAssembledArtifact(AssembledArtifact & pack)

View File

@@ -641,6 +641,16 @@ std::shared_ptr<const ILimiter> JsonUtils::parseLimiter(const JsonNode & limiter
}
return hexLimiter;
}
else if(limiterType == "HAS_CHARGES_LIMITER")
{
auto hasChargesLimiter = std::make_shared<HasChargesLimiter>();
if(!parameters.Vector().empty())
{
if(parameters.Vector().size() == 1 && parameters.Vector().front().isNumber())
hasChargesLimiter->chargeCost = parameters.Vector().front().Integer();
}
return hasChargesLimiter;
}
else
{
logMod->error("Error: invalid customizable limiter type %s.", limiterType);

View File

@@ -21,6 +21,7 @@
#include "../RoadHandler.h"
#include "../TerrainHandler.h"
#include "../bonuses/Limiters.h"
#include "../callback/IGameInfoCallback.h"
#include "../entities/artifact/CArtHandler.h"
#include "../entities/hero/CHeroHandler.h"
@@ -863,13 +864,19 @@ CArtifactInstance * CMap::createArtifact(const ArtifactID & artID, const SpellID
{
auto bonus = std::make_shared<Bonus>();
bonus->type = BonusType::ARTIFACT_CHARGE;
bonus->sid = artInst->getId();
bonus->val = 0;
artInst->addNewBonus(bonus);
artInst->addCharges(art->getDefaultStartCharges());
}
for (const auto & bonus : art->instanceBonuses)
artInst->addNewBonus(std::make_shared<Bonus>(*bonus));
{
auto instBonus = std::make_shared<Bonus>(*bonus);
if(const auto srcLimiter = std::static_pointer_cast<const HasChargesLimiter>(bonus->limiter))
instBonus->limiter = std::make_shared<HasChargesLimiter>(*srcLimiter, artInst->getId());
artInst->addNewBonus(instBonus);
}
return artInst;
}

View File

@@ -111,6 +111,7 @@ void registerTypes(Serializer &s)
s.template registerType<OppositeSideLimiter>(51);
s.template registerType<TownBuildingInstance>(52);
s.template registerType<TownRewardableBuildingInstance>(53);
s.template registerType<HasChargesLimiter>(55);
s.template registerType<CRewardableObject>(56);
s.template registerType<CTeamVisited>(57);
s.template registerType<CGObelisk>(58);

View File

@@ -3938,7 +3938,7 @@ void CGameHandler::castSpell(const spells::Caster * caster, SpellID spellID, con
s->adventureCast(spellEnv.get(), p);
if(const auto * hero = caster->getHeroCaster())
useChargedArtifactUsed(hero->id, spellID);
useChargeBasedSpell(hero->id, spellID);
}
bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & sl2)
@@ -4331,33 +4331,43 @@ void CGameHandler::startBattle(const CArmedInstance *army1, const CArmedInstance
battles->startBattle(army1, army2);
}
void CGameHandler::useChargedArtifactUsed(const ObjectInstanceID & heroObjectID, const SpellID & spellID)
void CGameHandler::useChargeBasedSpell(const ObjectInstanceID & heroObjectID, const SpellID & spellID)
{
const auto * hero = gameInfo().getHero(heroObjectID);
assert(hero);
assert(hero->canCastThisSpell(spellID.toSpell()));
if(vstd::contains(hero->getSpellsInSpellbook(), spellID))
return;
std::optional<std::tuple<ArtifactPosition, ArtifactInstanceID, uint16_t>> chargedArt;
std::vector<std::pair<ArtifactPosition, ArtifactInstanceID>> chargedArts;
// Check if hero used charge based spell
// To do this, we create a local copy of the hero with the necessary magical bonuses, except for the bonuses of charged artifacts
CGHeroInstance caster(&gameInfo());
caster.setHypothetic(true);
for(const auto & b : *hero->getAllBonuses(Selector::type()(BonusType::SPELLS_OF_LEVEL), nullptr))
caster.addNewBonus(b);
for(const auto & b : *hero->getAllBonuses(Selector::type()(BonusType::SPELLS_OF_SCHOOL), nullptr))
caster.addNewBonus(b);
for(const auto & spell : hero->getSpellsInSpellbook())
caster.addSpellToSpellbook(spell);
for(const auto & [slot, slotInfo] : hero->artifactsWorn)
{
const auto * artInst = slotInfo.getArt();
const auto * artType = artInst->getType();
if(artType->getDischargeCondition() == DischargeArtifactCondition::SPELLCAST)
{
chargedArts.emplace_back(slot, artInst->getId());
}
else
{
if(const auto bonuses = artInst->getBonusesOfType(BonusType::SPELL, spellID); !bonuses->empty())
return;
if(const auto spellCost = artType->getChargeCost(spellID))
{
chargedArt.emplace(slot, artInst->getId(), spellCost.value());
continue;
}
}
caster.putArtifact(slot, artInst);
}
if(caster.canCastThisSpell(spellID.toSpell()))
return;
assert(!chargedArts.empty());
DischargeArtifact msg(chargedArts.front().second, 1);
msg.artLoc.emplace(hero->id, chargedArts.front().first);
assert(chargedArt.has_value());
DischargeArtifact msg(std::get<1>(chargedArt.value()), std::get<2>(chargedArt.value()));
msg.artLoc.emplace(hero->id, std::get<0>(chargedArt.value()));
sendAndApply(msg);
}

View File

@@ -176,7 +176,7 @@ public:
void changeFogOfWar(const std::unordered_set<int3> &tiles, PlayerColor player,ETileVisibility mode) override;
void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override;
void useChargedArtifactUsed(const ObjectInstanceID & heroObjectID, const SpellID & spellID);
void useChargeBasedSpell(const ObjectInstanceID & heroObjectID, const SpellID & spellID);
/// Returns hero that is currently visiting this object, or nullptr if no visit is active
const CGHeroInstance * getVisitingHero(const CGObjectInstance *obj);

View File

@@ -124,7 +124,7 @@ bool BattleActionProcessor::doHeroSpellAction(const CBattleInfoCallback & battle
}
parameters.cast(gameHandler->spellcastEnvironment(), ba.getTarget(&battle));
gameHandler->useChargedArtifactUsed(h->id, ba.spell);
gameHandler->useChargeBasedSpell(h->id, ba.spell);
return true;
}

View File

@@ -436,7 +436,7 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
pack.artsPack0.emplace_back(MoveArtifactInfo(srcSlot, dstSlot));
if(ArtifactUtils::isSlotEquipment(dstSlot))
pack.artsPack0.back().askAssemble = true;
artFittingSet.putArtifact(dstSlot, const_cast<CArtifactInstance*>(art));
artFittingSet.putArtifact(dstSlot, art);
}
};