Browse Source

Merge pull request #5743 from SoundSSGood/num-of-charges-per-spell

Charge cost for charge based spells
Ivan Savenko 3 months ago
parent
commit
c15f628761

+ 15 - 0
docs/modders/Bonus/Bonus_Limiters.md

@@ -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

+ 6 - 0
lib/bonuses/Bonus.cpp

@@ -236,6 +236,12 @@ Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32
 	targetSourceType = BonusSource::OTHER;
 }
 
+Bonus::Bonus(const Bonus & inst, const BonusSourceID & sourceId)
+	: Bonus(inst)
+{
+	sid = sourceId;
+}
+
 std::shared_ptr<Bonus> Bonus::addPropagator(const TPropagatorPtr & Propagator)
 {
 	propagator = Propagator;

+ 2 - 9
lib/bonuses/Bonus.h

@@ -11,29 +11,21 @@
 
 #include "BonusEnum.h"
 #include "BonusCustomTypes.h"
-#include "../constants/VariantIdentifier.h"
-#include "../constants/EntityIdentifiers.h"
+#include "Limiters.h"
 #include "../serializer/Serializeable.h"
 #include "../texts/MetaString.h"
 #include "../filesystem/ResourcePath.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-struct Bonus;
 class IBonusBearer;
-class CBonusSystemNode;
-class ILimiter;
 class IPropagator;
 class IUpdater;
-class BonusList;
 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 TBonusListPtr = std::shared_ptr<BonusList>;
 using TConstBonusListPtr = std::shared_ptr<const BonusList>;
-using TLimiterPtr = std::shared_ptr<const ILimiter>;
 using TPropagatorPtr = std::shared_ptr<const IPropagator>;
 using TUpdaterPtr = std::shared_ptr<const IUpdater>;
 
@@ -86,6 +78,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>, public Se
 	Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID);
 	Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, BonusSubtypeID subtype);
 	Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, BonusSubtypeID subtype, BonusValueType ValType);
+	Bonus(const Bonus & inst, const BonusSourceID & sourceId);
 	Bonus() = default;
 
 	template <typename Handler> void serialize(Handler &h)

+ 4 - 0
lib/bonuses/BonusCustomTypes.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../constants/EntityIdentifiers.h"
+#include "../constants/VariantIdentifier.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -76,4 +77,7 @@ public:
 	static BonusCustomSubtype creatureLevel(int level);
 };
 
+using BonusSubtypeID = VariantIdentifier<BonusCustomSubtype, SpellID, CreatureID, PrimarySkill, TerrainId, GameResID, SpellSchool>;
+using BonusSourceID = VariantIdentifier<BonusCustomSource, SpellID, CreatureID, ArtifactID, ArtifactInstanceID, CampaignScenarioID, SecondarySkill, HeroTypeID, Obj, ObjectInstanceID, BuildingTypeUniqueID, BattleField>;
+
 VCMI_LIB_NAMESPACE_END

+ 22 - 5
lib/bonuses/Limiters.cpp

@@ -14,13 +14,9 @@
 
 #include "../CBonusTypeHandler.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 "../TerrainHandler.h"
 #include "../constants/StringConstants.h"
@@ -81,7 +77,7 @@ static const CCreature * retrieveCreature(const CBonusSystemNode *node)
 	}
 }
 
-ILimiter::EDecision ILimiter::limit(const BonusLimitationContext &context) const /*return true to drop the bonus */
+ILimiter::EDecision ILimiter::limit(const BonusLimitationContext &context) const
 {
 	return ILimiter::EDecision::ACCEPT;
 }
@@ -591,4 +587,25 @@ ILimiter::EDecision NoneOfLimiter::limit(const BonusLimitationContext & context)
 	return wasntSure ? ILimiter::EDecision::NOT_SURE : ILimiter::EDecision::ACCEPT;
 }
 
+HasChargesLimiter::HasChargesLimiter(const uint16_t cost)
+	: chargeCost(cost)
+{
+}
+
+ILimiter::EDecision HasChargesLimiter::limit(const BonusLimitationContext & context) const
+{
+	for(const auto & bonus : context.stillUndecided)
+	{
+		if(bonus->type == BonusType::ARTIFACT_CHARGE && bonus->sid == context.b.sid)
+			return ILimiter::EDecision::NOT_SURE;
+	}
+
+	for(const auto & bonus : context.alreadyAccepted)
+	{
+		if(bonus->type == BonusType::ARTIFACT_CHARGE && bonus->sid == context.b.sid)
+			return bonus->val >= chargeCost ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD;
+	}
+	return ILimiter::EDecision::DISCARD;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 26 - 5
lib/bonuses/Limiters.h

@@ -7,18 +7,21 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
+#pragma once
 
-#include "Bonus.h"
-
+#include "BonusCustomTypes.h"
+#include "BonusEnum.h"
 #include "../battle/BattleHexArray.h"
 #include "../serializer/Serializeable.h"
 #include "../constants/Enumerations.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+struct Bonus;
+class BonusList;
+class CBonusSystemNode;
 class CCreature;
-
-extern DLL_LINKAGE const std::map<std::string, TLimiterPtr> bonusLimiterMap;
+class JsonNode;
 
 struct BonusLimitationContext
 {
@@ -41,7 +44,7 @@ public:
 
 	virtual ~ILimiter() = default;
 
-	virtual EDecision limit(const BonusLimitationContext &context) const; //0 - accept bonus; 1 - drop bonus; 2 - delay (drops eventually)
+	virtual EDecision limit(const BonusLimitationContext &context) const;
 	virtual std::string toString() const;
 	virtual JsonNode toJsonNode() const;
 
@@ -50,6 +53,9 @@ public:
 	}
 };
 
+using TLimiterPtr = std::shared_ptr<const ILimiter>;
+extern DLL_LINKAGE const std::map<std::string, TLimiterPtr> bonusLimiterMap;
+
 class DLL_LINKAGE AggregateLimiter : public ILimiter
 {
 protected:
@@ -276,4 +282,19 @@ public:
 	}
 };
 
+class DLL_LINKAGE HasChargesLimiter : public ILimiter // works with bonuses that consume charges
+{
+public:
+	uint16_t chargeCost;
+
+	HasChargesLimiter(const uint16_t cost = 1);
+	EDecision limit(const BonusLimitationContext & context) const override;
+
+	template <typename Handler> void serialize(Handler &h)
+	{
+		h & static_cast<ILimiter&>(*this);
+		h & chargeCost;
+	}
+};
+
 VCMI_LIB_NAMESPACE_END

+ 5 - 0
lib/constants/EntityIdentifiers.cpp

@@ -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);

+ 2 - 0
lib/constants/EntityIdentifiers.h

@@ -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>

+ 17 - 2
lib/entities/artifact/CArtHandler.cpp

@@ -181,6 +181,7 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 		for(const auto & b : node["bonuses"].Vector())
 		{
 			auto bonus = JsonUtils::parseBonus(b);
+			bonus->sid = art->getId();
 			art->addNewBonus(bonus);
 		}
 	}
@@ -191,6 +192,7 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 			if (b.second.isNull())
 				continue;
 			auto bonus = JsonUtils::parseBonus(b.second, art->getBonusTextID(b.first));
+			bonus->sid = art->getId();
 			art->addNewBonus(bonus);
 		}
 	}
@@ -200,6 +202,7 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 		if (b.second.isNull())
 			continue;
 		auto bonus = JsonUtils::parseBonus(b.second, art->getBonusTextID(b.first));
+		bonus->sid = art->getId();
 		bonus->source = BonusSource::ARTIFACT;
 		bonus->duration = BonusDuration::PERMANENT;
 		bonus->description.appendTextID(art->getNameTextID());
@@ -254,8 +257,20 @@ 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());
+	}
+
+	// Some bonuses must be located in the instance.
+	for(const auto & b : art->getExportedBonusList())
+	{
+		if(std::dynamic_pointer_cast<const HasChargesLimiter>(b->limiter))
+		{
+			b->source = BonusSource::ARTIFACT;
+			b->duration = BonusDuration::PERMANENT;
+			b->description.appendTextID(art->getNameTextID());
+			b->description.appendRawString(" %+d");
+			art->instanceBonuses.push_back(b);
+			art->removeBonus(b);
+		}
 	}
 
 	return art;

+ 16 - 0
lib/entities/artifact/CArtifact.cpp

@@ -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),

+ 1 - 0
lib/entities/artifact/CArtifact.h

@@ -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.

+ 1 - 55
lib/entities/artifact/CArtifactInstance.cpp

@@ -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)

+ 0 - 2
lib/entities/artifact/CArtifactInstance.h

@@ -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();
 		}
 	}
 };

+ 3 - 0
lib/gameState/GameStatePackVisitor.cpp

@@ -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)

+ 10 - 0
lib/json/JsonBonus.cpp

@@ -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);

+ 27 - 23
lib/mapObjects/CGHeroInstance.cpp

@@ -902,22 +902,7 @@ void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) con
 
 bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const
 {
-	const bool isAllowed = cb->isAllowed(spell->getId());
-
-	const bool inSpellBook = vstd::contains(spells, spell->getId()) && hasSpellbook();
-	const bool specificBonus = hasBonusOfType(BonusType::SPELL, BonusSubtypeID(spell->getId()));
-
-	bool schoolBonus = false;
-
-	spell->forEachSchool([this, &schoolBonus](const SpellSchool & cnf, bool & stop)
-	{
-		if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, BonusSubtypeID(cnf)))
-		{
-			schoolBonus = stop = true;
-		}
-	});
-
-	const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, BonusCustomSubtype::spellLevel(spell->getLevel()));
+	const bool inSpellBook = spellbookContainsSpell(spell->getId()) && hasSpellbook();
 
 	if(spell->isSpecial())
 	{
@@ -925,9 +910,9 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const
 		{//hero has this spell in spellbook
 			logGlobal->error("Special spell %s in spellbook.", spell->getNameTranslated());
 		}
-		return specificBonus;
+		return hasBonusOfType(BonusType::SPELL, BonusSubtypeID(spell->getId()));
 	}
-	else if(!isAllowed)
+	else if(!cb->isAllowed(spell->getId()))
 	{
 		if(inSpellBook)
 		{
@@ -935,12 +920,8 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const
 			//it is normal if set in map editor, but trace it to possible debug of magic guild
 			logGlobal->trace("Banned spell %s in spellbook.", spell->getNameTranslated());
 		}
-		return inSpellBook || specificBonus || schoolBonus || levelBonus;
-	}
-	else
-	{
-		return inSpellBook || schoolBonus || specificBonus || levelBonus;
 	}
+	return !getSourcesForSpell(spell->getId()).empty();
 }
 
 bool CGHeroInstance::canLearnSpell(const spells::Spell * spell, bool allowBanned) const
@@ -1254,6 +1235,29 @@ bool CGHeroInstance::spellbookContainsSpell(const SpellID & spell) const
 	return vstd::contains(spells, spell);
 }
 
+std::vector<BonusSourceID> CGHeroInstance::getSourcesForSpell(const SpellID & spellId) const
+{
+	std::vector<BonusSourceID> sources;
+
+	if(hasSpellbook() && spellbookContainsSpell(spellId))
+		sources.emplace_back(getArt(ArtifactPosition::SPELLBOOK)->getId());
+
+	for(const auto & bonus : *getBonusesOfType(BonusType::SPELL, spellId))
+		sources.emplace_back(bonus->sid);
+
+	const auto spell = spellId.toSpell();
+	spell->forEachSchool([this, &sources](const SpellSchool & cnf, bool & stop)
+	{
+		for(const auto & bonus : *getBonusesOfType(BonusType::SPELLS_OF_SCHOOL, cnf))
+			sources.emplace_back(bonus->sid);
+	});
+
+	for(const auto & bonus : *getBonusesOfType(BonusType::SPELLS_OF_LEVEL, BonusCustomSubtype::spellLevel(spell->getLevel())))
+		sources.emplace_back(bonus->sid);
+
+	return sources;
+}
+
 void CGHeroInstance::removeSpellbook()
 {
 	spells.clear();

+ 1 - 0
lib/mapObjects/CGHeroInstance.h

@@ -153,6 +153,7 @@ public:
 	void addSpellToSpellbook(const SpellID & spell);
 	void removeSpellFromSpellbook(const SpellID & spell);
 	bool spellbookContainsSpell(const SpellID & spell) const;
+	std::vector<BonusSourceID> getSourcesForSpell(const SpellID & spell) const;
 	void removeSpellbook();
 	const std::set<SpellID> & getSpellsInSpellbook() const;
 	EAlignment getAlignment() const;

+ 3 - 1
lib/mapping/CMap.cpp

@@ -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,14 @@ 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));
+		artInst->addNewBonus(std::make_shared<Bonus>(*bonus, artInst->getId()));
 
 	return artInst;
 }

+ 1 - 0
lib/serializer/RegisterTypes.h

@@ -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);

+ 21 - 16
server/CGameHandler.cpp

@@ -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,38 @@ 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::vector<std::pair<ArtifactPosition, ArtifactInstanceID>> chargedArts;
-	for(const auto & [slot, slotInfo] : hero->artifactsWorn)
+	// Check if hero used charge based spell
+	// Try to find other sources of the spell besides the charged artifacts. If there are any, we use them.
+	std::optional<std::tuple<ArtifactPosition, ArtifactInstanceID, uint16_t>> chargedArt;
+	for(const auto & source : hero->getSourcesForSpell(spellID))
 	{
-		const auto * artInst = slotInfo.getArt();
-		const auto * artType = artInst->getType();
-		if(artType->getDischargeCondition() == DischargeArtifactCondition::SPELLCAST)
+		if(const auto * artInst = hero->getArtByInstanceId(source.as<ArtifactInstanceID>()))
 		{
-			chargedArts.emplace_back(slot, artInst->getId());
+			const auto * artType = artInst->getType();
+			const auto spellCost = artType->getChargeCost(spellID);
+			if(spellCost.has_value() && spellCost.value() <= artInst->getCharges() && artType->getDischargeCondition() == DischargeArtifactCondition::SPELLCAST)
+			{
+				chargedArt.emplace(hero->getArtPos(artInst), artInst->getId(), spellCost.value());
+			}
+			else
+			{
+				return;
+			}
 		}
 		else
 		{
-			if(const auto bonuses = artInst->getBonusesOfType(BonusType::SPELL, spellID); !bonuses->empty())
-				return;
+			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);
 }

+ 1 - 1
server/CGameHandler.h

@@ -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);

+ 1 - 1
server/battles/BattleActionProcessor.cpp

@@ -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;
 }

+ 1 - 1
server/battles/BattleResultProcessor.cpp

@@ -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);
 			}
 		};