Преглед изворни кода

Merge pull request #5720 from SoundSSGood/artifact-charges

Charged artifacts
Ivan Savenko пре 5 месеци
родитељ
комит
323231fc30

+ 2 - 0
Mods/vcmi/Content/config/english.json

@@ -28,6 +28,8 @@
 	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Movement points: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sorry, replay opponent turn is not implemented yet!",
 
+	"vcmi.artifact.charges" : "Charges",
+
 	"vcmi.bonusSource.artifact" : "Artifact",
 	"vcmi.bonusSource.creature" : "Ability",
 	"vcmi.bonusSource.spell" : "Spell",

+ 4 - 4
client/NetPacksClient.cpp

@@ -847,12 +847,12 @@ void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied &
 			UIHelper::getEagleEyeInfoWindowText(*hero, pack.learnedSpells.spells), UIHelper::getSpellsComponents(pack.learnedSpells.spells), soundBase::soundID(0));
 	}
 
-	if(!pack.artifacts.empty())
+	if(!pack.movingArtifacts.empty())
 	{
-		const auto artSet = GAME->interface()->cb->getArtSet(ArtifactLocation(pack.artifacts.front().dstArtHolder));
+		const auto artSet = GAME->interface()->cb->getArtSet(ArtifactLocation(pack.movingArtifacts.front().dstArtHolder));
 		assert(artSet);
 		std::vector<Component> artComponents;
-		for(const auto & artPack : pack.artifacts)
+		for(const auto & artPack : pack.movingArtifacts)
 		{
 			auto packComponents = UIHelper::getArtifactsComponents(*artSet, artPack.artsPack0);
 			artComponents.insert(artComponents.end(), std::make_move_iterator(packComponents.begin()), std::make_move_iterator(packComponents.end()));
@@ -861,7 +861,7 @@ void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied &
 			artComponents, soundBase::soundID(0));
 	}
 
-	for(auto & artPack : pack.artifacts)
+	for(auto & artPack : pack.movingArtifacts)
 		visitBulkMoveArtifacts(artPack);
 
 	if(pack.raisedStack.getCreature())

+ 9 - 1
client/widgets/CArtifactsOfHeroBase.cpp

@@ -271,7 +271,15 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit
 	{
 		artPlace->lockSlot(slotInfo->locked);
 		artPlace->setArtifact(slotInfo->getArt()->getTypeId(), slotInfo->getArt()->getScrollSpellID());
-		if(slotInfo->locked || slotInfo->getArt()->isCombined())
+		if(slotInfo->locked)
+			return;
+
+		const auto curArt = slotInfo->getArt();
+		// If the artifact has charges, add charges information
+		if(curArt->getType()->isCharged())
+			artPlace->addChargedArtInfo(curArt->getCharges());
+
+		if(curArt->isCombined())
 			return;
 
 		// If the artifact is part of at least one combined artifact, add additional information

+ 11 - 0
client/widgets/CComponentHolder.cpp

@@ -263,6 +263,17 @@ void CArtPlace::addCombinedArtInfo(const std::map<const ArtifactID, std::vector<
 	}
 }
 
+void CArtPlace::addChargedArtInfo(const uint16_t charges)
+{
+	MetaString info;
+	info.appendEOL();
+	info.appendEOL();
+	info.appendTextID("vcmi.artifact.charges");
+	info.appendRawString(" %d");
+	info.replaceNumber(charges);
+	text += info.toString();
+}
+
 CSecSkillPlace::CSecSkillPlace(const Point & position, const ImageSize & imageSize, const SecondarySkill & newSkillId, const uint8_t level)
 	: CComponentHolder(Rect(position, Point()), Point())
 {

+ 1 - 0
client/widgets/CComponentHolder.h

@@ -44,6 +44,7 @@ public:
 	void lockSlot(bool on);
 	bool isLocked() const;
 	void addCombinedArtInfo(const std::map<const ArtifactID, std::vector<ArtifactID>> & arts);
+	void addChargedArtInfo(const uint16_t charges);
 
 private:
 	ArtifactID artId;

+ 19 - 0
config/schemas/artifact.json

@@ -128,6 +128,25 @@
 		"onlyOnWaterMap" : {
 			"type" : "boolean",
 			"description" : "If set to true, artifact won't spawn on a map without water"
+		},
+		"charged": {
+			"type" : "object",
+			"additionalProperties" : false,
+			"description" : "Determines charged artifact behavior",
+			"required" : ["usageType"],
+			"properties" : {
+				"usageType": {
+					"type" : "string",
+					"enum" : ["SPELLCAST", "BATTLE", "BUILDING"],
+				},
+				"removeOnDepletion" : {
+					"type" : "boolean",
+				},
+				"startingCharges" : {
+					"type" : "number",
+					"description" : "Default starting charge amount"
+				}
+			}
 		}
 	}
 }

+ 7 - 1
docs/modders/Bonus/Bonus_Types.md

@@ -1031,10 +1031,16 @@ Increases amount of information available in affected thieves guild (in town or
 
 - val: additional number of 'levels' of information to grant access to
 
-### LEVEL_COUNTER
+### ARTIFACT_GROWING
 
 Internal bonus, do not use
 
+### ARTIFACT_CHARGE
+
+Consumable bonus. Used to perform actions specified by a charged artifact.
+
+- val: number of charges
+
 ### DISINTEGRATE
 
 When a unit affected by this bonus dies, no corpse is left behind

+ 13 - 0
docs/modders/Entities_Format/Artifact_Format.md

@@ -87,5 +87,18 @@ In order to make functional artifact you also need:
 		"bonusesPerLevel" : {},
 		"thresholdBonuses" : {},
 	}
+
+	// Optional, used for artifacts with charges.
+	"charged" : {
+    // Artifact discharging action
+    // SPELLCAST - Consumes a charge for each spellcast. Applies to every spell added through the "bonuses" section.
+    // BATTLE - Consumes one charge per battle.
+    // BUILDING (not implemented)
+    "usageType": "BATTLE",
+    // Optional, by default is false. Remove when fully discharged
+    "removeOnDepletion" : true,
+    // Optional, by default is 0. Default starting charge amount.
+    "startingCharges" : 2,
+	}
 }
 ```

+ 2 - 1
lib/bonuses/BonusEnum.h

@@ -16,7 +16,8 @@ class JsonNode;
 
 #define BONUS_LIST										\
 	BONUS_NAME(NONE) 									\
-	BONUS_NAME(LEVEL_COUNTER) /* for commander artifacts*/ \
+	BONUS_NAME(ARTIFACT_GROWING) \
+	BONUS_NAME(ARTIFACT_CHARGE) \
 	BONUS_NAME(MOVEMENT) /*Subtype is 1 - land, 0 - sea*/ \
 	BONUS_NAME(MORALE) \
 	BONUS_NAME(LUCK) \

+ 29 - 0
lib/entities/artifact/CArtHandler.cpp

@@ -152,6 +152,7 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 			JsonUtils::parseBonus(bonus["bonus"], &art->thresholdBonuses.back().second);
 		}
 	}
+
 	art->id = ArtifactID(index);
 	art->identifier = identifier;
 	art->modScope = scope;
@@ -220,6 +221,23 @@ std::shared_ptr<CArtifact> CArtHandler::loadFromJson(const std::string & scope,
 	if(art->isTradable())
 		art->possibleSlots.at(ArtBearer::ALTAR).push_back(ArtifactPosition::ALTAR);
 
+	if(!node["charged"].isNull())
+	{
+		art->setCondition(stringToDischargeCond(node["charged"]["usageType"].String()));
+		if(!node["charged"]["removeOnDepletion"].isNull())
+			art->setRemoveOnDepletion(node["charged"]["removeOnDepletion"].Bool());
+		if(!node["charged"]["startingCharges"].isNull())
+		{
+			const auto charges = node["charged"]["startingCharges"].Integer();
+			if(charges < 0)
+				logMod->warn("Warning! Charged artifact %s number of charges cannot be less than zero %d!", art->getNameTranslated(), charges);
+			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;
 }
 
@@ -311,6 +329,17 @@ EArtifactClass CArtHandler::stringToClass(const std::string & className)
 	return EArtifactClass::ART_SPECIAL;
 }
 
+DischargeArtifactCondition CArtHandler::stringToDischargeCond(const std::string & cond) const
+{
+	const std::unordered_map<std::string, DischargeArtifactCondition> growingConditionsMap =
+	{
+		{"SPELLCAST", DischargeArtifactCondition::SPELLCAST},
+		{"BATTLE", DischargeArtifactCondition::BATTLE},
+		//{"BUILDING", DischargeArtifactCondition::BUILDING},
+	};
+	return growingConditionsMap.at(cond);
+}
+
 void CArtHandler::loadClass(CArtifact * art, const JsonNode & node) const
 {
 	art->aClass = stringToClass(node["class"].String());

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

@@ -24,6 +24,7 @@ public:
 	void addBonuses(CArtifact * art, const JsonNode & bonusList);
 
 	static EArtifactClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor
+	DischargeArtifactCondition stringToDischargeCond(const std::string & cond) const;
 
 	bool legalArtifact(const ArtifactID & id) const;
 	static void makeItCreatureArt(CArtifact * a, bool onlyCreature = true);

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

@@ -246,6 +246,48 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b
 	}
 }
 
+CChargedArtifact::CChargedArtifact()
+	: condition(DischargeArtifactCondition::NONE)
+	,	removeOnDepletion(false)
+	, defaultStartCharges(0)
+{
+}
+
+bool CChargedArtifact::isCharged() const
+{
+	return condition != DischargeArtifactCondition::NONE;
+}
+
+void CChargedArtifact::setCondition(const DischargeArtifactCondition & dischargeCondition)
+{
+	condition = dischargeCondition;
+}
+
+void CChargedArtifact::setRemoveOnDepletion(const bool remove)
+{
+	removeOnDepletion = remove;
+}
+
+void CChargedArtifact::setDefaultStartCharges(const uint16_t charges)
+{
+	defaultStartCharges = charges;
+}
+
+uint16_t CChargedArtifact::getDefaultStartCharges() const
+{
+	return defaultStartCharges;
+}
+
+DischargeArtifactCondition CChargedArtifact::getDischargeCondition() const
+{
+	return condition;
+}
+
+bool CChargedArtifact::getRemoveOnDepletion() const
+{
+	return removeOnDepletion;
+}
+
 CArtifact::CArtifact()
 	: iconIndex(ArtifactID::NONE),
 	price(0)

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

@@ -13,6 +13,7 @@
 #include "EArtifactClass.h"
 
 #include "../../bonuses/CBonusSystemNode.h"
+#include "../../networkPacks/ArtifactLocation.h"
 
 #include <vcmi/Artifact.h>
 
@@ -64,8 +65,29 @@ public:
 	const std::vector<std::pair<ui16, Bonus>> & getThresholdBonuses() const;
 };
 
+class DLL_LINKAGE CChargedArtifact
+{
+	DischargeArtifactCondition condition;
+	bool removeOnDepletion;
+	uint16_t defaultStartCharges;
+
+protected:
+	CChargedArtifact();
+
+public:
+	bool isCharged() const;
+
+	void setCondition(const DischargeArtifactCondition & dischargeCondition);
+	void setRemoveOnDepletion(const bool remove);
+	void setDefaultStartCharges(const uint16_t charges);
+	uint16_t getDefaultStartCharges() const;
+	DischargeArtifactCondition getDischargeCondition() const;
+	bool getRemoveOnDepletion() const;
+};
+
 // Container for artifacts. Not for instances.
-class DLL_LINKAGE CArtifact final : public Artifact, public CBonusSystemNode, public CCombinedArtifact, public CScrollArtifact, public CGrowingArtifact
+class DLL_LINKAGE CArtifact final : public Artifact, public CBonusSystemNode,
+		public CCombinedArtifact, public CScrollArtifact, public CGrowingArtifact, public CChargedArtifact
 {
 	ArtifactID id;
 	std::string image;

+ 101 - 18
lib/entities/artifact/CArtifactInstance.cpp

@@ -96,39 +96,128 @@ SpellID CScrollArtifactInstance::getScrollSpellID() const
 void CGrowingArtifactInstance::growingUp()
 {
 	auto artInst = static_cast<CArtifactInstance*>(this);
+	const auto artType = artInst->getType();
 	
-	if(artInst->getType()->isGrowing())
+	if(artType->isGrowing())
 	{
+		const auto growingBonus = artInst->getBonusesOfType(BonusType::ARTIFACT_GROWING);
+		assert(!growingBonus->empty());
+		growingBonus->front()->val++;
 
-		auto bonus = std::make_shared<Bonus>();
-		bonus->type = BonusType::LEVEL_COUNTER;
-		bonus->val = 1;
-		bonus->duration = BonusDuration::COMMANDER_KILLED;
-		artInst->accumulateBonus(bonus);
-
-		for(const auto & bonus : artInst->getType()->getBonusesPerLevel())
+		for(const auto & bonus : artType->getBonusesPerLevel())
 		{
 			// Every n levels
-			if(artInst->valOfBonuses(BonusType::LEVEL_COUNTER) % bonus.first == 0)
+			if(artInst->valOfBonuses(BonusType::ARTIFACT_GROWING) % bonus.first == 0)
 			{
 				artInst->accumulateBonus(std::make_shared<Bonus>(bonus.second));
 			}
 		}
-		for(const auto & bonus : artInst->getType()->getThresholdBonuses())
+		for(const auto & bonus : artType->getThresholdBonuses())
 		{
 			// At n level
-			if(artInst->valOfBonuses(BonusType::LEVEL_COUNTER) == bonus.first)
+			if(artInst->valOfBonuses(BonusType::ARTIFACT_GROWING) == bonus.first)
 			{
 				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();
+
+	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);
+			}
+		}
+	}
+}
+
+void CChargedArtifactInstance::discharge(const uint16_t charges)
+{
+	auto artInst = static_cast<CArtifactInstance*>(this);
+
+	if(const auto chargedBonus = artInst->getBonusesOfType(BonusType::ARTIFACT_CHARGE); !chargedBonus->empty())
+	{
+		if(chargedBonus->front()->val > charges)
+			chargedBonus->front()->val -= charges;
+		else
+			chargedBonus->front()->val = 0;
+		onChargesChanged();
+	}
+}
+
+void CChargedArtifactInstance::addCharges(const uint16_t charges)
+{
+	auto artInst = static_cast<CArtifactInstance*>(this);
+
+	if(artInst->getType()->isCharged())
+	{
+		const auto chargedBonus = artInst->getBonusesOfType(BonusType::ARTIFACT_CHARGE);
+		assert(!chargedBonus->empty());
+		chargedBonus->front()->val += charges;
+		onChargesChanged();
+	}
+}
+
+uint16_t CChargedArtifactInstance::getCharges() const
+{
+	auto artInst = static_cast<const CArtifactInstance*>(this);
+
+	return artInst->valOfBonuses(BonusType::ARTIFACT_CHARGE);
+}
+
+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);
 	}
 }
 
 CArtifactInstance::CArtifactInstance(IGameInfoCallback *cb, const CArtifact * art)
 	:CArtifactInstance(cb)
 {
-	setType(art);
+	artTypeID = art->getId();
+	init();
 }
 
 CArtifactInstance::CArtifactInstance(IGameInfoCallback *cb)
@@ -137,12 +226,6 @@ CArtifactInstance::CArtifactInstance(IGameInfoCallback *cb)
 {
 }
 
-void CArtifactInstance::setType(const CArtifact * art)
-{
-	artTypeID = art->getId();
-	attachToSource(*art);
-}
-
 std::string CArtifactInstance::nodeName() const
 {
 	return "Artifact instance of " + (getType() ? getType()->getJsonKey() : std::string("uninitialized")) + " type";

+ 18 - 4
lib/entities/artifact/CArtifactInstance.h

@@ -75,16 +75,28 @@ public:
 	void growingUp();
 };
 
+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;
+};
+
 class DLL_LINKAGE CArtifactInstance final
-	: public CBonusSystemNode, public CCombinedArtifactInstance, public CScrollArtifactInstance, public CGrowingArtifactInstance
+	: public CBonusSystemNode, public CCombinedArtifactInstance, public CScrollArtifactInstance, public CGrowingArtifactInstance, public CChargedArtifactInstance
 {
 	ArtifactInstanceID id;
 	ArtifactID artTypeID;
 
+	void init();
+
 public:
 	CArtifactInstance(IGameInfoCallback *cb, const CArtifact * art);
 	CArtifactInstance(IGameInfoCallback *cb);
-	void setType(const CArtifact * art);
 	std::string nodeName() const override;
 	ArtifactID getTypeId() const;
 	const CArtifact * getType() const;
@@ -108,8 +120,10 @@ public:
 		h & id;
 
 		if(!h.saving && h.loadingGamestate)
-			setType(artTypeID.toArtifact());
-
+		{
+			init();
+			onChargesChanged();
+		}
 	}
 };
 

+ 30 - 18
lib/gameState/GameStatePackVisitor.cpp

@@ -820,6 +820,13 @@ void GameStatePackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
 		move.visit(*this);
 }
 
+void GameStatePackVisitor::visitGrowUpArtifact(GrowUpArtifact & pack)
+{
+	auto artInst = gs.getArtInstance(pack.id);
+	assert(artInst);
+	artInst->growingUp();
+}
+
 void GameStatePackVisitor::visitPutArtifact(PutArtifact & pack)
 {
 	auto art = gs.getArtInstance(pack.id);
@@ -914,6 +921,21 @@ void GameStatePackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 	bulkArtsPut(pack.artsPack0, artInitialSetLeft, *rightSet);
 }
 
+void GameStatePackVisitor::visitDischargeArtifact(DischargeArtifact & pack)
+{
+	auto artInst = gs.getArtInstance(pack.id);
+	assert(artInst);
+	artInst->discharge(pack.charges);
+	if(artInst->getType()->getRemoveOnDepletion() && artInst->getCharges() == 0 && pack.artLoc.has_value())
+	{
+		BulkEraseArtifacts ePack;
+		ePack.artHolder = pack.artLoc.value().artHolder;
+		ePack.creature = pack.artLoc.value().creature;
+		ePack.posPack.push_back(pack.artLoc.value().slot);
+		ePack.visit(*this);
+	}
+}
+
 void GameStatePackVisitor::visitAssembledArtifact(AssembledArtifact & pack)
 {
 	auto artSet = gs.getArtSet(pack.al.artHolder);
@@ -1217,22 +1239,6 @@ void GameStatePackVisitor::visitBattleResultAccepted(BattleResultAccepted & pack
 	if(const auto defenderHero = gs.getHero(pack.heroResult[BattleSide::DEFENDER].heroID))
 		defenderHero->removeBonusesRecursive(Bonus::OneBattle);
 
-	if(pack.winnerSide != BattleSide::NONE)
-	{
-		// Grow up growing artifacts
-		if(const auto winnerHero = gs.getHero(pack.heroResult[pack.winnerSide].heroID))
-		{
-			if(winnerHero->getCommander() && winnerHero->getCommander()->alive)
-
-			{
-				for(auto & art : winnerHero->getCommander()->artifactsWorn)
-					gs.getArtInstance(art.second.getID())->growingUp();
-			}
-			for(auto & art : winnerHero->artifactsWorn)
-				gs.getArtInstance(art.second.getID())->growingUp();
-		}
-	}
-
 	if(gs.getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE))
 	{
 		if(const auto attackerArmy = gs.getArmyInstance(pack.heroResult[BattleSide::ATTACKER].armyID))
@@ -1343,8 +1349,14 @@ void GameStatePackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack
 {
 	pack.learnedSpells.visit(*this);
 
-	for(auto & artPack : pack.artifacts)
-		artPack.visit(*this);
+	for(auto & discharging : pack.dischargingArtifacts)
+		discharging.visit(*this);
+
+	for(auto & growing : pack.growingArtifacts)
+		growing.visit(*this);
+
+	for(auto & movingPack : pack.movingArtifacts)
+		movingPack.visit(*this);
 
 	const auto currentBattle = std::find_if(gs.currentBattles.begin(), gs.currentBattles.end(),
 											[&](const auto & battle)

+ 2 - 0
lib/gameState/GameStatePackVisitor.h

@@ -41,11 +41,13 @@ public:
 	void visitInsertNewStack(InsertNewStack & pack) override;
 	void visitRebalanceStacks(RebalanceStacks & pack) override;
 	void visitBulkRebalanceStacks(BulkRebalanceStacks & pack) override;
+	void visitGrowUpArtifact(GrowUpArtifact & pack) override;
 	void visitPutArtifact(PutArtifact & pack) override;
 	void visitBulkEraseArtifacts(BulkEraseArtifacts & pack) override;
 	void visitBulkMoveArtifacts(BulkMoveArtifacts & pack) override;
 	void visitAssembledArtifact(AssembledArtifact & pack) override;
 	void visitDisassembledArtifact(DisassembledArtifact & pack) override;
+	void visitDischargeArtifact(DischargeArtifact & pack) override;
 	void visitHeroVisit(HeroVisit & pack) override;
 	void visitNewTurn(NewTurn & pack) override;
 	void visitGiveBonus(GiveBonus & pack) override;

+ 9 - 1
lib/mapping/CMap.cpp

@@ -838,7 +838,7 @@ CArtifactInstance * CMap::createArtifact(const ArtifactID & artID, const SpellID
 	if(art->isGrowing())
 	{
 		auto bonus = std::make_shared<Bonus>();
-		bonus->type = BonusType::LEVEL_COUNTER;
+		bonus->type = BonusType::ARTIFACT_GROWING;
 		bonus->val = 0;
 		artInst->addNewBonus(bonus);
 	}
@@ -847,6 +847,14 @@ CArtifactInstance * CMap::createArtifact(const ArtifactID & artID, const SpellID
 		artInst->addNewBonus(std::make_shared<Bonus>(BonusDuration::PERMANENT, BonusType::SPELL,
 													 BonusSource::ARTIFACT_INSTANCE, -1, BonusSourceID(ArtifactID(ArtifactID::SPELL_SCROLL)), BonusSubtypeID(spellId)));
 	}
+	if(art->isCharged())
+	{
+		auto bonus = std::make_shared<Bonus>();
+		bonus->type = BonusType::ARTIFACT_CHARGE;
+		bonus->val = 0;
+		artInst->addNewBonus(bonus);
+		artInst->addCharges(art->getDefaultStartCharges());
+	}
 	return artInst;
 }
 

+ 8 - 0
lib/networkPacks/ArtifactLocation.h

@@ -79,4 +79,12 @@ struct MoveArtifactInfo
 	}
 };
 
+enum class DischargeArtifactCondition : int8_t
+{
+	NONE,
+	SPELLCAST,
+	BATTLE,
+	//BUILDING	// not implemented
+};
+
 VCMI_LIB_NAMESPACE_END

+ 2 - 0
lib/networkPacks/NetPackVisitor.h

@@ -77,9 +77,11 @@ public:
 	virtual void visitInsertNewStack(InsertNewStack & pack) {}
 	virtual void visitRebalanceStacks(RebalanceStacks & pack) {}
 	virtual void visitBulkRebalanceStacks(BulkRebalanceStacks & pack) {}
+	virtual void visitGrowUpArtifact(GrowUpArtifact & pack) {}
 	virtual void visitPutArtifact(PutArtifact & pack) {}
 	virtual void visitBulkEraseArtifacts(BulkEraseArtifacts & pack) {}
 	virtual void visitBulkMoveArtifacts(BulkMoveArtifacts & pack) {}
+	virtual void visitDischargeArtifact(DischargeArtifact & pack) {}
 	virtual void visitAssembledArtifact(AssembledArtifact & pack) {}
 	virtual void visitDisassembledArtifact(DisassembledArtifact & pack) {}
 	virtual void visitHeroVisit(HeroVisit & pack) {}

+ 10 - 0
lib/networkPacks/NetPacksLib.cpp

@@ -303,6 +303,11 @@ void BulkRebalanceStacks::visitTyped(ICPackVisitor & visitor)
 	visitor.visitBulkRebalanceStacks(*this);
 }
 
+void GrowUpArtifact::visitTyped(ICPackVisitor & visitor)
+{
+	visitor.visitGrowUpArtifact(*this);
+}
+
 void PutArtifact::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitPutArtifact(*this);
@@ -323,6 +328,11 @@ void AssembledArtifact::visitTyped(ICPackVisitor & visitor)
 	visitor.visitAssembledArtifact(*this);
 }
 
+void DischargeArtifact::visitTyped(ICPackVisitor & visitor)
+{
+	visitor.visitDischargeArtifact(*this);
+}
+
 void DisassembledArtifact::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitDisassembledArtifact(*this);

+ 41 - 0
lib/networkPacks/PacksForClient.h

@@ -909,6 +909,24 @@ struct DLL_LINKAGE CArtifactOperationPack : CPackForClient
 {
 };
 
+struct DLL_LINKAGE GrowUpArtifact : CArtifactOperationPack
+{
+	ArtifactInstanceID id;
+
+	GrowUpArtifact() = default;
+	GrowUpArtifact(const ArtifactInstanceID & id)
+		: id(id)
+	{
+	}
+
+	void visitTyped(ICPackVisitor & visitor) override;
+
+	template <typename Handler> void serialize(Handler & h)
+	{
+		h & id;
+	}
+};
+
 struct DLL_LINKAGE PutArtifact : CArtifactOperationPack
 {
 	PutArtifact() = default;
@@ -1007,6 +1025,29 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack
 	}
 };
 
+struct DLL_LINKAGE DischargeArtifact : CArtifactOperationPack
+{
+	ArtifactInstanceID id;
+	uint16_t charges;
+	std::optional<ArtifactLocation> artLoc;
+
+	DischargeArtifact() = default;
+	DischargeArtifact(const ArtifactInstanceID & id, const uint16_t charges)
+		: id(id)
+		, charges(charges)
+	{
+	}
+
+	void visitTyped(ICPackVisitor & visitor) override;
+
+	template <typename Handler> void serialize(Handler & h)
+	{
+		h & id;
+		h & charges;
+		h & artLoc;
+	}
+};
+
 struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack
 {
 	ArtifactLocation al;

+ 6 - 4
lib/networkPacks/PacksForClientBattle.h

@@ -104,7 +104,6 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient
 	BattleID battleID = BattleID::NONE;
 	BattleSideArray<HeroBattleResults> heroResult;
 	BattleSide winnerSide;
-	std::vector<BulkMoveArtifacts> artifacts;
 
 	void visitTyped(ICPackVisitor & visitor) override;
 
@@ -113,7 +112,6 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient
 		h & battleID;
 		h & heroResult;
 		h & winnerSide;
-		h & artifacts;
 		assert(battleID != BattleID::NONE);
 	}
 };
@@ -391,7 +389,9 @@ struct DLL_LINKAGE BattleResultsApplied : public CPackForClient
 	PlayerColor victor;
 	PlayerColor loser;
 	ChangeSpells learnedSpells;
-	std::vector<BulkMoveArtifacts> artifacts;
+	std::vector<BulkMoveArtifacts> movingArtifacts;
+	std::vector<GrowUpArtifact> growingArtifacts;
+	std::vector<DischargeArtifact> dischargingArtifacts;
 	CStackBasicDescriptor raisedStack;
 	void visitTyped(ICPackVisitor & visitor) override;
 
@@ -401,7 +401,9 @@ struct DLL_LINKAGE BattleResultsApplied : public CPackForClient
 		h & victor;
 		h & loser;
 		h & learnedSpells;
-		h & artifacts;
+		h & movingArtifacts;
+		h & growingArtifacts;
+		h & dischargingArtifacts;
 		h & raisedStack;
 		assert(battleID != BattleID::NONE);
 	}

+ 2 - 0
lib/serializer/RegisterTypes.h

@@ -219,6 +219,7 @@ void registerTypes(Serializer &s)
 	s.template registerType<SwapStacks>(165);
 	s.template registerType<InsertNewStack>(166);
 	s.template registerType<RebalanceStacks>(167);
+	s.template registerType<GrowUpArtifact>(168);
 	s.template registerType<PutArtifact>(169);
 	s.template registerType<BulkEraseArtifacts>(170);
 	s.template registerType<AssembledArtifact>(171);
@@ -226,6 +227,7 @@ void registerTypes(Serializer &s)
 	s.template registerType<BulkMoveArtifacts>(173);
 	s.template registerType<PlayerMessageClient>(174);
 	s.template registerType<BulkRebalanceStacks>(175);
+	s.template registerType<DischargeArtifact>(176);
 	s.template registerType<SetRewardableConfiguration>(177);
 	s.template registerType<CPackForServer>(179);
 	s.template registerType<EndTurn>(180);

+ 34 - 0
server/CGameHandler.cpp

@@ -3931,6 +3931,9 @@ void CGameHandler::castSpell(const spells::Caster * caster, SpellID spellID, con
 
 	const CSpell * s = spellID.toSpell();
 	s->adventureCast(spellEnv, p);
+
+	if(const auto hero = caster->getHeroCaster())
+		useChargedArtifactUsed(hero->id, spellID);
 }
 
 bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & sl2)
@@ -4331,3 +4334,34 @@ void CGameHandler::startBattle(const CArmedInstance *army1, const CArmedInstance
 {
 	battles->startBattle(army1, army2);
 }
+
+void CGameHandler::useChargedArtifactUsed(const ObjectInstanceID & heroObjectID, const SpellID & spellID)
+{
+	const auto hero = 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)
+	{
+		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;
+		}
+	}
+
+	assert(!chargedArts.empty());
+	DischargeArtifact msg(chargedArts.front().second, 1);
+	msg.artLoc.emplace(hero->id, chargedArts.front().first);
+	sendAndApply(msg);
+}

+ 1 - 0
server/CGameHandler.h

@@ -169,6 +169,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);
 
 	/// Returns hero that is currently visiting this object, or nullptr if no visit is active
 	const CGHeroInstance * getVisitingHero(const CGObjectInstance *obj);

+ 1 - 0
server/battles/BattleActionProcessor.cpp

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

+ 52 - 4
server/battles/BattleResultProcessor.cpp

@@ -22,6 +22,7 @@
 #include "../../lib/IGameSettings.h"
 #include "../../lib/battle/SideInBattle.h"
 #include "../../lib/entities/artifact/ArtifactUtils.h"
+#include "../../lib/entities/artifact/CArtifact.h"
 #include "../../lib/entities/artifact/CArtifactFittingSet.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
@@ -427,7 +428,7 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 		}
 	}
 
-	// Artifacts handling
+	// Moving artifacts handling
 	if(result.result == EBattleResult::NORMAL && !finishingBattle->isDraw() && winnerHero)
 	{
 		CArtifactFittingSet artFittingSet(*winnerHero);
@@ -446,7 +447,7 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 
 		if(loserHero)
 		{
-			auto & packHero = resultsApplied.artifacts.emplace_back(finishingBattle->victor, finishingBattle->loserId, finishingBattle->winnerId, false);
+			auto & packHero = resultsApplied.movingArtifacts.emplace_back(finishingBattle->victor, finishingBattle->loserId, finishingBattle->winnerId, false);
 			packHero.srcArtHolder = finishingBattle->loserId;
 			for(const auto & slot : ArtifactUtils::commonWornSlots())
 			{
@@ -463,7 +464,7 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 
 			if(loserHero->getCommander())
 			{
-				auto & packCommander = resultsApplied.artifacts.emplace_back(finishingBattle->victor, finishingBattle->loserId, finishingBattle->winnerId, false);
+				auto & packCommander = resultsApplied.movingArtifacts.emplace_back(finishingBattle->victor, finishingBattle->loserId, finishingBattle->winnerId, false);
 				packCommander.srcCreature = loserHero->findStack(loserHero->getCommander());
 				for(const auto & artSlot : loserHero->getCommander()->artifactsWorn)
 					addArtifactToTransfer(packCommander, artSlot.first, artSlot.second.getArt());
@@ -471,7 +472,7 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 			auto armyObj = dynamic_cast<const CArmedInstance*>(gameHandler->getObj(finishingBattle->loserId));
 			for(const auto & armySlot : armyObj->stacks)
 			{
-				auto & packsArmy = resultsApplied.artifacts.emplace_back(finishingBattle->victor, finishingBattle->loserId, finishingBattle->winnerId, false);
+				auto & packsArmy = resultsApplied.movingArtifacts.emplace_back(finishingBattle->victor, finishingBattle->loserId, finishingBattle->winnerId, false);
 				packsArmy.srcArtHolder = armyObj->id;
 				packsArmy.srcCreature = armySlot.first;
 				for(const auto & artSlot : armySlot.second->artifactsWorn)
@@ -480,6 +481,53 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 		}
 	}
 
+	// Growing artifacts handling
+	if(!finishingBattle->isDraw() && winnerHero)
+	{
+		const auto addArtifactToGrowing = [&resultsApplied](const std::map<ArtifactPosition, ArtSlotInfo> & artMap)
+		{
+			for(const auto & [slot, slotInfo] : artMap)
+			{
+				const auto artInst = slotInfo.getArt();
+				assert(artInst);
+				if(artInst->getType()->isGrowing())
+					resultsApplied.growingArtifacts.emplace_back(artInst->getId());
+			}
+		};
+
+		if(const auto commander = winnerHero->getCommander(); commander && commander->alive)
+			addArtifactToGrowing(commander->artifactsWorn);
+		addArtifactToGrowing(winnerHero->artifactsWorn);
+	}
+
+	// Charged artifacts handling
+	const auto addArtifactToDischarging = [&resultsApplied](const std::map<ArtifactPosition, ArtSlotInfo> & artMap,
+			const ObjectInstanceID & id, const std::optional<SlotID> & creature = std::nullopt)
+	{
+		for(const auto & [slot, slotInfo] : artMap)
+		{
+			auto artInst = slotInfo.getArt();
+			assert(artInst);
+			if(const auto condition = artInst->getType()->getDischargeCondition(); condition == DischargeArtifactCondition::BATTLE)
+			{
+				auto & discharging = resultsApplied.dischargingArtifacts.emplace_back(artInst->getId(), 1);
+				discharging.artLoc.emplace(id, creature, slot);
+			}
+		}
+	};
+	if(winnerHero)
+	{
+		addArtifactToDischarging(winnerHero->artifactsWorn, winnerHero->id);
+		if(const auto commander = winnerHero->getCommander())
+			addArtifactToDischarging(commander->artifactsWorn, winnerHero->id, winnerHero->findStack(winnerHero->getCommander()));
+	}
+	if(loserHero)
+	{
+		addArtifactToDischarging(loserHero->artifactsWorn, loserHero->id);
+		if(const auto commander = loserHero->getCommander())
+			addArtifactToDischarging(commander->artifactsWorn, loserHero->id, loserHero->findStack(loserHero->getCommander()));
+	}
+
 	// Necromancy handling
 	// Give raised units to winner, if any were raised, units will be given after casualties are taken
 	if(winnerHero)