Browse Source

Rewardable objects may now define guards. Converted Crypt to rewardable.

Ivan Savenko 1 year ago
parent
commit
785036836c

+ 1 - 1
AI/VCAI/FuzzyHelper.cpp

@@ -302,6 +302,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
 		return iat.army.getStrength();
 	}
 	case Obj::MONSTER:
+	case Obj::CRYPT:
 	{
 		//TODO!!!!!!!!
 		const CGCreature * cre = dynamic_cast<const CGCreature *>(obj);
@@ -319,7 +320,6 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
 		const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
 		return a->getArmyStrength();
 	}
-	case Obj::CRYPT: //crypt
 	case Obj::CREATURE_BANK: //crebank
 	case Obj::DRAGON_UTOPIA:
 	case Obj::SHIPWRECK: //shipwreck

+ 38 - 38
config/objects/creatureBanks.json

@@ -833,7 +833,7 @@
 	},
 	"crypt" : {
 		"index" :84,
-		"handler": "bank",
+		"handler": "configurable",
 		"base" : {
 			"sounds" : {
 				"ambient" : ["LOOPDEAD"],
@@ -843,16 +843,27 @@
 		"types" : {
 			"crypt" : {
 				"index" : 0,
-				"resetDuration" : 0,
 				"name" : "Crypt",
 				"aiValue" : 1500,
+
 				"rmg" : {
 					"value"		: 1000,
 					"rarity"	: 100
 				},
-				"levels": [
+
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+				"onGuardedMessage" : 119, // Do you want to search the graves?
+				"onVisited" : [
 					{
-						"chance": 30,
+						"message" : 120, // Such a despicable act reduces your army's morale.
+						"bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE" } ]
+					}
+				],
+
+				"rewards": [
+					{
+						"appearChance" : { "min" : 0, "max" : 30 },
 						"guards": [
 							{ "amount": 10, "type": "skeleton" },
 							{ "amount": 10, "type": "walkingDead" },
@@ -860,17 +871,14 @@
 							{ "amount": 10, "type": "skeleton" },
 							{ "amount": 10, "type": "skeleton" }
 						],
-						"combat_value": 75,
-						"reward" : {
-							"value": 1500,
-							"resources":
-							{
-								"gold" : 1500
-							}
+						"message" : 121, // you search the graves and find something
+						"resources":
+						{
+							"gold" : 1500
 						}
 					},
 					{
-						"chance": 30,
+						"appearChance" : { "min" : 30, "max" : 60 },
 						"guards": [
 							{ "amount": 13, "type": "skeleton" },
 							{ "amount": 10, "type": "walkingDead" },
@@ -878,50 +886,42 @@
 							{ "amount": 10, "type": "walkingDead" },
 							{ "amount": 12, "type": "skeleton" }
 						],
-						"combat_value": 94,
-						"reward" : {
-							"value": 2000,
-							"resources":
-							{
-								"gold" : 2000
-							}
+						"message" : 121, // you search the graves and find something
+						"resources":
+						{
+							"gold" : 2000
 						}
 					},
 					{
-						"chance": 30,
+						"appearChance" : { "min" : 60, "max" : 90 },
 						"guards": [
 							{ "amount": 20, "type": "skeleton" },
 							{ "amount": 20, "type": "walkingDead" },
 							{ "amount": 10, "type": "wight" },
 							{ "amount": 5, "type": "vampire" }
 						],
-						"combat_value": 169,
-						"reward" : {
-							"value": 3500,
-							"resources":
-							{
-								"gold" : 2500
-							},
-							"artifacts": [ { "class" : "TREASURE" } ]
+						"message" : 121, // you search the graves and find something
+						"resources":
+						{
+							"gold" : 2500
+						},
+						"artifacts": [ { "class" : "TREASURE" } ]
 						}
 					},
 					{
-						"chance": 10,
+						"appearChance" : { "min" : 90, "max" : 100 },
 						"guards": [
 							{ "amount": 20, "type": "skeleton" },
 							{ "amount": 20, "type": "walkingDead" },
 							{ "amount": 10, "type": "wight" },
 							{ "amount": 10, "type": "vampire" }
 						],
-						"combat_value": 225,
-						"reward" : {
-							"value": 6000,
-							"resources":
-							{
-								"gold" : 5000
-							},
-							"artifacts": [ { "class" : "TREASURE" } ]
-						}
+						"message" : 121, // you search the graves and find something
+						"resources":
+						{
+							"gold" : 5000
+						},
+						"artifacts": [ { "class" : "TREASURE" } ]
 					}
 				]
 			}

+ 13 - 10
lib/mapObjectConstructors/CRewardableConstructor.cpp

@@ -62,17 +62,20 @@ Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCal
 
 void CRewardableConstructor::configureObject(CGObjectInstance * object, vstd::RNG & rng) const
 {
-	if(auto * rewardableObject = dynamic_cast<CRewardableObject*>(object))
-	{
-		rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID);
+	auto * rewardableObject = dynamic_cast<CRewardableObject*>(object);
 
-		if (rewardableObject->configuration.info.empty())
-		{
-			if (objectInfo.getParameters()["rewards"].isNull())
-				logMod->error("Object %s has invalid configuration! No defined rewards found!", getJsonKey());
-			else
-				logMod->error("Object %s has invalid configuration! Make sure that defined appear chances are continuous!", getJsonKey());
-		}
+	if (!rewardableObject)
+		throw std::runtime_error("Object " + std::to_string(object->getObjGroupIndex()) + ", " + std::to_string(object->getObjTypeIndex()) + " is not a rewardable object!" );
+
+	rewardableObject->configuration = generateConfiguration(object->cb, rng, object->ID);
+	rewardableObject->initializeGuards();
+
+	if (rewardableObject->configuration.info.empty())
+	{
+		if (objectInfo.getParameters()["rewards"].isNull())
+			logMod->error("Object %s has invalid configuration! No defined rewards found!", getJsonKey());
+		else
+			logMod->error("Object %s has invalid configuration! Make sure that defined appear chances are continuous!", getJsonKey());
 	}
 }
 

+ 0 - 19
lib/mapObjects/CBank.cpp

@@ -15,7 +15,6 @@
 #include <vcmi/spells/Service.h>
 
 #include "../texts/CGeneralTextHandler.h"
-#include "../CSoundBase.h"
 #include "../IGameSettings.h"
 #include "../CPlayerState.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
@@ -156,23 +155,18 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const
 	case Obj::DRAGON_UTOPIA:
 		banktext = 47;
 		break;
-	case Obj::CRYPT:
-		banktext = 119;
-		break;
 	case Obj::SHIPWRECK:
 		banktext = 122;
 		break;
 	case Obj::PYRAMID:
 		banktext = 105;
 		break;
-	case Obj::CREATURE_BANK:
 	default:
 		banktext = 32;
 		break;
 	}
 	BlockingDialog bd(true, false);
 	bd.player = h->getOwner();
-	bd.soundID = soundBase::invalid; // Sound is handled in json files, else two sounds are played
 	bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext);
 	bd.components = getPopupComponents(h->getOwner());
 	if (banktext == 32)
@@ -196,17 +190,12 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 		case Obj::DERELICT_SHIP:
 			textID = 43;
 			break;
-		case Obj::CRYPT:
-			textID = 121;
-			break;
 		case Obj::SHIPWRECK:
 			textID = 124;
 			break;
 		case Obj::PYRAMID:
 			textID = 106;
 			break;
-		case Obj::CREATURE_BANK:
-		case Obj::DRAGON_UTOPIA:
 		default:
 			textID = 34;
 			break;
@@ -218,7 +207,6 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 		{
 		case Obj::SHIPWRECK:
 		case Obj::DERELICT_SHIP:
-		case Obj::CRYPT:
 		{
 			GiveBonus gbonus;
 			gbonus.id = hero->id;
@@ -237,14 +225,9 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 				textID = 42;
 				gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.101");
 				break;
-			case Obj::CRYPT:
-				textID = 120;
-				gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.98");
-				break;
 			}
 			cb->giveHeroBonus(&gbonus);
 			iw.components.emplace_back(ComponentType::MORALE, -1);
-			iw.soundID = soundBase::invalid;
 			break;
 		}
 		case Obj::PYRAMID:
@@ -258,8 +241,6 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			iw.components.emplace_back(ComponentType::LUCK, -2);
 			break;
 		}
-		case Obj::CREATURE_BANK:
-		case Obj::DRAGON_UTOPIA:
 		default:
 			iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty
 			iw.text.replaceRawString(getObjectName());

+ 127 - 35
lib/mapObjects/CRewardableObject.cpp

@@ -10,16 +10,19 @@
 
 #include "StdInc.h"
 #include "CRewardableObject.h"
-#include "../gameState/CGameState.h"
-#include "../texts/CGeneralTextHandler.h"
+
 #include "../CPlayerState.h"
+#include "../GameSettings.h"
 #include "../IGameCallback.h"
+#include "../gameState/CGameState.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../mapObjectConstructors/CRewardableConstructor.h"
 #include "../mapObjects/CGHeroInstance.h"
 #include "../networkPacks/PacksForClient.h"
+#include "../networkPacks/PacksForClientBattle.h"
 #include "../serializer/JsonSerializeFormat.h"
+#include "../texts/CGeneralTextHandler.h"
 
 #include <vstd/RNG.h>
 
@@ -91,7 +94,42 @@ std::vector<Component> CRewardableObject::loadComponents(const CGHeroInstance *
 	return result;
 }
 
+bool CRewardableObject::guardedPotentially() const
+{
+	for (auto const & visitInfo : configuration.info)
+		if (!visitInfo.reward.guards.empty())
+			return true;
+
+	return false;
+}
+
+bool CRewardableObject::guardedPresently() const
+{
+	return stacksCount() > 0;
+}
+
 void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
+{
+	if (guardedPresently())
+	{
+		auto guardedIndexes = getAvailableRewards(h, Rewardable::EEventType::EVENT_GUARDED);
+		auto guardedReward = configuration.info.at(guardedIndexes.at(0));
+
+		// ask player to confirm attack
+		BlockingDialog bd(true, false);
+		bd.player = h->getOwner();
+		bd.text = guardedReward.message;
+		bd.components = getPopupComponents(h->getOwner());
+
+		cb->showBlockingDialog(&bd);
+	}
+	else
+	{
+		doHeroVisit(h);
+	}
+}
+
+void CRewardableObject::doHeroVisit(const CGHeroInstance *h) const
 {
 	if(!wasVisitedBefore(h))
 	{
@@ -181,39 +219,54 @@ void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const
 	grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero);
 }
 
-void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
+void CRewardableObject::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const
+{
+	if (result.winner == BattleSide::ATTACKER)
+	{
+		doHeroVisit(hero);
+	}
+}
+
+void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int32_t answer) const
 {
-	if(answer == 0)
+	if(guardedPresently())
+	{
+		if (answer)
+			cb->startBattleI(hero, this, true);
+	}
+	else
 	{
-		switch (configuration.visitMode)
+		if(answer == 0)
 		{
-			case Rewardable::VISIT_UNLIMITED:
-			case Rewardable::VISIT_BONUS:
-			case Rewardable::VISIT_HERO:
-			case Rewardable::VISIT_LIMITER:
+			switch(configuration.visitMode)
 			{
-				// workaround for object with refusable reward not getting marked as visited
-				// TODO: better solution that would also work for player-visitable objects
-				if (!wasScouted(hero->getOwner()))
+				case Rewardable::VISIT_UNLIMITED:
+				case Rewardable::VISIT_BONUS:
+				case Rewardable::VISIT_HERO:
+				case Rewardable::VISIT_LIMITER:
 				{
-					ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, hero->id);
-					cb->sendAndApply(&cov);
+					// workaround for object with refusable reward not getting marked as visited
+					// TODO: better solution that would also work for player-visitable objects
+					if(!wasScouted(hero->getOwner()))
+					{
+						ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, hero->id);
+						cb->sendAndApply(&cov);
+					}
 				}
 			}
+			return; // player refused
 		}
 
-		return; // player refused
-	}
-
-	if(answer > 0 && answer-1 < configuration.info.size())
-	{
-		auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
-		markAsVisited(hero);
-		grantReward(list[answer - 1], hero);
-	}
-	else
-	{
-		throw std::runtime_error("Unhandled choice");
+		if(answer > 0 && answer - 1 < configuration.info.size())
+		{
+			auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
+			markAsVisited(hero);
+			grantReward(list[answer - 1], hero);
+		}
+		else
+		{
+			throw std::runtime_error("Unhandled choice");
+		}
 	}
 }
 
@@ -368,19 +421,41 @@ std::vector<Component> CRewardableObject::getPopupComponentsImpl(PlayerColor pla
 	if (!configuration.showScoutedPreview)
 		return {};
 
-	auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
-	if (rewardIndices.empty() && !configuration.info.empty())
+	if (guardedPresently())
 	{
-		// Object has valid config, but current hero has no rewards that he can receive.
-		// Usually this happens if hero has already visited this object -> show reward using context without any hero
-		// since reward may be context-sensitive - e.g. Witch Hut that gives 1 skill, but always at basic level
-		return loadComponents(nullptr, {0});
+		if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION))
+			return {};
+
+		std::map<CreatureID, int> guardsAmounts;
+		std::vector<Component> result;
+
+		for (auto const & slot : Slots())
+			if (slot.second)
+				guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount();
+
+		for (auto const & guard : guardsAmounts)
+		{
+			Component comp(ComponentType::CREATURE, guard.first, guard.second);
+			result.push_back(comp);
+		}
+		return result;
 	}
+	else
+	{
+		auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
+		if (rewardIndices.empty() && !configuration.info.empty())
+		{
+			// Object has valid config, but current hero has no rewards that he can receive.
+			// Usually this happens if hero has already visited this object -> show reward using context without any hero
+			// since reward may be context-sensitive - e.g. Witch Hut that gives 1 skill, but always at basic level
+			return loadComponents(nullptr, {0});
+		}
 
-	if (rewardIndices.empty())
-		return {};
+		if (rewardIndices.empty())
+			return {};
 
-	return loadComponents(hero, rewardIndices);
+		return loadComponents(hero, rewardIndices);
+	}
 }
 
 std::vector<Component> CRewardableObject::getPopupComponents(PlayerColor player) const
@@ -440,4 +515,21 @@ void CRewardableObject::serializeJsonOptions(JsonSerializeFormat & handler)
 	handler.serializeStruct("rewardable", static_cast<Rewardable::Interface&>(*this));
 }
 
+void CRewardableObject::initializeGuards()
+{
+	clearSlots();
+
+	for (auto const & visitInfo : configuration.info)
+	{
+		for (auto const & guard : visitInfo.reward.guards)
+		{
+			auto slotID = getFreeSlot();
+			if (!slotID.validSlot())
+				return;
+
+			putStack(slotID, new CStackInstance(guard.getId(), guard.getCount()));
+		}
+	}
+}
+
 VCMI_LIB_NAMESPACE_END

+ 11 - 0
lib/mapObjects/CRewardableObject.h

@@ -45,7 +45,14 @@ protected:
 	std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const;
 	std::vector<Component> getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const;
 
+	void doHeroVisit(const CGHeroInstance *h) const;
+
+	/// Returns true if this object might have guards present, whether they were cleared or not
+	bool guardedPotentially() const;
+	/// Returns true if this object is currently guarded
+	bool guardedPresently() const;
 public:
+
 	/// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player)
 	bool wasVisited(PlayerColor player) const override;
 	bool wasVisited(const CGHeroInstance * h) const override;
@@ -56,6 +63,8 @@ public:
 	/// gives reward to player or ask for choice in case of multiple rewards
 	void onHeroVisit(const CGHeroInstance *h) const override;
 
+	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
+
 	///possibly resets object state
 	void newTurn(vstd::RNG & rand) const override;
 
@@ -66,6 +75,8 @@ public:
 	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 	void initObj(vstd::RNG & rand) override;
+
+	void initializeGuards();
 	
 	void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
 

+ 0 - 0
lib/mapObjects/CreatureBank.cpp


+ 0 - 0
lib/mapObjects/CreatureBank.h


+ 1 - 0
lib/networkPacks/NetPacksLib.cpp

@@ -2434,6 +2434,7 @@ void SetRewardableConfiguration::applyGs(CGameState *gs)
 		auto * rewardablePtr = dynamic_cast<CRewardableObject *>(objectPtr);
 		assert(rewardablePtr);
 		rewardablePtr->configuration = configuration;
+		rewardablePtr->initializeGuards();
 	}
 	else
 	{

+ 2 - 1
lib/rewardable/Configuration.h

@@ -44,7 +44,8 @@ enum class EEventType
 	EVENT_INVALID = 0,
 	EVENT_FIRST_VISIT,
 	EVENT_ALREADY_VISITED,
-	EVENT_NOT_AVAILABLE
+	EVENT_NOT_AVAILABLE,
+	EVENT_GUARDED
 };
 
 constexpr std::array<std::string_view, 4> SelectModeString{"selectFirst", "selectPlayer", "selectRandom", "selectAll"};

+ 12 - 0
lib/rewardable/Info.cpp

@@ -174,6 +174,8 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, vstd:
 	reward.removeObject = source["removeObject"].Bool();
 	reward.bonuses = randomizer.loadBonuses(source["bonuses"]);
 
+	reward.guards = randomizer.loadCreatures(source["guards"], rng, variables);
+
 	reward.primary = randomizer.loadPrimaries(source["primary"], rng, variables);
 	reward.secondary = randomizer.loadSecondaries(source["secondary"], rng, variables);
 
@@ -378,6 +380,16 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, vstd:
 		object.info.push_back(onEmpty);
 	}
 
+	if (!parameters["onGuardedMessage"].isNull())
+	{
+		Rewardable::VisitInfo onGuarded;
+		onGuarded.visitType = Rewardable::EEventType::EVENT_GUARDED;
+		onGuarded.message = loadMessage(parameters["onGuardedMessage"], TextIdentifier(objectTextID, "onGuarded"));
+		replaceTextPlaceholders(onGuarded.message, object.variables);
+
+		object.info.push_back(onGuarded);
+	}
+
 	configureResetInfo(object, rng, object.resetParameters, parameters["resetParameters"]);
 
 	object.canRefuse = parameters["canRefuse"].Bool();

+ 3 - 0
lib/rewardable/Reward.h

@@ -82,6 +82,9 @@ struct DLL_LINKAGE Reward final
 	/// fixed value, in form of percentage from max
 	si32 movePercentage;
 
+	/// Guards that must be defeated in order to access this reward, empty if not guarded
+	std::vector<CStackBasicDescriptor> guards;
+
 	/// list of bonuses, e.g. morale/luck
 	std::vector<Bonus> bonuses;