浏览代码

Merge pull request #4540 from IvanSavenko/rewardable_banks

Convert creature banks to rewardable objects
Ivan Savenko 1 年之前
父节点
当前提交
75f8614f26
共有 62 个文件被更改,包括 1404 次插入1218 次删除
  1. 3 3
      AI/BattleAI/StackWithBonuses.cpp
  2. 1 1
      AI/BattleAI/StackWithBonuses.h
  3. 0 6
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  4. 5 40
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  5. 0 8
      AI/Nullkiller/Engine/FuzzyHelper.h
  6. 0 53
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  7. 0 1
      AI/VCAI/AIUtility.cpp
  8. 0 6
      AI/VCAI/FuzzyEngines.cpp
  9. 5 44
      AI/VCAI/FuzzyHelper.cpp
  10. 0 8
      AI/VCAI/FuzzyHelper.h
  11. 0 2
      AI/VCAI/VCAI.cpp
  12. 2 3
      client/Client.h
  13. 0 87
      config/battleStartpos.json
  14. 1 1
      config/buildingsLibrary.json
  15. 72 2
      config/gameConfig.json
  16. 458 513
      config/objects/creatureBanks.json
  17. 76 0
      config/objects/pyramid.json
  18. 7 1
      config/schemas/object.json
  19. 14 2
      config/schemas/rewardable.json
  20. 58 10
      docs/modders/Map_Object_Format.md
  21. 111 2
      docs/modders/Map_Objects/Creature_Bank.md
  22. 30 2
      docs/modders/Map_Objects/Rewardable.md
  23. 2 0
      lib/CMakeLists.txt
  24. 5 0
      lib/CPlayerState.h
  25. 18 17
      lib/GameSettings.cpp
  26. 3 3
      lib/IGameCallback.h
  27. 24 23
      lib/IGameSettings.h
  28. 48 94
      lib/battle/BattleInfo.cpp
  29. 5 3
      lib/battle/BattleInfo.h
  30. 81 0
      lib/battle/BattleLayout.cpp
  31. 39 0
      lib/battle/BattleLayout.h
  32. 2 1
      lib/battle/IBattleState.h
  33. 13 10
      lib/mapObjectConstructors/CRewardableConstructor.cpp
  34. 0 1
      lib/mapObjectConstructors/CommonConstructors.h
  35. 15 130
      lib/mapObjects/CBank.cpp
  36. 1 1
      lib/mapObjects/CGCreature.cpp
  37. 1 1
      lib/mapObjects/CGDwelling.cpp
  38. 1 1
      lib/mapObjects/CGHeroInstance.cpp
  39. 2 2
      lib/mapObjects/CGPandoraBox.cpp
  40. 2 1
      lib/mapObjects/CGTownInstance.cpp
  41. 135 44
      lib/mapObjects/CRewardableObject.cpp
  42. 13 7
      lib/mapObjects/CRewardableObject.h
  43. 0 0
      lib/mapObjects/CreatureBank.cpp
  44. 0 0
      lib/mapObjects/CreatureBank.h
  45. 0 1
      lib/mapObjects/MapObjects.h
  46. 4 4
      lib/mapObjects/MiscObjects.cpp
  47. 1 1
      lib/mapping/MapFormatH3M.cpp
  48. 17 19
      lib/networkPacks/NetPacksLib.cpp
  49. 5 5
      lib/networkPacks/PacksForClient.h
  50. 1 0
      lib/rewardable/Configuration.cpp
  51. 13 1
      lib/rewardable/Configuration.h
  52. 69 7
      lib/rewardable/Info.cpp
  53. 3 0
      lib/rewardable/Reward.h
  54. 2 1
      lib/serializer/ESerializationVersion.h
  55. 4 9
      server/CGameHandler.cpp
  56. 2 3
      server/CGameHandler.h
  57. 15 20
      server/battles/BattleProcessor.cpp
  58. 5 6
      server/battles/BattleProcessor.h
  59. 3 2
      server/queries/BattleQueries.cpp
  60. 3 1
      test/game/CGameStateTest.cpp
  61. 2 3
      test/mock/mock_IGameCallback.h
  62. 2 1
      test/mock/mock_battle_IBattleState.h

+ 3 - 3
AI/BattleAI/StackWithBonuses.cpp

@@ -12,6 +12,7 @@
 
 
 #include <vcmi/events/EventBus.h>
 #include <vcmi/events/EventBus.h>
 
 
+#include "../../lib/battle/BattleLayout.h"
 #include "../../lib/CStack.h"
 #include "../../lib/CStack.h"
 #include "../../lib/ScriptHandler.h"
 #include "../../lib/ScriptHandler.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
@@ -479,10 +480,9 @@ int3 HypotheticBattle::getLocation() const
 	return int3(-1, -1, -1);
 	return int3(-1, -1, -1);
 }
 }
 
 
-bool HypotheticBattle::isCreatureBank() const
+BattleLayout HypotheticBattle::getLayout() const
 {
 {
-	// TODO
-	return false;
+	return subject->getBattle()->getLayout();
 }
 }
 
 
 int64_t HypotheticBattle::getTreeVersion() const
 int64_t HypotheticBattle::getTreeVersion() const

+ 1 - 1
AI/BattleAI/StackWithBonuses.h

@@ -160,7 +160,7 @@ public:
 	int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
 	int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
 	std::vector<SpellID> getUsedSpells(BattleSide side) const override;
 	std::vector<SpellID> getUsedSpells(BattleSide side) const override;
 	int3 getLocation() const override;
 	int3 getLocation() const override;
-	bool isCreatureBank() const override;
+	BattleLayout getLayout() const override;
 
 
 	int64_t getTreeVersion() const;
 	int64_t getTreeVersion() const;
 
 

+ 0 - 6
AI/Nullkiller/Engine/FuzzyEngines.cpp

@@ -208,12 +208,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
 		enemyFlyers->setValue(enemyStructure.flyers);
 		enemyFlyers->setValue(enemyStructure.flyers);
 		enemySpeed->setValue(enemyStructure.maxSpeed);
 		enemySpeed->setValue(enemyStructure.maxSpeed);
 
 
-		bool bank = dynamic_cast<const CBank *>(enemy);
-		if(bank)
-			bankPresent->setValue(1);
-		else
-			bankPresent->setValue(0);
-
 		const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
 		const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
 		if(fort)
 		if(fort)
 			castleWalls->setValue(fort->fortLevel());
 			castleWalls->setValue(fort->fortLevel());

+ 5 - 40
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -20,25 +20,6 @@
 namespace NKAI
 namespace NKAI
 {
 {
 
 
-ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
-{
-	//this one is not fuzzy anymore, just calculate weighted average
-
-	auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
-
-	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
-
-	ui64 totalStrength = 0;
-	ui8 totalChance = 0;
-	for(auto config : bankInfo->getPossibleGuards(bank->cb))
-	{
-		totalStrength += config.second.totalStrength * config.first;
-		totalChance += config.first;
-	}
-	return totalStrength / std::max<ui8>(totalChance, 1); //avoid division by zero
-
-}
-
 ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards)
 ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards)
 {
 {
 	auto cb = ai->cb.get();
 	auto cb = ai->cb.get();
@@ -158,30 +139,14 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 			return 0;
 			return 0;
 		[[fallthrough]];
 		[[fallthrough]];
 	}
 	}
-	case Obj::MONSTER:
-	case Obj::GARRISON:
-	case Obj::GARRISON2:
-	case Obj::CREATURE_GENERATOR1:
-	case Obj::CREATURE_GENERATOR4:
-	case Obj::MINE:
-	case Obj::ABANDONED_MINE:
-	case Obj::PANDORAS_BOX:
-	case Obj::CRYPT: //crypt
-	case Obj::CREATURE_BANK: //crebank
-	case Obj::DRAGON_UTOPIA:
-	case Obj::SHIPWRECK: //shipwreck
-	case Obj::DERELICT_SHIP: //derelict ship
+	default:
 	{
 	{
 		const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
 		const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
-		return a->getArmyStrength();
-	}
-	case Obj::PYRAMID:
-	{
-		return estimateBankDanger(dynamic_cast<const CBank *>(obj));
+		if (a)
+			return a->getArmyStrength();
+		else
+			return 0;
 	}
 	}
-	default:
-		return 0;
 	}
 	}
 }
 }
-
 }
 }

+ 0 - 8
AI/Nullkiller/Engine/FuzzyHelper.h

@@ -10,12 +10,6 @@
 #pragma once
 #pragma once
 #include "FuzzyEngines.h"
 #include "FuzzyEngines.h"
 
 
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CBank;
-
-VCMI_LIB_NAMESPACE_END
-
 namespace NKAI
 namespace NKAI
 {
 {
 
 
@@ -30,8 +24,6 @@ private:
 public:
 public:
 	FuzzyHelper(const Nullkiller * ai): ai(ai) {}
 	FuzzyHelper(const Nullkiller * ai): ai(ai) {}
 
 
-	ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class?
-
 	ui64 evaluateDanger(const CGObjectInstance * obj);
 	ui64 evaluateDanger(const CGObjectInstance * obj);
 	ui64 evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards = true);
 	ui64 evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards = true);
 };
 };

+ 0 - 53
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -118,25 +118,6 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
 	return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL  ? booster * 500 : 250);
 	return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL  ? booster * 500 : 250);
 }
 }
 
 
-TResources getCreatureBankResources(const CGObjectInstance * target, const CGHeroInstance * hero)
-{
-	//Fixme: unused variable hero
-
-	auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance);
-	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
-	auto resources = bankInfo->getPossibleResourcesReward();
-	TResources result = TResources();
-	int sum = 0;
-
-	for(auto & reward : resources)
-	{
-		result += reward.data * reward.chance;
-		sum += reward.chance;
-	}
-
-	return sum > 1 ? result / sum : result;
-}
-
 int32_t getResourcesGoldReward(const TResources & res)
 int32_t getResourcesGoldReward(const TResources & res)
 {
 {
 	int32_t result = 0;
 	int32_t result = 0;
@@ -303,22 +284,13 @@ uint64_t RewardEvaluator::getArmyReward(
 	{
 	{
 	case Obj::HILL_FORT:
 	case Obj::HILL_FORT:
 		return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
 		return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
-	case Obj::CREATURE_BANK:
-		return getCreatureBankArmyReward(target, hero);
 	case Obj::CREATURE_GENERATOR1:
 	case Obj::CREATURE_GENERATOR1:
 	case Obj::CREATURE_GENERATOR2:
 	case Obj::CREATURE_GENERATOR2:
 	case Obj::CREATURE_GENERATOR3:
 	case Obj::CREATURE_GENERATOR3:
 	case Obj::CREATURE_GENERATOR4:
 	case Obj::CREATURE_GENERATOR4:
 		return getDwellingArmyValue(ai->cb.get(), target, checkGold);
 		return getDwellingArmyValue(ai->cb.get(), target, checkGold);
-	case Obj::CRYPT:
-	case Obj::SHIPWRECK:
-	case Obj::SHIPWRECK_SURVIVOR:
-	case Obj::WARRIORS_TOMB:
-		return 1000;
 	case Obj::ARTIFACT:
 	case Obj::ARTIFACT:
 		return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->artType);
 		return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->artType);
-	case Obj::DRAGON_UTOPIA:
-		return 10000;
 	case Obj::HERO:
 	case Obj::HERO:
 		return  relations == PlayerRelations::ENEMIES
 		return  relations == PlayerRelations::ENEMIES
 			? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
 			? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
@@ -553,13 +525,6 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
 		return getResourceRequirementStrength(res);
 		return getResourceRequirementStrength(res);
 	}
 	}
 
 
-	case Obj::CREATURE_BANK:
-	{
-		auto resourceReward = getCreatureBankResources(target, nullptr);
-		
-		return getResourceRequirementStrength(resourceReward);
-	}
-
 	case Obj::TOWN:
 	case Obj::TOWN:
 	{
 	{
 		if(ai->buildAnalyzer->getDevelopmentInfo().empty())
 		if(ai->buildAnalyzer->getDevelopmentInfo().empty())
@@ -670,8 +635,6 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
 	case Obj::PANDORAS_BOX:
 	case Obj::PANDORAS_BOX:
 		//Can contains experience, spells, or skills (only on custom maps)
 		//Can contains experience, spells, or skills (only on custom maps)
 		return 2.5f;
 		return 2.5f;
-	case Obj::PYRAMID:
-		return 6.0f;
 	case Obj::HERO:
 	case Obj::HERO:
 		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
 		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
 			? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level
 			? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level
@@ -778,22 +741,6 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 		auto * mine = dynamic_cast<const CGMine*>(target);
 		auto * mine = dynamic_cast<const CGMine*>(target);
 		return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75);
 		return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75);
 	}
 	}
-	case Obj::MYSTICAL_GARDEN:
-	case Obj::WINDMILL:
-		return 100;
-	case Obj::CAMPFIRE:
-		return 800;
-	case Obj::WAGON:
-		return 100;
-	case Obj::CREATURE_BANK:
-		return getResourcesGoldReward(getCreatureBankResources(target, hero));
-	case Obj::CRYPT:
-	case Obj::DERELICT_SHIP:
-		return 3000;
-	case Obj::DRAGON_UTOPIA:
-		return 10000;
-	case Obj::SEA_CHEST:
-		return 1500;
 	case Obj::PANDORAS_BOX:
 	case Obj::PANDORAS_BOX:
 		return 2500;
 		return 2500;
 	case Obj::PRISON:
 	case Obj::PRISON:

+ 0 - 1
AI/VCAI/AIUtility.cpp

@@ -16,7 +16,6 @@
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/CHeroHandler.h"
-#include "../../lib/mapObjects/CBank.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/CQuest.h"
 #include "../../lib/mapObjects/CQuest.h"
 #include "../../lib/mapping/CMapDefines.h"
 #include "../../lib/mapping/CMapDefines.h"

+ 0 - 6
AI/VCAI/FuzzyEngines.cpp

@@ -219,12 +219,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
 		enemyFlyers->setValue(enemyStructure.flyers);
 		enemyFlyers->setValue(enemyStructure.flyers);
 		enemySpeed->setValue(enemyStructure.maxSpeed);
 		enemySpeed->setValue(enemyStructure.maxSpeed);
 
 
-		bool bank = dynamic_cast<const CBank *>(enemy);
-		if(bank)
-			bankPresent->setValue(1);
-		else
-			bankPresent->setValue(0);
-
 		const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
 		const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
 		if(fort)
 		if(fort)
 			castleWalls->setValue(fort->fortLevel());
 			castleWalls->setValue(fort->fortLevel());

+ 5 - 44
AI/VCAI/FuzzyHelper.cpp

@@ -16,7 +16,6 @@
 #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
 #include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
-#include "../../lib/mapObjects/CBank.h"
 #include "../../lib/mapObjects/CGCreature.h"
 #include "../../lib/mapObjects/CGCreature.h"
 #include "../../lib/mapObjects/CGDwelling.h"
 #include "../../lib/mapObjects/CGDwelling.h"
 #include "../../lib/gameState/InfoAboutArmy.h"
 #include "../../lib/gameState/InfoAboutArmy.h"
@@ -62,25 +61,6 @@ Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec)
 	return result;
 	return result;
 }
 }
 
 
-ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
-{
-	//this one is not fuzzy anymore, just calculate weighted average
-
-	auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
-
-	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
-
-	ui64 totalStrength = 0;
-	ui8 totalChance = 0;
-	for(auto config : bankInfo->getPossibleGuards(bank->cb))
-	{
-		totalStrength += config.second.totalStrength * config.first;
-		totalChance += config.first;
-	}
-	return totalStrength / std::max<ui8>(totalChance, 1); //avoid division by zero
-
-}
-
 float FuzzyHelper::evaluate(Goals::VisitTile & g)
 float FuzzyHelper::evaluate(Goals::VisitTile & g)
 {
 {
 	if(g.parent)
 	if(g.parent)
@@ -301,32 +281,13 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
 		cb->getTownInfo(obj, iat);
 		cb->getTownInfo(obj, iat);
 		return iat.army.getStrength();
 		return iat.army.getStrength();
 	}
 	}
-	case Obj::MONSTER:
-	{
-		//TODO!!!!!!!!
-		const CGCreature * cre = dynamic_cast<const CGCreature *>(obj);
-		return cre->getArmyStrength();
-	}
-	case Obj::CREATURE_GENERATOR1:
-	case Obj::CREATURE_GENERATOR4:
-	{
-		const CGDwelling * d = dynamic_cast<const CGDwelling *>(obj);
-		return d->getArmyStrength();
-	}
-	case Obj::MINE:
-	case Obj::ABANDONED_MINE:
+	default:
 	{
 	{
 		const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
 		const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
-		return a->getArmyStrength();
+		if (a)
+			return a->getArmyStrength();
+		else
+			return 0;
 	}
 	}
-	case Obj::CRYPT: //crypt
-	case Obj::CREATURE_BANK: //crebank
-	case Obj::DRAGON_UTOPIA:
-	case Obj::SHIPWRECK: //shipwreck
-	case Obj::DERELICT_SHIP: //derelict ship
-	case Obj::PYRAMID:
-		return estimateBankDanger(dynamic_cast<const CBank *>(obj));
-	default:
-		return 0;
 	}
 	}
 }
 }

+ 0 - 8
AI/VCAI/FuzzyHelper.h

@@ -10,12 +10,6 @@
 #pragma once
 #pragma once
 #include "FuzzyEngines.h"
 #include "FuzzyEngines.h"
 
 
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CBank;
-
-VCMI_LIB_NAMESPACE_END
-
 class DLL_EXPORT FuzzyHelper
 class DLL_EXPORT FuzzyHelper
 {
 {
 public:
 public:
@@ -42,8 +36,6 @@ public:
 	float evaluate(Goals::AbstractGoal & g);
 	float evaluate(Goals::AbstractGoal & g);
 	void setPriority(Goals::TSubgoal & g);
 	void setPriority(Goals::TSubgoal & g);
 
 
-	ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class?
-
 	Goals::TSubgoal chooseSolution(Goals::TGoalVec vec);
 	Goals::TSubgoal chooseSolution(Goals::TGoalVec vec);
 	//std::shared_ptr<AbstractGoal> chooseSolution (std::vector<std::shared_ptr<AbstractGoal>> & vec);
 	//std::shared_ptr<AbstractGoal> chooseSolution (std::vector<std::shared_ptr<AbstractGoal>> & vec);
 
 

+ 0 - 2
AI/VCAI/VCAI.cpp

@@ -2750,8 +2750,6 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
 
 
 	if(dynamic_cast<const CGDwelling *>(obj))
 	if(dynamic_cast<const CGDwelling *>(obj))
 		return true;
 		return true;
-	if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
-		return true;
 
 
 	switch(obj->ID)
 	switch(obj->ID)
 	{
 	{

+ 2 - 3
client/Client.h

@@ -193,9 +193,8 @@ public:
 	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
 	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
 	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
 	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
 	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
 	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
-	void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero
-	void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used
-	void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
+	void startBattle(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, const BattleLayout & layout, const CGTownInstance * town) override {}; //use hero=nullptr for no hero
+	void startBattle(const CArmedInstance * army1, const CArmedInstance * army2) override {}; //if any of armies is hero, hero will be used
 	bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;};
 	bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;};
 	void giveHeroBonus(GiveBonus * bonus) override {};
 	void giveHeroBonus(GiveBonus * bonus) override {};
 	void setMovePoints(SetMovePoints * smp) override {};
 	void setMovePoints(SetMovePoints * smp) override {};

+ 0 - 87
config/battleStartpos.json

@@ -1,87 +0,0 @@
-{
-	"battle_positions":
-	[
-		{
-			"name" : "attackerLoose", // loose formation, attacker
-			"levels": [
-				[            86                ],
-				[ 35,                 137      ],
-				[ 35,        86,      137      ],
-				[ 1,     69,     103,      171 ],
-				[ 1, 35,     86,      137, 171 ],
-				[ 1, 35, 69,     103, 137, 171 ],
-				[ 1, 35, 69, 86, 103, 137, 171 ]
-				]
-		},
-
-		{
-			"name" : "defenderLoose", // loose formation, defender
-			"levels": [
-				[             100                ],
-				[     49,               151      ],
-				[     49,     100,      151      ],
-				[ 15,     83,      117,      185 ],
-				[ 15, 49,     100,      151, 185 ],
-				[ 15, 49, 83,      117, 151, 185 ],
-				[ 15, 49, 83, 100, 117, 151, 185 ]
-				]
-		},
-
-		{
-			"name" : "attackerTight", // tight formation, attacker
-			"levels": [
-				[             86                ],
-				[         69,     103           ],
-				[         69, 86, 103           ],
-				[     35, 69,     103, 137      ],
-				[     35, 69, 86, 103, 137      ],
-				[  1, 35, 69,     103, 137, 171 ],
-				[  1, 35, 69, 86, 103, 137, 171 ]
-				]
-		},
-
-		{
-			"name" : "defenderTight", // tight formation, defender
-			"levels": [
-				[             100                ],
-				[         83,      117           ],
-				[         83, 100, 117           ],
-				[     49, 83,      117, 151      ],
-				[     49, 83, 100, 117, 151      ],
-				[ 15, 49, 83,      117, 151, 185 ],
-				[ 15, 49, 83, 100, 117, 151, 185 ]
-				]
-		},
-
-		{
-			"name" : "attackerCreBank", // creature bank, attacker
-			"levels": [
-				[ 57 ],
-				[ 57, 61 ],
-				[ 57, 61, 90 ],
-				[ 57, 61, 90, 93 ],
-				[ 57, 61, 90, 93, 96 ],
-				[ 57, 61, 90, 93, 96, 125 ],
-				[ 57, 61, 90, 93, 96, 125, 129 ]
-				]
-		},
-
-		{
-			"name" : "defenderCreBank", // creature bank, defender
-			"levels": [
-				[ 15 ],
-				[ 15, 185 ],
-				[ 15, 185, 172 ],
-				[ 15, 185, 172, 2 ],
-				[ 15, 185, 172, 2, 100 ],
-				[ 15, 185, 172, 2, 100, 86 ],
-				[ 15, 185, 172, 2, 100, 86, 8 ]
-				]
-		}
-	],
-	"commanderPositions":
-	{
-		"field" : [88, 98], //attacker/defender
-		"creBank" : [95, 8] //not expecting defendig hero at bank, but hell knows
-	}
-}

+ 1 - 1
config/buildingsLibrary.json

@@ -251,7 +251,7 @@
 	
 	
 	// Section 4 - buildings that now have dedicated mechanics
 	// Section 4 - buildings that now have dedicated mechanics
 	"ballistaYard": {
 	"ballistaYard": {
-		"blacksmith" : "ballista"
+		"warMachine" : "ballista"
 	},
 	},
 	
 	
 	"thievesGuild" : {
 	"thievesGuild" : {

+ 72 - 2
config/gameConfig.json

@@ -57,6 +57,7 @@
 		"config/objects/magicWell.json",
 		"config/objects/magicWell.json",
 		"config/objects/moddables.json",
 		"config/objects/moddables.json",
 		"config/objects/observatory.json",
 		"config/objects/observatory.json",
+		"config/objects/pyramid.json",
 		"config/objects/rewardableBonusing.json",
 		"config/objects/rewardableBonusing.json",
 		"config/objects/rewardableOncePerHero.json",
 		"config/objects/rewardableOncePerHero.json",
 		"config/objects/rewardableOncePerWeek.json",
 		"config/objects/rewardableOncePerWeek.json",
@@ -342,8 +343,77 @@
 			// limit of damage reduction that can be achieved by overpowering defense points
 			// limit of damage reduction that can be achieved by overpowering defense points
 			"defensePointDamageFactorCap": 0.7,
 			"defensePointDamageFactorCap": 0.7,
 			// If set to true, double-wide creatures will trigger obstacle effect when moving one tile forward or backwards
 			// If set to true, double-wide creatures will trigger obstacle effect when moving one tile forward or backwards
-			"oneHexTriggersObstacles": false
-		},	
+			"oneHexTriggersObstacles": false,
+			
+			// Positions of units on start of the combat
+			// If battle does not defines specific configuration, 'default' configuration will be used
+			// Configuration must define either 'attackerUnits' list of 7 elements or both 'attackerUnitsLoose' and 'attackerUnitsTight' lists of 7 elements, 1..7 elements each
+			// Similarly, for defender configuration must have either 'defenderUnits' or both 'defenderUnitsLoose' and 'defenderUnitsTight'
+			"layouts" : {
+				"default" : {
+					"tacticsAllowed" : true,
+					"obstaclesAllowed" : true,
+					"attackerCommander" : 88,
+					"defenderCommander" : 98,
+					"attackerWarMachines" : [ 52, 18, 154, 120 ],
+					"defenderWarMachines" : [ 66, 32, 168, 134 ],
+					"attackerUnitsLoose": [
+						[            86                ],
+						[ 35,                 137      ],
+						[ 35,        86,      137      ],
+						[ 1,     69,     103,      171 ],
+						[ 1, 35,     86,      137, 171 ],
+						[ 1, 35, 69,     103, 137, 171 ],
+						[ 1, 35, 69, 86, 103, 137, 171 ]
+					],
+					"defenderUnitsLoose": [
+						[             100                ],
+						[     49,               151      ],
+						[     49,     100,      151      ],
+						[ 15,     83,      117,      185 ],
+						[ 15, 49,     100,      151, 185 ],
+						[ 15, 49, 83,      117, 151, 185 ],
+						[ 15, 49, 83, 100, 117, 151, 185 ]
+					],
+					"attackerUnitsTight": [
+						[             86                ],
+						[         69,     103           ],
+						[         69, 86, 103           ],
+						[     35, 69,     103, 137      ],
+						[     35, 69, 86, 103, 137      ],
+						[  1, 35, 69,     103, 137, 171 ],
+						[  1, 35, 69, 86, 103, 137, 171 ]
+					],
+					"defenderUnitsTight": [
+						[             100                ],
+						[         83,      117           ],
+						[         83, 100, 117           ],
+						[     49, 83,      117, 151      ],
+						[     49, 83, 100, 117, 151      ],
+						[ 15, 49, 83,      117, 151, 185 ],
+						[ 15, 49, 83, 100, 117, 151, 185 ]
+					]
+				},
+				// Configuration for creature banks with single-tile enemies
+				"creatureBankNarrow" : {
+					"tacticsAllowed" : false,
+					"obstaclesAllowed" : false,
+					"attackerCommander" : 95,
+					"defenderCommander" : 8,
+					"attackerUnits": [ 57, 61, 90, 93, 96, 125, 129 ],
+					"defenderUnits": [ 15, 185, 172, 2, 100, 87, 8 ]
+				},
+				// Configuration for creature banks with double-wide enemies
+				"creatureBankWide" : {
+					"tacticsAllowed" : false,
+					"obstaclesAllowed" : false,
+					"attackerCommander" : 95,
+					"defenderCommander" : 8,
+					"attackerUnits": [ 57, 61, 90, 93, 96, 125, 129 ],
+					"defenderUnits": [ 15, 185, 171, 1, 100, 86, 8 ]
+				}
+			}
+		},
 
 
 		"creatures":
 		"creatures":
 		{
 		{

文件差异内容过多而无法显示
+ 458 - 513
config/objects/creatureBanks.json


+ 76 - 0
config/objects/pyramid.json

@@ -0,0 +1,76 @@
+{
+	"pyramid" : {
+		"index" :63,
+		"handler" : "configurable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["MYSTERY"]
+			}
+		},
+		"types" : {
+			"pyramid" : {
+				"index" : 0,
+
+				"name" : "Pyramid",
+				"aiValue" : 8000,
+				"rmg" : {
+					"value"		: 5000,
+					"rarity"	: 20
+				},
+				
+				"variables" : {
+					"spell" : {
+						"gainedSpell" : {
+							"level": 5
+						}
+					}
+				},
+
+				"onGuardedMessage" : 105,
+				"visitMode" : "once",
+				"selectMode" : "selectFirst",
+				"onVisited" : [
+					{
+						"message" : 107,
+						"bonuses" : [ { "type" : "LUCK", "val" : -2, "duration" : "ONE_BATTLE", "description" : 70 } ]
+					}
+				],
+				"guardsLayout" : "default",
+				"rewards" : [
+					{
+						"limiter" : {
+							"canLearnSpells" : [
+								"@gainedSpell"
+							]
+						},
+						"spells" : [
+							"@gainedSpell"
+						],
+						"message" : [ 106, "%s." ], // Upon defeating monsters, you learn new spell
+						"guards" : [
+							{ "amount" : 40, "type" : "goldGolem" },
+							{ "amount" : 10, "type" : "diamondGolem" },
+							{ "amount" : 10, "type" : "diamondGolem" }
+						]
+					}
+				],
+				"onEmpty" : [
+					{
+						"limiter" : {
+							"artifacts" : [
+								{
+									"type" : "spellBook"
+								}
+							]
+						},
+						"message" : [ 106, "%s.", 108 ] // No Wisdom
+					},
+					{
+						"message" : [ 106, "%s.", 109 ] // No spellbook
+					}
+				]
+
+			}
+		}
+	}
+}

+ 7 - 1
config/schemas/object.json

@@ -17,7 +17,13 @@
 			"type" : "number"
 			"type" : "number"
 		},
 		},
 		"handler" : {
 		"handler" : {
-			"type" : "string"
+			"type" : "string",
+			"enum" : [
+				"configurable", "dwelling", "hero", "town", "boat", "market", "hillFort", "shipyard", "monster", "resource", "static", "randomArtifact", 
+				"randomHero", "randomResource", "randomTown", "randomMonster", "randomDwelling", "generic", "artifact", "borderGate", "borderGuard", "denOfThieves", 
+				"event", "garrison", "heroPlaceholder", "keymaster", "lighthouse", "magi", "mine", "obelisk", "pandora", "prison", "questGuard", "seerHut", "sign", 
+				"siren", "monolith", "subterraneanGate", "whirlpool", "terrain"
+			]
 		},
 		},
 		"base" : {
 		"base" : {
 			"type" : "object"
 			"type" : "object"

+ 14 - 2
config/schemas/rewardable.json

@@ -98,6 +98,9 @@
 						"max" : { "type" : "number", "exclusiveMinimum" : 0, "maximum" : 100 }
 						"max" : { "type" : "number", "exclusiveMinimum" : 0, "maximum" : 100 }
 					}
 					}
 				},
 				},
+
+				"guards"  : { "$ref" : "#/definitions/identifierWithValueList" },
+				
 				"limiter" : { "$ref" : "#/definitions/limiter" },
 				"limiter" : { "$ref" : "#/definitions/limiter" },
 				"message" : { "$ref" : "#/definitions/message" },
 				"message" : { "$ref" : "#/definitions/message" },
 				"description" : { "$ref" : "#/definitions/message" },
 				"description" : { "$ref" : "#/definitions/message" },
@@ -256,6 +259,9 @@
 			},
 			},
 		},
 		},
 
 
+		"onGuardedMessage" : {
+			"$ref" : "#/definitions/message"
+		},
 		"onSelectMessage" : {
 		"onSelectMessage" : {
 			"$ref" : "#/definitions/message"
 			"$ref" : "#/definitions/message"
 		},
 		},
@@ -287,15 +293,21 @@
 			"type" : "boolean"
 			"type" : "boolean"
 		},
 		},
 		
 		
+		"coastVisitable": {
+			"type" : "boolean"
+		},
+		
 		"visitMode": {
 		"visitMode": {
 			"enum" : [ "unlimited", "once", "hero", "bonus", "limiter", "player" ],
 			"enum" : [ "unlimited", "once", "hero", "bonus", "limiter", "player" ],
 			"type" : "string"
 			"type" : "string"
 		},
 		},
 		
 		
-		"visitLimiter": {
-			"type" : "object"
+		"guardsLayout": {
+			"type" : "string"
 		},
 		},
 		
 		
+		"visitLimiter": { "$ref" : "#/definitions/limiter" },
+		
 		"selectMode": {
 		"selectMode": {
 			"enum" : [ "selectFirst", "selectPlayer", "selectRandom", "selectAll" ],
 			"enum" : [ "selectFirst", "selectPlayer", "selectRandom", "selectAll" ],
 			"type" : "string"
 			"type" : "string"

+ 58 - 10
docs/modders/Map_Object_Format.md

@@ -14,14 +14,6 @@ Full object consists from 3 parts:
     generated by the game. When new object is created its starting
     generated by the game. When new object is created its starting
     appearance will be copied from template
     appearance will be copied from template
 
 
-## Object types
-
-- [Rewardable](Map_Objects/Rewardable.md) - Visitable object which grants all kinds of rewards (gold, experience, Bonuses etc...)
-- [Creature Bank](Map_Objects/Creature_Bank.md) - Object that grants award on defeating guardians
-- [Dwelling](Map_Objects/Dwelling.md) - Object that allows recruitments of units outside of towns
-- [Market](Map_Objects/Market.md) - Trading resources, artifacts, creatures and such
-- [Boat](Map_Objects/Boat.md) - Object to move across different terrains, such as water
-
 ## Object group format
 ## Object group format
 
 
 ``` javascript
 ``` javascript
@@ -33,7 +25,8 @@ Full object consists from 3 parts:
 		// human readable name, localized 
 		// human readable name, localized 
 		"name": "My cool object",
 		"name": "My cool object",
 
 
-		//defines C++/script class name that handles behavior of this object
+		// defines C++ class name that handles behavior of this object
+		// see Object Types section below for possible values
 		"handler" : "mine",
 		"handler" : "mine",
 
 
 		// default values, will be merged with each type during loading
 		// default values, will be merged with each type during loading
@@ -46,6 +39,61 @@ Full object consists from 3 parts:
 }
 }
 ```
 ```
 
 
+## Object types
+
+### Moddable types
+These are object types that are available for modding and have configurable properties
+
+- `configurable` - see [Rewardable](Map_Objects/Rewardable.md). Visitable object which grants all kinds of rewards (gold, experience, Bonuses etc...)
+- `bank` - see [Creature Bank](Map_Objects/Creature_Bank.md). Object that grants award on defeating guardians. Deprectated in favor of [Rewardable](Map_Objects/Rewardable.md)
+- `dwelling` - see [Dwelling](Map_Objects/Dwelling.md). Object that allows recruitments of units outside of towns
+- `market` - see [Market](Map_Objects/Market.md). Trading resources, artifacts, creatures and such
+- `boat` - see [Boat](Map_Objects/Boat.md). Object to move across different terrains, such as water
+- `hillFort` - TODO: documentation. See config files in vcmi installation for reference
+- `shipyard` - TODO: documentation. See config files in vcmi installation for reference
+- `terrain` - Defines terrain overlays such as magic grounds. TODO: documentation. See config files in vcmi installation for reference
+
+### Common types
+These are types that don't have configurable properties, however it is possible to add additional map templates for this objects, for use in editor or in random maps generator
+
+- `static` - Defines unpassable static map obstacles that can be used by RMG
+- `generic` - Defines empty object type that provides no functionality. Note that unlike `static`, objects of this type are never used by RMG
+- `borderGate`
+- `borderGuard`
+- `lighthouse`
+- `magi`
+- `mine`
+- `obelisk`
+- `subterraneanGate`
+- `whirlpool`
+- `resource`
+- `denOfThieves`
+- `garrison`
+- `keymaster`
+- `pandora`
+- `prison`
+- `questGuard`
+- `seerHut`
+- `sign`
+- `siren`
+- `monolith`
+
+### Internal types
+These are internal types that are generally not available for modding and are handled by vcmi internally.
+
+- `hero`
+- `town`
+- `monster`
+- `randomArtifact`
+- `randomHero`
+- `randomResource`
+- `randomTown`
+- `randomMonster`
+- `randomDwelling`
+- `artifact`
+- `event`
+- `heroPlaceholder`
+
 ## Object type format
 ## Object type format
 
 
 ``` javascript
 ``` javascript
@@ -151,4 +199,4 @@ Full object consists from 3 parts:
 		"zIndex": 0
 		"zIndex": 0
 	}
 	}
 }
 }
-```
+```

+ 111 - 2
docs/modders/Map_Objects/Creature_Bank.md

@@ -3,7 +3,116 @@
 Reward types for clearing creature bank are limited to resources, creatures, artifacts and spell.
 Reward types for clearing creature bank are limited to resources, creatures, artifacts and spell.
 Format of rewards is same as in [Rewardable Objects](Rewardable.md)
 Format of rewards is same as in [Rewardable Objects](Rewardable.md)
 
 
-``` javascript
+Deprecated in 1.6. Please use [Rewardable Objects](Rewardable.md) instead. See Conversion from 1.5 format section below for help with migration
+
+### Example
+This example defines a rewardable object with functionality similar of H3 creature bank.
+See [Rewardable Objects](Rewardable.md) for detailed documentation of these properties.
+```jsonc
+{
+	"name" : "Cyclops Stockpile",
+
+	// Generic message to ask player whether he wants to attack a creature bank, can be replaced with custom string
+	"onGuardedMessage" : 32, 
+	
+	// Generic message to inform player that bank was already cleared
+	"onVisitedMessage" : 33, 
+	
+	// As an alternative to a generic message you can define 'reward' 
+	// that will be granted for visiting already cleared bank, such as morale debuff
+	"onVisited" : [ 
+		{
+			"message" : 123, // "Such a despicable act reduces your army's morale."
+			"bonuses" : [ { "type" : "MORALE", "val" : -1, "duration" : "ONE_BATTLE", "description" : 99 } ]
+		}
+	],
+	"visitMode" : "once", // Banks never reset
+	// Defines layout of guards. To emulate H3 logic, 
+	// use 'creatureBankNarrow' if guardian units are narrow (1-tile units)
+	// or, 'creatureBankWide' if defenders are double-hex units
+	"guardsLayout" : "creatureBankNarrow",
+	"rewards" : [
+		{
+			"message" : 34,
+			"appearChance" : { "min" : 0, "max" : 30 },
+			"guards" : [
+				{ "amount" : 4, "type" : "cyclop" },
+				{ "amount" : 4, "type" : "cyclop" },
+				{ "amount" : 4, "type" : "cyclop", "upgradeChance" : 50 },
+				{ "amount" : 4, "type" : "cyclop" },
+				{ "amount" : 4, "type" : "cyclop" }
+			],
+			"resources" : {
+				"gold" : 4000
+			}
+		},
+		{
+			"message" : 34,
+			"appearChance" : { "min" : 30, "max" : 60 },
+			"guards" : [
+				{ "amount" : 6, "type" : "cyclop" },
+				{ "amount" : 6, "type" : "cyclop" },
+				{ "amount" : 6, "type" : "cyclop", "upgradeChance" : 50 },
+				{ "amount" : 6, "type" : "cyclop" },
+				{ "amount" : 6, "type" : "cyclop" }
+			],
+			"resources" : {
+				"gold" : 6000
+			}
+		},
+		{
+			"message" : 34,
+			"appearChance" : { "min" : 60, "max" : 90 },
+			"guards" : [
+				{ "amount" : 8, "type" : "cyclop" },
+				{ "amount" : 8, "type" : "cyclop" },
+				{ "amount" : 8, "type" : "cyclop", "upgradeChance" : 50 },
+				{ "amount" : 8, "type" : "cyclop" },
+				{ "amount" : 8, "type" : "cyclop" }
+			],
+			"resources" : {
+				"gold" : 8000
+			}
+		},
+		{
+			"message" : 34,
+			"appearChance" : { "min" : 90, "max" : 100 },
+			"guards" : [
+				{ "amount" : 10, "type" : "cyclop" },
+				{ "amount" : 10, "type" : "cyclop" },
+				{ "amount" : 10, "type" : "cyclop", "upgradeChance" : 50 },
+				{ "amount" : 10, "type" : "cyclop" },
+				{ "amount" : 10, "type" : "cyclop" }
+			],
+			"resources" : {
+				"gold" : 10000
+			}
+		}
+	]
+},
+```
+
+### Conversion from 1.5 format
+This is a list of changes that needs to be done to bank config to migrate it to 1.6 system. See [Rewardable Objects](Rewardable.md) documentation for description of new fields
+
+- If your object type has defined `handler`, change its value from `bank` to `configurable`
+
+- If your object has non-zero `resetDuration`, replace with `resetParameters` entry
+
+- For each possible level, replace `chance` with `appearChance` entry
+
+- If you have `combat_value` or `field` entries inside 'reward' - remove them. These fields are unused in both 1.5 and in 1.6
+
+- Rename `levels` entry to `rewards`
+
+- Add property `"visitMode" : "once"`
+- Add property `"onGuardedMessage" : 119`, optionally - replace with custom message for object visit
+- Add property `"onVisitedMessage" : 33`, optionally - custom message or morale debuff
+- Add property `"message" : 34`, to every level of your reward, optionally - replace with custom message
+
+### Old format (1.5 or earlier)
+
+``` jsonc
 {
 {
 	/// If true, battle setup will be like normal - Attacking player on the left, enemy on the right
 	/// If true, battle setup will be like normal - Attacking player on the left, enemy on the right
 	"regularUnitPlacement" : true,
 	"regularUnitPlacement" : true,
@@ -63,4 +172,4 @@ Format of rewards is same as in [Rewardable Objects](Rewardable.md)
 	]
 	]
 }
 }
 
 
-```
+```

+ 30 - 2
docs/modders/Map_Objects/Rewardable.md

@@ -117,6 +117,9 @@ Rewardable object is defined similarly to other objects, with key difference bei
 // Message that will be shown if there are multiple selectable awards to choose from
 // Message that will be shown if there are multiple selectable awards to choose from
 "onSelectMessage" : "",
 "onSelectMessage" : "",
 
 
+// Message that will be shown if object has undefeated guards
+"onGuardedMessage" : "",
+
 // Message that will be shown if this object has been already visited before
 // Message that will be shown if this object has been already visited before
 "onVisitedMessage" : "{Warehouse of Crystal}\r\n\r\nThe owner of the storage is apologising: 'I am sorry Milord, no crystal here. Please, return next week!'",
 "onVisitedMessage" : "{Warehouse of Crystal}\r\n\r\nThe owner of the storage is apologising: 'I am sorry Milord, no crystal here. Please, return next week!'",
 
 
@@ -125,10 +128,22 @@ Rewardable object is defined similarly to other objects, with key difference bei
 "onVisited" : [
 "onVisited" : [
 ]
 ]
 
 
+// Layout of units in the battle (only used if guards are present)
+// Predefined values:
+// "default" - attacker is on the left, defender is on the right, war machine, tactics, and battlefield obstacles are present
+// "creatureBankNarrow" - emulates H3 logic for banks with narrow (1-tile wide) units
+// "creatureBankWide" - emulates H3 logic for banks with wide units that take 2 hexes
+// Additionally, it is possible to define new layouts, see "layouts" field in (vcmi install)/config/gameConfig.json file
+"guardsLayout" : "default"
+
 // if true, then player can refuse from reward and don't select anything
 // if true, then player can refuse from reward and don't select anything
 // Note that in this case object will not become "visited" and can still be revisited later
 // Note that in this case object will not become "visited" and can still be revisited later
 "canRefuse": true,
 "canRefuse": true,
 
 
+// If set to true, then this object can be visited from land when placed next to a coast.
+// NOTE: make sure that object also has "blockedVisitable" set to true. Othervice, behavior is undefined
+"coastVisitable" : true
+
 // Controls when object state will be reset, allowing potential revisits. See Reset Parameters definition section
 // Controls when object state will be reset, allowing potential revisits. See Reset Parameters definition section
 "resetParameters" : {
 "resetParameters" : {
 }
 }
@@ -479,8 +494,6 @@ Keep in mind, that all randomization is performed on map load and on object rese
 ],
 ],
 ```
 ```
 
 
-canLearnSpells
-
 ### Creatures
 ### Creatures
 - Can be used as limiter
 - Can be used as limiter
 - Can be used as reward, to give new creatures to a hero
 - Can be used as reward, to give new creatures to a hero
@@ -496,6 +509,21 @@ canLearnSpells
 ],
 ],
 ```
 ```
 
 
+### Guards
+- When used in a reward, these creatures will be added to guards of the objects
+- Hero must defeat all guards before being able to receive rewards
+- Guards are only reset when object rewards are reset
+- Requires `guardsLayout` property to be set in main part of object configuration
+- It is possible to add up to 7 slots of creatures
+- Guards of the same creature type will never merge or rearrange their stacks
+```jsonc
+"guards" : [
+    { "type" : "archer", "amount" : 20 },
+    { "type" : "archer", "amount" : 20, "upgradeChance" : 30 },
+    { "type" : "archer", "amount" : 20 }
+],
+```
+
 ### Creatures Change
 ### Creatures Change
 - Can NOT be used as limiter
 - Can NOT be used as limiter
 - Can be used as reward, to replace creatures in hero army. It is possible to use this parameter both for upgrades of creatures as well as for changing them into completely unrelated creature, e.g. similar to Skeleton Transformer
 - Can be used as reward, to replace creatures in hero army. It is possible to use this parameter both for upgrades of creatures as well as for changing them into completely unrelated creature, e.g. similar to Skeleton Transformer

+ 2 - 0
lib/CMakeLists.txt

@@ -48,6 +48,7 @@ set(lib_MAIN_SRCS
 	battle/BattleAttackInfo.cpp
 	battle/BattleAttackInfo.cpp
 	battle/BattleHex.cpp
 	battle/BattleHex.cpp
 	battle/BattleInfo.cpp
 	battle/BattleInfo.cpp
+	battle/BattleLayout.cpp
 	battle/BattleProxy.cpp
 	battle/BattleProxy.cpp
 	battle/BattleStateInfoForRetreat.cpp
 	battle/BattleStateInfoForRetreat.cpp
 	battle/CBattleInfoCallback.cpp
 	battle/CBattleInfoCallback.cpp
@@ -404,6 +405,7 @@ set(lib_MAIN_HEADERS
 	battle/BattleAttackInfo.h
 	battle/BattleAttackInfo.h
 	battle/BattleHex.h
 	battle/BattleHex.h
 	battle/BattleInfo.h
 	battle/BattleInfo.h
+	battle/BattleLayout.h
 	battle/BattleSide.h
 	battle/BattleSide.h
 	battle/BattleStateInfoForRetreat.h
 	battle/BattleStateInfoForRetreat.h
 	battle/BattleProxy.h
 	battle/BattleProxy.h

+ 5 - 0
lib/CPlayerState.h

@@ -153,6 +153,8 @@ public:
 	//TODO: boost::array, bool if possible
 	//TODO: boost::array, bool if possible
 	boost::multi_array<ui8, 3> fogOfWarMap; //[z][x][y] true - visible, false - hidden
 	boost::multi_array<ui8, 3> fogOfWarMap; //[z][x][y] true - visible, false - hidden
 
 
+	std::set<ObjectInstanceID> scoutedObjects;
+
 	TeamState();
 	TeamState();
 
 
 	template <typename Handler> void serialize(Handler &h)
 	template <typename Handler> void serialize(Handler &h)
@@ -173,6 +175,9 @@ public:
 
 
 		h & fogOfWarMap;
 		h & fogOfWarMap;
 		h & static_cast<CBonusSystemNode&>(*this);
 		h & static_cast<CBonusSystemNode&>(*this);
+
+		if (h.version >= Handler::Version::REWARDABLE_BANKS)
+			h & scoutedObjects;
 	}
 	}
 
 
 };
 };

+ 18 - 17
lib/GameSettings.cpp

@@ -37,6 +37,7 @@ GameSettings::GameSettings() = default;
 GameSettings::~GameSettings() = default;
 GameSettings::~GameSettings() = default;
 
 
 const std::vector<GameSettings::SettingOption> GameSettings::settingProperties = {
 const std::vector<GameSettings::SettingOption> GameSettings::settingProperties = {
+		{EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION,          "banks",     "showGuardsComposition"            },
 		{EGameSettings::BONUSES_GLOBAL,                         "bonuses",   "global"                           },
 		{EGameSettings::BONUSES_GLOBAL,                         "bonuses",   "global"                           },
 		{EGameSettings::BONUSES_PER_HERO,                       "bonuses",   "perHero"                          },
 		{EGameSettings::BONUSES_PER_HERO,                       "bonuses",   "perHero"                          },
 		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR,      "combat",    "attackPointDamageFactor"          },
 		{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR,      "combat",    "attackPointDamageFactor"          },
@@ -47,34 +48,46 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat",    "defensePointDamageFactorCap"      },
 		{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat",    "defensePointDamageFactorCap"      },
 		{EGameSettings::COMBAT_GOOD_LUCK_DICE,                  "combat",    "goodLuckDice"                     },
 		{EGameSettings::COMBAT_GOOD_LUCK_DICE,                  "combat",    "goodLuckDice"                     },
 		{EGameSettings::COMBAT_GOOD_MORALE_DICE,                "combat",    "goodMoraleDice"                   },
 		{EGameSettings::COMBAT_GOOD_MORALE_DICE,                "combat",    "goodMoraleDice"                   },
+		{EGameSettings::COMBAT_LAYOUTS,                         "combat",    "layouts"                          },
 		{EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,      "combat",    "oneHexTriggersObstacles"          },
 		{EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,      "combat",    "oneHexTriggersObstacles"          },
 		{EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH,   "creatures", "allowAllForDoubleMonth"           },
 		{EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH,   "creatures", "allowAllForDoubleMonth"           },
 		{EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS,   "creatures", "allowRandomSpecialWeeks"          },
 		{EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS,   "creatures", "allowRandomSpecialWeeks"          },
 		{EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE,       "creatures", "dailyStackExperience"             },
 		{EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE,       "creatures", "dailyStackExperience"             },
 		{EGameSettings::CREATURES_WEEKLY_GROWTH_CAP,            "creatures", "weeklyGrowthCap"                  },
 		{EGameSettings::CREATURES_WEEKLY_GROWTH_CAP,            "creatures", "weeklyGrowthCap"                  },
 		{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT,        "creatures", "weeklyGrowthPercent"              },
 		{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT,        "creatures", "weeklyGrowthPercent"              },
+		{EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE,    "spells",    "dimensionDoorExposesTerrainType"  },
+		{EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS,   "spells",    "dimensionDoorFailureSpendsPoints" },
+		{EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells",    "dimensionDoorOnlyToUncoveredTiles"},
+		{EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,  "spells",    "dimensionDoorTournamentRulesLimit"},
+		{EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS,         "spells",    "dimensionDoorTriggersGuards"      },
 		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,      "dwellings", "accumulateWhenNeutral"            },
 		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,      "dwellings", "accumulateWhenNeutral"            },
 		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED,        "dwellings", "accumulateWhenOwned"              },
 		{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED,        "dwellings", "accumulateWhenOwned"              },
 		{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT,             "dwellings", "mergeOnRecruit"                   },
 		{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT,             "dwellings", "mergeOnRecruit"                   },
+		{EGameSettings::HEROES_BACKPACK_CAP,                    "heroes",    "backpackSize"                     },
+		{EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS,          "heroes",    "minimalPrimarySkills"             },
 		{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP,           "heroes",    "perPlayerOnMapCap"                },
 		{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP,           "heroes",    "perPlayerOnMapCap"                },
 		{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP,            "heroes",    "perPlayerTotalCap"                },
 		{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP,            "heroes",    "perPlayerTotalCap"                },
 		{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,   "heroes",    "retreatOnWinWithoutTroops"        },
 		{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,   "heroes",    "retreatOnWinWithoutTroops"        },
 		{EGameSettings::HEROES_STARTING_STACKS_CHANCES,         "heroes",    "startingStackChances"             },
 		{EGameSettings::HEROES_STARTING_STACKS_CHANCES,         "heroes",    "startingStackChances"             },
-		{EGameSettings::HEROES_BACKPACK_CAP,                    "heroes",    "backpackSize"                     },
 		{EGameSettings::HEROES_TAVERN_INVITE,                   "heroes",    "tavernInvite"                     },
 		{EGameSettings::HEROES_TAVERN_INVITE,                   "heroes",    "tavernInvite"                     },
-		{EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS,          "heroes",    "minimalPrimarySkills"             },
-		{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA,      "mapFormat", "restorationOfErathia"             },
 		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,           "mapFormat", "armageddonsBlade"                 },
 		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,           "mapFormat", "armageddonsBlade"                 },
-		{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH,             "mapFormat", "shadowOfDeath"                    },
 		{EGameSettings::MAP_FORMAT_CHRONICLES,                  "mapFormat", "chronicles"                       },
 		{EGameSettings::MAP_FORMAT_CHRONICLES,                  "mapFormat", "chronicles"                       },
 		{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS,           "mapFormat", "hornOfTheAbyss"                   },
 		{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS,           "mapFormat", "hornOfTheAbyss"                   },
 		{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS,         "mapFormat", "inTheWakeOfGods"                  },
 		{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS,         "mapFormat", "inTheWakeOfGods"                  },
 		{EGameSettings::MAP_FORMAT_JSON_VCMI,                   "mapFormat", "jsonVCMI"                         },
 		{EGameSettings::MAP_FORMAT_JSON_VCMI,                   "mapFormat", "jsonVCMI"                         },
+		{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA,      "mapFormat", "restorationOfErathia"             },
+		{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH,             "mapFormat", "shadowOfDeath"                    },
 		{EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD,    "markets",   "blackMarketRestockPeriod"         },
 		{EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD,    "markets",   "blackMarketRestockPeriod"         },
-		{EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION,          "banks",     "showGuardsComposition"            },
 		{EGameSettings::MODULE_COMMANDERS,                      "modules",   "commanders"                       },
 		{EGameSettings::MODULE_COMMANDERS,                      "modules",   "commanders"                       },
 		{EGameSettings::MODULE_STACK_ARTIFACT,                  "modules",   "stackArtifact"                    },
 		{EGameSettings::MODULE_STACK_ARTIFACT,                  "modules",   "stackArtifact"                    },
 		{EGameSettings::MODULE_STACK_EXPERIENCE,                "modules",   "stackExperience"                  },
 		{EGameSettings::MODULE_STACK_EXPERIENCE,                "modules",   "stackExperience"                  },
+		{EGameSettings::PATHFINDER_IGNORE_GUARDS,               "pathfinder", "ignoreGuards"                    },
+		{EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES,          "pathfinder", "originalFlyRules"                },
+		{EGameSettings::PATHFINDER_USE_BOAT,                    "pathfinder", "useBoat"                         },
+		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom"         },
+		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique"         },
+		{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY,        "pathfinder", "useMonolithTwoWay"               },
+		{EGameSettings::PATHFINDER_USE_WHIRLPOOL,               "pathfinder", "useWhirlpool"                    },
 		{EGameSettings::TEXTS_ARTIFACT,                         "textData",  "artifact"                         },
 		{EGameSettings::TEXTS_ARTIFACT,                         "textData",  "artifact"                         },
 		{EGameSettings::TEXTS_CREATURE,                         "textData",  "creature"                         },
 		{EGameSettings::TEXTS_CREATURE,                         "textData",  "creature"                         },
 		{EGameSettings::TEXTS_FACTION,                          "textData",  "faction"                          },
 		{EGameSettings::TEXTS_FACTION,                          "textData",  "faction"                          },
@@ -85,18 +98,6 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::TEXTS_ROAD,                             "textData",  "road"                             },
 		{EGameSettings::TEXTS_ROAD,                             "textData",  "road"                             },
 		{EGameSettings::TEXTS_SPELL,                            "textData",  "spell"                            },
 		{EGameSettings::TEXTS_SPELL,                            "textData",  "spell"                            },
 		{EGameSettings::TEXTS_TERRAIN,                          "textData",  "terrain"                          },
 		{EGameSettings::TEXTS_TERRAIN,                          "textData",  "terrain"                          },
-		{EGameSettings::PATHFINDER_IGNORE_GUARDS,               "pathfinder", "ignoreGuards"                    },
-		{EGameSettings::PATHFINDER_USE_BOAT,                    "pathfinder", "useBoat"                         },
-		{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY,        "pathfinder", "useMonolithTwoWay"               },
-		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique"         },
-		{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom"         },
-		{EGameSettings::PATHFINDER_USE_WHIRLPOOL,               "pathfinder", "useWhirlpool"                    },
-		{EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES,          "pathfinder", "originalFlyRules"                },
-		{EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells",    "dimensionDoorOnlyToUncoveredTiles"},
-		{EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE,    "spells",    "dimensionDoorExposesTerrainType"  },
-		{EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS,   "spells",    "dimensionDoorFailureSpendsPoints" },
-		{EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS,         "spells",    "dimensionDoorTriggersGuards"      },
-		{EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,  "spells",    "dimensionDoorTournamentRulesLimit"},
 		{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP,           "towns",     "buildingsPerTurnCap"              },
 		{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP,           "towns",     "buildingsPerTurnCap"              },
 		{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES,        "towns",     "startingDwellingChances"          },
 		{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES,        "towns",     "startingDwellingChances"          },
 	};
 	};

+ 3 - 3
lib/IGameCallback.h

@@ -28,6 +28,7 @@ struct TeleportDialog;
 struct StackLocation;
 struct StackLocation;
 struct ArtifactLocation;
 struct ArtifactLocation;
 struct BankConfig;
 struct BankConfig;
+struct BattleLayout;
 class CCreatureSet;
 class CCreatureSet;
 class CStackBasicDescriptor;
 class CStackBasicDescriptor;
 class CGCreature;
 class CGCreature;
@@ -128,9 +129,8 @@ public:
 	virtual void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0;
 	virtual void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0;
 	virtual void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero)=0;
 	virtual void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero)=0;
 	virtual void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0;
 	virtual void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0;
-	virtual void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr)=0; //use hero=nullptr for no hero
-	virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false)=0; //if any of armies is hero, hero will be used
-	virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false)=0; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
+	virtual void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town)=0; //use hero=nullptr for no hero
+	virtual void startBattle(const CArmedInstance *army1, const CArmedInstance *army2)=0; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
 	virtual bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveMove, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL)=0;
 	virtual bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode moveMove, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL)=0;
 	virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0;
 	virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0;
 	virtual void giveHeroBonus(GiveBonus * bonus)=0;
 	virtual void giveHeroBonus(GiveBonus * bonus)=0;

+ 24 - 23
lib/IGameSettings.h

@@ -15,6 +15,7 @@ class JsonNode;
 
 
 enum class EGameSettings
 enum class EGameSettings
 {
 {
+	BANKS_SHOW_GUARDS_COMPOSITION,
 	BONUSES_GLOBAL,
 	BONUSES_GLOBAL,
 	BONUSES_PER_HERO,
 	BONUSES_PER_HERO,
 	COMBAT_ATTACK_POINT_DAMAGE_FACTOR,
 	COMBAT_ATTACK_POINT_DAMAGE_FACTOR,
@@ -25,26 +26,46 @@ enum class EGameSettings
 	COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP,
 	COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP,
 	COMBAT_GOOD_LUCK_DICE,
 	COMBAT_GOOD_LUCK_DICE,
 	COMBAT_GOOD_MORALE_DICE,
 	COMBAT_GOOD_MORALE_DICE,
+	COMBAT_LAYOUTS,
+	COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,
 	CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH,
 	CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH,
 	CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS,
 	CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS,
 	CREATURES_DAILY_STACK_EXPERIENCE,
 	CREATURES_DAILY_STACK_EXPERIENCE,
 	CREATURES_WEEKLY_GROWTH_CAP,
 	CREATURES_WEEKLY_GROWTH_CAP,
 	CREATURES_WEEKLY_GROWTH_PERCENT,
 	CREATURES_WEEKLY_GROWTH_PERCENT,
+	DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE,
+	DIMENSION_DOOR_FAILURE_SPENDS_POINTS,
+	DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES,
+	DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,
+	DIMENSION_DOOR_TRIGGERS_GUARDS,
 	DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,
 	DWELLINGS_ACCUMULATE_WHEN_NEUTRAL,
 	DWELLINGS_ACCUMULATE_WHEN_OWNED,
 	DWELLINGS_ACCUMULATE_WHEN_OWNED,
 	DWELLINGS_MERGE_ON_RECRUIT,
 	DWELLINGS_MERGE_ON_RECRUIT,
+	HEROES_BACKPACK_CAP,
+	HEROES_MINIMAL_PRIMARY_SKILLS,
 	HEROES_PER_PLAYER_ON_MAP_CAP,
 	HEROES_PER_PLAYER_ON_MAP_CAP,
 	HEROES_PER_PLAYER_TOTAL_CAP,
 	HEROES_PER_PLAYER_TOTAL_CAP,
 	HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,
 	HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,
 	HEROES_STARTING_STACKS_CHANCES,
 	HEROES_STARTING_STACKS_CHANCES,
-	HEROES_BACKPACK_CAP,
 	HEROES_TAVERN_INVITE,
 	HEROES_TAVERN_INVITE,
-	HEROES_MINIMAL_PRIMARY_SKILLS,
+	MAP_FORMAT_ARMAGEDDONS_BLADE,
+	MAP_FORMAT_CHRONICLES,
+	MAP_FORMAT_HORN_OF_THE_ABYSS,
+	MAP_FORMAT_IN_THE_WAKE_OF_GODS,
+	MAP_FORMAT_JSON_VCMI,
+	MAP_FORMAT_RESTORATION_OF_ERATHIA,
+	MAP_FORMAT_SHADOW_OF_DEATH,
 	MARKETS_BLACK_MARKET_RESTOCK_PERIOD,
 	MARKETS_BLACK_MARKET_RESTOCK_PERIOD,
-	BANKS_SHOW_GUARDS_COMPOSITION,
 	MODULE_COMMANDERS,
 	MODULE_COMMANDERS,
 	MODULE_STACK_ARTIFACT,
 	MODULE_STACK_ARTIFACT,
 	MODULE_STACK_EXPERIENCE,
 	MODULE_STACK_EXPERIENCE,
+	PATHFINDER_IGNORE_GUARDS,
+	PATHFINDER_ORIGINAL_FLY_RULES,
+	PATHFINDER_USE_BOAT,
+	PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM,
+	PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE,
+	PATHFINDER_USE_MONOLITH_TWO_WAY,
+	PATHFINDER_USE_WHIRLPOOL,
 	TEXTS_ARTIFACT,
 	TEXTS_ARTIFACT,
 	TEXTS_CREATURE,
 	TEXTS_CREATURE,
 	TEXTS_FACTION,
 	TEXTS_FACTION,
@@ -55,28 +76,8 @@ enum class EGameSettings
 	TEXTS_ROAD,
 	TEXTS_ROAD,
 	TEXTS_SPELL,
 	TEXTS_SPELL,
 	TEXTS_TERRAIN,
 	TEXTS_TERRAIN,
-	MAP_FORMAT_RESTORATION_OF_ERATHIA,
-	MAP_FORMAT_ARMAGEDDONS_BLADE,
-	MAP_FORMAT_SHADOW_OF_DEATH,
-	MAP_FORMAT_CHRONICLES,
-	MAP_FORMAT_HORN_OF_THE_ABYSS,
-	MAP_FORMAT_JSON_VCMI,
-	MAP_FORMAT_IN_THE_WAKE_OF_GODS,
-	PATHFINDER_USE_BOAT,
-	PATHFINDER_IGNORE_GUARDS,
-	PATHFINDER_USE_MONOLITH_TWO_WAY,
-	PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE,
-	PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM,
-	PATHFINDER_USE_WHIRLPOOL,
-	PATHFINDER_ORIGINAL_FLY_RULES,
 	TOWNS_BUILDINGS_PER_TURN_CAP,
 	TOWNS_BUILDINGS_PER_TURN_CAP,
 	TOWNS_STARTING_DWELLING_CHANCES,
 	TOWNS_STARTING_DWELLING_CHANCES,
-	COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,
-	DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES,
-	DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE,
-	DIMENSION_DOOR_FAILURE_SPENDS_POINTS,
-	DIMENSION_DOOR_TRIGGERS_GUARDS,
-	DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT,
 
 
 	OPTIONS_COUNT,
 	OPTIONS_COUNT,
 	OPTIONS_BEGIN = BONUSES_GLOBAL
 	OPTIONS_BEGIN = BONUSES_GLOBAL

+ 48 - 94
lib/battle/BattleInfo.cpp

@@ -9,6 +9,8 @@
  */
  */
 #include "StdInc.h"
 #include "StdInc.h"
 #include "BattleInfo.h"
 #include "BattleInfo.h"
+
+#include "BattleLayout.h"
 #include "CObstacleInstance.h"
 #include "CObstacleInstance.h"
 #include "bonuses/Limiters.h"
 #include "bonuses/Limiters.h"
 #include "bonuses/Updaters.h"
 #include "bonuses/Updaters.h"
@@ -74,22 +76,6 @@ void BattleInfo::localInit()
 	exportBonuses();
 	exportBonuses();
 }
 }
 
 
-namespace CGH
-{
-	static void readBattlePositions(const JsonNode &node, std::vector< std::vector<int> > & dest)
-	{
-		for(const JsonNode &level : node.Vector())
-		{
-			std::vector<int> pom;
-			for(const JsonNode &value : level.Vector())
-			{
-				pom.push_back(static_cast<int>(value.Float()));
-			}
-
-			dest.push_back(pom);
-		}
-	}
-}
 
 
 //RNG that works like H3 one
 //RNG that works like H3 one
 struct RandGen
 struct RandGen
@@ -173,22 +159,20 @@ struct RangeGenerator
 	std::function<int()> myRand;
 	std::function<int()> myRand;
 };
 };
 
 
-BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, bool creatureBank, const CGTownInstance * town)
+BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, const BattleLayout & layout, const CGTownInstance * town)
 {
 {
 	CMP_stack cmpst;
 	CMP_stack cmpst;
-	auto * curB = new BattleInfo();
+	auto * curB = new BattleInfo(layout);
 
 
 	for(auto i : { BattleSide::LEFT_SIDE, BattleSide::RIGHT_SIDE})
 	for(auto i : { BattleSide::LEFT_SIDE, BattleSide::RIGHT_SIDE})
 		curB->sides[i].init(heroes[i], armies[i]);
 		curB->sides[i].init(heroes[i], armies[i]);
 
 
-
 	std::vector<CStack*> & stacks = (curB->stacks);
 	std::vector<CStack*> & stacks = (curB->stacks);
 
 
 	curB->tile = tile;
 	curB->tile = tile;
 	curB->battlefieldType = battlefieldType;
 	curB->battlefieldType = battlefieldType;
 	curB->round = -2;
 	curB->round = -2;
 	curB->activeStack = -1;
 	curB->activeStack = -1;
-	curB->creatureBank = creatureBank;
 	curB->replayAllowed = false;
 	curB->replayAllowed = false;
 
 
 	if(town)
 	if(town)
@@ -225,7 +209,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	}
 	}
 
 
 	//randomize obstacles
 	//randomize obstacles
- 	if (town == nullptr && !creatureBank) //do it only when it's not siege and not creature bank
+	if (layout.obstaclesAllowed)
  	{
  	{
 		RandGen r{};
 		RandGen r{};
 		auto ourRand = [&](){ return r.rand(); };
 		auto ourRand = [&](){ return r.rand(); };
@@ -321,63 +305,40 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 		}
 		}
 	}
 	}
 
 
-	//reading battleStartpos - add creatures AFTER random obstacles are generated
-	//TODO: parse once to some structure
-	BattleSideArray<std::vector<std::vector<int>>> looseFormations;
-	BattleSideArray<std::vector<std::vector<int>>> tightFormations;
-	BattleSideArray<std::vector<std::vector<int>>> creBankFormations;
-	BattleSideArray<int> commanderField;
-	BattleSideArray<int> commanderBank;
-	const JsonNode config(JsonPath::builtin("config/battleStartpos.json"));
-	const JsonVector &positions = config["battle_positions"].Vector();
-
-	CGH::readBattlePositions(positions[0]["levels"], looseFormations[BattleSide::ATTACKER]);
-	CGH::readBattlePositions(positions[1]["levels"], looseFormations[BattleSide::DEFENDER]);
-	CGH::readBattlePositions(positions[2]["levels"], tightFormations[BattleSide::ATTACKER]);
-	CGH::readBattlePositions(positions[3]["levels"], tightFormations[BattleSide::DEFENDER]);
-	CGH::readBattlePositions(positions[4]["levels"], creBankFormations[BattleSide::ATTACKER]);
-	CGH::readBattlePositions(positions[5]["levels"], creBankFormations[BattleSide::DEFENDER]);
-
-	commanderField[BattleSide::ATTACKER] = config["commanderPositions"]["field"][0].Integer();
-	commanderField[BattleSide::DEFENDER] = config["commanderPositions"]["field"][1].Integer();
-
-	commanderBank[BattleSide::ATTACKER] = config["commanderPositions"]["creBank"][0].Integer();
-	commanderBank[BattleSide::DEFENDER] = config["commanderPositions"]["creBank"][1].Integer();
-
 	//adding war machines
 	//adding war machines
-	if(!creatureBank)
+	//Checks if hero has artifact and create appropriate stack
+	auto handleWarMachine = [&](BattleSide side, const ArtifactPosition & artslot, BattleHex hex)
 	{
 	{
-		//Checks if hero has artifact and create appropriate stack
-		auto handleWarMachine = [&](BattleSide side, const ArtifactPosition & artslot, BattleHex hex)
+		const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot);
+
+		if(nullptr != warMachineArt && hex.isValid())
 		{
 		{
-			const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot);
+			CreatureID cre = warMachineArt->artType->getWarMachine();
 
 
-			if(nullptr != warMachineArt)
-			{
-				CreatureID cre = warMachineArt->artType->getWarMachine();
+			if(cre != CreatureID::NONE)
+				curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex);
+		}
+	};
 
 
-				if(cre != CreatureID::NONE)
-					curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex);
-			}
-		};
+	if(heroes[BattleSide::ATTACKER])
+	{
+		auto warMachineHexes = layout.warMachines.at(BattleSide::ATTACKER);
 
 
-		if(heroes[BattleSide::ATTACKER])
-		{
+		handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH1, warMachineHexes.at(0));
+		handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH2, warMachineHexes.at(1));
+		handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH3, warMachineHexes.at(2));
+		if(town && town->fortificationsLevel().wallsHealth > 0)
+			handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH4, warMachineHexes.at(3));
+	}
 
 
-			handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH1, 52);
-			handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH2, 18);
-			handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH3, 154);
-			if(town && town->fortificationsLevel().wallsHealth > 0)
-				handleWarMachine(BattleSide::ATTACKER, ArtifactPosition::MACH4, 120);
-		}
+	if(heroes[BattleSide::DEFENDER])
+	{
+		auto warMachineHexes = layout.warMachines.at(BattleSide::DEFENDER);
 
 
-		if(heroes[BattleSide::DEFENDER])
-		{
-			if(!town) //defending hero shouldn't receive ballista (bug #551)
-				handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH1, 66);
-			handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH2, 32);
-			handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH3, 168);
-		}
+		if(!town) //defending hero shouldn't receive ballista (bug #551)
+			handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH1, warMachineHexes.at(0));
+		handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH2, warMachineHexes.at(1));
+		handleWarMachine(BattleSide::DEFENDER, ArtifactPosition::MACH3, warMachineHexes.at(2));
 	}
 	}
 	//war machines added
 	//war machines added
 
 
@@ -390,20 +351,10 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 		int k = 0; //stack serial
 		int k = 0; //stack serial
 		for(auto i = armies[side]->Slots().begin(); i != armies[side]->Slots().end(); i++, k++)
 		for(auto i = armies[side]->Slots().begin(); i != armies[side]->Slots().end(); i++, k++)
 		{
 		{
-			std::vector<int> *formationVector = nullptr;
-			if(armies[side]->formation == EArmyFormation::TIGHT )
-				formationVector = &tightFormations[side][formationNo];
-			else
-				formationVector = &looseFormations[side][formationNo];
+			const BattleHex & pos = layout.units.at(side).at(k);
 
 
-			if(creatureBank)
-				formationVector = &creBankFormations[side][formationNo];
-
-			BattleHex pos = (k < formationVector->size() ? formationVector->at(k) : 0);
-			if(creatureBank && i->second->type->isDoubleWide())
-				pos += side == BattleSide::RIGHT_SIDE ? BattleHex::LEFT : BattleHex::RIGHT;
-
-			curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos);
+			if (pos.isValid())
+				curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos);
 		}
 		}
 	}
 	}
 
 
@@ -412,9 +363,8 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	{
 	{
 		if (heroes[i] && heroes[i]->commander && heroes[i]->commander->alive)
 		if (heroes[i] && heroes[i]->commander && heroes[i]->commander->alive)
 		{
 		{
-			curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, creatureBank ? commanderBank[i] : commanderField[i]);
+			curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, layout.commanders.at(i));
 		}
 		}
-
 	}
 	}
 
 
 	if (curB->town)
 	if (curB->town)
@@ -453,8 +403,6 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	//////////////////////////////////////////////////////////////////////////
 	//////////////////////////////////////////////////////////////////////////
 
 
 	//tactics
 	//tactics
-	bool isTacticsAllowed = !creatureBank; //no tactics in creature banks
-
 	BattleSideArray<int> battleRepositionHex = {};
 	BattleSideArray<int> battleRepositionHex = {};
 	BattleSideArray<int> battleRepositionHexBlock = {};
 	BattleSideArray<int> battleRepositionHexBlock = {};
 	for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER})
 	for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER})
@@ -475,7 +423,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	   double tactics will be implemented.
 	   double tactics will be implemented.
 	*/
 	*/
 
 
-	if(isTacticsAllowed)
+	if(layout.tacticsAllowed)
 	{
 	{
 		if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0)
 		if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0)
 			logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!");
 			logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!");
@@ -523,7 +471,14 @@ CStack * BattleInfo::getStack(int stackID, bool onlyAlive)
 	return const_cast<CStack *>(battleGetStackByID(stackID, onlyAlive));
 	return const_cast<CStack *>(battleGetStackByID(stackID, onlyAlive));
 }
 }
 
 
+BattleInfo::BattleInfo(const BattleLayout & layout):
+	BattleInfo()
+{
+	*this->layout = layout;
+}
+
 BattleInfo::BattleInfo():
 BattleInfo::BattleInfo():
+	layout(std::make_unique<BattleLayout>()),
 	round(-1),
 	round(-1),
 	activeStack(-1),
 	activeStack(-1),
 	town(nullptr),
 	town(nullptr),
@@ -535,6 +490,11 @@ BattleInfo::BattleInfo():
 	setNodeType(BATTLE);
 	setNodeType(BATTLE);
 }
 }
 
 
+BattleLayout BattleInfo::getLayout() const
+{
+	return *layout;
+}
+
 BattleID BattleInfo::getBattleID() const
 BattleID BattleInfo::getBattleID() const
 {
 {
 	return battleID;
 	return battleID;
@@ -679,12 +639,6 @@ int3 BattleInfo::getLocation() const
 	return tile;
 	return tile;
 }
 }
 
 
-bool BattleInfo::isCreatureBank() const
-{
-	return creatureBank;
-}
-
-
 std::vector<SpellID> BattleInfo::getUsedSpells(BattleSide side) const
 std::vector<SpellID> BattleInfo::getUsedSpells(BattleSide side) const
 {
 {
 	return getSide(side).usedSpellsHistory;
 	return getSide(side).usedSpellsHistory;

+ 5 - 3
lib/battle/BattleInfo.h

@@ -22,10 +22,12 @@ class CStack;
 class CStackInstance;
 class CStackInstance;
 class CStackBasicDescriptor;
 class CStackBasicDescriptor;
 class BattleField;
 class BattleField;
+struct BattleLayout;
 
 
 class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState
 class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState
 {
 {
 	BattleSideArray<SideInBattle> sides; //sides[0] - attacker, sides[1] - defender
 	BattleSideArray<SideInBattle> sides; //sides[0] - attacker, sides[1] - defender
+	std::unique_ptr<BattleLayout> layout;
 public:
 public:
 	BattleID battleID = BattleID(0);
 	BattleID battleID = BattleID(0);
 
 
@@ -33,7 +35,6 @@ public:
 	si32 activeStack;
 	si32 activeStack;
 	const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege)
 	const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege)
 	int3 tile; //for background and bonuses
 	int3 tile; //for background and bonuses
-	bool creatureBank; //auxiliary field, do not serialize
 	bool replayAllowed;
 	bool replayAllowed;
 	std::vector<CStack*> stacks;
 	std::vector<CStack*> stacks;
 	std::vector<std::shared_ptr<CObstacleInstance> > obstacles;
 	std::vector<std::shared_ptr<CObstacleInstance> > obstacles;
@@ -65,6 +66,7 @@ public:
 	}
 	}
 
 
 	//////////////////////////////////////////////////////////////////////////
 	//////////////////////////////////////////////////////////////////////////
+	BattleInfo(const BattleLayout & layout);
 	BattleInfo();
 	BattleInfo();
 	virtual ~BattleInfo();
 	virtual ~BattleInfo();
 
 
@@ -108,7 +110,7 @@ public:
 	int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
 	int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
 
 
 	int3 getLocation() const override;
 	int3 getLocation() const override;
-	bool isCreatureBank() const override;
+	BattleLayout getLayout() const override;
 
 
 	std::vector<SpellID> getUsedSpells(BattleSide side) const override;
 	std::vector<SpellID> getUsedSpells(BattleSide side) const override;
 
 
@@ -152,7 +154,7 @@ public:
 	const CGHeroInstance * getHero(const PlayerColor & player) const; //returns fighting hero that belongs to given player
 	const CGHeroInstance * getHero(const PlayerColor & player) const; //returns fighting hero that belongs to given player
 
 
 	void localInit();
 	void localInit();
-	static BattleInfo * setupBattle(const int3 & tile, TerrainId, const BattleField & battlefieldType, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, bool creatureBank, const CGTownInstance * town);
+	static BattleInfo * setupBattle(const int3 & tile, TerrainId, const BattleField & battlefieldType, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, const BattleLayout & layout, const CGTownInstance * town);
 
 
 	BattleSide whatSide(const PlayerColor & player) const;
 	BattleSide whatSide(const PlayerColor & player) const;
 
 

+ 81 - 0
lib/battle/BattleLayout.cpp

@@ -0,0 +1,81 @@
+/*
+ * BattleLayout.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 "BattleLayout.h"
+
+#include "../GameSettings.h"
+#include "../IGameCallback.h"
+#include "../VCMI_Lib.h"
+#include "../json/JsonNode.h"
+#include "../mapObjects/CArmedInstance.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+BattleLayout BattleLayout::createDefaultLayout(IGameCallback * cb, const CArmedInstance * attacker, const CArmedInstance * defender)
+{
+	return createLayout(cb, "default", attacker, defender);
+}
+
+BattleLayout BattleLayout::createLayout(IGameCallback * cb, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender)
+{
+	const auto & loadHex = [](const JsonNode & node)
+	{
+		if (node.isNull())
+			return BattleHex();
+		else
+			return BattleHex(node.Integer());
+	};
+
+	const auto & loadUnits = [](const JsonNode & node)
+	{
+		UnitsArrayType::value_type result;
+		for (size_t i = 0; i < GameConstants::ARMY_SIZE; ++i)
+		{
+			if (!node[i].isNull())
+				result[i] = BattleHex(node[i].Integer());
+		}
+		return result;
+	};
+
+	const JsonNode & configRoot = cb->getSettings().getValue(EGameSettings::COMBAT_LAYOUTS);
+	const JsonNode & config = configRoot[layoutName];
+
+	BattleLayout result;
+
+	result.commanders[BattleSide::ATTACKER] = loadHex(config["attackerCommander"]);
+	result.commanders[BattleSide::DEFENDER] = loadHex(config["defenderCommander"]);
+
+	for (size_t i = 0; i < 4; ++i)
+		result.warMachines[BattleSide::ATTACKER][i] = loadHex(config["attackerWarMachines"][i]);
+
+	for (size_t i = 0; i < 4; ++i)
+		result.warMachines[BattleSide::DEFENDER][i] = loadHex(config["attackerWarMachines"][i]);
+
+	if (attacker->formation == EArmyFormation::LOOSE && !config["attackerUnitsLoose"].isNull())
+		result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsLoose"][attacker->stacksCount() - 1]);
+	else if (attacker->formation == EArmyFormation::TIGHT && !config["attackerUnitsTight"].isNull())
+		result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnitsTight"][attacker->stacksCount() - 1]);
+	else
+		result.units[BattleSide::ATTACKER] = loadUnits(config["attackerUnits"]);
+
+	if (attacker->formation == EArmyFormation::LOOSE && !config["defenderUnitsLoose"].isNull())
+		result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsLoose"][attacker->stacksCount() - 1]);
+	else if (attacker->formation == EArmyFormation::TIGHT && !config["defenderUnitsTight"].isNull())
+		result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnitsTight"][attacker->stacksCount() - 1]);
+	else
+		result.units[BattleSide::DEFENDER] = loadUnits(config["defenderUnits"]);
+
+	result.obstaclesAllowed = config["obstaclesAllowed"].Bool();
+	result.tacticsAllowed = config["tacticsAllowed"].Bool();
+
+	return result;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 39 - 0
lib/battle/BattleLayout.h

@@ -0,0 +1,39 @@
+/*
+ * BattleLayout.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
+ *
+ */
+#pragma once
+
+#include "BattleHex.h"
+#include "BattleSide.h"
+#include "../constants/NumericConstants.h"
+#include "../constants/Enumerations.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CArmedInstance;
+class IGameCallback;
+
+struct DLL_EXPORT BattleLayout
+{
+	using UnitsArrayType = BattleSideArray<std::array<BattleHex, GameConstants::ARMY_SIZE>>;
+	using MachinesArrayType = BattleSideArray<std::array<BattleHex, 4>>;
+	using CommanderArrayType = BattleSideArray<BattleHex>;
+
+	UnitsArrayType units;
+	MachinesArrayType warMachines;
+	CommanderArrayType commanders;
+
+	bool tacticsAllowed = false;
+	bool obstaclesAllowed = false;
+
+	static BattleLayout createDefaultLayout(IGameCallback * cb, const CArmedInstance * attacker, const CArmedInstance * defender);
+	static BattleLayout createLayout(IGameCallback * cb, const std::string & layoutName, const CArmedInstance * attacker, const CArmedInstance * defender);
+};
+
+VCMI_LIB_NAMESPACE_END

+ 2 - 1
lib/battle/IBattleState.h

@@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class ObstacleChanges;
 class ObstacleChanges;
 class UnitChanges;
 class UnitChanges;
 struct Bonus;
 struct Bonus;
+struct BattleLayout;
 class JsonNode;
 class JsonNode;
 class JsonSerializeFormat;
 class JsonSerializeFormat;
 class BattleField;
 class BattleField;
@@ -72,7 +73,7 @@ public:
 	virtual int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0;
 	virtual int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0;
 
 
 	virtual int3 getLocation() const = 0;
 	virtual int3 getLocation() const = 0;
-	virtual bool isCreatureBank() const = 0;
+	virtual BattleLayout getLayout() const = 0;
 };
 };
 
 
 class DLL_LINKAGE IBattleState : public IBattleInfo
 class DLL_LINKAGE IBattleState : public IBattleInfo

+ 13 - 10
lib/mapObjectConstructors/CRewardableConstructor.cpp

@@ -62,17 +62,20 @@ Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCal
 
 
 void CRewardableConstructor::configureObject(CGObjectInstance * object, vstd::RNG & rng) const
 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 - 1
lib/mapObjectConstructors/CommonConstructors.h

@@ -26,7 +26,6 @@ class CGHeroInstance;
 class CGMarket;
 class CGMarket;
 class CHeroClass;
 class CHeroClass;
 class CGCreature;
 class CGCreature;
-class CBank;
 class CGBoat;
 class CGBoat;
 class CFaction;
 class CFaction;
 class CStackBasicDescriptor;
 class CStackBasicDescriptor;

+ 15 - 130
lib/mapObjects/CBank.cpp

@@ -15,7 +15,6 @@
 #include <vcmi/spells/Service.h>
 #include <vcmi/spells/Service.h>
 
 
 #include "../texts/CGeneralTextHandler.h"
 #include "../texts/CGeneralTextHandler.h"
-#include "../CSoundBase.h"
 #include "../IGameSettings.h"
 #include "../IGameSettings.h"
 #include "../CPlayerState.h"
 #include "../CPlayerState.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
@@ -138,140 +137,31 @@ bool CBank::wasVisited (PlayerColor player) const
 
 
 void CBank::onHeroVisit(const CGHeroInstance * h) const
 void CBank::onHeroVisit(const CGHeroInstance * h) const
 {
 {
-	ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id);
+	ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id);
 	cb->sendAndApply(&cov);
 	cb->sendAndApply(&cov);
 
 
-	if(!bankConfig && (ID.toEnum() == Obj::CREATURE_BANK || ID.toEnum() == Obj::DRAGON_UTOPIA))
-	{
-		blockingDialogAnswered(h, 1);
-		return;
-	}
-
-	int banktext = 0;
-	switch (ID.toEnum())
-	{
-	case Obj::DERELICT_SHIP:
-		banktext = 41;
-		break;
-	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);
 	BlockingDialog bd(true, false);
 	bd.player = h->getOwner();
 	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.text.appendLocalString(EMetaText::ADVOB_TXT, 32);
 	bd.components = getPopupComponents(h->getOwner());
 	bd.components = getPopupComponents(h->getOwner());
-	if (banktext == 32)
-		bd.text.replaceRawString(getObjectName());
-
+	bd.text.replaceRawString(getObjectName());
 	cb->showBlockingDialog(this, &bd);
 	cb->showBlockingDialog(this, &bd);
 }
 }
 
 
 void CBank::doVisit(const CGHeroInstance * hero) const
 void CBank::doVisit(const CGHeroInstance * hero) const
 {
 {
-	int textID = -1;
 	InfoWindow iw;
 	InfoWindow iw;
 	iw.type = EInfoWindowMode::AUTO;
 	iw.type = EInfoWindowMode::AUTO;
 	iw.player = hero->getOwner();
 	iw.player = hero->getOwner();
 	MetaString loot;
 	MetaString loot;
 
 
-	if (bankConfig)
-	{
-		switch (ID.toEnum())
-		{
-		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;
-		}
-	}
-	else
+	if (!bankConfig)
 	{
 	{
-		switch (ID.toEnum())
-		{
-		case Obj::SHIPWRECK:
-		case Obj::DERELICT_SHIP:
-		case Obj::CRYPT:
-		{
-			GiveBonus gbonus;
-			gbonus.id = hero->id;
-			gbonus.bonus.duration = BonusDuration::ONE_BATTLE;
-			gbonus.bonus.source = BonusSource::OBJECT_TYPE;
-			gbonus.bonus.sid = BonusSourceID(ID);
-			gbonus.bonus.type = BonusType::MORALE;
-			gbonus.bonus.val = -1;
-			switch (ID.toEnum())
-			{
-			case Obj::SHIPWRECK:
-				textID = 123;
-				gbonus.bonus.description = MetaString::createFromTextID("core.arraytxt.99");
-				break;
-			case Obj::DERELICT_SHIP:
-				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:
-		{
-			GiveBonus gb;
-			gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, -2, BonusSourceID(id));
-			gb.bonus.description = MetaString::createFromTextID("core.arraytxt.70");
-			gb.id = hero->id;
-			cb->giveHeroBonus(&gb);
-			textID = 107;
-			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());
-		}
-		if(textID != -1)
-		{
-			iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID);
-		}
+		iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty
+		iw.text.replaceRawString(getObjectName());
 		cb->showInfoDialog(&iw);
 		cb->showInfoDialog(&iw);
 	}
 	}
 
 
-
 	//grant resources
 	//grant resources
 	if (bankConfig)
 	if (bankConfig)
 	{
 	{
@@ -297,17 +187,15 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 		//display loot
 		//display loot
 		if (!iw.components.empty())
 		if (!iw.components.empty())
 		{
 		{
-			iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID);
-			if (textID == 34)
+			iw.text.appendLocalString(EMetaText::ADVOB_TXT, 34);
+			const auto * strongest = boost::range::max_element(bankConfig->guards, [](const CStackBasicDescriptor & a, const CStackBasicDescriptor & b)
 			{
 			{
-				const auto * strongest = boost::range::max_element(bankConfig->guards, [](const CStackBasicDescriptor & a, const CStackBasicDescriptor & b)
-				{
-					return a.type->getFightValue() < b.type->getFightValue();
-				})->type;
+				return a.type->getFightValue() < b.type->getFightValue();
+			})->type;
+
+			iw.text.replaceNamePlural(strongest->getId());
+			iw.text.replaceRawString(loot.buildList());
 
 
-				iw.text.replaceNamePlural(strongest->getId());
-				iw.text.replaceRawString(loot.buildList());
-			}
 			cb->showInfoDialog(&iw);
 			cb->showInfoDialog(&iw);
 		}
 		}
 
 
@@ -320,10 +208,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			std::set<SpellID> spells;
 			std::set<SpellID> spells;
 
 
 			bool noWisdom = false;
 			bool noWisdom = false;
-			if(textID == 106)
-			{
-				iw.text.appendLocalString(EMetaText::ADVOB_TXT, textID); //pyramid
-			}
+
 			for(const SpellID & spellId : bankConfig->spells)
 			for(const SpellID & spellId : bankConfig->spells)
 			{
 			{
 				const auto * spell = spellId.toEntity(VLC);
 				const auto * spell = spellId.toEntity(VLC);
@@ -398,7 +283,7 @@ void CBank::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) c
 	if (answer)
 	if (answer)
 	{
 	{
 		if (bankConfig) // not looted bank
 		if (bankConfig) // not looted bank
-			cb->startBattleI(hero, this, !regularUnitPlacement);
+			cb->startBattle(hero, this);
 		else
 		else
 			doVisit(hero);
 			doVisit(hero);
 	}
 	}

+ 1 - 1
lib/mapObjects/CGCreature.cpp

@@ -465,7 +465,7 @@ void CGCreature::fight( const CGHeroInstance *h ) const
 		}
 		}
 	}
 	}
 
 
-	cb->startBattleI(h, this);
+	cb->startBattle(h, this);
 
 
 }
 }
 
 

+ 1 - 1
lib/mapObjects/CGDwelling.cpp

@@ -505,7 +505,7 @@ void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answ
 	if(stacksCount() > 0  && relations == PlayerRelations::ENEMIES) //guards present
 	if(stacksCount() > 0  && relations == PlayerRelations::ENEMIES) //guards present
 	{
 	{
 		if(answer)
 		if(answer)
-			cb->startBattleI(hero, this);
+			cb->startBattle(hero, this);
 	}
 	}
 	else if(answer)
 	else if(answer)
 	{
 	{

+ 1 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -512,7 +512,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const
 			if(visitedTown) //we're in town
 			if(visitedTown) //we're in town
 				visitedTown->onHeroVisit(h); //town will handle attacking
 				visitedTown->onHeroVisit(h); //town will handle attacking
 			else
 			else
-				cb->startBattleI(h,	this);
+				cb->startBattle(h,	this);
 		}
 		}
 	}
 	}
 	else if(ID == Obj::PRISON)
 	else if(ID == Obj::PRISON)

+ 2 - 2
lib/mapObjects/CGPandoraBox.cpp

@@ -193,7 +193,7 @@ void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, int32_t an
 		if(stacksCount() > 0) //if pandora's box is protected by army
 		if(stacksCount() > 0) //if pandora's box is protected by army
 		{
 		{
 			hero->showInfoDialog(16, 0, EInfoWindowMode::MODAL);
 			hero->showInfoDialog(16, 0, EInfoWindowMode::MODAL);
-			cb->startBattleI(hero, this); //grants things after battle
+			cb->startBattle(hero, this); //grants things after battle
 		}
 		}
 		else if(getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT).empty())
 		else if(getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT).empty())
 		{
 		{
@@ -332,7 +332,7 @@ void CGEvent::activated( const CGHeroInstance * h ) const
 		else
 		else
 			iw.text.appendLocalString(EMetaText::ADVOB_TXT, 16);
 			iw.text.appendLocalString(EMetaText::ADVOB_TXT, 16);
 		cb->showInfoDialog(&iw);
 		cb->showInfoDialog(&iw);
-		cb->startBattleI(h, this);
+		cb->startBattle(h, this);
 	}
 	}
 	else
 	else
 	{
 	{

+ 2 - 1
lib/mapObjects/CGTownInstance.cpp

@@ -15,6 +15,7 @@
 #include "../spells/CSpellHandler.h"
 #include "../spells/CSpellHandler.h"
 #include "../bonuses/Bonus.h"
 #include "../bonuses/Bonus.h"
 #include "../battle/IBattleInfoCallback.h"
 #include "../battle/IBattleInfoCallback.h"
+#include "../battle/BattleLayout.h"
 #include "../CConfigHandler.h"
 #include "../CConfigHandler.h"
 #include "../texts/CGeneralTextHandler.h"
 #include "../texts/CGeneralTextHandler.h"
 #include "../IGameCallback.h"
 #include "../IGameCallback.h"
@@ -321,7 +322,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const
 
 
 				const_cast<CGHeroInstance *>(defendingHero)->inTownGarrison = false; //hack to return visitor from garrison after battle
 				const_cast<CGHeroInstance *>(defendingHero)->inTownGarrison = false; //hack to return visitor from garrison after battle
 			}
 			}
-			cb->startBattlePrimary(h, defendingArmy, getSightCenter(), h, defendingHero, false, (isBattleOutside ? nullptr : this));
+			cb->startBattle(h, defendingArmy, getSightCenter(), h, defendingHero, BattleLayout::createDefaultLayout(cb, h, defendingArmy), (isBattleOutside ? nullptr : this));
 		}
 		}
 		else
 		else
 		{
 		{

+ 135 - 44
lib/mapObjects/CRewardableObject.cpp

@@ -10,16 +10,20 @@
 
 
 #include "StdInc.h"
 #include "StdInc.h"
 #include "CRewardableObject.h"
 #include "CRewardableObject.h"
-#include "../gameState/CGameState.h"
-#include "../texts/CGeneralTextHandler.h"
+
 #include "../CPlayerState.h"
 #include "../CPlayerState.h"
+#include "../GameSettings.h"
 #include "../IGameCallback.h"
 #include "../IGameCallback.h"
+#include "../battle/BattleLayout.h"
+#include "../gameState/CGameState.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../mapObjectConstructors/CRewardableConstructor.h"
 #include "../mapObjectConstructors/CRewardableConstructor.h"
 #include "../mapObjects/CGHeroInstance.h"
 #include "../mapObjects/CGHeroInstance.h"
 #include "../networkPacks/PacksForClient.h"
 #include "../networkPacks/PacksForClient.h"
+#include "../networkPacks/PacksForClientBattle.h"
 #include "../serializer/JsonSerializeFormat.h"
 #include "../serializer/JsonSerializeFormat.h"
+#include "../texts/CGeneralTextHandler.h"
 
 
 #include <vstd/RNG.h>
 #include <vstd/RNG.h>
 
 
@@ -91,7 +95,48 @@ std::vector<Component> CRewardableObject::loadComponents(const CGHeroInstance *
 	return result;
 	return result;
 }
 }
 
 
-void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
+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 *hero) const
+{
+	if(!wasScouted(hero->getOwner()))
+	{
+		ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_SCOUTED, id, hero->id);
+		cb->sendAndApply(&cov);
+	}
+
+	if (guardedPresently())
+	{
+		auto guardedIndexes = getAvailableRewards(hero, Rewardable::EEventType::EVENT_GUARDED);
+		auto guardedReward = configuration.info.at(guardedIndexes.at(0));
+
+		// ask player to confirm attack
+		BlockingDialog bd(true, false);
+		bd.player = hero->getOwner();
+		bd.text = guardedReward.message;
+		bd.components = getPopupComponents(hero->getOwner());
+
+		cb->showBlockingDialog(this, &bd);
+	}
+	else
+	{
+		doHeroVisit(hero);
+	}
+}
+
+void CRewardableObject::doHeroVisit(const CGHeroInstance *h) const
 {
 {
 	if(!wasVisitedBefore(h))
 	if(!wasVisitedBefore(h))
 	{
 	{
@@ -154,7 +199,7 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
 
 
 		if(!objectRemovalPossible && getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty())
 		if(!objectRemovalPossible && getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty())
 		{
 		{
-			ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id);
+			ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id);
 			cb->sendAndApply(&cov);
 			cb->sendAndApply(&cov);
 		}
 		}
 	}
 	}
@@ -164,7 +209,7 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
 
 
 		if (!wasVisited(h->getOwner()))
 		if (!wasVisited(h->getOwner()))
 		{
 		{
-			ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id);
+			ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_PLAYER, id, h->id);
 			cb->sendAndApply(&cov);
 			cb->sendAndApply(&cov);
 		}
 		}
 
 
@@ -181,39 +226,36 @@ void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const
 	grantRewardAfterLevelup(cb, configuration.info.at(selectedReward), this, hero);
 	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(answer == 0)
+	if (result.winner == BattleSide::ATTACKER)
 	{
 	{
-		switch (configuration.visitMode)
-		{
-			case Rewardable::VISIT_UNLIMITED:
-			case Rewardable::VISIT_BONUS:
-			case Rewardable::VISIT_HERO:
-			case Rewardable::VISIT_LIMITER:
-			{
-				// 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
+		doHeroVisit(hero);
 	}
 	}
+}
 
 
-	if(answer > 0 && answer-1 < configuration.info.size())
+void CRewardableObject::blockingDialogAnswered(const CGHeroInstance * hero, int32_t answer) const
+{
+	if(guardedPresently())
 	{
 	{
-		auto list = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT);
-		markAsVisited(hero);
-		grantReward(list[answer - 1], hero);
+		if (answer)
+		{
+			auto layout = BattleLayout::createLayout(cb, configuration.guardsLayout, hero, this);
+			cb->startBattle(hero, this, visitablePos(), hero, nullptr, layout, nullptr);
+		}
 	}
 	}
 	else
 	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");
+		}
 	}
 	}
 }
 }
 
 
@@ -221,7 +263,7 @@ void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const
 {
 {
 	cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, true);
 	cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, true);
 
 
-	ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id);
+	ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_HERO, id, hero->id);
 	cb->sendAndApply(&cov);
 	cb->sendAndApply(&cov);
 }
 }
 
 
@@ -277,7 +319,7 @@ bool CRewardableObject::wasVisited(PlayerColor player) const
 
 
 bool CRewardableObject::wasScouted(PlayerColor player) const
 bool CRewardableObject::wasScouted(PlayerColor player) const
 {
 {
-	return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id));
+	return vstd::contains(cb->getPlayerTeam(player)->scoutedObjects, ObjectInstanceID(id));
 }
 }
 
 
 bool CRewardableObject::wasVisited(const CGHeroInstance * h) const
 bool CRewardableObject::wasVisited(const CGHeroInstance * h) const
@@ -365,22 +407,44 @@ std::vector<Component> CRewardableObject::getPopupComponentsImpl(PlayerColor pla
 	if (!wasScouted(player))
 	if (!wasScouted(player))
 		return {};
 		return {};
 
 
-	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 (!cb->getSettings().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
+	{
+		if (!configuration.showScoutedPreview)
+			return {};
 
 
-	if (rewardIndices.empty())
-		return {};
+		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 {};
 
 
-	return loadComponents(hero, rewardIndices);
+		return loadComponents(hero, rewardIndices);
+	}
 }
 }
 
 
 std::vector<Component> CRewardableObject::getPopupComponents(PlayerColor player) const
 std::vector<Component> CRewardableObject::getPopupComponents(PlayerColor player) const
@@ -440,4 +504,31 @@ void CRewardableObject::serializeJsonOptions(JsonSerializeFormat & handler)
 	handler.serializeStruct("rewardable", static_cast<Rewardable::Interface&>(*this));
 	handler.serializeStruct("rewardable", static_cast<Rewardable::Interface&>(*this));
 }
 }
 
 
+void CRewardableObject::initializeGuards()
+{
+	clearSlots();
+
+	// Workaround for default creature banks strings that has placeholder for object name
+	// TODO: find better location for this code
+	for (auto & visitInfo : configuration.info)
+		visitInfo.message.replaceRawString(getObjectName());
+
+	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()));
+		}
+	}
+}
+
+bool CRewardableObject::isCoastVisitable() const
+{
+	return configuration.coastVisitable;
+}
+
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END

+ 13 - 7
lib/mapObjects/CRewardableObject.h

@@ -45,7 +45,14 @@ protected:
 	std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const;
 	std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const;
 	std::vector<Component> getPopupComponentsImpl(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:
 public:
+
 	/// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player)
 	/// 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(PlayerColor player) const override;
 	bool wasVisited(const CGHeroInstance * h) 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
 	/// gives reward to player or ask for choice in case of multiple rewards
 	void onHeroVisit(const CGHeroInstance *h) const override;
 	void onHeroVisit(const CGHeroInstance *h) const override;
 
 
+	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
+
 	///possibly resets object state
 	///possibly resets object state
 	void newTurn(vstd::RNG & rand) const override;
 	void newTurn(vstd::RNG & rand) const override;
 
 
@@ -66,6 +75,10 @@ public:
 	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 	void blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const override;
 
 
 	void initObj(vstd::RNG & rand) override;
 	void initObj(vstd::RNG & rand) override;
+
+	bool isCoastVisitable() const override;
+
+	void initializeGuards();
 	
 	
 	void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
 	void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
 
 
@@ -89,13 +102,6 @@ public:
 };
 };
 
 
 //TODO:
 //TODO:
-
-// MAX
-// class DLL_LINKAGE CBank : public CArmedInstance
-// class DLL_LINKAGE CGPyramid : public CBank
-
-// EXTRA
-// class DLL_LINKAGE CTownBonus : public CGTownBuilding
 // class DLL_LINKAGE CGKeys : public CGObjectInstance //Base class for Keymaster and guards
 // class DLL_LINKAGE CGKeys : public CGObjectInstance //Base class for Keymaster and guards
 // class DLL_LINKAGE CGKeymasterTent : public CGKeys
 // class DLL_LINKAGE CGKeymasterTent : public CGKeys
 // class DLL_LINKAGE CGBorderGuard : public CGKeys, public IQuestObject
 // class DLL_LINKAGE CGBorderGuard : public CGKeys, public IQuestObject

+ 0 - 0
lib/mapObjects/CreatureBank.cpp


+ 0 - 0
lib/mapObjects/CreatureBank.h


+ 0 - 1
lib/mapObjects/MapObjects.h

@@ -14,7 +14,6 @@
 #include "CObjectHandler.h"
 #include "CObjectHandler.h"
 
 
 #include "CArmedInstance.h"
 #include "CArmedInstance.h"
-#include "CBank.h"
 #include "CGDwelling.h"
 #include "CGDwelling.h"
 #include "CGHeroInstance.h"
 #include "CGHeroInstance.h"
 #include "CGMarket.h"
 #include "CGMarket.h"

+ 4 - 4
lib/mapObjects/MiscObjects.cpp

@@ -219,7 +219,7 @@ void CGMine::battleFinished(const CGHeroInstance *hero, const BattleResult &resu
 void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 {
 	if(answer)
 	if(answer)
-		cb->startBattleI(hero, this);
+		cb->startBattle(hero, this);
 }
 }
 
 
 void CGMine::serializeJsonOptions(JsonSerializeFormat & handler)
 void CGMine::serializeJsonOptions(JsonSerializeFormat & handler)
@@ -352,7 +352,7 @@ void CGResource::battleFinished(const CGHeroInstance *hero, const BattleResult &
 void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 {
 	if(answer)
 	if(answer)
-		cb->startBattleI(hero, this);
+		cb->startBattle(hero, this);
 }
 }
 
 
 void CGResource::serializeJsonOptions(JsonSerializeFormat & handler)
 void CGResource::serializeJsonOptions(JsonSerializeFormat & handler)
@@ -919,7 +919,7 @@ void CGArtifact::battleFinished(const CGHeroInstance *hero, const BattleResult &
 void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 void CGArtifact::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
 {
 	if(answer)
 	if(answer)
-		cb->startBattleI(hero, this);
+		cb->startBattle(hero, this);
 }
 }
 
 
 void CGArtifact::afterAddToMap(CMap * map)
 void CGArtifact::afterAddToMap(CMap * map)
@@ -999,7 +999,7 @@ void CGGarrison::onHeroVisit (const CGHeroInstance *h) const
 	auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner);
 	auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner);
 	if (relations == PlayerRelations::ENEMIES && stacksCount() > 0) {
 	if (relations == PlayerRelations::ENEMIES && stacksCount() > 0) {
 		//TODO: Find a way to apply magic garrison effects in battle.
 		//TODO: Find a way to apply magic garrison effects in battle.
-		cb->startBattleI(h, this);
+		cb->startBattle(h, this);
 		return;
 		return;
 	}
 	}
 
 

+ 1 - 1
lib/mapping/MapFormatH3M.cpp

@@ -1459,7 +1459,7 @@ CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::sha
 CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
 CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
 {
 {
 	if(objectTemplate->subid == 0)
 	if(objectTemplate->subid == 0)
-		return new CBank(map->cb);
+		return readGeneric(mapPosition, objectTemplate);
 
 
 	return new CGObjectInstance(map->cb);
 	return new CGObjectInstance(map->cb);
 }
 }

+ 17 - 19
lib/networkPacks/NetPacksLib.cpp

@@ -1035,32 +1035,32 @@ void ChangeObjPos::applyGs(CGameState *gs)
 void ChangeObjectVisitors::applyGs(CGameState *gs)
 void ChangeObjectVisitors::applyGs(CGameState *gs)
 {
 {
 	switch (mode) {
 	switch (mode) {
-		case VISITOR_ADD:
+		case VISITOR_ADD_HERO:
+			gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->scoutedObjects.insert(object);
 			gs->getHero(hero)->visitedObjects.insert(object);
 			gs->getHero(hero)->visitedObjects.insert(object);
 			gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjects.insert(object);
 			gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjects.insert(object);
 			break;
 			break;
-		case VISITOR_ADD_TEAM:
-			{
-				TeamState *ts = gs->getPlayerTeam(gs->getHero(hero)->tempOwner);
-				for(const auto & color : ts->players)
-				{
-					gs->getPlayerState(color)->visitedObjects.insert(object);
-				}
-			}
+		case VISITOR_ADD_PLAYER:
+			gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->scoutedObjects.insert(object);
+			for(const auto & color : gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->players)
+				gs->getPlayerState(color)->visitedObjects.insert(object);
+
 			break;
 			break;
 		case VISITOR_CLEAR:
 		case VISITOR_CLEAR:
+			// remove visit info from all heroes, including those that are not present on map
 			for (CGHeroInstance * hero : gs->map->allHeroes)
 			for (CGHeroInstance * hero : gs->map->allHeroes)
-			{
 				if (hero)
 				if (hero)
-				{
-					hero->visitedObjects.erase(object); // remove visit info from all heroes, including those that are not present on map
-				}
-			}
+					hero->visitedObjects.erase(object);
 
 
 			for(auto &elem : gs->players)
 			for(auto &elem : gs->players)
-			{
 				elem.second.visitedObjects.erase(object);
 				elem.second.visitedObjects.erase(object);
-			}
+
+			for(auto &elem : gs->teams)
+				elem.second.scoutedObjects.erase(object);
+
+			break;
+		case VISITOR_SCOUTED:
+			gs->getPlayerTeam(gs->getHero(hero)->tempOwner)->scoutedObjects.insert(object);
 
 
 			break;
 			break;
 		case VISITOR_GLOBAL:
 		case VISITOR_GLOBAL:
@@ -1069,9 +1069,6 @@ void ChangeObjectVisitors::applyGs(CGameState *gs)
 				gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjectsGlobal.insert({objectPtr->ID, objectPtr->subID});
 				gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjectsGlobal.insert({objectPtr->ID, objectPtr->subID});
 				break;
 				break;
 			}
 			}
-		case VISITOR_REMOVE:
-			gs->getHero(hero)->visitedObjects.erase(object);
-			break;
 	}
 	}
 }
 }
 
 
@@ -2434,6 +2431,7 @@ void SetRewardableConfiguration::applyGs(CGameState *gs)
 		auto * rewardablePtr = dynamic_cast<CRewardableObject *>(objectPtr);
 		auto * rewardablePtr = dynamic_cast<CRewardableObject *>(objectPtr);
 		assert(rewardablePtr);
 		assert(rewardablePtr);
 		rewardablePtr->configuration = configuration;
 		rewardablePtr->configuration = configuration;
+		rewardablePtr->initializeGuards();
 	}
 	}
 	else
 	else
 	{
 	{

+ 5 - 5
lib/networkPacks/PacksForClient.h

@@ -1216,11 +1216,11 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient
 {
 {
 	enum VisitMode
 	enum VisitMode
 	{
 	{
-		VISITOR_ADD,      // mark hero as one that have visited this object
-		VISITOR_ADD_TEAM, // mark team as one that have visited this object
-		VISITOR_GLOBAL,   // mark player as one that have visited object of this type
-		VISITOR_REMOVE,   // unmark visitor, reversed to ADD
-		VISITOR_CLEAR     // clear all visitors from this object (object reset)
+		VISITOR_ADD_HERO,   // mark hero as one that have visited this object
+		VISITOR_ADD_PLAYER, // mark player as one that have visited this object instance
+		VISITOR_GLOBAL,     // mark player as one that have visited object of this type
+		VISITOR_SCOUTED,    // marks targeted team as having scouted this object
+		VISITOR_CLEAR,      // clear all visitors from this object (object reset)
 	};
 	};
 	VisitMode mode = VISITOR_CLEAR; // uses VisitMode enum
 	VisitMode mode = VISITOR_CLEAR; // uses VisitMode enum
 	ObjectInstanceID object;
 	ObjectInstanceID object;

+ 1 - 0
lib/rewardable/Configuration.cpp

@@ -103,6 +103,7 @@ void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler)
 	handler.serializeStruct("resetParameters", resetParameters);
 	handler.serializeStruct("resetParameters", resetParameters);
 	handler.serializeBool("canRefuse", canRefuse);
 	handler.serializeBool("canRefuse", canRefuse);
 	handler.serializeBool("showScoutedPreview", showScoutedPreview);
 	handler.serializeBool("showScoutedPreview", showScoutedPreview);
+	handler.serializeBool("coastVisitable", coastVisitable);
 	handler.serializeInt("infoWindowType", infoWindowType);
 	handler.serializeInt("infoWindowType", infoWindowType);
 }
 }
 
 

+ 13 - 1
lib/rewardable/Configuration.h

@@ -44,7 +44,8 @@ enum class EEventType
 	EVENT_INVALID = 0,
 	EVENT_INVALID = 0,
 	EVENT_FIRST_VISIT,
 	EVENT_FIRST_VISIT,
 	EVENT_ALREADY_VISITED,
 	EVENT_ALREADY_VISITED,
-	EVENT_NOT_AVAILABLE
+	EVENT_NOT_AVAILABLE,
+	EVENT_GUARDED
 };
 };
 
 
 constexpr std::array<std::string_view, 4> SelectModeString{"selectFirst", "selectPlayer", "selectRandom", "selectAll"};
 constexpr std::array<std::string_view, 4> SelectModeString{"selectFirst", "selectPlayer", "selectRandom", "selectAll"};
@@ -155,12 +156,16 @@ struct DLL_LINKAGE Configuration
 	/// Limiter that will be used to determine that object is visited. Only if visit mode is set to "limiter"
 	/// Limiter that will be used to determine that object is visited. Only if visit mode is set to "limiter"
 	Rewardable::Limiter visitLimiter;
 	Rewardable::Limiter visitLimiter;
 
 
+	std::string guardsLayout;
+
 	/// if true - player can refuse visiting an object (e.g. Tomb)
 	/// if true - player can refuse visiting an object (e.g. Tomb)
 	bool canRefuse = false;
 	bool canRefuse = false;
 
 
 	/// if true - right-clicking object will show preview of object rewards
 	/// if true - right-clicking object will show preview of object rewards
 	bool showScoutedPreview = false;
 	bool showScoutedPreview = false;
 
 
+	bool coastVisitable = false;
+
 	/// if true - object info will shown in infobox (like resource pickup)
 	/// if true - object info will shown in infobox (like resource pickup)
 	EInfoWindowMode infoWindowType = EInfoWindowMode::AUTO;
 	EInfoWindowMode infoWindowType = EInfoWindowMode::AUTO;
 	
 	
@@ -189,6 +194,13 @@ struct DLL_LINKAGE Configuration
 		h & canRefuse;
 		h & canRefuse;
 		h & showScoutedPreview;
 		h & showScoutedPreview;
 		h & infoWindowType;
 		h & infoWindowType;
+		if (h.version >= Handler::Version::REWARDABLE_BANKS)
+		{
+			h & coastVisitable;
+			h & guardsLayout;
+		}
+		else
+			coastVisitable = false;
 	}
 	}
 };
 };
 
 

+ 69 - 7
lib/rewardable/Info.cpp

@@ -174,6 +174,8 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, vstd:
 	reward.removeObject = source["removeObject"].Bool();
 	reward.removeObject = source["removeObject"].Bool();
 	reward.bonuses = randomizer.loadBonuses(source["bonuses"]);
 	reward.bonuses = randomizer.loadBonuses(source["bonuses"]);
 
 
+	reward.guards = randomizer.loadCreatures(source["guards"], rng, variables);
+
 	reward.primary = randomizer.loadPrimaries(source["primary"], rng, variables);
 	reward.primary = randomizer.loadPrimaries(source["primary"], rng, variables);
 	reward.secondary = randomizer.loadSecondaries(source["secondary"], rng, variables);
 	reward.secondary = randomizer.loadSecondaries(source["secondary"], rng, variables);
 
 
@@ -264,16 +266,64 @@ void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variab
 
 
 void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const
 void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const
 {
 {
-	for (const auto & artifact : info.reward.artifacts )
-		target.replaceName(artifact);
+	if (!info.reward.guards.empty())
+	{
+		replaceTextPlaceholders(target, variables);
+
+		CreatureID strongest = info.reward.guards.at(0).getId();
+
+		for (const auto & guard : info.reward.guards )
+		{
+			if (strongest.toEntity(VLC)->getFightValue() < guard.getId().toEntity(VLC)->getFightValue())
+				strongest = guard.getId();
+		}
+		target.replaceNamePlural(strongest); // FIXME: use singular if only 1 such unit is in guards
+
+		MetaString loot;
+
+		for (GameResID it : GameResID::ALL_RESOURCES())
+		{
+			if (info.reward.resources[it] != 0)
+			{
+				loot.appendRawString("%d %s");
+				loot.replaceNumber(info.reward.resources[it]);
+				loot.replaceName(it);
+			}
+		}
+
+		for (const auto & artifact : info.reward.artifacts )
+		{
+			loot.appendRawString("%s");
+			loot.replaceName(artifact);
+		}
 
 
-	for (const auto & spell : info.reward.spells )
-		target.replaceName(spell);
+		for (const auto & spell : info.reward.spells )
+		{
+			loot.appendRawString("%s");
+			loot.replaceName(spell);
+		}
+
+		for (const auto & secondary : info.reward.secondary )
+		{
+			loot.appendRawString("%s");
+			loot.replaceName(secondary.first);
+		}
 
 
-	for (const auto & secondary : info.reward.secondary )
-		target.replaceName(secondary.first);
+		target.replaceRawString(loot.buildList());
+	}
+	else
+	{
+		for (const auto & artifact : info.reward.artifacts )
+			target.replaceName(artifact);
 
 
-	replaceTextPlaceholders(target, variables);
+		for (const auto & spell : info.reward.spells )
+			target.replaceName(spell);
+
+		for (const auto & secondary : info.reward.secondary )
+			target.replaceName(secondary.first);
+
+		replaceTextPlaceholders(target, variables);
+	}
 }
 }
 
 
 void Rewardable::Info::configureRewards(
 void Rewardable::Info::configureRewards(
@@ -378,10 +428,22 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, vstd:
 		object.info.push_back(onEmpty);
 		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"]);
 	configureResetInfo(object, rng, object.resetParameters, parameters["resetParameters"]);
 
 
 	object.canRefuse = parameters["canRefuse"].Bool();
 	object.canRefuse = parameters["canRefuse"].Bool();
 	object.showScoutedPreview = parameters["showScoutedPreview"].Bool();
 	object.showScoutedPreview = parameters["showScoutedPreview"].Bool();
+	object.guardsLayout = parameters["guardsLayout"].String();
+	object.coastVisitable = parameters["coastVisitable"].Bool();
 
 
 	if(parameters["showInInfobox"].isNull())
 	if(parameters["showInInfobox"].isNull())
 		object.infoWindowType = EInfoWindowMode::AUTO;
 		object.infoWindowType = EInfoWindowMode::AUTO;

+ 3 - 0
lib/rewardable/Reward.h

@@ -82,6 +82,9 @@ struct DLL_LINKAGE Reward final
 	/// fixed value, in form of percentage from max
 	/// fixed value, in form of percentage from max
 	si32 movePercentage;
 	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
 	/// list of bonuses, e.g. morale/luck
 	std::vector<Bonus> bonuses;
 	std::vector<Bonus> bonuses;
 
 

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -59,6 +59,7 @@ enum class ESerializationVersion : int32_t
 	CHRONICLES_SUPPORT, // 860 - support for heroes chronicles
 	CHRONICLES_SUPPORT, // 860 - support for heroes chronicles
 	PER_MAP_GAME_SETTINGS, // 861 - game settings are now stored per-map
 	PER_MAP_GAME_SETTINGS, // 861 - game settings are now stored per-map
 	CAMPAIGN_OUTRO_SUPPORT, // 862 - support for campaign outro video
 	CAMPAIGN_OUTRO_SUPPORT, // 862 - support for campaign outro video
+	REWARDABLE_BANKS, // 863 - team state contains list of scouted objects, coast visitable rewardable objects
 
 
-	CURRENT = CAMPAIGN_OUTRO_SUPPORT
+	CURRENT = REWARDABLE_BANKS
 };
 };

+ 4 - 9
server/CGameHandler.cpp

@@ -4114,17 +4114,12 @@ void CGameHandler::newObject(CGObjectInstance * object, PlayerColor initiator)
 	sendAndApply(&no);
 	sendAndApply(&no);
 }
 }
 
 
-void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town)
+void CGameHandler::startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town)
 {
 {
-	battles->startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town);
+	battles->startBattle(army1, army2, tile, hero1, hero2, layout, town);
 }
 }
 
 
-void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank )
+void CGameHandler::startBattle(const CArmedInstance *army1, const CArmedInstance *army2 )
 {
 {
-	battles->startBattleI(army1, army2, tile, creatureBank);
-}
-
-void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank )
-{
-	battles->startBattleI(army1, army2, creatureBank);
+	battles->startBattle(army1, army2);
 }
 }

+ 2 - 3
server/CGameHandler.h

@@ -149,9 +149,8 @@ public:
 
 
 	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override;
 	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override;
 	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override;
 	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override;
-	void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override; //use hero=nullptr for no hero
-	void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false) override; //if any of armies is hero, hero will be used
-	void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false) override; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
+	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) override; //use hero=nullptr for no hero
+	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2) override; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
 	bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override;
 	bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override;
 	void giveHeroBonus(GiveBonus * bonus) override;
 	void giveHeroBonus(GiveBonus * bonus) override;
 	void setMovePoints(SetMovePoints * smp) override;
 	void setMovePoints(SetMovePoints * smp) override;

+ 15 - 20
server/battles/BattleProcessor.cpp

@@ -23,6 +23,7 @@
 #include "../../lib/battle/CBattleInfoCallback.h"
 #include "../../lib/battle/CBattleInfoCallback.h"
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/battle/BattleInfo.h"
 #include "../../lib/battle/BattleInfo.h"
+#include "../../lib/battle/BattleLayout.h"
 #include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/entities/building/TownFortifications.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapping/CMap.h"
 #include "../../lib/mapping/CMap.h"
@@ -52,9 +53,8 @@ void BattleProcessor::engageIntoBattle(PlayerColor player)
 	gameHandler->sendAndApply(&pb);
 	gameHandler->sendAndApply(&pb);
 }
 }
 
 
-void BattleProcessor::restartBattlePrimary(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile,
-								const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank,
-								const CGTownInstance *town)
+void BattleProcessor::restartBattle(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile,
+								const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town)
 {
 {
 	auto battle = gameHandler->gameState()->getBattle(battleID);
 	auto battle = gameHandler->gameState()->getBattle(battleID);
 
 
@@ -90,12 +90,11 @@ void BattleProcessor::restartBattlePrimary(const BattleID & battleID, const CArm
 	bc.battleID = battleID;
 	bc.battleID = battleID;
 	gameHandler->sendAndApply(&bc);
 	gameHandler->sendAndApply(&bc);
 
 
-	startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town);
+	startBattle(army1, army2, tile, hero1, hero2, layout, town);
 }
 }
 
 
-void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile,
-								const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank,
-								const CGTownInstance *town)
+void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile,
+								const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town)
 {
 {
 	assert(gameHandler->gameState()->getBattle(army1->getOwner()) == nullptr);
 	assert(gameHandler->gameState()->getBattle(army1->getOwner()) == nullptr);
 	assert(gameHandler->gameState()->getBattle(army2->getOwner()) == nullptr);
 	assert(gameHandler->gameState()->getBattle(army2->getOwner()) == nullptr);
@@ -103,7 +102,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm
 	BattleSideArray<const CArmedInstance *> armies{army1, army2};
 	BattleSideArray<const CArmedInstance *> armies{army1, army2};
 	BattleSideArray<const CGHeroInstance*>heroes{hero1, hero2};
 	BattleSideArray<const CGHeroInstance*>heroes{hero1, hero2};
 
 
-	auto battleID = setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces
+	auto battleID = setupBattle(tile, armies, heroes, layout, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces
 
 
 	const auto * battle = gameHandler->gameState()->getBattle(battleID);
 	const auto * battle = gameHandler->gameState()->getBattle(battleID);
 	assert(battle);
 	assert(battle);
@@ -144,20 +143,16 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm
 	flowProcessor->onBattleStarted(*battle);
 	flowProcessor->onBattleStarted(*battle);
 }
 }
 
 
-void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank)
+void BattleProcessor::startBattle(const CArmedInstance *army1, const CArmedInstance *army2)
 {
 {
-	startBattlePrimary(army1, army2, tile,
-		army1->ID == Obj::HERO ? static_cast<const CGHeroInstance*>(army1) : nullptr,
-		army2->ID == Obj::HERO ? static_cast<const CGHeroInstance*>(army2) : nullptr,
-		creatureBank);
+	startBattle(army1, army2, army2->visitablePos(),
+		army1->ID == Obj::HERO ? dynamic_cast<const CGHeroInstance*>(army1) : nullptr,
+		army2->ID == Obj::HERO ? dynamic_cast<const CGHeroInstance*>(army2) : nullptr,
+		BattleLayout::createDefaultLayout(gameHandler, army1, army2),
+		nullptr);
 }
 }
 
 
-void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank)
-{
-	startBattleI(army1, army2, army2->visitablePos(), creatureBank);
-}
-
-BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, bool creatureBank, const CGTownInstance *town)
+BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, const BattleLayout & layout, const CGTownInstance *town)
 {
 {
 	const auto & t = *gameHandler->getTile(tile);
 	const auto & t = *gameHandler->getTile(tile);
 	TerrainId terrain = t.terType->getId();
 	TerrainId terrain = t.terType->getId();
@@ -170,7 +165,7 @@ BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray<const CArmedIns
 
 
 	//send info about battles
 	//send info about battles
 	BattleStart bs;
 	BattleStart bs;
-	bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town);
+	bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, layout, town);
 	bs.battleID = gameHandler->gameState()->nextBattleID;
 	bs.battleID = gameHandler->gameState()->nextBattleID;
 
 
 	engageIntoBattle(bs.info->getSide(BattleSide::ATTACKER).color);
 	engageIntoBattle(bs.info->getSide(BattleSide::ATTACKER).color);

+ 5 - 6
server/battles/BattleProcessor.h

@@ -20,6 +20,7 @@ class BattleAction;
 class int3;
 class int3;
 class CBattleInfoCallback;
 class CBattleInfoCallback;
 struct BattleResult;
 struct BattleResult;
+struct BattleLayout;
 class BattleID;
 class BattleID;
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END
 
 
@@ -45,7 +46,7 @@ class BattleProcessor : boost::noncopyable
 	void engageIntoBattle(PlayerColor player);
 	void engageIntoBattle(PlayerColor player);
 
 
 	bool checkBattleStateChanges(const CBattleInfoCallback & battle);
 	bool checkBattleStateChanges(const CBattleInfoCallback & battle);
-	BattleID setupBattle(int3 tile, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, bool creatureBank, const CGTownInstance *town);
+	BattleID setupBattle(int3 tile, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, const BattleLayout & layout, const CGTownInstance *town);
 
 
 	bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba);
 	bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba);
 
 
@@ -56,13 +57,11 @@ public:
 	~BattleProcessor();
 	~BattleProcessor();
 
 
 	/// Starts battle with specified parameters
 	/// Starts battle with specified parameters
-	void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr);
-	/// Starts battle between two armies (which can also be heroes) at specified tile
-	void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false);
+	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town);
 	/// Starts battle between two armies (which can also be heroes) at position of 2nd object
 	/// Starts battle between two armies (which can also be heroes) at position of 2nd object
-	void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false);
+	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2);
 	/// Restart ongoing battle and end previous battle
 	/// Restart ongoing battle and end previous battle
-	void restartBattlePrimary(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr);
+	void restartBattle(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town);
 
 
 	/// Processing of incoming battle action netpack
 	/// Processing of incoming battle action netpack
 	bool makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction & ba);
 	bool makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction & ba);

+ 3 - 2
server/queries/BattleQueries.cpp

@@ -17,6 +17,7 @@
 
 
 #include "../../lib/battle/IBattleState.h"
 #include "../../lib/battle/IBattleState.h"
 #include "../../lib/battle/SideInBattle.h"
 #include "../../lib/battle/SideInBattle.h"
+#include "../../lib/battle/BattleLayout.h"
 #include "../../lib/CPlayerState.h"
 #include "../../lib/CPlayerState.h"
 #include "../../lib/mapObjects/CGObjectInstance.h"
 #include "../../lib/mapObjects/CGObjectInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
@@ -95,14 +96,14 @@ void CBattleDialogQuery::onRemoval(PlayerColor color)
 	assert(answer);
 	assert(answer);
 	if(*answer == 1)
 	if(*answer == 1)
 	{
 	{
-		gh->battles->restartBattlePrimary(
+		gh->battles->restartBattle(
 			bi->getBattleID(),
 			bi->getBattleID(),
 			bi->getSideArmy(BattleSide::ATTACKER),
 			bi->getSideArmy(BattleSide::ATTACKER),
 			bi->getSideArmy(BattleSide::DEFENDER),
 			bi->getSideArmy(BattleSide::DEFENDER),
 			bi->getLocation(),
 			bi->getLocation(),
 			bi->getSideHero(BattleSide::ATTACKER),
 			bi->getSideHero(BattleSide::ATTACKER),
 			bi->getSideHero(BattleSide::DEFENDER),
 			bi->getSideHero(BattleSide::DEFENDER),
-			bi->isCreatureBank(),
+			bi->getLayout(),
 			bi->getDefendedTown()
 			bi->getDefendedTown()
 		);
 		);
 	}
 	}

+ 3 - 1
test/game/CGameStateTest.cpp

@@ -24,6 +24,7 @@
 #include "../../lib/TerrainHandler.h"
 #include "../../lib/TerrainHandler.h"
 
 
 #include "../../lib/battle/BattleInfo.h"
 #include "../../lib/battle/BattleInfo.h"
+#include "../../lib/battle/BattleLayout.h"
 #include "../../lib/CStack.h"
 #include "../../lib/CStack.h"
 
 
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/filesystem/ResourcePath.h"
@@ -197,10 +198,11 @@ public:
 
 
 		auto terrain = t.terType->getId();
 		auto terrain = t.terType->getId();
 		BattleField terType(0);
 		BattleField terType(0);
+		BattleLayout layout = BattleLayout::createDefaultLayout(gameState->callback, attacker, defender);
 
 
 		//send info about battles
 		//send info about battles
 
 
-		BattleInfo * battle = BattleInfo::setupBattle(tile, terrain, terType, armedInstancies, heroes, false, nullptr);
+		BattleInfo * battle = BattleInfo::setupBattle(tile, terrain, terType, armedInstancies, heroes, layout, nullptr);
 
 
 		BattleStart bs;
 		BattleStart bs;
 		bs.info = battle;
 		bs.info = battle;

+ 2 - 3
test/mock/mock_IGameCallback.h

@@ -79,9 +79,8 @@ public:
 	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
 	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
 	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
 	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
 	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
 	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}
-	void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override {} //use hero=nullptr for no hero
-	void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false) override {} //if any of armies is hero, hero will be used
-	void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false) override {} //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
+	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const BattleLayout & layout, const CGTownInstance *town) override {} //use hero=nullptr for no hero
+	void startBattle(const CArmedInstance *army1, const CArmedInstance *army2) override {}
 	bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}
 	bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}
 	bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}
 	bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}
 	void giveHeroBonus(GiveBonus * bonus) override {}
 	void giveHeroBonus(GiveBonus * bonus) override {}

+ 2 - 1
test/mock/mock_battle_IBattleState.h

@@ -11,6 +11,7 @@
 #pragma once
 #pragma once
 
 
 #include "../../lib/battle/IBattleState.h"
 #include "../../lib/battle/IBattleState.h"
+#include "../../lib/battle/BattleLayout.h"
 #include "../../lib/int3.h"
 #include "../../lib/int3.h"
 
 
 class BattleStateMock : public IBattleState
 class BattleStateMock : public IBattleState
@@ -37,7 +38,7 @@ public:
 	MOCK_CONST_METHOD3(getActualDamage, int64_t(const DamageRange &, int32_t, vstd::RNG &));
 	MOCK_CONST_METHOD3(getActualDamage, int64_t(const DamageRange &, int32_t, vstd::RNG &));
 	MOCK_CONST_METHOD0(getBattleID, BattleID());
 	MOCK_CONST_METHOD0(getBattleID, BattleID());
 	MOCK_CONST_METHOD0(getLocation, int3());
 	MOCK_CONST_METHOD0(getLocation, int3());
-	MOCK_CONST_METHOD0(isCreatureBank, bool());
+	MOCK_CONST_METHOD0(getLayout, BattleLayout());
 	MOCK_CONST_METHOD1(getUsedSpells, std::vector<SpellID>(BattleSide));
 	MOCK_CONST_METHOD1(getUsedSpells, std::vector<SpellID>(BattleSide));
 
 
 	MOCK_METHOD0(nextRound, void());
 	MOCK_METHOD0(nextRound, void());

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