浏览代码

HAS_CHARGES_LIMITER

SoundSSGood 5 月之前
父节点
当前提交
6752ab3a75

+ 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

+ 1 - 1
lib/bonuses/Bonus.h

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

+ 1 - 0
lib/bonuses/CBonusSystemNode.h

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

+ 27 - 0
lib/bonuses/Limiters.cpp

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

+ 18 - 0
lib/bonuses/Limiters.h

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

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

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

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

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

+ 8 - 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,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;
 }

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

+ 25 - 15
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,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::vector<std::pair<ArtifactPosition, ArtifactInstanceID>> chargedArts;
+	std::optional<std::tuple<ArtifactPosition, ArtifactInstanceID, uint16_t>> chargedArt;
+
+	// 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);
 }

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