浏览代码

Breaking things - first commit towards configurable object(s).
- New files: lib/CObjectWithReward.h/cpp
- Classes that will be replaced by configurable object are now in this
fil

Status: far from functional, currently at "it compiles" point, some
essential pieces are still missing.

Ivan Savenko 11 年之前
父节点
当前提交
43ba3d30ea

+ 1 - 0
AI/VCAI/VCAI.cpp

@@ -3,6 +3,7 @@
 #include "Goals.h"
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/CObjectHandler.h"
+#include "../../lib/CObjectWithReward.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CHeroHandler.h"
 

+ 1 - 1
client/CMusicHandler.cpp

@@ -2,9 +2,9 @@
 #include <SDL_mixer.h>
 
 #include "CMusicHandler.h"
+#include "CGameInfo.h"
 #include "../lib/CCreatureHandler.h"
 #include "../lib/CSpellHandler.h"
-#include "../client/CGameInfo.h"
 #include "../lib/JsonNode.h"
 #include "../lib/GameConstants.h"
 #include "../lib/filesystem/Filesystem.h"

+ 0 - 1
lib/CDefObjInfoHandler.cpp

@@ -3,7 +3,6 @@
 
 #include "filesystem/Filesystem.h"
 #include "filesystem/CBinaryReader.h"
-//#include "../client/CGameInfo.h"
 #include "../lib/VCMI_Lib.h"
 #include "GameConstants.h"
 #include "StringConstants.h"

+ 2 - 1
lib/CGameState.h

@@ -168,6 +168,7 @@ public:
 	ObjectInstanceID currentSelection; //id of hero/town, 0xffffffff if none
 	TeamID team;
 	TResources resources;
+	std::set<ObjectInstanceID> visitedObjects; // as a std::set, since most accesses here will be from visited status checks
 	std::vector<ConstTransitivePtr<CGHeroInstance> > heroes;
 	std::vector<ConstTransitivePtr<CGTownInstance> > towns;
 	std::vector<ConstTransitivePtr<CGHeroInstance> > availableHeroes; //heroes available in taverns
@@ -184,7 +185,7 @@ public:
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & color & human & currentSelection & team & resources & status;
-		h & heroes & towns & availableHeroes & dwellings;
+		h & heroes & towns & availableHeroes & dwellings & visitedObjects;
 		h & getBonusList(); //FIXME FIXME FIXME
 		h & status & daysWithoutCastle;
 		h & enteredLosingCheatCode & enteredWinningCheatCode;

+ 3 - 1
lib/CMakeLists.txt

@@ -16,7 +16,7 @@ set(lib_SRCS
 		registerTypes/TypesClientPacks1.cpp
 		registerTypes/TypesClientPacks2.cpp
 		registerTypes/TypesMapObjects1.cpp
-                registerTypes/TypesMapObjects2.cpp
+		registerTypes/TypesMapObjects2.cpp
 		registerTypes/TypesPregamePacks.cpp
 		registerTypes/TypesServerPacks.cpp
 
@@ -68,6 +68,8 @@ set(lib_SRCS
 		CHeroHandler.cpp
 		CModHandler.cpp
 		CObstacleInstance.cpp
+		CObjectWithReward.cpp
+		CObjectWithReward.h
 		CSpellHandler.cpp
 		CThreadHelper.cpp
 		CTownHandler.cpp

文件差异内容过多而无法显示
+ 18 - 980
lib/CObjectHandler.cpp


+ 2 - 122
lib/CObjectHandler.h

@@ -28,7 +28,6 @@ class IGameCallback;
 struct BattleResult;
 class CGObjectInstance;
 class CScript;
-class CObjectScript;
 class CGHeroInstance;
 class CTown;
 class CHero;
@@ -356,7 +355,7 @@ public:
 	//std::vector<const CArtifact*> artifacts; //hero's artifacts from bag
 	//std::map<ui16, const CArtifact*> artifWorn; //map<position,artifact_id>; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5
 	std::set<SpellID> spells; //known spells (spell IDs)
-
+	std::set<ObjectInstanceID> visitedObjects;
 
 	struct DLL_LINKAGE Patrol
 	{
@@ -423,7 +422,7 @@ public:
 		h & static_cast<CArmedInstance&>(*this);
 		h & static_cast<CArtifactSet&>(*this);
 		h & exp & level & name & biography & portrait & mana & secSkills & movement
-			& sex & inTownGarrison & spells & patrol & moveDir & skillsInfo;
+			& sex & inTownGarrison & spells & patrol & moveDir & skillsInfo & visitedObjects;
 		h & visitedTown & boat;
 		h & type & specialty & commander;
 		BONUS_TREE_DESERIALIZATION_FIX
@@ -557,34 +556,6 @@ private:
 	void heroAcceptsCreatures(const CGHeroInstance *h) const;
 };
 
-
-class DLL_LINKAGE CGVisitableOPH : public CGObjectInstance //objects visitable only once per hero
-{
-public:
-	std::set<ObjectInstanceID> visitors; //ids of heroes who have visited this obj
-	TResources treePrice; //used only by trees of knowledge: empty, 2000 gold, 10 gems
-
-	const std::string & getHoverText() const override;
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	void initObj() override;
-	bool wasVisited (const CGHeroInstance * h) const override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
-
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-		h & visitors & treePrice;
-	}
-protected:
-	void setPropertyDer(ui8 what, ui32 val) override;//synchr
-private:
-	void onNAHeroVisit(const CGHeroInstance * h, bool alreadyVisited) const;
-	///dialog callbacks
-	void treeSelected(const CGHeroInstance * h, ui32 result) const;
-	void schoolSelected(const CGHeroInstance * h, ui32 which) const;
-	void arenaSelected(const CGHeroInstance * h, int primSkill) const;
-};
 class DLL_LINKAGE CGTownBuilding : public IObjectInterface
 {
 ///basic class for town structures handled as map objects
@@ -1046,22 +1017,6 @@ public:
 	}
 };
 
-class DLL_LINKAGE CGPickable : public CGObjectInstance //campfire, treasure chest, Flotsam, Shipwreck Survivor, Sea Chest
-{
-public:
-	ui32 type, val1, val2;
-
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	void initObj() override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-		h & type & val1 & val2;
-	}
-};
-
 class DLL_LINKAGE CGShrine : public CPlayersVisited
 {
 public:
@@ -1098,24 +1053,6 @@ public:
 	ui32 defaultResProduction();
 };
 
-class DLL_LINKAGE CGVisitableOPW : public CGObjectInstance //objects visitable OPW
-{
-public:
-	ui8 visited; //true if object has been visited this week
-
-	bool wasVisited(PlayerColor player) const;
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	virtual void newTurn() const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-		h & visited;
-	}
-protected:
-	void setPropertyDer(ui8 what, ui32 val) override;
-};
-
 class DLL_LINKAGE CGTeleport : public CGObjectInstance //teleports and subterranean gates
 {
 public:
@@ -1132,43 +1069,6 @@ public:
 	}
 };
 
-class DLL_LINKAGE CGBonusingObject : public CGObjectInstance //objects giving bonuses to luck/morale/movement
-{
-public:
-	bool wasVisited (const CGHeroInstance * h) const;
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	const std::string & getHoverText() const override;
-	void initObj() override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-	}
-};
-
-class DLL_LINKAGE CGMagicSpring : public CGVisitableOPW
-{///unfortunately, this one is quite different than others
-	enum EVisitedEntrance
-	{
-		CLEAR = 0, LEFT = 1, RIGHT
-	};
-public:
-	EVisitedEntrance visitedTile; //only one entrance was visited - there are two
-
-	std::vector<int3> getVisitableOffsets() const;
-	int3 getVisitableOffset() const override;
-	void setPropertyDer(ui8 what, ui32 val) override;
-	void newTurn() const override;
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	const std::string & getHoverText() const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-		h & visitedTile & visited;
-	}
-};
-
 class DLL_LINKAGE CGMagicWell : public CGObjectInstance //objects giving bonuses to luck/morale/movement
 {
 public:
@@ -1290,26 +1190,6 @@ public:
 	}
 };
 
-class DLL_LINKAGE CGOnceVisitable : public CPlayersVisited
-///wagon, corpse, lean to, warriors tomb
-{
-public:
-	ui8 artOrRes; //0 - nothing; 1 - artifact; 2 - resource
-	ui32 bonusType, //id of res or artifact
-		bonusVal; //resource amount (or not used)
-
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	const std::string & getHoverText() const override;
-	void initObj() override;
-	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CPlayersVisited&>(*this);;
-		h & artOrRes & bonusType & bonusVal;
-	}
-};
-
 class DLL_LINKAGE CBank : public CArmedInstance
 {
 	public:

+ 994 - 0
lib/CObjectWithReward.cpp

@@ -0,0 +1,994 @@
+/*
+ * CObjectWithReward.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "CObjectWithReward.h"
+#include "CHeroHandler.h"
+#include "CGeneralTextHandler.h"
+#include "../client/CSoundBase.h"
+#include "NetPacks.h"
+
+bool CRewardLimiter::heroAllowed(const CGHeroInstance * hero) const
+{
+	if (dayOfWeek != 0)
+	{
+		if (IObjectInterface::cb->getDate(Date::DAY_OF_WEEK) != dayOfWeek)
+			return false;
+	}
+
+	for (auto & reqStack : creatures)
+	{
+		size_t count = 0;
+		for (auto slot : hero->Slots())
+		{
+			const CStackInstance * heroStack = slot.second;
+			if (heroStack->type == reqStack.type)
+				count += heroStack->count;
+		}
+		if (count < reqStack.count) //not enough creatures of this kind
+			return false;
+	}
+
+	if (!IObjectInterface::cb->getPlayer(hero->tempOwner)->resources.canAfford(resources))
+		return false;
+
+	if (hero->level < minLevel)
+		return false;
+
+	for (size_t i=0; i<primary.size(); i++)
+	{
+		if (primary[i] < hero->getPrimSkillLevel(PrimarySkill::PrimarySkill(i)))
+			return false;
+	}
+
+	for (auto & skill : secondary)
+	{
+		if (skill.second < hero->getSecSkillLevel(skill.first))
+			return false;
+	}
+
+	for (auto & art : artifacts)
+	{
+		if (!hero->hasArt(art))
+			return false;
+	}
+
+	return true;
+}
+
+std::vector<ui32> CObjectWithReward::getAvailableRewards(const CGHeroInstance * hero) const
+{
+	std::vector<ui32> ret;
+
+	for (size_t i=0; i<info.size(); i++)
+	{
+		const CVisitInfo & visit = info[i];
+
+		if (numOfGrants[i] < visit.limiter.numOfGrants && visit.limiter.heroAllowed(hero))
+		{
+			ret.push_back(i);
+		}
+	}
+	return ret;
+}
+
+void CObjectWithReward::onHeroVisit(const CGHeroInstance *h) const
+{
+	if (wasVisited(h))
+	{
+		auto rewards = getAvailableRewards(h);
+		switch (rewards.size())
+		{
+			case 0: // no rewards, e.g. empty flotsam
+			{
+				InfoWindow iw;
+				iw.player = h->tempOwner;
+				iw.soundID = soundID;
+				iw.text = onEmpty;
+				cb->showInfoDialog(&iw);
+				onRewardGiven(h);
+				break;
+			}
+			case 1: // one reward. Just give it with message
+			{
+				grantReward(info[rewards[0]], h);
+				InfoWindow iw;
+				iw.player = h->tempOwner;
+				iw.soundID = soundID;
+				iw.text = onGrant;
+				info[rewards[0]].reward.loadComponents(iw.components);
+				cb->showInfoDialog(&iw);
+				onRewardGiven(h);
+				break;
+			}
+			default: // multiple rewards. Let player select
+			{
+				BlockingDialog sd(false,true);
+				sd.player = h->tempOwner;
+				sd.soundID = soundID;
+				sd.text = onGrant;
+				for (auto index : rewards)
+					sd.components.push_back(info[index].reward.getDisplayedComponent());
+				cb->showBlockingDialog(&sd);
+				return;
+			}
+		}
+	}
+	else
+	{
+		InfoWindow iw;
+		iw.player = h->tempOwner;
+		iw.soundID = soundID;
+		iw.text = onVisited;
+		cb->showInfoDialog(&iw);
+	}
+}
+
+void CObjectWithReward::heroLevelUpDone(const CGHeroInstance *hero) const
+{
+	grantRewardAfterLevelup(info[selectedReward], hero);
+}
+
+void CObjectWithReward::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
+{
+	if (answer > 0 && answer-1 < info.size())
+	{
+		auto list = getAvailableRewards(hero);
+		grantReward(info[list[answer - 1]], hero);
+	}
+	else
+	{
+		throw std::runtime_error("Unhandled choice");
+	}
+}
+
+void CObjectWithReward::onRewardGiven(const CGHeroInstance * hero) const
+{
+	// no implementation, virtual function for overrides
+}
+
+void CObjectWithReward::grantReward(const CVisitInfo & info, const CGHeroInstance * hero) const
+{
+	assert(hero);
+	assert(hero->tempOwner.isValidPlayer());
+	assert(stacks.empty());
+	assert(info.reward.creatures.size() <= GameConstants::ARMY_SIZE);
+	assert(!cb->isVisitCoveredByAnotherQuery(this, hero));
+
+	cb->giveResources(hero->tempOwner, info.reward.resources);
+
+	for (auto & entry : info.reward.secondary)
+	{
+		int current = hero->getSecSkillLevel(entry.first);
+		if( (current != 0 && current < entry.second) ||
+			(hero->canLearnSkill() ))
+		{
+			cb->changeSecSkill(hero, entry.first, entry.second);
+		}
+	}
+
+	for(int i=0; i< info.reward.primary.size(); i++)
+		if(info.reward.primary[i] > 0)
+			cb->changePrimSkill(hero, static_cast<PrimarySkill::PrimarySkill>(i), info.reward.primary[i], false);
+
+
+	si64 expToGive = 0;
+	expToGive += VLC->heroh->reqExp(hero->level+info.reward.gainedLevels) - VLC->heroh->reqExp(hero->level);
+	expToGive += hero->calculateXp(info.reward.gainedExp);
+	if (expToGive)
+	{
+		cb->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expToGive);
+	}
+	else
+	{
+		grantRewardAfterLevelup(info, hero);
+	}
+}
+
+void CObjectWithReward::grantRewardAfterLevelup(const CVisitInfo & info, const CGHeroInstance * hero) const
+{
+	if (info.reward.manaDiff || info.reward.manaPercentage >= 0)
+	{
+		si32 mana = hero->mana;
+		if (info.reward.manaPercentage >= 0)
+			mana = hero->manaLimit() * info.reward.manaPercentage / 100;
+
+		cb->setManaPoints(hero->id, mana + info.reward.manaDiff);
+	}
+
+	if(info.reward.movePoints || info.reward.movePercentage >= 0)
+	{
+		SetMovePoints smp;
+		smp.hid = hero->id;
+		smp.val = hero->movement;
+
+		if (info.reward.movePercentage >= 0) // percent from max
+			smp.val = hero->maxMovePoints(hero->boat != nullptr) * info.reward.movePercentage / 100;
+		smp.val = std::max<si32>(0, smp.val + info.reward.movePoints);
+
+		cb->setMovePoints(&smp);
+	}
+
+	for (const Bonus & bonus : info.reward.bonuses)
+	{
+		GiveBonus gb;
+		gb.bonus = bonus;
+		gb.id = hero->id.getNum();
+		cb->giveHeroBonus(&gb);
+	}
+
+	for (ArtifactID art : info.reward.artifacts)
+		cb->giveHeroNewArtifact(hero, VLC->arth->artifacts[art],ArtifactPosition::FIRST_AVAILABLE);
+
+	if (!info.reward.spells.empty())
+	{
+		std::set<SpellID> spellsToGive(info.reward.spells.begin(), info.reward.spells.end());
+		cb->changeSpells(hero, true, spellsToGive);
+	}
+
+	if (!info.reward.creatures.empty())
+	{
+		CCreatureSet creatures;
+		for (auto & crea : info.reward.creatures)
+			creatures.addToSlot(creatures.getFreeSlot(), new CStackInstance(crea.type, crea.count));
+
+		cb->giveCreatures(this, hero, creatures, false);
+	}
+
+	onRewardGiven(hero);
+}
+
+bool CObjectWithReward::wasVisited (PlayerColor player) const
+{
+	switch (visitMode)
+	{
+		case VISIT_UNLIMITED:
+			return false;
+		case VISIT_ONCE:
+			return numOfGrants.empty() || *boost::range::max_element(numOfGrants) == 0;
+		case VISIT_HERO:
+			return false;
+		case VISIT_PLAYER:
+			return vstd::contains(cb->getPlayer(player)->visitedObjects, ObjectInstanceID(ID));
+		default:
+			return false;
+	}
+}
+
+bool CObjectWithReward::wasVisited (const CGHeroInstance * h) const
+{
+	switch (visitMode)
+	{
+		case VISIT_HERO:
+			return vstd::contains(h->visitedObjects, ObjectInstanceID(ID));
+		default:
+			return wasVisited(h->tempOwner);
+	}
+}
+
+void CRewardInfo::loadComponents(std::vector<Component> & comps) const
+{
+	for (size_t i=0; i<resources.size(); i++)
+	{
+		if (resources[i] !=0)
+			comps.push_back(Component(Component::RESOURCE, i, resources[i], 0));
+	}
+
+	if (gainedExp)    comps.push_back(Component(Component::EXPERIENCE, 0, gainedExp, 0));
+	if (gainedLevels) comps.push_back(Component(Component::EXPERIENCE, 0, gainedLevels, 0));
+
+	if (manaDiff)   comps.push_back(Component(Component::PRIM_SKILL, 5, manaDiff,   0));
+
+	for (size_t i=0; i<primary.size(); i++)
+	{
+		if (primary[i] !=0)
+			comps.push_back(Component(Component::PRIM_SKILL, i, primary[i], 0));
+	}
+
+	for (auto & entry : secondary)
+		comps.push_back(Component(Component::SEC_SKILL, entry.first, entry.second, 0));
+
+	for (auto & entry : artifacts)
+		comps.push_back(Component(Component::ARTIFACT, entry, 1, 0));
+
+	for (auto & entry : spells)
+		comps.push_back(Component(Component::SPELL, entry, 1, 0));
+
+	for (auto & entry : creatures)
+		comps.push_back(Component(Component::CREATURE, entry.type->idNumber, entry.count, 0));
+}
+
+Component CRewardInfo::getDisplayedComponent() const
+{
+	std::vector<Component> comps;
+	loadComponents(comps);
+	assert(!comps.empty());
+	return comps.front();
+}
+
+// FIXME: copy-pasted from CObjectHandler
+static std::string & visitedTxt(const bool visited)
+{
+	int id = visited ? 352 : 353;
+	return VLC->generaltexth->allTexts[id];
+}
+
+const std::string & CObjectWithReward::getHoverText() const
+{
+	const CGHeroInstance *h = cb->getSelectedHero(cb->getCurrentPlayer());
+	hoverName = VLC->generaltexth->names[ID];
+	if(h && wasVisited(h))
+	{
+		bool visited = h->hasBonusFrom(Bonus::OBJECT,ID);
+		hoverName += " " + visitedTxt(visited);
+	}
+	return hoverName;
+}
+
+void CObjectWithReward::setPropertyDer(ui8 what, ui32 val)
+{
+	switch (what)
+	{
+		case ObjProperty::REWARD_RESET:
+			numOfGrants.clear();
+			numOfGrants.resize(info.size(), 0);
+			break;
+		case ObjProperty::REWARD_SELECT:
+			selectedReward = val;
+			break;
+		case ObjProperty::REWARD_ADD_VISITOR:
+			//cb->getHero(ObjectInstanceID(val))->visitedObjects.insert(ObjectInstanceID(ID));
+			break;
+	}
+}
+
+void CObjectWithReward::newTurn() const
+{
+	if (cb->getDate(Date::DAY) % resetDuration == 0)
+		cb->setObjProperty(id, ObjProperty::REWARD_RESET, 0);
+}
+
+CObjectWithReward::CObjectWithReward():
+	soundID(soundBase::invalid),
+	selectMode(0),
+	selectedReward(0),
+	resetDuration(0)
+{}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+///               END OF CODE FOR COBJECTWITHREWARD AND RELATED CLASSES                         ///
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+/// Helper, selects random art class based on weights
+static int selectRandomArtClass(int treasure, int minor, int major, int relic)
+{
+	int total = treasure + minor + major + relic;
+	assert(total != 0);
+	int hlp = IObjectInterface::cb->gameState()->getRandomGenerator().nextInt(total - 1);
+
+	if(hlp < treasure)
+		return CArtifact::ART_TREASURE;
+	if(hlp < treasure + minor)
+		return CArtifact::ART_MINOR;
+	if(hlp < treasure + minor + major)
+		return CArtifact::ART_MAJOR;
+	return CArtifact::ART_RELIC;
+}
+
+/// Helper, adds random artifact to reward selecting class based on weights
+static void loadRandomArtifact(CVisitInfo & info, int treasure, int minor, int major, int relic)
+{
+	int artClass = selectRandomArtClass(treasure, minor, major, relic);
+	ArtifactID artID = VLC->arth->pickRandomArtifact(IObjectInterface::cb->gameState()->getRandomGenerator(), artClass);
+	info.reward.artifacts.push_back(artID);
+}
+
+CGPickable::CGPickable()
+{
+	visitMode = VISIT_ONCE;
+	selectMode = SELECT_PLAYER;
+}
+
+void CGPickable::initObj()
+{
+	blockVisit = true;
+	switch(ID)
+	{
+	case Obj::CAMPFIRE:
+		{
+			soundID = soundBase::experience;
+			onGrant.addTxt(MetaString::ADVOB_TXT,23);
+			int givenRes = cb->gameState()->getRandomGenerator().nextInt(5);
+			int givenAmm = cb->gameState()->getRandomGenerator().nextInt(4, 6);
+
+			info.resize(1);
+			info[0].reward.resources[givenRes] = givenAmm;
+			info[0].reward.resources[Res::GOLD]= givenAmm * 100;
+			break;
+		}
+	case Obj::FLOTSAM:
+		{
+			int type = cb->gameState()->getRandomGenerator().nextInt(3);
+			soundID = soundBase::GENIE;
+			if (type == 0)
+				onEmpty.addTxt(MetaString::ADVOB_TXT, 51+type);
+			else
+				onGrant.addTxt(MetaString::ADVOB_TXT, 51+type);
+			switch(type)
+			{
+			//case 0:
+			case 1:
+				{
+					info.resize(1);
+					info[0].reward.resources[Res::WOOD] = 5;
+					break;
+				}
+			case 2:
+				{
+					info.resize(1);
+					info[0].reward.resources[Res::WOOD] = 5;
+					info[0].reward.resources[Res::GOLD] = 200;
+					break;
+				}
+			case 3:
+				{
+					info.resize(1);
+					info[0].reward.resources[Res::WOOD] = 10;
+					info[0].reward.resources[Res::GOLD] = 500;
+					break;
+				}
+			}
+			break;
+		}
+	case Obj::SEA_CHEST:
+		{
+			soundID = soundBase::chest;
+			int hlp = cb->gameState()->getRandomGenerator().nextInt(99);
+			if(hlp < 20)
+			{
+				onGrant.addTxt(MetaString::ADVOB_TXT, 116);
+			}
+			else if(hlp < 90)
+			{
+				info.resize(1);
+				info[0].reward.resources[Res::GOLD] = 1500;
+				onGrant.addTxt(MetaString::ADVOB_TXT, 118);
+			}
+			else
+			{
+				info.resize(1);
+				loadRandomArtifact(info[0], 100, 0, 0, 0);
+				info[0].reward.resources[Res::GOLD] = 1000;
+				onGrant.addTxt(MetaString::ADVOB_TXT, 117);
+				onGrant.addReplacement(MetaString::ART_NAMES, info[0].reward.artifacts.back());
+			}
+		}
+		break;
+	case Obj::SHIPWRECK_SURVIVOR:
+		{
+			soundID = soundBase::experience;
+			info.resize(1);
+			loadRandomArtifact(info[0], 55, 20, 20, 5);
+			onGrant.addTxt(MetaString::ADVOB_TXT, 125);
+			onGrant.addReplacement(MetaString::ART_NAMES, info[0].reward.artifacts.back());
+		}
+		break;
+	case Obj::TREASURE_CHEST:
+		{
+			int hlp = cb->gameState()->getRandomGenerator().nextInt(99);
+			if(hlp >= 95)
+			{
+				soundID = soundBase::treasure;
+				info.resize(1);
+				loadRandomArtifact(info[0], 100, 0, 0, 0);
+				onGrant.addTxt(MetaString::ADVOB_TXT,145);
+				onGrant.addReplacement(MetaString::ART_NAMES, info[0].reward.artifacts.back());
+				return;
+			}
+			else if (hlp >= 65)
+			{
+				soundID = soundBase::chest;
+				onGrant.addTxt(MetaString::ADVOB_TXT,146);
+				info.resize(2);
+				info[0].reward.resources[Res::GOLD] = 2000;
+				info[1].reward.gainedExp = 1500;
+			}
+			else if(hlp >= 33)
+			{
+				soundID = soundBase::chest;
+				onGrant.addTxt(MetaString::ADVOB_TXT,146);
+				info.resize(2);
+				info[0].reward.resources[Res::GOLD] = 1500;
+				info[1].reward.gainedExp = 1000;
+			}
+			else
+			{
+				soundID = soundBase::chest;
+				onGrant.addTxt(MetaString::ADVOB_TXT,146);
+				info.resize(2);
+				info[0].reward.resources[Res::GOLD] = 1000;
+				info[1].reward.gainedExp = 500;
+			}
+		}
+		break;
+	}
+}
+
+void CGPickable::onRewardGiven(const CGHeroInstance * hero) const
+{
+	cb->removeObject(this);
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+CGBonusingObject::CGBonusingObject()
+{
+	visitMode = VISIT_UNLIMITED;
+	selectMode = SELECT_FIRST;
+}
+
+void CGBonusingObject::initObj()
+{
+	auto configureBonusDuration = [&](CVisitInfo & visit, Bonus::BonusDuration duration, Bonus::BonusType type, si32 value, si32 descrID)
+	{
+		Bonus b(duration, type, Bonus::OBJECT, value, ID, descrID != 0 ? VLC->generaltexth->advobtxt[descrID] : "");
+		visit.reward.bonuses.push_back(b);
+	};
+
+	auto configureBonus = [&](CVisitInfo & visit, Bonus::BonusType type, si32 value, si32 descrID)
+	{
+		configureBonusDuration(visit, Bonus::ONE_BATTLE, type, value, descrID);
+	};
+
+	auto configureMessage = [&](int onGrantID, int onVisitedID, soundBase::soundID sound)
+	{
+		onGrant.addTxt(MetaString::ADVOB_TXT, onGrantID);
+		onVisited.addTxt(MetaString::ADVOB_TXT, onVisitedID);
+		soundID = sound;
+	};
+
+	if(ID == Obj::BUOY || ID == Obj::MERMAID)
+		blockVisit = true;
+
+	info.resize(1);
+	CVisitInfo & visit = info[0];
+
+	switch(ID)
+	{
+	case Obj::BUOY:
+		configureMessage(21, 22, soundBase::MORALE);
+		configureBonus(visit, Bonus::MORALE, +1, 94);
+		break;
+	case Obj::SWAN_POND:
+		configureMessage(29, 30, soundBase::LUCK);
+		configureBonus(visit, Bonus::LUCK, 2, 67);
+		visit.reward.movePercentage = 0;
+		break;
+	case Obj::FAERIE_RING:
+		configureMessage(49, 50, soundBase::LUCK);
+		configureBonus(visit, Bonus::LUCK, 2, 71);
+		break;
+	case Obj::FOUNTAIN_OF_FORTUNE:
+		selectMode = SELECT_RANDOM;
+		configureMessage(55, 56, soundBase::LUCK);
+		info.resize(5);
+		for (int i=0; i<5; i++)
+			configureBonus(info[i], Bonus::LUCK, i-1, 69); //NOTE: description have %d that should be replaced with value
+		break;
+	case Obj::IDOL_OF_FORTUNE:
+
+		configureMessage(62, 63, soundBase::experience);
+		info.resize(7);
+		for (int i=0; i<6; i++)
+		{
+			info[i].limiter.dayOfWeek = i+1;
+			configureBonus(info[i], i%2 ? Bonus::MORALE : Bonus::LUCK, 1, 68);
+		}
+		info.back().limiter.dayOfWeek = 7;
+		configureBonus(info.back(), Bonus::MORALE, 1, 68); // on last day of week
+		configureBonus(info.back(), Bonus::LUCK,   1, 68);
+
+		break;
+	case Obj::MERMAID:
+		configureMessage(83, 82, soundBase::LUCK);
+		configureBonus(visit, Bonus::LUCK, 1, 72);
+		break;
+	case Obj::RALLY_FLAG:
+		configureMessage(111, 110, soundBase::MORALE);
+		configureBonus(visit, Bonus::MORALE, 1, 102);
+		configureBonus(visit, Bonus::LUCK,   1, 102);
+		visit.reward.movePoints = 400;
+		break;
+	case Obj::OASIS:
+		configureMessage(95, 94, soundBase::MORALE);
+		onGrant.addTxt(MetaString::ADVOB_TXT, 95);
+		configureBonus(visit, Bonus::MORALE, 1, 95);
+		visit.reward.movePoints = 800;
+		break;
+	case Obj::TEMPLE:
+		configureMessage(140, 141, soundBase::temple);
+		info[0].limiter.dayOfWeek = 7;
+		info.resize(2);
+		configureBonus(info[0], Bonus::MORALE, 2, 96);
+		configureBonus(info[1], Bonus::MORALE, 1, 97);
+		break;
+	case Obj::WATERING_HOLE:
+		configureMessage(166, 167, soundBase::MORALE);
+		configureBonus(visit, Bonus::MORALE, 1, 100);
+		visit.reward.movePoints = 400;
+		break;
+	case Obj::FOUNTAIN_OF_YOUTH:
+		configureMessage(57, 58, soundBase::MORALE);
+		configureBonus(visit, Bonus::MORALE, 1, 103);
+		visit.reward.movePoints = 400;
+		break;
+	case Obj::STABLES:
+		configureMessage(137, 136, soundBase::STORE);
+
+		configureBonusDuration(visit, Bonus::ONE_WEEK, Bonus::LAND_MOVEMENT, 600, 0);
+		visit.reward.movePoints = 600;
+		//TODO: upgrade champions to cavaliers
+/*
+		bool someUpgradeDone = false;
+
+		for (auto i = h->Slots().begin(); i != h->Slots().end(); ++i)
+		{
+			if(i->second->type->idNumber == CreatureID::CAVALIER)
+			{
+				cb->changeStackType(StackLocation(h, i->first), VLC->creh->creatures[CreatureID::CHAMPION]);
+				someUpgradeDone = true;
+			}
+		}
+		if (someUpgradeDone)
+		{
+			grantMessage.addTxt(MetaString::ADVOB_TXT, 138);
+			iw.components.push_back(Component(Component::CREATURE,11,0,1));
+		}*/
+		break;
+	}
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+CGOnceVisitable::CGOnceVisitable()
+{
+	visitMode = VISIT_ONCE;
+	selectMode = SELECT_FIRST;
+}
+
+void CGOnceVisitable::initObj()
+{
+	switch(ID)
+	{
+	case Obj::CORPSE:
+		{
+			onGrant.addTxt(MetaString::ADVOB_TXT, 37);
+			onEmpty.addTxt(MetaString::ADVOB_TXT, 38);
+			soundID = soundBase::MYSTERY;
+			blockVisit = true;
+			if(cb->gameState()->getRandomGenerator().nextInt(99) < 20)
+			{
+				info.resize(1);
+				loadRandomArtifact(info[0], 10, 10, 10, 0);
+			}
+		}
+		break;
+
+	case Obj::LEAN_TO:
+		{
+			soundID = soundBase::GENIE;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 64);
+			onEmpty.addTxt(MetaString::ADVOB_TXT, 65);
+			info.resize(1);
+			int type =  cb->gameState()->getRandomGenerator().nextInt(5); //any basic resource without gold
+			int value = cb->gameState()->getRandomGenerator().nextInt(1, 4);
+			info[0].reward.resources[type] = value;
+		}
+		break;
+
+	case Obj::WARRIORS_TOMB:
+		{
+			// TODO: line 161 - ask if player wants to search the Tomb
+			soundID = soundBase::GRAVEYARD;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 162);
+			onVisited.addTxt(MetaString::ADVOB_TXT, 163);
+
+			info.resize(2);
+			loadRandomArtifact(info[0], 30, 50, 25, 5);
+
+			Bonus bonus(Bonus::ONE_BATTLE, Bonus::MORALE, Bonus::OBJECT, -3, ID);
+			info[0].reward.bonuses.push_back(bonus);
+			info[1].reward.bonuses.push_back(bonus);
+		}
+		break;
+	case Obj::WAGON:
+		{
+			soundID = soundBase::GENIE;
+			onVisited.addTxt(MetaString::ADVOB_TXT, 156);
+
+			int hlp = cb->gameState()->getRandomGenerator().nextInt(99);
+
+			if(hlp < 40) //minor or treasure art
+			{
+				onGrant.addTxt(MetaString::ADVOB_TXT, 155);
+				info.resize(1);
+				loadRandomArtifact(info[0], 10, 10, 0, 0);
+			}
+			else if(hlp < 90) //2 - 5 of non-gold resource
+			{
+				onGrant.addTxt(MetaString::ADVOB_TXT, 154);
+				info.resize(1);
+				int type  = cb->gameState()->getRandomGenerator().nextInt(5);
+				int value = cb->gameState()->getRandomGenerator().nextInt(2, 5);
+				info[0].reward.resources[type] = value;
+			}
+			// or nothing
+		}
+		break;
+	}
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+CGVisitableOPH::CGVisitableOPH()
+{
+	visitMode = VISIT_HERO;
+	selectMode = SELECT_PLAYER;
+}
+
+void CGVisitableOPH::initObj()
+{
+	switch(ID)
+	{
+		case Obj::ARENA:
+			soundID = soundBase::NOMAD;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 0);
+			info.resize(2);
+			info[0].reward.primary[PrimarySkill::ATTACK] = 2;
+			info[1].reward.primary[PrimarySkill::DEFENSE] = 2;
+			break;
+		case Obj::MERCENARY_CAMP:
+			info.resize(1);
+			info[0].reward.primary[PrimarySkill::ATTACK] = 1;
+			soundID = soundBase::NOMAD;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 80);
+			break;
+		case Obj::MARLETTO_TOWER:
+			info.resize(1);
+			info[0].reward.primary[PrimarySkill::DEFENSE] = 1;
+			soundID = soundBase::NOMAD;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 39);
+			break;
+		case Obj::STAR_AXIS:
+			info.resize(1);
+			info[0].reward.primary[PrimarySkill::SPELL_POWER] = 1;
+			soundID = soundBase::gazebo;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 100);
+			break;
+		case Obj::GARDEN_OF_REVELATION:
+			info.resize(1);
+			info[0].reward.primary[PrimarySkill::KNOWLEDGE] = 1;
+			soundID = soundBase::GETPROTECTION;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 59);
+			break;
+		case Obj::LEARNING_STONE:
+			info.resize(1);
+			info[0].reward.gainedExp = 1000;
+			soundID = soundBase::gazebo;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 143);
+			break;
+		case Obj::TREE_OF_KNOWLEDGE:
+			soundID = soundBase::gazebo;
+			info.resize(1);
+			info[0].reward.gainedLevels = 1;
+
+			info.resize(1);
+			switch (cb->gameState()->getRandomGenerator().nextInt(2))
+			{
+			case 0: // free
+				break;
+			case 1:
+				info[0].limiter.resources[Res::GOLD] = 2000;
+				info[0].reward.resources[Res::GOLD] = -2000;
+				break;
+			case 2:
+				info[0].limiter.resources[Res::GEMS] = 10;
+				info[0].reward.resources[Res::GEMS] = -10;
+				break;
+			}
+			break;
+		case Obj::LIBRARY_OF_ENLIGHTENMENT:
+		{
+			onGrant.addTxt(MetaString::ADVOB_TXT, 66);
+			onVisited.addTxt(MetaString::ADVOB_TXT, 67);
+			onEmpty.addTxt(MetaString::ADVOB_TXT, 68);
+
+			// Don't like this one but don't see any easier approach
+			CVisitInfo visit;
+			visit.reward.primary[PrimarySkill::ATTACK] = 2;
+			visit.reward.primary[PrimarySkill::DEFENSE] = 2;
+			visit.reward.primary[PrimarySkill::KNOWLEDGE] = 2;
+			visit.reward.primary[PrimarySkill::SPELL_POWER] = 2;
+
+			static_assert(SecSkillLevel::LEVELS_SIZE == 4, "Behavior of Library of Enlignment may not be correct");
+			for (int i=0; i<SecSkillLevel::LEVELS_SIZE; i++)
+			{
+				visit.limiter.minLevel = 10 - i * 2;
+				visit.limiter.secondary[SecondarySkill::DIPLOMACY] = i;
+				info.push_back(visit);
+			}
+			soundID = soundBase::gazebo;
+			break;
+		}
+		case Obj::SCHOOL_OF_MAGIC:
+			info.resize(2);
+			info[0].reward.primary[PrimarySkill::SPELL_POWER] = 1;
+			info[1].reward.primary[PrimarySkill::KNOWLEDGE] = 1;
+			soundID = soundBase::faerie;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 71);
+			break;
+		case Obj::SCHOOL_OF_WAR:
+			info.resize(2);
+			info[0].reward.primary[PrimarySkill::ATTACK] = 1;
+			info[1].reward.primary[PrimarySkill::DEFENSE] = 1;
+			soundID = soundBase::MILITARY;
+			onGrant.addTxt(MetaString::ADVOB_TXT, 158);
+			break;
+	}
+}
+/*
+const std::string & CGVisitableOPH::getHoverText() const
+{
+	int pom = -1;
+	switch(ID)
+	{
+	case Obj::ARENA:
+		pom = -1;
+		break;
+	case Obj::MERCENARY_CAMP:
+		pom = 8;
+		break;
+	case Obj::MARLETTO_TOWER:
+		pom = 7;
+		break;
+	case Obj::STAR_AXIS:
+		pom = 11;
+		break;
+	case Obj::GARDEN_OF_REVELATION:
+		pom = 4;
+		break;
+	case Obj::LEARNING_STONE:
+		pom = 5;
+		break;
+	case Obj::TREE_OF_KNOWLEDGE:
+		pom = 18;
+		break;
+	case Obj::LIBRARY_OF_ENLIGHTENMENT:
+		break;
+	case Obj::SCHOOL_OF_MAGIC:
+		pom = 9;
+		break;
+	case Obj::SCHOOL_OF_WAR:
+		pom = 10;
+		break;
+	default:
+		throw std::runtime_error("Wrong CGVisitableOPH object ID!\n");
+	}
+	hoverName = VLC->generaltexth->names[ID];
+	if(pom >= 0)
+		hoverName += ("\n" + VLC->generaltexth->xtrainfo[pom]);
+	const CGHeroInstance *h = cb->getSelectedHero (cb->getCurrentPlayer());
+	if(h)
+	{
+		hoverName += "\n\n";
+		bool visited = vstd::contains (visitors, h->id);
+		hoverName += visitedTxt (visited);
+	}
+	return hoverName;
+}
+*/
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+CGVisitableOPW::CGVisitableOPW()
+{
+	visitMode = VISIT_ONCE;
+	selectMode = SELECT_RANDOM;
+	resetDuration = 7;
+}
+
+void CGVisitableOPW::initObj()
+{
+	switch (ID)
+	{
+	case Obj::MYSTICAL_GARDEN:
+		soundID = soundBase::experience;
+		onGrant.addTxt(MetaString::ADVOB_TXT, 92);
+		onEmpty.addTxt(MetaString::ADVOB_TXT, 93);
+		info.resize(2);
+		info[0].reward.resources[Res::GEMS] = 5;
+		info[1].reward.resources[Res::GOLD] = 500;
+		break;
+	case Obj::WINDMILL:
+		soundID = soundBase::GENIE;
+		onGrant.addTxt(MetaString::ADVOB_TXT, 170);
+		onEmpty.addTxt(MetaString::ADVOB_TXT, 169);
+		// 3-6 of any resource but wood and gold
+		// this is UGLY. TODO: find better way to describe this
+		for (int resID = Res::MERCURY; resID < Res::GOLD; resID++)
+		{
+			for (int val = 3; val <=6; val++)
+			{
+				CVisitInfo visit;
+				visit.reward.resources[resID] = val;
+				info.push_back(visit);
+			}
+		}
+		break;
+	case Obj::WATER_WHEEL:
+		soundID = soundBase::GENIE;
+		onGrant.addTxt(MetaString::ADVOB_TXT, 164);
+		onEmpty.addTxt(MetaString::ADVOB_TXT, 165);
+
+		info.resize(2);
+		info[0].limiter.dayOfWeek = 7; // double amount on sunday
+		info[0].reward.resources[Res::GOLD] = 1000;
+		info[1].reward.resources[Res::GOLD] = 500;
+		break;
+	}
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+std::vector<int3> CGMagicSpring::getVisitableOffsets() const
+{
+	std::vector <int3> visitableTiles;
+
+	for(int y = 0; y < 6; y++)
+		for (int x = 0; x < 8; x++) //starting from left
+			if (appearance.isVisitableAt(x, y))
+				visitableTiles.push_back (int3(x, y , 0));
+
+	return visitableTiles;
+}
+
+int3 CGMagicSpring::getVisitableOffset() const
+{
+	auto visitableTiles = getVisitableOffsets();
+
+	if (visitableTiles.size() != info.size())
+	{
+		logGlobal->warnStream() << "Unexpected number of visitable tiles of Magic Spring at " << pos << "!";
+		return int3(-1,-1,-1);
+	}
+
+	for (size_t i=0; i<visitableTiles.size(); i++)
+	{
+		if (numOfGrants[i] == 0)
+			return visitableTiles[i];
+	}
+	return visitableTiles[0]; // return *something*. This is valid visitable tile but already used
+}
+
+std::vector<ui32> CGMagicSpring::getAvailableRewards(const CGHeroInstance * hero) const
+{
+	auto tiles = getVisitableOffsets();
+	for (size_t i=0; i<tiles.size(); i++)
+	{
+		if (pos - tiles[i] == hero->getPosition() && numOfGrants[i] == 0)
+		{
+			return std::vector<ui32>(1, i);
+		}
+	}
+	// hero is either not on visitable tile (should not happen) or tile is already used
+	return std::vector<ui32>();
+}

+ 320 - 0
lib/CObjectWithReward.h

@@ -0,0 +1,320 @@
+#pragma once
+
+#include "CObjectHandler.h"
+#include "NetPacksBase.h"
+
+/*
+ * CObjectWithReward.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+/// Limiters of rewards. Rewards will be granted to hero only if he satisfies requirements
+/// Note: for this is only a test - it won't remove anything from hero (e.g. artifacts or creatures)
+/// NOTE: in future should (partially) replace seer hut/quest guard quests checks
+class DLL_LINKAGE CRewardLimiter
+{
+public:
+	/// how many times this reward can be granted, 0 for unlimited
+	si32 numOfGrants;
+
+	/// day of week, unused if 0, 1-7 will test for current day of week
+	si32 dayOfWeek;
+
+	/// level that hero needs to have
+	si32 minLevel;
+
+	/// resources player needs to have in order to trigger reward
+	TResources resources;
+
+	/// skills hero needs to have
+	std::vector<si32> primary;
+	std::map<SecondarySkill, si32> secondary;
+
+	/// artifacts that hero needs to have (equipped or in backpack) to trigger this
+	/// Note: does not checks for multiple copies of the same arts
+	std::vector<ArtifactID> artifacts;
+
+	/// creatures that hero needs to have
+	std::vector<CStackBasicDescriptor> creatures;
+
+	CRewardLimiter():
+		numOfGrants(1),
+		dayOfWeek(0),
+		minLevel(0)
+	{}
+
+	bool heroAllowed(const CGHeroInstance * hero) const;
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & numOfGrants & dayOfWeek & minLevel & resources;
+		h & primary & secondary & artifacts & creatures;
+	}
+};
+
+/// Reward that can be granted to a hero
+/// NOTE: eventually should replace seer hut rewards and events/pandoras
+class DLL_LINKAGE CRewardInfo
+{
+public:
+	/// resources that will be given to player
+	TResources resources;
+
+	/// received experience
+	ui32 gainedExp;
+	/// received levels (converted into XP during grant)
+	ui32 gainedLevels;
+
+	/// mana given to/taken from hero, fixed value
+	si32 manaDiff;
+	/// fixed value, in form of percentage from max
+	si32 manaPercentage;
+
+	/// movement points, only for current day. Bonuses should be used to grant MP on any other day
+	si32 movePoints;
+	/// fixed value, in form of percentage from max
+	si32 movePercentage;
+
+	/// list of bonuses, e.g. morale/luck
+	std::vector<Bonus> bonuses;
+
+	/// skills that hero may receive or lose
+	std::vector<si32> primary;
+	std::map<SecondarySkill, si32> secondary;
+
+	/// objects that hero may receive
+	std::vector<ArtifactID> artifacts;
+	std::vector<SpellID> spells;
+	std::vector<CStackBasicDescriptor> creatures;
+
+	/// Generates list of components that describes reward
+	virtual void loadComponents(std::vector<Component> & comps) const;
+	Component getDisplayedComponent() const;
+
+	CRewardInfo() :
+		gainedExp(0),
+		gainedLevels(0),
+		manaDiff(0),
+		manaPercentage(-1),
+		movePoints(0),
+		movePercentage(-1)
+	{}
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & resources;
+		h & gainedExp & gainedLevels & manaDiff & movePoints;
+		h & primary & secondary & bonuses;
+		h & artifacts & spells & creatures;
+	}
+};
+
+class CVisitInfo
+{
+public:
+	CRewardLimiter limiter;
+	CRewardInfo reward;
+
+	MetaString message;
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & limiter & reward & message;
+	}
+};
+
+/// Base class that can handle granting rewards to visiting heroes.
+/// Inherits from CArmedInstance for proper trasfer of armies
+class DLL_LINKAGE CObjectWithReward : public CArmedInstance
+{
+	/// function that must be called if hero got level-up during grantReward call
+	void grantRewardAfterLevelup(const CVisitInfo & reward, const CGHeroInstance * hero) const;
+
+protected:
+	/// controls selection of reward granted to player
+	enum ESelectMode
+	{
+		SELECT_FIRST,  // first reward that matches limiters
+		SELECT_PLAYER, // player can select from all allowed rewards
+		SELECT_RANDOM  // reward will be selected from allowed randomly
+	};
+
+	enum EVisitMode
+	{
+		VISIT_UNLIMITED, // any number of times
+		VISIT_ONCE,      // only once, first to visit get all the rewards
+		VISIT_HERO,      // every hero can visit object once
+		VISIT_PLAYER     // every player can visit object once
+	};
+
+	/// filters list of visit info and returns rewards that can be granted to current hero
+	virtual std::vector<ui32> getAvailableRewards(const CGHeroInstance * hero) const;
+
+	/// grants reward to hero
+	void grantReward(const CVisitInfo & reward, const CGHeroInstance * hero) const;
+
+	/// Rewars that can be granted by an object
+	std::vector<CVisitInfo> info;
+
+	/// How many times these rewards have been granted since last reset
+	std::vector<ui32> numOfGrants;
+
+	/// MetaString's that contain text for messages for specific situations
+	MetaString onGrant;
+	MetaString onVisited;
+	MetaString onEmpty;
+
+	/// sound that will be played alongside with *any* message
+	ui16 soundID;
+	/// how reward will be selected, uses ESelectMode enum
+	ui8 selectMode;
+	/// contols who can visit an object, uses EVisitMode enum
+	ui8 visitMode;
+	/// reward selected by player
+	ui16 selectedReward;
+
+	/// object visitability info will be reset each resetDuration days
+	ui16 resetDuration;
+
+public:
+	void setPropertyDer(ui8 what, ui32 val) override;
+	const std::string & getHoverText() const override;
+
+	/// 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;
+
+	/// gives reward to player or ask for choice in case of multiple rewards
+	void onHeroVisit(const CGHeroInstance *h) const override;
+
+	///possibly resets object state
+	void newTurn() const override;
+
+	/// gives second part of reward after hero level-ups for proper granting of spells/mana
+	void heroLevelUpDone(const CGHeroInstance *hero) const override;
+
+	/// applies player selection of reward
+	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+
+	/// function that will be called once reward is fully granted to hero
+	virtual void onRewardGiven(const CGHeroInstance * hero) const;
+
+	CObjectWithReward();
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & static_cast<CArmedInstance&>(*this);
+		h & info & numOfGrants;
+		h & onGrant & onVisited & onEmpty;
+		h & soundID & selectMode & selectedReward;
+	}
+};
+
+class DLL_LINKAGE CGPickable : public CObjectWithReward //campfire, treasure chest, Flotsam, Shipwreck Survivor, Sea Chest
+{
+public:
+	void initObj() override;
+	void onRewardGiven(const CGHeroInstance *hero) const;
+
+	CGPickable();
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & static_cast<CObjectWithReward&>(*this);
+	}
+};
+
+class DLL_LINKAGE CGBonusingObject : public CObjectWithReward //objects giving bonuses to luck/morale/movement
+{
+public:
+	void initObj() override;
+
+	CGBonusingObject();
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & static_cast<CGObjectInstance&>(*this);
+	}
+};
+
+class DLL_LINKAGE CGOnceVisitable : public CObjectWithReward // wagon, corpse, lean to, warriors tomb
+{
+public:
+	void initObj() override;
+
+	CGOnceVisitable();
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & static_cast<CObjectWithReward&>(*this);
+	}
+};
+
+class DLL_LINKAGE CGVisitableOPH : public CObjectWithReward //objects visitable only once per hero
+{
+public:
+	void initObj() override;
+
+	CGVisitableOPH();
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & static_cast<CObjectWithReward&>(*this);
+	}
+};
+
+class DLL_LINKAGE CGVisitableOPW : public CObjectWithReward //objects visitable once per week
+{
+public:
+	void initObj() override;
+
+	CGVisitableOPW();
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & static_cast<CObjectWithReward&>(*this);
+	}
+};
+
+///Special case - magic spring that has two separate visitable entrances
+class DLL_LINKAGE CGMagicSpring : public CGVisitableOPW
+{
+protected:
+	std::vector<ui32> getAvailableRewards(const CGHeroInstance * hero) const override;
+
+public:
+	std::vector<int3> getVisitableOffsets() const;
+	int3 getVisitableOffset() const override;
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & static_cast<CGVisitableOPW&>(*this);
+	}
+};
+
+//TODO:
+
+// MAX
+// class DLL_LINKAGE CGPandoraBox : public CArmedInstance
+// class DLL_LINKAGE CGEvent : public CGPandoraBox  //event objects
+// class DLL_LINKAGE CGSeerHut : public CArmedInstance, public IQuestObject //army is used when giving reward
+// class DLL_LINKAGE CGQuestGuard : public CGSeerHut
+// class DLL_LINKAGE CBank : public CArmedInstance
+// class DLL_LINKAGE CGPyramid : public CBank
+
+// EXTRA
+// class DLL_LINKAGE COPWBonus : public CGTownBuilding
+// class DLL_LINKAGE CTownBonus : public CGTownBuilding
+// class DLL_LINKAGE CGKeys : public CGObjectInstance //Base class for Keymaster and guards
+// class DLL_LINKAGE CGKeymasterTent : public CGKeys
+// class DLL_LINKAGE CGBorderGuard : public CGKeys, public IQuestObject
+
+// POSSIBLE
+// class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles
+// class DLL_LINKAGE CGWitchHut : public CPlayersVisited
+// class DLL_LINKAGE CGScholar : public CGObjectInstance

+ 4 - 1
lib/NetPacks.h

@@ -1026,7 +1026,10 @@ namespace ObjProperty
 		BANK_CLEAR_CONFIG, BANK_INIT_ARMY, BANK_RESET,
 
 		//magic spring
-		LEFT_VISITED, RIGHT_VISITED, LEFTRIGHT_CLEAR
+		LEFT_VISITED, RIGHT_VISITED, LEFTRIGHT_CLEAR,
+
+		//object with reward
+		REWARD_RESET, REWARD_ADD_VISITOR, REWARD_SELECT
 	};
 }
 

+ 3 - 4
lib/NetPacksLib.cpp

@@ -246,15 +246,14 @@ DLL_LINKAGE void GiveBonus::applyGs( CGameState *gs )
 		&& gs->map->objects[bonus.sid]->ID == Obj::EVENT) //it's morale/luck bonus from an event without description
 	{
 		descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle"
-
-		// Some of(?) versions of H3 use %s here instead of %d. Try to replace both of them
-		boost::replace_first(descr,"%d",boost::lexical_cast<std::string>(std::abs(bonus.val)));
-		boost::replace_first(descr,"%s",boost::lexical_cast<std::string>(std::abs(bonus.val)));
 	}
 	else
 	{
 		bdescr.toString(descr);
 	}
+	// Some of(?) versions of H3 use %s here instead of %d. Try to replace both of them
+	boost::replace_first(descr,"%d",boost::lexical_cast<std::string>(std::abs(bonus.val)));
+	boost::replace_first(descr,"%s",boost::lexical_cast<std::string>(std::abs(bonus.val)));
 }
 
 DLL_LINKAGE void ChangeObjPos::applyGs( CGameState *gs )

+ 1 - 0
lib/mapping/MapFormatH3M.cpp

@@ -21,6 +21,7 @@
 #include "../CGeneralTextHandler.h"
 #include "../CHeroHandler.h"
 #include "../CObjectHandler.h"
+#include "../CObjectWithReward.h"
 #include "../CDefObjInfoHandler.h"
 #include "../VCMI_Lib.h"
 #include "../NetPacksBase.h"

+ 7 - 7
lib/registerTypes/RegisterTypes.h

@@ -5,6 +5,7 @@
 #include "../VCMI_Lib.h"
 #include "../CArtHandler.h"
 #include "../CObjectHandler.h"
+#include "../CObjectWithReward.h"
 #include "../CGameState.h"
 #include "../CHeroHandler.h"
 #include "../CTownHandler.h"
@@ -32,7 +33,6 @@ void registerTypesMapObjects1(Serializer &s)
 
 	// Non-armed objects
 	s.template registerType<CGObjectInstance, CGTeleport>();
-	s.template registerType<CGObjectInstance, CGPickable>();
 	s.template registerType<CGObjectInstance, CGSignBottle>();
 	s.template registerType<CGObjectInstance, CGScholar>();
 	s.template registerType<CGObjectInstance, CGBonusingObject>();
@@ -80,16 +80,16 @@ void registerTypesMapObjects2(Serializer &s)
 		s.template registerType<CGTownBuilding, CTownBonus>();
 			s.template registerType<CGTownBuilding, COPWBonus>();
 
-
-	s.template registerType<CGObjectInstance, CGVisitableOPH>();
-
-	s.template registerType<CGObjectInstance, CGVisitableOPW>();
-	s.template registerType<CGVisitableOPW, CGMagicSpring>();
+	s.template registerType<CGObjectInstance, CObjectWithReward>();
+		s.template registerType<CObjectWithReward, CGPickable>();
+		s.template registerType<CObjectWithReward, CGVisitableOPH>();
+		s.template registerType<CObjectWithReward, CGVisitableOPW>();
+		s.template registerType<CObjectWithReward, CGOnceVisitable>();
+			s.template registerType<CGVisitableOPW, CGMagicSpring>();
 
 	s.template registerType<CGObjectInstance, CPlayersVisited>();
 		s.template registerType<CPlayersVisited, CGWitchHut>();
 		s.template registerType<CPlayersVisited, CGShrine>();
-		s.template registerType<CPlayersVisited, CGOnceVisitable>();
 		s.template registerType<CPlayersVisited, CCartographer>();
 		s.template registerType<CPlayersVisited, CGObelisk>();
 

部分文件因为文件数量过多而无法显示